socket套接字编程-Python

本文内容基于计网自顶向下第二章的套接字编程作业:

1.Web 服务器 2.UDP ping 程序 3.邮件客户端 4. 多线程 Web 代理服务器

以下基于Web官网所给作业框架完成,包含拓展部分

作业细节(官网)自取:https://pan.baidu.com/s/1BYlQNwsjc9QCpeOQQ_R0ZQ 密码: vgfl

下面的代码也同步放在了github上:https://github.com/Hoper-J/Socket-Programming

ps: 以下代码均可跑通,做了部分注解,后续有需要再补相关说明

如何在一台电脑上运行服务器和客户端

开两个终端(terminal)分别运行,或者Pycharm+终端,抑或其他的,能跑就行

实现简单Web服务器(TCP)

webserver.py

from socket import *
import sys # In order to terminate the program


SERVER = gethostbyname(gethostname())
PORT = 18888
ADDR = (SERVER, PORT)

BUFSIZE = 4096
FORMAT = 'UTF-8'

serverSocket = socket(AF_INET, SOCK_STREAM) #Prepare a sever socket
serverSocket.bind(ADDR)

while True:
print('Ready to serve...')
serverSocket.listen()
connectionSocket, addr = serverSocket.accept()
try:
message = connectionSocket.recv(BUFSIZE).decode(FORMAT)

filename = message.split()[1]

f = open(filename[1:])

outputdata = f.readlines()

# Send one HTTP header line into socket
connectionSocket.send('HTTP/1.1 200 OK\r\n\r\n'.encode(FORMAT))

# Send the content of the requested file to the client
for i in range(0, len(outputdata)):
connectionSocket.send(outputdata[i].encode())
connectionSocket.send("\r\n".encode())

connectionSocket.close()

except IOError:
# Send response message for file not found
connectionSocket.send('HTTP/1.1 404 Not found\r\n\r\n'.encode(FORMAT))
connectionSocket.send('文件不存在\r\n'.encode(FORMAT))
# Close client socket
connectionSocket.close()

serverSocket.close()
sys.exit() # Terminate the program after sending the corresponding data

多线程 webserver_thread.py

import sys  # In order to terminate the program
import threading

from socket import *

SERVER = gethostbyname(gethostname())
PORT = 18888
ADDR = (SERVER, PORT)

HEADER = 64
BUFSIZE = 4096
FORMAT = 'UTF-8'
CLOSE_CONNECTION = '!QUIT'

serverSocket = socket(AF_INET, SOCK_STREAM) #Prepare a sever socket
serverSocket.bind(ADDR)


def start():
print('Ready to serve...')
serverSocket.listen()
while True:
connectionSocket, addr = serverSocket.accept()
thread = threading.Thread(target=handle_client, args=(connectionSocket,))
thread.start()
print(f"[当前连接数量]: {threading.activeCount() - 1}") # 去除创立的线程

def handle_client(connectionSocket):
while True:
try:
message = connectionSocket.recv(BUFSIZE).decode(FORMAT)
if message == CLOSE_CONNECTION:
break

filename = message.split()[0]

f = open(filename[1:])

outputdata = f.readlines()

# Send one HTTP header line into socket
connectionSocket.send('HTTP/1.1 200 OK\r\n\r\n'.encode(FORMAT))

# Send the content of the requested file to the client
for i in range(0, len(outputdata)):
connectionSocket.send(outputdata[i].encode())
connectionSocket.send("\r\n".encode())

# connectionSocket.close()

except (OSError, IOError):
# Send response message for file not found

connectionSocket.send('HTTP/1.1 404 Not found\r\n\r\n'.encode(FORMAT))
# connectionSocket.send('文件不存在\r\n'.encode(FORMAT))

# Close client socket
connectionSocket.close()

start()
serverSocket.close()
sys.exit()

UDP ping 程序

客户端

下面的代码结合了作业的扩展练习 1:

  1. Currently, the program calculates the round-trip time for each packet and prints it out individually. Modify this to correspond to the way the standard ping program works. You will need to report the minimum, maximum, and average RTTs at the end of all pings from the client. In addition, calculate the packet loss rate (in percentage).

UDPPingerClient.py


import socket
import time

PORT = 12000
FORMAT = 'utf-8'
DISCONNECT_MESSAGE = '!DISCONNECT'
SERVER = '127.0.0.1'
ADDR = (SERVER, PORT)

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

time_queue = []
loss_number = 0


