Socket编程规范及底层原理(二)---粘包现象及解决方法

时间:2019-02-17
本文章向大家介绍Socket编程规范及底层原理(二)---粘包现象及解决方法,主要包括Socket编程规范及底层原理(二)---粘包现象及解决方法使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一.什么是粘包

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。-----官方解释

粘包说白了就是上次从缓冲区拿的数据拿少了,下次和其它数据一起接收过来,比如缓冲区有『abcdefg』,设rec()一次接受5个字符,那么用户第一次只能接收到「abcde」,「fg」接收不到,那么下次「fg」和其它数据一起被用户接收。

二.为什么出现粘包现象

  (1)发送方原因

  我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。

  所以,正是Nagle算法造成了发送方有可能造成粘包现象。

  (2)接收方原因

  TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。

说白了就是因为接收方不知道消息之间的界限,不知道一次提取多少字节的数据所造成的。

须知:只有TCP有粘包现象,UDP无需担心。

两种情况下会发生粘包:

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

'''接收方单次接收能力远大于发送方发的数据'''
#服务端
import  socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

phone.bind(('127.0.0.2',8000))
phone.listen(5)
#
conn,addr = phone.accept()

msg=conn.recv(1024)  # 收消息------#自动合并,输出'helloworld!!!!!!!!!'  ------粘包现象

print("one---客户端发来",msg)
msg=conn.recv(1024)  # 收消息------#输出空

print("two---客户端发来",msg)
msg=conn.recv(1024)  # 收消息------#输出空

print("three---客户端发来",msg)

conn.close()

phone.close() 




#客户端
'''远程操作'''
#粘包现象及解决方法  ----- TCP采用了优化算法,会自动把小数据合并
from socket import  *
ip_port = ('127.0.0.2',8000)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

tcp_client.send("hello".encode("utf-8"))
tcp_client.send("world".encode("utf-8"))  
tcp_client.send("!!!!!!!!!".encode("utf-8")) 

tcp_client.close()

2.接受端缓冲区远小于发送端发送数据,造成粘包(发送数据大于接收能力,产生粘包)

'''接收方单次接收能力远小于发送方发的数据'''
#服务端
import  socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

phone.bind(('127.0.0.2',8000))
phone.listen(5)
#
conn,addr = phone.accept()

msg=conn.recv(5)  # 收消息------#输出'hello'  ------粘包现象

print("one---客户端发来",msg)
msg=conn.recv(40)  # 收消息------#输出'world!!!!!!!!!world!!!!!!!!!'

print("two---客户端发来",msg)



conn.close()

phone.close() 




#客户端
'''远程操作'''

from socket import  *
ip_port = ('127.0.0.2',8000)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

tcp_client.send("hello".encode("utf-8"))
tcp_client.send("world".encode("utf-8"))  
tcp_client.send("!!!!!!!!!".encode("utf-8")) 

tcp_client.close()

UDP不会发生粘包,因为它有消息保护边界

#服务端
from socket import  *

#UDP服务端没有链接,故没有listen,accept
ip_port = ('127.0.0.1',8000)
buffer_size = 1024
ss=socket(AF_INET,SOCK_DGRAM) #数据报套接字

ss.bind(ip_port)

data,addr=ss.recvfrom(5)
print(data)
data,addr=ss.recvfrom(5)
print(data)
data,addr=ss.recvfrom(2)
print(data)
data,addr=ss.recvfrom(5)
print(data)
ss.close()

#客户端
from socket import  *
#UDP服务端没有链接,故没有listen,accept
ip_port = ('127.0.0.1',8000)
buffer_size = 1024
ss=socket(AF_INET,SOCK_DGRAM)

ss.sendto("hello".encode("utf-8"),ip_port)
ss.sendto("world".encode("utf-8"),ip_port)
ss.sendto("!!!!!".encode("utf-8"),ip_port)
ss.close()

三.粘包解决办法

两种途径:

    1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符;

    2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

low版本


