Usando Socket com C – Parte 1: Servidor

por Fabio A. Mazzarino

A biblioteca boost::Asio é uma ótima opção para networking no C++. Mas em C o mais popular é o socket mesmo. Vamos aprender algumas tarefas básicas com socket neste, e nos próximos posts.

Nestes exemplos vamos utilizar sistema operacional linux.

Como Funciona?

Antes de mais nada vamos ver um diagrama de sequência pra entender como funciona uma comunicação TCP/IP no unix:

A comunicação toda começa com o servidor criando um socket, utilizando a função bind() pra configurá-lo, configurando o socket para ligá-lo a uma porta com listen(), e chamando a função accept() para aguardar a conexão a partir do cliente. A função accept() é blocante, e vai bloquear todo o processamento até que uma conexão seja solicitada pelo cliente.

Do lado do cliente, cria-se um socket, e já utilizamos a função connect() para configurá-lo e efetuar a conexão.

Uma vez os dois conectados utiliza-se as funções read() e write() para a comunicação.

Na Prática

Como exemplo vamos fazer um servidor de echo. Um servidor de echo é meramente um servidor de teste, tudo que for enviado para o servidor é enviado de volta, simples assim. Segue o código, e a explicação na sequência:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>


int main(int argc, char* argv[]) {
    int serversocket = 0;
    struct sockaddr_in serveraddr;

    /* prepare server socket */
    if ((serversocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("Cannot create socket\n\n");
        exit(1);
    };
    bzero((void*)&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(5000);
    bind(serversocket, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    listen(serversocket, 10);
    
/* accept loop */
    for (; ; ) {
        struct sockaddr_in clientaddr;
        int clientaddrsize = sizeof(clientaddr);
        int clientsocket = 0;
        int childpid = 0;
        char buffer[128];
        int nread = 0;
        int nwritten = 0;
        
        bzero((void*)&clientaddr, sizeof(clientaddr));
        printf("Accepting connection on port 5000\n"); 
        if ((clientsocket = accept(serversocket, (struct sockaddr*)&clientaddr, &clientaddrsize)) == -1) {
            printf("Failure accepting connection. Skipping\n");
            continue;
        }
        printf("Incomming connection...\n");

        /* fork to accept more connections */
        childpid = fork();
        if (childpid != 0) {
            /* parent keep accepting connections */
            close(clientsocket);
            continue;
        }

        /* child read data from socket */
        bzero((void*)buffer, sizeof(buffer));
        if ((nread = read(clientsocket, buffer, sizeof(buffer) - 1)) == -1) {
            printf("Failure receiving data. Closing connection");
            close(clientsocket);
            exit(1);
        }
        
/* echo back data read from socket */
        printf("Echoing back %s\n", buffer);
        nwritten = write(clientsocket, buffer, strlen(buffer));
        if (nwritten == -1) {
            printf("Failure sending data. Closing connection");
            close(clientsocket);
            exit(1);
        }
        close(clientsocket);
        exit(0);
    }
}

Primeiro precisamos criar um socket aonde definimos qual é a forma de conexão, AF_INET indica que vamos utilizar uma conexão IPv4, e SOCK_STREAM uma conexão TCP. Em seguida temos que preencher a struct sockaddr_in com os dados de tipo de conexão, endereço de IP que irá aceitar conexão e em qual porta (nesse caso 5000), essa estrutura vai servir pra fazer o bind entre socket e porta, com a função bind(). A função listen() vai reservar o socket criado como passivo, ou seja, como servidor; o segundo parâmetro de listen() é a quantidade de conexões simultâneas que o socket vai enfileirar, em breve vamos saber porque somente 10 é mais que suficiente.

Em seguida um loop infinito para receber as conexões. A função accept() bloqueia o processamento a espera de uma nova conexão, então o código vai ficar parado nessa função até que chegue alguma solicitação de conexão TCP/IP na porta indicada, 5000.

Agora vem uma dica bacana pra lidar com o bloqueio do accept(). Como o accept() é blocante, não dá pra processar a requisição enquanto está aceitando conexões, e também não dá pra aceitar conexões enquanto está processando. Enquanto estamos processando as novas conexões vão ficar esperando na fila de tamanho indicado na função listen(). Para evitar que essa fila fique muito grande, e para reduzir o tempo de responsa do servidor, basta fazer um fork, o processo pai termina o loop e volta pro accept(), enquanto que o filho continua o processamento e quando terminar dá um exit(0).

Agora ficou fácil, basta um read() pra recuperar os dados do socket, e um write() pra enviar os dados de volta. Não pode esquecer de fechar o socket e finalizar o filho.

Testando o Servicor

Para testar vamos utilizar um cliente telnet, basta executá-lo pela linha de comando:

$ telnet localhost 5000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
abc
abc
Connection closed by foreign host.

A entrada digitada foi simplesmente abc, em seguida o servidor enviou de volta a entrada.

No próximo post vamos abordar o lado do cliente.