粘包现象:

如上篇博客中最后的示例,客户端有个 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. jsp声明周期

    https://www.w3cschool.cn/jsp/jsp-life-cycle.html 几点注意: jsp初始化期: 容器载入jsp文件后,它会在为请求提供任何服务前调用jspinit()方 ...

  2. mysql 中 时间函数 now() current_timestamp() 和 sysdate() 比较

    转载请注明出处 https://www.cnblogs.com/majianming/p/9647786.html 在mysql中有三个时间函数用来获取当前的时间,分别是now().current_t ...

  3. 6 Specialzed layers 特殊层 第一部分 读书笔记

    6 Specialzed layers 特殊层  第一部分  读书笔记   Specialization is a feature of every complex organization. 专注是 ...

  4. 10.3 Implementing pointers and objects and 10.4 Representing rooted trees

    Algorithms 10.3 Implementing pointers and  objects  and 10.4 Representing rooted trees Allocating an ...

  5. scss常规用法

    保持sass条理性和可读性的最基本的三个方法:嵌套.导入和注释. 一般情况下,你反复声明一个变量,只有最后一处声明有效且它会覆盖前边的值. $link-color: blue; $link-color ...

  6. 一些常用的meta标签及其作用

    声明文档使用的字符编码  <meta charset='utf-8'>优先使用 IE 最新版本和 Chrome  <meta http-equiv="X-UA-Compat ...

  7. KMS

    slmgr -ipk 73KQT-CD9G6-K7TQG-66MRP-CQ22C

  8. RequireJS 上手使用

    首先 点击此处 得到requirejs. 捣鼓了俩小时终于运行成功了,原因是因为require(['我是空格underscore',...],function(){...})的时候 变量多个空格(坑爹 ...

  9. java web 学习笔记 - tomcat数据源

    1. 数据库源 以前的JDBC连接步骤为: 1.加载数据库驱动 2.通过DriverManger获取数据库连接connection 3.通过connection执行prepareStatement的响 ...

  10. vue vueRouter vuex Axios webpack 前端常用内容

    Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中.