Sockets em C (WinSock)

Postado por Yoshio Iwamoto em 10/09/2007

Um socket é um mecanismo de comunicação que permite que dois ou mais aplicativos troquem informações, seja no mesmo computador ou em computadores distintos, como os programas de chats, games on-line, navegadores, etc. No geral, programas que utilizam Network Communication ouInterprocess Communication utilizam sockets.

Sockets são importantes, por isso eu tinha que tomar vergonha na cara e aprender a usá-los, comecei a ler sobre o assunto e consegui fazer uma pequena aplicação que envia mensagens para outra.

Primeiramente vou mostrar o código e depois ir (tentando) descrever as funções. Não vou me aprofundar, é só para ter uma idéia de como a coisa funciona, mesmo porque eu acho que aprofundei demais na explicação (mas quanto mais se sabe, mas se sabe que não se sabe nada XD).

O código foi feito para Windows, mas a implementação no Linux é bem parecida. No Windows você deve usar a WinSock API para trabalhar com sockets, não se esqueça de linkar a lib na hora de compilar. Eu uso o Dev-C++ e a lib é a libwsock32.a.

Criando o servidor que recebe mensagens

 

1.                   /*
2.                       SERVIDOR
3.                   */
4.                    
5.                   #include <stdio.h>
6.                   #include <stdlib.h>
7.                   #include <string.h>
8.                   #include <winsock.h>
9.                    
10.               #define BACKLOG_MAX 5
11.               #define BUFFER_SIZE 128
12.               #define EXIT_CALL_STRING "#quit"
13.                
14.               int local_socket = 0;
15.               int remote_socket = 0;
16.                
17.               int remote_length = 0;
18.               int message_length = 0;
19.                
20.               unsigned short local_port = 0;
21.               unsigned short remote_port = 0;
22.                
23.               char message[BUFFER_SIZE];
24.                
25.               struct sockaddr_in local_address;
26.               struct sockaddr_in remote_address;
27.                
28.               WSADATA wsa_data;
29.                
30.               /* Exibe uma mensagem de erro e termina o programa */
31.               void msg_err_exit(char *msg)
32.               {
33.                   fprintf(stderr, msg);
34.                   system("PAUSE");
35.                   exit(EXIT_FAILURE);
36.               }
37.                
38.               int main(int argc, char **argv)
39.               {
40.                   // inicia o Winsock 2.0 (DLL), Only for Windows
41.                   if (WSAStartup(MAKEWORD(2, 0), &wsa_data) != 0)
42.                       msg_err_exit("WSAStartup() failed\n");
43.                
44.                   // criando o socket local para o servidor
45.                   local_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
46.                   if (local_socket == INVALID_SOCKET)
47.                   {
48.                       WSACleanup();
49.                       msg_err_exit("socket() failed\n");
50.                   }
51.                
52.                   printf("Porta local: ");
53.                   scanf("%d", &local_port);
54.                   fflush(stdin);
55.                
56.                   // zera a estrutura local_address
57.                   memset(&local_address, 0, sizeof(local_address));
58.                
59.                   // internet address family
60.                   local_address.sin_family = AF_INET;
61.                
62.                   // porta local
63.                   local_address.sin_port = htons(local_port);
64.                
65.                   // endereco
66.                   local_address.sin_addr.s_addr = htonl(INADDR_ANY); // inet_addr("127.0.0.1")
67.                
68.                   // interligando o socket com o endereço (local)
69.                   if (bind(local_socket, (struct sockaddr *) &local_address, sizeof(local_address)) == SOCKET_ERROR)
70.                   {
71.                       WSACleanup();
72.                       closesocket(local_socket);
73.                       msg_err_exit("bind() failed\n");
74.                   }
75.                
76.                   // coloca o socket para escutar as conexoes
77.                   if (listen(local_socket, BACKLOG_MAX) == SOCKET_ERROR)
78.                   {
79.                       WSACleanup();
80.                       closesocket(local_socket);
81.                       msg_err_exit("listen() failed\n");
82.                   }
83.                
84.                   remote_length = sizeof(remote_address);
85.                
86.                   printf("aguardando alguma conexao...\n");
87.                   remote_socket = accept(local_socket, (struct sockaddr *) &remote_address, &remote_length);
88.                   if(remote_socket == INVALID_SOCKET)
89.                   {
90.                       WSACleanup();
91.                       closesocket(local_socket);
92.                       msg_err_exit("accept() failed\n");
93.                   }
94.                
95.                   printf("conexao estabelecida com %s\n", inet_ntoa(remote_address.sin_addr));
96.                   printf("aguardando mensagens...\n");
97.                   do
98.                   {
99.                       // limpa o buffer
100.                   memset(&message, 0, BUFFER_SIZE);
101.            
102.                   // recebe a mensagem do cliente
103.                   message_length = recv(remote_socket, message, BUFFER_SIZE, 0);
104.                   if(message_length == SOCKET_ERROR)
105.                       msg_err_exit("recv() failed\n");
106.            
107.                   // exibe a mensagem na tela
108.                   printf("%s: %s\n", inet_ntoa(remote_address.sin_addr), message);
109.               }
110.               while(strcmp(message, EXIT_CALL_STRING)); // sai quando receber um "#quit" do cliente
111.            
112.               printf("encerrando\n");
113.               WSACleanup();
114.               closesocket(local_socket);
115.               closesocket(remote_socket);
116.            
117.               system("PAUSE");
118.               return 0;
119.           }

 

