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.
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()

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
argsrecebe 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,).