Sockets: comunicação em rede com C
1. Introdução aos Sockets em C
Sockets são abstrações de software que permitem a comunicação entre processos, seja em uma mesma máquina ou através de uma rede. Em C, a API de sockets (definida em <sys/socket.h>) é a base para construir aplicações de rede no modelo cliente-servidor. Nesse modelo, um processo servidor aguarda passivamente por conexões, enquanto clientes iniciam a comunicação ativamente.
A API de sockets em sistemas Unix/Linux segue o padrão Berkeley, oferecendo funções como socket(), bind(), listen(), accept(), connect(), send(), recv() e close(). A compreensão dessas funções é essencial para desenvolver aplicações de rede robustas e eficientes.
2. Endereçamento e Estruturas de Dados
Para comunicação em rede IPv4, utilizamos a estrutura sockaddr_in:
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // Porta em network byte order
struct in_addr sin_addr; // Endereço IPv4
char sin_zero[8]; // Preenchimento
};
struct in_addr {
uint32_t s_addr; // Endereço em network byte order
};
A conversão entre endereços legíveis e binários é feita com inet_pton() (presentation to network) e inet_ntop() (network to presentation):
#include <arpa/inet.h>
struct sockaddr_in addr;
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
A ordem de bytes em rede (big-endian) é obrigatória para campos como porta e endereço. As funções de conversão são:
uint16_t htons(uint16_t hostshort); // host to network short
uint32_t htonl(uint32_t hostlong); // host to network long
uint16_t ntohs(uint16_t netshort); // network to host short
uint32_t ntohl(uint32_t netlong); // network to host long
3. Criação e Configuração do Socket
A função socket() cria um endpoint de comunicação:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // UDP
Parâmetros:
- AF_INET: domínio IPv4
- SOCK_STREAM: socket TCP (orientado a conexão)
- SOCK_DGRAM: socket UDP (datagramas)
- Último parâmetro: 0 para protocolo padrão
Para reutilizar endereços após encerramento abrupto, usamos setsockopt():
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
O bind associa o socket a um endereço e porta:
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY; // Todas as interfaces
serv_addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
4. Servidor TCP: Ciclo de Vida Completo
Após criar e configurar o socket, o servidor TCP segue estas etapas:
- listen(): Coloca o socket em modo passivo
- accept(): Aceita conexões de clientes
- send()/recv(): Troca dados com o cliente
Exemplo prático: servidor eco que lê dados e os reenvia ao cliente
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// Cria socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// Configura reuso de endereço
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// Configura endereço
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// Bind
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// Listen
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Servidor aguardando conexões na porta %d...\n", PORT);
// Accept e loop de eco
if ((client_fd = accept(server_fd, (struct sockaddr *)&address,
(socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Cliente conectado!\n");
int valread;
while ((valread = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
printf("Recebido: %s", buffer);
send(client_fd, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE);
}
close(client_fd);
close(server_fd);
return 0;
}
5. Cliente TCP: Conectando e Trocando Dados
O cliente TCP segue estas etapas:
- Cria socket com
socket() - Conecta ao servidor com
connect() - Troca dados com
send()erecv()
Exemplo prático: cliente que envia mensagens e aguarda resposta
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *message = "Olá do cliente!";
char buffer[BUFFER_SIZE] = {0};
// Cria socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation error");
return -1;
}
// Configura endereço do servidor
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Converte IP
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("invalid address");
return -1;
}
// Conecta
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
return -1;
}
// Envia mensagem
send(sock, message, strlen(message), 0);
printf("Mensagem enviada\n");
// Recebe resposta
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Resposta do servidor: %s\n", buffer);
close(sock);
return 0;
}
6. Sockets UDP: Comunicação sem Conexão
Diferente do TCP, UDP não estabelece conexão prévia. Cada envio é um datagrama independente, com possibilidade de perda ou reordenação. As funções principais são sendto() e recvfrom().
Servidor UDP simples:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 9090
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
char buffer[BUFFER_SIZE];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
int len = sizeof(cliaddr);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("Recebido do cliente: %s\n", buffer);
sendto(sockfd, "Recebido!", 9, 0,
(struct sockaddr *)&cliaddr, len);
close(sockfd);
return 0;
}
Cliente UDP:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 9090
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in servaddr;
char buffer[BUFFER_SIZE];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
sendto(sockfd, "Olá servidor!", 13, 0,
(struct sockaddr *)&servaddr, sizeof(servaddr));
int len = sizeof(servaddr);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&servaddr, &len);
buffer[n] = '\0';
printf("Resposta do servidor: %s\n", buffer);
close(sockfd);
return 0;
}
7. Tratamento de Erros e Boas Práticas
Todas as funções de socket retornam valores que devem ser verificados:
if (socket(...) < 0) {
perror("socket"); // Imprime erro no stderr
exit(EXIT_FAILURE);
}
// Alternativa com strerror
if (bind(...) < 0) {
fprintf(stderr, "Erro no bind: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
O fechamento correto de sockets é fundamental:
close(sockfd); // Fecha o socket e libera recursos
Para evitar o sinal SIGPIPE ao escrever em socket fechado, configure:
signal(SIGPIPE, SIG_IGN); // Ignora SIGPIPE
// Ou use MSG_NOSIGNAL no send:
send(sockfd, data, len, MSG_NOSIGNAL);
Para implementar timeouts, use select():
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
fd_set fds;
FD_ZERO(&fds);
FD_SET(sockfd, &fds);
int ret = select(sockfd + 1, &fds, NULL, NULL, &tv);
if (ret == 0) {
printf("Timeout!\n");
} else if (ret < 0) {
perror("select");
}
Boas práticas adicionais incluem:
- Sempre inicializar estruturas com memset() antes de usar
- Verificar erros em cada chamada de sistema
- Fechar sockets em caso de erro para evitar vazamentos
- Usar SO_REUSEADDR em servidores para reinicialização rápida
- Preferir poll() sobre select() para maior escalabilidade
Referências
- Beej's Guide to Network Programming — Guia completo e prático sobre programação de sockets em C, com exemplos TCP e UDP
- Linux man pages: socket(2) — Documentação oficial da função socket() no Linux
- GNU C Library: Sockets — Seção completa sobre sockets na documentação da glibc
- Tutorial de Sockets em C do GeeksforGeeks — Tutoriais práticos com exemplos de servidores e clientes TCP/UDP
- RFC 793: Transmission Control Protocol — Especificação original do protocolo TCP, base para implementações de sockets
- IBM Developer: Socket programming in C — Tutorial da IBM sobre programação de sockets em ambientes Unix/Linux