WSAStartup()

A função WSAStartup() inicia o Windows Sockets Dynamic Link (WinSock DLL). Ela também é usada para confirma a versão do WinSock DLL. Estamos utilizando a 2.0.

Declaração:

1.                   int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

 

O parâmetro wVersionRequested é número da maior versão que a aplicação pode usar. Repare que ele é uma WORD onde o byte de maior ordem especifica a número da minor version e o byte de ordem menor indica a major version. Por isso eu utilizo a função MAKEWORD para retornar uma WORD (e facilitar minha vida), por exemplo, se quiser usar a versão do WinSock 1.1 é só alterar para MAKEWORD(1, 1). Fácil demais...

O parâmetro lpWSAData é um ponteiro para um estrutura WSADATA que receberá os detalhes da implementação do WinSock.

Se der tudo certo a função retornará 0. Se ocorrer um erro você pode usar a função WSAGetLastError() para saber mais detalhes.

socket()

Use a função socket() parar criar o socket \o/.

Declaração:

1.                   SOCKET WSAAPI socket(int af, int type, int protocol);

 

O parâmetro af especifica o "address family" que este socket usa. Nós iremos usar o AF_INET. A versão 1.1 do WinSock só suporta o formato AF_INET.

Alguns formatos possíveis:
AF_INET: Arpa Internet Protocols
AF_UNIX: Unix Internet Protocols
AF_ISSO: Iso Protocols
AF_NS: Xerox Network System Protocols

Se você olhar no header (winsock.h) vai encontrar outros como oAF_FIREFOX, mas é melhor ver quais realmente podem ser usados no MSDN.

O parâmetro type especifica o tipo do socket, no nosso caso é SOCK_STREAM(utilizado na maioria dos programas), mas você pode usar o SOCK_DGRAM(UDP), mas não vou abordar as diferenças entre eles.

Em protocol deixe 0 (zero), isto indica que o socket irá usar o valor padrão. Oprotocol pode ser o padrão porque a combinação doAF_INET+SOCK_STREAM já indica que o protocolo é TCP. Eu coloqueiIPPROTO_TCP no código, mas não precisa, é só deixar como 0.

Alguns valores possíveis para o protocol:
IPPROTO_IP: Internet Protocol (0)
IPPROTO_ICMP: Internet Control Message Protocol (1)
IPPROTO_IGMP: Internet Group Multicast Protocol (2)
IPPROTO_GGP: Gateway-Gateway Protocol (3)
IPPROTO_TCP: Transmission Control Protocol (6)
IPPROTO_UDP: User Datagrama Protocol (17)

Se der tudo certo a função irá retornar o socket descriptor, que é um número que identifica o socket criado. Se falhar irá retorna INVALID_SOCKET, você pode usar a função WSAGetLastError() para saber mais detalhes do erro.

bind()

Agora precisamos dar nomes aos bois, precisamos dizer que o nosso socket se chama "192.168.0.1", por exemplo. Usaremos a função bind() para isso.

Declaração:

1.                   int bind(SOCKET s, const struct sockaddr *addr, int namelen);

 

O parâmetro s é o socket descriptor retornado pela função socket(). O parâmetro addr é um ponteiro para uma estrutura do tipo sockaddr. Onamelen é o tamanho em bytes da estrutura, use a função sizeof() para isto.

