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.
- El servidor crea un
ServerSocketen un puerto y se queda esperando enaccept(). - Cada conexión aceptada devuelve un
Socketcon 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.
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 eldoGetde un servlet (sección 5).HttpExchangereú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
setExecutorse 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 endoGet/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) |