1.什么是粘包?

粘包指的是数据与数据之间没有明确的分界线,导致不能正确的读取数据。换句话说就是接收方不知道发送方一次到底发送了多少数据。只有TCP才会出现粘包现象,UDP不会出现粘包现象,因为TCP是流式协议,UDP是数据报协议。那么要理解粘包问题,就需要先了解TCP协议传输数据时的具体流程:

应用程序无法直接操作硬件,应用程序想要发送数据必须将数据交给操作系统,由操作系统来完成数据的传输,但是操作系统要为所有的应用程序提供数据传输的服务,也就意味着操作系统不可能立马将数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据。

UDP:

UDP在收发数据时时基于数据包的,即一个一个的包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区后也是一个个独立的数据包,接收方从操作系统缓冲区中,将数据包加载到应用程序。这种方式存在的问题有:

1.发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送

2.接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度,则会发生数据丢失,而且缓冲区不可能无限大

TCP:

当我们需要传输较大的数据或者需要保证数据的完整性时,就需要使用TCP协议。

与UDP不同的是,TCP增加了一套校验规则来保证数据的完整性,会将超过TCP包最大长度的数据拆分为多个TCP包 并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界,这样就会容易产生粘包现象。

产生粘包问题的三种情况:

1.当单个数据包较小时,接收方可能一次性读取了多个包的数据

2.当整体数据较大时,接收方可能一次只读取了一个包的一部分内容

3.另外TCP协议为了提高效率,增加了一种优化机制,会将数据较小且发送时间间隔较短的数据和并发送,该机制也会导致发送方将两个数据包黏在一起发送。

2.粘包的解决方案

1.基础解决方案

在发送数据前先发送数据长度

server.py

import socket
import subprocess server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
client, addr = server.accept()
while True:
try:
cmd = client.recv(1024).decode('utf-8') # 接收命令
if not cmd:
client.close()
break
p = subprocess.Popen(cmd, # 接收命令
shell=True, # 判断参数是否是一个系统命令
stdout=subprocess.PIPE, # 指定结果的输出管道
stderr=subprocess.PIPE) # 指定错误结果的输出管道 std = p.stdout.read() # 获取结果
std_err = p.stderr.read() # 获取错误的结果 length = len(std) + len(std_err) # 统计结果的长度
len_size = str(length).encode('utf-8') # 将int类型转换为bytes类型 client.send(len_size) # 发送结果的长度的bytes数据
client.send(std) # 发送结果数据
client.send(std_err) # 发送错误结果数据
except ConnectionResetError as e:
print(e)
client.close()
break client.py import socket client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
cmd = input('>>:').strip().encode('utf-8') # 用户输入命令,并且编码
if not cmd:continue
client.send(cmd) # 发送命令
length_size = client.recv(1024) # 接收数据长度的bytes数据
length = length_size.decode('utf-8') # 将bytes反解成int类型,得到数据长度
all_data = b''
recv_len = 0
while recv_len < int(length): # 条件为如果循环接收的数据长度小于得到的数据长度则执行
data = client.recv(1024) # 接收真实数据
recv_len += len(data) # 累加接收的数据长度
all_data += data # 累加真实数据
print(all_data.decode('gbk')) # 解码打印最终结果 client.close()

解决粘包基础版

但是由于negle优化机制的存在,长度信息和数据还是有可能会发生粘包现象,而对方并不知道长度信息具体几个字节,所以现在的问题是如何能够将长度信息做成一个固定长度的bytes数据。所以有个内置模块:struct模块为我们提供了一个功能,可以将int类型转换为固定长度的bytes。

2.解决粘包问题升级版

import struct

# 整型转bytes
res1 = struct.pack('i', 1000) # 无论数字(int)有多大,最后结果的长度都是4
res2 = struct.pack('q', 1000) # 无论数字(long long)有多大,最后结果的长度都是8 print(res1, len(res1)) # b'\xe8\x03\x00\x00' 4
print(res2, len(res2)) # b'\xe8\x03\x00\x00\x00\x00\x00\x00' 8 # bytes转整型
data1 = struct.unpack('i', res1)
data2 = struct.unpack('q', res2)
# 结果是一个元组的形式
print(data1) # (1000,)
print(data2) # (1000,)
server.py

