粘包现象:

如上篇博客中最后的示例,客户端有个 phone.recv(2014) , 当服务端发送给客户端的数据大于1024个字节时, 多于1024的数据就会残留在管道中,下次客户端再给服务端发命令时,残留在管道的数据会先发送给客户端,新命令产生的数据会排在上次命令残留数据的后面发送到客户端,即两次结果的数据粘在一起了, 这个就是粘包现象。

粘包现象的原理分析:

# 运行一个软件或程序需要的硬件:CPU、内存、硬盘
# CPU负责执行;CPU执行需要数据,而数据从内存中取,最后可以把数据存到硬盘(内存速度会比硬盘快很多)
# 操作系统所占的内存空间和应用程序所占的内存空间互相隔离 # 客户端的 phone.send(数据) send是应用软件的代码,是发送给操作系统的命令,应用软件先把要发送的数据copy给操作系统的内存然后让操作系统把该数据发送出去
# 应用软件把数据复制给操作系统后,操作系统怎么发这个数据应用软件控制不了
# recv的完成需要2步:1.recv对应的操作系统等待接收对方传过来的数据;(耗时长);2.recv的操作系统将接收的数据复制给应用软件(耗时短,因为是本地copy)
# send的完成只需要1步: 将数据从自己(应用软件)的内存空间复制到操作系统的内存空间。 # send和recv对比
# 1. 不管是recv还是send都不是在直接接收对方的数据,而是在操作自己的操作系统内存---> so,不是一个send就要对应一个recv
# 2. recv:
# wait data 耗时非常长
# copy data
# send:
# copy data
# 3. TCP协议的特点:发送端为了将多个发往接收端的包能有效的发到对方,会将多次间隔较小且数据量小的数据,合并成一个大的数据包,然后进行封装;这样接收端就难以分辨出来了,即面向流的通信是无消息保护边界的。

粘包解决方法普通版(制作自己的“报头”):

补充知识点struct模块:

import struct

# 制作报头
res = struct.pack("i",123498654) # 输出结果是bytes格式 # i 代表整型,如果是整型,res这个bytes就是固定长度4(跟后面整数具体的大小无关); #有两个参数:第一个是格式("i"代表整型),第二个是值
print(res,type(res),len(res))
"""
struct.pack可用于制作报头,把描述信息传入第二个参数value
"""
# 打印结果
# b'\x9ep\\\x07' <class 'bytes'> 4 # 解析报头
unpack_res = struct.unpack("i",res) # 对res这个bytes格式的字符串进行解包 # 解包结果为元祖形式 # 也有两个参数: 1. 格式("i") 2. bytes格式的字符串
print(unpack_res) # 打印结果
# (123498654,)

粘包解决方法普通版客户端代码如下:

import socket
import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9901))
while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd:continue
client.send(cmd.encode("utf-8")) # 2. 拿到命令结果并打印
# 第一步:先收“报头”
head = client.recv(4) # head为bytes格式; # 由于报头的长度固定为4,所以recv应该为4
# 第二步:从报头解析出对真实数据的描述信息(真实数据的长度)
total_size = struct.unpack("i",head)[0] # 对head这个报头解析 # struct.unpack()的结果是元祖的形式 # 第三步:接收真实的数据
"""开始循环接收服务端发来的真实数据"""
recv_size = 0 # 用于计算接收到的bytes数
total_recv_res = b"" # 设置一个bytes格式的空字符串,用于拼接、接收服务端发来的真实数据
while recv_size < total_size: # 已经接收的bytes数小于服务端发送的全部字节数
recv_res = client.recv(1024)
total_recv_res += recv_res # 把每次从服务端接收到的真实数据添加到total_recv_res 里面
recv_size += len(recv_res) # 每次从服务端接收到的数据的bytes数加到 recv_size里面; 不要用 recv_size += 1024,这种方法不能准确计算出bytes数
print(total_recv_res.decode("gbk")) phone.close()

