├── ClientSocket.py ├── MsgProtol.py ├── README.md ├── ServerSocket.py └── testPack.py /ClientSocket.py: -------------------------------------------------------------------------------- 1 | ''''''''' 2 | @file: ClientSocket.py 3 | @author: MRL Liu 4 | @time: 2021/3/31 21:48 5 | @env: Python3 6 | @desc: 提供基于Python程序的客户端通信功能 7 | @ref:https://blog.csdn.net/yannanxiu/article/details/52096465 8 | @blog: https://blog.csdn.net/qq_41959920 9 | ''''''''' 10 | import json 11 | import logging 12 | import socket 13 | from MsgProtol import MsgCmd,MsgProtol 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger("client") 17 | 18 | class ClientSocket(object): 19 | def __init__(self, ip="127.0.0.1", port=5006): 20 | self.ip = ip # ip地址 21 | self.port = port # 端口号 22 | self._buffer_size = 12000 # 接收客户端消息的内存大小 23 | self.msg_protol =MsgProtol() # 网络通信协议 24 | 25 | def Connect(self): 26 | """初始化套接字""" 27 | try: 28 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建Socket对象 29 | self._socket.connect((self.ip,self.port)) # 连接服务器 30 | logger.info("客户端的IP地址是{}:{}".format(self.ip,self.port)) 31 | except socket.error: 32 | self.close() 33 | raise socket.error("无法连接服务器,可能是服务器没有启动") 34 | 35 | def Send(self,cmd,data,recv): 36 | try: 37 | msg = self.msg_protol.pack(cmd,data,recv) 38 | self._socket.send(msg) # 发送消息 39 | logger.info("发送1个数据包->cmd:" + MsgCmd(cmd).name+" body:"+str(data)) 40 | # 是否接收回复 41 | if recv==1: 42 | self.Receive() 43 | except socket.error: 44 | raise 45 | 46 | def Receive(self): 47 | try: 48 | flag = True 49 | logger.info("开始接收消息...") 50 | while flag: 51 | msg = self._socket.recv(self._buffer_size) # 接收消息 52 | flag = self.msg_protol.unpack(msg, self._handle_msg) # 解封消息包 53 | logger.info("接收消息结束...") 54 | except socket.error as e: 55 | logging.debug("客户端接收消息出错") 56 | self.close() 57 | raise 58 | 59 | def _handle_msg(self,headPack,body): 60 | """分类处理接收到的消息字符串""" 61 | if self._socket: 62 | # 数据处理 63 | cmd = MsgCmd(headPack[1]).name # 获取Code的值 64 | is_recv = headPack[2] 65 | logging.info("收到1个数据包->bodySize:{}, cmd:{},recv:{}".format(headPack[0],cmd,is_recv)) 66 | p = json.loads(body) # 将字符串解码并且反序列化为json对象 67 | #print(body) 68 | # 检查消息类型 69 | if cmd == "EXIT": # 关闭消息 70 | self.close() 71 | return 72 | elif cmd == "INFORM": # 通知消息 73 | #msg = p["msg"] 74 | print(p) 75 | else: 76 | logging.error("\n未知的cmd:{0}".format(cmd)) 77 | # 继续接收消息 78 | #self._recv_bytes() 79 | 80 | def close(self): 81 | try: 82 | if(self._socket!=None): 83 | self._socket.close() 84 | logger.info('套接字已经关闭') 85 | except socket.error: 86 | logging.debug("套接字关闭出错") 87 | raise 88 | 89 | if __name__ == '__main__': 90 | client = ClientSocket() 91 | client.Connect() 92 | # 发送一个字典并且让服务端回复 93 | protol={} 94 | protol["type"] = MsgCmd.PARAM.name 95 | protol["apiNumber"]="大家伙" 96 | protol["AcademyName"] = "s" 97 | protol["logPath"] = "大家伙" 98 | protol["brainNames"] = "大家伙" 99 | protol["externalBrainNames"] = "s" 100 | # 发送消息 101 | client.Send(MsgCmd.PARAM.value,protol,1) -------------------------------------------------------------------------------- /MsgProtol.py: -------------------------------------------------------------------------------- 1 | ''''''''' 2 | @file: MsgProtol.py 3 | @author: MRL Liu 4 | @time: 2021/4/1 11:04 5 | @env: Python3 6 | @desc: 网络通信的消息协议 7 | @ref: https://blog.csdn.net/yannanxiu/article/details/52096465 8 | @blog: https://blog.csdn.net/qq_41959920 9 | ''''''''' 10 | import json 11 | import struct 12 | from enum import Enum 13 | 14 | class MsgCmd(Enum): 15 | INFORM = 1 # 通知,不需要回复 16 | REQUEST = 2 # 请求,需要回复 17 | PARAM =3 # 参数 18 | EXIT = 4 #退出 19 | 20 | class MsgProtol(object): 21 | def __init__(self): 22 | self.dataBuffer = bytes() # 接收消息的缓存 23 | self.headerSize = 12 # 3*4,消息头的字节总数 24 | 25 | 26 | def pack(self,cmd,_body,_recv=1): 27 | body = json.dumps(_body) # 将消息正文转换成Json格式,并转换成字节编码 28 | header = [body.__len__(),cmd,_recv] # 将消息头按顺序组成一个列表 29 | headPack= struct.pack("3I", *header) # 使用struct打包消息头,得到字节编码 30 | sendData = headPack+body.encode("utf8") # 将消息头字节和消息正文字节组合在一起 31 | return sendData 32 | 33 | def unpack(self,data,msgHandler): 34 | if data: 35 | self.dataBuffer += data 36 | while True: 37 | # 数据量不足消息头部时跳出函数继续接收数据 38 | if len(self.dataBuffer) < self.headerSize: 39 | #print("数据包(%s Byte)小于消息头部长度,跳出小循环" % len(self.dataBuffer)) 40 | break 41 | # struct中:!代表Network order,3I代表3个unsigned int数据 42 | #msg_length = struct.unpack("I", bytearray(msg[:4]))[0] # 获取信息长度 43 | headPack = struct.unpack('3I', bytearray(self.dataBuffer[:self.headerSize]))# 解码出消息头部 44 | # 获取消息正文长度 45 | bodySize = headPack[0] 46 | # 分包情况处理,跳出函数继续接收数据 47 | if len(self.dataBuffer) < self.headerSize + bodySize: 48 | #print("数据包(%s Byte)不完整(总共%s Byte),跳出小循环" % (len(self.dataBuffer), self.headerSize + bodySize)) 49 | break 50 | # 读取消息正文的内容 51 | body = self.dataBuffer[self.headerSize:self.headerSize + bodySize] 52 | msgHandler(headPack,body.decode("utf8")) 53 | # 粘包情况的处理,获取下一个数据包部分 54 | self.dataBuffer = self.dataBuffer[self.headerSize + bodySize:] 55 | if len(self.dataBuffer)!=0: 56 | return True # 继续接收消息 57 | else: 58 | return False # 不再接收消息 59 | else: 60 | return False # 不再接收消息 61 | 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MNetSocket-Python 2 | 3 | [TOC] 4 | 5 | # 一、项目简介 6 | 7 | 本项目是一个只有4份Python代码的开源小工程,用来学习基于TCP的套接字通信包,可以自定义通信协议,处理分包和粘包。内置一个服务端和客户端程序,也有对应的博客讲解代码。整个工程基本基于良好的面向对象思想,代码注释清晰简洁。 8 | 9 | 这个小项目共有4份代码,各自作用如下: 10 | 11 | | **代码模块** | **主要功能** | 12 | | --------------- | -------------------------------------------- | 13 | | MsgProtol.py | 网络通信的消息协议,提供消息打包和解包功能 | 14 | | ServerSocket.py | 提供基于Python程序的服务端通信功能 | 15 | | ClientSocket.py | 提供基于Python程序的客户端通信功能 | 16 | | testPack.py | 本模块用来设计测试服务端处理分包和粘包的能力 | 17 | 18 | # 二、背景知识 19 | 20 | 基于TCP的**套接字通信**是深入学习Python程序的必备技能之一,**套接字**不仅可以用于**网络编程**,在本地**不同进程之间的通信**、**不同编程语言的程序通信**中也应用十分广泛。 21 | 本篇文章是在之前**了解套接字编程接口**的基础上进一步扩展,写出一套**真正可用于实际程序通信的代码**。 22 | 23 | 本项目会集中研究套接字通信中的**分包和粘包问题**、**自定义通信协议**的方法,方便入门套接字的同学进阶。 24 | 25 | 如果您不了解基础的基于Python的TCP套接字接口,可以先简单阅读我的博客**[Python编程——基于TCP的套接字简单通信](https://blog.csdn.net/qq_41959920/article/details/115328992?spm=1001.2014.3001.5501),如果觉得对您有帮助,欢迎收藏、点赞。** 26 | 27 | 完整的项目开源信息如下: 28 | 29 | | **MNetSocket-Python** | 基于Python的网络通信包 | 30 | | --------------------- | :---------------------------------------------------------: | 31 | | **开发者** | MRL Liu | 32 | | **编程语言** | Python3 | 33 | | **项目描述** | 基于TCP的套接字通信包,可以自定义通信协议,处理分包和粘包 | 34 | | **博客** | https://blog.csdn.net/qq_41959920/article/details/115380403 | 35 | | **GitHub** | https://github.com/MagicDeveloperDRL/MNetSocket-Python | 36 | | **参考博客** | https://blog.csdn.net/yannanxiu/article/details/52096465 | 37 | 38 | # **三、项目研究问题** 39 | 40 | 众所周知,套接字是主流语言提供的用于进行网络进程通信的程序接口,其中最常见的是基于TCP协议的套接字编程,基于TCP协议的套接字可以保证: 41 | 42 | > **可靠传播。**在一般情况下,数据传输过程中数据不会发生丢失,并且数据发送和接收的顺序不会改变,即我发送一个“Hello”和“World”,对方一定会按顺序先后收到“Hello”和“World”。 43 | > 44 | > **数据可靠。**在一般情况下,数据传输过程中无论数据包被如何组合拆分等,都不会添加与发送信息无关的无效信息,即数据不会被污染,按照指定编码方式进行解码即可还原原来的信息。 45 | 46 | 但是在实际进行TCP编程的过程中,如果使用原生的套接字接口,在处理消息通信时,需要处理以下两个典型问题: 47 | 48 | ## 1、分包和粘包问题 49 | 50 | ### (1)分包问题 51 | 52 | > TCP是以**段**(Segment)为单位发送数据的,建立TCP链接后,有一个**最大消息长度**(MSS)。如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送。这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。 53 | 54 | 简单理解,分包现象就是一次传输的数据过多时,TCP协议会自动将本次的数据拆分成多个消息包进行发送。 55 | 56 | 例如,发送方发送字符串”helloworld”,接收方却接收到了两个字符串”hello”和”world”。 57 | 58 | ### (2)粘包问题 59 | 60 | > 在某些特殊环境下,TCP为了提高网络的利用率,会使用一个叫做**Nagle**的算法。该算法是指,发送端即使有要发送的数据,如果很少的话,会延迟发送。如果应用层给TCP传送数据很快的话,就会把两个应用层数据包“粘”在一起,TCP最后只发一个TCP数据包给接收端。 61 | 62 | 简单理解,粘包现象就是当网络繁忙时,TCP协议会将多份小的消息包打包成一个消息包进行发送。 63 | 64 | 例如,发送方发送两个字符串”hello”+”world”,接收方却一次性接收到了”helloworld”。 65 | 66 | ## 2、自定义通信协议 67 | 68 | TCP套接字可以提供基本的字符串类型的信息传输,但是字符串可以代表什么含义,怎么解释它需要开发者来自定义。更重要的是,为了解决分包和粘包问题,我们也必须自定义一个简单的通信协议。 69 | 70 | > **通信协议**听起来很高大上,其实本质上就是**发送方**和**接收方**达成的约定,对每个完整的**消息包格式**做出规定,接收数据时按照这个格式进行检查,从而得到一个个正确的消息包。 71 | 72 | 最简单的消息格式就是我们在消息正文的前方加入**一个固定长度的数字**表示**消息正文的长度**,这样就可以基本解决分包和粘包问题。 73 | 74 | 但是为了之后可以判断我们的消息类型,也为了便于设计客户端和服务端,我们这里稍微复杂一点,即本文定义的消息格式如下: 75 | 76 | | **消息头** | **消息正文** | | | 77 | | -------------------- | ------------------ | ------------------ | ---- | 78 | | 正文长度(bodySize) | 指令类型(cmd) | 是否回复(recv) | 正文 | 79 | | 无符号32位整型变量 | 无符号32位整型变量 | 无符号32位整型变量 | —— | 80 | | 4字节 | 4字节 | 4字节 | —— | 81 | 82 | 本人博客会详细介绍如何实现上述设计。 -------------------------------------------------------------------------------- /ServerSocket.py: -------------------------------------------------------------------------------- 1 | ''''''''' 2 | @file: ServerSocket.py 3 | @author: MRL Liu 4 | @time: 2021/3/31 17:05 5 | @env: Python3 6 | @desc: 提供基于Python程序的服务端通信功能 7 | @ref: https://blog.csdn.net/yannanxiu/article/details/52096465 8 | @blog: https://blog.csdn.net/qq_41959920 9 | ''''''''' 10 | import json 11 | import logging 12 | import socket 13 | from MsgProtol import MsgCmd,MsgProtol 14 | 15 | logging.basicConfig(level=logging.INFO) # 定义日志输出的水平 16 | logger = logging.getLogger("server") 17 | 18 | 19 | 20 | class ServerSocket(object): 21 | def __init__(self, ip="127.0.0.1", port=5006): 22 | self.ip = ip # ip地址 23 | self.port = port # 端口号 24 | self._buffer_size = 12000 # 接收客户端消息的内存大小 25 | self._is_init_socket = False # 套接字是否初始化 26 | self._recv_init_msg = False # 是否接收到客户端连接发送来的初始化参数 27 | self.msg_protol = MsgProtol() # 网络消息,为了调用其打包和解包功能 28 | self._init_socket() # 初始化套接字 29 | self._socket.settimeout(30) # 设置套接字的监听时间 30 | self._conn_client() # 连接客户端 31 | 32 | def _init_socket(self): 33 | """初始化套接字""" 34 | try: 35 | # 初始化Socket 36 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建Socket对象 37 | self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置Socket对象 38 | ip_port = (self.ip, self.port) # 定义IP地址和端口号 39 | logging.info("服务端的IP地址是 {}:{}".format(self.ip,self.port)) 40 | self._socket.bind(ip_port)# 绑定IP地址 41 | self._socket.setblocking(False)# 设置阻塞模式为False 42 | self._is_init_socket = True 43 | except socket.error: 44 | self._is_init_socket = True 45 | self.close() 46 | raise socket.error("无法初始化服务端套接字 ") 47 | 48 | def _conn_client(self): 49 | """连接客户端""" 50 | try: 51 | try: 52 | self._socket.listen(1)# 开启侦听 53 | self._client_socket, addr = self._socket.accept()# 接收到请求 54 | logging.info('接收到客户端连接') 55 | self._client_socket.settimeout(30) # 设置客户端套接字的连接时长 56 | except socket.timeout as e: 57 | raise socket.error("客户端连接超时") 58 | 59 | self._recv_bytes() # 接收消息 60 | except socket.error: 61 | logging.debug("连接客户端出错") 62 | self.close() 63 | raise 64 | 65 | def _recv_bytes(self): 66 | """接收客户端的字节消息,按照消息长度切分成一个完整的字符串""" 67 | try: 68 | flag = True 69 | logger.info("开始接收消息...") 70 | while True and self._is_init_socket: 71 | msg = self._client_socket.recv(self._buffer_size)# 接收消息 72 | flag = self.msg_protol.unpack(msg,self._handle_msg)# 解封消息包 73 | #logger.info("接收消息结束...") 74 | except socket.timeout as e: 75 | logging.debug("客户端发送消息超时, 即将关闭客户端套接字") 76 | self.close() 77 | 78 | def _handle_msg(self,headPack,body): 79 | """分类处理接收到的消息字符串""" 80 | if self._is_init_socket: 81 | # 数据处理 82 | cmd = MsgCmd(headPack[1]).name # 获取Code的值 83 | is_recv = headPack[2] 84 | logging.info("收到1个数据包->bodySize:{}, cmd:{},recv:{}".format(headPack[0],cmd,is_recv)) 85 | p = json.loads(body) # 将字符串解码并且反序列化为json对象 86 | #print(body) 87 | # 检查消息类型 88 | if cmd == "EXIT": 89 | self.close() 90 | return 91 | elif cmd == "INFORM": 92 | print(p) 93 | elif cmd == "PARAM": 94 | # 存储参数信息 95 | self._log_path = p["logPath"] 96 | self._brain_names = p["brainNames"] 97 | print(self._log_path) 98 | print(self._brain_names) 99 | self._recv_init_msg = True 100 | if is_recv ==1: 101 | print("应该发送消息") 102 | self._send(MsgCmd.INFORM.value,"这是消息") 103 | elif cmd == "REQUEST": 104 | print(p) 105 | if is_recv ==1: 106 | print("应该发送消息") 107 | self._send(MsgCmd.INFORM.value,"这是消息") 108 | else: 109 | logging.error("\n未知的cmd:{0}".format(cmd)) 110 | # 继续接收消息 111 | #self._recv_bytes() 112 | 113 | def _send(self,cmd,data,is_recv=0): 114 | """发送一个消息字符串""" 115 | try: 116 | #obj = {"type": MsgType.REQUEST.name, "msg":msg,"time": time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))} 117 | msg = self.msg_protol.pack(cmd, data,is_recv) 118 | self._client_socket.send(msg) # 发送消息 119 | logger.info("发送1个数据包->cmd:" + MsgCmd(cmd).name + " body:" + str(data)) 120 | except socket.error: 121 | raise 122 | 123 | def close(self): 124 | logger.info("服务端套接字正在关闭..._loaded:" + str(self._recv_init_msg) + " _open_socket:" + str(self._is_init_socket)) 125 | # 当服务端套接字被初始化过并且连接到了客户端连接则关闭客户端套接字 126 | if self._recv_init_msg & self._is_init_socket: 127 | self._client_socket.send(b"EXIT") # 通知客户端关闭连接 128 | self._client_socket.close() #关闭客户端调节 129 | self._recv_init_msg = False 130 | # 当服务套接字被初始化过 131 | if self._is_init_socket: 132 | self._socket.close() # 关闭服务端套接字 133 | self._is_init_socket = False 134 | else: 135 | raise socket.error("无法关闭服务端套接字,因为没有初始化") 136 | 137 | if __name__=='__main__': 138 | server = ServerSocket() 139 | -------------------------------------------------------------------------------- /testPack.py: -------------------------------------------------------------------------------- 1 | ''''''''' 2 | @file: testPack.py 3 | @author: MRL Liu 4 | @time: 2021/4/1 16:42 5 | @env: Python3 6 | @desc: 本模块用来设计测试服务端处理分包和粘包的能力 7 | @ref: https://blog.csdn.net/yannanxiu/article/details/52096465 8 | @blog: https://blog.csdn.net/qq_41959920 9 | ''''''''' 10 | import socket 11 | import time 12 | import struct 13 | import json 14 | 15 | host = "localhost" 16 | port = 5006 17 | 18 | ADDR = (host, port) 19 | 20 | if __name__ == '__main__': 21 | client = socket.socket() 22 | client.connect(ADDR) 23 | 24 | # 正常数据包定义 25 | ver = 1 26 | body = json.dumps(dict(hello="world")) 27 | print(body) 28 | cmd = 1 29 | header = [body.__len__(),cmd,ver] 30 | headPack = struct.pack("!3I", *header) 31 | sendData1 = headPack+body.encode() 32 | 33 | # 分包数据定义 34 | ver = 2 35 | body = json.dumps(dict(hello="world2")) 36 | print(body) 37 | cmd = 1 38 | header = [body.__len__(),cmd,ver] 39 | headPack = struct.pack("!3I", *header) 40 | sendData2_1 = headPack+body[:2].encode() 41 | sendData2_2 = body[2:].encode() 42 | 43 | # 粘包数据定义 44 | ver = 3 45 | body1 = json.dumps(dict(hello="world3")) 46 | print(body1) 47 | cmd = 1 48 | header = [body.__len__(),cmd,ver] 49 | headPack1 = struct.pack("!3I", *header) 50 | 51 | ver = 4 52 | body2 = json.dumps(dict(hello="world4")) 53 | print(body2) 54 | cmd = 1 55 | header = [body.__len__(),cmd,ver] 56 | headPack2 = struct.pack("!3I", *header) 57 | 58 | sendData3 = headPack1+body1.encode()+headPack2+body2.encode() 59 | 60 | # 正常数据包 61 | client.send(sendData1) 62 | time.sleep(3) 63 | 64 | # 分包测试 65 | client.send(sendData2_1) 66 | time.sleep(0.2) 67 | client.send(sendData2_2) 68 | time.sleep(3) 69 | 70 | # 粘包测试 71 | client.send(sendData3) 72 | time.sleep(3) 73 | client.close() 74 | 75 | 76 | --------------------------------------------------------------------------------