import socket
import subprocess
import struct server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
client, addr = server.accept()
while True:
try:
cmd = client.recv(1024).decode('utf-8') # 接收命令
if not cmd:
client.close()
break
p = subprocess.Popen(cmd, # 接收命令
shell=True, # 判断参数是否是一个系统命令
stdout=subprocess.PIPE, # 指定结果的输出管道
stderr=subprocess.PIPE) # 指定错误结果的输出管道 std = p.stdout.read() # 获取结果
std_err = p.stderr.read() # 获取错误的结果 length = len(std) + len(std_err) # 统计结果的长度
len_size = struct.pack('i', length) # 将int类型转换为bytes类型 client.send(len_size) # 发送结果的长度的bytes数据
client.send(std) # 发送结果数据
client.send(std_err) # 发送错误结果数据
except ConnectionResetError as e:
print(e)
client.close()
break client.py import socket
import struct client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
cmd = input('>>:').strip().encode('utf-8') # 用户输入命令,并且编码
if not cmd:continue
client.send(cmd) # 发送命令
length_size = client.recv(4) # 接收数据长度的bytes数据
length = struct.unpack('i', length_size)[0] # 将bytes反解成int类型,得到数据长度
all_data = b''
recv_len = 0
while recv_len < length: # 条件为如果循环接收的数据长度小于得到的数据长度则执行
data = client.recv(1024) # 接收真实数据
recv_len += len(data) # 累加接收的数据长度
all_data += data # 累加真实数据
print(all_data.decode('gbk')) # 解码打印最终结果 client.close()

解决粘包升级版

3.自定义报头解决粘包

上述方案已经完美解决了粘包问题,但是扩展性不高,假如我们要实现文件的上传和下载,不光要传输文件数据,还需要传输文件名字,md5值等等,如何能实现?

解决步骤:

发送端:

1.先将所有的额外信息打包到一个头中

2.然后先发送头部数据

3.最后发送真实数据

接收端:

1.接收固定长度的头部长度数据

2.根据长度数据获取头部数据

3.根据头部数据获取真实数据

server.py

import socket
import subprocess
import datetime
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
client, addr = server.accept()
while True:
try:
cmd = client.recv(1024).decode('utf-8')
if not cmd:
client.close()
break
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
std = p.stdout.read()
std_err = p.stderr.read() # 自定义报头
t = {}
t['time'] = str(datetime.datetime.now())
t['size'] = len(std) + len(std_err) json_data = json.dumps(t) # 序列化成json格式的数据
t_data = json_data.encode('utf-8') # 将json格式的数据转换成字节
json_len = struct.pack('i', len(t_data)) client.send(json_len)
client.send(t_data) client.send(std)
client.send(std_err)
except ConnectionResetError as e:
print(e)
client.close()
break client.py import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
cmd = input('>>:').strip().encode('utf-8')
if not cmd:continue
client.send(cmd) json_len = client.recv(4)
json_size = struct.unpack('i', json_len)[0] json_data = client.recv(json_size)
json_dic = json.loads(json_data.decode('utf-8')) print('执行时间:%s' % json_dic['time'])
all_data = b''
recv_len = 0
while recv_len < json_dic['size']:
data = client.recv(1024)
recv_len += len(data)
all_data += data
print(all_data.decode('gbk')) client.close()

解决粘包问题终极版

 FTP文件上传下载程序:

import socket
import struct
import json
import os server = socket.socket() server.bind(("127.0.0.1", 9090))
server.listen() def run():
while True:
client, addr = server.accept()
while True:
try:
# 接收报头
len_data = client.recv(4)
if not len_data:
print("客户端已断开....")
client.close()
break
head_len = struct.unpack("i", len_data)[0]
head = json.loads(client.recv(head_len).decode("utf-8")) # 从报头中获取用户要执行的操作
if head["opt"] == "login":
res = login(head) # 调用登录 无论成功失败 都会得到一个结果
send_response(res, client) # 将结果发送给客户端
elif head["opt"] == "register":
res = register(head) # 调用注册 无论成功失败 都会得到一个结果
send_response(res, client) # 将结果发送给客户端
else:
print("请求错误!") except ConnectionResetError:
print("客户端异常断开...")
client.close()
break def login(head):
dir_path = r"D:\脱产5期内容\选课系统\黏包作业\登录注册\user_data"
path = os.path.join(dir_path, head["name"]) if not os.path.exists(path):
response = {"status": "", "msg": "用户名不存在!"}
return response
with open(path, "rt", encoding="utf-8") as f:
user_dic = json.load(f)
if user_dic["pwd"] == head["pwd"]:
response = {"status": "", "msg": "登录成功!"}
return response
else:
response = {"status": "", "msg": "密码错误!"}
return response def register(head):
user_dic = {"name": head["name"], "pwd": head["pwd"]}
dir_path = r"D:\脱产5期内容\选课系统\黏包作业\登录注册\user_data"
path = os.path.join(dir_path, head["name"]) if os.path.exists(path):
print("用户名已存在!")
response = {"status": "", "msg": "用户名已存在!"}
return response with open(path, "wt", encoding="utf-8") as f:
json.dump(user_dic, f)
response = {"status": "", "msg": "注册成功"}
return response def send_response(resp, client):
response_data = json.dumps(resp).encode("utf-8")
client.send(struct.pack("i", len(response_data)))
client.send(response_data) run()

服务器

