补充知识
首先需要掌握一个socket收发消息的原理
在我们使用socket的时候,两个网络之间是怎么具体操作的我们先不看,只先关注服务器端和客户端。用户态和内核态都是内存的状态,用户态是应用程序的,内核态是操作控制底层硬件的。
当客户端socket程序想向服务器端的socket程序发送消息的时候,不可能自己就发过去了,他需要用过操作系统也就是内核态去完成。
1、首先消息msg从客户端socket程序拷贝到客户端的缓存(操作系统)
2、然后由操作系统决定你什么时候发以及以什么形式发。
3、服务器端的操作系统接收msg之后放入缓存中
4、服务器端的socket程序从服务端的内核态中取
由于他都是在自己的缓存中拿数据,导致了其根本不知道对方一次发过来的数据有多少,所以产生了粘包现象
粘包现象
粘包现象产生的原因
第一种:
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起发送,产生粘包)
产生的具体原因:tcp中有一个negal算法,用途是这样的:通信两端有很多小的数据包要发送,虽然传送的数据很少,但是流程一点没少,也需要tcp的各种确认,校验。这样小的数据包如果很多,会造成网络资源很大的浪费,negal算法做了这样一件事,当来了一个很小的数据包,我不急于发送这个包,而是等来了更多的包,将这些小包组合成大包之后一并发送,不就提高了网络传输的效率的嘛。这个想法收到了很好的效果,但是我们想一下,如果是分属于两个不同页面的包,被合并在了一起,这就是粘包问题。
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello feng'.encode('utf-8'))
客户端
第二种:
接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
服务器端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(10)
data2=conn.recv(10)
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
客户端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
解决办法
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
方案一:
发送端发送数据之前,先将数据长度让接收端知晓,接收端根据总长度利用循环完成消息的接收。
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(ip_port)
s.listen(5)
while True:
conn,addr=s.accept()
print('客户端',addr)
while True:
msg=conn.recv(1024)
if not msg:break
res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
stdin=subprocess.PIPE,\
stderr=subprocess.PIPE,\
stdout=subprocess.PIPE)
err=res.stderr.read()
if err:
ret=err
else:
ret=res.stdout.read()
data_length=len(ret)
conn.send(str(data_length).encode('utf-8'))
data=conn.recv(1024).decode('utf-8')
if data == 'recv_ready':
conn.sendall(ret)
conn.close()
服务端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if len(msg) == 0:continue
if msg == 'quit':break
s.send(msg.encode('utf-8'))
length=int(s.recv(1024).decode('utf-8'))
s.send('recv_ready'.encode('utf-8'))
send_size=0
recv_size=0
data=b''
while recv_size < length:
data+=s.recv(1024)
recv_size+=len(data)
print(data.decode('utf-8'))
客户端
方案二:
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
知识补充:
struck模块:
对python基本类型值与用python字符串格式表示的c struct类型间的转化。
|
#该模块可以把一个类型,如数字,转成固定长度的bytes类型
import struct
res = struct.pack('i',12345)
print(res,len(res),type(res)) #长度是4
res2 = struct.pack('i',12345111)
print(res,len(res),type(res2)) #长度也是4
unpack_res =struct.unpack('i',res2)
print(unpack_res) #(12345111,)
print(unpack_res[0]) #12345111
所以我们通过struck模块将需要发送的内容的长度进行打包成一个4字节长度的数据发送给接收端,接收端只要取出前4个字节,然后对这个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。
#服务端
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8080)) #绑定手机卡
phone.listen(5) #阻塞的最大数
print('start runing.....')
while True: #链接循环
coon,addr = phone.accept()# 等待接电话
print(coon,addr)
while True: #通信循环
# 收发消息
cmd = coon.recv(1024) #接收的最大数
print('接收的是:%s'%cmd.decode('utf-8'))
#处理过程
res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
stdout=subprocess.PIPE, #标准输出
stderr=subprocess.PIPE #标准错误
)
stdout = res.stdout.read()
stderr = res.stderr.read()
# 制作报头
header_dic = {
'total_size': len(stdout)+len(stderr), # 总共的大小
'filename': None,
'md5': None
}
header_json = json.dumps(header_dic) #字符串类型
header_bytes = header_json.encode('utf-8') #转成bytes类型(但是长度是可变的)
#先发报头的长度
coon.send(struct.pack('i',len(header_bytes))) #发送固定长度的报头
#再发报头
coon.send(header_bytes)
#最后发命令的结果
coon.send(stdout)
coon.send(stderr)
coon.close()
phone.close()
#客户端
import socket
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080)) #连接服务器
while True:
# 发收消息
cmd = input('请你输入命令>>:').strip()
if not cmd:continue
phone.send(cmd.encode('utf-8')) #发送
#先收报头的长度
header_len = struct.unpack('i',phone.recv(4))[0] #吧bytes类型的反解
#在收报头
header_bytes = phone.recv(header_len) #收过来的也是bytes类型
header_json = header_bytes.decode('utf-8') #拿到json格式的字典
header_dic = json.loads(header_json) #反序列化拿到字典了
total_size = header_dic['total_size'] #就拿到数据的总长度了
#最后收数据
recv_size = 0
total_data=b''
while recv_size<total_size: #循环的收
recv_data = phone.recv(1024) #1024只是一个最大的限制
recv_size+=len(recv_data) #有可能接收的不是1024个字节,或许比1024多呢,
# 那么接收的时候就接收不全,所以还要加上接收的那个长度
total_data+=recv_data #最终的结果
print('返回的消息:%s'%total_data.decode('gbk'))
phone.close()