Pular para o conteúdo principal
InicioTutoriaisPython

Um guia completo para a programação de soquetes em Python

Aprenda os fundamentos da programação de soquetes em Python
abr. de 2024  · 41 min leer

A conexão de dispositivos para troca de informações é o objetivo da rede. Os soquetes são uma parte essencial da comunicação de rede eficaz, pois são o conceito subjacente usado para transmitir mensagens entre dispositivos em redes locais ou globais e processos diferentes na mesma máquina. Eles fornecem uma interface de baixo nível que permite um controle refinado sobre o tráfego que deve ser enviado ou recebido.

Essa natureza de baixo nível possibilita a criação de canais de comunicação de alto desempenho (ou protocolos personalizados) para casos de uso específicos com baixa sobrecarga que pode estar presente nos protocolos tradicionais, que são construídos sobre a comunicação de soquete.

É isso que torna os soquetes excepcionalmente úteis em aplicativos cliente-servidor em tempo real que dependem da troca instantânea de mensagens ou operam com grandes quantidades de dados.

Neste artigo, abordaremos os conceitos básicos da programação de soquetes e forneceremos um guia passo a passo para a criação de aplicativos cliente e servidor baseados em soquetes usando Python. Então, sem mais delongas, vamos mergulhar de cabeça!

Noções básicas de rede

A rede permite a comunicação e o compartilhamento de informações de qualquer tipo.

É um processo de conexão de dois ou mais dispositivos para permitir a troca de informações. Um conjunto desses dispositivos interconectados é chamado de rede.

Há muitas redes que podemos observar em nosso mundo físico: redes de linhas aéreas ou de energia elétrica ou cidades interconectadas umas às outras por meio de rodovias são alguns bons exemplos.

Da mesma forma, há várias redes na tecnologia da informação; a mais proeminente e conhecida delas é a Internet, a rede global de redes que conecta uma infinidade de dispositivos e a que você provavelmente está usando agora para ler este artigo.

Tipos de redes

A Internet contém muitas outras redes, que diferem em escala ou em outras propriedades, dentro dela mesma: por exemplo, redes locais (LANs), que normalmente conectam computadores localizados próximos uns dos outros. Máquinas em empresas ou outras instituições (bancos, universidades, etc.) ou até mesmo seus dispositivos domésticos conectados a um roteador formam uma rede desse tipo.

Há também tipos maiores ou menores de redes, como PANs (personal area network, rede de área pessoal), que podem ser simplesmente seu smartphone conectado a um laptop via Bluetooth, MANs (metropolitan area network, rede de área metropolitana), que podem interconectar dispositivos em toda a cidade, e WANs (wide area network, rede de área ampla), que podem cobrir países inteiros ou o mundo inteiro. E, sim, a maior rede WAN é a própria Internet.

Não é preciso dizer que as redes de computadores podem ser muito complexas e consistem em muitos elementos. Um dos primitivos mais básicos e cruciais é um protocolo de comunicação.

Tipos de protocolos de comunicação de rede

Os protocolos de comunicação especificam as regras de como e em que formato as informações devem ser enviadas e recebidas. Esses protocolos são reunidos em uma hierarquia para gerenciar as várias tarefas envolvidas na comunicação de rede.

Em outras palavras, alguns protocolos lidam com a forma como o hardware recebe, envia ou encaminha pacotes, enquanto outros são de mais alto nível e se preocupam, por exemplo, com a comunicação no nível do aplicativo etc.

Alguns protocolos de comunicação de rede comumente usados e amplamente conhecidos incluem:

Wi-Fi

Um exemplo de protocolo de camada de link, o que significa que ele fica muito próximo ao hardware e é responsável pelo envio físico de dados de um dispositivo para outro em um ambiente sem fio.

IP (Protocolo de Internet)

O IP é um protocolo de camada de rede responsável principalmente pelo roteamento de pacotes e pelo endereçamento IP.

TCP (Protocolo de Controle de Transmissão)

Um protocolo confiável e orientado à conexão que fornece comunicação full duplex e garante a integridade e o fornecimento de dados. Esse é um protocolo de camada de transporte que gerencia conexões, detecta erros e controla o fluxo de informações.

UDP (Protocolo de datagrama do usuário)

Um protocolo do mesmo conjunto de protocolos que o TCP. A principal diferença é que o UDP é um protocolo sem conexão mais simples, rápido, mas não confiável, que não executa nenhuma verificação de entrega e segue o paradigma de "disparar e esquecer". Assim como o TCP, o UPD também está localizado na camada de transporte.

HTTP (Protocolo de transferência de hipertexto)

Um protocolo de camada de aplicativo e o protocolo mais comumente usado para comunicação entre navegador e servidor na Web, usado para atender a sites em particular. Não é preciso dizer que este artigo que você está lendo agora também foi fornecido via HTTP. O protocolo HTTP se baseia no TCP e gerencia e transfere informações relevantes para aplicativos da Web, como cabeçalhos, que são usados para transferir metadados e cookies, diferentes métodos HTTP (GET, POST, DELETE, UPDATE) etc.

MQTT (Transporte de Telemetria de Fila de Mensagens)

Outro exemplo de protocolo de nível de aplicativo usado para dispositivos com capacidade de processamento e duração de bateria limitadas, operando em condições de rede não confiáveis (por exemplo, sensores de gás em um local de mineração ou simplesmente uma lâmpada inteligente em sua casa). O MQTT é um protocolo de mensagens padrão usado na IoT (Internet das Coisas). Ele é leve e simples de usar, projetado com mecanismos de retransmissão integrados para aumentar a confiabilidade. Se estiver interessado em usar esse protocolo com Python, você pode ler este guia Python MQTT que fornece uma visão geral detalhada do cliente Paho MQTT.

Uma observação importante é que todos os protocolos mencionados acima usam soquetes, mas acrescentam sua própria lógica e processamento de dados. Isso se deve ao fato de os soquetes serem uma interface de baixo nível para qualquer comunicação de rede em dispositivos modernos, conforme discutiremos na próxima seção.

Principais conceitos e termos