服务端代码:

import subprocess
import struct
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",9901))
phone.listen(5) while True:
conn,client_addr = phone.accept() while True:
try:
# 1. 收命令
cmd = conn.recv(1024) # 2. 执行命令,拿到结果
obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read() # 3. 把命令结果返回给客户端
# 第一步:制定固定长度的“报头”(报头一定需要是固定长度)
head = struct.pack("i",len(stdout)+len(stderr)) # head 是bytes格式,长度固定为4 # 第二步: 把报头发送给客户端
conn.send(head) # 第三步:再发送真实的结果数据
# conn.send(stdout+stderr) 解决的方法如下所示:
conn.send(stdout)
conn.send(stderr) # 不需要再用“+”,因为这种形式的发送TCP协议就会把数据粘在一起
except ConnectionResetError:
break conn.close()
phone.close()

粘包解决最终版(利用字典制作自己的报头)

服务端代码:

import socket
import subprocess
import json
import struct server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
cmd = conn.recv(1024) # 2. 处理命令
obj = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout = obj.stdout.read() # bytes格式
stderr = obj.stderr.read() # 3. 把处理结果发送给客户端
"""
报头应该包含多种信息,而不只是只包含真实信息的长度,所以考虑利用字典去制定报头
"""
# 3.1 用字典形式制作报头
header_dict = {
"filename":"cmd处理",
"md5":"xxxxxx",
"total_size":len(stdout)+len(stderr)
} # 字典不能用于send(),只有bytes才可以
header_json = json.dumps(header_dict) # 将报头字典转化成json格式的字符串
header_bytes = header_json.encode("utf-8") # 将json形式的字符串转化成bytes格式
"""
字典是报头,报头转化成bytes之后你并不能确定bytes的个数,但报头又需要是固定长度,
所以先把head_bytes(报头的bytes格式)的长度利用struct模块打包成固定长度发送给客户端,然后再把header_bytes发送给客户端,
对应的,客户端先收报头长度,然后再接收报头长度个数的bytes,那么客户端第二次接收的bytes就是报头的完整信息
"""
# 3.2 发送报头bytes的长度
header_length = struct.pack("i",len(header_bytes)) # 把报头bytes的个数利用struct.pack()打包、制定成固定长度(4)
conn.send(header_length)
# 3.3 发送报头bytes的真实信息
conn.send(header_bytes)
# 3.4 发送处理结果的真实数据
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError:
break
conn.close()
server.close()

客户端代码:

import socket
import json
import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080)) while True:
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8")) # 1. 接收报头的长度
obj_contain_length = client.recv(4) # 因为报头bytes的长度已经经过struct.pack()的打包,长度固定为4
header_bytes_length = struct.unpack("i",obj_contain_length)[0] # 报头bytes的长度
# 2. 接收报头的数据
header_bytes = client.recv(header_bytes_length) # 接收报头bytes个数的bytes数,就是完整的报头bytes信息
header_json = header_bytes.decode("utf-8") # 将bytes格式解码成json字符串格式
header_dict = json.loads(header_json) # 将报头的json字符串格式反序列化得到报头的字典形式
total_size = header_dict.get("total_size") # 得到报头字典里的处理结果的bytes数
# 3. 接收处理结果的数据
recv_size = 0
total_recv_bytes = b""
while recv_size < total_size:
recv_bytes = client.recv(1024)
total_recv_bytes += recv_bytes
recv_size += len(recv_bytes) print(total_recv_bytes.decode("gbk")) client.close()

文件传输功能:

文件的目录结构如下:

服务端代码:

import socket
import json
import struct
import os
import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8089))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
res = conn.recv(1024) # b"get a.txt" # 2. 解析命令,提取相应命令参数
cmd,file_name = res.decode("utf-8").split() # ["get","a.txt"] # 3. 以读的模式打开文件,读取文件内容发送给客户端
# 第一步:用字典形式制作报头
file = os.path.join(BASE_DIR,"share",file_name)
header_dict = {
"filename":file_name,
"md5":"xxxxxx",
"total_size": os.path.getsize(file)
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8") # 第二步:发送报头bytes的长度
header_length = struct.pack("i",len(header_bytes))
conn.send(header_length)
# 第三步:发送报头bytes的真实信息
conn.send(header_bytes) # 4. 发送处理结果的真实数据
with open(file,"rb") as f:
for line in f:
conn.send(line) # 单行发送跟一下全部发送效果上没有区别,因为单行发送也是粘在一起 except ConnectionResetError:
break
conn.close()
server.close()

客户端代码:

import socket
import json
import struct
import os,sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8089)) while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8")) # 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
# 第一步: 接收报头的长度
obj_contain_length = client.recv(4)
header_bytes_length = struct.unpack("i",obj_contain_length)[0]
# 第二步:再收报头,从报头中解析出对真实信息的描述信息
header_bytes = client.recv(header_bytes_length)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
total_size = header_dict.get("total_size")
file_name = header_dict["filename"]
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR,"download",file_name),"wb") as f:
recv_size = 0
while recv_size < total_size:
line = client.recv(1024)
f.write(line)
recv_size += len(line)
print("文件总大小:%s;已下载:%s;已下载比例:%s"%(total_size,recv_size,(recv_size/total_size))) client.close()

文件传输功能函数版:

客户端代码:

import socket
import json
import struct
import os,sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) """下载功能:即从服务端接收文件"""
def get(client,cmd):
# 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
# 第一步: 接收报头的长度
file_name = cmd[1]
obj_contain_length = client.recv(4)
header_bytes_length = struct.unpack("i", obj_contain_length)[0]
# 第二步:再收报头,从报头中解析出对真实信息的描述信息
header_bytes = client.recv(header_bytes_length)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
total_size = header_dict.get("total_size")
# file_name = header_dict["filename"]
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR, "download", file_name), "wb") as f:
recv_size = 0
while recv_size < total_size:
line = client.recv(1024)
f.write(line)
recv_size += len(line)
print("文件总大小:%s;已下载:%s;已下载比例:%s" % (total_size, recv_size, (recv_size / total_size))) """上传功能:即发送文件给服务端"""
def put(client,cmd):
file_name = cmd[1]
file = os.path.join(BASE_DIR,"download",file_name)
# 1. 制定报头
file_size = os.path.getsize(file)
head_dict ={
"filename":file_name,
"dm5": "xxxxxxx",
"total_size":file_size
}
head_json = json.dumps(head_dict)
head_bytes = head_json.encode("utf-8")
# 2. 发送报头
head_bytes_length = struct.pack("i",len(head_bytes))
client.send(head_bytes_length)
client.send(head_bytes)
# 3. 发送真实数据
with open(file,"rb") as f:
send_size = 0
for line in f:
client.send(line)
send_size += len(line)
print("文件总大小:%s;已上传:%s;上传比例:%s"%(file_size,send_size,(send_size/file_size))) def run():
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8089)) while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8"))
cmd_list = cmd.split()
cmd_dict = {
"get":get,
"put":put
}
for k,v in cmd_dict.items():
if cmd_list[0] == k:
v(client,cmd_list) client.close() if __name__ == "__main__":
run()

服务端代码:

import socket
import json
import struct
import os
import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) """下载功能:即发送数据给客户端"""
def get(conn,cmd):
file_name = cmd[1]
file = os.path.join(BASE_DIR, "share", file_name)
header_dict = {
"filename": file_name,
"md5": "xxxxxx",
"total_size": os.path.getsize(file)
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8") header_length = struct.pack("i", len(header_bytes))
conn.send(header_length)
conn.send(header_bytes) # 发送处理结果的真实数据
with open(file, "rb") as f:
for line in f:
conn.send(line) """上传功能:即接收客户端发来的数据"""
def put(conn,cmd):
file_name = cmd[1]
# 1. 先接收报头长度
bytes_header_length = conn.recv(4)
header_length = struct.unpack("i",bytes_header_length)[0]
# 2. 接收报头数据
header_bytes = conn.recv(header_length) # bytes格式的字符串
header_json = header_bytes.decode("utf-8")
header = json.loads(header_json)
total_size = header["total_size"] # 要上传数据的总大小
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR,"share",file_name),"wb") as f:
recv_size = 0
while recv_size < total_size:
recv_bytes = conn.recv(1024)
f.write(recv_bytes)
recv_size += len(recv_bytes) def run():
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8089))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
res = conn.recv(1024) # b"get a.txt"
# 2. 解析命令,提取相应命令参数
cmd = res.decode("utf-8").split() # ["get","a.txt"] cmd_dict = {
"get": get,
"put": put
}
for k,v in cmd_dict.items():
if cmd[0] == k:
v(conn,cmd) except ConnectionResetError:
break
conn.close()
server.close() if __name__ == "__main__":
run()

基于UDP协议的套接字:

客户端代码:

from socket import *

client = socket(AF_INET,SOCK_DGRAM)

