一、TCP协议 粘包现象 和解决方案

黏包现象
让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
执行远程命令的模块

需要用到模块subprocess

subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。

import subprocess
sub_obj = subprocess.Popen(
'ls', #系统指令
shell=True, #固定
stdout=subprocess.PIPE, #标准输出 PIPE 管道,保存着指令的执行结果
stderr=subprocess.PIPE #标准错误输出
)
print('正确输出',sub_obj.stdout.read().decode('gbk'))
print('错误输出',sub_obj.stderr.read().decode('gbk'))

基于tcp协议实现的黏包

###server
while 1:
from_client_cmd=conn.recv(1024)
print(from_client_cmd.decode('utf-8'))#接收客户端数据解码 sub_obj=subprocess.Popen(
from_client_cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
###client
import socket
client=socket.socket()
client.connect(('127.0.0.1',8001)) while 1:
cmd=input('请输入指令:')
client.send(cmd.encode('utf-8'))
server_cmd_result=client.recv(1024)
print(server_cmd_result.decode('gbk'))

这就是黏包现象

因为每次执行,固定为1024字节。它只能接收到1024字节,那么超出部分怎么办?
等待下一次执行命令dir时,优先执行上一次,还没有传完的信息。传完之后,再执行dir命令

 总结:

发送过来的一整条信息
由于server端没有及时接受
后来发送的数据和之前没有接收完的数据黏在了一起
这就是著名的黏包现象

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

解决方案一

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

原理:

黏包现象的成因

  你不知道在哪儿断句

解决问题

  在发送数据的时候,先告诉对方要发送的大小就可以了

自定义协议

先和服务端商量好,发送多少字节,再传输数据。

####server
# 原理
# 黏包现象的成因
# 你不知道在哪儿断句
# 解决问题
# 在发送数据的时候,先告诉对方要发送的大小就可以了
# 在发送的时候 先发送数据的大小 在发送内容
# 在接受的时候 先接受大小 再根据大小接受内容
# 自定义协议 #_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080) tcp_socket_server=socket()
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept()
lenth = conn.recv(1) # 接收1个字节,返回 b'5'
#print(lenth)
lenth = int(lenth.decode('utf-8')) # 转化字符串,返回5 data1=conn.recv(lenth) # 接收5字节,返回 b'hello'
lenth2 = conn.recv(1) # 接收1个字节
lenth2 = int(lenth2.decode('utf-8')) # 转化字符串,返回3
data2=conn.recv(lenth2) # 接收3个字节,返回b'egg' print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8')) conn.close()
tcp_socket_server.close()
####client
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080) s=socket.socket()
res=s.connect_ex(ip_port) # 功能与connect(address)相同,但是成功返回0,失败返回errno的值
lenth = str(len('hello')).encode('utf-8') # 获取hello的字符的长度,并转化为str,最后编码
s.send(lenth) # 发送数字5
s.send('hello'.encode('utf-8')) # 发送hello
lenth = str(len('egg')).encode('utf-8') # 获取长度,结果为3
s.send(lenth) # 发送3
s.send('egg'.encode('utf-8')) # 发送egg s.close()
先执行服务端,再执行客户端,执行输出:

-----> hello
-----> egg
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶

刚刚的方法,问题在于我们我们在发送

通过struck模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到对端,对端只要取出前4个字节,然后对这四个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。

这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
ret = struct.pack('i',1000000) # i表示int类型
print(ret)
print(len(ret)) # 返回4 ret1 = struct.unpack('i',ret) # 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
print(ret1) # 返回一个元组 执行输出:
b'@B\x0f\x00'
4
(1000000,)

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时

先发报头长度

先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
####server
import socket
import subprocess
import struct
server = socket.socket()
ip_port = ('127.0.0.1',8001)
server.bind(ip_port)
server.listen()
conn,addr = server.accept()
while 1:
from_client_cmd = conn.recv(1024)#接收的大小 print(from_client_cmd.decode('utf-8'))#答应查看一下
#接收到客户端发送来的系统指令,我服务端通过subprocess模块到服务端自己的系统里面执行这条指令
sub_obj = subprocess.Popen(
from_client_cmd.decode('utf-8'),#解析客户端发来的命令
shell=True,
stdout=subprocess.PIPE, #正确结果的存放位置
stderr=subprocess.PIPE #错误结果的存放位置
)
#从管道里面拿出结果,通过subprocess.Popen的实例化对象.stdout.read()方法来获取管道中的结果
std_msg = sub_obj.stdout.read()#管道里面拿出结果 #为了解决黏包现象,我们统计了一下消息的长度,先将消息的长度发送给客户端,客户端通过这个长度来接收后面我们要发送的真实数据
std_msg_len = len(std_msg)
print('指令的执行结果长度>>>>',len(std_msg)) msg_lenint_struct = struct.pack('i',std_msg_len)#把长度byes加上int标识 4个长度 conn.send(msg_lenint_struct+std_msg)#发送拼接给客户端
####client
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1',8001))
while 1:
cmd = input('请输入指令:')
#发送指令
client.send(cmd.encode('utf-8'))#发送给server你想要执行的命令
#接收数据长度,首先接收4个字节长度的数据,因为这个4个字节是长度
server_res_len = client.recv(4)
msg_len = struct.unpack('i',server_res_len)[0] print('来自服务端的消息长度',msg_len)
#通过解包出来的长度,来接收后面的真实数据
server_cmd_result = client.recv(msg_len) print(server_cmd_result.decode('gbk'))

