一、tcp粘包问题出现的原因

前引:

tcp的客户端与服务端进行通信的时候是以一种数据流的形式

服务端把要发送的数据送到服务端的缓存,通过网卡把数据从服务端的缓存到客户端的缓存,反过来同理。

而且这种数据流的交互是在两个不同的单向链接中进行,本身互不影响。

粘包问题

当我们服务端要给客户端回一个非常大的数据,但是客户端的最大接受量只设置了1024,这时候客户端只会接受服务端数据流的前1024个字节。其余的都在客户端的缓存里躺着。

这时候即使是修改最大接受量也行不通,因为数据流在数据传输完之前,我们不知道整个数据流有多大,而且,如果数据流的大小超出了我们缓存大小,我们即使把最大接收量改成无穷大依旧不行,因为当数据流把缓存占满了,其他数据流还在传输的时候,客户端就已经在读取数据了。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

二、解决粘包问题low的办法

粘包问题的根源在于,接受数据一方不知道这个数据的大小,所以我们可以让发送方在发送数据之前就把数据的大小先发送给接收方,以便接收方通过一个循环去完整的接受所有数据流

服务端

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port)
s.listen(5) while True:
conn,addr=s.accept()
print('客户端',addr)
while True:
msg=conn.recv(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)
# 把数据长度编码成utf8格式,并发送数据长度
conn.send(str(data_length).encode('utf-8'))
# 获取接收方发来的准备完毕信息
data=conn.recv(1024).decode('utf-8')
if data == 'recv_ready':
# 发送真实数据
conn.sendall(ret)
conn.close()

客户端

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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
if msg == 'quit':break s.send(msg.encode('utf-8'))
# 接受服务端发送的数据长度
length=int(s.recv(1024).decode('utf-8'))
# 向服务端发送准备好接受数据
s.send('recv_ready'.encode('utf-8'))
send_size=0
recv_size=0
data=b''
while recv_size < length:
# 通过循环以每次1024个字节数去接受服务端发回来的数据
data+=s.recv(1024)
recv_size+=len(data) print(data.decode('utf-8'))

low的原因:

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

三、egon式解决粘包问题

上面一种办法虽然解决了粘包问题,但是多了一次io操作。为了避免这种多于操作,我们要知道那次io的目的是什么

为了在数据传输之前就知道数据流的大小,但是我们本身不知道数据长度的大小,所以要分开发送。

这里引入struct模块,用来把数字转换成固定长度的bytes

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)

服务端

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)) phone.listen(5) 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)
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_*_
__author__ = 'Linhaifeng'
import socket,time,struct 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
if msg == 'quit':break s.send(msg.encode('utf-8'))
# 这是我们是先设定好的已经知道数据头长度为4,取第一个数据流的前四个字节进行解码操作就能得到数据流长度
l=s.recv(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把我们的数据头字典序列化,再用struct打包成数字发送给接收端进行反解得到头长度。

原理和上面的方法类似

只传数据流长度的情况:

我们不知道数据流长度,所以先把数据流长度通过struct打包成固定长度的4个字节,发送给客户端,客户端在事先已经被我们定义好了把数据流的前4个字节反解成数据流长度。

传数据头的情况

我们不知道数据头的长度,所以事先把数据头给json序列化成字符串,把序列化的字符串长度通过struct打包成固定长度的4个字节,发送给客户端,客户端反序列化得到数据头长度,就可以再限定下一次接受数据的长度,接受到数据头后,解析出里面的数据流大小,再通过循环把数据流完整的取出来

服务端

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)) phone.listen(5) 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)
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')
# 第一次发送:把数据头序列化成二进制的长度先发送出去
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)
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'))
# 事先先定好我们的数据头长度的接收(第一次接收:数据头长度)
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
recv_data=b''
# 第三次接受,循环接受数据流
while recv_size < data_len:
recv_data+=client.recv(1024)
recv_size+=len(recv_data) print(recv_data.decode('utf-8'))
#print(recv_data.decode('gbk')) #windows默认gbk编码

四、实现并发

1 tcp

服务端

import socketserver

# 自定义的类
class MyRequestHandle(socketserver.BaseRequestHandler):
# 必须要写的handle方法 在使用这个类的时候会直接执行这个方法
def handle(self):
try:
while True:
# self.request == conn()
data = self.request.recv(1024)
if len(data) == 0: break
print('客户端send:', data.decode('utf-8'))
self.request.send(data.upper())
except Exception:
self.request.close()
# 固定写法,在接受到一个客户端发来的请求时,启动一个线程去为他服务
s=socketserver.ThreadingTCPServer(('127.0.0.1',8888),MyRequestHandle)
s.serve_forever()

客户端

from socket import *

client = socket(AF_INET,SOCK_STREAM)

client.connect(('127.0.0.1',8888))

while True:
msg = input(">>>:")
if msg == 'q':break
if len(msg) == 0:continue
client.send(msg.encode('utf-8'))
rec = client.recv(1024)
print(rec)
client.close()

2 udp

服务端

import socketserver