import  subprocess
from  socket import  *
ip_port = ('127.0.0.1',8000)
back_log = 5
buffer_size = 100

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
    conn,addr = tcp_server.accept()
    print("linked-----:",addr)
    while True:
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:  #linux系统下客户端断开时服务端会一直接受空信息,win下会报错
                break
            print("命令:",cmd)
            res=subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             stdin=subprocess.PIPE)  #打开shell窗口并获取相关信息,PIPE是管道,不加stdout参数的话直接输出到屏幕
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()
            if not cmd_res:
                cmd_res="success".encode("gbk")
            #low版解决粘包版本

            #先通知接受端数据长度,接受端告诉发送端准备好了,发送端再发送,接受端接受数据。
            conn.send(str(len(cmd_res)).encode('utf-8')) # 告知长度

            client_ready = conn.recv(buffer_size) #被告知准备好了
            if client_ready.decode("utf-8") == 'ready':

                conn.send(cmd_res) #发送数据
        except ConnectionResetError:
            break
    conn.close()
    print("close---",conn)
tcp_server.close()
'''远程操作'''
#粘包现象及解决方法
from socket import  *
ip_port = ('127.0.0.1',8000)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
    cmd=input(">>")
    if not cmd:
        continue
    if cmd == 'quit':
        break
    tcp_client.send(cmd.encode("utf-8"))

    length = tcp_client.recv(buffer_size)#获得要接受数据的长度
    length = int(length.decode("utf-8"))

    tcp_client.send(b"ready") #告知发送端准备可以发送了,之所以要加这一步,因为发送间隔时间太短会自动合并
#分割接受数据,解决一次性接受数据量太大问题
    recv_size=0
    recv_msg=b''
    while recv_size < length:

        recv_msg += tcp_client.recv(length)
        recv_size+=len(recv_msg)

    print("执行结果:",recv_msg.decode("utf-8"))
tcp_client.close()

高级版本

import struct
import  subprocess
from  socket import  *
ip_port = ('127.0.0.2',8000)
back_log = 5
buffer_size = 100

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
    conn,addr = tcp_server.accept()
    print("linked-----:",addr)
    while True:
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:  #linux系统下客户端断开时服务端会一直接受空信息,win下会报错
                break
            print("命令:",cmd)
            res=subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             stdin=subprocess.PIPE)  #打开shell窗口并获取相关信息,PIPE是管道,不加stdout参数的话直接输出到屏幕
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()
            if not cmd_res:
                cmd_res="success".encode("gbk")


            #将长度和数据合并一起发过去
            data_length = struct.pack("i",len(cmd_res))#将数据长度转为转为4个字节
            conn.send(data_length)
            conn.send(cmd_res)

        except ConnectionResetError:
            break
    conn.close()
    print("close---",conn)
tcp_server.close()
'''远程操作'''
#粘包现象及解决方法
from socket import  *
import  struct
from functools import  partial
ip_port = ('127.0.0.2',8000)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
    cmd=input(">>")
    if not cmd:
        continue
    if cmd == 'quit':
        break
    tcp_client.send(cmd.encode("utf-8"))

    length = tcp_client.recv(4)#获得要接受数据的长度
    length = struct.unpack('i',length)[0]

#分割接受数据牛逼方法,----有bug

    #recv_msg=''.join(iter(partial(tcp_client.recv,1024),b''))
    
    #正普通分割数据
    recv_size=0
    recv_msg=b''
    while recv_size < length:

        recv_msg += tcp_client.recv(length)
        recv_size+=len(recv_msg)
    print("执行结果:",recv_msg.decode("utf-8"))
tcp_client.close()

****上节说到

           TCP协议rec在自己这端的缓冲区为空时,阻塞;

            UDP协议recvfrom在自己这端的缓冲区为空时,就收到一个空。

原理:TCP是面向流,没有消息保护边界,空消息因此无法接收,而UDP是面向数据流每个UDP包中就有消息头(消息来源地址,端口等信息),是有消息保护边界的,所以即使是空消息,udp也会帮你封装上消息头

 

能力有限,粘包暂时讲到这里,哪有错误希望大家批评指正!