一花一世界,一叶一菩提。
粘包现象
粘包的现象:
[root@localhost]# netstat -ano
活动连接
协议 本地地址 外部地址 状态 PID
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 468
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:5040 0.0.0.0:0 LISTENING 1988
TCP 0.0.0.0:5357 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:11200 0.0.0.0:0 LISTENING 5420
TCP 0.0.0.0:16422 0.0.0.0:0 LISTENING 5420
TCP 0.0.0.0:27036 0.0.0.0:0 LISTENING 7232
TCP 0.0.0.0:49664 0.0.0.0:0 LISTENING 660
TCP 0.0.0.0:49665 0.0.0.0:0 LISTENING 1444
TCP 0.0.0.0:49666 0.0.0.0:0 LISTENING 1744
TCP 0.0.0.0:49667 0.0.0.0:0 LISTENING 3684
TCP 0.0.0.0:49671 0.0.0.0:0 LISTENING 736
TCP 0.0.0.0:49679 0
[root@localhost]#
明显可以看出这是一个==不完整的返回!==那么我们如果在输入一次 dir结果会是什么样的呢?
篇幅问题请自行测试吧。(文章后面可能会给附加文件!)**
明显的一个粘包问题!
为什么会有粘包问题呢?
须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来
首先需要掌握一个socket收发消息的原理
我们都知道我们把TCP协议称之为流水协议,就是把它当做流水一样去看待,一滴滴的水从一头滴落到另外一头。
我们来举个例子:
第一次我们客户端传输了700个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取,第一次我们收取到的是700个字节。
那么第二次我们客户端又传输了1500个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取,所以这一次我们最多可以收取到的是1024个字节。那么我们还收取第二次吗?(除非是在循环取的,不然我们是不可能继续取的。而且如果是循环是不是就感觉很LOW)
那么第三次我们客户端又传输了300个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取,这一次我们收取到的是1024个字节。那么为什么是1024个字节,而不是300个字节呢,(那就要说道TCP协议了,TCP协议中它是以个稳定协议,也就是说它永远都会把所有数据都发完,不会留着,也不会随机发多少一说),而我们发过去的数据量有都是存在服务端的缓存中不会丢失的,就像我们去井口去取水,我们不会把最下面的水拿出来把,拿出来的肯定是最上面的。所以就发生了所谓的粘包现象,也就是说,一个没取干净,导致全程乱序的事故
总结:
简而言之:所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
两种情况下会发生粘包。
一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
二:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
解决粘包问题的方法
struct模块
用这个struct模块有什么好处
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
该模块可以把一个类型,如数字,转成固定长度的bytes
>>> struct.pack('i',1111111111111)
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
TCP通信下普通报头添加
对于就以种要求的报头处理方法
(比如只需要知道后续文件的长度。)
服务端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
服务端应该满足的特性:
1、一直对外提供服务
2、并发地提供服务
"""
import socket
import subprocess
import struct
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM=》TCP协议
# 2、插手机卡
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 就是它,在bind前加
phone.bind(("127.0.0.1", 8080)) # 本地回环
# 3、开机
phone.listen(5)
print('starting %s:%s' % ("127.0.0.1", 8080))
# 4、等电话链接=>链接循环
while True:
conn, client_addr = phone.accept()
print(client_addr)
# 5、收/发消息=>通信循环
while True:
try:
cmd = conn.recv(1024) # 最大接收的字节个数
if len(cmd) == 0: # 针对linux系统
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout = obj.stdout.read()
stderr = obj.stdout.read()
total_size = len(stdout) + len(stderr)
# 先发送数据的长度
conn.send(struct.pack('i', total_size))
# 发送真正的数据
conn.send(stdout)
conn.send(stderr)
except Exception: # 针对windows系统
break
# 6、关闭
conn.close() # 挂电话
phone.close() # 关机
客户端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import struct
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM=》TCP协议
# 2、拨电话
phone.connect(("127.0.0.1", 8080))
# 3、发/收消息=>通信循环
while True:
cmd = input("[root@localhost]# ").strip()
if len(cmd) == 0:
continue
phone.send(cmd.encode('utf-8'))
# 先收数据的长度
header = phone.recv(4)
total_size = struct.unpack('i', header)[0]
# 收真正的数据
recv_size = 0
res = b''
while recv_size < total_size:
data = phone.recv(1024)
res += data
recv_size += len(data)
print(res.decode('gbk'))
# 4、关闭
phone.close()
结果:
自行测试(输入cmd 中的方法 例如 dir 等等…)
TCP通信下复杂报头添加
对于需要处理多个信息的情况
思路:
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
上传下载文件代码:
服务端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import struct
import json
import hashlib
from socket import *
server = socket(AF_INET, SOCK_STREAM)
# print(server)
server.bind(('127.0.0.1', 8082))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode('utf-8')
cmd,file_path=msg.split()
m5 = hashlib.md5()
print(cmd)
if cmd == "get":
with open(r'%s' % file_path, mode='rb') as f:
for line in f:
m5.update(line)
md5=m5.hexdigest()
# 一、制作报头
header_dic={
"total_size":os.path.getsize(file_path),
"filename":os.path.basename(file_path),
"md5":md5}
header_json=json.dumps(header_dic)
header_json_bytes=header_json.encode('utf-8')
# 二、发送数据
# 1、先发送报头的长度
header_size=len(header_json_bytes)
conn.send(struct.pack('i',header_size))
# 2、再发送报头
conn.send(header_json_bytes)
# 3、最后发送真实的数据
with open(r'%s' %file_path,mode='rb') as f:
for line in f:
conn.send(line)
elif cmd == 'dup':
print('2')
# 1、先接收报头的长度
n = 0
header = b''
while n < 4:
data = conn.recv(1)
header += data
n += len(data)
header_size = struct.unpack('i', header)[0]
# 2、再接收报头
header_json_bytes = conn.recv(header_size)
header_json = header_json_bytes.decode('utf-8')
header_dic = json.loads(header_json)
# 3、最后接收真实的数据
total_size = header_dic['total_size']
filename = header_dic['filename']
recv_size = 0
with open(fr'{file_path}', mode='wb') as f:
while recv_size < total_size:
data = conn.recv(1024)
f.write(data)
recv_size += len(data)
except Exception:
break
conn.close()
server.close()
客户端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import struct
import json
import hashlib
import os
from socket import *
#客户端上传路径(服务器中所存放的位置)
#dup C:UsersAdministratorDesktop每日笔记day32图片2 img.jpg
#客户端上传路径 (用户所需要上传文件的位置)
#D:图片 img.jpg
# 客户端下载路径(服务器中文件所在的位置):
#get D:图片 img.jpg
# 客户端下载路径(用户所需要存放该文件的位置):
#C:UsersAdministratorDesktop每日笔记day32图片
client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8082))
while True:
cmd = input("服务器文件路径/或者目标地址: ").strip() # get 文件路径
addr=input('存放路径/目标存放文件路径: ').strip()
if len(cmd) == 0:
continue
if len(addr) == 0:
continue
cmds, file_path = cmd.split()
if cmds =='get':
client.send(cmd.encode('utf-8'))
# 1、先接收报头的长度
n = 0
header = b''
while n < 4:
data = client.recv(1)
header += data
n += len(data)
header_size=struct.unpack('i',header)[0]
# 2、再接收报头
header_json_bytes=client.recv(header_size)
header_json=header_json_bytes.decode('utf-8')
header_dic=json.loads(header_json)
print(header_dic)
# 3、最后接收真实的数据
total_size=header_dic['total_size']
filename=header_dic['filename']
recv_size = 0
with open(fr"{addr}%s" %filename, mode='wb') as f:
while recv_size < total_size:
data = client.recv(1024)
f.write(data)
recv_size += len(data)
elif cmds=='dup':
client.send(cmd.encode('utf-8'))
m5 = hashlib.md5()
with open(fr'{addr}', mode='rb') as f:
print('打开文件')
for line in f:
m5.update(line)
md5 = m5.hexdigest()
# 一、制作报头
header_dic = {
"total_size": os.path.getsize(addr),
"filename": os.path.basename(addr),
"md5": md5}
print(header_dic)
header_json = json.dumps(header_dic)
header_json_bytes = header_json.encode('utf-8')
# 二、发送数据
# 1、先发送报头的长度
header_size = len(header_json_bytes)
client.send(struct.pack('i', header_size))
# 2、再发送报头
client.send(header_json_bytes)
# 3、最后发送真实的数据
with open(fr'{addr}', mode='rb') as f:
for line in f:
client.send(line)
print('文件上传成功!')
client.close()
结果:
自行测试(输入上述客户端所提示的地址。请看清楚。)