进击のpython
网络编程——粘包现象
前面我们提到了套接字的使用方法,以及相关bug的排除
还记得我们提到过一个1024吗?
我们现在要针对这个来研究一下一个陷阱
在研究这个陷阱之前我要先教你几条语句
这是windows的命令啊
ipfonfig 查看本地网卡的ip地址
dir 查看某一个文件夹下的子文件名和子文件夹名
tasklist 查看运行的进程
那我这三条命令怎么执行呢??直接敲??
好像没什么用,所以说我需要打开我的cmd窗口来键入这些命令
而cmd也就是一个能把特殊的字母组合执行出来的一个程序而已
当我在cmd里键入dir的时候得到的就是这些东西
那我想在编译器里搞这个东西呢?
哦!第一反应就是os模块
import os
os.system("dir")
就执行起来了吧
那我这算是拿到结果了吗?
我觉得不算,为什么?
咱们想要达到的效果是我在客户端输入一个dir发送给服务端,服务端给我返回这一堆东西才叫拿到结果了是吧
import os
res = os.system("dir")
print(f"返回的结果是:{res}")
那结果我打印的是什么呢??是0!那为什么是这个呢?
这个0是代表这个命令是不是成功
如果返回的是0,就是成功了,如果是非零,就是失败了!
所以说他返回的是一个是否成功执行语句的状态,而不是执行语句的返回结果
那os模块就被pass掉了,因为他无法返回我们需要的东西
那除了os.还有什么吗?subprocess
他下面有一个方法
import subprocess
subprocess.Popen()
里面接收两个参数,第一个参数是字符串的命令
第二个是shell=True,作用是在终端也就是cmd下运行
那我这么写就没问题了
import subprocess
subprocess.Popen("dir", shell=True)
但是我不要把这个结果给终端,我要把这个结果给客户端
那我是不是就要把结果传进管道然后进行传输呢?
好,那这个方法就可以传递第三个属性stdout = subprocess.PIPE
这个管道是用来接收正确的结果的
那错误的结果传在哪呢?第四个属性! stderr = subprocess.PIPE
好,当我把所有的参数都填进去之后我们再看,是不是在控制台就没有输出结果了啊
输出结果去哪了呢?放到管道里去了!
import subprocess
obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
那我想把管道里的东西取出来怎么弄呢?
我们先来看看我打印obj是个啥?很明显是个对象是吧
那既然是对象就能调用方法
print(obj.stdout)
<_io.BufferedReader name=3>
看到IO第一反应就是文件,用read()方法读一下
你发现你打印的时候什么???这不就是我们想要的字节类型的数据嘛
然后我们再来看
import subprocess
obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(obj.stdout.read().decode("gbk"))
print(obj.stdout.read().decode("gbk"))
打印了几次?只有一次!为什么?
因为我把数据放到管道之后,第一次打印就把结果拿出来了,第二次再拿就啥也拿不到了
反而错误管道里就有信息了
那现在就可以把代码写进去吧
# 服务端
import socket
import subprocess
# 买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定手机卡
phone.bind(("127.0.0.1", 8080))
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 开机
phone.listen(5)
# 等电话
connet, client_addr = phone.accept()
# 收发消息
while 1:
try:
k = connet.recv(1024)
obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
connet.send(stdout + stderr)
except ConnectionResetError:
break
# 挂电话
connet.close()
# 关机
phone.close()
# 客户端
import socket
# 买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 拨号
phone.connect(("127.0.0.1", 8080))
# 发收信息
while 1:
msg = input(">>>")
phone.send(msg.encode("gbk"))
k = phone.recv(1024)
k = k.decode("gbk")
print(f"从服务端接收的消息:{k}")
connet.close()
# 关闭
phone.close()
其实到现在,我们其实完成了一个模拟ssh远程执行命令的操作,就是客户端获取主机的信息的操作
但是我们会发现问题,我要是传的超过1024个字节的数据,怎么办呢???
还有一个就是 stdout + stderr 这个加号就相当于新开个内存空间
而不是直接在他本身上加,所以这个效率应该是可以优化的
比如说我传一个超过1024的,会发生什么现象?
我先打印一下D盘的文件
我发现信息没有完整的显示出来
然后我再打印一下IP信息
我会发现,打印结果的上面,还有上一次打印没打印出来的信息!
说明溢出的数据没有丢,而是等待下一次的调用然后传过去
其实也很好理解
我们这不是流式传输嘛,那水流还能 嘎巴 一下就断了?
不能吧,当我拿到一点水后关闭通道,本该进来的水就因为容器太小的原因
留在了外面,等到下次调用的时候,在外面的这一部分就先进来,然后再进其他的
这个现象,就叫粘包!
指的是多个包的返回值黏在一起了
那应该怎么解决呢?
我是不是可以在发消息之前,先告诉服务端我要传多大的文件
然后服务端就对这个信息做出相应的操作
有一种方法是把1024改一个更大的数,但其实
还是治标不治本,因为你不知道返回值有多大,就有可能被超越
那还有一种方法,那我没接完我就继续接呗
那我就应该把数据长度发给客户端,然后再发送数据
然后再说说+的问题,因为他是流数据,那我按顺序发,他是不是就自己拼上了啊
connet.send(stdout)
connet.send(stderr)
而服务端首先应该接收到数据长度然后再接收数据是吧
那我客户端大概应该这么写
msg = input(">>>")
phone.send(msg.encode("gbk"))
re_len = 1025 # 数据长度
re_size = 0
r = b"" # 我传过来的是字节模式
if re_size < re_len:
k = phone.recv(1024)
r += k
re_size += len(k)
print(f"从服务端接收的消息:{k}")
那这个数据长度,就应该是服务端传过来的对吧
所以服务端大概应该这么写
stdout = obj.stdout.read()
stderr = obj.stderr.read()
r_size = len(stdout)+len(stderr)
connet.send(str(r_size).encode("gbk")) # 数字模式不能传,只能传字符串
connet.send(stdout)
connet.send(stderr)
但是问题就出现了!我这三个发送信息也是粘包,那我怎么能让客户端进行分辨?
还记得我们在说传输数据的时候提到了报头的概念嘛?
所以其实我们是在写报头,而报头是固定长度的
所以我现在就要学会如何发报头对吧!
那我们现在开始自定义报头吧
这时候我们就需要学习一个新的模块struct
struck.pack()
相当于打包这里面传的是两个参数,第一个是数据类型,第二个的是数据
res = struct.pack("i", 1234)
print(res, type(res), len(res))
打印的是:
b'xd2x04x00x00' <class 'bytes'> 4
所以,我这就算是拿到了报头的长度
那我这打包怎么解包???
res = struct.unpack("i",res)
(1234,)
我拿到的是元组,所以[0]是不是就拿到了1234了
那长度是不是也就拿到了
那服务端就可以写了
r_size = len(stdout) + len(stderr)
res = struct.pack("i", r_size)
那客户端就知道怎么做了
res = phone.recv(4)
re_len = struct.unpack("i", res)[0]
那总的来说,代码就如下:
# 客户端
import socket
# 买手机
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 拨号
phone.connect(("127.0.0.1", 8080))
# 发收信息
while 1:
msg = input(">>>")
phone.send(msg.encode("gbk"))
res = phone.recv(4)
re_len = struct.unpack("i", res)[0]
re_size = 0
r = b"" # 我传过来的是字节模式
while re_size < re_len:
k = phone.recv(1024)
r += k
re_size += len(k)
print(re_size, re_len)
print(f'从服务端接收的消息:{r.decode("gbk")}')
connet.close()
# 关闭
phone.close()
# 服务端
import socket
import struct
import subprocess
# 买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定手机卡
phone.bind(("127.0.0.1", 8080))
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 开机
phone.listen(5)
# 等电话
connet, client_addr = phone.accept()
# 收发消息
while 1:
try:
k = connet.recv(1024)
obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
r_size = len(stdout) + len(stderr)
res = struct.pack("i", r_size)
connet.send(res)
connet.send(stdout)
connet.send(stderr)
except ConnectionResetError:
break
# 挂电话
connet.close()
# 关机
phone.close()