class MyRequestHandle(socketserver.BaseRequestHandler):
def handle(self):
data = self.request[0]
sever = self.request[1]
addr = self.client_address
print('客户端send:', data.decode('utf-8'))
sever.sendto(data.upper(),addr) s = socketserver.ThreadingUDPServer(('127.0.0.1',8888),MyRequestHandle)
s.serve_forever()

客户端

from socket import *

client = socket(AF_INET,SOCK_DGRAM)

while True:
msg = input(">>>:")
if msg == 'q':break
if len(msg) == 0:continue
client.sendto(msg.encode('utf-8'),('127.0.0.1',8888))
rec = client.recvfrom(1024)
print(rec)
client.close()

day36 解决粘包问题的更多相关文章

  1. c# socket 解决粘包,半包

    处理原理: 半包:即一条消息底层分几次发送,先有个头包读取整条消息的长度,当不满足长度时,将消息临时缓存起来,直到满足长度再解码 粘包:两条完整/不完整消息粘在一起,一般是解码完上一条消息,然后再判断 ...

  2. Socket解决粘包问题1

    粘包是指发送端发送的包速度过快,到接收端那边多包并成一个包的现象,比如发送端连续10次发送1个字符'a',因为发送的速度很快,接收端可能一次就收到了10个字符'aaaaaaaaaa',这就是接收端的粘 ...

  3. python3全栈开发-什么是粘包、粘包现象、如何解决粘包

    一.粘包现象 让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig) 注意注意注意: res=subprocess.Popen(cmd.decode( ...

  4. Netty解决粘包和拆包问题的四种方案

    在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接.由于微服务往对方发送信息的时候,所有的请求都是使 ...

  5. Python开发【socket篇】解决粘包

    客户端 import os import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',8 ...

  6. 网络编程基础【day09】:socket解决粘包问题之MD5(八)

    本节内容 1.概述 2.代码实现 一.概述 上一篇博客讲到的用MD5来校验还是用的之前解决粘包的方法,就是客户端发送一个请求,等待服务端的确认的这样的一个笨方法.下面我们用另外一种方法:就是客户端已经 ...

  7. python全栈开发day28-网络编程之粘包、解决粘包,上传和下载的作业

    一.昨日内容回顾 1. tcp和udp编码 2. 自定义mysocket解决编码问题 二.今日内容总结 1.粘包 1)产生粘包原因: (1).接收方不知道消息之间的边界,不知道一次性要取多少字节的数据 ...

  8. Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题

    即使关闭nagle算法,也不能解决粘包问题 https://waylau.com/netty-4-user-guide/Getting%20Started/Dealing%20with%20a%20S ...

  9. python 解决粘包问题

    客户端发送hello,如果服务端 recv(1) ,那只能接收到 h 这一个字符,然后再recv(1) 一下,可以再接收一个 e , 因为客户端发送的结果长,所以只能把其他的先缓存下来,下次recv的 ...

随机推荐

  1. POJ - 2184 Cow Exhibition 题解

    题目大意 有 \(N(N \le 100)\) 头奶牛,没有头奶牛有两个属性 \(s_i\) 和 \(f_i\),两个范围均为 \([-1000, 1000]\). 从中挑选若干头牛,\(TS = \ ...

  2. 君荣 TS--8200 消费机显示说明

     Err 001——不在消费时段内Err 002——非本系统卡Err 003——余额不足Err 004——级别未开放Err 005——卡已挂失Err 006——有效期未生效Err 007——已过有效期 ...

  3. 从零开始的Spring Boot(6、Thymeleaf内置对象及表达式大全)

    1.1 基础对象 #ctx:上下文对象 ${#ctx.locale} ${#ctx.variableNames} ${#ctx.request} ${#ctx.response} ${#ctx.ses ...

  4. python获取本地时间戳

    import time print(time.time())#获当前时间的时间戳 print(time.localtime())#获取本地时间 print(time.strftime('%Y-%m-% ...

  5. 使用torch实现RNN

    (本文对https://blog.csdn.net/out_of_memory_error/article/details/81456501的结果进行了复现.) 在实验室的项目遇到了困难,弄不明白LS ...

  6. 恕我直言你可能真的不会java第2篇:Java Stream API?

    一.什么是Java Stream API? Java Stream函数式编程接口最初是在Java 8中引入的,并且与lambda一起成为Java开发的里程碑式的功能特性,它极大的方便了开放人员处理集合 ...

  7. rust 支持的CPU架构

    Available CPUs for this target: native - Select the CPU of the current host (currently haswell). amd ...

  8. MFC编辑框接收数据动态更新与刷新方法代码示例-如何让编辑框内容实时更新

    MFC编辑框接收数据动态更新与刷新方法代码示例-如何让编辑框内容实时更新 关键代码: //发送数据通知 //from txwtech@163.com LRESULT CCommSampleDlg::O ...

  9. Docker镜像命令笔记

    docker安装 官方Ubuntu安装文档 获取 docker pull NAME[:TAG] docker pull registry.docker-cn.com/library/ubuntu:14 ...

  10. GIT本地库基本操作-命令行

    GIT本地库操作基本原理 GIT作为分布式版本库软件,每个机器上都是一个版本库. git初始化后,有三个区,分别是 工作区,暂存区,本地库: 工作区是我们编辑代码的区别,包括新增,修改,删除代码操作, ...