粘包问题的解决,上传与下载,多用户聊天
- 2019 年 12 月 16 日
- 筆記
subprocess模块补充
1.可以帮你通过代码执行操作系统终端的命令
2.并可以返回终端的执行结果 subprocess.Popen(1,2,3,4) 1:cmd命令 2:shell = True 3:返回正确结果参数 stdout = subprocess.PIPE 4:返回错误的参数 stderr = subprocess.PIPE 返回的数据类型是二进制(bytes) 当res = stdout.read()+stderr.read()时 则正确的和错误的结果都可以返回
#服务端 import subprocess import socket server = socket.socket() server.bind(('127.0.0.1',6666)) while True: try: conn,addr = server.accept() print(addr) cmd = conn.recv(1024).decode('utf-8') res = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) result = res.stdout.read() + res.stderr.read()#这里有人说stdout要放在stderr前面否则可能会出错,不过本人没有测试 conn.send(result) except Exception as e : print(e) break conn.close()
粘包问题
1.什么是粘包问题?
服务端第一次发送数据,客户端无法第一次精确接收完毕,或者每次发送的数据太少而且发送的次数比较的频繁时,下一次发送的数据与上一次数据黏在了一起。
1.无法预测对方需要接收的数据大小和长度。 2.多次连续发送时,发送数据量小其发送时间间隔短的数据会打包并在一起发送。
模拟问题的发生
#服务端 import socket server = socket.socket() server.bind(('127.0.0.1',6666)) server.listen(5) conn,addr = server.accept() print(addr) res1 = conn.recv(1024).decode("utf-8") print(res1) res2 = conn.recv(1024).decode('utf-8') print(res2) res3 = conn.recv(1024).decode('utf-8') #执行结果 ('127.0.0.1', 3066) 123123123
#客户端 import socket client = socket.socket() client.connect(('127.0.0.1',6666)) client.send(b'123') print(b'123') client.send(b'123') print(b'123') client.send(b'123') print(b'123') #执行结果 b'123' b'123' b'123'
可以看到,发送的三个数据报文被当做一个进行接收了。
关于数据报文、数据包、数据报的区别:
数据发送时,由上层向下层封装。 四层,协议层传输的是数据报文,主要是协议格式; 三层,网络层传输的是数据包,包含数据报文,并且增加传输使用的IP地址等三层信息; 二层,数据链路层传输的是数据帧,包含数据包,并且增加相应MAC地址与二层信息。 数据接收的时候,下层向上层解封装。 具体区别就是所工作的层不同,可根据ISO七层模型或者TCP/IP四层模型理解。
2.如何解决粘包问题?
解决这个问题的方法有两种:
1.先读出文件的长度,将文件的长度放在数据头部,传输的过程中连同数据的长度信息一同传过去,这样就可以实现动态的修改接收端的接收数据的长度。但是缺陷是如果数据文件太大就会造成内存爆满。
2.每次按照固定的长度去读取文件然后判断文件的长度,如果文件太大就固定的长度一点一点发送。如果发送的不是文件则按照第一种方式去发送,如果是文件就按照第二种方法去发送。
struct模块 是一个可以将很大的数据压缩成一个固定长度的二进制数据,可以作为报文的报头 这里的作用是获取数据包的数据长度。 struct.pack('i',数据) i模式:会将数据长度压缩成四个byte的一个数据
第一种情况解决方法
#客户端 import socket import struct client = socket.socket() client.connect(('127.0.0.1',6666)) while True: send_data = input('请输入你的指令:').strip() client.send(send_data.encode('utf-8')) header1 = client.recv(4) header = struct.unpack('i',header1) msg = client.recv(header[0]).decode('gbk')#header是一个元组,第一个元素是数据 print(msg)
可以通过执行cmd指令:tasklist查看Windows所有任务进程的方法进行验证
#服务端 import struct import socket import subprocess server = socket.socket() server.bind(('127.0.0.1',6666)) server.listen(5) while True: conn,addr = server.accept() print(addr) while True: cmd = conn.recv(1024).decode('utf-8') res = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) result = res.stdout.read() + res.stderr.read() header = len(result) print(result.decode('gbk')) header1 = struct.pack('i',header) conn.send(header1) conn.send(result)
第二种方法(用于传输较大的文件,本人用700M的电影进行了测试还是会出现内存占用较大的问题可能比直接读入700m要好一些):
#客户端 import socket import struct import json client = socket.socket() client.connect( ('127.0.0.1', 6666) ) while True: file_name = input('请输入你的文件名:').strip() client.send(file_name.encode('utf-8')) init_data = 0 num = 1 tag = 1 with open(r'G:360Downloads怦然心动BD中英双字.rmvb', 'rb') as f: while tag: send_data = f.read(1024) n = len(send_data) if n < 1024:#当读出的数据长度小于1024时就跳出循环 tag = 0 print(send_data, num) # 每次发送1024数据 client.send(send_data)
#接收端 import socket while True: conn, addr = server.accept() try: file_name = conn.recv(1000).decode('utf-8') init_data = 0 # 1.以文件名打开文件,准备写入 with open(f'{file_name}', 'wb') as f: tag = 1 # 一点一点接收文件,并写入 while tag: data = conn.recv(1024) # 2.开始写入视频文件 f.write(data) if len(data) < 1024: tag = 0 init_data += 1 print(init_data) print('文件接收完毕!') except Exception as e: print(e) break conn.close()
UDP协议
UDP(UDP,User Datagram Protoco)是一种数据传输协议。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据报的方法。
UDP协议的特点:
- 不需要建立双向管道
- 不会粘包
- 发数据不需要接收回复
- 不可靠,会丢包
基于UDP的套接字
udp是无链接的,先启动哪一端都不会报错
UDP协议是数据报协议,发空的时候也会自带报头,因此客户端输入空,服务端也能收到
#服务端 import socket #SOCK_DGRAM:代表UDP server = socket.socket(type=socket.SOCK_DGRAM) #服务端绑定计算机的IP和软件的端口号 server.bind(('127.0.0.1',6666)) msg,addr = server.recvfrom(1024)#接收的方法与TCP传输方式下不同 msg1,addr1 = server.recvfrom(1024) msg2,addr2 = server.recvfrom(1024) print(msg,msg1,msg2)
#客户端 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1',6666)#此处与TCP传输套接字的方法不同 client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port) client.sendto(b'hello',server_ip_port)
基于UDP套接字实现多对一聊天
下面的服务端程序有一个bug,就是当服务端与多人聊天的时候必须先回复前一个人发来的消息才能收到后一个人的消息。
#服务端 import socket #SOCK_DGRAM:代表UDP server = socket.socket(type=socket.SOCK_DGRAM) #服务端绑定计算机的IP和软件的端口号 server.bind(('127.0.0.1',6666)) while True: msg,addr = server.recvfrom(1024) print(addr) print(msg.decode('utf-8')) send_msg = input('服务端发送消息:').encode('utf-8') server.sendto(send_msg, addr) msg1, addr1 = server.recvfrom(1024) print(addr1) print(msg1.decode('utf-8')) send_msg = input('服务端发送消息:').encode('utf-8') server.sendto(send_msg, addr1) msg2, addr2 = server.recvfrom(1024) print(addr2) print(msg2.decode('utf-8')) send_msg = input('服务端发送消息:').encode('utf-8') server.sendto(send_msg, addr2)
#客户端1 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1',6666)#此处与TCP传输套接字的方法不同 while True: send_msg = input('客户端1:').encode('utf-8') #发送消息时需加上对方的地址 client.sendto(send_msg,server_ip_port) #可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8')) #客户端2 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1', 6666) # 此处与TCP传输套接字的方法不同 while True: send_msg = input('客户端2:').encode('utf-8') # 发送消息时需加上对方的地址 client.sendto(send_msg, server_ip_port) # 可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8')) #客户端3 import socket client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1', 6666) # 此处与TCP传输套接字的方法不同 while True: send_msg = input('客户端3:').encode('utf-8') # 发送消息时需加上对方的地址 client.sendto(send_msg, server_ip_port) # 可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8'))

