一、基于TCP协议的socket通信
以打电话为理解方式进行TCP的通信。
Server端代码:
1import socket 2phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #购买电话卡,AF_INET服务器之间网络通信,socket.SOCK_STREAM,流式协议,就是TCP协议 3phone.bind(('127.0.0.1', 8080)) #选择电话号码,绑定IP地址 4phone.listen(5) #开机,监控5个请求,最多能进入6个,第7个会报错 5conn, addr = phone.accept() #等待接听电话,阻塞状态,获取IP和端口 6print(conn, addr) #打印IP和端口 7client_data = conn.recv(1024) #交流过程 8print(client_data) #打印接收到的数据 9conn.send(client_data.upper()) #接收到的英文字母变成大写并回传数据 10conn.close() 11phone.close()
Client端代码:
1import socket 2phone = socket.socket() 3phone.connect(('127.0.0.1', 8080)) 4msg = input('>>>').strip() 5phone.send(msg.encode('utf-8')) 6server_data = phone.recv(1024) #限制最大接收字节 7print(server_data) 8phone.close()
先运行Server端,再运行Client端,代码运行结果:
1------------------------------------Client端运行结果--------------------------------------- 2>>>Others laugh at me for being mad. I laugh at others for not being able to see through. #>>>符号右侧为输入内容 3b'OTHERS LAUGH AT ME FOR BEING MAD. I LAUGH AT OTHERS FOR NOT BEING ABLE TO SEE THROUGH.' #为服务端返回内容 4------------------------------------------------------------------------------------------ 5 6------------------------------------Server端运行结果---------------------------------------- 7<socket.socket fd=244, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 64096)> ('127.0.0.1', 64096) 8b'Others laugh at me for being mad. I laugh at others for not being able to see through.' #接收到客户端的内容 9-------------------------------------------------------------------------------------------
二、单循环模式
Server端代码:
1import socket 2phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #使用TCP协议创建网络通信 3phone.bind(('127.0.0.1', 8080)) #绑定IP地址和端口 4phone.listen(5) #设置最大连接数,但是同一时间只能处理一个请求while 1: #使服务端能够一直接受数据 5 conn, addr = phone.accept() #获取连接到服务器的IP和端口 6 try: 7 client_data = conn.recv(1024) #接收1024字节的数据 8 print(client_data) #打印数据(数字为bytes类型) 9 conn.send(client_data + b'-yes-') #源数据加上-yes-并返回给客户端 10 except Exception: 11 break 12conn.close() 13phone.close()
Client端代码:
1import socket 2phone = socket.socket() 3phone.connect(('127.0.0.1', 8080)) 4 5while 1: 6 msg = input('>>>').strip() #可输入要传输的字符 7 if msg.upper() == 'Q': #如果输出q则退出 8 break 9 elif not msg: #如果msg接到到的内容为空,则结束本次循环 10 continue 11 phone.send(msg.encode('utf-8')) #使用utf-8的编码进行发送数据 12 server_data = phone.recv(1024) #最多只能接受1024字节,只问题会产生粘包 13 print(server_data.decode('utf-8')) #打印解码后的数据 14phone.close()
先运行Server端,再运行Client端,代码运行结果:
1------------------------------------Client端运行结果--------------------------------------- 2>>>laugh #输入要传到S端的内容 3laugh-yes- #打印出来的是S端返回的内容 4>>>crazy 5crazy-yes- 6>>>q 7 8Process finished with exit code 0 9------------------------------------------------------------------------------------------- 10 11------------------------------------Server端运行结果---------------------------------------- 12 13b'laugh' #收到C端发送来的内容b'crazy' 14 15-------------------------------------------------------------------------------------------
三、远程执行命令
3.1 远程执行命令之粘包现象的产生,示例如下:
Server端代码:
1import socket 2import subprocess #远程执行命令需要使用到subprocess模块 3phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)#(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket 5phone.bind(('127.0.0.1', 8080)) 6phone.listen(5) 7while 1: 8 conn, addr = phone.accept() 9 try: 10 cmd = conn.recv(1024) 11 obj = subprocess.Popen(cmd.decode('utf-8'), #要执行的命令或可执行文件的路径。一个由字符串组成的序列(通常是列表),列表的第一个元素是可执行程序的路径,\ 12 #剩下的是传给这个程序的参数,如果没有要传给这个程序的参数,args 参数可以仅仅是一个字符串。 13 shell=True, #Linux下相当于执行/bin/bash,Windows下相当于执行cmd.exe 14 stdout=subprocess.PIPE, #指定子进程的标准输出 15 stderr=subprocess.PIPE, #指定子进程的标准错误输出 16 ) 17 ret = obj.stdout.read() #把标准的输出结果赋值给ret变量 18 ret1 = obj.stderr.read() #把标准的错误输出结果赋值给ret1变量 19 conn.send(ret + ret1) 20 except Exception: 21 break 22conn.close() 23phone.close()
Client端代码:
1import socket 2phone = socket.socket() 3phone.connect(('127.0.0.1', 8080)) 4while 1: 5 msg = input('>>>').strip() 6 if msg.upper() == 'Q': #输出q则退出程序 7 break 8 elif not msg: 9 continue #解决的是输入为空程序会夯住的问题。 10 phone.send(msg.encode('utf-8')) #将接收到的数据进行编码后发送到服务端 11 server_data = phone.recv(1024) #每次只接收1024个字节 12 print(server_data.decode('gbk')) #打印接收数据 13phone.close()
先运行Server端,再运行Client端,代码运行结果:
1------------------------------------Client端运行结果--------------------------------------- 2>>>ipconfig /all #查看机器所有的IP地址 3 4Windows IP 配置 5 6 主机名 . . . . . . . . . . . . . : WINDOWS-CP2ICQS 7 主 DNS 后缀 . . . . . . . . . . . : 8 节点类型 . . . . . . . . . . . . : 混合 9 IP 路由已启用 . . . . . . . . . . : 否 10 WINS 代理已启用 . . . . . . . . . : 否 11 12以太网适配器 本地连接: 13 14 连接特定的 DNS 后缀 . . . . . . . : 15 描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller 16 物理地址. . . . . . . . . . . . . : 20-89-84-D1-E1-8B 17 DHCP 已启用 . . . . . . . . . . . : 否 18 自动配置已启用. . . . . . . . . . : 是 19 本地链接 IPv6 地址. . . . . . . . : fe80::91d8:554a:b7fb:f16e%13(首选) 20 IPv4 地址 . . . . . . . . . . . . : 192.168.77.77(首选) 21 子网掩码 . . . . . . . . . . . . : 255.255.255.0 22 默认网关. . . . . . . . . . . . . : 192.168.77.1 23 DHCPv6 IAID . . . . . . . . . . . : 287345028 24 DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-21-DE-4B-33-20-89-84-D1-E1-8B 25 DNS 服务器 . . . . . . . . . . . : 114.114.114.114 26 TCPIP 上的 NetBIOS . . . . . . . : 已启用 27 28隧道适 #到此时数据明显没有接收全,因为每次只接收1024个字节 29>>>dir #直到发送下一个命令的时候,才把剩余的字节接收完成,这就是粘包现象之一 30配器 isatap.{D3F0159A-0A26-4494-9B2A-92BE10466E18}: #dir命令并未列出当前Windows目录的内容 31 32 媒体状态 . . . . . . . . . . . . : 媒体已断开 33 连接特定的 DNS 后缀 . . . . . . . : 34 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter 35 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 36 DHCP 已启用 . . . . . . . . . . . : 否 37 自动配置已启用. . . . . . . . . . : 是 38 39隧道适配器 Teredo Tunneling Pseudo-Interface: 40 41 媒体状态 . . . . . . . . . . . . : 媒体已断开 42 连接特定的 DNS 后缀 . . . . . . . : 43 描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface 44 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 45 DHCP 已启用 . . . . . . . . . . . : 否 46 自动配置已启用. . . . . . . . . . : 是 47 48>>>q 49 50Process finished with exit code 0 51-------------------------------------------------------------------------------------------
3.2 什么是粘包:
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
3.3 以下两种情况发发生粘包(只有tcp协议会产生粘包):
1,接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
2,发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
四、解决粘包的方法
4.1 使用struct模块
该模块可以把一个类型,如数字转换成固定长度的bytes

