Nota: Os exemplos de códigos foram feitos utilizando a versão 2.7.12 do interpretador Python, com o Sistema Operacional Linux Lite 3.4, baseado no Ubuntu 16.04. Este material foi criado para a disciplina de Redes de Computadores da Universidade Federal Fluminense, pelos alunos Robson Araújo Lima e Reza Amirahmadi

Cliente e Servidor UDP

O protocolo UDP (User Datagram Protocol), é um protocolo orientado a mensagens e, não precisa do estabelecimento de uma conexão. Por outro lado, ele não garante a entrega confiável de pacotes.

Servidor

Como o servidor não precisa de uma conexão, então apenas precisamos criar um socket e vincula-lo à uma porta usando a função bind() e, aguardar por pacotes individuais.

import socket
import sys

#deixar vazio para receber conexões fora da rede local
host = ''

#porta onde o servidor irá escutar
port = 1234

#cria um UDP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

#garante que o socket será destruído (pode ser reusado) após uma interrupção da execução 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
#associa o socket à uma porta
s.bind((host, port))
    
while True:
	print('waiting to receive message')
	data, address = s.recvfrom(1024)
		
	print 'received: ' + data + '\nfrom: ' + address[0] + '\nlistening on port: ' + str(address[1])

A função recvfrom() recebe dados de um socket e, retorna a tupla (string, address), onde string é uma string representando os dados e address é o endereço do socket que está transmitindo os dados. O valor 1024 na função recvfrom() indica a quantidade máxima de bytes recebidos por pacotes.

Salve o código em um arquivo de texto com a extensão .py, por exemplo server.py.

Cliente

Agora, o cliente UDP apenas tem que se preocupar em enviar as mensagens para o servidor. Para isso, é necessário cria um socket e utilizar a função sendto() para enviar os dados.

import socket
import sys

#endereço para o qual os dados vão ser enviados
host = 'localhost'

#número da porta que o servidor que vai receber os dados está escutando
port = 1234

#cria um UDP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

print 'Para sair use CTRL+X e pressione enter\n'
msg = raw_input()

while msg <> '\x18':
	#envia os dados
	s.sendto(msg, (host, port))
	
	msg = raw_input()
	
print('closing socket')
s.close()

A função sendto(), recebe como parâmetros a mensagem a ser enviada e, o endereço do destino. O endereço é composto por um endereço IP e um número de porta. Lembre-se que o nosso servidor server.py, está escutando na porta 1234. A função cliente portanto, vai enviar dados para o endereço 'localhost' (pois estamos executando cliente e servidor na mesma máquina) e para a porta 1234.

Perceba que todo socket deve ser destruído após seu uso, isso é feito com a função close(). Salve a função acima em um arquivo de texto com o nome client.py

Agora, abra um terminal no mesmo diretório onde se encontram os arquivos server.py e client.py. Execute primeiro o arquivo server.py com o comando:

robson@robson:~$ python server.py

Agora, abra uma nova janela do terminal e, execute o arquivo client.py com o comando:

robson@robson:~$ python client.py

No terminal onde está sendo executado o cliente a saída deve ser algo como:

robson@robson:~$ python client.py
Para sair use CTRL+X e pressione enter

Digite algumas palavras e pressione enter.

robson@robson:~$ python client.py
Para sair use CTRL+X e pressione enter
Hello Word!

No terminal onde está sendo executado o servidor a saída deve ser semelhante a esta:

robson@robson:~$ python server.py
waiting to receive message
received: Hello Word!
from: 127.0.0.1
listening on port: 34834
waiting to receive message

Cliente UDP com mensagens broadcast

Imagine agora, uma situação onde precismos mandar a mesma mensagem para vários computadores. Para tal tarefa, utilizamos sockets broadcast.

Para criar um cliente com um socket broadcast, reaproveitaremos os códigos do arquivo client.py alterando apenas algumas linhas.

import socket
import sys

#endereço broadcast para o qual os dados vão ser enviados
host = '192.168.255.255'

#número da porta que o servidor que vai receber os dados está escutando
port = 1234

#cria um UDP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

#Envie para todo mundo
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)           
	
print 'Para sair use CTRL+X e pressione enter\n'
msg = raw_input()

while msg <> '\x18':
	#envia os dados
	s.sendto(msg, (host, port))
	
	msg = raw_input()
	
print('closing socket')
s.close()
Repare que a única mudança feita no código foi a inserção da linha s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) e, o endereço de host que mudou para o endereço broadcast da rede local.

