基于IO复用的并发服务器设计
实验目的
1) 了解和掌握基于IO复用的网络程序的运行机制和编程方法;
2)能够参考源代码,编写一个网络通信应用程序:客户机发出数据请求命令,服务器根据其命令提供数据;
实验环境
1) 浏览器
2) TCP/IP协议
3) 编程语言:Python
4) linux或者windows系统
实验内容:
编写基于TCP协议的通信程序,包括Server与Client两个部分。实现回声程序:即客户端发送消息,服务器端将收到的消息原样会送给客户端。
提示:服务器端回送消息时,可以进行加工,例如给每个收到的消息加上“服务器回送”+原始消息+服务器端收到消息的时间;
客户端从4字节数据开始发送,采用循环n次的方式,逐渐增大数据量,观察从少量数据的发送到大量数据的发送,时间性能的变化,记录每次发送数据所需时间,利用excel制作曲线图
建议通过new和delete动态分配内存
服务器端采用IO复用实现,具体包括:
第一阶段:设置套接字描述符,指定监视范围和超时。
第二阶段:调用select函数。
第三阶段:查看调用结果。
- 在单机上运行程序,同时开启多个客户端,验证其通信结果;并观察系统内进程的创建和销毁情况。
- 在多机上运行程序,同时开启多个客户端,验证其通信结果;并观察系统内进程的创建和销毁情况。(Server只需运行在一台主机上,Client可在其它主机上运行(要知道Server所在主机的IP地址)。
- 如果有兴趣,可以进一步实现基于UDP协议的IO复用并发通讯程序,或者启动大量客户端,对服务器端进行压力测试。
实验步骤:
本次报告为实验1的实验报告。传输的数据设为字符串“abcd”,随着传输次数i的增加字符串“abcd”的长度也将以i的平方增长(每100次一个循环),代码思想如下:
- 单机实验:
将服务器和客户端的IP地址按以下设置:
在一台主机上先执行程序client2.py,此时客户端会每隔五秒钟监听一次,看服务器是否连接成功,如果连接不成功就会一直提醒,如图:
接下来我们运行服务器端,此时客户端和服务器连接成功并开始输送消息,并告诉我们所连客户端的IP:
服务器接收到成倍增长的“abcd”如图:
客户端接收到服务器端回送的“abcd”串,并在字符串后给出接收该消息所用的时间(单位为毫秒)如图:
至此一个客户端对一个服务器通信实验成功。
接下来我们进行服务器一对多实验,即一个服务器为两个客户端进行通信服务。我们先运行client2,client3两个客户端,两个客户端开始进行监听,接下来我们运行服务器得到以下结果:
服务器:
Client2:
Client3:
- 多机实验
同单机实验一样,只需将服务器和客户端的IP地址改为当前电脑所使用的IP地址即可。
查看当前电脑的IP地址:
将server3.py中的IP改为当前IP如图:
在其他电脑上运行多个客户端
服务端能够成功接收到客户端信息,同时客户端也能接收到服务端的回传消息说明实验成功。
#客户端代码(所有客户端都用的同一个代码):
#客户端
import socket
import struct
import time
while True:
try:
client = socket.socket()
client.connect(('127.0.0.1', 8080))
print('已连接到服务端')
while True:
try:
#将运行时间记录进time.txt
with open('time.txt', 'w') as f:
star_time = time.time()
for i in range(1,100):
msge='abcd'*(i**2)
msg = msge.encode('utf-8')
head = struct.pack('i', len(msg))
client.send(head)
client.send(msg)
# 接收服务端发送的回声消息
echo_head = client.recv(4)
echo_size = struct.unpack('i', echo_head)[0]
echo_data = client.recv(echo_size)
end_time=time.time()
total_time=end_time-star_time
f.write(str(total_time))
f.write('\n')
print('服务器回送信息:', echo_data.decode('utf-8'),'\t',total_time)
#f.close()#记录完成关闭文件
except ConnectionResetError:
print('服务端已中断连接')
client.close()
break
except ConnectionRefusedError:
print('无法连接到服务器')
#服务端代码:
#服务端
import socket
import struct
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
inputs = [server]
outputs = []
message_queues = {}
while inputs:
readable, writable, exceptional = select.select(inputs, outputs, inputs)
for sock in readable:
if sock is server:
conn, client_addr = server.accept()
print('客户端已连接:', client_addr)
conn.setblocking(0)
inputs.append(conn)
message_queues[conn] = []
else:
try:
head = sock.recv(4)
if not head:
print('客户端已关闭:', sock.getpeername())
if sock in outputs:
outputs.remove(sock)
inputs.remove(sock)
del message_queues[sock]
sock.close()
continue
size = struct.unpack('i', head)[0]
data = b""
while len(data) < size:
try:
chunk = sock.recv(size - len(data))
if not chunk:
print('客户端已关闭:', sock.getpeername())
if sock in outputs:
outputs.remove(sock)
inputs.remove(sock)
del message_queues[sock]
sock.close()
break
data += chunk
except BlockingIOError:
break
if not data:
continue
print('已收到客户端信息:', data.decode('utf-8'))
# 加工回送消息
message = data.decode('utf-8')
message_queues[sock].append(message.encode('utf-8'))
if sock not in outputs:
outputs.append(sock)
except ConnectionResetError:
print('客户端已中断连接:', sock.getpeername())
if sock in outputs:
outputs.remove(sock)
inputs.remove(sock)
del message_queues[sock]
sock.close()
for sock in writable:
if sock in message_queues:
queue = message_queues[sock]
while queue:
message = queue.pop(0)
try:
sock.send(struct.pack('i', len(message)))
sock.send(message)
except BlockingIOError:
break
for sock in exceptional:
print('发生异常的套接字:', sock.getpeername())
inputs.remove(sock)
if sock in outputs:
outputs.remove(sock)
del message_queues[sock]
sock.close()
实验结果与分析
为方便统计时间,我添加了一个time.txt来记录运行时间:
time.txt
两个客户端都能接收到从服务器回传回来的消息,而在传统的阻塞IO模型中,每个IO操作都会阻塞线程,导致程序无法同时处理多个IO操作。而本次实验中我们使用select机制利用操作系统提供的IO复用机制,可以同时监视多个IO事件的就绪状态,并在有事件就绪时进行处理,而不阻塞线程,实现服务器1对多的功能。
为下面用excel画曲线图做准备。
由单机实验结果建立接收时间(95次)曲线图:
time.xlsx
观察上图,容易发现与基于TCP的网络程序实验(文章链接)中得到的图有明显的不同:
基于TCP的网络程序实验中的time.xlsx
结果分析:
采用IO复用实现的服务器的曲线图比没有不采用IO复用实现的服务器的曲线图平整光滑,而是有点凹凸不平的,甚至到后面传递信息量较大的情况下还出现了断点。同时,采用IO复用实现的服务器比没有采用IO复用实现的服务器的消息传递的时间要慢很多。
而在基于IO复用的并发服务器中,消息发送机制可以通过以下步骤实现:
1.为每个客户端连接维护一个消息队列:在服务器端,为每个客户端连接创建一个消息队列,用于存储待发送的消息。
2.监听可写事件:在主循环中,使用IO复用机制监听可写事件,即检查是否有连接的发送缓冲区可写,可以继续发送消息。
3.将消息添加到客户端的消息队列中:当需要向某个客户端发送消息时,将消息添加到该客户端连接对应的消息队列中。
4.处理可写事件:当可写事件发生时,遍历所有具有可写缓冲区的连接。对于每个连接,从其消息队列中获取待发送的消息。
5.发送消息:使用非阻塞方式向连接的发送缓冲区写入待发送的消息。如果写操作成功,表示消息已成功发送;如果写操作返回"写入阻塞"错误,表示发送缓冲区已满,稍后再继续发送。
6.更新消息队列:根据实际情况,可以在发送完消息后更新消息队列,例如删除已发送的消息或将未发送的消息保留在队列中等。
其实通过分析IO复用的并发服务器消息发送机制不难理解产生上述差异的原因:
曲线图不平整是因为服务器交替向多个客户端发送信息,向不同客户端发送的信息的信息量有可能存在差别(信息是i次方倍增长的,这也说明了当传输次数够多信息量够大时,曲线图出现了断点)导致的。
而采用IO复用实现的服务器的信息传输时间比没有不采用IO复用实现的服务器的信息传输时间长则是在等待缓冲区数据充满后才发送信息的等待时间。