A comunicação via TCP (Transmission Control Protocol) é um processo colaborativo entre cliente e servidor, envolvendo um ciclo de vida completo para estabelecer, manter e encerrar conexões de forma confiável. Este artigo detalha as etapas cruciais: o estabelecimento da conexão através do "three-way handshake", a transferência bidirecional de dados e a liberação da conexão com o "four-way handshake".
O Ciclo de Comunicação TCP
O "Three-Way Handshake": Estabelecendo a Conexão
O processo de três vias garante que ambos os lados estejam prontos para se comunicar.
Perspectiva do Cliente (Iniciador)
- SYN (Synchronize): O cliente envia um pacote SYN com um número de sequência inicial (ex:
seq=x) para o servidor, indicando seu desejo de iniciar uma conexão. O cliente entra no estadoSYN_SENT. - SYN-ACK (Synchronize-Acknowledge): Aguarda a resposta do servidor. Ao receber um pacote SYN-ACK (com
seq=yeack=x+1), o cliente confirma que o servidor está pronto. - ACK (Acknowledge): O cliente envia um pacote ACK (com
ack=y+1) para o servidor. A conexão agora está no estadoESTABLISHED.
Perspectiva do Servidor (Receptor]
- LISTEN: O servidor está escutando por conexões de entrada.
- SYN Received: Ao receber o SYN do cliente, o servidor aloca recursos e entra no estado
SYN_RCVD. - SYN-ACK: O servidor responde com um pacote SYN-ACK (
seq=y,ack=x+1), indicando sua prontidão e confirmando o pacote do cliente. - ESTABLISHED: Após receber o ACK final do cliente (
ack=y+1), o servidor também entra no estadoESTABLISHED.
A Necessidade das Três Vias
As três vias são essenciais para:
- Confirmação Mútua: Garantir que ambos os lados possam enviar e receber dados.
- Prevenção de Conexões Inválidas: Evitar que pacotes SYN antigos e atrasados estabeleçam conexões espúrias com o servidor.
- Alocação Eficiente de Recursos: O servidor só aloca recursos após ter uma confirmação inicial do cliente.
Transferência de Dados: Um Fluxo Bidirecional
Uma vez estabelecida a conexão, os dados fluem em ambas as direções simultaneamente (full-duplex).
Exemplo de Transferência de Dados
Lado do Cliente:
// Envio de requisição pelo cliente
escrever(socket_cliente, dados_requisicao, tamanho_requisicao);
// Recebimento da resposta do servidor
ler(socket_cliente, buffer_resposta, tamanho_buffer);
Lado do Servidor:
// Recebimento da requisição do cliente
ler(socket_conexao, buffer_requisicao, tamanho_buffer);
// Processamento e envio da resposta
escrever(socket_conexao, dados_resposta, tamanho_resposta);
Características importantes durante a transferência de dados incluem controle de fluxo (via janelas deslizantes), controle de congestionamento e garantia de entrega confiável através de números de sequência e retransmissões.
O "Four-Way Handshake": Encerrando a Conexão
A desconexão também é um processo ordenado para garantir que todos os dados tenham sido trnasmitidos.
Lado Ativo (Ex: Cliente Iniciando o Fechamento)
- FIN (Finish): O cliente envia um pacote FIN, indicando que não enviará mais dados. Entra em
FIN_WAIT_1. - ACK: Aguarda o ACK do servidor. Ao receber, entra em
FIN_WAIT_2. - FIN: Recebe o FIN do servidor.
- ACK Final: Envia um ACK para o FIN do serviodr, entrando no estado
TIME_WAIT.
Lado Passivo (Ex: Servidor Recebendo o Fechamento)
- ACK: Recebe o FIN do cliente e responde imediatamente com um ACK. Entra em
CLOSE_WAIT. - CLOSE: Após processar e enviar todos os dados pendentes, o servidor chama
close(). - FIN: Envia seu próprio pacote FIN. Entra em
LAST_ACK. - ACK: Recebe o ACK final do cliente e entra em
CLOSED.
Por Que Quatro Vias?
Devido à natureza full-duplex do TCP, cada lado deve sinalziar independentemente que terminou de enviar dados. Um FIN de uma parte não impede que a outra parte envie seus últimos dados antes de também enviar seu FIN.
O estado TIME_WAIT no lado que iniciou o fechamento é crucial para garantir que o último ACK seja recebido e para limpar pacotes antigos que possam estar vagando pela rede.
Tratamento de Exceções
O TCP possui mecanismos para lidar com falhas:
- Falhas no Handshake: Timeouts de SYN e ataques de SYN Flood.
- Falhas na Transferência: Perda de pacotes (resolvida com retransmissão) e congestionamento de rede (resolvido com algoritmos de controle).
- Falhas na Desconexão: Perda de pacotes FIN ou travamentos abruptos (detectados por mecanismos como Keepalive).
Exemplo de Implementação (Servidor e Cliente em C)
Servidor TCP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORTA 8080
#define TAMANHO_BUFFER 1024
#define BACKLOG 5
void lidar_com_cliente(int descriptor_conexao, struct sockaddr_in endereco_cliente) {
char buffer[TAMANHO_BUFFER];
ssize_t bytes_lidos;
printf("Cliente %s:%d conectado\n",
inet_ntoa(endereco_cliente.sin_addr),
ntohs(endereco_cliente.sin_port));
while ((bytes_lidos = read(descriptor_conexao, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_lidos] = '\0';
printf("Recebido de %s:%d: %s\n",
inet_ntoa(endereco_cliente.sin_addr),
ntohs(endereco_cliente.sin_port),
buffer);
char resposta[TAMANHO_BUFFER];
snprintf(resposta, sizeof(resposta), "Servidor recebeu: %s", buffer);
write(descriptor_conexao, resposta, strlen(resposta));
printf("Resposta enviada.\n");
}
if (bytes_lidos == 0) {
printf("Cliente %s:%d desconectou.\n",
inet_ntoa(endereco_cliente.sin_addr),
ntohs(endereco_cliente.sin_port));
} else {
perror("Erro na leitura");
}
close(descriptor_conexao);
printf("Conexão encerrada.\n\n");
}
int main() {
int socket_escuta, descriptor_conexao;
struct sockaddr_in addr_servidor, addr_cliente;
socklen_t tamanho_cliente = sizeof(addr_cliente);
if ((socket_escuta = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Falha na criação do socket");
exit(EXIT_FAILURE);
}
printf("Socket criado.\n");
int opt = 1;
setsockopt(socket_escuta, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&addr_servidor, 0, sizeof(addr_servidor));
addr_servidor.sin_family = AF_INET;
addr_servidor.sin_addr.s_addr = INADDR_ANY;
addr_servidor.sin_port = htons(PORTA);
bind(socket_escuta, (struct sockaddr*)&addr_servidor, sizeof(addr_servidor));
printf("Porta %d vinculada.\n", PORTA);
listen(socket_escuta, BACKLOG);
printf("Aguardando conexões...\n\n");
while (1) {
if ((descriptor_conexao = accept(socket_escuta, (struct sockaddr*)&addr_cliente, &tamanho_cliente)) < 0) {
perror("Falha no accept");
continue;
}
lidar_com_cliente(descriptor_conexao, addr_cliente);
}
close(socket_escuta);
return 0;
}
Cliente TCP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define IP_SERVIDOR "127.0.0.1"
#define PORTA 8080
#define TAMANHO_BUFFER 1024
void comunicar_com_servidor(int socket_cliente) {
char buffer[TAMANHO_BUFFER];
ssize_t n;
printf("Conectado ao servidor. Digite 'quit' para sair.\n");
while (1) {
printf("Mensagem: ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "quit") == 0) {
printf("Fechando conexão...\n");
break;
}
write(socket_cliente, buffer, strlen(buffer));
printf("Enviado: %s\n", buffer);
n = read(socket_cliente, buffer, sizeof(buffer) - 1);
if (n < 0) {
perror("Erro na leitura");
break;
} else if (n == 0) {
printf("Servidor desconectou.\n");
break;
} else {
buffer[n] = '\0';
printf("Resposta do servidor: %s\n", buffer);
}
}
}
int main() {
int socket_cliente;
struct sockaddr_in addr_servidor;
if ((socket_cliente = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Falha na criação do socket");
exit(EXIT_FAILURE);
}
printf("Socket criado.\n");
memset(&addr_servidor, 0, sizeof(addr_servidor));
addr_servidor.sin_family = AF_INET;
addr_servidor.sin_port = htons(PORTA);
inet_pton(AF_INET, IP_SERVIDOR, &addr_servidor.sin_addr);
printf("Conectando a %s:%d...\n", IP_SERVIDOR, PORTA);
if (connect(socket_cliente, (struct sockaddr*)&addr_servidor, sizeof(addr_servidor)) < 0) {
perror("Falha na conexão");
close(socket_cliente);
exit(EXIT_FAILURE);
}
printf("Conectado com sucesso!\n");
comunicar_com_servidor(socket_cliente);
printf("Fechando socket...\n");
close(socket_cliente);
return 0;
}