简单的文件传送

文件的上传和下载

需要文件的名字,文件的大小,文件的内容

自定义一个文件传输协议:

{'filesize':000,'filename':'XXXX'}

###server
import os
import json
import struct
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen() conn,addr=sk.accept() #接收来自客户端的连接
dic={'filename':'python18期 2组员资料.rar',
'filesize': os.path.getsize(r'E:\python18期 2组员资料.rar')
}
str_dic=json.dumps(dic).encode('utf-8')#把字典转换成json然后转成byes
dic_len=struct.pack('i',len(str_dic))#把长度byes加上int标识 4个长度
conn.send(dic_len +str_dic) # conn.send(str_dic)
with open(r'E:\python18期 2组员资料.rar','rb')as f:
content=f.read()
conn.send(content)
conn.close()
sk.close()
import struct
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',9090))
dic_len=sk.recv(4)
dic_len=struct.unpack('i',dic_len)[0]#接受到长度
str_dic = sk.recv(dic_len).decode('utf-8')#接收server的字节
dic = json.loads(str_dic)
# print(dic)#{'filename': 'python18期 2组员资料.rar', 'filesize': 4786326} #接收到的文件名 包大小
with open(dic['filename'],'wb') as f:
content=sk.recv(dic['filesize'])
f.write(content)
sk.close()

注意:

大文件的传输,不能一次性读到内存里

上传一个视频,几台电脑之间能互相传,视频要3个G左右。

进阶需求,加一个登陆功能

1. 引入模块
import hashlib
2. 创建md5对象(实例化)
obj = hashlib.md5(b"盐")
3. 把加密的内容交给md5
obj.update(bytes)
4. 获取密文
obj.hexdigest()

import  hashlib
obj=hashlib.md5(b'121212')#加盐
obj.update('2131231'.encode('utf-8'))
print(obj.hexdigest())#拿到密文

server.py

import os
import json
import struct
import socket
import hashlib sk = socket.socket()
sk.bind(('127.0.0.1',9999))
sk.listen() conn,addr = sk.accept()
print(addr) filename = '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4' # 文件名
absolute_path = os.path.join('E:\BaiduYunDownload',filename) # 文件绝对路径
buffer_size = 1024*1024 # 缓冲大小,这里表示1MB md5obj = hashlib.md5()
with open(absolute_path, 'rb') as f:
while True:
content = f.read(buffer_size) # 每次读取指定字节
if content:
md5obj.update(content)
else:
break # 当内容为空时,终止循环 md5 = md5obj.hexdigest()
print(md5) # 打印md5值 dic = {'filename':filename, 'filename_md5':str(md5),'buffer_size':buffer_size,
'filesize':os.path.getsize(absolute_path)}
str_dic = json.dumps(dic).encode('utf-8') # 将字典转换为json
dic_len = struct.pack('i', len(str_dic)) # 获取字典长度,转换为struct
conn.send(dic_len) # 发送字典长度
conn.send(str_dic) # 发送字典 with open(absolute_path, 'rb') as f: # 打开文件
while True:
content = f.read(buffer_size) # 每次读取指定大小的字节
if content: # 判断内容不为空
conn.send(content) # 每次读取指定大小的字节
else:
break conn.close() # 关闭连接
sk.close() # 关闭套接字

client.py

import json
import struct
import socket
import hashlib
import time start_time = time.time()
sk = socket.socket()
sk.connect(('127.0.0.1',9999)) dic_len = sk.recv(4) # 接收4字节,因为struct的int为4字节
dic_len = struct.unpack('i',dic_len)[0] # 反解struct得到元组,获取元组第一个元素
#print(dic_len) # 返回一个数字
str_dic = sk.recv(dic_len).decode('utf-8') # 接收指定长度,获取完整的字典,并解码
#print(str_dic) # json类型的字典
dic = json.loads(str_dic) # 反序列化得到真正的字典
#print(dic) # 返回字典 md5 = hashlib.md5()
with open(dic['filename'],'wb') as f:
while True:
content = sk.recv(dic['buffer_size'])
if not content:
break
md5.update(content)
md5 = md5.hexdigest()
print(md5) # 打印md5值 if dic['filename_md5'] == str(md5):
f.write(content)
print('md5校验正确--下载成功')
else:
print('文件验证失败') sk.close() end_time = time.time()
print('本次下载花费了{}秒'.format(end_time-start_time))

