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