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()
    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())
        tcpCliSock.send("Content-Type:text/html\r\n".encode())

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

                # 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('404 not found'.encode())

    # Close the client and the server sockets
    tcpCliSock.close()
tcpSerSock.close()
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hoper.J

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值