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 arrivesreadLine() 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 emDataInputStreame chamarreadFully().- Um valor de retorno de
-1significa 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 qualquerread/readLinepode bloquear aguardando dados. Quando expira, a chamada lançaSocketTimeoutExceptionsem 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.
O que extrair da execução:
- O lado do cliente são apenas três passos: construir um
Socket, chamarconnect()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 timeoutnew 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()egetLocalSocketAddress()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. OServerSocketdo 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.