Introdução a Sockets
Um socket, essencialmente, é a combinação de um endereço IP e uma porta. Essa combinação permite a comunicação com aplicações em um host remoto. Enquanto o endereço IP identifica um dispositivo específico em uma rede, a porta distingue um aplicativo particular dentro desse dispositivo. Portanto, um par IP+porta define unicamente um aplicativo em um determinado host. Os sockets, originados no UNIX, operam de forma semelhante a arquivos, suportando operações como abertura, fechamento, leitura e escrita. Em suma, os sockets são a chave para a comunicação entre hosts em uma rede.
Protocolos TCP e UDP
Para que a comunicação em rede ocorra de maneira ordenada, são necessários protocolos que definam as regras. TCP e UDP são exemplos desses protocolos:
TCP (Transmission Control Protocol)
O TCP é um protocolo de transmissão de dados confiável e orientado à conexão. "Confiável" significa que os dados transmitidos não serão perdidos nem duplicados. O mecanismo de confirmação do TCP garante que, a cada pacote enviado, o remetente espera uma confirmação do destinatário antes de enviar o próximo. Essa troca de confirmações assegura a integridade dos dados. A transmissão "bidirecional" indica que ambos os lados da comunicação podem atuar como remetente ou destinatário.
UDP (User Datagram Protocol)
O UDP é um protocolo de transmissão de dados não confiável e sem conexão. Ele simplesmente envia os dados sem verificar se foram recebidos ou não, focando apenas na transmissão. Essa característica o torna inadequado para aplicações que exigem alta confiabilidade, mas é ideal para transmissões onde a perda ocasional de pacotes é aceitável, como na transmissão de vídeo, onde a perda de alguns quadros pode não impactar significativamente a experiência do usuário.
Sockets podem operar tanto sobre TCP quanto sobre UDP, permitindo a escolha do protocolo mais adequado às necessidades da aplicação.
Exemplo de Programa de Comunicação Simples
Para ilustrar o uso de sockets, apresentamos um exemplo simples dividido em duas partes: o servidor e o cliente.
Criação do Servidor
O processo de criação de um servidor utilizando sockets envolve as seguintes etapas:
1. Criação do Socket
A comunicação requer um socket. A função socket() é utilizada para criá-lo:
int create_socket(int family, int type, int protocol);
family: Define a família de endereços.AF_INETé comumente usado para IPv4 eAF_INET6para IPv6.type: Especifica o tipo de comunicação.SOCK_STREAMpara transmissão orientada a fluxo (TCP) eSOCK_DGRAMpara transmissão de datagramas (UDP).protocol: O protocolo a ser utilizado, comoIPPROTO_TCPouIPPROTO_UDP.
Esta função retorna um descritor de socket, que é um identificador inteiro.
2. Associação do Socket (Bind)
A função bind() associa o socket a um endereço IP e porta específicos:
int bind_socket(int sock_fd, const struct sockaddr *addr, socklen_t addrlen);
sock_fd: O descritor do socket a ser associado.addr: Um ponteiro para uma estruturasockaddrque contém o endereço IP e a porta.addrlen: O tamanho da estruturasockaddr.
O exemplo a seguir demonstra a criação e associação de um socket para IPv4 e TCP:
// Usando IPv4 e protocolo TCP
SOCKET server_socket_fd = create_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_address;
server_address.sin_addr.s_addr = htonl(INADDR_ANY); // Aceita conexões de qualquer IP
server_address.sin_family = AF_INET; // Família IPv4
server_address.sin_port = htons(8080); // Porta 8080
bind_socket(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address));
3. Escuta de Conexões (Listen)
Após configurar o socket, o servidor pode começar a escutar por conexões de entrada:
int listen_for_connections(int sock_fd, int backlog);
sock_fd: O descritor do socket do servidor.backlog: O número máximo de conexões pendentes que o sistema pode enfileirar.
4. Aceitação de Conexões (Accept)
A função accept() bloqueia a execução até que um cliente solicite uma conexão:
SOCKET accept_connection(int listen_sock_fd, struct sockaddr *client_addr, socklen_t *addrlen);
listen_sock_fd: O descritor do socket de escuta.client_addr: Um ponteiro para uma estruturasockaddrpara armazenar as informações do endereço do cliente.addrlen: O tamanho da estrutura do endereço do cliente.
É crucial notar que accept() retorna um *novo* descritor de socket, que será usado especificamente para a comunicação com o cliente conectado. O descritor original continua sendo usado para escutar novas conexões.
5. Envio e Recepção de Dados (Send/Recv)
Uma vez estabelecida a conexão, a comunicação de dados pode ocorrer usando funções como send() e recv() (ou write() e read()):
ssize_t send_data(int sockfd, const void *buf, size_t len, int flags);
ssize_t receive_data(int sockfd, void *buf, size_t len, int flags);
sockfd: O descritor do socket para comunicação.buf: Um buffer contendo os dados a serem enviados ou para receber os dados.len: O número de bytes a serem enviados ou recebidos.flags: Flags de controle de operação (geralmente 0).
6. Fechamento do Socket (CloseSocket)
Ao final da comunicação, o socket deve ser fechado:
void close_socket(SOCKET sock_fd);
Exemplo Completo de Servidor (Simplificado)
#include <stdio.h>
#include <winsock2.h> // Para Windows Sockets
#pragma comment(lib, "ws2_32.lib") // Link com a biblioteca de sockets do Windows
int main() {
SOCKET listening_socket;
SOCKET client_socket;
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
int client_addr_size;
char data_buffer[64];
WSADATA wsa_data;
WORD version_requested = MAKEWORD(2, 2);
int init_result = WSAStartup(version_requested, &wsa_data);
if (init_result != 0) {
printf("Falha na inicialização do Winsock.\n");
return 1;
}
// Criação do socket
listening_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listening_socket == INVALID_SOCKET) {
printf("Erro ao criar o socket: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Configuração do endereço do servidor
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Escuta em qualquer interface
server_addr.sin_port = htons(8888); // Porta 8888
// Associação do socket ao endereço e porta
if (bind(listening_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("Erro no bind: %d\n", WSAGetLastError());
closesocket(listening_socket);
WSACleanup();
return 1;
}
// Inicia a escuta por conexões
if (listen(listening_socket, 3) == SOCKET_ERROR) {
printf("Erro no listen: %d\n", WSAGetLastError());
closesocket(listening_socket);
WSACleanup();
return 1;
}
printf("Servidor escutando na porta 8888...\n");
// Aceita uma conexão de cliente
client_addr_size = sizeof(client_addr);
client_socket = accept(listening_socket, (struct sockaddr *)&client_addr, &client_addr_size);
if (client_socket == INVALID_SOCKET) {
printf("Erro no accept: %d\n", WSAGetLastError());
closesocket(listening_socket);
WSACleanup();
return 1;
}
printf("Cliente conectado de: %s\n", inet_ntoa(client_addr.sin_addr));
// Envia mensagem de boas-vindas
sprintf(data_buffer, "Bem-vindo ao servidor!");
send(client_socket, data_buffer, strlen(data_buffer), 0);
// Recebe dados do cliente
int bytes_received = recv(client_socket, data_buffer, sizeof(data_buffer) - 1, 0);
if (bytes_received > 0) {
data_buffer[bytes_received] = '\0'; // Garante terminação nula
printf("Mensagem do cliente: %s\n", data_buffer);
} else if (bytes_received == 0) {
printf("Cliente desconectou.\n");
} else {
printf("Erro ao receber dados: %d\n", WSAGetLastError());
}
// Fecha os sockets
closesocket(client_socket);
closesocket(listening_socket);
WSACleanup(); // Limpa os recursos do Winsock
return 0;
}
Criação do Cliente
O cliente difere do servidor principalmente por sua natureza ativa. Em vez de esperar por conexões, o cliente inicia a comunicação. Ele não utiliza as funções listen() ou accept(). Em vez disso, emprega a função connect() para estabelecer uma conexão com o servidor.
Conexão com o Servidor (Connect)
A função connect() é usada pelo cliente para iniciar uma conexão com um servidor:
int establish_connection(int sock_fd, const struct sockaddr *serv_addr, socklen_t addrlen);
sock_fd: O descritor do socket do cliente.serv_addr: Um ponteiro para a estruturasockaddrcontendo o endereço IP e a porta do servidor.addrlen: O tamanho da estrutura do endereço do servidor.
Exemplo Completo de Cliente (Simplificado)
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
SOCKET client_socket;
struct sockaddr_in server_addr;
char buffer[64];
WSADATA wsa_data;
WORD version_requested = MAKEWORD(2, 2);
int init_result = WSAStartup(version_requested, &wsa_data);
if (init_result != 0) {
printf("Falha na inicialização do Winsock.\n");
return 1;
}
// Criação do socket
client_socket = socket(AF_INET, SOCK_STREAM, 0); // 0 para usar o protocolo padrão (TCP)
if (client_socket == INVALID_SOCKET) {
printf("Erro ao criar o socket: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Configuração do endereço do servidor
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP do servidor (localhost)
server_addr.sin_port = htons(8888); // Porta do servidor (deve ser a mesma)
// Tenta conectar ao servidor
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
printf("Erro ao conectar ao servidor: %d\n", WSAGetLastError());
closesocket(client_socket);
WSACleanup();
return 1;
}
printf("Conectado ao servidor.\n");
// Recebe a mensagem de boas-vindas do servidor
int bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Servidor diz: %s\n", buffer);
} else if (bytes_received == 0) {
printf("Conexão fechada pelo servidor.\n");
} else {
printf("Erro ao receber dados: %d\n", WSAGetLastError());
}
// Envia uma mensagem para o servidor
sprintf(buffer, "Olá, servidor!");
send(client_socket, buffer, strlen(buffer), 0);
// Fecha o socket
closesocket(client_socket);
WSACleanup();
return 0;
}
Para que a comunicação funcione, o programa do servidor deve ser executado antes do programa do cliente. A execução desses exemplos básicos fornece uma introdução prática ao funcionamento dos sockets.