socketserver模块
SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

使用这个模块就可以解决多人聊天时不能同时收到多人消息的bug。
ThreadingTCPServer
ThreadingTCPServer实现的Soket服务器内部会为每个client创建一个 “线程”,该线程用来和客户端进行交互。
1、ThreadingTCPServer基础
使用ThreadingTCPServer:
- 创建一个继承自 SocketServer.BaseRequestHandler 的类
- 类中必须定义一个名称为 handle 的方法
- 启动ThreadingTCPServer
这个程序只实现了群发消息,至于怎样实现多人聊天而且消息单发,还有待探索。
#服务端 import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): print(self.request,self.client_address,self.server) conn = self.request conn.sendall('欢迎致电XXXX公司,退出请输‘exit’,和我聊天请输‘0’.'.encode('utf-8')) Flag = True while Flag: data = conn.recv(1024).decode('utf-8') if data == 'exit': Flag = False elif data == '0': conn.sendall('通话可能会被录音.balabala一大推'.encode('utf-8')) else: conn.sendall('请重新输入.'.encode('utf-8')) if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',6666),MyServer) server.serve_forever()
注意这里使用的是TCP协议,别搞错了!
#客户端 import socket client = socket.socket() client.connect(('127.0.0.1',6666))#此处与TCP传输套接字的方法不同 while True: send_msg = input('客户端1:').strip().encode('utf-8') #发送消息时需加上对方的地址 client.send(send_msg) #可以接收任何人的消息 msg = client.recv(1204) print(msg.decode('utf-8'))