Fundamentos de Sockets para Comunicação em Rede

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 e AF_INET6 para IPv6.
  • type: Especifica o tipo de comunicação. SOCK_STREAM para transmissão orientada a fluxo (TCP) e SOCK_DGRAM para transmissão de datagramas (UDP).
  • protocol: O protocolo a ser utilizado, como IPPROTO_TCP ou IPPROTO_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 estrutura sockaddr que contém o endereço IP e a porta.
  • addrlen: O tamanho da estrutura sockaddr.

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 estrutura sockaddr para 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 estrutura sockaddr contendo 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.

Tags: sockets TCP UDP Programação de Rede cliente-servidor

Publicado em 6-10 04:04 por Thomas