一、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秒