Servidor de Eco

Servidores, tipicamente, ficam à disposição em tempo integral para que os usuários, através de clientes, usem o serviço em qualquer momento e quantas vezes for necessário. O Hello, World! do exemplo anterior não tem essa característica. Ele encerra a conexão e o serviço como um todo, logo depois de atender o cliente. Neste exemplo, veremos um pequeno servidor que implementa o típico loop de um serviço de rede.

Este exemplo veremos um servidor de Eco. Trata-se de um exemplo clássico do ensino de programação em rede. O servidor deve aceitar conexões de clientes e, uma vez estabelecida a conexão, passa a ecoar (repetir de volta) todas as mensagens que receber do cliente. Adicionarei apenas dois pequenos detalhes. Nosso servidor deve devolver as mensagens em caixa alta e deve se desconectar quando receber a mensagem tchau. Dessa forma, o servidor pode ser liberado para atender a novos clientes. Abaixo, segue o código completo do servidor. Em seguida, um pequeno vídeo em que você pode ver como o servidor de Eco funciona (na parte de cima, o servidor; abaixo, dois clientes). Em seguida, o código completo do servidor, seguido de alguns comentários.

import socket

# 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]))

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

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

Vídeo Servidor de Eco

Se você entendeu o exemplo anterior, certamente este exemplo será facílimo de entender. Há duas novidades apenas: primeiro, o uso de dois laços while que, embora não precisem de explicações do ponto de vista de linguagem, merecem uma observação relativa a seu uso em servidores; segundo, o uso do método recv().

Um servidor tipicamente requer um laço principal responsável por “ouvir” e aceitar novas conexões. Esse laço é responsável por fazer o servidor prover o serviço continuamente. A ideia é que, tão logo seja possível, o servidor volte a dizer ao sistema que está disponível para atender uma nova conexão (lembre que isso é feito pelo método accept()). O sistema, então, deverá estabelecer uma nova conexão com um dos pedidos que estiver em fila ou, se não houver, deve bloquear o servidor. É esse o papel do laço while mais externo.

O segundo laço (o mais interno) tem outro papel. Ao ser executado, já há uma conexão estabelecida com um cliente. Esse laço, portanto, tem o papel de manter a interação com o cliente enquanto durar a conexão. No caso do servidor de eco, a interação consiste em receber as mensagens e ecoá-las. Em um código real, é comum que a tal tipo de laço seja colocado em uma função separada, para garantir a separação de responsabilidades. No código mais abaixo, faço essa adaptação que também nos será útil para o próximo exemplo.

Uma observação importante aqui. O servidor só pode atender a vários clientes de forma sequencial: um depois do outro. E isso só ocorre porque o sistema operacional coloca os pedidos de conexão em fila e os entrega um a um ao processo, sempre que faz um accept(). O servidor, da forma como está escrito, não tem como atender mais de um cliente por vez. Isso pode ser percebido no vídeo acima. Veja que o segundo cliente só começa a ser atendido (e estabelecer a conexão) depois que o primeiro cliente desconecta. No próximo exemplo, trararemos dessa questão.

Finalmente, há o método recv() cujo papel que é fácil de compreender. É esse método que permite ler dados de um socket. Dois detalhes merecem destaque. Primeiro, o método recebe um inteiro como parâmetro, para indicar o tamanho máximo (em bytes) da mensagem a ser lida do socket. Observe que neste nosso pequeno exemplo o tamanho é irrelevante. Contudo, isso ajuda a ressaltar que a conexão TCP é um stream de bytes e que não há uma relação um para um entre mensagens enviadas e mensagens recebidas. Cabe ao protocolo de comunicação acima de TCP determinar como o stream de bytes deve ser dividido em mensagens. Em nosso servidor, simplesmente assumiremos que podemos ler blocos de tamanho menor que 1024 bytes. Na prática, isso está longe de ser adequado.