先执行server.py,再执行client.py

server输出:

('127.0.0.1', 54230)
30e63a254cf081e8e93c036b21057347

client输出:

30e63a254cf081e8e93c036b21057347
md5校验正确--下载成功
本次下载花费了25.687340021133423秒

python tcp黏包和struct模块解决方法,大文件传输方法及MD5校验的更多相关文章

  1. Python之黏包的解决

    黏包的解决方案 发生黏包主要是因为接收者不知道发送者发送内容的长度,因为tcp协议是根据数据流的,计算机操作系统有缓存机制, 所以当出现连续发送或连续接收的时候,发送的长度和接收的长度不匹配的情况下就 ...

  2. Python之黏包

    黏包现象 让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd) res=subprocess.Popen(cmd.decode('utf-8'), shell ...

  3. tcp粘包问题原因及解决办法

    1.粘包概念及产生原因 1.1粘包概念: TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾. 粘包可能由发送方造成,也可能由接收方造成. ...

  4. Python网络编程基础 struct模块 解决黏包问题 FTP

    struct模块 解决黏包问题 FTP

  5. 黏包-黏包的成因、解决方式及struct模块初识、文件的上传和下载

    黏包: 同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包. 只有TCP协议中才会产生黏包,UDP协议中不会有黏包(udp协议中数 ...

  6. Python网络编程基础 ❷ 基于upd的socket服务 TCP黏包现象

    TCP的长连接 基于upd的socket服务 TCP黏包现象

  7. (day27)subprocess模块+粘包问题+struct模块+ UDP协议+socketserver

    目录 昨日回顾 软件开发架构 C/S架构 B/S架构 网络编程 互联网协议 socket套接字 今日内容 一.subprocess模块 二.粘包问题 三.struct模块 四.UDP 五.QQ聊天室 ...

  8. socketserver tcp黏包

    socket (套接字) tcp(黏包现象原因) 传输中由于内核区缓冲机制(等待时间,文件大小),会在 发送端 缓冲区合并连续send的数据,也会出现在 接收端 缓冲区合并recv的数据给指定port ...

  9. netty]--最通用TCP黏包解决方案

    netty]--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender 2017年02月19日 15:02:11 惜暮 阅读数:1 ...

随机推荐

  1. P1055 书号

    P1055 题目描述 每一本正式出版的图书都有一个ISBN号码与之对应,ISBN码包括99位数字.11位识别码和33位分隔符,其规定格式如x-xxx-xxxxx-x,其中符号-就是分隔符(键盘上的减号 ...

  2. hibernate中持久化对象的生命周期(转载)

    三态的基本概念 1, 临时状态(Transient):也叫自由态,只存在于内存中,而在数据库中没有相应数据.用new创建的对象,它没有持久化,没有处于Session中,处于此状态的对象叫临时对象: 2 ...

  3. Hdoj 1102.Constructing Roads 题解

    Problem Description There are N villages, which are numbered from 1 to N, and you should build some ...

  4. 【agc030f】Permutation and Minimum(动态规划)

    [agc030f]Permutation and Minimum(动态规划) 题面 atcoder 给定一个长度为\(2n\)的残缺的排列\(A\),定义\(b_i=min\{A_{2i-1},A_{ ...

  5. 【LOJ#2542】[PKUWC2018]随机游走(min-max容斥,动态规划)

    [LOJ#2542][PKUWC2018]随机游走(min-max容斥,动态规划) 题面 LOJ 题解 很明显,要求的东西可以很容易的进行\(min-max\)容斥,那么转为求集合的\(min\). ...

  6. 「浙大ACM」图森未来杯游记一篇以及简易口胡题解

    前言 蒟蒻有参加了ACM比赛,这一次有适合HY和慕容宝宝大佬一起比的,他们好巨啊,把我带飞了. 又是窝掌机,QAQ,他们仗着自己巨,就欺负窝... 我又打了\(4\)个小时的代码,而且那个键盘太恶心了 ...

  7. Arukas.io云主机安装CentOS

    创建应用   1 jdeathe/centos-ssh:centos-6 启动应用 电机启动应用,应用会自动部署,等显示Running 就说明成功了.估计需要几分钟. 查看用户以及密码 自己保存下用户 ...

  8. 洛谷P2468 SDOI 2010 粟粟的书架

    题意:给你一个矩形书架,每个点是这本书的页数,每次询问(x1,y1)(x2,y2)这个小矩形里最少需要取几本书使得页数和等于Hi. 题解:小数据二位前缀和预处理+二分答案,大数据一行所以用主席树做,感 ...

  9. maven插件运行过程中自动执行sql文件

    配置pom.propertis即可 <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId> ...

  10. SRM 605 div 2 T3

    #include <bits/stdc++.h> #define Mo 1000000007 #define MAXN 50 #define MAXK 10 using namespace ...