Python网络编程进阶篇
下面是小凰凰的简介,看下吧!
????人生态度:珍惜时间,渴望学习,热爱音乐,把握命运,享受生活
????学习技能:网络 -> 云计算运维 -> python全栈( 当前正在学习中)
????您的点赞、收藏、关注是对博主创作的最大鼓励,在此谢过!
有相关技能问题可以写在下方评论区,我们一起学习,一起进步。
后期会不断更新python全栈学习笔记,秉着质量博文为原则,写好每一篇博文。
文章目录
一、粘包现象
首先粘包现象是TCP独有的,UDP中没有
!至于原因后面我会仔细讲解。因此我拿入门篇中的一个TCP项目(远程执行命令)
来描述这个现象!:
# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果远程控制云服务器,请改成公网ip+port,且安全组开放指定的端口
READ_SIZE = 2048
# tcp服务端
import subprocess
import socket
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from导,可以明确看出AF_INET这些属性都是模块的,而不是socket类的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
conn,addr = server.accept()
while True:
msg = conn.recv(settings.READ_SIZE)
if len(msg) == 0:
print('客户端正常close,也会发送空数据给服务端!')
break
msg = msg.decode('utf-8')
obj = subprocess.Popen(msg,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_msg = obj.stdout.read()
err_msg = obj.stderr.read()
msg = std_msg + err_msg
conn.send(msg)
conn.close()
# tcp客户端
import socket
import settings
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
msg = input('请输入您要执行的命令 >>>').strip()
if msg == '':
continue
if msg == 'exit':
break
client.send(msg.encode('utf-8'))
feedback = client.recv(settings.READ_SIZE)
print(feedback.decode('utf-8'))
client.close()
我们客户端先执行ifconfig,再执行ls /
:
发现ls /
得到的结果是ifconfig没取干净的内容
。这种现象就是粘包现象!
注意:光理论是不够的,在此送大家一套2020最新Python全栈实战****,点击此处 免费获取一起进步哦!
二、什么是粘包
上面说只有TCP有粘包现象,UDP永远不会粘包,为何,请看下文!
1、socket收发消息的原理
2、为什么TCP出现粘包现象?
TCP是面向连接的,流式协议,如果send一个数据过大,他会采用分段传输。数据过小,会粘在一起传输(nagle算法),所以TCP传输数据,像一段水流一样,流到客户端,recv(1024),是最多接收1024字节,一般缓存区只要有数据,就会recv,我们这里是本地做实验,网络延时基本不存在,所以,最多接收1024字节,那么它肯定是接收了1024字节的,但是如果,网络延时很高,水流到客户端,起初只有几个字节,那么recv也只会收几个字节,就造成了没有收干净的情况,后面的水再流过来,还是会继续放到缓存区,下次你再输入的命令,取的就是上次没取干净的内容了,一直这样下去,每次执行命令返回的结果都是错乱的。
3、为什么UDP不会出现粘包现象
然而UDP传输消息,是传输一个整个消息到服务端,且是不可靠的,如果你一次没取完,剩下的就直接扔掉了。因此它不会出现粘包现象,一般UDP用于传输短数据,UDP可以用于聊天,你会发现聊天有些时候有字数限制,这是因为要限制你的数据长度,过长就容易丢失,短的数据用UDP还是比较可靠的。
我们现在知道了是因为没收干净导致的粘包现象,因此我们接下来的解决方案,就是致力于使每次收包,都能收干净,才能执行下条命令!
补充知识点一:
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
补充知识点二:
# send(字节流)与sendall的区别。
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
三、解决粘包的low比处理方法
问题的根源在于,接收端不知道发送端将要传送的字节流的长度
,以至于收不完数据,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果远程控制云服务器,请改成公网ip+port,且安全组开放指定的端口
READ_SIZE = 2048
# tcp服务端
import subprocess
import socket
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from导,可以明确看出AF_INET这些属性都是模块的,而不是socket类的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
conn,addr = server.accept()
while True:
msg = conn.recv(settings.READ_SIZE)
if len(msg) == 0:
print('客户端正常close,也会发送空数据给服务端!')
break
msg = msg.decode('utf-8')
obj = subprocess.Popen(msg,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_msg = obj.stdout.read()
err_msg = obj.stderr.read()
msg = std_msg + err_msg
conn.send(str(len(msg)).encode('utf-8'))
flag = conn.recv(1024).decode('utf-8')
if flag == '已收到数据长度,可以开始正式传输数据!':
conn.sendall(msg)
conn.close()
# tcp客户端
import socket
import settings
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
msg = input('请输入您要执行的命令 >>>').strip()
if msg == '':
continue
if msg == 'exit':
break
client.send(msg.encode('utf-8'))
data_len = int(client.recv(1024).decode('utf-8'))
client.send('已收到数据长度,可以开始正式传输数据!'.encode('utf-8'))
recv_data_len = 0
data = b''
while recv_data_len < data_len:
feedback = client.recv(settings.READ_SIZE)
recv_data_len += len(feedback)
data += feedback
print(data.decode('utf-8'))
client.close()
执行结果:
为何low:
程序的运行速度远快于网络传输速度,所以服务端在发送一段字节的数据前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
四、解决粘包的大招
大招:自定义协议
。当然我们解决的思想还是:自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
,只是知晓的手段更加高明!
1、解决粘包的大招思想
答:我们服务端在发送数据时,在数据上封装一层固定长度的头部,头部里面装的就是数据的长度。我们客户端接收的时候,先接受那固定长度的头部,拿到数据的长度,然后再循环接收后面的数据,直到数据取完!
2、struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
>>> res = struct.pack('i',111111111) # i是什么?i是说明你要转换的对象是一个整型数字,注意长度是有限的,如果你要转换很大的数字,你需要指定l长整型或者q也可以
>>> res # i转换出来固定长度为4个字节
b'\xc7k\x9f\x06'
>>> struct.unpack('i',res) # 注意得到的是一个小元组
(111111111,)
3、大招源码
# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果远程控制云服务器,请改成公网ip+port,且安全组开放指定的端口
READ_SIZE = 2048
# tcp服务端
import subprocess
import socket
import struct
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from导,可以明确看出AF_INET这些属性都是模块的,而不是socket类的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
conn,addr = server.accept()
while True:
msg = conn.recv(settings.READ_SIZE)
if len(msg) == 0:
print('客户端正常close,也会发送空数据给服务端!')
break
msg = msg.decode('utf-8')
obj = subprocess.Popen(msg,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_msg = obj.stdout.read()
err_msg = obj.stderr.read()
msg = std_msg + err_msg
msg_len = struct.pack('i',len(msg)) # pack的结果就是个字节类型的哈
conn.send(msg_len) # 打个头部,就是在数据之前发送,没啥高深的
conn.send(msg)
conn.close()
# tcp客户端
import socket
import settings
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
msg = input('请输入您要执行的命令 >>>').strip()
if msg == '':
continue
if msg == 'exit':
break
client.send(msg.encode('utf-8'))
data_len_tuple = client.recv(4)
data_len = struct.unpack('i',data_len_tuple)[0]
recv_len = 0
data = b''
while recv_len < data_len:
feedback = client.recv(settings.READ_SIZE)
recv_len += len(feedback)
data += feedback
print(data.decode('utf-8'))
client.close()
先执行ps aux
,再执行ls /
:
成功!!像这样我们加了个头部,头部长度固定,就是自定义了个协议,只是这个协议比较简单而已。
4、存在的不足之处,定义更通用的协议
假如我是基于tcp实现的是文件的上传和下载
,把文件打开,把数据传过来,我们不仅需要将文件里面数据的长度先传到客户端,也要把文件名、文件的md5校验码(校验文件完整性)也先数据传一步传过来
,这样我们才能打开一个相同名字的文件,把接收到的数据,写入这个文件。
(1)解决思想
1. '服务端的操作:'我们可以创建一个字典,把需要先一步发送的东西,全放进去,然后用json将其序列化得到一个字符串,然后将其转为bytes类型,然后服务器会把这个bytes当作头部传输过去,因此我们需要知道头部的长度啊,因此我们需要struct把len(字典序列化后转成的bytes类型)打成一个固定长度的'头部的头部'。
2. '客户端的操作:'客户端先recv4个字节的头部,里面存的是'字典序列化转成的bytes类型的长度',unpack得到其长度假如为length,然后再recv(length),得到'字典序列化转成的bytes类型',然后decode,然后json.loads得到字典,这样就拿到了你想要先数据一步传输过来的信息。再进行其他操作即可。
(2)源码解析流程
import json,struct
# 假设通过客户端上传1T:1073741824000的文件a.txt
# 为避免粘包,必须自定制报头
header_dic={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} # 1T数据,文件路径和md5值
# 为了该报头能传送,需要序列化并且转为bytes
head_bytes=json.dumps(header_dic).encode('utf-8') # 序列化得到一个字符串并转成bytes,用于传输
# 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) # 这4个字节里只包含了一个数字,该数字是报头的长度
# 服务端开始发送
conn.send(head_len_bytes) # 先发报头的长度,4个bytes
conn.send(head_bytes) # 再发报头的字节格式
conn.sendall(文件内容) # 然后发真实内容的字节格式
# 客户端开始接收
head_len_bytes=s.recv(4) # 先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] # 提取报头的长度
head_bytes=s.recv(x) # 按照报头长度x,收取报头的bytes格式
head_dic = json.loads(head_bytes.decode('utf-8')) # 提取字典,字典里面的所有需要先数据一步传入的信息,客户端就拿到了
# 最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
五、socketserver实现并发
1、socketserver基于(TCP)实现并发
# 服务端
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self): # 这个方法必须定义,里面写TCP的通信逻辑
#通信循环
while True:
try:
data=self.request.recv(1024) # self.request就是conn那个管道
if len(data)==0:break # 发空表示客户端断开连接了,所以break掉通信循环
self.request.send(data.upper()) # 相当于conn.send(data.upper())
except Exception:
break
s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyHandler,bind_and_activate=True)
s.serve_forever() # 代表连接循环,不断accept建立连接
# 每建立一个连接就会启动一个线程(服务员),然后调用MyHandler类产生一个对象,调用该对像下的handle方法,与刚刚建立好的连接进行通信循环
# 客户端,复制多份就是多个客户端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080)) # 指定服务端ip和端口
while True:
msg=input('>>: ').strip()
if len(msg) == 0:continue
if msg = 'exit':break
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8'))
client.close() # 客户端输入命令exit,跳出通信循环,客户端close断开连接
2、socketserver基于(UDP)实现并发
# 服务端
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self): # 写UDP通信逻辑
data=self.request[0] # 首先你应该明白这个self,到底是啥,它是专为某个客户端创建MyHandler对象,这里的request和tcp的不一样,这里的self.request[0],直接获取到的是客户端发来的数据。
# UDP面向无连接,一次消息的发送,是一整个消息,想下每个消息,一个线程,对应一个Myhandler的对象,发送的数据我可以直接存储在这个对象里就行了。
# TCP是面向连接的,一次连接,多次数据交互,所以我们必须还得使用recv、send操作,不能把数据存给对象的某个数据属性,如果赋值给某个属性,那么会覆盖,得到数据不完整。
print('客户端消息',data)
self.request[1].sendto(data.upper(),self.client_address) # self.client_address获取client的ip_port小元组,指定客户端的地址端口
s=socketserver.ThreadingUDPServer(('127.0.01',8080),MyHandler)
s.serve_forever() # 来一个消息,就为其分配一个线程,然后这个线程,执行MyHandler类得到一个对象,调用handle方法,
# 客户端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
msg='client11111111111'
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data,server_addr=client.recvfrom(1024)
print(data)
client.close()
注意上述为了更加清晰的解释socketserver实现并发的原理,并没有添加粘包的处理方式。可以自己尝试。
注意:最后送大家一套2020最新企业Pyhon项目实战****,点击此处 免费获取一起进步哦!