W3docs

Java Sockets

Abre conexiones TCP de cliente en Java con la clase Socket: flujos, timeouts y ejemplo completo.

Bajo HTTP y cualquier otro protocolo de aplicación se encuentra el socket: una conexión TCP bidireccional y sin formato entre dos extremos. La clase java.net.Socket de Java es el lado cliente — se crea una instancia, se conecta a un host y puerto, y luego se leen y escriben bytes a través de InputStream/OutputStream ordinarios. Este es el nivel más bajo de red al que se recurre cuando se habla un protocolo personalizado o se accede a un servicio que no es HTTP.

Este capítulo cubre lo que ofrece un socket conectado, las dos formas de conectarse (y por qué una es más segura), cómo leer y escribir texto y bytes sin formato, un ejemplo completo ejecutable de cliente hablando con un servidor, los timeouts y problemas que afectan al código real, y dónde encajan los sockets respecto al HttpClient de mayor nivel que ya conoces.

Lo que ofrece un Socket

Un Socket conectado es una tubería con dos flujos:

  • socket.getOutputStream() — los bytes que escribes viajan al otro extremo.
  • socket.getInputStream() — los bytes que escribe el otro extremo llegan aquí.

TCP garantiza que los bytes lleguen de forma confiable y en orden. No impone ninguna estructura de mensajes — un socket es un flujo de bytes, no de mensajes. El encuadre (dónde termina un mensaje y empieza el siguiente) es tu trabajo: usa saltos de línea, prefijos de longitud o un protocolo de mayor nivel. Si necesitas que se conserven los límites de los mensajes en lugar de un flujo, ese es el trabajo de los sockets de datagrama (UDP), que sacrifican el orden y la confiabilidad a cambio de paquetes discretos.

Los flujos provienen directamente del sistema de I/O de Java: getInputStream() devuelve un InputStream simple y getOutputStream() un OutputStream simple, por lo que cada envoltura que conoces — BufferedReader, PrintWriter, DataInputStream — funciona sobre un socket exactamente igual que sobre un archivo.

Conexión

// Style 1: connect in the constructor
Socket socket = new Socket("example.com", 80);

// Style 2: create then connect with a timeout (preferred)
Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000);

La segunda forma permite establecer un timeout de conexión — sin él, un host inalcanzable puede bloquear el hilo durante el tiempo predeterminado del sistema operativo (que suele ser un minuto o más). Una vez conectado, envuelve los flujos en lectores/escritores con buffer y especifica explícitamente un conjunto de caracteres.

Lectura y escritura de texto

var out = new PrintWriter(
        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
var in = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

out.println("ping");                 // autoFlush=true sends it immediately
String reply = in.readLine();        // blocks until a line arrives

readLine() bloquea hasta que llegan datos (o fin del flujo) — la característica distintiva de la API de socket de bloqueo clásica. Siempre cierra el socket (try-with-resources) para liberar la conexión.

Lectura de bytes sin formato

El texto es conveniente, pero muchos protocolos son binarios — datos de imagen, tramas con prefijo de longitud, un formato de cable personalizado. En ese caso, omite los lectores y trabaja directamente con los flujos:

try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress("example.com", 80), 2000);
    OutputStream out = socket.getOutputStream();
    InputStream in = socket.getInputStream();

    out.write("PING".getBytes(StandardCharsets.UTF_8));
    out.flush();                          // streams are not auto-flushed

    byte[] buffer = new byte[4096];
    int n = in.read(buffer);              // bytes read, or -1 at end-of-stream
    if (n > 0) {
        String chunk = new String(buffer, 0, n, StandardCharsets.UTF_8);
        System.out.println(chunk);
    }
}

Dos hechos determinan cada lectura a nivel de bytes:

  • read(byte[]) devuelve cuántos bytes obtuvo realmente, que no es necesariamente lo que pediste. Una escritura en el otro extremo puede llegar como varias lecturas, y varias escrituras pueden llegar como una sola lectura — TCP coalesce y divide a voluntad. Para obtener un número fijo de bytes debes usar un bucle, o envolver el flujo en DataInputStream y llamar a readFully().
  • Un valor de retorno de -1 significa que el par cerró su extremo (fin del flujo), no "no hay datos ahora mismo". Esa es la señal para dejar de leer.

Timeouts importantes