Para testar, é preciso executar o arquivo server.py em máquinas de sua rede local, depois executar o cliente broadcast em apenas uma máquina. Todos as máquinas que estão com o servidor rodando vão receber a mensagem enviada pelo cliente.

Implementando um servidor DNS (ou quase)

Servidores DNS (Domain Name System, ou sistema de nomes de domínios) são os responsáveis por localizar e traduzir para números IP os endereços dos sites que digitamos nos navegadores. Com o conhecimentos que obtivemos até agora, já somos capazes de implementar um simples servidor que recebe uma string e retorna um endereço IP correspondente.

O servidor DNS terá que saber previamente todos os nomes e endereços IP correspondentes, para isso, utilizaremos uma matriz, onde cada linha contém uma string e um endereço IP.

import socket
import sys

host = ''
port = 1234

	
matriz = [['robson','192.168.1.14'],
	 ['arthur','192.168.1.44'],
	 ['reza','192.168.3.221']]

#create a UDP/IP socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
#Bind the socket to the port
s.bind((host, port))
    
while True:
	print('waiting to receive message')
	data, address = s.recvfrom(1024)
	
	#procura na matriz a string (data) recebida, quando encontra, sai do for
	#com o índice corresponde da linha.		  
	for i in xrange(3):
		if data == matriz[i][0]:
			break
	data = matriz[i][1]
		
	if data:
		s.sendto(data, address)
		print('sent %s back to %s'%(data, address))

Perceba que na coluna 1 da matriz temos as strings e, na segunda coluna são os IP correspondentes. Salve esse código com o nome dns.py.

Agora, para o cliente usar nosso servidor DNS, ele precisa primeiro, mandar o nome de quem (destinatário) ele quer mandar mensagens. Então, o servidor responde ao cliente com o IP, só assim, o cliente consegue mandar mensagens para o destino. Abaixo codificamos um possível cliente para ser usado com o servidor.

import socket
import sys


port = 1234


#Create a udp socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

print 'Com quem você quer conversar?\n'
msg = raw_input()

#Endereço do servidor dns
s.sendto(msg, ('192.168.4.13',1234))

#Servidor DNS responde com o IP na variável host
host, addr = s.recvfrom(1024)

print 'Para sair use CTRL+X e pressione enter\n'
msg = raw_input()
while msg <> '\x18':
	
	# send the data
	s.sendto(msg, (host, port))	

	msg = raw_input()
		
print('closing socket')
s.close()

Salve o código com o nome dns_client.py. Para testar os códigos, é preciso preencher a tabela com alguns nomes fictícios e os endereços IP de algumas máquina em um rede local. No exemplo, o servidor DNS está rodando na máquina com IP 192.168.4.13 e escutando na porta 1234. Se executarmos o dns_client.py em uma máquina diferente do servidor DNS, podemos por exemplo, falar com o robson, apenas digitando o nome robson no terminal do cliente DNS.

Cliente e Servidor TCP

Diferentemente dos sockets UDP, o protocolo TCP estabelece uma conexão entre o socket cliente e socket servidor, antes do envio de qualquer mensagem de dados.

Servidor

Um servidor TCP/IP, precisa, como no servidor UDP, associar um socket à alguma porta. Quando o servidor recebe uma solicitação de conexão, ele primeiramente precisa aceitá-la, para depois receber dados.

O nosso servidor TCP/IP vai receber uma conexão por vez e, responder ao cliente uma confirmação do recebimento da mensagem.


import socket
import sys

# Significa que é possível receber conexões fora da rede local
host = ''

port = 1234

# Cria um socket TCP/IP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#garante que o socket será destruído (pode ser reusado) após uma interrupção da execução 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	
# associa o socket a porta
s.bind((host, port))

print('Server %s on port %s' % (socket.gethostname(), port))

A função listen() coloca o socket no modo servidor, e define a quantidade de solicitações de novas conexões que podem ser enfileiradas. A função accept() espera por novas solicitações de conexões.


# Enfileira uma nova conexão,  (define o modo servidor)
s.listen(1)

while True:
	# Espera por novas conexões
	print('waiting for a connection')
	
	# Aceita conexões
	conn, address = s.accept();
	