1import struct 2ret = struct.pack('i', 183346) #将一个int型数字转化成等长度的bytes类型。 3print(ret, type(ret), len(ret)) 4----------------------------------------运行结果------------------------------------------- 5b'2\xcc\x02\x00' <class 'bytes'> 4 6------------------------------------------------------------------------------------------ 7 8ret1 = struct.unpack('i', ret)[0] #通过unpack反解回来 9print(ret1, type(ret1)) 10----------------------------------------运行结果------------------------------------------- 11183346 <class 'int'> 12------------------------------------------------------------------------------------------ 13 14 15ret = struct.pack('l', 4323241232132324) # 但是通过struct 处理不能处理太大 16print(ret, type(ret), len(ret)) # 报错 17----------------------------------------运行结果------------------------------------------- 18 File "D:/PycharmProjects/python/pack.py", line 21, in <module> 19 ret = struct.pack('l', 4323241232132324) 20struct.error: argument out of range 21 22------------------------------------------------------------------------------------------
4.2 方案代码如下:
1整个流程的大致解释: 2我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。 3我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下 4 5发送时: 6先发报头长度 7再编码报头内容然后发送 8最后发真实内容 9 10接收时: 11先收报头长度,用struct取出来 12根据取出的长度收取报头内容,然后解码,反序列化 13从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
server端代码:
1import socket 2import subprocess 3import struct 4import json 5 6phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 8phone.bind(('127.0.0.1', 8080)) 9phone.listen(5) 10conn, addr = phone.accept() 11while 1: 12 try: 13 cmd = conn.recv(1024) 14 obj = subprocess.Popen(cmd.decode('utf-8'), 15 shell=True, 16 stdout=subprocess.PIPE, 17 stderr=subprocess.PIPE, 18 ) 19 ret = obj.stdout.read() #1.获取执行命令返回的结果,计算结果的总大小 20 ret1 = obj.stderr.read() 21 total_size = len(ret + ret1) 22 head_dict = { #2.构建header字典 23 'filename': 'file.txt', 24 'md5': '11111111111111', 25 'total_size': total_size, 26 } 27 dict_json = json.dumps(head_dict) #3.将字典转换json格式 28 dict_bytes = dict_json.encode('utf-8') #4.将字典转化成bytes 29 dict_len = len(dict_bytes) #5.计算字典bytes类型的大小 30 head_dict_len = struct.pack('i', dict_len) #6.将bytes类型的 字典的长度转化成固定长度的bytes 31 conn.send(head_dict_len) #7.发送字典的总大小 32 conn.send(dict_bytes) #8.发送 bytes类型的字典 33 conn.send(ret) #9.发送数据部分。 34 conn.send(ret1) 35 except Exception: 36 break 37conn.close() 38phone.close()
Client端代码:
1import socket 2import struct 3import json 4 5phone = socket.socket() 6phone.connect(('127.0.0.1', 8080)) 7while 1: 8 msg = input('>>>').strip() 9 if msg.upper() == 'Q': 10 break 11 elif not msg: 12 continue 13 phone.send(msg.encode('utf-8')) 14 dict_size = struct.unpack('i', phone.recv(4))[0] #1.接收字典的大小。 15 header_dict_json = phone.recv(dict_size).decode('utf-8') #2.获取报头字典json格式。 16 header_dict = json.loads(header_dict_json) #3.通过json 反解成 字典 17 data_size = 0 #4.接收数据部分。 18 res = b'' 19 while data_size < header_dict['total_size']: 20 data = phone.recv(1024) 21 res = res + data 22 data_size = data_size + len(data) 23 print(res.decode('gbk')) 24phone.close()
先运行Server端,再运行Client端,代码运行结果:

1>>>ipconfig /all 2 3Windows IP 配置 4 5 主机名 . . . . . . . . . . . . . : WINDOWS-CP2ICQS 6 主 DNS 后缀 . . . . . . . . . . . : 7 节点类型 . . . . . . . . . . . . : 混合 8 IP 路由已启用 . . . . . . . . . . : 否 9 WINS 代理已启用 . . . . . . . . . : 否 10 11以太网适配器 本地连接: 12 13 连接特定的 DNS 后缀 . . . . . . . : 14 描述. . . . . . . . . . . . . . . : Realtek PCIe GBE Family Controller 15 物理地址. . . . . . . . . . . . . : 20-89-84-D1-E1-8B 16 DHCP 已启用 . . . . . . . . . . . : 否 17 自动配置已启用. . . . . . . . . . : 是 18 本地链接 IPv6 地址. . . . . . . . : fe80::91d8:554a:b7fb:f16e%13(首选) 19 IPv4 地址 . . . . . . . . . . . . : 192.168.77.77(首选) 20 子网掩码 . . . . . . . . . . . . : 255.255.255.0 21 默认网关. . . . . . . . . . . . . : 192.168.77.1 22 DHCPv6 IAID . . . . . . . . . . . : 287345028 23 DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-21-DE-4B-33-20-89-84-D1-E1-8B 24 DNS 服务器 . . . . . . . . . . . : 114.114.114.114 25 TCPIP 上的 NetBIOS . . . . . . . : 已启用 26 27隧道适配器 isatap.{D3F0159A-0A26-4494-9B2A-92BE10466E18}: 28 29 媒体状态 . . . . . . . . . . . . : 媒体已断开 30 连接特定的 DNS 后缀 . . . . . . . : 31 描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter 32 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 33 DHCP 已启用 . . . . . . . . . . . : 否 34 自动配置已启用. . . . . . . . . . : 是 35 36隧道适配器 Teredo Tunneling Pseudo-Interface: 37 38 媒体状态 . . . . . . . . . . . . : 媒体已断开 39 连接特定的 DNS 后缀 . . . . . . . : 40 描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface 41 物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0 42 DHCP 已启用 . . . . . . . . . . . : 否 43 自动配置已启用. . . . . . . . . . : 是 44 45>>>dir 46 驱动器 D 中的卷没有标签。 47 卷的序列号是 FCBB-EB88 48 49 D:\PycharmProjects\python\01.py 的目录 50 512018/12/17 17:00 <DIR> . 522018/12/17 17:00 <DIR> .. 532018/12/17 16:11 581 su.py 542018/12/17 15:57 1,011 客户.py 552018/12/17 17:00 1,802 服务.py 56 3 个文件 3,394 字节 57 2 个目录 43,045,126,144 可用字节 58 59>>>q 60 61Process finished with exit code 0
View Code