黏包
黏包现象
让咱们基于tcp先制做一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)html
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)算法
#的结果的编码是以当前所在的系统为准的,若是是windows,那么res.stdout.read()shell
读出的#就是GBK编码的,在接收端须要用GBK解码json
#且只能从管道里读一次结果windows
同时执行多条命令以后,获得的结果极可能只有一部分,在执行其余命令的时候又接缓存
收到以前执行的另一部分结果,这种显现就是黏包。服务器
基于tcp协议实现的黏包网络
1 tcp-server:socket
#_*_coding:utf-8_*_
from socket import *#引入套接字的全部模块
import subprocess#引入subprocess模块tcp
ip_port=('127.0.0.1',8888)#设置ip和端口号
BUFSIZE=1024#设置字节大小为1024
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#tcp的服务器为网络传输模式
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)#设置套接字
tcp_socket_server.bind(ip_port)#绑定地址和端口
tcp_socket_server.listen(5)#服务器监听5个字节
while True:#循环为真
conn,addr=tcp_socket_server.accept()#连接和接收地址
print('客户端',addr)#打印客户端地址
while True:#循环为真
cmd=conn.recv(BUFSIZE)#接收信息为5个字节
if len(cmd) == 0:break#若是 命令长度等于0,则退出
res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
stderr=res.stderr.read()
stdout=res.stdout.read()
conn.send(stderr)#连接发送
conn.send(stdout)#连接发送
2 tcp client:
#_*_coding:utf-8_*_
import socket#引入套接字模块
BUFSIZE=1024#设置大小为1024个字节
ip_port=('127.0.0.1',8888)#设置IP地址及端口号,必须和服务器端同样
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#初始化一个套接字对象,设
置为网络传输
res=s.connect_ex(ip_port)#链接服务器的ip地址及端口
while True:#循环为真
msg=input('>>: ').strip()#请输入你的信息(去两边的空格)
if len(msg) == 0:continue#若是信息的长度为0则跳过
if msg == 'quit':break#若是信息='quit'则退出
s.send(msg.encode('utf-8'))#发送文件utf-8编码的信息
act_res=s.recv(BUFSIZE)#s接收信息以1024字节为限制
print(act_res.decode('utf-8'),end='')#打印utf-8的接收信息,end=''一行显
示全部信息
基于udp协议实现的黏包:
1 udp-server:
#_*_coding:utf-8_*_
from socket import *#引入socket包中的全部模块
import subprocess#引入subprocess模块
ip_port=('127.0.0.1',9000)#设置一个ip和端口
bufsize=1024#设置大小为1024
udp_server=socket(AF_INET,SOCK_DGRAM)#获得一个socket的对象,用网络链接
udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
udp_server.bind(ip_port)#绑定ip和端口
while True:#循环为真
#收消息
cmd,addr=udp_server.recvfrom(bufsize)#接收消息设置1024字节
print('用户命令----->',cmd)#打印接收的命令
#逻辑处理
res=subprocess.Popen(cmd.decode('utf-
8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subproce
ss.PIPE)
stderr=res.stderr.read()
stdout=res.stdout.read()
#发消息
udp_server.sendto(stderr,addr)#发送消息到对端地址
udp_server.sendto(stdout,addr)#发送消息到对端地址
udp_server.close()#关闭这个服务器
2 udp—client:
from socket import *#引入全部的套接字模块
ip_port=('127.0.0.1',9000)#ip和端口
bufsize=1024#字节大小为1024
udp_client=socket(AF_INET,SOCK_DGRAM)#udp的客户端用网络链接
while True:#循环为真
msg=input('>>: ').strip()#输入信息
udp_client.sendto(msg.encode('utf-8'),ip_port)#客户端发送信息,到服务器
err,addr=udp_client.recvfrom(bufsize)#接收字节大小为1024的字节
out,addr=udp_client.recvfrom(bufsize)#接收字节大小为1024的字节
if err:#若是有err
print('error : %s'%err.decode('utf-8'),end='')#打印错误:信息内容。
用一行表示
if out:#若是有out
print(out.decode('utf-8'), end='')#打印信息,用一行表示
注意:只有TCP有粘包现象,UDP永远不会粘包
黏包成因
TCP协议中的数据传递
tcp协议的拆包机制
#当发送端缓冲区的长度大于网卡的MTU时,tcp会将此次发送的数据拆成几个数据包发
送出去。
#MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的
单位是字节。 大部分网络设备的MTU都是1500。若是本机的MTU比网关的MTU大,大的
数据包就会被拆开来传送,这样会产生不少数据包碎片,增长丢包率,下降网络速度
。
面向流的通讯特色和Nagle算法
#TCP(transport control protocol,传输控制协议)是面向链接的,面向流的,提
供高可靠性服务。
#收发两端(客户端和服务器端)都要有一一成对的socket,所以,发送端为了将多个
发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将屡次间隔较
小且数据量小的数据,合并成一个大的数据块,而后进行封包。
#这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通讯是
无消息保护边界的。
#对于空消息:tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和
服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输
入的是空内容(直接回车),也能够被发送,udp协议会帮你封装上消息头发送过去。
#可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继
续接收,己端老是在收到ack时才会清除缓冲区内容。数据是可靠的,可是会粘包。
基于tcp协议特色的黏包现象成因 :
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字
节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
此外,发送方引发的粘包是由TCP协议自己形成的,TCP为提升传输效率,发送方每每
要收集到足够多的数据后才发送一个TCP段。若连续几回须要send的数据都不多,一般
TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了
粘包数据。
UDP不会发生黏包
#UDP(user datagram protocol,用户数据报协议)是无链接的,面向消息的,提供
高效率服务。
#不会使用块的合并优化算法,, 因为UDP支持的是一对多的模式,因此接收端的
skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就
有了消息头(消息来源地址,端口等信息),这样,对于接收端来讲,就容易进行区
分处理了。 即面向消息的通讯是有消息保护边界的。
#对于空消息:tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和
服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输
入的是空内容(直接回车),也能够被发送,udp协议会帮你封装上消息头发送过去。
#不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对惟一一
个sendinto(y),收完了x个字节的数据就算完成,如果y;x数据就丢失,这意味着udp根
本不会粘包,可是会丢数据,不可靠。
补充说明:
#用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) –
UDP头(8)=65507字节。用sendto函数发送数据时,若是发送数据长度大于该值,则函
数会返回错误。(丢弃这个包,不进行发送)
#用TCP协议发送时,因为TCP是数据流协议,所以不存在包大小的限制(暂不考虑缓冲
区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的
这段数据并不必定会一次性发送出去,若是这段数据比较长,会被分段发送,若是比
较短,可能会等待和下一次数据一块儿发送。
会发生黏包的两种状况
状况一 发送方的缓存机制
发送端须要等缓冲区满才发送出去,形成粘包(发送数据时间间隔很短,数据了很小
,会合到一块儿,产生粘包)
#服务器端
#_*_coding:utf-8_*_
from socket import *#引入套接字的全部模块
ip_port=('127.0.0.1',8080)#设置ip地址的端口号
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#甚至套接字以网络方式传送
tcp_socket_server.bind(ip_port)#绑定IP地址和端口
tcp_socket_server.listen(5)#设置监听5个字节
conn,addr=tcp_socket_server.accept()#接收链接和地址
data1=conn.recv(10)#数据1接收10个字节
data2=conn.recv(10)#数据2接收10个字节
print('----->',data1.decode('utf-8'))#打印数据1的内容
print('----->',data2.decode('utf-8'))#打印数据2的内容
conn.close()#关闭链接
#客户端
#_*_coding:utf-8_*_
import socket#引入套接字模块
BUFSIZE=1024#设置文件大小为1024
ip_port=('127.0.0.1',8080)#设置ip和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个套接字以网络的
形式传输
res=s.connect_ex(ip_port)#链接ip和端口
s.send('hello'.encode('utf-8'))#发送信息
s.send('egg'.encode('utf-8'))#发送信息
状况二 接收方的缓存机制
接收方不及时接收缓冲区的包,形成多个包接收(客户端发送了一段数据,服务端只
收了一小部分,服务端下次再收的时候仍是从缓冲区拿上次遗留的数据,产生粘包)
#服务器端
#_*_coding:utf-8_*_
from socket import *引入 套接字模块
ip_port=('127.0.0.1',8080)#设置ip地址
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#实例化一个服套接字对象
tcp_socket_server.bind(ip_port)#绑定ip和端口
tcp_socket_server.listen(5)#监听大小为5个字节
conn,addr=tcp_socket_server.accept()#接收链接、地址
data1=conn.recv(2) #一次没有收完整#数据1为接收2个字节
data2=conn.recv(10)#下次收的时候,会先取旧的数据,而后取新的#数据二接收为10个
字节
print('----->',data1.decode('utf-8'))#打印数据1内容
print('----->',data2.decode('utf-8'))#打印数据2内容
conn.close()#关闭链接
#客户端
#_*_coding:utf-8_*_
import socket#引入套接字模块
BUFSIZE=1024#设置大小为1024
ip_port=('127.0.0.1',8080)#IP地址和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个ftp套接字对象
以网络传输
res=s.connect_ex(ip_port)#绑定ip
s.send('hello egg'.encode('utf-8'))#发送信息内容
总结
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是由于发送方和接收方的缓存机制、tcp协议面向流通讯
的特色。
2.实际上,主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节
的数据所形成的
黏包的解决方案
解决方案一
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,因此解决粘包的方
法就是围绕,如何让发送端在发送数据前,把本身将要发送的字节流总大小让接收端
知晓,而后接收端来一个死循环接收完全部数据。
#服务端
#_*_coding:utf-8_*_
import socket,subprocess#引入套接字模块和远程控制模块
ip_port=('127.0.0.1',8080)#设置一个ip地址和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)设置套接字
s.bind(ip_port)#绑定ip和端口
s.listen(5)#设置监听的字节位5
while True:#循环为真
conn,addr=s.accept()#链接和获得地址
print('客户端',addr)#打印客户端和地址
while True:#循环为真
msg=conn.recv(1024)#设置接收1024字节
if not msg:break#若是没有接收信息则打断
res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
stdin=subprocess.PIPE,\
stderr=subprocess.PIPE,\
stdout=subprocess.PIPE)
err=res.stderr.read()#
if err:#若是有错误
ret=err
else:#不然
ret=res.stdout.read()
data_length=len(ret)#长度等于内容长度
conn.send(str(data_length).encode('utf-8'))#链接发送字符串的数据长
度
data=conn.recv(1024).decode('utf-8')#数据接收,设置大小为1024个字节
if data == 'recv_ready':#若是数据等于
conn.sendall(ret)
conn.close()#关闭链接
#客户端
#_*_coding:utf-8_*_
import socket,time#引入套接字和时间模块
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个套接字
res=s.connect_ex(('127.0.0.1',8080))#链接服务器的地址和端口
while True:#循环为真
msg=input('>>: ').strip()#输入你的内容
if len(msg) == 0:continue#若是信息为0则打断
if msg == 'quit':break#若是信息输入'quit'则打断
s.send(msg.encode('utf-8'))#发送信息
length=int(s.recv(1024).decode('utf-8'))#接收一个数字的信息
s.send('recv_ready'.encode('utf-8'))#发送接收的信息
send_size=0#发送 大小为0
recv_size=0#接收 大小为0
data=b''#数据为bytes类型
while recv_size < length:#循环若是接收的长度小于,内容长度
data+=s.recv(1024)#data+=接收的数据
recv_size+=len(data)#接收的大小+=数据长度
print(data.decode('utf-8'))#打印这个数据
存在的问题:
程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字
节流长度,这种方式会放大网络延迟带来的性能损耗.
解决方案进阶
刚刚的方法,问题在于咱们咱们在发送
咱们能够借助一个模块,这个模块能够把要发送的数据长度转换成固定长度的字节。
这样客户端每次接收消息以前只要先接受这个固定长度字节的内容看一看接下来要接
收的信息大小,那么最终接受的数据只要达到这个值就中止,就能恰好很少很多的接
收完整的数据了。
struct模块
该模块能够把一个类型,如数字,转成固定长度的bytes
#>>> struct.pack('i',1111111111111)
#struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
import json,struct
#假设经过客户端上传1T:1073741824000的文件a.txt
#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
#为了该报头能传送,须要序列化而且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #而后发真实内容的字节格式
#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,获得报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头
#最后根据报头的内容提取真实的数据,好比
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
__author__ = 'Linhaifeng'
import struct
import binascii
import ctypes
values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')
print(s1.size,s2.size)
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)
s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)
print('After pack',binascii.hexlify(prebuffer))
print(s1.unpack_from(prebuffer,0))
print(s2.unpack_from(prebuffer,s1.size))
s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
print(s3.unpack_from(prebuffer,0))
使用struct解决黏包
借助struct模块,咱们知道长度数字能够被转换成一个标准大小的4字节数字。所以能够利用这个特色来预先发送数据长度。
发送时 接收时
1先发送struct转换好的数据长度4字节 2先接受4个字节使用struct转换成数字来获取要接收的数据长度
1再发送数据 2再按照长度接收数据
#服务器端(自定制报头)
import socket,struct,json#引入三个模块
import subprocess#引入远程控制模块
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))#绑定一个ip和端口
phone.listen(5)#设置监听大小为5个字节
while True:#循环为真
conn,addr=phone.accept()#接受链接和地址
while True:#循环为真
cmd=conn.recv(1024)#接收1024个字节大小
if not cmd:break#若是没有信息则打断
print('cmd: %s' %cmd)#打印信息
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
#设置远程控制命令
err=res.stderr.read()#读取错误信息
print(err)#打印这个信息
if err:#若是有错误
back_msg=err
else:#不然读取内容
back_msg=res.stdout.read()
conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
conn.sendall(back_msg) #在发真实的内容
conn.close()#关闭链接
#客户端(自定制报头)
#_*_coding:utf-8_*_
import socket,time,struct#引入三个模块
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个套接字对象
res=s.connect_ex(('127.0.0.1',8080))#设置一个IP地址和端口
while True:#循环为真
msg=input('>>: ').strip()#输入信息
if len(msg) == 0:continue#若是信息长度为0则跳过
if msg == 'quit':break#若是输入'quit'则打断
s.send(msg.encode('utf-8'))#发送信息
l=s.recv(4)#接收4个字节
x=struct.unpack('i',l)[0]
print(type(x),x)
# print(struct.unpack('I',l))
r_s=0
data=b''
while r_s < x:
r_d=s.recv(1024)
data+=r_d
r_s+=len(r_d)
# print(data.decode('utf-8'))#打印这个数据
print(data.decode('gbk')) #windows默认gbk编码
咱们还能够把报头作成字典,字典里包含将要发送的真实数据的详细信息,而后json序列化,而后用struck将序列化后的数据长度打包成4个字节(4个本身足够用了)
发送时
先发报头长度,再编码报头内容而后发送,最后发真实内容
接收时:
先收报头长度,用struct取出来,根据取出的长度收取报头内容,而后解码,反序列化,从反序列化的结果中取出待取数据的详细信息,而后去取真实的数据内容
#服务器端定制复杂的头
import socket,struct,json#引入三个模块
import subprocess#引入远程控制命令
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化一个对象
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))#绑定一个ip和端口
phone.listen(5)#监听字节设置为5
while True:#循环 为真
conn,addr=phone.accept()#链接,接受一个地址
while True:#循环为真
cmd=conn.recv(1024)#接收消息,大小为1024
if not cmd:break#若是没有信息,则打断
print('cmd: %s' %cmd)#打印这个信息
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
#设置远程控制命令
err=res.stderr.read()#读取这个错误
print(err)#打印这个错误
if err:#若是有错误
back_msg=err
else:#不然读取内容
back_msg=res.stdout.read()
headers={'data_size':len(back_msg)}#获得一个头的字典大小:对应成都
head_json=json.dumps(headers)#序列化这个头
head_json_bytes=bytes(head_json,encoding='utf-8')#获得bytes结构的头
conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
conn.send(head_json_bytes) #再发报头
conn.sendall(back_msg) #在发真实的内容
conn.close()#关闭链接
#客户端
from socket import *#引入套接字的全部模块
import struct,json#引入两个模块
ip_port=('127.0.0.1',8080)#ip和端口
client=socket(AF_INET,SOCK_STREAM)#实例化一个套接字
client.connect(ip_port)#创建链接
while True:#循环为真
cmd=input('>>: ')#输入内容
if not cmd:continue#若是没有信息则跳过
client.send(bytes(cmd,encoding='utf-8'))#发送一个bytes类型的信息
head=client.recv(4)#接受一个头部
head_json_len=struct.unpack('i',head)[0]#查看头部的长度
head_json=json.loads(client.recv(head_json_len).decode('utf-8'))#加载接收头部的长度
data_len=head_json['data_size']#数据长度的大小
recv_size=0设置接收为0
recv_data=b''#设置接收的数据为bytes类型
while recv_size < data_len:#循环若是接收的字节大小<数据的长度
recv_data+=client.recv(1024)#接收数据+=接收的长度(1024)字节
recv_size+=len(recv_data)#接受大小+=接收的数据长度
print(recv_data.decode('utf-8'))#打印这个接收的数据 #print(recv_data.decode('gbk')) #windows默认gbk编码