网络编程——通信循环链接循环粘包问题
Posted linagcheng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程——通信循环链接循环粘包问题相关的知识,希望对你有一定的参考价值。
通信循环、链接循环、粘包问题
一、通信循环
服务端和客户端可以进行连续的信息交流
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) conn, addr = ser_socket.accept() while True: try: # 抛出异常,若不抛出处理,一旦客户端强行退出,服务端就会报错 data = conn.recv(1024) print(data.decode(‘utf-8‘)) conn.send(data.upper()) except ConnectionResetError: break conn.close() ser_socket.close()
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) #通信循环,可以多次输入 while True: msg = input(‘>>>>:‘).strip() if len(msg) == 0: # 如果输入为空,给服务端发送信息之后,服务端什么都没接受,一直处于阻塞状态 continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘utf-8‘)) cli_socket.close()
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制
二、链接循环
可以启动多个客户端,但是只有一个客户端是处于连接状态,其余部分在半连接池等待连接,等待的数量不能超过半连接池的最大监听数量
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) #链接循环,可以同时启动最多6个客户端,但是只有一个处于连接状态,其余最多5个在半连接池等待。只有当连接状态的客户端断开连接,下一个客户端才进入连接 while True: conn, addr = ser_socket.accept() # 通信循环 while True: try: data = conn.recv(1024) print(data.decode(‘utf-8‘)) conn.send(data.upper()) except ConnectionResetError: break conn.close() ser_socket.close()
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) while True: msg = input(‘>>>>:‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘utf-8‘)) cli_socket.close()
三、粘包问题
1、模拟ssh远程执行命令
from socket import socket, AF_INET, SOCK_STREAM import subprocess ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8882)) ser_socket.listen(5) while True: conn, addr = ser_socket.accept() while True: try: data = conn.recv(1024) obj = subprocess.Popen(data.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() ser_socket.close()
from socket import socket, AF_INET, SOCK_STREAM cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8882)) while True: msg = input(‘>>>‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘gbk‘)) # Windows系统,默认编码gbk,所以用gbk解码 cli_socket.close()
2、产生粘包原因
(1)所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
(2)此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
在上面的例子中,如果执行命令tasklist,那么就会存在粘包问题。由于TCP协议是流式协议,所以数据都以流的形式传输。假如数据大小是123456,可是已经设定了接收的大小 是1024,所以只接受了数据中的一小部分,但是,剩余部分数据并不会消失,会一直存在于操作系统中,所以下一次接收数据的时候是优先从剩余数据中接收。这样所有数据就乱套了,这就是粘包问题。
3、发生粘包的两种情况
(1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) conn, addr = ser_socket.accept() data = conn.recv(1024) print(‘第一次接收:‘, data.decode(‘utf-8‘)) data1 = conn.recv(5) print(‘第二次接收:‘, data1.decode(‘utf-8‘)) data2 = conn.recv(1024) print(‘第三次接收:‘, data2.decode(‘utf-8‘)) conn.send(data.upper()) conn.close() ser_socket.close()
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) cli_socket.send(‘hello‘.encode(‘utf-8‘)) cli_socket.send(‘world‘.encode(‘utf-8‘)) cli_socket.send(‘object‘.encode(‘utf-8‘)) # data = cli_socket.recv(1024) # print(data.decode(‘utf-8‘)) cli_socket.close()
(2)接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
例如:模拟ssh远程执行命令,若执行tasklist命令,在客户端,无法几次性全部接受执行结果,所以剩余结果会在下一次执行命令式优先接收
from socket import socket, AF_INET, SOCK_STREAM import subprocess ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8882)) ser_socket.listen(5) while True: conn, addr = ser_socket.accept() while True: try: data = conn.recv(1024) obj = subprocess.Popen(data.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() ser_socket.close()
from socket import socket, AF_INET, SOCK_STREAM cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8882)) while True: msg = input(‘>>>‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘gbk‘)) cli_socket.close()
4、解决粘包问题的方法
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
(1)简单版本
# 服务端必须满足至少三点: # 1. 绑定一个固定的ip和port # 2. 一直对外提供服务,稳定运行 # 3. 能够支持并发 from socket import * import subprocess import struct 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=struct.pack(‘i‘,len(stdout) + len(stderr)) # 2. 再发送报头 conn.send(header) # 3. 最后发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()
from socket import * import struct 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. 先收报头,从报头里解出数据的长度 header=client.recv(4) total_size=struct.unpack(‘i‘,header)[0] #2. 接收真正的数据 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()
(2)终极版本
# 服务端必须满足至少三点: # 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()
以上是关于网络编程——通信循环链接循环粘包问题的主要内容,如果未能解决你的问题,请参考以下文章