服务端:

 import socket
import subprocess phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
conn.send(stdout+stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

客户端:

 import socket
import os
if os.name =="nt":
code = "GBK"
else:
code="utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
#1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd:continue
phone1.send(cmd.encode("utf-8"))
#2,接收服务器执行命令后的结果.
data = phone1.recv(1024)
print(data.decode(code))
phone1.close()

我们分别启动服务端和客户端.然后在客户端上执行一个名 tree c:\ (windows系统).服务端返回的结果如下:

 C:\
├─e_object
├─GeePlayerDownload
├─Intel
│ └─Logs
├─Program Files
│ ├─Common Files
│ │ ├─Microsoft Shared
│ │ │ ├─Filters
│ │ │ ├─ink
│ │ │ │ ├─ar-SA
│ │ │ │ ├─bg-BG
│ │ │ │ ├─cs-CZ
│ │ │ │ ├─da-DK
│ │ │ │ ├─de-DE
│ │ │ │ ├─el-GR
│ │ │ │ ├─en-US
│ │ │ │ ├─es-ES
│ │ │ │ ├─et-EE
│ │ │ │ ├─fi-FI
│ │ │ │ ├─fr-FR
│ │ │ │ ├─fsdefinitions
│ │ │ │ │ ├─auxpad
│ │ │ │ │ ├─keypad
│ │ │ │ │ ├─main
│ │ │ │ │ ├─numbers
│ │ │ │ │ ├─oskmenu
│ │ │ │ │ ├─osknumpad
│ │ │ │ │ ├─oskpred
│ │ │ │ │ ├─symbols
│ │ │ │ │ └─web
│ │ │ │ ├─he-IL
│ │ │ │ ├─hr-HR
│ │ │ │ ├─hu-HU
│ │ │ │ ├─HWRCustomization
│ │ │ │ ├─it-IT
│ │ │ │ ├─ja-

我们此时,在客户端继续输入ifconfig 命令,发现返回的数据依然是上次tree c:\的结果.这是为什么呢?

这是因为,客户端一次只能接收1024个字节的数据,如果超过1024个字节,那么这些数据就会在服务器的IO缓存区里暂存下来.如果现在在客户端输入ipconfg命令后,在服务端返回数据给客户端时,因为IO缓存区还有上次tree命令存留的信息,所以会先把上次的信息返回给客户端.等tree命令所有的数据都返回给客户端后,才会返回ipconfig的数据.就造成了两条命令的结果都在某一次的返回数据中.这种现象就叫做粘包.

粘包发生需要满足的条件:

一,在客户端:

  由于TCP协议使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。如果连续发送2个2bytes的包,这时候在客户端就已经发生了粘包现象.但是此时在服务端不一定会发生粘包.

二,服务端:

  如果这2个包没有超出服务器接收的最大字节数(1024),就不会发生粘包.如果服务器每次只接收1bytes,那么在服务端也会发生粘包.

怎么解决粘包这种现象呢?有人说把客户端接收的最大字节值改成其他更大的数字,不就可以了吗?一般情况下,最大接收字节数的值不超过8192.超过这个数,会影响接收的稳定性和速度.

send和recv对比:

1.不管是send还是recv,都不是直接把数据发送给对方,而是通过系统发送.然后从系统内存中读取返回的数据.

2.send和recv不是一一对应的.

3.send工作流程:把数据发送给操作系统,让系统调用网卡进行发送.send就完成了工作

recv工作流程,等待客户端发送过来的数据.这个时间比较长.接收到数据后,再从系统内存中调用数据.

粘包问题只存在于TCP中,Not UDP

还是看上图,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

总结

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

  

  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

解决粘包现象的思路:

  通过上述的实验和例子,我们知道,粘包现象的产生,主要是客户端不知道要接收多少数据(或者说多大的数据).那么,按照这个思路,那么我们知道,在服务端执行完命令后,我们可以在服务端获取结果的大小.再发送给客户端,让客户端知道被接收数据的大小,然后再通过一个循环,来接收数据即可.这时我们需要用一个新的模块,struct来制作报头信息.发送给客户端.

import struct
pack = struct.pack("i",10000) # 定义格式
print(pack,len(pack),type(pack)) # pack的类型是bytes,传输的时候,就不用encode了. t = struct.unpack("i",pack) #解包,
print(t) # 获取元组形式的数据.
t = struct.unpack("i",pack)[0] # 直接获取数据的值. """
b"\x10'\x00\x00" 4 <class 'bytes'>
(10000,)
直接获取: 10000
"""
 #!_*_ coding:utf-8 _*_
import socket
import subprocess
import struct phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
#3-1 把报头(固定长度)发送给客户端
total_size = len(stdout+stderr)
print(total_size)
header = struct.pack("i",total_size) # i是类型,total_size是值.这个命令会把total_size打包成一个4个字节长度的字节数据类型
conn.send(header) # 把报头发送给客户端
#302 发送数据给客户端 conn.send(stdout)
conn.send(stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

粘包解决服务端

 #!_*_ coding:utf-8 _*_
import socket
import os
import struct if os.name == "nt":
code = "GBK"
else:
code = "utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
# 1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd: continue
phone1.send(cmd.encode("utf-8"))
# 2,接收服务器执行命令后的结果.
# 2-1 接收服务器发过来的报头
header = phone1.recv(4) # 收报头
total_size = struct.unpack("i", header)[0] #解包,并取出报头中数据 # 2-2 循环接收数据
recv_size = 0
recv_data = b""
while recv_size < total_size:
data = phone1.recv(1024) # 接收数据
recv_data += data # 拼接数据
recv_size += len(data) # 设置已接收数据的大小
print(recv_data.decode(code))
phone1.close()

粘包解决客户端

上面粘包解决办法中存在着一些问题:

1,struct制作报头的时候,不管是i还是l模式,total_size都有可能超出它们俩的范围.程序就会报错.

   total_size = len(stdout+stderr)
print(total_size)
header = struct.pack("i",total_size)

2,报头信息不应该只有文件大小信息.还应该包含其他文件信息.

新思路:

   设置一个字典,字典中包含了文件的信息,(大小,名称,md5等).然后通过json.dumps转换成字符串格式,再把转换后的数据转成bytes类型(便于网络传输)
.再然后,通过struct模块,把bytes类型的制作成一个报头(报头长度依然是4bytes),发给客户端.然后客户端接收后,反序列化,获取字典中文件的大小.然后开始接收文件.

服务端:

 #!_*_ coding:utf-8 _*_
import socket
import subprocess
import struct
import json phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
#3-1 把报头(固定长度)发送给客户端
header_dict ={"filename":"a.txt",
"md5":"a0id2ndnk23nmnm1bazi23",
"total_size":len(stdout+stderr)
}
#3-1-1,把字典序列化为字符串
header_json = json.dumps(header_dict)
#3-1-2,把序列化后的数据转成bytes类型,便于网络传输
header_bytes = header_json.encode("utf-8")
#3-1-3,把bytes类型的数据做成一个报头
struct.pack("i",len(header_bytes)) # 对应客户端的 obj = phone1.recv(4)
#3-1-4,发送报头给客户端
conn.send(struct.pack("i",len(header_bytes)))
#3-1-5.把报头信息发给客户端
conn.send(header_bytes) #对应客户端的header_bytes = phone1.recv(header_size)
#302 发送数据给客户端
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

客户端:

 #!_*_ coding:utf-8 _*_
import socket
import os
import struct
import json
if os.name == "nt":
code = "GBK"
else:
code = "utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
# 1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd: continue
phone1.send(cmd.encode("utf-8"))
# 2,接收服务器执行命令后的结果.
# 2-1 接收服务器发过来的报头
obj = phone1.recv(4) # 收报头
header_size= struct.unpack("i",obj)[0] #获取报头长度
header_bytes = phone1.recv(header_size) # 收取报头信息(bytes格式)
header_json = header_bytes.decode("utf-8") #解码报头信息
header_dict=json.loads(header_json) # 反序列化,获取字典内容
print(header_dict)
total_size = header_dict["total_size"] # 获取total_size的值 # 2-2 循环接收数据
recv_size = 0
recv_data = b""
while recv_size < total_size:
data = phone1.recv(1024) # 接收数据
recv_data += data # 拼接数据
recv_size += len(data) # 设置已接收数据的大小
print(recv_data.decode(code))
phone1.close()

制作报头的流程:

Day 6-3 粘包现象的更多相关文章

  1. TCP的粘包现象

    看面经时,看到有面试官问TCP的粘包问题.想起来研一做购物车处理数据更新时遇到粘包问题,就总结一下吧. 1 什么是粘包现象 TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看, ...

  2. 2、粘包现象(struct模块)

    昨天我们所做的套接字是有漏洞的,它会出现粘包现象,没有发现这个问题的我们今天会进行演示.今天也会稍微讲解一下基于udp的套接字. 一.基于udp的套接字 udp是无链接的,先启动哪一端都不会报错 ud ...

  3. Python网络编程(2)-粘包现象及socketserver模块实现TCP并发

    1. 基于Tcp的远程调用命令实现 很多人应该都使用过Xshell工具,这是一个远程连接工具,通过上面的知识,就可以模拟出Xshell远程连接服务器并调用命令的功能. Tcp服务端代码如下: impo ...

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

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

  5. 网络编程-SOCKET开发之----2. TCP粘包现象产生分析

    1. 粘包现象及产生原因 1)概念 指TCP协议中,发送方发送的若干个包数据到接收方接收时粘成一包.发送方粘包:发送方把若干个要发送的数据包封装成一个包,一次性发送,减少网络IO延迟:接收方粘包:接收 ...

  6. tcp的粘包现象与解决方案

    粘包现象: 粘包1:连续的小包,会被优化机制给合并 粘包2:服务端一次性无法完全就收完客户端发送的数据,第二再次接收的时候,会接收到第一次遗留的内容 模拟一个粘包现象 服务端 import socke ...

  7. Python之路(第三十一篇) 网络编程:简单的tcp套接字通信、粘包现象

    一.简单的tcp套接字通信 套接字通信的一般流程 服务端 server = socket() #创建服务器套接字 server.bind() #把地址绑定到套接字,网络地址加端口 server.lis ...

  8. python之路--subprocess,粘包现象与解决办法,缓冲区

    一. subprocess 的简单用法 import subprocess sub_obj = subprocess.Popen( 'dir', #系统指令 shell=True, #固定方法 std ...

  9. 29、粘包现象(struct模块)

    昨天我们所做的套接字是有漏洞的,它会出现粘包现象,没有发现这个问题的我们今天会进行演示.今天也会稍微讲解一下基于udp的套接字. 本篇导航: 基于udp的套接字 粘包现象 粘包 解决粘包方法 stru ...

随机推荐

  1. (转)Spring Boot 2 (九):【重磅】Spring Boot 2.1.0 权威发布

    http://www.ityouknow.com/springboot/2018/11/03/spring-boot-2.1.html 如果这两天登录 https://start.spring.io/ ...

  2. Mysql的用户管理

  3. Loj#6183. 看无可看

    Loj#6183. 看无可看 题目描述 首先用特征根求出通项公式\(A_n=p\cdot 3^n+q\cdot(-1)^n\).通过给定的\(f_0,f_1\)可以解出\(p,q\). 然后我们要求的 ...

  4. WPF设计の画刷(Brush)

    一.什么是画刷 画刷是是一种渲染方式,用于填充图形形状,如矩形.椭圆.扇形.多边形和封闭路径.在GDI+中,画刷分为以下几种:SolidBrush,TextureBrush,HatchBrush,Li ...

  5. 文本分类实战(一)—— word2vec预训练词向量

    1 大纲概述 文本分类这个系列将会有十篇左右,包括基于word2vec预训练的文本分类,与及基于最新的预训练模型(ELMo,BERT等)的文本分类.总共有以下系列: word2vec预训练词向量 te ...

  6. SpringMVC Controller 返回值几种类型

    SpringMVC Controller 返回值几种类型 2016年06月21日 19:31:14 为who而生 阅读数:4189 标签: Controller 返回值类型spring mvc 更多 ...

  7. 手动安装 Eclipse 插件 Viplugin

    对 Vimer 来说,切换到 Eclipse 环境,传统的码code方式明显降低效率,Viplugin 是一款类 Vi 模拟器,能实现 Vi 的基本编辑功能. 安装方法 (适用于Windows 和 L ...

  8. NOIP2018初赛游记

    NOIP2018初赛游记 (编辑中)

  9. Java虚拟机性能监测工具Visual VM与OQL对象查询语言

    1.Visual VM多合一工具 Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具,它集成了多种性能统计工具的功能,使用 Visual VM 可以代替jstat.jmap.jha ...

  10. Wechart 饼图

    预览 Preview | Usage Source | Pie Source | Tutorial Wechart by Cax Cax 众所周知 Cax 既能开发游戏.又能开发图表.本文将从饼图开始 ...