粘包问题

加上链接循环的socket

上一篇博客我们写了如何用套接字socket实现两台电脑间的信息发送,但是它的弊端很明显,就是我们发完一个消息就没得了。直接就结束了,所以我们要怎么实现他可以不停地发送信息呢?

server端

import socket
#生成一个socket对象
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#绑定地址跟端口号
soc.bind(('127.0.0.1',8001))
#监听(半连接池的大小),不是连接数
soc.listen(3)
#等着客户端来连接,conn相当于连接通道,addr是客户端的地址
while True:
    print('等待客户端连接')
    conn,addr=soc.accept()    #卡主,如果没有客户端连接,会一直卡在这,当有连接,才继续往下走
    print('有个客户端连接上了',addr)
    while True:
        try:
            #windows如果客户端断开,会报错,加了try
            #linux如果客户端,断开,不会报错,会收到空,所有当data为空时,也break
            #等待接收,最大收取1024个字节
            data=conn.recv(1024)    #会卡主,当客户端有数据过来,才会执行
            if len(data)==0:  #处理linux客户端断开,如果在window下这段代码根本不会执行(即便是客服端发了空,这个地方也不会走到)
                break
            print(data)
        except Exception:

            break
    # 关闭通道
    conn.close()


# 关闭套接字
soc.close()

client端

import socket

soc=socket.socket()

soc.connect(('127.0.0.1',8001))
while True:
    in_s=input('请输入要发送的数据:')
    #发送的数据必须是b格式,in_s.encode('utf-8')  把字符串编码成b格式
    #把b格式转成字符串
    # ss=str(b'hello',encoding='utf-8')
    # ss=b'hello'.decode('utf-8')
    # #把字符串转成b格式
    # by=bytes('hello',encoding='utf-8')
    # by='hello'.encode('utf-8')


    soc.send(in_s.encode('utf-8'))

每一步的注释都已经写好了,看着就行了,这是基础,还不是难点。

模拟ssh

ssh就是远程执行命令

这里要引出一个模块 subproess

这个模块的作用就是执行系统命令

我们来模拟一下

server端

import socket
import subprocess
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8001))
soc.listen(3)
while True:
    print('等待客户端连接')
    conn,addr=soc.accept()
    print('有个客户端连接上了',addr)
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:
                break
            print(data)
            obj = subprocess.Popen(str(data,encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            #执行正确的结果 b 格式,gbk编码(windows平台)
            msg=obj.stdout.read()
            #把执行的结果通过网络传给c端
            conn.send(msg)
        except Exception:

            break
    # 关闭通道
    conn.close()


# 关闭套接字
soc.close()

这里的代码和之前一样,只不过这里要传送的数据,是执行了客户端发来的命令之后而产生的。

client端

import socket

soc=socket.socket()

soc.connect(('127.0.0.1',8001))
while True:
    in_s=input('请输入要执行的命令:')
    soc.send(in_s.encode('utf-8'))
    data=soc.recv(1024)
    print(str(data,encoding='gbk'))

粘包问题

什么是粘包问题

就是我们之前的语句 data=soc.recv(1024),里面的1024代表的是什么,就是一次性拿取的字节数。好这里放一下。

我们都知道我们是从应用层发送数据,然后一直传下去,再传到对方的物理层对吧,这个过程,我们传送的数据包,其实都会经过网卡,数据包都会在网卡上短暂的停留,这时候,要是我们过快的传送了下一个数据包,这时候前一个数据包还在网卡中,这时候就会把两个数据包当成了一份,然后一起发送了出去,这个时候,接收端就会照单全收,两个数据包变成了一个。这就是粘包,数据包都粘在一起。

那么我们怎么解决这种问题呢?

我们是不是可以告诉对方,我们的数据包的大小,这样即使发生了粘包,接收端也可以控制拿取得大小。

server端

import socket
import subprocess
import struct
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8001))
soc.listen(3)
while True:
    print('等待客户端连接')
    conn,addr=soc.accept()
    print('有个客户端连接上了',addr)
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:
                break
            print(data)
            obj = subprocess.Popen(str(data,encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            #执行正确的结果 b 格式,gbk编码(windows平台)
            msg=obj.stdout.read()
            #发送的时候需要先把长度计算出来
            #头必须是固定长度
            #10
            #100
            #先取出要发送数据长度l

            l=len(msg)
            #head 是固定四个字节
            head=struct.pack('i',l)
            #发了头
            conn.send(head)
            #发了内容
            conn.send(msg)
        except Exception:

            break
    # 关闭通道
    conn.close()


# 关闭套接字
soc.close()

有人有疑问了,这什么玩意儿,不是只用send一个长度的数字就好了吗,好,这时候问题又来了,如果他的数据包的长度是两位数,和三位数,你让接收端,是收两个字节,还是三个字节?如果你只有两个字节,而接收端接受了三个,那你的数据包不就缺失了一个字节了吗?所以我们要规范这个传的数字的长度,这里就要引出一个新的模块

struct模块

import struct
#把一个数字打包成固定长度的4字节
obj=struct.pack('i',1098)
print(obj)
print(len(obj))

l=struct.unpack('i',obj)[0]
print(l)

他的作用就是把你输入进来的数字给转换成四位的字节,这样,无论你输入多大的数字,都可以转换成四位字节,这样接收端就固定收四个字节就好了。

client端

import socket
import struct
soc=socket.socket()

soc.connect(('127.0.0.1',8001))
while True:
    in_s=input('请输入要执行的命令:')
    soc.send(in_s.encode('utf-8'))
    head=soc.recv(4)
    l=struct.unpack('i',head)[0]
    # data=soc.recv(l)
    count=0
    data_total=b''

    while count<l:
        if l<1024: #如果接受的数据小于1024 ,直接接受数据大小
            data=soc.recv(l)
        else:#如果接受的数据大于1024
            if l-count>=1024: #总数据长度-count(目前收到多少,count就是多少) 如果还大于1024  ,在收1024
                data=soc.recv(1024)
            else: #总数据长度-count(目前收到多少,count就是多少) 如果小于1024,只收剩下的部分就可
                data=soc.recv(l-count)

        data_total+=data
        count+=len(data)

    print(str(data_total,encoding='gbk'))

好了,粘包问题就解决了

转眼,有一个新的问题来了。

我们需要用struct来打包这个,它里面的第一个参数是‘i'

,他代表的意思是int,所以如果当我们那个表示数据内容长度的数字,位数过多导致int容不下,该怎么办?

这个时候我们先上代码

server端

import socket
import subprocess
import struct
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8001))
soc.listen(3)
while True:
    print('等待客户端连接')
    conn,addr=soc.accept()
    print('有个客户端连接上了',addr)
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:
                break
            print(data)
            obj = subprocess.Popen(str(data,encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            #执行正确的结果 b 格式,gbk编码(windows平台)
            msg=obj.stdout.read()

            #发送的时候需要先把长度计算出来
            #头必须是固定长度
            #先发4位,头的长度
            import json
            dic={'size':len(msg)}
            dic_bytes=(json.dumps(dic)).encode('utf-8')
            #head_count是4个字节的长度
            head_count=struct.pack('i',len(dic_bytes))
            print(dic)
            conn.send(head_count)
            #发送头部内容
            conn.send(dic_bytes)
            #发了内容
            conn.send(msg)
        except Exception:

            break
    # 关闭通道
    conn.close()


# 关闭套接字
soc.close()

首先我们已经获得了这个数据了,我们来构建一个他的数据头,新建了一个字典,然后把这个数据的长度放进了这个字典,然后用json模块,把这个字典转换成了字符类型。这个时候把这个字典(字符类型)的长度打包成head_count(四个字节长度),这个时候在把这个head_count发送给接收端,然后把被转换成字节类型的字典也传过去,最后再把数据传过去。

client端

import socket
import struct
import json
soc=socket.socket()

soc.connect(('127.0.0.1',8001))
while True:
    in_s=input('请输入要执行的命令:')
    soc.send(in_s.encode('utf-8'))
    #头部字典的长度
    head_dic_len=soc.recv(4)
    #解出真正的长度
    head_l=struct.unpack('i',head_dic_len)[0]
    #byte 字典的长度
    #收真正的头部字典
    dic_byte=soc.recv(head_l)
    head=json.loads(dic_byte)
    print(head)
    l=head['size']
    count=0
    data_total=b''
    print(l)
    while count<l:
        if l<1024: #如果接受的数据小于1024 ,直接接受数据大小
            data=soc.recv(l)
        else:#如果接受的数据大于1024
            if l-count>=1024: #总数据长度-count(目前收到多少,count就是多少) 如果还大于1024  ,在收1024
                data=soc.recv(1024)
            else: #总数据长度-count(目前收到多少,count就是多少) 如果小于1024,只收剩下的部分就可
                data=soc.recv(l-count)

        data_total+=data
        count+=len(data)

    print(str(data_total,encoding='gbk'))

之前在服务端我们发送了三个数据,第一个是被转换成字节类型的字典的长度,第二个是被转成字节类型的字典,第三个就是我们最重要的数据本身了。

head_dic_len接受了第一个数据,把它解包,得到了转换成字节类型的字典的长度,这里注意一下,因为每次解包之后的到的内容都是一个元组,所以我们要在后面加上[ ],然后取第一个就行了。这个时候我们知道了转换成字节类型的字典的长度了,再一次接收,这次dic_byte=soc.recv(转换成字节类型的字典的长度),这样就能拿到这个转换成字节类型的字典而不会多拿到后面传来的真正的数据了,接下来把这个转换成字节类型的字典用json的loads再转换为字典类型,取出里面的长度就好了,接下来的操作都和之前一样了,这里有点绕,需要仔细看清楚。

这下粘包问题,就真的彻底解决了

结束,撒花

posted @ 2019-09-06 22:18  chanyuli  阅读(152)  评论(0编辑  收藏  举报