Esta parte pode confundir, antes de usar a função bind() nós precisamos conhecer as estruturas sockaddr, sockaddr_in e in_addr, mas a que iremos utilizar é a sockaddr_in.

Definições da estrutura sockaddr:

1.                   struct sockaddr
2.                   {
3.                       u_short sa_family; // address family
4.                       char sa_data[14]; // address
5.                   };

 

O item sa_data da estrutura vai depende do "address family". No WinSock 1.1, como eu disse, apenas o AF_INET é suportado, logo somente um "endereçamento de internet" é suportado no sa_data. Lembrando que você não vai utilizar esta estrutura, mas eu coloquei porque ela aparece em um "cast" de um dos parâmetros da função bind() que estamos utilizando. Você deve utilizar uma outra estrutura no lugar da sockaddr quando chamar a função bind(), use a sockaddr_in.

Definições da estrutura sockaddr_in:

1.                   struct sockaddr_in
2.                   {
3.                       short sin_family; // address family
4.                       u_short sin_port; // port
5.                       struct in_addr sin_addr; // internet address
6.                       char sin_zero[8]; // to make a beautiful cast
7.                   };

 

Primeiro você deve zerar a estrutura e depois preencher alguns itens. No código que eu fiz está assim:

1.                   struct sockaddr_in local_address;
2.                   /* ... */
3.                   memset(&local_address, 0, sizeof(local_address)); // zera a estrutura
4.                    
5.                   local_address.sin_family = AF_INET;
6.                   local_address.sin_port = htons(local_port);
7.                   local_address.sin_addr.s_addr = htonl(INADDR_ANY);

 

Importante!
Nem todos os computadores armazenam os dados na mesma ordem na memória. Existem computadores que trabalham no formato "Big Endian", onde byte menos significativo fica no endereço de memória de maior valor. Por exemplo, para armazenar o número 0x01234567 ficaria assim:
endereço 0x101: 01
endereço 0x102: 23
endereço 0x103: 45
endereço 0x104: 67

O maior endereço é o 0x104 e o byte menos significativo de 0x01234567 é o último da direita (67).

Já nos computadores "Little Endian", ocorre o inverso, o byte menos significativo fica no endereço de memória de menor valor:
endereço 0x101: 67
endereço 0x102: 45
endereço 0x103: 23
endereço 0x104: 01

O menor endereço é o 0x101 e o byte menos significativo de 0x01234567 é o último da direita (67).

Os computadores com processadores baseados no Intel x86 são "Little Endian", mas a ordem dos bytes na rede (network) é "Big Endian". Precisamos converter os dados antes de enviá-los usando as funções htons() e htonl():
htons(): converte um unsigned short (host-to-network)
htonl(): converte um unsigned long (host-to-network)

Existem outras funções para esta converter os dados, mas nós iremos utilizar apenas esses.

Voltando a estrutura sockaddr_in, vamos falar do item sin_addr. Veja na declaração que este item é uma estrutura do tipo in_ddr.

Definições da estrutura in_addr:

1.                   struct in_addr
2.                   {
3.                       union
4.                       {
5.                           struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
6.                           struct { u_short s_w1,s_w2; } S_un_w;
7.                           u_long S_addr;
8.                       } S_un;
9.                       #define s_addr S_un.S_addr
10.                   #define s_host S_un.S_un_b.s_b2
11.                   #define s_net S_un.S_un_b.s_b1
12.                   #define s_imp S_un.S_un_w.s_w2
13.                   #define s_impno S_un.S_un_b.s_b4
14.                   #define s_lh S_un.S_un_b.s_b3
15.               };

 

Repare nos defines que ela possui, você pode acessar o dados da estrutura através deles. Quando você acessa local_address.sin_addr.s_addr na verdade esta acessando local_address.sin_addr.S_un.S_addr. Mas sempre use os defines para manter compatibilidade, não acesse os dados diretamente.

O valor indicado para local_address.sin_addr.s_addr éhtonl(INADDR_ANY), isto indica que usaremos todos os endereços locais designados ao nosso servidor. Por exemplo, se sua máquina possui duas placas de rede (192.168.0.1 e 192.168.0.2), você pode usar os dois endereços para o seu socket. Mas se você quiser especificar apenas uma placa, use a função inet_addr(). Nós usaremos esta função para fazer o programa cliente.

