Un servidor web es un programa que escucha en un puerto, lee peticiones HTTP y escribe respuestas. Este tema lo desmonta por dentro en cuatro pasos: el canal que ofrece el sistema operativo (el socket), un servidor HTTP escrito a mano sobre ese canal, el servidor HTTP ya hecho que trae el JDK y la organización del código con servlets y su contenedor, que es como lo hacen los servidores reales.
Un socket es el punto de comunicación de un programa en la red: combina una dirección IP (la máquina) y un puerto (la aplicación que escucha en ella). Con sockets TCP dos programas intercambian un flujo de bytes ordenado y fiable.
ServerSocket en un puerto y se queda esperando en accept().Socket con los flujos de entrada y salida.new Socket(host, puerto).El ejemplo mínimo en Java repite ese esquema: un servidor que repite lo que recibe y el cliente que se conecta a él.
public class EchoServer { public static void main(String[] args) throws IOException { try (ServerSocket serverSocket = new ServerSocket(8080)) { while (true) { try (Socket socket = serverSocket.accept()) { BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); String line; while ((line = in.readLine()) != null) { out.println("ECHO: " + line); } } } } } }
public class EchoClient { public static void main(String[] args) throws IOException { try (Socket socket = new Socket("localhost", 8080); BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { out.println("hola"); System.out.println(in.readLine()); // ECHO: hola } } }
El servidor repite lo que recibe, línea a línea, hasta que el cliente cierra la conexión.
try-with-resources). Una conexión sin cerrar deja recursos ocupados en ambos extremos.
Un socket solo transmite bytes: ¿dónde termina un mensaje? ¿qué significa cada línea y en qué orden llega? El protocolo es el acuerdo que responde a esas preguntas. Un servidor HTTP es, básicamente, un socket que funciona con uno de estos protocolos: HTTP define petición y respuesta con exactitud (su formato se detalla en el tema 03) y, como el formato es texto, puede escribirse a mano con los sockets de la sección anterior:
public class SocketHttpServer { public static void main(String[] args) throws IOException { try (ServerSocket serverSocket = new ServerSocket(8080)) { while (true) { try (Socket socket = serverSocket.accept()) { handle(socket); } } } } private static void handle(Socket socket) throws IOException { BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream()); String requestLine = in.readLine(); // GET /books/42 HTTP/1.1 String route = requestLine.split(" ")[1]; readHeaders(in); // hasta la línea vacía String body = "{\"message\":\"Hello from " + route + "\"}"; out.print("HTTP/1.1 200 OK\r\n"); out.print("Content-Type: application/json\r\n"); out.print("Content-Length: " + body.getBytes().length + "\r\n"); out.print("\r\n"); out.print(body); out.flush(); } private static void readHeaders(BufferedReader in) throws IOException { String line; while ((line = in.readLine()) != null && !line.isEmpty()) { // procesar cada cabecera si hace falta } } }
Con un navegador o curl contra http://localhost:8080/books/42 se obtiene la respuesta JSON.
Escribir el protocolo a mano deja claro el porqué, pero es un trabajo que no se repite a diario: el JDK ya trae un servidor HTTP completo, la clase HttpServer del paquete com.sun.net.httpserver. El servidor procesa el protocolo —línea y cabeceras de la petición, Content-Length, *keep-alive*— y entrega a cada petición su exchange: un objeto con la petición y donde se escribe la respuesta:
public class BookServer { public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/books", exchange -> { String id = exchange.getRequestURI().getQuery(); // id=42 byte[] body = ("{\"id\":\"" + id + "\"}").getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, body.length); try (OutputStream out = exchange.getResponseBody()) { out.write(body); } }); server.start(); // hilo propio del servidor; el main puede terminar } }
HttpServer.create: deja el servidor escuchando en el puerto.createContext: asigna a cada ruta su handler, la función que atiende sus peticiones. Es el mismo papel que el doGet de un servlet (sección 5).HttpExchange reúne la petición y la respuesta: método y ruta (getRequestMethod, getRequestURI), cabeceras, cuerpo…setExecutor se le entrega un pool de hilos (tema 05).jwebserver.
Ambos servidores —el escrito a mano y HttpServer sin configurar— atienden una conexión a la vez: mientras están con un cliente, los demás esperan.
Cliente A ── conecta ──> [ atendido ] Cliente B ── conecta ──> [ esperando ]...
Y queda más por hacer: elegir rutas y parámetros con todas las reglas del protocolo, servir archivos estáticos, errores estándar…
| Necesidad | Solución que llega después |
|---|---|
| Atender varias peticiones a la vez | Hilos (tema 05) |
| Rutas, parámetros y cuerpos con todas las reglas | Servlets (siguiente sección) |
| Ciclo de vida, archivos estáticos, errores estándar | El contenedor (Tomcat, tema 06) |
Un servlet es un componente Java que procesa peticiones HTTP dentro de un contenedor. El desarrollador escribe el método que responde; el contenedor se ocupa de sockets, hilos, parseo y envío.
@WebServlet("/books") public class BookServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String id = request.getParameter("id"); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().print("{\"id\":" + id + "}"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { // procesar el cuerpo y crear el recurso response.setStatus(HttpServletResponse.SC_CREATED); } }
| Elemento | Función |
|---|---|
HttpServlet | Clase base con el ciclo de vida gestionado por el contenedor |
doGet / doPost | Responden a los métodos HTTP correspondientes |
HttpServletRequest | Petición: parámetros, cabeceras, cuerpo |
HttpServletResponse | Respuesta: estado, cabeceras, cuerpo |
@WebServlet | Asocia el servlet con una ruta |
El servlet necesita un contenedor que lo instancie, lo registre y le entregue las peticiones: es la pieza que Tomcat aporta (tema 06).
init() — el contenedor crea el servlet una vez al arrancar (o a la primera petición).service() — cada petición delega en doGet/doPost. Una sola instancia atiende todas las peticiones.destroy() — el contenedor lo retira al parar.@WebServlet("/counter") public class CounterServlet extends HttpServlet { private int counter = 0; // ¡compartido por todas las peticiones! @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { counter++; // condición de carrera entre peticiones concurrentes response.getWriter().print(counter); } }
El estado de instancia del servlet es compartido por todas las peticiones: las reglas de los hilos (tema 05) deciden qué es seguro guardar ahí.
Un filtro se ejecuta en la cadena antes (o después) del servlet. Sirve para lógica transversal: registro, compresión, autenticación.
@WebFilter("/*") public class TokenFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String header = ((HttpServletRequest) request).getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { // validar el token; si es válido, continuar chain.doFilter(request, response); } else { ((HttpServletResponse) response).setStatus(401); } } }
chain.doFilter(…) pasa al siguiente filtro o al servlet; sin llamarlo, la petición termina ahí.
| Resuelve el contenedor | Sigue siendo responsabilidad del desarrollador |
|---|---|
| Sockets, pool de hilos, HTTP | Qué hace cada ruta (la lógica) |
| Enrutado a servlets por ruta | Estructura de la aplicación |
| Parseo de parámetros y cabeceras | Reglas de negocio y acceso a datos |
| Devolución de errores de protocolo | Cuerpo y formato de los datos (JSON) |