A função accept() retorna um novo socket para ser usado com aquela conexão e, o endereço do cliente. O novo socket utiliza uma nova porta atribuída automaticamente pelo kernel. Os dados são recebidos com a função recv() e, enviados com a função sendall().

	
	try:
		print('Connected to %s on port %s ' % (client_address))
		
		while True:
			data = conn.recv(1024)
			
			reply = 'OK...' + data			
			if not data:
				break
				
			conn.sendall(reply)
			
	finally:
		# Clean up the connection
		conn.close()

Quando a comunicação com o cliente for encerrada, o socket retornado pela função accept() precisa ser destruído usando a função close(). Um bloco try:finally é utilizado para garantir que mesmo na ocorrências de erros o socket seja encerrado.

Salve as linhas de códigos em um arquivo de texto com o nome server_tcp.py

Cliente

O cliente TCP, precisa se conectar ao servidor usando o função connect(), que recebe como parâmetro o endereço do servidor.
import socket
import sys

host = 'localhost'
port = 1234

# Cria um socket TCP/IP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))

Os dados são enviados com a função sendall() e, recebidos com a função recv().
	
print 'Para sair use CTRL+X e pressione enter\n'
msg = raw_input()

try:	
	while msg <> '\x18':			
		# Envia os dados
		s.sendall(msg)
		
		#recebe a resposta do servidor
		data = s.recv(1024)
		print data
		
		msg = raw_input()
	
finally:
		print('closing connection bye...')
		s.close()

Note que nesta função cliente, além de enviarmos dados, estamos recebendo uma confirmação do servidor que nossos dados foram recebidos com sucesso.

Salve em um documente de texto com o nome client_tcp.py. Abra um terminal e execute primeiro o arquivo server_tcp.py. Note que, precisamos necessariamente executar primeiro o arquivo do servidor, pois se executássemos o cliente primeiro, não teria nenhum servidor escutando e, consequentemente geraria um erro.

Após executar o servidor a saída será:

robson@robson:~$ python server_tcp.py
Server robson on port 1234
waiting for a connection

Agora execute o client_tcp.py, digite algum texto e pressione enter, a saída será:

robson@robson:~$ python client_tcp.py
Para sair use CTRL+X e pressione enter
Hello Earth!
OK...Hello Earth!

Temos então um cliente conversando com um servidor. Mas o que acontece se mais de um cliente tentar se conectar ao servidor? Neste caso, o servidor irá enfileirar a conexão até que a conexão atual seja encerrada. Tente você mesmo abrir uma nova janela do terminal e, executar mais um cliente. Você verá que apenas o primeiro cliente que conectou que receberá uma resposta. O segundo receberá uma resposta logo após o encerramento da conexão do primeiro.

Para termos um servidor que responda a mais de um cliente por vez, devemos criar uma thread para cada nova conexão ao servidor.

Servidor TCP/IP usando threads

Este servidor responde da mesma forma que o último servidor TCP/IP apresentado. A função clientthread() recebe como parâmetro o socket retornado pela função accept(), na qual, é este socket que responde ao respectivo cliente que solicitou a conexão.

Quando um cliente encerra a conexão, o socket no servidor que estava respondendo este cliente é destruído.

import socket
import sys
from thread import *
 
host = ''   
port = 1234
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
#garante que o socket será destruído (pode ser reusado) após uma interrupção da execução 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
#Bind socket to local host and port
try:
    s.bind((host, port))
except socket.error as msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()
      
s.listen(1)
 
#Função para lidar com cada conexão (cliente)
def clientthread(conn):
    #Sai do loop quando o cliente desconecta, pois a variável data não conterá
    #nenhum conteúdo
    while True:
         
        #Receiving from client
        data = conn.recv(1024)
        reply = 'OK...' + data
        
        if not data: 
            break
     
        conn.sendall(reply)
     
    #destroi o socket e encerra thread ao sair do loop
    conn.close()
 
#Continua recebendo conexões
while True:
    #Aceita conexões
    conn, addr = s.accept()
    
    print 'Connected with ' + addr[0] + ':' + str(addr[1])
     
    #Cria nova thread para uma nova conexão. O primeiro
    #argumento é o nome da função, e o segundo é uma tupla
    #com os parâmetros da função.
    start_new_thread(clientthread ,(conn,))

Salve o código em um arquivo com o nome server_thread.py e depois o execute com o comando python no terminal.

Agora abra pelo menos duas novas janelas do terminal e execute o cliente TCP/IP client_tcp.py nos dois terminais. Em um dos terminais, digite uma frase e pressione enter. Você terá a saída:

robson@robson:~$ python client.py
Para sair use CTRL+X e pressione enter