Sobre o item sin_zero da estrutura sockaddr_in, ele server apenas para ocupar espaço, de modo que a estrutura tenha o mesmo tamanho (em bytes) da estrutura sockaddr. Assim podemos fazer "casts" de uma estrutura para outra (utilizado no parâmetro da função bind()). Por isso a estrutura deve ser preenchida com zeros antes de ser utilizada.

Se der tudo certo a função bind() retornará 0, caso contrário retornaráSOCKET_ERROR, use o WSAGetLastError() para analisar o erro.

listen()

A função listen() coloca o socket para escutar as conexões. Seria como ligar o socket, agora ele pode receber por conexões dos clientes.

Declaração:

1.                   int listen(SOCKET s, int backlog);

 

O parâmetro s é o socket descriptor que você já conhece. O backlog indica quantas conexões pendentes o socket pode deixar na fila para serem processadas, quando as conexões são aceitas elas são removidas da fila. O mínimo para o backlog é 1 e o máximo 5, mas não achei alguma coisa no site do MSDN que indicasse que o limite é sempre 5.

Se tudo der certo ele vai retornar 0, caso contrário ele retornará umSOCKET_ERROR. Novamente você pode usar a função WSAGetLastError()para analisar o erro se quiser.

accept()

O accept() aceita a conexão quando ela é detectada.

Declaração:

1.                   SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);

 

O parâmetro s é o socket descriptor. O parâmetro addr é um ponteiro para uma estrutura do tipo sockaddr (igual ao do função bind()), nela a função irá armazenar a o endereço (estrutura) da entidade (cliente) que requisitou a conexão. Você não precisa armazenar este valor se não quiser obter informações do cliente, pode colocar apenas um NULL no lugar.

O parâmetro addrlen é um ponteiro onde a função irá colocar o tamanho em bytes da estrutura addr recebida do cliente. Se você passar um NULL para o parâmetro addr então o addrlen retornará NULL para o ponteiro. Você também pode apenas colocar um NULL no parâmetro do addrlen.

Repare que o programa ficará esperando por uma conexão, ele ficará meio travado enquanto isso. Você terá que fazer ele rodar de forma assíncrona, mas isso não é importante agora, primeiro temos que fazer o servidor e o cliente funcionarem.

A função accept() irá retornar um socket descriptor do cliente. Se der algum problema será retornado um INVALID_SOCKET. Novamente use oWSAGetLastError() para verificar os erros.

recv()

O recv() recebe os dados de uma conexão.

Declaração:

1.                   int recv(SOCKET s, char* buf, int len, int flags);

 

O parâmetro s é o socket descriptor do cliente. O parâmetro buf é o buffer onde serão armazenadas as informações recebidas. O len é o tamanho deste buffer.

O parâmetro flags indica o modo como serão recebidos os dados, eu deixei como 0 (zero) assim ele não fará nenhuma ação especial. Para mais detalhes sobre outras opções olhe no site MSDN.

O recv() irá retornar o total de bytes recebidos, mas você não irá receber mais bytes do que especificou em len. Ele retorna 0 (zero) quando a conexão é fechada normalmente. Se der algum erro ele retorna um SOCKET_ERROR. Use o WSAGetLastError() para saber mais do erro.

Repare que eu também usei a função inet_ntoa() para exibir junto com a mensagem. Essa função converte um endereço de Internet (IPv4) em uma string do endereço formatado nos padrões de Internet com pontos decimais. Ela já retorna em "Little Endian".

O recv() está dentro de um loop, antes de receber uma mensagem o buffer (variável message no código) deverá ser limpo. Após receber a mensagem, ela é exibida. O loop só pára quando o cliente enviar uma mensagem "#quit". Não é a melhor maneira de ser terminar o programa, mas serve por enquanto.

Depois de tudo pronto, nós devemos finalizar a aplicação com as funçõesWSACleanup() e closesocket(). Você só precisa olhar o código para entender como usa-las. E é isso, o micro-nano-humilde-servidor tá pronto agora só falta fazer o cliente.

Criando cliente que envia as mensagens

 

