python-基于tcp协议的套接字(加强版)及粘包问题
一、基于tcp协议的套接字(通信循环+链接循环)
服务端应该遵循:
1.绑定一个固定的ip和port
2.一直对外提供服务,稳定运行
3.能够支持并发
基础版套接字:
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) conn, client_addr = server.accept() # 通信循环 while True: data = conn.recv(1024) conn.send(data.upper()) conn.close() server.close()
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) # 通信循环 while True: msg=input('>>: ').strip() client.send(msg.encode('utf-8')) data=client.recv(1024) print(data) client.close()
以上的程序存在两个bug
1.当客户端单方面终止程序时,服务端抛出异常(linux可以用判断是否为空来处理)
2.recv收到空时,一直在等待。
解决方法:
1.异常捕获
2.再输入时进行判断
改进版:
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8082)) server.listen(5) conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: data = conn.recv(1024) if len(data) == 0:break # 针对linux系统 print('-->收到客户端的消息: ',data) conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: msg=input('>>: ').strip() #msg='' if len(msg) == 0:continue client.send(msg.encode('utf-8')) #client.send(b'') # print('has send') data=client.recv(1024) # print('has recv') print(data) client.close()
链接循环:服务器改进
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8082)) server.listen(5) conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: data = conn.recv(1024) if len(data) == 0:break # 针对linux系统 print('-->收到客户端的消息: ',data) conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
模拟ssh实现远程执行命令
from socket import * import subprocess server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: cmd = conn.recv(1024) #cmd=b'dir' if len(cmd) == 0: break # 针对linux系统 obj=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() print(len(stdout) + len(stderr)) conn.send(stdout+stderr) except ConnectionResetError: break conn.close() server.close()
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: cmd=input('>>: ').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) cmd_res=client.recv(1024000) print(cmd_res.decode('gbk')) client.close()
recv其实是和本地计算机要数据,所以解码的时候应该用gbk格式
二、粘包
注:只有tcp存在粘包现象,udp永远不存在粘包。
tcp是面向流的协议,发送端可以1k,1k的发送数据,而接收端可以2k,2k的提取数据,发送方往往收集到足够多的数据后才生成一个tcp段,若连续几次需要发送的数据都很少,通常tcp会根据(Nagle)优化算法,把这些数据合成一个TCP段后发出去,这样接收方就收到了粘包数据。
总的来说,粘包问题就是因为接收方不知道消息之间的界限,不知道一次性提取多少字节造成的。
解决方法:(服务端)为字节流加上一个报头,将这个报头(字典形式)json序列化,编码。然后用struct发送报头的长度,再发送报头,最后发送真实数据
(客户端)先解出报头的长度(struct),再接收报头,将拿到的报头解码再反序列化,得到报头字典,最后接收真正的数据
# 服务端必须满足至少三点: # 1. 绑定一个固定的ip和port # 2. 一直对外提供服务,稳定运行 # 3. 能够支持并发 from socket import * import subprocess import struct import json server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8081)) server.listen(5) # 链接循环 while True: conn, client_addr = server.accept() print(client_addr) # 通信循环 while True: try: cmd = conn.recv(1024) # cmd=b'dir' if len(cmd) == 0: break # 针对linux系统 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stderr.read() # 1. 先制作报头 header_dic = { 'filename': 'a.txt', 'md5': 'asdfasdf123123x1', 'total_size': len(stdout) + len(stderr) } header_json = json.dumps(header_dic) header_bytes = header_json.encode('utf-8') # 2. 先发送4个bytes(包含报头的长度) conn.send(struct.pack('i', len(header_bytes))) # 3 再发送报头 conn.send(header_bytes) # 4. 最后发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
from socket import * import struct import json client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8081)) # 通信循环 while True: cmd=input('>>: ').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) #1. 先收4bytes,解出报头的长度 header_size=struct.unpack('i',client.recv(4))[0] #2. 再接收报头,拿到header_dic header_bytes=client.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #3. 接收真正的数据 cmd_res=b'' recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode('gbk')) client.close()
补充:struct模块
该模块可以帮一个类型,如数字,转成固定长度的bytes
1、 struct.pack
struct.pack用于将Python的值根据格式符,转换为字符串(因为Python中没有字节(Byte)类型,可以把这里的字符串理解为字节流,或字节数组)。其函数原型为:struct.pack(fmt, v1, v2, ...),参数fmt是格式字符串,关于格式字符串的相关信息在下面有所介绍。v1, v2, ...表示要转换的python值。
2、 struct.unpack
struct.unpack做的工作刚好与struct.pack相反,用于将字节流转换成python数据类型。它的函数原型为:struct.unpack(fmt, string),该函数返回一个元组。
import struct obj1=struct.pack('i',13321111) print(obj1,len(obj1))#b'\x97C\xcb\x00' 4 res1=struct.unpack('i',obj1) print(res1[0])#13321111