1.为何会出现粘包??html
让咱们基于tcp先制做一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig)python
注意注意注意:算法
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)shell
的结果的编码是以当前所在的系统为准的,若是是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码json
发送端能够是一K一K地发送数据,而接收端的应用程序能够两K两K地提走数据,固然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个总体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,所以TCP协议是面向流的协议,这也是容易出现粘包问题的缘由。而UDP是面向消息的协议,每一个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不一样的。怎样定义消息呢?能够认为对方一次性write/send的数据为一个消息,须要明白的是当对方send一条信息的时候,不管底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈如今内核缓冲区。windows
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束缓存
所谓粘包问题主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所形成的。服务器
此外,发送方引发的粘包是由TCP协议自己形成的,TCP为提升传输效率,发送方每每要收集到足够多的数据后才发送一个TCP段。若连续几回须要send的数据都不多,一般TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。网络
udp的recvfrom是阻塞的,一个recvfrom(x)必须对惟一一个sendinto(y),收完了x个字节的数据就算完成,如果y>x数据就丢失,这意味着udp根本不会粘包,可是会丢数据,不可靠socket
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端老是在收到ack时才会清除缓冲区内容。数据是可靠的,可是会粘包。
两种状况下会发生粘包。
发送端须要等缓冲区满才发送出去,形成粘包(发送数据时间间隔很短,数据了很小,会合到一块儿,产生粘包)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() 服务端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('feng'.encode('utf-8')) 客户端
接收方不及时接收缓冲区的包,形成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候仍是从缓冲区拿上次遗留的数据,产生粘包)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次没有收完整 data2=conn.recv(10)#下次收的时候,会先取旧的数据,而后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() 服务端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8')) 客户端
拆包的发生状况
当发送端缓冲区的长度大于网卡的MTU时,tcp会将此次发送的数据拆成几个数据包发送出去。
补充问题一:为什么tcp是可靠传输,udp是不可靠传输
基于tcp的数据传输请参考个人另外一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在数据传输时,发送端先把数据发送到本身的缓存中,而后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则从新发送数据,因此tcp是可靠的
而udp发送数据,对端是不会返回确认信息的,所以不可靠
补充问题二:send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,而后由协议控制将缓存内容发往对端,若是待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,因此解决粘包的方法就是围绕,如何让发送端在发送数据前,把本身将要发送的字节流总大小让接收端知晓,而后接收端来一个死循环接收完全部数据
程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
2.解决粘包的方法
为字节流加上自定义固定长度报头,报头中包含字节流长度,而后一次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)
咱们能够把报头作成字典,字典里包含将要发送的真实数据的详细信息,而后json序列化,而后用struck将序列化后的数据长度打包成4个字节(4个本身足够用了)
发送时:
先发报头长度
再编码报头内容而后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,而后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,而后去取真实的数据内容
#为字节流加上自定义固定长度报头,报头中包含字节流长度,而后一次send到对端, # 对端在接收时,先从缓存中取出定长的报头,而后再取真实数据 #struct模块 # 该模块能够把一个类型,如数字,转成固定长度的bytes ''' 咱们能够把报头作成字典,字典里包含将要发送的真实数据的详细信息,而后json序列化, 而后用struck将序列化后的数据长度打包成4个字节(4个本身足够用了) 发送时: 先发报头长度 再编码报头内容而后发送 最后发真实内容 接收时: 先手报头长度,用struct取出来 根据取出的长度收取报头内容,而后解码,反序列化 从反序列化的结果中取出待取数据的详细信息,而后去取真实的数据内容 ''' # >>> struct.pack("i","abc") # Traceback (most recent call last): # File "<pyshell#1>", line 1, in <module> # struct.pack("i","abc") # struct.error: required argument is not an intege #服务端(定制稍微复杂一点的报头) import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_sort=("127.0.0.1",8080) back_log=5 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(ip_sort) phone.listen(back_log) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print("cmd: %s" %cmd) res=subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE ,stdin=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)#序列化成字符串 print(type(head_json)) head_json_bytes=bytes(head_json,encoding="utf-8") #struct.pack("i"转换成包的类型,第二个参数必须是数字) conn.send(struct.pack("i",len(head_json_bytes)))#先发报头的长度 conn.send(head_json.encode("utf-8"))#再发报头 conn.sendall(back_msg)#再发真实的内容 conn.close()
#客户端解决粘包的方法 import socket,struct,json ip_port=("127.0.0.1",8080) tcp_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) tcp_client.connect_ex(ip_port) while True: cmd=input(">>") if not cmd:continue tcp_client.send(cmd.encode('utf-8')) data=tcp_client.recv(4)#接收报头消息长度 num=struct.unpack("i",data)[0]#unpack解包出来是一个元祖 print(num) header=json.loads(tcp_client.recv(num).decode("utf-8"))#经过接受报头长度接受报头 data_len=header["data_size"]#获取发送消息的长度 recv_size=0 recv_data=b'' while recv_size<data_len: recv_data+=tcp_client.recv(1024) recv_size=len(recv_data) print(recv_data.decode("gbk")) #print(recv_data.decode("GBK")) #windows默认编码为GBK