É claro que há muitos outros conceitos e termos importantes usados no contexto das redes. Aqui está um resumo rápido de alguns dos mais importantes que podem surgir no restante do tutorial:

  • Pacote: uma unidade padrão de transmissão de dados em uma rede de computadores (pode-se compará-la coloquialmente ao termo "mensagem").
  • Endpoint: um destino onde os pacotes chegam.
  • Endereço IP: um identificador numérico que identifica exclusivamente um dispositivo na rede. Um exemplo de endereço IP é: 192.168.0.0
  • Portas: um identificador numérico que identifica exclusivamente um processo que está sendo executado em um dispositivo e lida com comunicações de rede específicas: por exemplo, ele serve seu site por HTTP. Enquanto um endereço IP identifica o dispositivo, uma porta identifica o aplicativo (todo aplicativo é um processo ou consiste em processos). Alguns exemplos de portas bem conhecidas são: a porta 80, que é convencionalmente usada por aplicativos de servidor para gerenciar o tráfego HTTP, e a porta 443 para HTTPS (HTTP seguro).
  • Gateway: um tipo especial de nó de rede (dispositivo) que serve como ponto de acesso de uma rede para outra. Essas redes podem até usar protocolos diferentes, portanto, pode ser necessário que o gateway execute alguma tradução de protocolo. Um exemplo de gateway pode ser um roteador que conecta uma rede local doméstica à Internet.

Entendendo os soquetes

O que é um soquete?

Um soquete é uma interface (porta) para comunicação entre diferentes processos localizados na mesma máquina ou em máquinas diferentes. No último caso, estamos falando de soquetes de rede.

Os soquetes de rede abstraem o gerenciamento de conexões. Você pode pensar neles como manipuladores de conexão. Nos sistemas Unix, em particular, os soquetes são simplesmente arquivos que suportam as mesmas operações de gravação e leitura, mas enviam todos os dados pela rede.

Quando um soquete está no estado de escuta ou de conexão, ele está sempre vinculado a uma combinação de um endereço IP mais um número de porta que identifica o host (máquina/dispositivo) e o processo.

Como funcionam as conexões de soquete

Os soquetes podem escutar conexões de entrada ou realizar conexões de saída por conta própria. Quando uma conexão é estabelecida, o soquete de escuta (soquete do servidor) é adicionalmente vinculado ao IP e à porta do lado da conexão.

Ou, alternativamente, é criado um novo soquete que agora está vinculado a dois pares de endereços IP e números de porta de um ouvinte e um solicitante. Dessa forma, dois soquetes conectados em máquinas diferentes podem identificar um ao outro e compartilhar uma única conexão para transmissão de dados sem bloquear o soquete de escuta que, enquanto isso, continua escutando outras conexões.

No caso do soquete de conexão (soquete do cliente), ele é implicitamente vinculado ao endereço IP do dispositivo e a um número de porta acessível aleatório no início da conexão. Em seguida, após o estabelecimento da conexão, a vinculação ao IP e à porta do outro lado da comunicação ocorre da mesma forma que para um soquete de escuta, mas sem criar um novo soquete.

Sockets no contexto de redes

Neste tutorial, não estamos preocupados com a implementação de soquetes, mas com o que os soquetes significam no contexto das redes.

Pode-se dizer que um soquete é um ponto final de conexão (destino de tráfego) que, de um lado, está associado ao endereço IP da máquina host e ao número da porta do aplicativo para o qual o soquete foi criado e, de outro, está associado ao endereço IP e à porta do aplicativo em execução em outra máquina com a qual a conexão é estabelecida.

Programação de soquetes

Quando falamos de programação de soquete, instanciamos objetos de soquete em nosso código e realizamos operações neles (ouvir, conectar, receber, enviar etc.). Nesse contexto, os soquetes são simplesmente objetos especiais que criamos em nosso programa e que têm métodos especiais para trabalhar com conexões e tráfego de rede.

Esses métodos chamam o kernel do sistema operacional ou, mais especificamente, a pilha de rede, que é uma parte especial do kernel responsável pelo gerenciamento das operações de rede.

Sockets e comunicação cliente-servidor

Agora, também é importante mencionar que os soquetes aparecem com frequência no contexto da comunicação cliente-servidor.

A ideia é simples: os soquetes estão relacionados a conexões; eles são manipuladores de conexões. Na Web, sempre que quiser enviar ou receber alguns dados, você inicia uma conexão (que está sendo iniciada por meio da interface chamada sockets).

Agora, você ou a parte à qual está tentando se conectar atua como servidor e a outra parte como cliente. Enquanto um servidor fornece dados aos clientes, os clientes se conectam proativamente e solicitam dados de um servidor. Um servidor escuta novas conexões por meio de um soquete de escuta, estabelece-as, recebe as solicitações do cliente e comunica os dados solicitados em sua resposta ao cliente.

Por outro lado, um cliente cria um soquete usando o endereço IP e a porta do servidor ao qual deseja se conectar, inicia uma conexão, comunica sua solicitação ao servidor e recebe dados em resposta. Essa troca contínua de informações entre os soquetes do cliente e do servidor forma a espinha dorsal de vários aplicativos de rede.

Sockets como base para protocolos de rede

O fato de os soquetes formarem um backbone também significa que há vários protocolos criados e usados sobre eles. Os mais comuns são o UDP e o TCP, sobre os quais já falamos brevemente. Os soquetes que usam um desses protocolos de transporte são chamados de soquetes UDP ou TCP.

Soquetes IPC

Além dos soquetes de rede, há também outros tipos. Por exemplo, soquetes IPC (Inter Process Communication). Os soquetes IPC destinam-se a transferir dados entre processos na mesma máquina, enquanto os soquetes de rede podem fazer o mesmo na rede.

A vantagem dos soquetes IPC é que eles evitam grande parte da sobrecarga de construir pacotes e resolver as rotas para enviar os dados. Como no contexto do IPC o remetente e o destinatário são processos locais, a comunicação por meio de soquetes IPC normalmente tem menor latência.

Unix-sockets

Um bom exemplo de soquetes IPC são os soquetes Unix que, como tudo no Unix, são apenas arquivos no sistema de arquivos. Eles não são identificados pelo endereço IP e pela porta, mas sim pelo caminho do arquivo no sistema de arquivos.

Soquetes de rede como soquetes IPC