def send(msg):
try:
message = msg.encode(FORMAT)
b = time.time()
client.sendto(message, ADDR)
client.settimeout(0.1)
modified_msg = client.recv(2048).decode()
a = time.time()
time_queue.append(a-b)
print(f"RESPONSE: {modified_msg}")
print(f"RTT: {time_queue[-1]}s")
# print(client.recv(2048).decode())
except socket.timeout:
global loss_number
loss_number += 1
print("Request timed out!")


for i in range(1,11):
send(f"Ping {i} {time.asctime()}")
else:
client.close()
print(f"""---ping statics---
10 transmitted, {10 - loss_number} received, {loss_number/10:.2%} loss
min/max/avg: {min(time_queue):f}/{max(time_queue):f}/{sum(time_queue)/10:f} s
""")
# send(input())

UDPPingerServer.py

# We will need the following module to generate randomized lost packets
import random
from socket import *

# Create a UDP socket
# Notice the use of SOCK_DGRAM for UDP packets
serverSocket = socket(AF_INET, SOCK_DGRAM)

# Assign IP address and port number to socket
serverSocket.bind(('', 12000))


while True:
# Generate random number in the range of 0 to 10
rand = random.randint(0, 10)
# Receive the client packet along with the address it is coming from
message, address = serverSocket.recvfrom(1024)
# Capitalize the message from the client
message = message.upper()

# If rand is less is than 4, we consider the packet lost and do not respond
if rand < 4:
continue

