TCP/IP网络通讯粘包问题
案例:模拟执行shell命令,服务器返回相应的类容。发送指令的客户端容错率暂无考虑,按照正确的指令发送即可。
服务端代码
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 import subprocess 8 9 10 def server_Tcp(): 11 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 12 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 13 server.bind(('', 9999)) 14 server.listen(10) 15 16 while True: 17 client_socket, client_info = server.accept() 18 print("%s 已连接" % str(client_info)) 19 try: 20 21 while True: 22 client_msg = client_socket.recv(1024) 23 if not client_msg: break 24 print("%s 收到指令<<%s" % (client_info, client_msg.decode('utf-8'))) 25 # 处理执行的命令 26 res = subprocess.Popen(client_msg.decode('utf-8'), shell=True, stderr=subprocess.PIPE, 27 stdout=subprocess.PIPE, stdin=subprocess.PIPE) 28 err = res.stderr.read() 29 if err: 30 cmd_err = err 31 else: 32 cmd_err = res.stdout.read() 33 34 # print(cmd_err.decode("gbk")) 35 client_socket.send(cmd_err) 36 except Exception as e: 37 print("%s 已断开" % str(client_info)) 38 print(e) 39 continue 40 client_socket.close() 41 42 server.close() 43 44 45 if __name__ == "__main__": 46 server_Tcp()
客户端代码
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 8 9 def client_Tcp(): 10 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 11 12 client.connect(('127.0.0.1', 9999)) 13 14 while True: 15 msg = input('>>').strip() 16 if not msg: continue 17 if msg == 'quit': break 18 client.send(msg.encode('utf-8')) 19 result = client.recv(1024) 20 print(result) 21 print("<<%s" % (result.decode('utf-8'))) 22 23 client.close() 24 25 26 if __name__ == "__main__": 27 client_Tcp()
说明:python中执行命令的指令模块是
import subprocess
# 处理执行的命令
res = subprocess.Popen(client_msg.decode('utf-8'), shell=True, stderr=subprocess.PIPE,stdout=subprocess.PIPE, stdin=subprocess.PIPE)
执行演示
首先运行服务端,然后运行客户端输入ipconfig指令。
服务端
客户端
我们在windows下的cmd命令下看看,或者是在shell客户端查看Linux系统的一样。
以上客户端接收的结果为什么不是完整的简要说明
首先我们的客户端和服务端在电脑中其实就是两个程序。也就是应用程序,应用程序是无法和硬件接触的,应用程序去调用socket层去处理收发数据,而socket后面是去和操作系统对接,最后我们的收发数据是由操作系统去和硬件对接完成的。我们的数据都是在内存中读取的。如下图
当服务器一次性发送了10M数据,然而客户端每次接收2M这时候客户端就没有接收到完整的数据,下次接收还是在接收上次服务器发送的数据,也就是说,服务端可以发送任意大小的数据(配置条件下),而客户端不知道这次接收多大的数据就产生了黏包。
还有一种情况是服务端连续多次send如果这几次send相隔很近,TCP内部机制会将这个几次send组包为一次send发送内容。
在上述案例中我输入第二个指令dir 结果接收的还是第一次的内容
解决黏包方法之一
思路是在我们每次发送的信息中封装一个头部信息,告诉对方这次发送数据的大小这样客户端就知道要接收多少数据量了。
服务端
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 import subprocess 8 import struct 9 10 BUFFER_SIZE = 1024 11 12 13 def server_Tcp(): 14 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 16 server.bind(('', 7777)) 17 server.listen(10) 18 19 while True: 20 client_socket, client_info = server.accept() 21 print("%s 已连接" % str(client_info)) 22 try: 23 24 while True: 25 client_msg = client_socket.recv(BUFFER_SIZE) 26 if not client_msg: break 27 print("%s 收到指令<<%s" % (client_info, client_msg.decode('utf-8'))) 28 # 处理执行的命令 29 res = subprocess.Popen(client_msg.decode('utf-8'), shell=True, stderr=subprocess.PIPE, 30 stdout=subprocess.PIPE, stdin=subprocess.PIPE) 31 err = res.stderr.read() 32 if err: 33 cmd_err = err 34 else: 35 cmd_err = res.stdout.read() 36 37 # # 第一种方式:解决粘包问题 38 # msg_len = len(cmd_err) 39 # print("数据长度为:", msg_len) 40 # client_socket.send(str(msg_len).encode('utf-8')) 41 # # 马上等待回复 42 # is_ok = client_socket.recv(BUFFER_SIZE) 43 # if is_ok == b"OK": 44 # client_socket.send(cmd_err) 45 46 # 第二种方式:解决粘包问题 47 msg_len = len(cmd_err) 48 msg_len = struct.pack('i', msg_len) 49 # 下面两次发送,在客户端会当成一次接收 50 client_socket.send(msg_len) 51 client_socket.send(cmd_err) 52 53 except Exception as e: 54 print("%s 已断开" % str(client_info)) 55 print(e) 56 continue 57 client_socket.close() 58 59 server.close() 60 61 62 if __name__ == "__main__": 63 server_Tcp()
客户端
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 import struct 8 9 BUFFER_SIZE = 1024 10 11 12 def client_Tcp(): 13 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 15 client.connect(('127.0.0.1', 7777)) 16 17 while True: 18 msg = input('>>').strip() 19 if not msg: continue 20 if msg == 'quit': break 21 22 client.send(msg.encode('utf-8')) 23 24 # # 第一种方式:解决粘包问题 25 # content_length = client.recv(BUFFER_SIZE) 26 # # 规定好第一次发来的内容是内容大小,给服务器回复个OK 27 # client.send(b"OK") 28 # content_length = int(content_length.decode('utf-8')) 29 # 第二种方式:解决粘包问题 30 # 先接收四个字节 31 length_data = client.recv(4) 32 content_length = struct.unpack('i', length_data)[0] 33 34 print("准备接收%d大小的数据") 35 recv_size = 0 36 recv_msg = b'' 37 # 循环获取数据 38 while recv_size < content_length: 39 recv_msg += client.recv(BUFFER_SIZE) 40 recv_size = len(recv_msg) 41 42 print("<<%s" % (recv_msg.decode('gbk'))) 43 44 client.close() 45 46 47 if __name__ == "__main__": 48 client_Tcp()
当我们再次运行后输入ipconfig命令,这时候客户端收到的就是完整的内容了。截图略....
总结:
TCP是面向连接的,传输是基于数据流的,收发两端进行连接,数据传输,断开连接都是要经过来回的确认。TCP协议数据传输是不会丢失的,一次没有接收完,下次依然会接收,直到一端接收到ack时才知道数据接收完成。
UDP是无连接的。面向消息的,不管发送的是什么内容,内部都会为当前消息组装一个报文头。
UDP不会产生粘包问题,因为UDP只收一次,每次都会有个报文头+内容
socketserver ------多线程服务
用法很简单,在服务端代码中,创建一个类,让这个类继承BaseRequestHandler 然后在类中实现父类方法handle ,客户端代码不需做改变。详见代码
服务端
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socketserver 7 8 BUFFER_SIZE = 1024 9 ADDRESS_IP = ('172.16.6.5', 7777) 10 11 12 class Communication(socketserver.BaseRequestHandler): 13 14 def handle(self): 15 print(self.request) # -------> 16 print(self.client_address) 17 while True: 18 try: 19 client_msg = self.request.recv(BUFFER_SIZE) 20 if not client_msg: 21 break 22 23 print("客户端【%s】>>%s" % (self.client_address, client_msg)) 24 self.request.sendall(client_msg.upper()) 25 except Exception as e: 26 print(e) 27 break 28 29 30 if __name__ == "__main__": 31 server = socketserver.ThreadingTCPServer(ADDRESS_IP, Communication) 32 server.serve_forever()
客户端代码
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 8 BUFFER_SIZE = 1024 9 ADDRESS_IP = ('172.16.6.5', 7777) 10 11 12 def client_Tcp(): 13 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 client.connect(ADDRESS_IP) 15 16 while True: 17 msg = input('>>:') 18 client.sendall(msg.encode('utf-8')) 19 20 server_msg = client.recv(BUFFER_SIZE) 21 print("<<%s" % (server_msg.decode('utf-8'))) 22 23 client.close() 24 25 26 if __name__ == "__main__": 27 client_Tcp()
这样就可以实现服务端响应多个客户端了。
验证合法性
案例:写一个服务端,客户端。服务端验证客户端是否是合法的,验证通过的客户端才能正常进行通讯。
思路:和解决粘包思路一样,在头部再次封装一个验证的信息,当客户端连接服务成功后,服务端发送一个验证信息,客户端接收后通过key值加密后发回给服务端,服务端接收客户发来的密文和自己的进行比较,相同则是合法的。
服务端
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socketserver 7 import struct 8 import os 9 import hmac 10 11 BUFFER_SIZE = 1024 12 BIND_IP_PORT = ('172.16.6.5', 9999) 13 AUTO_KEY = b"bc892ed2-8b2c-11e8-a328-f40669b72fef" 14 15 16 class Communication(socketserver.BaseRequestHandler): 17 """ 通讯类型 """ 18 19 def handle(self): 20 # print(self.client_address) 21 # print(self.request) 22 cl, pl = self.client_address 23 print("[%s:%s]客户端已连接" % (cl, pl)) 24 # 合法验证 25 if conn_auth(self.request) == True: 26 # 循环接收客户端信息 27 while True: 28 try: 29 # 等待客户端消息 30 # 数据粘包处理 31 # 数据的前四位存储的是数据的大小 32 # 先接收四个字节 33 length_data = self.request.recv(4) 34 if not length_data: break 35 36 content_length = struct.unpack('i', length_data)[0] 37 38 print("接收客户端[%s][%d]大小的数据" % (self.client_address, content_length)) 39 recv_size = 0 40 recv_msg = b'' 41 # 循环获取数据 42 while recv_size < content_length: 43 recv_msg += self.request.recv(BUFFER_SIZE) 44 recv_size = len(recv_msg) 45 print("[%s]>>%s" % (self.client_address, recv_msg.decode('utf-8'))) 46 # 服务器回复客户端 47 server_msg = input("<<") 48 49 server_send_length = struct.pack('i', len(server_msg)) 50 51 self.request.sendall(server_send_length) 52 self.request.sendall(server_msg.encode("utf-8")) 53 except Exception as e: 54 print("[%s]客户端异常关闭" % self.client_address) 55 print(e) 56 else: 57 print("客户端【%s:%s】非法接入" % (cl, pl)) 58 59 60 def conn_auth(conn): 61 # 合法性验证 62 msg = os.urandom(32) 63 conn.sendall(msg) 64 h = hmac.new(AUTO_KEY, msg) 65 digest = h.digest() 66 respone = conn.recv(len(digest)) 67 return hmac.compare_digest(respone, digest) 68 69 70 if __name__ == "__main__": 71 server = socketserver.ThreadingTCPServer(BIND_IP_PORT, Communication) 72 server.serve_forever()
客户端
1 # -*- coding: utf-8 -*- 2 3 # 声明字符编码 4 # coding:utf-8 5 6 import socket 7 import struct 8 import os, hmac 9 10 BUFFER_SIZE = 1024 11 BIND_IP_PORT = ('172.16.6.5', 9999) 12 AUTO_KEY = b"bc892ed2-8b2c-11e8-a328-f40669b72fef" 13 14 15 def client_Tcp(): 16 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 18 client.connect(BIND_IP_PORT) 19 20 # 合法性验证 21 conn_auth(client) 22 while True: 23 24 msg = input('>>').strip() 25 if not msg: continue 26 if msg == 'quit': break 27 28 server_send_length = struct.pack('i', len(msg)) 29 client.send(server_send_length) 30 client.send(msg.encode('utf-8')) 31 # # 第一种方式:解决粘包问题 32 # content_length = client.recv(BUFFER_SIZE) 33 # # 规定好第一次发来的内容是内容大小,给服务器回复个OK 34 # client.send(b"OK") 35 # content_length = int(content_length.decode('utf-8')) 36 # 第二种方式:解决粘包问题 37 # 先接收四个字节 38 length_data = client.recv(4) 39 content_length = struct.unpack('i', length_data)[0] 40 41 print("准备接收服务端[%d]大小的数据" % content_length) 42 recv_size = 0 43 recv_msg = b'' 44 # 循环获取数据 45 while recv_size < content_length: 46 recv_msg += client.recv(BUFFER_SIZE) 47 recv_size = len(recv_msg) 48 49 print("<< %s" % (recv_msg.decode('utf-8'))) 50 51 client.close() 52 53 54 def conn_auth(conn): 55 msg = conn.recv(32) 56 h = hmac.new(AUTO_KEY, msg) 57 digest = h.digest() 58 conn.sendall(digest) 59 60 61 if __name__ == "__main__": 62 client_Tcp()
程序跑起来..................
客户端发送一个内容
服务端接收
当我把客户端的key值改一下试试..................
客户端连接服务端后,服务端进行验证不通过。