Observe que você também pode usar soquetes de rede para comunicações entre processos se o servidor e o receptor estiverem no localhost (ou seja, tiverem o endereço IP 127.0.0.1).

É claro que, por um lado, isso aumenta a latência devido à sobrecarga associada ao processamento dos dados pela pilha de rede, mas, por outro lado, permite que não nos preocupemos com o sistema operacional subjacente, pois os soquetes de rede estão presentes e funcionam em todos os sistemas, ao contrário dos soquetes IPC, que são específicos de um determinado sistema operacional ou família de sistemas operacionais.

Biblioteca de soquetes Python

Para a programação de soquetes em Python, usamos a biblioteca oficial de soquetes Python incorporada, que consiste em funções, constantes e classes usadas para criar, gerenciar e trabalhar com soquetes. Algumas funções comumente usadas dessa biblioteca incluem:

  • socket(): Cria um novo soquete.
  • bind(): Associa o soquete a um endereço e a uma porta específicos.
  • listen(): Inicia a escuta de conexões de entrada no soquete.
  • accept(): Aceita uma conexão de um cliente e retorna um novo soquete para comunicação.
  • connect(): Estabelece uma conexão com um servidor remoto.
  • send(): Envia dados pelo soquete.
  • recv(): Recebe dados do soquete.
  • close(): Fecha a conexão do soquete.

Exemplo de soquete Python

Vamos dar uma olhada na programação de soquetes com um exemplo prático escrito em Python. Aqui, nosso objetivo é conectar dois aplicativos e fazer com que eles se comuniquem entre si. Usaremos a biblioteca de soquete Python para criar um aplicativo de soquete de servidor que se comunicará e trocará informações com um cliente em uma rede.

Considerações e limitações

Observe, no entanto, que, para fins didáticos, nosso exemplo é simplificado e os aplicativos serão executados localmente e não se comunicarão pela rede real - usaremos um endereço localhost de loopback para conectar o cliente ao servidor.

Isso significa que tanto o cliente quanto o servidor serão executados no mesmo computador e o cliente iniciará uma conexão com o mesmo computador em que está sendo executado, embora com um processo diferente que representa o servidor.

Execução em máquinas diferentes

Como alternativa, você pode ter seus aplicativos em dois dispositivos diferentes e conectá-los ao mesmo roteador Wi-Fi, o que formaria uma rede local. Assim, o cliente executado em um dispositivo poderia se conectar ao servidor executado em uma máquina diferente.

Nesse caso, no entanto, você precisaria saber os endereços IP que o roteador atribuiu aos dispositivos e usá-los em vez do endereço IP de loopback localhost (127.0.0.1) (para ver os endereços IP, use o comando de terminal ifconfig para sistemas do tipo Unix ou ipconfig - para Windows). Depois de obter os endereços IP de seus aplicativos, você pode alterá-los no código de acordo, e o exemplo ainda funcionará.

De qualquer forma, vamos começar com nosso exemplo. Obviamente, você precisará ter o Python instalado se quiser acompanhar o processo.

Criando um servidor de soquete em Python

Vamos começar com a criação de um servidor de soquete (servidor Python TCP, em particular, já que ele trabalhará com soquetes TCP, como veremos), que trocará mensagens com os clientes. Para esclarecer a terminologia, embora tecnicamente qualquer servidor seja um servidor de soquete, uma vez que os soquetes são sempre usados nos bastidores para iniciar conexões de rede, usamos a expressão "servidor de soquete" porque nosso exemplo faz uso explícito da programação de soquete.

Portanto, siga as etapas abaixo:

Criação de um arquivo python com alguns boilerplates

  • Crie um arquivo chamado server.py
  • Importe o módulo socket em seu script Python.
import socket
  • Adicione uma função chamada run_server. Adicionaremos a maior parte do nosso código lá. Quando você adicionar seu código à função, não se esqueça de recuá-lo corretamente:
def run_server():
    # your code will go here

Instanciando o objeto de soquete

Como próxima etapa, em run_server, crie um objeto de soquete usando a função socket.socket().

O primeiro argumento (socket.AF_INET) especifica a família de endereços IP para IPv4 (outras opções incluem: AF_INET6 para a família IPv6 e AF_UNIX para Unix-sockets)

O segundo argumento (socket.SOCK_STREAM) indica que estamos usando um soquete TCP.

No caso de usar o TCP, o sistema operacional criará uma conexão confiável com entrega de dados em ordem, detecção e retransmissão de erros e controle de fluxo. Você não terá que pensar em implementar todos esses detalhes.

Há também uma opção para especificar um soquete UDP: socket.SOCK_DGRAM. Isso criará um soquete que implementa todos os recursos do UDP sob o capô.

Caso queira ir além desse nível e criar seu próprio protocolo de camada de transporte sobre o protocolo de camada de rede TCP/IP usado pelos soquetes, você pode usar o valor socket.RAW_SOCKET para o segundo argumento. Nesse caso, o sistema operacional não manipulará nenhum recurso de protocolo de nível superior para você, e você terá que implementar todos os cabeçalhos, a confirmação da conexão e as funcionalidades de retransmissão, se precisar delas. Há também outros valores sobre os quais você pode ler na documentação.

# create a socket object
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Vinculação do soquete do servidor ao endereço IP e à porta

Defina o nome do host ou o IP e a porta do servidor para indicar o endereço a partir do qual o servidor poderá ser acessado e onde ele escutará as conexões de entrada. Neste exemplo, o servidor está escutando no computador local, o que é definido pela variável server_ip definida como 127.0.0.1 (também chamada de localhost).

A variável port é definida como 8000, que é o número da porta pela qual o aplicativo do servidor será identificado pelo sistema operacional (recomenda-se usar valores acima de 1023 para os números de porta a fim de evitar colisões com as portas usadas pelos processos do sistema).

server_ip = "127.0.0.1"
    port = 8000

Prepare o soquete para receber conexões, vinculando-o ao endereço IP e à porta que definimos anteriormente.

# bind the socket to a specific address and port
    server.bind((server_ip, port))

Escuta de conexões de entrada

Configure um estado de escuta no soquete do servidor usando a função listen para poder receber conexões de clientes de entrada.

