====== 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.