"""
UTP不需要建链接(通道)。所以不需要 connect
""" while True:
msg = input(">>>").strip()
client.sendto(msg.encode("utf-8"),("127.0.0.1",8080)) # sendto传入两个参数:数据和接收端的IP和端口 # msg也是bytes格式 data = client.recvfrom(1024)
print(data) client.close() # 运行结果:
# (b'HELLO', ('127.0.0.1', 8080)) """
UTP协议不会粘包
UTP协议能够发送空消息,所以不需要写 if not msg: continue
UTP协议一定是一个sendto对应一个 recvfrom
对于recvfrom(1024),在Windows上,如果接收的数据大于1024个bytes,会报错;在Linux上,如果接收的数据大于1024个bytes,程序只接收1024个,多余的数据就丢失了
"""

客户端代码:

from socket import *  # 导入socket模块时可以利用 import *

server = socket(AF_INET,SOCK_DGRAM)  # DGRAM 是UDP协议,即“数据报协议”
server.bind(("127.0.0.1",8080)) # UDP协议也需要bind """
UTP协议没有 listen和accept;因为UTP不需要建通道,而TCP中的listen和accept是为了建通道
"""
while True:
data,addr = server.recvfrom(1) # 接收数据;收到的也是bytes格式 # 接收到的数据是元祖形式:第一个元素是数据信息,第二个是发送端的IP和端口 # 数据信息也是bytes格式
print(data,addr) server.sendto(data.upper(),addr) # recvfrom中包含发送端的IP和端口,还通过这个IP端口发发送端回数据
server.close() # 运行结果:
# b'hello' ('127.0.0.1', 53729)

网络编程基础:粘包现象、基于UDP协议的套接字的更多相关文章

  1. 网络编程----socket介绍、基于tcp协议的套接字实现、基于udp协议的套接字实现

    一.客户端/服务器架构(C/S架构)                                                即C/S架构,包括: 1.硬件C/S架构(打印机) 2.软件C/S架 ...

  2. 网络编程(基于udp协议的套接字/socketserver模块/进程简介)

    一.基于UDP协议的套接字 TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据.相对TCP,UDP则是面向无连接的协议. 使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就 ...

  3. 基于udp协议的套接字及udp协议粘包问题

    udp协议的套接字 udp协议传输  服务端和客户端没有建立连接一说. import socket # 总结一下基础工作流程:服务端生成套接字并绑定ip_port,进入数据传输循环,服务端接受客户端发 ...

  4. 网络编程之基于UDP协议的套接字编程、基于socketserver实现并发的socket

    目录 基于UDP协议的套接字编程 UDP套接字简单示例 服务端 客户端 基于socketserver实现并发的socket 基于TCP协议 server类 request类 继承关系 服务端 客户端1 ...

  5. python 之 网络编程(基于UDP协议的套接字通信)

    8.5 基于UDP协议的套接字通信 UDP协议:数据报协议 特点:无连接,一发对应一收,先启动哪一端都不会报错 优点:发送效率高,但有效传输的数据量最多为500bytes 缺点:不可靠:发送数据,无需 ...

  6. 网络编程(四)--基于udp协议的套接字、socketserver模块

    一.UDP协议(数据报协议) 1.何为udp协议 不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包. 以太网头 ip头                  ...

  7. 网络编程[第二篇]基于udp协议的套接字编程

    udp协议下的套接字编程 一.udp是无链接的    不可靠的 而上篇的tcp协议是可靠的,会有反馈信息来确认信息交换的完成与否 基于udp协议写成的服务端与客户端,各司其职,不管对方是否接收到信息, ...

  8. 网络编程(四)——基于udp协议的套接字socket、socketserver模块的使用

    基于udp协议的套接字.socketserver模块 一.UDP协议(数据报协议) 1.何为udp协议 不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包. ...

  9. 基于UDP协议的套接字编程

    基于udp协议的套接字编程 UDP是无链接的,先启动那一端都不会报错 UDP协议是数据报协议,发空的时候也会自带报头,因此客户端输入空,服务端也能收到 一般不用与传输大数据 虽然没有粘包问题,但是不能 ...

随机推荐

  1. 448 Find All Numbers Disappeared in an Array 找到所有数组中消失的数字

    给定一个范围在  1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次.找到所有在 [1, n] 范围之间没有出现在数组中的数字.您能在不使用 ...

  2. YUM报错及解决办法

    [root@xuegod60 ~]# yum clean all Loaded plugins: product-id, refresh-packagekit, security, subscript ...

  3. java数据结构和算法04(链表)

    前面我们看了数组,栈和队列,大概就会这些数据结构有了一些基本的认识,首先回顾一下之前的东西: 在数组中,其实是分为有序数组和无序数组,我简单实现了无序数组,为什么呢?因为有序数组的实现就是将无序数组进 ...

  4. LN : leetcode 413 Arithmetic Slices

    lc 413 Arithmetic Slices 413 Arithmetic Slices A sequence of number is called arithmetic if it consi ...

  5. git --删除文件、重命名

    修改最后一次提交 git commit --amend -m “” 删除文件:. git rm <需要删除的文件> 只是删除当前工作目录和暂存区的文件,也就是取消跟踪.在下次提交时不纳入版 ...

  6. 锐动SDK应用于行车记录仪

    方案架构 手机端直播与录播功能忠实记录旅途中各种突发事件,还原事实真相,与家人和朋友分享沿途美景,一同感受美妙之旅. 强大的视频编辑功能,像编辑图片一样给视频添加各种滤镜,配音,配乐,标题文字等特效. ...

  7. linux下php访问sql server设置

    安装freeIDS 官网下载地址: wget ftp://ftp.freetds.org/pub/freetds/stable/freetds-1.00.18.tar.gz 1.1.到下载目录解压 t ...

  8. Farseer.net轻量级开源框架说明及链接索引

    Farseer.net是什么? 基于.net framework 4 开发的一系列解决方案. 完全开源在GitHub中托管.并发布到NuGet中. Farseer.Net由最初的关系数据库ORM框架后 ...

  9. 迅为iTOP-4412物联网开发板入门学习高手进阶项目开发超树莓派

    免费视频教程: 为初学者精心录制的整套视频教程全部免费,随IT技术发展而不断增添的视频教程仍然免费!一支有经验的工程师团队会始终成为您的后盾. 项目实战---全开源: 手机远程控制开发板 门禁系统 W ...

  10. MySQL_将ubuntu18.04上的数据库导出并导入windows10下

    1.使用:mysqldump -uroot -p databasename>filename.sql databasename:要导出的数据库名. filename:指定导出sql文件的文件名. ...