Essa função aceita um argumento chamado backlog que especifica o número máximo de conexões não aceitas na fila. Neste exemplo, usamos o valor 0 para esse argumento. Isso significa que apenas um único cliente pode interagir com o servidor. Uma tentativa de conexão de qualquer cliente realizada enquanto o servidor estiver trabalhando com outro cliente será recusada.

Se você especificar um valor maior do que 0, digamos 1, ele informa ao sistema operacional quantos clientes podem ser colocados na fila antes que o método accept seja chamado neles.

Quando accept é chamado, o cliente é removido da fila e não é mais contado para esse limite. Isso pode ficar mais claro quando você vir outras partes do código, mas o que esse parâmetro faz essencialmente pode ser ilustrado da seguinte forma: quando o servidor de escuta receber a solicitação de conexão, ele adicionará esse cliente à fila e continuará aceitando a solicitação. Se antes de o servidor poder chamar internamente accept no primeiro cliente, ele receber uma solicitação de conexão de um segundo cliente, ele empurrará esse segundo cliente para a mesma fila, desde que haja espaço suficiente nela. O tamanho exato dessa fila é controlado pelo argumento backlog. Assim que o servidor aceita o primeiro cliente, esse cliente é removido da fila e o servidor começa a se comunicar com ele. O segundo cliente ainda está na fila, esperando que o servidor fique livre e aceite a conexão.

Se você omitir o argumento backlog, ele será definido como o padrão do seu sistema (no Unix, normalmente você pode ver esse padrão no arquivo /proc/sys/net/core/somaxconn ).

# listen for incoming connections
    server.listen(0)
    print(f"Listening on {server_ip}:{port}")

Aceitação de conexões de entrada

Em seguida, aguarde e aceite as conexões de entrada do cliente. O método accept interrompe o thread de execução até que um cliente se conecte. Em seguida, ele retorna um par de tuplas de (conn, address), em que endereço é uma tupla do endereço IP e da porta do cliente, e conn é um novo objeto de soquete que compartilha uma conexão com o cliente e pode ser usado para se comunicar com ele.

accept cria um novo soquete para se comunicar com o cliente, em vez de vincular o soquete de escuta (chamado server em nosso exemplo) ao endereço do cliente e usá-lo para a comunicação, porque o soquete de escuta precisa escutar outras conexões de outros clientes, caso contrário, ele seria bloqueado. É claro que, no nosso caso, só lidamos com um único cliente e recusamos todas as outras conexões enquanto fazemos isso, mas isso será mais relevante quando chegarmos ao exemplo do servidor multithread.

# accept incoming connections
    client_socket, client_address = server.accept()
    print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

Criação de um ciclo de comunicação

Assim que uma conexão com o cliente é estabelecida (após chamar o método accept ), iniciamos um loop infinito para nos comunicarmos. Nesse loop, fazemos uma chamada para o método recv do objeto client_socket. Esse método recebe o número especificado de bytes do cliente - no nosso caso, 1024.

1024 bytes é apenas uma convenção comum para o tamanho da carga útil, pois é uma potência de dois que é potencialmente melhor para fins de otimização do que algum outro valor arbitrário. No entanto, você pode alterar esse valor como quiser.

Como os dados recebidos do cliente na variável request estão em formato binário bruto, nós os transformamos de uma sequência de bytes em uma string usando a função decode.

Em seguida, temos uma instrução if, que sai do loop de comunicação caso recebamos uma mensagem ”close”. Isso significa que, assim que nosso servidor recebe uma string ”close” na solicitação, ele envia a confirmação de volta ao cliente e encerra a conexão com ele. Caso contrário, imprimimos a mensagem recebida no console. A confirmação, no nosso caso, é apenas o envio de uma cadeia de caracteres ”closed” para o cliente.

Observe que o método lower que usamos na string request na instrução if simplesmente a converte em letras minúsculas. Dessa forma, não nos importamos se a cadeia de caracteres close foi originalmente escrita com caracteres maiúsculos ou minúsculos.

# receive data from the client
    while True:
        request = client_socket.recv(1024)
        request = request.decode("utf-8") # convert bytes to string
        
        # if we receive "close" from the client, then we break
        # out of the loop and close the conneciton
        if request.lower() == "close":
            # send response to the client which acknowledges that the
            # connection should be closed and break out of the loop
            client_socket.send("closed".encode("utf-8"))
            break

        print(f"Received: {request}")

Envio de resposta de volta ao cliente

Agora devemos lidar com a resposta normal do servidor para o cliente (ou seja, quando o cliente não deseja fechar a conexão). Dentro do loop while, logo após print(f"Received: {request}"), adicione as seguintes linhas, que converterão uma cadeia de caracteres de resposta (”accepted” em nosso caso) em bytes e a enviarão ao cliente. Dessa forma, sempre que o servidor receber uma mensagem do cliente que não seja ”close”, ele enviará a string ”accepted” como resposta:

response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)

Liberação de recursos

Quando saímos do loop infinito while, a comunicação com o cliente está concluída, portanto, fechamos o soquete do cliente usando o método close para liberar os recursos do sistema. Também fechamos o soquete do servidor usando o mesmo método, o que efetivamente encerra o nosso servidor. Em um cenário do mundo real, é claro que provavelmente queremos que nosso servidor continue ouvindo outros clientes e não seja desligado depois de se comunicar com apenas um, mas não se preocupe, veremos outro exemplo mais adiante.

Por enquanto, adicione as seguintes linhas após o loop infinito while:

# close connection socket with the client
    client_socket.close()
    print("Connection to client closed")
    # close server socket
    server.close()

Observação: não se esqueça de chamar a função run_server no final do arquivo server.py. Basta usar a seguinte linha de código:

run_server()

Exemplo completo de código de soquete de servidor

Aqui está o código-fonte completo do site server.py:

import socket


def run_server():
    # create a socket object
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"
    port = 8000

    # bind the socket to a specific address and port
    server.bind((server_ip, port))
    # listen for incoming connections
    server.listen(0)
    print(f"Listening on {server_ip}:{port}")

    # accept incoming connections
    client_socket, client_address = server.accept()
    print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

    # receive data from the client
    while True:
        request = client_socket.recv(1024)
        request = request.decode("utf-8") # convert bytes to string
        
        # if we receive "close" from the client, then we break
        # out of the loop and close the conneciton
        if request.lower() == "close":
            # send response to the client which acknowledges that the
            # connection should be closed and break out of the loop
            client_socket.send("closed".encode("utf-8"))
            break

        print(f"Received: {request}")

        response = "accepted".encode("utf-8") # convert string to bytes
        # convert and send accept response to the client
        client_socket.send(response)

    # close connection socket with the client
    client_socket.close()
    print("Connection to client closed")
    # close server socket
    server.close()