# Otherwise, the server responds
serverSocket.sendto(message, address

心跳包(Heartbeat packets)

UDPHeartbeatClient.py

import socket
import threading
import time


PORT = 12000
FORMAT = 'utf-8'
DISCONNECT_MESSAGE = '!DISCONNECT'
SERVER = '127.0.0.1'
ADDR = (SERVER, PORT)

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

noTransferSequence = True
sequenceLength = 5 # 指定message的发送格式,以序列 12 为例:f" 12{TIME}"


def listen_recv():
while True:
print(client.recv(2048).decode())


def send_length():
global noTransferSequence
while noTransferSequence:
client.sendto(str(sequenceLength).encode(FORMAT), ADDR)
status = client.recv(2048).decode()
if status == 'Length OK':
noTransferSequence = False
# 创建线程监听收到的信息
threading.Thread(target=listen_recv).start()

print(status)


def send(msg):
send_length()
client.sendto(msg.encode(FORMAT), ADDR)
time.sleep(0.1)


i = 0
while True:
message = f"{i:{sequenceLength}}{time.time()}" # message[:sequenceLength]存放序列号
send(message)
i += 1

# send(input())

UDPHeartbeatServer.py

"""
time.time()在不同平台上可能会有不同表现,这里仅为了本地演示
ps: 对心跳包不太了解,按自己理解做了个简单的
"""

# We will need the following module to generate randomized lost packets
import random
import threading
import time
from socket import *

# Create a UDP socket
# Notice the use of SOCK_DGRAM for UDP packets
serverSocket = socket(AF_INET, SOCK_DGRAM)

# Assign IP address and port number to socket
serverSocket.bind(('', 12000))

TIMEOUT = 2 # 设定心跳包间隔不超过 2 秒
sendTimeSequence = [] # 心跳包时间序列

noSequenceLength = True # 标识是否收到序列长度
noThreadMonitoring = True # 标识是否有线程监控超时


def handle_heatbeat(sequence_length):
while sequence_length:
time.sleep(0.1)
now_time = time.time()
latest_send = sendTimeSequence[-1] # 获取最近一次客户端发送心跳包的时间
if now_time - latest_send > TIMEOUT:
serverSocket.close()
break


def start():
global noSequenceLength, noThreadMonitoring
print('Ready to serve')
latest_number = 0
sequence_length = 0
while True:
try:
# Generate random number in the range of 0 to 10
rand = random.randint(0, 10)
# Receive the client packet along with the address it is coming from
message, address = serverSocket.recvfrom(1024)
# If rand is less is than 1, we consider the packet lost and do not respond
if rand < 1:
if noSequenceLength:
serverSocket.sendto(b'Retransmission', address)
continue

# Otherwise, the server responds
msg = message.decode()
if noSequenceLength: # 此时已经收到序列长度,对第一次收到的序列长度进行处理
sequence_length = int(msg[:5])
noSequenceLength = False
serverSocket.sendto(b'Length OK', address)
continue

number = int(msg[:sequence_length])
sendTimeSequence.append(float(msg[sequence_length:]))
if noThreadMonitoring:
threading.Thread(target=handle_heatbeat,args=(sequence_length,)).start()
noThreadMonitoring = False

for i in range(latest_number + 1, number): # 若间隔为1,则代表未丢失,不需回复
serverSocket.sendto(f'{i} have lost'.encode(), address)
latest_number = number
except OSError:
print('CLOSE')
break


if __name__ == '__main__':
start()

邮件客户端

为选择的是网易的163邮箱作为实验对象,下文中发件邮箱以及收件邮箱都作了更改,可以根据自己的邮箱地址进行替换。

服务器地址默认端口号SSL协议端口号
pop3.163.com110995
smtp.163.com25465/994

MailClient.py

smtp.163.com 的默认端口号为 25,建议初学者使用 telnet smtp.163.com 25 (com 和 25 之间为空格而非:)在终端进行同步的实验,结果一致

提前解释一下, AUTH LOGIN 命令返回的 334 dXNlcm5hbWU6,实际是经过base64加密的 username:,同样的UGFzc3dvcmQ6就是password:

image-20210415163722750

# 非 SSL 加密
import base64

from socket import *

msg = "\r\n I love computer networks!" # 信息前面需空行
endmsg = "\r\n.\r\n"
recv = []

# Choose a mail server (e.g. Google mail server) and call it mailserver
mailserver = "smtp.163.com"

# Create socket called sslClientSocket and establish a TCP connection with mailserver
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((mailserver, 25))

recv.append(clientSocket.recv(1024).decode())

if recv[-1][:3] != '220':
print('220 reply not received from server.')
print(recv[-1])

# Send HELO command and print server response.
heloCommand = 'HELO Alice\r\n'
clientSocket.send(heloCommand.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

# Login
LoginCommand = 'AUTH LOGIN\r\n' # 要加\r\n,否则会报 504 错误
User = b'你的账户'
Psw = b'你的密码'

UserB64 = base64.b64encode(User) # 账号密码需要base64加密
PswB64 = base64.b64encode(Psw)

clientSocket.send(LoginCommand.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

clientSocket.send(UserB64 + b'\r\n')
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

clientSocket.send(PswB64 + b'\r\n')
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

# Send MAIL FROM command and print server response.

FromCommand = 'mail from: <your_email_address>\r\n'
clientSocket.send(FromCommand.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

# Send RCPT TO command and print server response.
ToCommand = 'rcpt to: <recipient_email_address>\r\n'
clientSocket.send(ToCommand.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

# Send DATA command and print server response.
DataCommand = 'data'
clientSocket.send(DataCommand.encode())

# Send message data.
header = f'''
from: <your_email_address>
to: <recipient_email_address>
subject: test
'''
clientSocket.send((header + msg).encode())

# Message ends with a single period.
clientSocket.send(endmsg.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

# Send QUIT command and get server response.
QuitCommand = 'QUIT\r\n'
clientSocket.send(QuitCommand.encode())
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])
recv.append(clientSocket.recv(1024).decode())
print(recv[-1])

SecureMailClient.py

此部分为额外的练习,可于终端下使用 openssl s_client -connect smtp.163.com:465加深理解

原文:Mail servers like Google mail (address: smtp.gmail.com, port: 587) requires your client to add a Transport Layer Security (TLS) or Secure Sockets Layer (SSL) for authentication and security reasons, before you send MAIL FROM command. Add TLS/SSL commands to your existing ones and implement your client using Google mail server at above address and port.

因为换个端口号就能使用 SSL 协议,接下来继续用网易做示范。

smtp.163.com SSL协议的端口号为465/994,任选其一使用

# SSL 加密
import base64
import ssl

from socket import *

msg = "\r\n I love computer networks!"
endmsg = "\r\n.\r\n"
recv = []

# Choose a mail server (e.g. Google mail server) and call it mailserver
mailserver = "smtp.163.com"

# Create socket called sslClientSocket and establish a TCP connection with mailserver
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((mailserver, 465))

# ssl
purpose = ssl.Purpose.SERVER_AUTH
context = ssl.create_default_context(purpose)

sslClientSocket = context.wrap_socket(clientSocket, server_hostname=mailserver)

recv.append(sslClientSocket.recv(1024).decode())

if recv[-1][:3] != '220':
print('220 reply not received from server.')

print(recv[-1])
# Send HELO command and print server response.
heloCommand = 'HELO Alice\r\n'
sslClientSocket.send(heloCommand.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

# Login
LoginCommand = 'AUTH LOGIN\r\n' # 命令要加\r\n
User = b'你的账户'
Psw = b'你的密码'

UserB64 = base64.b64encode(User)
PswB64 = base64.b64encode(Psw)

sslClientSocket.send(LoginCommand.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])
sslClientSocket.send(UserB64 + b'\r\n')
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])
sslClientSocket.send(PswB64 + b'\r\n')
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

# Send MAIL FROM command and print server response.

FromCommand = 'mail from: <your_email_address>\r\n'
sslClientSocket.send(FromCommand.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

# Send RCPT TO command and print server response.
ToCommand = 'rcpt to: <recipient_email_address>\r\n'
sslClientSocket.send(ToCommand.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

# Send DATA command and print server response.
DataCommand = 'data'
sslClientSocket.send(DataCommand.encode())

# Send message data.
header = f'''
from: <your_email_address>
to: <recipient_email_address>
subject: test
'''
sslClientSocket.send((header + msg).encode())

# Message ends with a single period.
sslClientSocket.send(endmsg.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

# Send QUIT command and get server response.

QuitCommand = 'QUIT\r\n'
sslClientSocket.send(QuitCommand.encode())
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])
recv.append(sslClientSocket.recv(1024).decode())
print(recv[-1])

ProxyServer.py

程序从自己手中跑起来的瞬间令人着迷

Mac改代理:在Safari中Command+, -> 高级 -> 更改设置… -> 代理 -> 网页代理 (HTTP) ,点击并且应用,测试完记得取消代理。

image-20210425172041771

# Test URL:http://gaia.cs.umass.edu/wireshark-labs/HTTP-wireshark-file1.html
import sys

from socket import *

if len(sys.argv) <= 1:
print('Usage : "python ProxyServer.py server_ip"\n'
'[server_ip : It is the IP Address Of Proxy Server\n'
'The default address is 0.0.0.0')
IP = ''
# sys.exit(2)
else:
IP = sys.argv[1]

PORT = 8086
FORMAT = 'utf-8'

# Create a server socket, bind it to a port and start listening
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind((IP, PORT))
tcpSerSock.listen()

while 1:
# Start receiving data from the client
print('Ready to serve...')
tcpCliSock, addr = tcpSerSock.accept()
print('Received a connection from:', addr)
message = tcpCliSock.recv(2048).decode(FORMAT)
print(message)
# Extract the filename from the given message print(message.split()[1])
print(message.split()[1])
filename = message.split()[1].partition("//")[2]
# filename = message.split()[1].split(':')[0]
print(filename)
fileExist = "false"
filetouse = filename.replace('/', '_')
print(filetouse)
try:
# Check wether the file exist in the cache
f = open(filetouse, "r")
outputdata = f.readlines()
fileExist = "true"
# ProxyServer finds a cache hit and generates a response message
tcpCliSock.send("HTTP/1.0 200 OK\r\n".encode(FORMAT))
tcpCliSock.send("Content-Type:text/html\r\n".encode(FORMAT))

# Send the content of the requested file to the client
for data in outputdata:
tcpCliSock.send(data.encode(FORMAT))
tcpCliSock.send("\r\n".encode(FORMAT))

print('Read from cache')

# Error handling for file not found in cache
except IOError:
if fileExist == "false":
# Create a socket on the proxyserver
c = socket(AF_INET, SOCK_STREAM) # Fill in start. # Fill in end.
hostn = filename.replace("www.", "", 1).split('/')[0]
print(f'Host: {hostn}')
try:
# Connect to the socket to port 80
c.connect((hostn, 80))

# Create a temporary file on this socket and ask port 80 for the file requested by the client
fileobj = c.makefile('rwb', 0)
fileobj.write(message.encode(FORMAT))

# Read the response into b_buffer
b_buffer = fileobj.read() # 这里没有 decode()
print(b_buffer)
# Create a new file in the cache for the requested file.
# Also send the response in the b_buffer to client socket and the corresponding file in the cache
tcpCliSock.send(b_buffer)

tmpFile = open(filename.replace('/', '_'), "w+b") # 事实上这里的路径等于 filetouse

# https://stackoverflow.com/questions/48639285/a-bytes-like-object-is-required-not-int
tmpFile.write(b_buffer) # 不要对 bytes 使用 writelines
tmpFile.close()

fileobj.close()

except:
print('Illegal request')
else:
# HTTP response message for file not found
tcpCliSock.send('HTTP/1.1 404 Not Found\r\n'.encode(FORMAT))
tcpCliSock.send('Content-Type:text/html\r\n'.encode(FORMAT))
with open('404 Not Found.html', "r") as f:
notfound_data = f.readlines()
for data in notfound_data:
tcpCliSock.send(data.encode(FORMAT))
tcpCliSock.send("\r\n".encode(FORMAT))

# Close the client and the server sockets
tcpCliSock.close()
tcpSerSock.close()