TCP通信与Web服务器
TCP与Web服务器
TCP服务是面向连接的服务,其传输服务流程:客户机向服务器发送连接请求,服务器确定连接请求并与客户机程序交换控制信息,在两个进程的套接字之间建立一个TCP连接;客户机向服务器发送请求报文,服务器接收请求报文,产生响应报文并发送到套接字,服务器关闭TCP连接;客户机接受响应报文。
Web的应用层协议是超文本传输协议(HTTP)。HTTP由两个程序实现,客户机程序与服务器程序。
HTTP请求报文格式:
HTTP响应报文格式:
Python实现套接字编程
实验准备
(1)自定义编写的一个myfile.html
文件,表示客户机向服务器申请的文件。为了方便访问,该文件存放在与服务器程序同一目录下。
(2)自定义编写的一个error.html
文件,如果服务器未能找到客户机申请访问的文件,则将该文件返回给客户机(实际上,该文件就是404 Not Found
)。同样为了方便访问,该文件存放在与服务器程序同一目录下。
服务器端程序
# TCP服务器程序:TCPServer.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
import numpy as np
from socket import *
serverSocket = socket(AF_INET,SOCK_STREAM) # 生成服务器的TCP连接套接字
serverPort = 6121 # 端口号
serverSocket.bind(("",serverPort)) # 绑定服务器套接字和端口号
serverSocket.listen(10)
print("服务器已启动,正在提供服务......")
while True:
connectionSocket,address = serverSocket.accept() # 根据客户创建一个TCP连接
try :
# 接收请求报文并读取文件
message = connectionSocket.recv(1024).decode() # 接收客户机的请求报文
print("接收到请求报文的时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")
print("已接收到请求报文:\n", message)
filename = message.split()[1] # 解析请求报文,获取文件名
with open(filename[1:], "r") as f:
content = f.read() # 根据文件名读取文件内容
## 生成响应报文(状态行+首部行+文件内容)
stateRow = "HTTP/1.1 200 OK\r\n" # 状态行
firstRow = "Connection close\r\nDate:"+time.strftime("%Y-%m-%d",time.localtime(time.time()))+"\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:"+str(len(content))+"\r\nContent-Type:html\r\n\r\n" # 首部行
outputdata = stateRow+firstRow+content # 响应报文=状态行+首部行+文件内容
connectionSocket.send(outputdata.encode()) # 返回响应报文字节流
connectionSocket.close() # 关闭TCP连接
except IOError: # 抛出异常
print("[ERROR]The file being fetched is not existed.")
with open("error.html","r") as f: # 异常则打开异常响应文件(404)
content = f.read()
## 生成响应报文(状态行+首部行+文件内容)
stateRow = "HTTP/1.1 404 Not Found\r\n" # 状态行
firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
len(content)) + "\r\nContent-Type:html\r\n\r\n" # 首部行
outputdata = stateRow + firstRow + content # 响应报文=状态行+首部行+文件内容
connectionSocket.send(outputdata.encode()) # 返回错误响应字节流
connectionSocket.close() # 关闭TCP连接
服务器端程序代码解析:生成服务器套接字 serverSocket
,绑定服务器主机与指定的端口号(此处端口号选用6121),服务器保持等待,等待客户机请求。有客户机请求连接时,根据客户机创建一个TCP连接connectionSocket
和address
,在服务器端查找请求报文中请求的文件,如果能够找到该文件,那么将该文件以HTTP响应报文的格式封装后(读取文件内容content
,加上响应报文的头部信息)发送给客户机,否则发送error.html
文件。随后关闭TCP连接,继续等待下一个用户。
客户机端程序
# TCP客户机程序:TCPClient.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
from socket import *
import numpy as np
serverName = "192.168.0.104" # 服务器主机
serverPort = 6121 # 端口号
clientsocket = socket(AF_INET,SOCK_STREAM) # 创建客户机套接字
clientsocket.connect((serverName,serverPort)) # 建立连接
## 发送请求报文并接收服务器的回复
fetch_file = "/myfile.html" # 需要请求的文件
requestRow = "Get "+fetch_file+" HTTP/1.1\r\n" # 请求行
firstRow = "Host:192.168.0.104\r\nUser-agent:Microsoft Edge/100.0.1185.36\r\nConnection:close\r\nAccept-language:ch\r\n\r\n" # 首部行
requestMessages = requestRow+firstRow # 请求报文(请求行+首部行)
print("请求报文发出时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")
start = time.perf_counter()
clientsocket.send(requestMessages.encode()) # 发送请求报文
responseMessage = clientsocket.recv(1024) # 接收服务器的回复
end = time.perf_counter()
print("RTT:",end-start,"s")
print("响应报文:\n",responseMessage.decode(),sep = "")
## 生成本地html文件
f = open('localHtml.html','w')
message = responseMessage.decode() # 报文解码
message = message.split("\r\n") # 解析报文
content = message[-1] # 获取向服务器申请的文件
f.write(content) # 保存到本地文件中
f.close()
## 关闭套接字
clientsocket.close()
客户机端程序代码解析:生成客户机套接字 clientSocket
,绑定服务器主机与指定的端口号(此处端口号选用6121)并向服务器发送连接请求,服务器确认连接后,将需要访问的文件信息按照请求报文的格式封装后发送给服务器,等待服务器的回传。接收服务器的响应报文,解析报文,提取出文件数据,保存到本地 localHtml.html
,关闭连接。
运行程序步骤
(1)运行服务器程序TCPServer.py
,保持服务器在正常工作中,即不能终止程序。
(2)运行客户机程序TCPClient.py
。
测试运行结果
(1)利用客户机TCPClient.py
程序测试服务器:
(a)调用客户机程序,访问服务器中存在的文件(即TCPClient.py
中fetch_file = “/myfile.html”
)
![]() |
![]() |
![]() |
(b)调用客户机程序,访问服务器中不存在的文件(可设置TCPClient.py
中fetch_file = “/file.html”
)
![]() |
![]() |
![]() |
(2)利用浏览器直接请求服务器(服务器IP和端口号:192.168.0.104:6121)
(a)请求访问存在的文件:
(b)请求访问不存在的文件:
服务器端优化
缺陷
当有多个客户机同时向服务器TCPServer.py
发送TCP连接请求时,服务器必须与前一个客户机断开TCP连接后才能与下一个客户机建立连接,这影响了服务器的性能。
实验准备
服务器TCPServer.py
程序决定了它与客户机的TCP连接的时间几乎是很短的。因此,为了模拟下一个客户机请求到达时上一个客户机仍与服务器保持TCP连接,做以下的操作:
(1)为了模拟客户机长时占用服务器,即TCP连接时间较长。修改服务器程序,在服务器向客户机发送响应报文后,让服务器延迟断开TCP连接。代码修改部分如下:
(2)为了让多个客户机访问服务器,只需要多次运行客户机程序即可。(不能确保这十次请求是同时发生的,但是发送10次请求报文的时间在3秒内完成,又由于服务器返回报文需要等待10秒,所以可以模拟认为10次请求是同时发生的)
(注:pycharm可在‘运行/调试配置’中打开‘允许并行允许’,可以让客户机程序同时运行多次)
服务器优化前程序运行结果
运行增加延迟断开TCP连接的服务器程序TCPServer.py
,连续运行客户机程序TCPClient.py
十次。
记录客户机发送请求报文和服务器接收到报文的时刻,计算客户机发送请求报文到接收响应报文的时间差,作为一个时延RRT。实验得到的数据如下表
序号 | 客户机请求报文的发送时间 (%Y-%m-%d %H:%M:%S) | 服务器接收到请求报文的时间 (%Y-%m-%d %H:%M:%S) | 时延RRT (秒/s) |
---|---|---|---|
1 | 2022-04-14 14:36:47.138 | 2022-04-14 14:36:47.964 | 10.0027 |
2 | 2022-04-14 14:36:47.447 | 2022-04-14 14:36:57.974 | 19.7024 |
3 | 2022-04-14 14:36:47.709 | 2022-04-14 14:37:07.981 | 29.4850 |
4 | 2022-04-14 14:36:48.019 | 2022-04-14 14:37:17.993 | 39.1469 |
5 | 2022-04-14 14:36:48.356 | 2022-04-14 14:37:28.000 | 48.8227 |
6 | 2022-04-14 14:36:48.669 | 2022-04-14 14:37:38.008 | 58.5192 |
7 | 2022-04-14 14:36:48.940 | 2022-04-14 14:37:48.017 | 68.2455 |
8 | 2022-04-14 14:36:49.223 | 2022-04-14 14:37:58.018 | 77.9665 |
9 | 2022-04-14 14:36:49.482 | 2022-04-14 14:38:08.023 | 87.7094 |
10 | 2022-04-14 14:36:49.802 | 2022-04-14 14:38:18.025 | 97.3891 |
分析表可知,在服务器与一个客户机建立连接时,当有其他客户机请求连接时,必须要等到前一个客户与服务器断开连接。因此在时延上就表现为:即使是10个客户机是“同时”向服务器发送请求的,相对晚连接的客户机的时延相对较大,最后一个申请连接的客户经过长时间的等待,造成巨大的时延。
服务器程序优化
多线程原理:进程是系统正在运行的应用程序,线程是进程的基本就行单元,进程的所有任务都在线程中执行。一个线程中任务的执行是串行的,当一个线程中执行多个任务时只能按照顺序执行这些任务,即在同一时间内只能执行一个任务。一个进程可以开启多条线程,每条线程可以同时并发执行不同的任务,能够提高程序的执行效率和资源利用率。多线程并发执行,本质上是CPU快速在多条线程之间切换。
Python可以利用threading
模块中的Thread
类的构造器创建线程。即直接对类threading.Thread
进行实例化创建线程,并且通过调用start()
方法启动线程。
将初始服务器程序TCPServer.py
中接收请求报文部分代码包装为函数TCPServer()
,放入threading.Thread()
中进行优化。得到的多进程Web服务器程序Mul_TCPServer.py
如下:
# 多进程TCP服务器程序:Mul_TCPServer.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
import numpy as np
from socket import *
import threading
def TCPServer(connectionSocket,address):
# 将实验1中的服务器程序包装成函数
try:
# 接收请求报文并读取文件
message = connectionSocket.recv(1024).decode() # 接收客户机的请求报文
print("接收到请求报文的时间:", time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())), np.mod(time.time(), 60), sep="")
print("已接收到请求报文:\n", message)
filename = message.split()[1] # 解析请求报文,获取文件名
with open(filename[1:], "r") as f:
content = f.read() # 根据文件名读取文件内容
## 生成响应报文(状态行+首部行+文件内容)
stateRow = "HTTP/1.1 200 OK\r\n" # 状态行
firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
len(content)) + "\r\nContent-Type:html\r\n\r\n" # 首部行
outputdata = stateRow + firstRow + content # 响应报文 = 状态行+首部行+文件内容
time.sleep(10) # 使服务器睡眠10秒,模拟堵塞,便于实现多次请求
connectionSocket.send(outputdata.encode()) # 返回响应报文字节流
connectionSocket.close() # 关闭TCP连接
except IOError: # 抛出异常
print("[ERROR]The file being fetched is not existed.")
with open("error.html", "r") as f: # 出现异常则返回错误的网页(404)
content = f.read()
## ## 生成响应报文(状态行+首部行+文件内容)
stateRow = "HTTP/1.1 404 Not Found\r\n" # 状态行
firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
len(content)) + "\r\nContent-Type:html\r\n\r\n" # 首部行
outputdata = stateRow + firstRow + content # 响应报文 = 状态行+首部行+文件内容
time.sleep(10) # 使服务器睡眠10秒,模拟堵塞,便于实现多次请求
connectionSocket.send(outputdata.encode()) # 返回错误响应字节流
connectionSocket.close() # 关闭TCP连接
serverSocket = socket(AF_INET, SOCK_STREAM) # 生成服务器的TCP连接套接字
serverPort = 6121 # 端口号
serverSocket.bind(("", serverPort)) # 绑定服务器套接字和端口号
serverSocket.listen(10) # 聆听客户连接
while True:
connectionSocket,address = serverSocket.accept() # 等待连接
thread = threading.Thread(target=TCPServer, args=(connectionSocket,address)) # 加入线程,多线程进行处理
thread.start()
对应的,客户机程序Mul_TCPClient.py
也做出部分修改(实现任务与初始客户机TCPClient.py
一致,只是修改时间表示部分):
# TCP客户机程序
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
from socket import *
import numpy as np
serverName = "192.168.0.104" # 服务器主机
serverPort = 6121 # 端口号
clientsocket = socket(AF_INET,SOCK_STREAM) # 创建客户机套接字
clientsocket.connect((serverName,serverPort)) # 建立连接
## 发送请求报文并接收服务器的回复
fetch_file = "/myfile.html" # 需要请求的文件
requestRow = "Get "+fetch_file+" HTTP/1.1\r\n" # 请求行
firstRow = "Host:192.168.0.104\r\nUser-agent:Microsoft Edge/100.0.1185.36\r\nConnection:close\r\nAccept-language:ch\r\n\r\n" # 首部行
requestMessages = requestRow+firstRow # 请求报文(请求行+首部行)
print("请求报文发出时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")
start = time.perf_counter()
clientsocket.send(requestMessages.encode()) # 发送请求报文
responseMessage = clientsocket.recv(1024) # 接收服务器的回复
end = time.perf_counter()
print("RTT:",end-start,"s")
print("响应报文:\n",responseMessage.decode(),sep = "")
## 生成本地html文件
f = open('localHtml.html','w')
message = responseMessage.decode() # 报文解码
message = message.split("\r\n") # 解析报文
content = message[-1] # 获取向服务器申请的文件
f.write(content) # 保存到本地文件中
f.close()
## 关闭套接字
clientsocket.close()
服务器优化后程序运行结果
运行多进程服务器程序Mul_TCPServer.py
,连续运行客户机程序Mul_TCPClient.py
十次。
记录客户机发送请求报文和服务器接收到报文的时刻,计算客户机发送请求报文到接收响应报文的时间差,作为一个时延RRT。实验得到的数据如下表
序号 | 客户机请求报文的发送时间 (%Y-%m-%d %H:%M:%S) | 服务器接收到请求报文的时间 (%Y-%m-%d %H:%M:%S) | 时延RRT (秒/s) |
---|---|---|---|
1 | 2022-04-14 15:15:43.977 | 2022-04-14 15:15:44.836 | 10.0292 |
2 | 2022-04-14 15:15:44.221 | 2022-04-14 14:15:45.077 | 10.0191 |
3 | 2022-04-14 15:15:44.423 | 2022-04-14 14:15:45.279 | 10.0200 |
4 | 2022-04-14 15:15:44.602 | 2022-04-14 14:15:45.459 | 10.0129 |
5 | 2022-04-14 15:15:44.778 | 2022-04-14 14:15:45.635 | 10.0248 |
6 | 2022-04-14 15:15:44.939 | 2022-04-14 14:15:45.931 | 10.4694 |
7 | 2022-04-14 15:15:45.075 | 2022-04-14 14:15:46.023 | 10.0333 |
8 | 2022-04-14 15:15:45.217 | 2022-04-14 14:15:46.074 | 10.0191 |
9 | 2022-04-14 14:15:45.309 | 2022-04-14 14:15:46.167 | 10.0202 |
10 | 2022-04-14 14:15:45.430 | 2022-04-14 14:15:46.288 | 10.0230 |
分析表可知,服务器接受到10条请求报文是在2秒内完成的,考虑到实际客户机发送报文是不同步的,可以认为服务器同时接收到10条请求报文;观察表的“时延”一列,客户机的10次连接均是在10s左右完成,即在这10次请求中,服务器从建立连接到返回响应报文的时间是一样的。因此可以得到结论:先到的报文并不会影响后到的报文与服务器建立连接,服务器是同时处理10次连接请求和回复的。
问题及解决方法
(1)服务器TCPServer.py
报错:OSError: [WinError 10045] 参考的对象类型不支持尝试的操作
服务器创建套接字时使用了错误的SOCK_DGRAM,正确的应该是使用SOCK_STREAM
(2)服务器TCPServer.py
报错:TypeError: a bytes-like object is required, not ‘str’
服务器回复的响应报文应该是字节流,所以需要将字符串类型转码为字节流。
(3)对于一次只能处理一个请求连接的服务器,客户机同时发出多个请求连接时,只有前几个请求正常得到服务器回复,后面的请求出现如下报错:TimeoutError: [WinError 10060] 由于连接方在一段时间内没有正确答复或连接的主机没有反应,连接尝试失败。
服务器提供的可连接数过小,访问失败。解决方法是将可连接数调大,即将服务器程序中的serverSocket.listen(1)
改为serverSocket.listen(10)
。
代码提取
相关代码可在https://github.com/Hny1216/Socket_TCP.git
上提取。