实验目的
1) 了解和掌握基于多线程的网络程序的运行机制和编程方法;
2) 能够参考源代码,编写一个网络通信应用程序:客户机发出数据请求命令,服务器根据其命令提供数据;
实验环境
1) 浏览器
2) TCP/IP协议
3) 编程语言:python
4) linux或者windows系统
实验内容:
1)编写基于TCP协议的通信程序,包括Server与Client两个部分。实现回声程序:即客户端发送消息,服务器端将收到的消息原样会送给客户端。
提示:服务器端回送消息时,可以进行加工,例如给每个收到的消息加上“服务器回送”+原始消息+服务器端收到消息的时间;
客户端从4字节数据开始发送,采用循环n次的方式,逐渐增大数据量,观察从少量数据的发送到大量数据的发送,时间性能的变化,记录每次发送数据所需时间,利用excel制作曲线图
建议通过new和delete动态分配内存
服务器端采用多线程实现,所有新接入的客户端均由线程负责,可以考虑每接入一个新客户端就创建一个线程,或者利用线程池机制(可以减少线程的频繁创建和销毁的资源消耗)。请注意线程之间的同步问题,并应用互斥量和信号量技术解决。
2) 在单机上运行程序,同时开启多个客户端,验证其通信结果;并观察服务器端系统内线程的创建和销毁情况。
3) 在多机上运行程序,同时开启多个客户端,验证其通信结果,并观察系统内线程的创建和销毁情况;(Server只需运行在一台主机上,Client可在其它主机上运行(要知道Server所在主机的IP地址),可以在客户端处开发多线程程序替代多个客户端程序,产生并发的访问;)。
实验步骤
本次报告为实验8的实验报告。传输的数据设为字符串“abcd”,随着传输次数i的增加字符串“abcd”的长度也将以i的平方增长(每100次一个循环),代码思想如下:
- 单机实验:
将服务器和客户端的IP地址按以下设置:
在一台主机上先执行程序client2.py,此时客户端会每隔五秒钟监听一次,看服务器是否连接成功,如果连接不成功就会一直提醒,如图:
接下来我们运行服务器端,此时客户端和服务器连接成功并开始输送消息:
服务器接收到成倍增长的“abcd”如图:
至此说明服务器和客户端已经通信结果成功,接下来我们停止client2.py此时服务器提示我们客户端已断开并给出断开客户端的IP:
至此,单机实验中服务器客户端1对1测试完成。
接下来我们进行服务器一对多实验,即一个服务器为两个客户端进行通信服务。我们先运行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 threading
import time
#创建线程类
class ClientThread(threading.Thread):
def __init__(self, client_socket):
threading.Thread.__init__(self)
self.client_socket = client_socket
def run(self):
print('新客户端已连接:', self.client_socket.getpeername())
while True:
try:
head = self.client_socket.recv(4)
if not head:
print('客户端已关闭:', self.client_socket.getpeername())
self.client_socket.close()
break
size = struct.unpack('i', head)[0]
data = b""
while len(data) < size:
chunk = self.client_socket.recv(size - len(data))
if not chunk:
print('客户端已关闭:', self.client_socket.getpeername())
self.client_socket.close()
return
data += chunk
print('已收到客户端信息:', data.decode('utf-8'))
# 加工回送消息
response = data.decode('utf-8')
response_data = response.encode('utf-8')
response_head = struct.pack('i', len(response_data))
self.client_socket.send(response_head)
self.client_socket.send(response_data)
except ConnectionResetError:
print('客户端已中断连接:', self.client_socket.getpeername())
self.client_socket.close()
break
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
threads = []
while True:
client_socket, client_addr = server.accept()
client_thread = ClientThread(client_socket)
client_thread.start()
threads.append(client_thread)
# 等待所有线程结束
for thread in threads:
thread.join()
实验结果与分析
为方便统计时间,我添加了一个time.txt来记录运行时间:
time.txt
为下面用excel画曲线图做准备。
由单机实验中一对多结果建立接收时间(82次)曲线图:
time.xlsx
基于TCP的网络程序实验中的time.xlsx
TCP的网络程序实验可以参考)
对比两个曲线图我们发现两者的走向形状基本一致,甚至数据传输所需的时间几乎都没有差别,而唯一的区别就是在基于多线程的并发服务器的时间曲线图不像基于TCP的网络程序实验中的曲线图平整,而是分成类似的一段一段的结构。
原因是在服务器端的每个线程中,都有维护一个消息队列(message queue)用于存储待发送的消息。
当服务器收到客户端的消息时,将待发送的回应消息加入到对应线程的消息队列中。
而在每个线程中,使用互斥量(锁)来保证对消息队列的互斥访问,避免多个线程同时修改队列导致的竞态条件。
最后在每个线程中,通过循环检查消息队列的方式,将队列中的消息发送给客户端。
这也说明了为什么在多线程的并发服务器的服务端消息的接送是并发的,接收的消息是夹杂在一起的而客户端还是可以按顺序接收到正确的消息。