Hi xoxo
OK...Hi xoxo

No terminal onde está rodando o servidor server_thread.py você terá a saída:

robson@robson:~$ python server_thread.py
Connected with 127.0.0.1:38318
Connected with 127.0.0.1:38324

Repare agora que, o servidor está respondendo os dois terminais ao mesmo tempo.

Podemos usar o programa telnet como um cliente TCP/IP do nosso servidor. Com algum servidor TCP/IP rodando, abra uma nova janela do terminal e digite os seguintes comandos:

robson@robson:~$ telnet localhost 1234

Após digitar os comandos e pressionar enter, escreva alguma frase e pressiona enter novamente. Você obterá a saída:

robson@robson:~$ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hi telnet
OK...Hi telnet

Ideia básica de um servidor HTTP

O Protocolo de Transferência de Hipertextos (HTTP em inglês), trabalha como um protocolo de requisição-resposta entre um cliente e um servidor. Um navegador web pode ser o cliente e uma aplicação em um computador que hospeda um site pode ser o servidor.

O funcionamento básico do protocolo é feito da seguinte forma: Um cliente (navegador) envia uma requisição HTTP para um servidor, solicitando algum arquivo desse servidor, como por exemplo o arquivo index.html. O servidor retorna uma resposta para o cliente, contendo informações sobre o status da requisição e, o arquivo solicitado.

Podemos, portanto, implementar um simples servidor, que recebe requisições de um navegador e, retorna um Hello Word como resposta.

Para este servidor, usaremos o mesmo código do arquivo server_thread.py, com uma pequena modificação na função clientthread(). O código ficou assim:

import socket
import sys
from thread import *
 
host = ''   
port = 1234
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
#garante que o socket será destruído (pode ser reusado) após uma interrupção da execução 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
#Bind socket to local host and port
try:
    s.bind((host, port))
except socket.error as msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()
      
s.listen(1)
 
#Função para lidar com cada conexão (cliente)
def clientthread(conn):
	#Sai do loop quando o cliente desconecta, pois a variável data não conterá
    #mais nenhum conteúdo.
    while True:
         
        #Receiving from client
        data = conn.recv(1024)
        
        print data
        
        reply = 'HTTP/1.1 200 OK\n\nHello Word'
             
        conn.sendall(reply)
        #destroi o socket e encerra thread ao sair do loop
        conn.close()
        break
        
        
#Continua recebendo conexões
while True:
    #Aceita conexões
    conn, addr = s.accept()
    
    print 'Connected with ' + addr[0] + ':' + str(addr[1])
     
    #Cria nova thread para uma nova conexão. O primeiro
    #argumento é o nome da função, e o segundo é uma tupla
    #com os parâmetros da função.
    start_new_thread(clientthread ,(conn,))

Quando alguma mensagem é recebida através da função recv(), apenas exibimos ela. Logo após encaminhamos a resposta para o cliente (HTTP/1.1 200 OK), assim como o conteúdo (Hello Word). O navegador interpretará a resposta da seguinte forma:
O servidor confirmou que vai manter a comunicação na versão do protocolo HTTP/1.1 e o número 200 significa que a requisição foi processada com sucesso, por isso o OK. Depois do cabeçalho, é importante notar que, deve-se sempre ter uma linha em branco, entre o cabeçalho e o coteúdo, por isso colocamos dois \n\n. Isso também facilita o navegador identificar o que é cabeçalho e o que é o corpo da mensagem. Depois, vem a parte do conteúdo (Hello Word), simples assim!

Salve este arquivo com o nome http_server.py. Abra o terminal e o execute.

robson@robson:~$ python http_server.py

Não se preocupe, é normal não ser exibido nenhuma mensagem até agora, aliás, não colocamos nada no código que exiba algo até um cliente se conectar. Agora abra um navegador, e digite:

http://localhost:1234/hello

Pressione enter. Neste momento, deve aparecer um Hello Word no seu navegador. No terminal onde está rodando o servidor, deve aparecer algo como:

GET /hello HTTP/1.1
Host: localhost:1234
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36 OPR/47.0.2631.80
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8

Estas são as informações que o navegador manda numa requisição. Repare na primeira linha, GET /hello HTTP/1.1, aqui o navegador diz, preciso (GET) do arquivo (hello) e quero me comunicar utilizando a versão HTTP/1.1. As outras informações são sobre codificação, língua suportada e informações específicas do navegador, neste exemplo usei o Opera 47.0.