run_server()

Observe que, para não complicar esse exemplo básico, omitimos o tratamento de erros. É claro que você deve adicionar blocos try-except e certificar-se de que sempre fecha os soquetes na cláusula finally. Continue lendo e veremos um exemplo mais avançado.

Criação de soquete de cliente em Python

Depois de configurar seu servidor, a próxima etapa é configurar um cliente que se conectará e enviará solicitações ao seu servidor. Então, vamos começar com as etapas abaixo:

Criação de um arquivo python com alguns boilerplates

  • Crie um novo arquivo chamado client.py
  • Importar a biblioteca de soquetes:
import socket
  • Defina a função run_client onde colocaremos todo o nosso código:
def run_client():
    # your code will go here

Instanciando o objeto de soquete

Em seguida, use a função socket.socket() para criar um objeto de soquete TCP que serve como ponto de contato do cliente com o servidor.

# create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Conectando-se ao soquete do servidor

Especifique o endereço IP e a porta do servidor para poder se conectar a ele. Eles devem corresponder ao endereço IP e à porta que você definiu anteriormente em server.py.

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number

Estabeleça uma conexão com o servidor usando o método connect no objeto de soquete do cliente. Observe que não vinculamos o soquete do cliente a nenhum endereço IP ou porta. Isso é normal para o cliente, porque o connect escolherá automaticamente uma porta livre e um endereço IP que forneça a melhor rota para o servidor a partir das interfaces de rede do sistema (127.0.0.1 em nosso caso) e associará o soquete do cliente a elas.

    # establish connection with server
    client.connect((server_ip, server_port))

Criação de um ciclo de comunicação

Depois de estabelecer uma conexão, iniciamos um loop de comunicação infinito para enviar várias mensagens ao servidor. Obtemos a entrada do usuário usando a função input incorporada do Python e, em seguida, codificamos em bytes e cortamos para que tenha no máximo 1024 bytes. Depois disso, enviamos a mensagem para o servidor usando client.send.

   while True:
        # input message and send it to the server
        msg = input("Enter message: ")
        client.send(msg.encode("utf-8")[:1024])

Tratamento da resposta do servidor

Quando o servidor recebe uma mensagem do cliente, ele responde a ela. Agora, em nosso código de cliente, queremos receber a resposta do servidor. Para isso, no loop de comunicação, usamos o método recv para ler no máximo 1024 bytes. Em seguida, convertemos a resposta de bytes em uma string usando decode e verificamos se ela é igual ao valor ”closed”. Se esse for o caso, saímos do loop que, como veremos mais tarde, encerrará a conexão do cliente. Caso contrário, imprimimos a resposta do servidor no console.

       # receive message from the server
        response = client.recv(1024)
        response = response.decode("utf-8")

        # if server sent us "closed" in the payload, we break out of the loop and close our socket
        if response.lower() == "closed":
            break

        print(f"Received: {response}")

Liberação de recursos

Por fim, após o loop while, feche a conexão do soquete do cliente usando o método close. Isso garante que os recursos sejam liberados adequadamente e que a conexão seja encerrada (ou seja, quando recebermos a mensagem “closed” e sairmos do loop while).

   # close client socket (connection to the server)
    client.close()
    print("Connection to server closed")

Observação: Novamente, não se esqueça de chamar a função run_client, que implementamos acima, no final do arquivo, como segue:

run_client()

Exemplo completo de código de soquete de cliente

Aqui está o código completo do site client.py:

import socket


def run_client():
    # create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number
    # establish connection with server
    client.connect((server_ip, server_port))

    while True:
        # input message and send it to the server
        msg = input("Enter message: ")
        client.send(msg.encode("utf-8")[:1024])

        # receive message from the server
        response = client.recv(1024)
        response = response.decode("utf-8")

        # if server sent us "closed" in the payload, we break out of the loop and close our socket
        if response.lower() == "closed":
            break

        print(f"Received: {response}")

    # close client socket (connection to the server)
    client.close()
    print("Connection to server closed")

run_client()

Teste seu cliente e servidor

Para testar a implementação do servidor e do cliente que escrevemos acima, faça o seguinte:

  • Abra duas janelas de terminal simultaneamente.
  • Em uma janela de terminal, navegue até o diretório em que o arquivo server.py está localizado e execute o seguinte comando para iniciar o servidor:
   python server.py

Isso associará o soquete do servidor ao endereço localhost (127.0.0.1) na porta 8000 e começará a escutar as conexões de entrada.

  • No outro terminal, navegue até o diretório em que o arquivo client.py está localizado e execute o seguinte comando para iniciar o cliente:
   python client.py

Isso solicitará a entrada do usuário. Em seguida, você pode digitar sua mensagem e pressionar Enter. Isso transferirá sua entrada para o servidor e a exibirá na janela do terminal. O servidor enviará sua resposta ao cliente e este solicitará a entrada novamente. Isso continuará até que você envie a cadeia de caracteres ”close” para o servidor.

Trabalhando com vários clientes - Multithreading

Vimos como um servidor responde às solicitações de um único cliente no exemplo anterior; no entanto, em muitas situações práticas, vários clientes podem precisar se conectar a um único servidor ao mesmo tempo. É aí que entra o multithreading. O multithreading é usado em situações em que você precisa lidar com várias tarefas (por exemplo, executar várias funções) simultaneamente (ao mesmo tempo).

A ideia é gerar um thread, que é um conjunto independente de instruções que podem ser manipuladas pelo processador. Os threads são muito mais leves do que os processos porque, na verdade, vivem dentro do próprio processo e você não precisa alocar muitos recursos para eles.

Limitações do multithreading em python

