W3docs

Java Sockets

Abra conexões TCP de cliente em Java com a classe Socket e leia e escreva bytes por streams.

Abaixo do HTTP e de todos os outros protocolos de aplicação está o socket: uma conexão TCP bruta e bidirecional entre dois endpoints. O java.net.Socket do Java é o lado do cliente — você cria um, conecta-o a um host e porta e, em seguida, lê e escreve bytes por meio de InputStream/OutputStreams comuns. Este é o nível mais baixo de rede que você usa quando fala um protocolo personalizado ou conversa com um serviço que não é HTTP.

Este capítulo aborda o que um socket conectado oferece, as duas formas de conectar (e por que uma é mais segura), como ler e escrever texto e bytes brutos, um exemplo completo e executável de cliente que conversa com um servidor, os timeouts e armadilhas que afetam código real, e onde os sockets se encaixam em relação ao HttpClient de nível mais alto que você já conheceu.

O que um Socket oferece

Um Socket conectado é um canal com dois streams:

  • socket.getOutputStream() — bytes que você escreve chegam ao outro lado.
  • socket.getInputStream() — bytes que o outro lado escreve chegam aqui.

O TCP garante que os bytes chegam de forma confiável e em ordem. Ele não impõe nenhuma estrutura de mensagem — um socket é um stream de bytes, não de mensagens. O enquadramento (onde uma mensagem termina e a próxima começa) é seu trabalho: use quebras de linha, prefixos de comprimento ou um protocolo de nível mais alto. Se você precisa que os limites das mensagens sejam preservados em vez de um stream, essa é a função dos sockets de datagrama (UDP), que trocam ordenação e confiabilidade por pacotes discretos.

Os streams vêm diretamente do sistema de I/O do Java: getInputStream() retorna um InputStream simples e getOutputStream() um OutputStream simples, portanto todo wrapper que você conhece — BufferedReader, PrintWriter, DataInputStream — funciona sobre um socket exatamente como funciona sobre um arquivo.

Conectando

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

A segunda forma permite definir um timeout de conexão — sem ele, um host inacessível pode bloquear a thread pelo padrão do sistema operacional (geralmente um minuto ou mais). Uma vez conectado, envolva os streams em readers/writers com buffer e escolha um conjunto de caracteres explicitamente.

Lendo e escrevendo 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() bloqueia até que dados (ou fim de stream) cheguem — a característica marcante da API de socket de bloqueio clássica. Sempre feche o socket (try-with-resources) para liberar a conexão.

Lendo bytes brutos

Texto é conveniente, mas muitos protocolos são binários — dados de imagem, frames com prefixo de comprimento, um formato de wire personalizado. Nesses casos, ignore os readers e trabalhe diretamente com os streams:

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

Dois fatos moldam toda leitura no nível de bytes:

  • read(byte[]) retorna quantos bytes foram realmente lidos, que não é necessariamente o que você pediu. Uma escrita no outro lado pode chegar como várias leituras, e várias escritas podem chegar como uma leitura — o TCP coalece e divide à vontade. Para obter um número fixo de bytes, você deve fazer um loop ou envolver o stream em DataInputStream e chamar readFully().
  • Um valor de retorno de -1 significa que o peer fechou o seu lado (fim de stream), não "sem dados agora". Esse é o sinal para parar de ler.

Timeouts que importam

Um socket de bloqueio pode travar em dois lugares distintos, e eles precisam de dois timeouts distintos:

Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000); // connect timeout
socket.setSoTimeout(5000);                                       // read timeout
  • O timeout de conexão (o segundo argumento de connect) limita quanto tempo o handshake TCP pode levar. Sem ele, um host inacessível bloqueia a thread pelo padrão do sistema operacional — geralmente um minuto ou mais.
  • O timeout de leitura (setSoTimeout) limita quanto tempo qualquer read/readLine pode bloquear aguardando dados. Quando expira, a chamada lança SocketTimeoutException sem fechar o socket, para que você possa decidir se tenta novamente ou desiste. Sem ele, um peer silencioso bloqueia você para sempre.

O código de rede real deve definir ambos. Os dois erros que você deve estar pronto para capturar são UnknownHostException (o DNS não conseguiu resolver o nome) e a ampla família IOException (conexão recusada, redefinida ou com timeout); consulte exceções Java para padrões de tratamento.

Um exemplo prático: um cliente conversando com um servidor de echo em loopback

Este programa inicia um servidor de echo de uso único em uma thread em segundo plano vinculada ao endereço de loopback, e então — o foco real do capítulo — conecta um Socket de cliente, envia uma linha e lê a resposta. É uma conversa TCP completa dentro de uma JVM, sem rede externa.

java— editable, runs on the server

O que extrair da execução:

  • O lado do cliente são apenas três passos: construir um Socket, chamar connect() com um endereço e porta, e então ler e escrever seus streams. Tudo o que o HTTP fez por você em capítulos anteriores — linhas de requisição, cabeçalhos, códigos de status — desaparece; um socket move bytes brutos e nada mais.
  • connect(new InetSocketAddress(...), 2000) definiu um timeout de conexão de 2 segundos. O construtor sem timeout new Socket(host, port) bloquearia no padrão do sistema operacional se o host fosse inacessível, portanto a forma com timeout explícito é o hábito mais seguro para qualquer rede real.
  • O protocolo era uma convenção, não uma funcionalidade: o cliente escreveu uma linha e leu uma linha porque ambos os lados concordaram que linhas são mensagens. O TCP entregou um stream de bytes ordenado; o enquadramento por nova linha que o transformou em "mensagens" foi totalmente definido pela aplicação.
  • readLine() bloqueou até que a resposta do servidor chegasse. Esse modelo de uma thread por conexão, bloqueando até os dados, é simples e correto, e é exatamente o custo que as threads virtuais visam tornar barato quando o número de conexões cresce.
  • getRemoteSocketAddress() e getLocalSocketAddress() mostraram ambos os lados da conexão ativa — a porta de loopback do servidor e a porta local atribuída pelo sistema operacional ao cliente. Cada conexão TCP é identificada por esse par de endpoints. O ServerSocket do lado do servidor constrói o listener que aceitou essa conexão.

Quando usar um socket bruto

Um Socket é a ferramenta certa quando não existe uma biblioteca que já fale o seu protocolo:

  • Você está implementando ou consumindo um protocolo TCP personalizado (um servidor de jogo, um formato de wire de message broker, uma porta de administração baseada em linhas).
  • Você precisa falar com um serviço não-HTTP — SMTP, um protocolo de texto no estilo Redis, um servidor de linhas legado.

Para qualquer coisa que seja HTTP, não construa requisições manualmente sobre um socket. Use o moderno HttpClient, que oferece pooling de conexões, redirecionamentos, HTTP/2 e TLS gratuitamente. A relação é a mesma que entre streams de bytes brutos e os readers de nível mais alto construídos sobre eles: desça para o nível mais baixo apenas quando o mais alto não puder expressar o que você precisa.

Prática

Prática
Um cliente lê de um servidor usando 'socket.getInputStream()' envolto em um 'BufferedReader', enviando um comando terminado em nova linha e esperando uma resposta terminada em nova linha. Ocasionalmente uma resposta é dividida em dois segmentos TCP e o cliente a lê incorretamente. Qual é o entendimento correto?
Um cliente lê de um servidor usando 'socket.getInputStream()' envolto em um 'BufferedReader', enviando um comando terminado em nova linha e esperando uma resposta terminada em nova linha. Ocasionalmente uma resposta é dividida em dois segmentos TCP e o cliente a lê incorretamente. Qual é o entendimento correto?
Was this page helpful?