1.                   /*
2.                       CLIENTE
3.                   */
4.                    
5.                   #include <stdio.h>
6.                   #include <stdlib.h>
7.                   #include <string.h>
8.                   #include <winsock.h>
9.                    
10.               #define BUFFER_SIZE 128
11.               #define EXIT_CALL_STRING "#quit"
12.                
13.               int remote_socket = 0;
14.               int message_length = 0;
15.                
16.               unsigned short remote_port = 0;
17.                
18.               char remote_ip[32];
19.               char message[BUFFER_SIZE];
20.                
21.               struct sockaddr_in remote_address;
22.                
23.               WSADATA wsa_data;
24.                
25.               /* Exibe uma mensagem de erro e termina o programa */
26.               void msg_err_exit(char *msg)
27.               {
28.                   fprintf(stderr, msg);
29.                   system("PAUSE");
30.                   exit(EXIT_FAILURE);
31.               }
32.                
33.               int main(int argc, char **argv)
34.               {
35.                   if (WSAStartup(MAKEWORD(2, 0), &wsa_data) != 0)
36.                       msg_err_exit("WSAStartup() failed\n");
37.                
38.                   printf("IP do servidor: ");
39.                   scanf("%s", remote_ip);
40.                   fflush(stdin);
41.                
42.                   printf("Porta do servidor: ");
43.                   scanf("%d", &remote_port);
44.                   fflush(stdin);
45.                
46.                   remote_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
47.                   if (remote_socket == INVALID_SOCKET)
48.                   {
49.                       WSACleanup();
50.                       msg_err_exit("socket() failed\n");
51.                   }
52.                
53.                   // preenchendo o remote_address (servidor)
54.                   memset(&remote_address, 0, sizeof(remote_address));
55.                   remote_address.sin_family = AF_INET;
56.                   remote_address.sin_addr.s_addr = inet_addr(remote_ip);
57.                   remote_address.sin_port = htons(remote_port);
58.                
59.                   printf("conectando ao servidor %s...\n", remote_ip);
60.                   if (connect(remote_socket, (struct sockaddr *) &remote_address, sizeof(remote_address)) == SOCKET_ERROR)
61.                   {
62.                       WSACleanup();
63.                       msg_err_exit("connect() failed\n");
64.                   }
65.                
66.                   printf("digite as mensagens\n");
67.                   do
68.                   {
69.                       // limpa o buffer
70.                       memset(&message, 0, BUFFER_SIZE);
71.                
72.                       printf("msg: ");
73.                       gets(message);
74.                       fflush(stdin);
75.                
76.                       message_length = strlen(message);
77.                
78.                       // envia a mensagem para o servidor
79.                       if (send(remote_socket, message, message_length, 0) == SOCKET_ERROR)
80.                       {
81.                           WSACleanup();
82.                           closesocket(remote_socket);
83.                           msg_err_exit("send() failed\n");
84.                       }
85.                   }
86.                   while(strcmp(message, EXIT_CALL_STRING)); // sai quando enviar um "#quit" para o servidor
87.                
88.                   printf("encerrando\n");
89.                   WSACleanup();
90.                   closesocket(remote_socket);
91.                
92.                   system("PAUSE");
93.                   return 0;
94.               }

 

Essa parte vai ser mais rápida, pois já vimos o básico sobre sockets no servidor.

Você deve criar um socket no cliente para poder conectar ao servidor. Ele precisa preencher uma estrutura do tipo socketaddr_in (que nós já vimos) com as informações do servidor, olhe o código para entender melhor.

connect()

A função connect() irá conectar o cliente com o servidor.

Declaração:

1.                   int connect(SOCKET s, const struct sockaddr* name, int namelen);

 

Ele recebe parâmetros iguais ao da função bind(). Se der algum erro ele retorna um SOCKET_ERROR.

A função connect() tem o mesmo problema da função accept(), enquanto ele estiver tentando conectar ficará travado (precisa ser assíncrono), mas não vou mostra como se faz isso, fica como lição de casa XD.

send()

E o send() é a função responsável por enviar informações ao servidor.

Declaração:

1.                   int send(SOCKET s, const char* buf, int len, int flags);

 

Os parâmetro que ele recebe também são iguais ao da função recv(). Osend() retonará o número de bytes enviados, esse número não será maior dolen (e ele não enviará mais o que o especificado em len). Se der um erro ele retornará SOCKET_ERROR. Use o WSAGetLastError() para mais informações.

Lembre-se de limpar o buffer de mensagens, senão você irá enviar "lixo" para o servidor.

Pronto!

Compile os dois programas, depois execute primeiro o servidor depois o cliente. Se estiver rodando na mesma máquina você pode conectar o cliente no IP "127.0.0.1" que dará no mesmo.