Tabla de Contenidos

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.

ServidorClienteServidorClienteServidorClienteServerSocket(8080) y accept()Socket("localhost", 8080)accept() devuelve elSocket de esa conexiónflujos de entrada y salidacierra la conexiónvuelve a accept()

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
    }
}

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

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