Observe que o multithreading em Python é limitado. A implementação padrão do Python (CPython) não pode executar threads verdadeiramente em paralelo. Somente um único thread pode ser executado por vez devido ao bloqueio do interpretador global (GIL). No entanto, esse é um tópico separado, que não discutiremos. Para o nosso exemplo, o uso de threads limitados do CPython é suficiente e transmite o ponto de vista. No entanto, em um cenário do mundo real, se for usar Python, você deve procurar a programação assíncrona. Não falaremos sobre isso agora, porque é novamente um tópico separado e geralmente abstrai algumas operações de soquete de baixo nível nas quais nos concentramos especificamente neste artigo.

Exemplo de servidor multithread

Vejamos o exemplo abaixo sobre como o multithreading pode ser adicionado ao seu servidor para lidar com um grande número de clientes. Observe que, desta vez, também adicionaremos um tratamento básico de erros usando os blocos try-except-finally. Para começar, siga as etapas abaixo:

Criação de função de servidor de geração de thread

Em seu arquivo python, importe os módulos socket e threading para poder trabalhar com soquetes e threads:

import socket
import threading

Defina a função run_server que, como no exemplo acima, criará um soquete de servidor, o associará e ouvirá as conexões de entrada. Em seguida, chame accept em um loop infinito while. Isso sempre manterá a escuta de novas conexões. Depois que o site accept obtiver uma conexão de entrada e retornar, crie um thread usando o construtor threading.Thread. Esse thread executará a função handle_client, que definiremos mais tarde, e passará client_socket e addr para ela como argumentos (a tuplaaddr contém um endereço IP e uma porta do cliente conectado). Depois que o thread é criado, chamamos start para iniciar sua execução.

Lembre-se de que a chamada accept está bloqueando, portanto, na primeira iteração do loop while, quando chegamos à linha com accept, interrompemos e aguardamos uma conexão com o cliente sem executar mais nada. Assim que o cliente se conecta, o método accept retorna e continuamos a execução: geramos um thread que cuidará desse cliente e passamos para a próxima iteração, onde paramos novamente na chamada accept, aguardando a conexão de outro cliente.

No final da função, temos um tratamento de erros que garante que o soquete do servidor seja sempre fechado caso algo inesperado aconteça.

def run_server():
    server_ip = "127.0.0.1"  # server hostname or IP address
    port = 8000  # server port number
    # create a socket object
    try:
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # bind the socket to the host and port
        server.bind((server_ip, port))
        # listen for incoming connections
        server.listen()
        print(f"Listening on {server_ip}:{port}")

        while True:
            # accept a client connection
            client_socket, addr = server.accept()
            print(f"Accepted connection from {addr[0]}:{addr[1]}")
            # start a new thread to handle the client
            thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
            thread.start()
    except Exception as e:
        print(f"Error: {e}")
    finally:
        server.close()

Observe que o servidor em nosso exemplo só será interrompido se ocorrer um erro inesperado. Caso contrário, ele escutará os clientes indefinidamente, e você terá que encerrar o terminal se quiser interrompê-lo.

Criação de função de tratamento de cliente para execução em thread separado

Agora, acima da função run_server, defina outra função chamada handle_client. Essa função será executada em um thread separado para cada conexão de cliente. Ele recebe o objeto de soquete do cliente e a tupla addr como argumentos.

Dentro dessa função, fazemos o mesmo que fizemos em um exemplo de thread único, além de algum tratamento de erros: iniciamos um loop para obter mensagens do cliente usando recv.

Em seguida, verificamos se recebemos uma mensagem de fechamento. Em caso afirmativo, respondemos com a cadeia de caracteres ”closed” e fechamos a conexão saindo do loop. Caso contrário, imprimimos a string de solicitação do cliente no console e prosseguimos para a próxima iteração do loop para receber a próxima mensagem do cliente.

No final dessa função, temos um tratamento de erros para casos inesperados (cláusulaexcept ) e também uma cláusula finally em que liberamos client_socket usando close. Essa cláusula finally será sempre executada, não importa o que aconteça, o que garante que o soquete do cliente seja sempre liberado corretamente.

def handle_client(client_socket, addr):
    try:
        while True:
            # receive and print client messages
            request = client_socket.recv(1024).decode("utf-8")
            if request.lower() == "close":
                client_socket.send("closed".encode("utf-8"))
                break
            print(f"Received: {request}")
            # convert and send accept response to the client
            response = "accepted"
            client_socket.send(response.encode("utf-8"))
    except Exception as e:
        print(f"Error when hanlding client: {e}")
    finally:
        client_socket.close()
        print(f"Connection to client ({addr[0]}:{addr[1]}) closed")

Quando o handle_client retornar, o thread que o executa também será liberado automaticamente.

Observação: Não se esqueça de chamar a função run_server no final do seu arquivo.

Exemplo completo de código de servidor multithread

Agora, vamos montar o código completo do servidor multithreading:

import socket
import threading


def handle_client(client_socket, addr):
    try:
        while True:
            # receive and print client messages
            request = client_socket.recv(1024).decode("utf-8")
            if request.lower() == "close":
                client_socket.send("closed".encode("utf-8"))
                break
            print(f"Received: {request}")
            # convert and send accept response to the client
            response = "accepted"
            client_socket.send(response.encode("utf-8"))
    except Exception as e:
        print(f"Error when hanlding client: {e}")
    finally:
        client_socket.close()
        print(f"Connection to client ({addr[0]}:{addr[1]}) closed")


def run_server():
    server_ip = "127.0.0.1"  # server hostname or IP address
    port = 8000  # server port number
    # create a socket object
    try:
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # bind the socket to the host and port
        server.bind((server_ip, port))
        # listen for incoming connections
        server.listen()
        print(f"Listening on {server_ip}:{port}")

        while True:
            # accept a client connection
            client_socket, addr = server.accept()
            print(f"Accepted connection from {addr[0]}:{addr[1]}")
            # start a new thread to handle the client
            thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
            thread.start()
    except Exception as e:
        print(f"Error: {e}")
    finally:
        server.close()


run_server()

Observação: Em um código do mundo real, para evitar possíveis problemas como situações de corrida ou inconsistências de dados ao lidar com servidores multithread, é fundamental levar em consideração as técnicas de sincronização e segurança de thread. No entanto, em nosso exemplo simples, isso não é um problema.

Exemplo de cliente com tratamento básico de erros

