一、实验目的
学会使用socket进行套接字编程,掌握UDP与TCP套接字编程。
二、实验内容
1.选择套接字编程6个作业中的两个进行实验。我选择的是作业1的Web服务器和作业2的UDP ping程序。
2.完成本章与该实验相关的习题。
三、具体实验
(一)Web服务器
在这个编程作业中,你将用 Python 语言开发一个简单的Web服务器,它仅能处理一个请求。具体而言,你的Web服务器将:(1)当一个客户(浏览器)联系时创建一个连接套接字;(2)从这个连接接收HTTP请求:(3)解释该请求以确定所请求的特定文件;(4)从服务器的文件系统获得请求的文件(5)创建一个由请求的文件组成的 HTTP 响应报文,报文前面有首部行;(6)经TCP连接向请求的浏览器发送响应。如果浏览器请求一个在该服务器中不存在的文件,服务器应当返回一个“404 Not Found”差错报文。
在配套网站中,我们提供了用于该服务器的框架代码。你的任务是完善该代码,运行你的服务器通过在不同主机上运行的浏览器发送请求来测试该服务器。如果运行你服务器的主机上已经有一个 Web服务器在运行,你应当为该Web服务器使用一个不同于80端口的其他端口。
1.找到官网配套网站上的框架代码如下:
#import socket module
from socket import *
serverSocket = socket(AF_INET, SOCK_STREAM)
#Prepare a sever socket
#Fill in start
#Fill in end
while True:
#Establish the connection
print 'Ready to serve...'
connectionSocket, addr = #Fill in start #Fill in end
try:
message = #Fill in start #Fill in end
filename = message.split()[1]
f = open(filename[1:])
outputdata = #Fill in start #Fill in end
#Send one HTTP header line into socket
#Fill in start
#Fill in end
#Send the content of the requested file to the client
for i in range(0, len(outputdata)):
connectionSocket.send(outputdata[i])
connectionSocket.close()
except IOError:
#Send response message for file not found
#Fill in start
#Fill in end
#Close client socket
#Fill in start
#Fill in end
serverSocket.close()
2.在代码框架的基础上进行代码填充
(1)创建一个连接套接字serverSocket
1)localhost 是计算机的回环地址(即127.0.0.1),它指向当前机器。为了开发和测试方便,使服务器和客户端都运行在同一台计算机上。
2)而端口号可以任意选一个本机没有被占用的端口号,我这里用的是‘8000’。
3)用.bind将服务器的端口号与该套接字关联起来。
4)用.listen(1)等待并聆听某个客户敲门,参数定义了请求连接的最大数量,这里为1。
(2)接收HTTP请求
1)用while循环使服务器一直运转起来。
2)accept()会阻塞当前线程,直到有客户端请求连接。即,它会一直等待,直到一个客户端通过网络连接到该服务器。如果没有客户端连接,accept()会一直处于挂起状态。serverSocket.accept()方法返回两个值:一个是 connectionSocket,另一个是 addr。
connectionSocket:这是一个新的套接字对象,用于与客户端通信。你可以通过它发送和接收数据。每次有客户端连接时,accept() 返回一个新的套接字实例,它代表与该特定客户端的连接。
addr:这是一个元组,包含客户端的地址信息,通常是 (客户端的 IP 地址, 客户端的端口号)。这可以帮助服务器了解来自哪个客户端的连接请求。
3).recv()会从客户端接收最大1024字节的数据。然而得到这个数据是含二进制数据的字符串,为了后续解析它,我们需要将其转换(即用解码decode())成是由字符组成的文本数据
(3)解释该请求以确定所请求的特定文件
1).split() 是 Python 字符串(str)的一个方法,用于将一个字符串拆分成多个子字符串,并返回一个由这些子字符串组成的列表。你可以指定拆分的分隔符,也可以使用默认的空白字符(空格、换行符、制表符等)作为分隔符。
2)我们先来看一下HTTP请求报文的组成结构:
-
请求行 (Request Line): 请求行是请求报文的第一行,包含了请求方法、请求的 URL 和 HTTP 版本。
-
请求头部 (Request Headers): 请求头部是请求报文中的一部分,它包含了很多额外的信息,例如客户端的环境、服务器的期望响应等。请求头和请求体之间有一个空行。
-
请求体 (Request Body): 请求体在一些 HTTP 请求方法(如 POST 或 PUT)中出现,用于包含客户端向服务器发送的数据(例如,表单提交的数据或上传的文件)。在 GET 请求中,通常没有请求体,数据会包含在 URL 中。
所以,我们用.split后默认以空格进行分割,故数组第二位就是要客户要请求的目录文件,将其取出来。
(4)从服务器的文件系统获得请求的文件
由于文件目录格式是"/file.html",所以我们要将‘/’去掉才是一个完整的文件名,然后用open()打开,同理要将其编码成相应的二进制字符串。再用read()读到outputdata变量中。
(5)创建HTTP响应报文(包含首部行)
1)头文件按照相应格式进行编写,如上图所示。
2)而报文内容则在outputdata中,我们以请求服务器下的test.html文件为例,在我们服务器程序下创建一个test.html并且对其内容进行编写。
(按照html文件格式进行编写)
(6)发送HTTP响应
将相应报文发送给对应客户端。
1)如果浏览器请求一个在该服务器中不存在的文件,服务器返回一个“404 Not Found”差错报文。
2)在Python中,except IOError: 是用来捕获和处理I/O(输入/输出)异常的语句。当你在执行文件操作,如打开、读取或写入文件时,可能会遇到各种I/O错误,例如文件不存在、没有权限等。使用except IOError:可以捕获这些异常,并允许你定义当这些错误发生时应采取的行动。
3.完整代码:
from socket import * # 导入python套接字编程的库
# 创建服务器套接字
serverSocket = socket(AF_INET, SOCK_STREAM)
serverName = 'localhost'
serverPort = 8000
serverSocket.bind((serverName, serverPort))
serverSocket.listen(1) # 设置最大连接数为1
# 主体程序:服务器和客户机进行信息的交互
while True:
print('Ready to serve...')
# 等待客户端连接并接受请求
connectionSocket, addr = serverSocket.accept()
try:
message = connectionSocket.recv(1024).decode('utf-8') # 接收客户端请求报文
# 从HTTP请求报文中解析出请求的文件名
filename = message.split()[1]
print(f"Requested file: {filename[1:]}")
# 打开请求的文件并读取内容
# 假设请求的文件都在服务器程序的当前目录下
f = open(filename[1:], 'r', encoding='utf-8')
outputdata = f.read() # 读取文件内容
# 构造 HTTP 响应头
response_header = 'HTTP/1.1 200 OK\n'
response_header += 'Content-Type: text/html; charset=utf-8\n'
response_header += 'Content-Length: {len(outputdata)}\n'
response_header += '\n'
# 发送 HTTP 响应头
connectionSocket.send(response_header.encode('utf-8'))
# 发送文件内容到客户端
connectionSocket.send(outputdata.encode('utf-8'))
print(f"Sending response header: {response_header}")
print(f"Sending output data: {outputdata}")
# 关闭连接
connectionSocket.close()
except IOError:
response_header = 'HTTP/1.1 404 Not Found\n'
connectionSocket.send(response_header.encode())
connectionSocket.close()
# 关闭服务器主套接字
serverSocket.close()
4.Web展示
(1)首先,运行服务器程序
服务器一直在等待,下面是一个调试输出:
(2)在该服务器的本机的浏览器上访问Web:http://localhost:8000/test.html
(二)UDP ping程序
在这个编程作业中,你将用Python编写一个客户ping程序。该客户将发送一个简单的ping报文,接收一个从服务器返回的对应 pong报文,并确定从该客户发送ping报文到接收到pong报文为止的时延该时延称为往返时延(RTT)。由该客户和服务器提供的功能类似于在现代操作系统中可用的标准ping程序。然而,标准的 ping使用互联网控制报文协议(ICMP)(我们将在第5章中学习ICMP)。此时我们将创建一个非标准(但简单)的基于UDP的ping程序。
你的 ping程序经 UDP向目标服务器发送10个ping报文。对于每个报文,当对应的 pong 报文返回时,你的客户要确定和打印RTT。因为UDP是一个不可靠的协议,由客户发送的分组可能会丢失。为此,客户不能无限期地等待对ping报文的回答。客户等待服务器回答的时间至多为1秒;如果没有收到回答,客户假定该分组丢失并相应地打印一条报文。
在此作业中,你将给出服务器的完整代码(在配套网站中可找到)。你的任务是编写客户代码,该代码与服务器代码非常类似。建议你先仔细学习服务器的代码,然后编写你的客户代码,可以随意地从服务器代码中剪贴代码行。
1.给出一个ping服务器程序
# UDPPingerServer.py
# We will need the following module to generate randomized lost packets import random
from socket import *
import random
# 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:
print("Ready to serve...")
# 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
print(address)
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)
使用 random.randint(0, 10) 生成一个0到10之间的随机整数。
模拟了丢包情况。
实现的功能是将请求报文字符串用.upper()变成大写作为响应报文发给客户端。
2.编写UDP ping客户端程序
完整代码:
import time
from socket import *
# 设置服务器地址和端口
serverName = '127.0.0.1'
serverPort = 12000
# 创建UDP套接字
clientSocket = socket(AF_INET, SOCK_DGRAM)
clientSocket.settimeout(1) # 设置超时时间为1秒
# 发送10次ping请求
for i in range(1, 11):
# 构造要发送的消息
message = f'Ping {i} {time.time()}'
# 记录发送时间
start_time = time.time()
# 发送ping请求
clientSocket.sendto(message.encode(), (serverName, serverPort))
try:
# 接收服务器响应
response, serverAddress = clientSocket.recvfrom(1024)
# 计算往返时间
rtt = time.time() - start_time
print(f'Reply from {serverAddress}: {response.decode()} RTT = {rtt:.10f} seconds')
except timeout:
# 超时未收到响应
print(f'Request timed out for ping {i}')
# 关闭套接字
clientSocket.close()
(1)要计算RTT往返时间,则需要time模块,且用.settimeout(1)设置超时时间为1秒,在发送ping请求之前用开始计时,再收到响应报文后结束计时。
(2)我的ping请求报文组成
Ping+i(第几个请求报文)+当时时间
(3)若等待超时,返回超时消息
3.效果展示
(注:先启动ping服务器程序,再启动ping客户端程序)
(1)启动服务器程序:
(2)启动客户端程序:
由于服务器和客户端在本地运行,网络延迟几乎为零。所以大部分RTT都是0。
(三)实验相关习题
1.在一台主机上安装并编译TCPClient 和 UDPCient Python 程序,在另一台主机上安装并编译 TCPServer 和 UDPServer 程序
a. 假设你在运行 TCPServer 之前运行 TCPClient,将发生什么现象?为什么?
现象:TCPClient 会尝试连接时遇到一个“连接拒绝”(Connection Refused)或类似的错误消息。
(web是http协议基于TCP实现的)
原因:在 TCP 协议中,客户端需要连接到服务器端的某个端口。服务器在指定端口上监听连接请求。如果服务器没有运行并监听该端口,客户端的连接请求就会被拒绝。
b. 假设你在运行 UDPServer 之前运行 UDPCIient,将发生什么现象?为什么?
现象:
原因:对于 UDP 协议,即使 UDPServer 没有运行,客户端仍然可以发送数据。然而,由于服务器没有在目标端口上接收数据,客户端在等待响应时可能会遇到错误,尤其是当使用 recvfrom() 等待数据时。
c. 如果你对客户端和服务器端使用了不同的喘口,将发生什么现象?
现象:
TCP:
UDP:
2.假定在UDPClient.py中在创建套接字后增加了下面一行:
clientSocket.bind((‘’,5432))
有必要修改 UDPServer.py吗? UDPClient 和UDPServer中的套接字端口号是多少?在变化之前它们是多少?
答: 无需修改 UDPServer.py
(1)添加clientSocket.bind((‘’,5432))之前
同理,UDPClient中的套接字端口号为程序自己设置的:12000
如果不显示给客户端添加端口号,操作系统会自动分配一个临时端口。为了获得其端口号需要再服务器程序里添加一条输出调试打印出接收到的客户端的端口号:
可知UDPClient中的套接字端口号为:52489
(2)添加clientSocket.bind((‘’,5432))之后
UDPClient中的套接字端口号为程序自己设置的:12000
UDPClient中的套接字端口号为也是程序自己设置的:5432
验证: