2、粘包现象(struct模块)

来源:互联网 发布:autodesk的造型软件 编辑:程序博客网 时间:2024/06/11 11:53

昨天我们所做的套接字是有漏洞的,它会出现粘包现象,没有发现这个问题的我们今天会进行演示。今天也会稍微讲解一下基于udp的套接字。

 

一、基于udp的套接字

udp是无链接的,先启动哪一端都不会报错

udp服务端:

ss = socket()   #创建一个服务器的套接字ss.bind()       #绑定服务器套接字while True :       #服务器无限循环    cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)ss.close()                         # 关闭服务器套接字

udp客户端:

cs = socket()   # 创建客户套接字while True :      # 通讯循环    cs.sendto()/cs.recvfrom()   # 对话(发送/接收)cs.close()                      # 关闭客户套接字

1、udp套接字简单实例

服务端:

from socket import *udp_ss=socket(AF_INET,SOCK_DGRAM)udp_ss.bind(('127.0.0.1',8080))while True:    msg,addr=udp_ss.recvfrom(1024)    print(msg,addr)    udp_ss.sendto(msg.upper(),addr)

客户端:

from socket import *udp_cs=socket(AF_INET,SOCK_DGRAM)while True:    msg=input('>>: ').strip()    if not msg:continue    udp_cs.sendto(msg.encode('utf-8'),('127.0.0.1',8080))    msg,addr=udp_cs.recvfrom(1024)    print(msg.decode('utf-8'),addr)

2、模拟聊天(由于udp无连接,所以可以同时多个客户端去跟服务端通信)

服务端:

from socket import *udp_ss=socket(AF_INET,SOCK_DGRAM)udp_ss.bind(('127.0.0.1',8081))while True:    msg,addr=udp_ss.recvfrom(1024)    print('来自[%s]的一条消息:%s' %(addr,msg.decode('utf-8')))    msg_b=input('回复消息: ').strip()    udp_ss.sendto(msg_b.encode('utf-8'),addr)

客户端1:

from socket import *udp_cs = socket(AF_INET,SOCK_DGRAM)while True :    msg = input('请输入消息,回车发送: ').strip()    if msg == 'quit' : break    if not msg : continue    udp_cs.sendto(msg.encode('utf-8'),('127.0.0.1',8081))    back_msg,addr = udp_cs.recvfrom(1024)    print('来自[%s]的一条消息:%s' %(addr,back_msg.decode('utf-8')))udp_cs.close()

客户端2:

from socket import *udp_cs = socket(AF_INET,SOCK_DGRAM)while True :    msg = input('请输入消息,回车发送: ').strip()    if msg == 'quit' : break    if not msg : continue    udp_cs.sendto(msg.encode('utf-8'),('127.0.0.1',8081))    back_msg,addr = udp_cs.recvfrom(1024)    print('来自[%s]的一条消息:%s' %(addr,back_msg.decode('utf-8')))udp_cs.close()

客户端3:

... ...

因为不同的客户端是向同一个服务端发送信息所以客户端的代码都相同,如果兴趣的可以用几台电脑来测试(电脑需要联网)


 

二、粘包现象

先做粘包现象:

服务端:

from socket import *phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)phone.bind(('127.0.0.1',8080))phone.listen(5)conn,client_addr=phone.accept()data1=conn.recv(1024)print('data1: ',data1)data2=conn.recv(1024)print('data2:',data2)

客户端:

from socket import *phone=socket(AF_INET,SOCK_STREAM)phone.connect(('127.0.0.1',8080))phone.send('hello'.encode('utf-8'))phone.send('world'.encode('utf-8'))

我们再将上个随笔里的ssh例子拿出来(先执行 ipconfig /all 再执行 dir 看结果)

客户端:

from socket import *import subprocesscs=socket(AF_INET,SOCK_STREAM)cs.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)cs.bind(('127.0.0.1',8082))cs.listen(5)print('starting...')while True:    conn,addr=cs.accept()    print('-------->',conn,addr)    while True:        try:            cmd=conn.recv(1024)            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,                                   stdout=subprocess.PIPE,                                   stderr=subprocess.PIPE)            stdout=res.stdout.read()            stderr=res.stderr.read()            #发送命令的结果            conn.send(stdout+stderr)        except Exception:            break    conn.close() #挂电话cs.close() #关机

服务端:

from socket import *ss=socket(AF_INET,SOCK_STREAM) #买手机ss.connect(('127.0.0.1',8082)) #绑定手机卡#发,收消息while True:    cmd=input('>>: ').strip()    if not cmd:continue    ss.send(cmd.encode('utf-8'))    cmd_res=ss.recv(1024)    print(cmd_res.decode('gbk'))ss.close()

注意:

subprocess模块的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码


 

三、粘包

注意:只有TCP有粘包现象,UDP永远不会粘包,首先需要掌握一个socket收发消息的原理

应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

两种情况会粘包:

1、发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

2、接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

拆包的发生情况:

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。


 

四、解决粘包方法

粘包现象中第一个现象解决:

解决一:(需要知道每次发过来的数据大小 不现实)

from socket import *phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)phone.bind(('127.0.0.1',8080))phone.listen(5)conn,client_addr=phone.accept()data1=conn.recv(10)print('data1: ',data1)data2=conn.recv(4)print('data2:',data2)
服务端
from socket import *phone=socket(AF_INET,SOCK_STREAM)phone.connect(('127.0.0.1',8080))phone.send('helloworld'.encode('utf-8'))phone.send('egon'.encode('utf-8'))
客户端

解决二:

from socket import *phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)phone.bind(('127.0.0.1',8080))phone.listen(5)conn,client_addr=phone.accept()data1=conn.recv(1024)print('data1: ',data1)data2=conn.recv(1024)print('data2:',data2)
服务端
from socket import *import timephone=socket(AF_INET,SOCK_STREAM)phone.connect(('127.0.0.1',8080))phone.send('hello'.encode('utf-8'))time.sleep(5)phone.send('world'.encode('utf-8'))
客户端

ssh例子问题解决:

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

from socket import *import subprocessimport structss=socket(AF_INET,SOCK_STREAM)ss.bind(('127.0.0.1',8082)) ss.listen(5)print('starting...')while True:    conn,addr=ss.accept()    print('-------->',conn,addr)    while True:        try:            cmd=conn.recv(1024)            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,                                   stdout=subprocess.PIPE,                                   stderr=subprocess.PIPE)            stdout=res.stdout.read()            stderr=res.stderr.read()            #先发报头(转成固定长度的bytes类型)            header = struct.pack('i',len(stdout)+len(stderr))            conn.send(header)            #再发送命令的结果            conn.send(stdout)            conn.send(stderr)        except Exception:            break    conn.close()ss.close()
服务端
from socket import *import structcs=socket(AF_INET,SOCK_STREAM)cs.connect(('127.0.0.1',8082))while True:    cmd=input('>>: ').strip()    if not cmd:continue    cs.send(cmd.encode('utf-8'))    #先收报头    header_struct=cs.recv(4)    unpack_res = struct.unpack('i', header_struct)    total_size=unpack_res[0]    #再收数据    recv_size=0 #10241=10240+1    total_data=b''    while recv_size < total_size:        recv_data=cs.recv(1024)        recv_size+=len(recv_data)        total_data+=recv_data    print(total_data.decode('gbk'))cs.close()
客户端

 

五、struct模块(了解)

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

struct.pack('i',11111111)#struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

struct.pack用于将Python的值根据格式符,转换为字符串(因为Python中没有字节(Byte)类型)。它的函数原型为:struct.unpack(fmt, string)。

struct.unpack做的工作刚好与struct.pack相反,用于将字节流转换成python数据类型。它的函数原型为:struct.unpack(fmt, string),该函数返回一个元组