Agora que temos uma implementação de servidor capaz de lidar com vários clientes simultaneamente, podemos usar a mesma implementação de cliente vista acima nos primeiros exemplos básicos para iniciar a conexão, ou podemos atualizá-la ligeiramente e adicionar algum tratamento de erros. Abaixo você encontra o código, que é idêntico ao exemplo de cliente anterior, com a adição de blocos try-except:

import socket


def run_client():
    # create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number
    # establish connection with server
    client.connect((server_ip, server_port))

    try:
        while True:
            # get input message from user and send it to the server
            msg = input("Enter message: ")
            client.send(msg.encode("utf-8")[:1024])

            # receive message from the server
            response = client.recv(1024)
            response = response.decode("utf-8")

            # if server sent us "closed" in the payload, we break out of
            # the loop and close our socket
            if response.lower() == "closed":
                break

            print(f"Received: {response}")
    except Exception as e:
        print(f"Error: {e}")
    finally:
        # close client socket (connection to the server)
        client.close()
        print("Connection to server closed")


run_client()

Testando o exemplo de multithreading

Se você quiser testar a implementação de vários clientes, abra várias janelas de terminal para clientes e uma para o servidor. Primeiro, inicie o servidor com python server.py. Depois disso, inicie alguns clientes usando o site python client.py. Nas janelas do terminal do servidor, você verá como os novos clientes se conectam ao servidor. Agora você pode continuar enviando mensagens de diferentes clientes inserindo texto nos respectivos terminais, e todas elas serão tratadas e impressas no console no lado do servidor.

Aplicativos de programação de soquete na ciência de dados

Embora todos os aplicativos de rede usem soquetes criados pelo sistema operacional, há vários sistemas que dependem muito da programação de soquetes, seja para determinados casos de uso especiais ou para melhorar o desempenho. Mas como exatamente a programação de soquetes é útil no contexto da ciência de dados? Bem, ele definitivamente desempenha um papel importante sempre que há a necessidade de receber ou enviar grandes quantidades de dados rapidamente. Portanto, a programação de soquetes é usada principalmente para coleta de dados e processamento em tempo real, computação distribuída e comunicação entre processos. Mas vamos dar uma olhada mais de perto em alguns aplicativos específicos no campo da ciência de dados.

Coleta de dados em tempo real

Os soquetes são amplamente usados para coletar dados em tempo real de diferentes fontes para processamento posterior, encaminhamento para um banco de dados ou para um pipeline de análise etc. Por exemplo, um soquete pode ser usado para receber instantaneamente dados de um sistema financeiro ou de uma API de mídia social para processamento subsequente por cientistas de dados.

Computação distribuída

Os cientistas de dados podem usar a conectividade de soquete para distribuir o processamento e a computação de enormes conjuntos de dados em várias máquinas. A programação de soquetes é comumente usada no Apache Spark e em outras estruturas de computação distribuída para a comunicação entre os nós.

Implementação do modelo

A programação de soquetes pode ser usada ao servir modelos de aprendizado de máquina aos usuários, permitindo a entrega instantânea de previsões e sugestões. Para facilitar a tomada de decisões em tempo real, os cientistas de dados podem usar aplicativos de servidor baseados em soquetes de alto desempenho que recebem grandes quantidades de dados, processam-nos usando modelos treinados para fornecer previsões e, em seguida, retornam rapidamente as descobertas para o cliente.

Comunicação entre processos (IPC)

Os soquetes podem ser usados para IPC, o que permite que diferentes processos em execução na mesma máquina se comuniquem entre si e troquem dados. Isso é útil na ciência de dados para distribuir cálculos complexos e com uso intenso de recursos em vários processos. De fato, a biblioteca de subprocessamento do Python é usada com frequência para essa finalidade: ela gera vários processos para utilizar vários núcleos de processador e aumentar o desempenho do aplicativo ao realizar cálculos pesados. A comunicação entre esses processos pode ser implementada por meio de soquetes IPC.

Colaboração e comunicação

A programação de soquetes permite a comunicação e a colaboração em tempo real entre cientistas de dados. Para facilitar a colaboração eficaz e o compartilhamento de conhecimento, são usados aplicativos de bate-papo baseados em soquete ou plataformas de análise de dados colaborativos.

Vale a pena dizer que, em muitos dos aplicativos acima, os cientistas de dados podem não estar diretamente envolvidos no trabalho com soquetes. Normalmente, eles usam bibliotecas, estruturas e sistemas que abstraem todos os detalhes de baixo nível da programação de soquetes. No entanto, por trás de tudo isso, todas essas soluções são baseadas na comunicação por soquete e utilizam a programação por soquete.

Desafios e práticas recomendadas de programação de soquetes

Como os soquetes são um conceito de baixo nível de gerenciamento de conexões, os desenvolvedores que trabalham com eles precisam implementar toda a infraestrutura necessária para criar aplicativos robustos e confiáveis. É claro que isso traz muitos desafios. No entanto, existem algumas práticas recomendadas e diretrizes gerais que podem ser seguidas para superar esses problemas. Abaixo estão alguns dos problemas mais frequentes encontrados na programação de soquetes, juntamente com algumas dicas gerais:

Gerenciamento de conexões

Trabalhar com muitas conexões ao mesmo tempo, gerenciar vários clientes e garantir o tratamento eficiente de solicitações simultâneas pode ser desafiador e nada trivial. Requer gerenciamento e coordenação cuidadosos dos recursos para evitar gargalos

Práticas recomendadas

  • Mantenha o controle das conexões ativas usando estruturas de dados como listas ou dicionários. Ou use técnicas avançadas, como pooling de conexões, que também ajudam na escalabilidade.
  • Use técnicas de programação assíncrona ou de encadeamento para lidar com várias conexões de clientes ao mesmo tempo.
  • Feche as conexões corretamente para liberar recursos e evitar vazamentos de memória.

Tratamento de erros

É fundamental lidar com erros, como falhas de conexão, tempos limite e problemas de transmissão de dados. Lidar com esses erros e fornecer feedback apropriado aos clientes pode ser um desafio, especialmente quando se faz programação de soquete de baixo nível.

Práticas recomendadas

  • Use blocos try-except-finally para capturar e tratar tipos específicos de erros.
  • Forneça mensagens de erro informativas e considere o uso de registro para ajudar na solução de problemas.

