====== 04 - Servidor HTTP: de sockets a servlets ======
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.
===== 1. Socket =====
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.
@startuml
participant "Servidor" as S
participant "Cliente" as C
S -> S : ServerSocket(8080) y accept()
C -> S : Socket("localhost", 8080)
note right of S : accept() devuelve el\nSocket de esa conexión
S <-> C : flujos de entrada y salida
C -> S : cierra la conexión
S -> S : vuelve a accept()
@enduml
* El **servidor** crea un ''ServerSocket'' en un puerto y se queda esperando en ''accept()''.
* Cada conexión aceptada devuelve un ''Socket'' con los flujos de entrada y salida.
* El **cliente** se conecta con ''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.
Cierra siempre los sockets y flujos (aquí con ''try-with-resources''). Una conexión sin cerrar deja recursos ocupados en ambos extremos.
===== 2. Servidor HTTP sobre sockets =====
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.
===== 3. Servidor HTTP del JDK =====
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...
* No es un contenedor de servlets: sin anotaciones, filtros ni ciclo de vida; un servidor real, pero básico.
* Por defecto atiende las peticiones una a una; con ''setExecutor'' se le entrega un pool de hilos (tema 05).
* Para servir una carpeta de ficheros estáticos sin escribir código, Java 18 también trae la orden ''jwebserver''.
===== 4. Límites =====
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) |
===== 5. Servlet =====
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).
===== 6. Ciclo de vida =====
* **''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í.
===== 7. Filtros =====
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í.
===== 8. Qué resuelve y qué no =====
^ 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) |
El **socket** aporta el canal y **HTTP** es el protocolo que lo recorre: el servidor escrito a mano muestra que ese protocolo es texto, el servidor del JDK evita escribirlo a mano y el **servlet** organiza el código. El **contenedor** (el **Tomcat** del tema 06) lo completa para producción: ciclo de vida de servlets y filtros, pool de hilos y errores de protocolo.