import socket
import struct
import json c = socket.socket()
c.connect(("127.0.0.1", 9090)) def run():
print("""
1.登录
2.注册
3.退出
""") res = input("请选择功能:")
if res == "":
return
elif res == "":
login()
elif res == "":
register()
else:
print("输入错误!") def login():
name = input("name:")
pwd = input("password:")
dic = {"name": name, "pwd": pwd, "opt": "login"}
res = send_request(dic)
print(res) def register():
name = input("name:")
pwd = input("password:")
dic = {"name": name, "pwd": pwd, "opt": "register"}
res = send_request(dic)
print(res) def send_request(dic):
dic_data = json.dumps(dic).encode("utf-8")
c.send(struct.pack("i", len(dic_data)))
c.send(dic_data) # 接收返回结果
res_len = struct.unpack("i", c.recv(4))[0]
res = json.loads(c.recv(res_len).decode("utf-8")) return res run()

客户端

TCP粘包问题的更多相关文章

  1. Socket编程(4)TCP粘包问题及解决方案

    ① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...

  2. Netty(三)TCP粘包拆包处理

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 粘包.拆包问题说明 假设客户端分别发送数据包D1和D ...

  3. netty 解决TCP粘包与拆包问题(二)

    TCP以流的方式进行数据传输,上层应用协议为了对消息的区分,采用了以下几种方法. 1.消息固定长度 2.第一篇讲的回车换行符形式 3.以特殊字符作为消息结束符的形式 4.通过消息头中定义长度字段来标识 ...

  4. Netty的TCP粘包/拆包(源码二)

    假设客户端分别发送了两个数据包D1和D2给服务器,由于服务器端一次读取到的字节数是不确定的,所以可能发生四种情况: 1.服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包. 2.服 ...

  5. netty 解决TCP粘包与拆包问题(一)

    1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...

  6. TCP粘包/拆包问题

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...

  7. tcp粘包问题(封包)

    tcp粘包分析     http://blog.csdn.net/zhangxinrun/article/details/6721495 解决TCP网络传输“粘包”问题(经典)       http: ...

  8. 1. Netty解决Tcp粘包拆包

    一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...

  9. TCP 粘包/拆包问题

    简介    TCP 是一个’流’协议,所谓流,就是没有界限的一串数据. 大家可以想想河里的流水,是连成一片的.期间并没有分界线, TCP 底层并不了解上层业务数据的具体含义 ,它会根据 TCP 缓冲区 ...

  10. TCP粘包/拆包问题的解决

    TCP粘包拆包问题 一个完整的包可能被TCP拆分成多个包,或多个小包封装成一个大的数据包发送. 解决策略 消息定长,如果不够,空位补空格 在包尾增加回车换行符进行分割,例如FTP协议 将消息分为消息头 ...

随机推荐

  1. 【kafka】生产者速度测试

    非常有用的参考博客:http://blog.csdn.net/qq_33160722/article/details/52903380 pykafka文档:http://pykafka.readthe ...

  2. hdu4044 依赖背包变形 好题!

    由于不是求最大的可拦截的HP值,而是要将最小值最大化,那么就需要分配每个子树用的钱数以达到最小值最大化 第一步解决如何分配钱使得结点u的子树中用了j元钱后可以拦截的HP最大,这就是变形的分组(依赖)背 ...

  3. django的查看sql语句setting设置

    LOGGING = {     'version': 1,     'disable_existing_loggers': False,     'handlers': {         'cons ...

  4. C++ 使用LockWorkStation()的过程遇到的问题

    关于函数“LockWorkStation()”,参见:https://msdn.microsoft.com/en-us/library/windows/desktop/aa376875.aspx Ho ...

  5. windows 7 下用git

    参考:http://my.oschina.net/longxuu/blog/141699

  6. pycharm导入本地py文件时,模块下方出现红色波浪线时如何解决

    有时候导入本地模块或者py文件时,下方会出现红色的波浪线,但不影响程序的正常运行,但是在查看源函数文件时,会出现问题 问题如下:  解决方案: 1. 进入设置,找到Console下的Python Co ...

  7. Cookie中设置了 HttpOnly,Secure 属性,有效的防止XSS攻击,X-Frame-Options 响应头避免点击劫持

    属性介绍: 1) secure属性当设置为true时,表示创建的 Cookie 会被以安全的形式向服务器传输(ssl),即 只能在 HTTPS 连接中被浏览器传递到服务器端进行会话验证, 如果是 HT ...

  8. 网站申请HTTPS 访问

    #生成证书和key openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -keyout /opt/nginx/pdk.key -out /opt ...

  9. Caused by: java.lang.NumberFormatException: For input string: "18446744073709551615"

    问题:Caused by: java.lang.NumberFormatException: For input string: "18446744073709551615" 原因 ...

  10. OAuth2:隐式授权(Implicit Grant)类型的开放授权

    适用范围 仅需临时访问的场景 用户会定期在API提供者那里进行登录 OAuth客户端运行在浏览器中(Javascript.Flash等) 浏览器绝对可信,因为该类型可能会将访问令牌泄露给恶意用户或应用 ...