Servidor de Eco Multi-Threads

O servidor de Eco do exemplo anterior é capaz de atender vários clientes, mas apenas de forma sequencial. Quando há mais de uma tentativa de conexão simultânea, o sistema operacional mantém os pedidos de conexão em uma fila até que o servidor aceite outra conexão (via método accept()). Na prática, contudo, é quase sempre desejável que um servidor possa se conectar e atender a múltiplos clientes simultaneamente. Este exemplo demonstra como _threads são tipicamente usadas em servidores para este fim.

O Código

Abaixo o código completo do servidor, seguido de um pequeno vídeo que demonstra seu funcionamento. Observe que o código deste exemplo foi dividido em duas funções: main() e atende_cliente(). Há dois motivos para essa decisão. Primeiro, há melhor separação de responsabilidades dessa forma, dado que cada uma das funções tem um papel claro e independente. E, segundo, porque funções facilitam a adoção de threads, como veremos mais adiante. Agora, leia e tente entender o código abaixo e só depois passe aos comentários que se seguem.

import socket
from threading import Thread

def atende_cliente(client, num_cliente):
    print('atendendo cliente...')
    # 4. usar e 5. fechar a conexão
    client.send('Servidor de eco (cliente {}).\n'.format(num_cliente).encode('utf-8'))
    while True:
        mensagem = client.recv(1024).decode('utf-8')
        if not mensagem: break
        client.send(mensagem.upper().encode('utf-8'))
        if mensagem.strip() == "tchau": break
    client.close()


def main():
    # 1. criar um socket
    listen_socket = socket.socket()

    # 2. configurar o socket como servidor
    porta = 9090
    listen_socket.bind(('localhost', porta))
    listen_socket.listen()

    clientes = 0
    while True:
        # 3. aguardar conexões
        print('Aguarda conexão na porta {}'.format(porta))
        client_socket, endereco = listen_socket.accept()
        clientes += 1
        print('Conexão estabelecida de {}:{}'.format(endereco[0], endereco[1]))
        Thread(target=atende_cliente, args=(client_socket, clientes)).start()

    # 5. fechar o listening socket
    listen_socket.close()

main()

Servidor de Eco com Threads

Comentários

Como mencionei antes, a decomposição em funções permite que usemos threads de forma clara e controlada. A função atende_cliente() isola a lógica de atendimento à conexão do cliente. Perceba que o corpo dessa função é, essencialmente, o corpo do laço mais interno do código do servidor anterior. Essa separação isola e deixa mais clara também a responsabilidade do laço principal que é tratar da aceitação de novas conexões. A função main(), portanto, reúne a a responsabilidade de criar e configurar o socket e a de executar o laço de aceitação e despacho de novas conexões.

É importante ressaltar que a função atende_cliente() também isola a porção de código que é executada pelas threads. Observe que todas as threads criadas executam a mesma função. Contudo, cada uma delas recebe um client_socket diferente e, portanto, não deve haver qualquer problema de concorrência.

Observe ainda como a biblioteca de threads de Python foi usada. A segunda linha do código importa apenas a classe Thread do pacote de threads da API. Essa classe nos permite criar threads com facilidade. Basta instanciar a classe, passando-lhe a função a ser executada e invocar o método start(), quando quisermos que a thread inicie a execução. Por simplicidade, neste código isto é feito em uma única linha, sem que precisemos guardar a referência da thread (isto permite que seja garbage collected depois que terminar sua execução).

Thread(target=atende_cliente, args=(client_socket, clientes)).start()

Observação. Perceba que o keyword argument args recebe uma tupla ((client_socket, clientes)) como argumento. Se a função target receber apenas um argumento, lembre que a notação exigiria uma vírgula após o elemento, para diferenciar a notação de um simples parêntesis: args=(client_socket,).