目录
粘包问题
213131
TCP与UDP协议
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
粘包现象
socket收发消息的原理
服务端可以1kb,1kb地发向客户端送数据,客户端的应用程序可以在缓存当中2kb,2kb地取走数据,当然也可以更多,或都更少。也就是说,应用程序看到的数据是来个整体。或者说是一个流。一条消息有多少字节对应用程序是不可见的,TCP协议是面向流的协议,这就是它容易粘包的问题原因。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据所造成的。
tcp协议才会有粘包问题,udp协议没有
此外,发送方引起的粘粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往收集到足够多的数据后才一个TCP段。若连续几次需要发送的数据都很少,通常TCP会根据(Nagle)优化算法,把这些数据合成一个TCP段后发出去,这样接收方就收到了粘包数据。
粘包情况一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)
1、服务端
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
conn,client_addr=server.accept()
res1=conn.recv(1024)
print('第一次:',res1)
res2=conn.recv(1024)
print('第二次:',res2)
conn.close()
server.close()
2、客户端
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
# 这两次发送数据由于数据量小且时间短,所以会被TCP优化到一起
client.send(b'hello')
client.send(b'world')
client.close()
先启动服务端,后再启动客户端,服务端得到的结果为:
第一次: b'helloworld'
第二次: b''
粘包情况二:客户端发关了一段数据,服务端只收了一小部分,服务端下次再收的时候不是从缓冲区拿上次遗留的数据,产生粘包。
情况一的,客户端不变,服务端略作修改,如下:
from socket import *
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
conn,client_addr=server.accept()
res1=conn.recv(2) # 只接受两个字节
print('第一次:',res1)
res2=conn.recv(3)
print('第二次:',res2)
conn.close()
server.close()
第一次: b'he'
第二次: b'llo'
解决方案
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包问题的方法就是在发送端发送前,发一个头文件包,告诉发送的字节流总大小
第一次情况
服务端
from socket import *
import struct
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
conn,client_addr=server.accept()
# 因为struct中的i就是接受4个字节
res_bytes = conn.recv(4)
count_len = struct.unpack('i', res_bytes)[0]
res1 = conn.recv(count_len)
print('第一次:',res1)
res_bytes = conn.recv(4)
count_len = struct.unpack('i', res_bytes)[0]
res2 = conn.recv(count_len)
print('第二次:',res2)
conn.close()
server.close()
客户端
from socket import *
import struct
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
count_len = len('hello')
res_bytes = struct.pack('i', count_len)
# 先发送字节流总大小,再发送数据
client.send(res_bytes)
client.send(b'hello')
count_len = len('world')
res_bytes = struct.pack('i', count_len)
client.send(res_bytes)
client.send(b'world')
client.close()
第二种情况,就以ssh的tasklist命令为例,本来该命令由于太长,无法获取
服务端
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(("127.0.0.1", 8087))
phone.listen(5)
print('wait...')
while True:
conn, client_addr = phone.accept()
print(client_addr)
while True:
try:
cmd = conn.recv(1024)
print(cmd)
pipeline = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = pipeline.stdout.read()
stderr = pipeline.stderr.read()
print(stderr)
print(stdout)
count_len = len(stderr) + len(stdout)
res_len = struct.pack('i', count_len)
# 同理,先发送字节流大小,再发送数据
conn.send(res_len)
conn.send(stdout + stderr)
except ConnectionResetError:
break
conn.close()
客户端
import socket
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8087))
while True:
msg = input('please your enter msg')
phone.send(msg.encode('utf-8'))
count_len = phone.recv(4)
res_len = struct.unpack('i', count_len)[0]
data = phone.recv(res_len)
print(data.decode('gbk'))