Escalabilidade e desempenho

Garantir o desempenho ideal e minimizar a latência são as principais preocupações ao lidar com fluxos de dados de alto volume ou aplicativos em tempo real.

Práticas recomendadas

  • Otimize o desempenho de seu código, minimizando o processamento desnecessário de dados e a sobrecarga da rede.
  • Implementar técnicas de buffering para lidar eficientemente com grandes transferências de dados.
  • Considere técnicas de balanceamento de carga para distribuir solicitações de clientes em várias instâncias de servidor.

Segurança e autenticação

Pode ser difícil proteger a comunicação baseada em soquete e implementar mecanismos de autenticação adequados. A garantia da privacidade dos dados, a prevenção do acesso não autorizado e a proteção contra atividades mal-intencionadas exigem uma análise cuidadosa e a implementação de protocolos seguros.

Práticas recomendadas

  • Utilize protocolos de segurança SSL/TLS para garantir a segurança da transmissão de dados por meio da criptografia das informações.
  • Garanta a identidade do cliente implementando métodos de autenticação seguros, como autenticação baseada em token, criptografia de chave pública ou nome de usuário/senha.
  • Certifique-se de que os dados confidenciais, como senhas ou chaves de API, sejam protegidos e criptografados ou, de preferência, não sejam armazenados (apenas seus hashes, se necessário).

Confiabilidade e resiliência da rede

Lidar com interrupções de rede, largura de banda flutuante e conexões não confiáveis pode representar desafios. Manter uma conexão estável, lidar com desconexões de forma elegante e implementar mecanismos de reconexão são essenciais para aplicativos de rede robustos.

Práticas recomendadas

  • Use mensagens keep-alive para detectar conexões inativas ou interrompidas.
  • Implemente tempos limite para evitar o bloqueio indefinido e garantir o tratamento oportuno das respostas.
  • Implemente a lógica de reconexão de backoff exponencial para estabelecer uma conexão novamente se ela for perdida.

Capacidade de manutenção do código

Por último, mas não menos importante, a manutenção do código. Devido à natureza de baixo nível da programação de soquetes, os desenvolvedores acabam escrevendo mais código. Isso pode se transformar rapidamente em um código espaguete que não pode ser mantido, por isso é essencial organizá-lo e estruturá-lo o mais cedo possível e dedicar um esforço extra ao planejamento da arquitetura do seu código.

Práticas recomendadas

  • Divida seu código em classes ou funções que, idealmente, não devem ser muito longas.
  • Escreva testes de unidade desde o início, simulando suas implementações de cliente e servidor
  • Considere o uso de mais bibliotecas de alto nível para lidar com conexões, a menos que seja absolutamente necessário usar a programação de soquete.

Conclusão: Programação de soquetes em Python

Os soquetes são parte integrante de todos os aplicativos de rede. Neste artigo, examinamos a programação de soquetes em Python. Aqui estão os principais pontos a serem lembrados:

  • Os soquetes são interfaces que abstraem o gerenciamento de conexões.
  • Os soquetes permitem a comunicação entre diferentes processos (geralmente um cliente e um servidor) localmente ou em uma rede.
  • Em Python, o trabalho com soquetes é feito por meio da biblioteca socket, que, entre outras coisas, fornece um objeto de soquete com vários métodos, como recv, send, listen, close.
  • A programação de soquetes tem várias aplicações úteis na ciência de dados, incluindo coleta de dados, comunicação entre processos e computação distribuída.
  • Os desafios da programação de soquetes incluem gerenciamento de conexões, integridade de dados, escalabilidade, tratamento de erros, segurança e manutenção de código.

Com habilidades de programação de soquete, os desenvolvedores podem criar aplicativos de rede eficientes e em tempo real. Ao dominar os conceitos e as práticas recomendadas, eles podem aproveitar todo o potencial da programação de soquetes para desenvolver soluções confiáveis e dimensionáveis.

No entanto, a programação de soquete é uma técnica de nível muito baixo, difícil de usar porque os engenheiros de aplicativos precisam levar em conta cada pequeno detalhe da comunicação do aplicativo.

Hoje em dia, muitas vezes não precisamos trabalhar com soquetes diretamente, pois eles são normalmente manipulados por bibliotecas e estruturas de nível superior, a menos que haja a necessidade de realmente extrair o desempenho do aplicativo ou dimensioná-lo.

No entanto, compreender os soquetes e ter alguns insights sobre como as coisas funcionam nos bastidores leva a um melhor conhecimento geral como desenvolvedor ou cientista de dados e é sempre uma boa ideia.

Para saber mais sobre a função do Python na análise de rede, confira nosso curso Intermediate Network Analysis in Python. Você também pode seguir nossa trilha de habilidades de programação em Python para aprimorar suas habilidades de programação em Python.

Temas
Relacionado
Data Skills

blog

6 práticas recomendadas de Python para um código melhor

Descubra as práticas recomendadas de codificação Python para escrever os melhores scripts Python da categoria.
Javier Canales Luna 's photo

Javier Canales Luna

13 min

tutorial

Programação orientada a objetos em Python (OOP): Tutorial

Aborde os fundamentos da programação orientada a objetos (OOP) em Python: explore classes, objetos, métodos de instância, atributos e muito mais!
Théo Vanderheyden's photo

Théo Vanderheyden

12 min

tutorial

Guia passo a passo para criar mapas em Python usando a biblioteca Plotly

Faça seus dados se destacarem com mapas impressionantes criados com Plotly em Python
Moez Ali's photo

Moez Ali

7 min

tutorial

Declaração de caso de troca do Python: Um guia para iniciantes

Explore o match-case do Python: um guia sobre sua sintaxe, aplicativos em ciência de dados, ML e uma análise comparativa com o switch-case tradicional.
Matt Crabtree's photo

Matt Crabtree

5 min

tutorial

Tutorial de mineração de regras de associação em Python

Descobrindo padrões ocultos em Python com mineração de regras de associação
Moez Ali's photo

Moez Ali

14 min

tutorial

Dados JSON em Python

Trabalhando com JSON em Python: Um guia passo a passo para iniciantes
Moez Ali's photo

Moez Ali

6 min

See MoreSee More