Un socket de bloqueo puede detenerse en dos lugares distintos, y necesitan dos timeouts distintos:

Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000); // connect timeout
socket.setSoTimeout(5000);                                       // read timeout
  • El timeout de conexión (el segundo argumento de connect) limita el tiempo que puede durar el handshake TCP. Sin él, un host inalcanzable bloquea el hilo durante el tiempo predeterminado del sistema operativo — que suele ser un minuto o más.
  • El timeout de lectura (setSoTimeout) limita el tiempo que cualquier read/readLine puede bloquearse esperando datos. Cuando expira, la llamada lanza SocketTimeoutException sin cerrar el socket, por lo que puedes decidir si reintentar o abandonar. Sin él, un par silencioso te bloquea indefinidamente.

El código de red real debería establecer ambos. Los dos errores que debes estar preparado para capturar son UnknownHostException (DNS no pudo resolver el nombre) y la amplia familia de IOException (conexión rechazada, restablecida o con timeout); consulta excepciones en Java para ver patrones de manejo.

Un ejemplo práctico: un cliente hablando con un servidor echo de loopback

Este programa inicia un servidor echo de un solo uso en un hilo de fondo vinculado a la dirección de loopback, y luego — el verdadero objetivo del capítulo — conecta un Socket cliente, envía una línea y lee la respuesta. Es una conversación TCP completa dentro de una sola JVM, sin red externa.

java— editable, runs on the server

Lo que hay que extraer de la ejecución:

  • El lado cliente es solo tres pasos: construir un Socket, llamar a connect() con una dirección y puerto, y luego leer y escribir en sus flujos. Todo lo que HTTP hacía por ti en capítulos anteriores — líneas de solicitud, cabeceras, códigos de estado — ha desaparecido; un socket mueve bytes sin formato y nada más.
  • connect(new InetSocketAddress(...), 2000) estableció un timeout de conexión de 2 segundos. El constructor sin timeout new Socket(host, port) bloquearía en el predeterminado del sistema operativo si el host fuera inalcanzable, por lo que la forma con timeout explícito es el hábito más seguro para cualquier red real.
  • El protocolo era una convención, no una función: el cliente escribió una línea y leyó una línea porque ambos lados acordaron que las líneas son mensajes. TCP entregó un flujo de bytes ordenado; el encuadre con saltos de línea que lo convirtió en "mensajes" fue completamente definido por la aplicación.
  • readLine() bloqueó hasta que llegó la respuesta del servidor. Este modelo de un hilo por conexión y bloqueo hasta recibir datos es simple y correcto, y es exactamente el costo que los hilos virtuales pretenden abaratar cuando el número de conexiones crece.
  • getRemoteSocketAddress() y getLocalSocketAddress() mostraron ambos extremos de la conexión activa — el puerto de loopback del servidor y el puerto local asignado por el sistema operativo al cliente. Cada conexión TCP se identifica por ese par de extremos. El ServerSocket del lado servidor construye el listener que aceptó esta conexión.

Cuándo usar un socket sin formato

Un Socket es la herramienta adecuada cuando no existe una biblioteca que ya implemente tu protocolo:

  • Estás implementando o consumiendo un protocolo TCP personalizado (un servidor de juegos, un formato de cable de broker de mensajes, un puerto de administración basado en líneas).
  • Necesitas hablar con un servicio que no sea HTTP — SMTP, un protocolo de texto estilo Redis, un servidor de líneas heredado.

Para cualquier cosa que sea HTTP, no hagas solicitudes manualmente sobre un socket. Usa el moderno HttpClient, que te ofrece agrupación de conexiones, redirecciones, HTTP/2 y TLS de forma gratuita. La relación es la misma que entre los flujos de bytes sin formato y los lectores de mayor nivel construidos sobre ellos: desciende al nivel inferior solo cuando el superior no puede expresar lo que necesitas.

Práctica

Práctica
Un cliente lee de un servidor usando 'socket.getInputStream()' envuelto en un 'BufferedReader', enviando un comando terminado en salto de línea y esperando una respuesta también terminada en salto de línea. En ocasiones una respuesta llega dividida en dos segmentos TCP y el cliente la lee incorrectamente. ¿Cuál es la comprensión correcta?
Un cliente lee de un servidor usando 'socket.getInputStream()' envuelto en un 'BufferedReader', enviando un comando terminado en salto de línea y esperando una respuesta también terminada en salto de línea. En ocasiones una respuesta llega dividida en dos segmentos TCP y el cliente la lee incorrectamente. ¿Cuál es la comprensión correcta?
Was this page helpful?