网络编程——通信循环链接循环粘包问题

Posted linagcheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程——通信循环链接循环粘包问题相关的知识,希望对你有一定的参考价值。

通信循环、链接循环、粘包问题

一、通信循环

服务端和客户端可以进行连续的信息交流

技术分享图片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)

conn, addr = ser_socket.accept()

while True:
    try:               # 抛出异常,若不抛出处理,一旦客户端强行退出,服务端就会报错
        data = conn.recv(1024)
        print(data.decode(utf-8))

        conn.send(data.upper())
    except ConnectionResetError:
        break

conn.close()

ser_socket.close()
通信循环服务端
技术分享图片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

#通信循环,可以多次输入
while True:
    msg = input(>>>>:).strip()
    if len(msg) == 0:            # 如果输入为空,给服务端发送信息之后,服务端什么都没接受,一直处于阻塞状态
        continue
    cli_socket.send(msg.encode(utf-8))

    data = cli_socket.recv(1024)
    print(data.decode(utf-8))

cli_socket.close()
通信循环客户端

 

tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制

 

二、链接循环

可以启动多个客户端,但是只有一个客户端是处于连接状态,其余部分在半连接池等待连接,等待的数量不能超过半连接池的最大监听数量

技术分享图片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)


#链接循环,可以同时启动最多6个客户端,但是只有一个处于连接状态,其余最多5个在半连接池等待。只有当连接状态的客户端断开连接,下一个客户端才进入连接
while True:
    conn, addr = ser_socket.accept()

    # 通信循环
    while True:
        try:
            data = conn.recv(1024)
            print(data.decode(utf-8))

            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

ser_socket.close()
链接循环服务端
技术分享图片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

while True:
    msg = input(>>>>:).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))

    data = cli_socket.recv(1024)
    print(data.decode(utf-8))

cli_socket.close()
链接循环客户端

三、粘包问题

1、模拟ssh远程执行命令

技术分享图片
from socket import socket, AF_INET, SOCK_STREAM
import subprocess

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8882))

ser_socket.listen(5)
while True:
    conn, addr = ser_socket.accept()
    while True:
        try:
            data = conn.recv(1024)
            obj = subprocess.Popen(data.decode(utf-8), 
                                   shell=True, 
                                   stdout=subprocess.PIPE, 
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            conn.send(stdout + stderr)
        except ConnectionResetError:
            break
    conn.close()

ser_socket.close()
服务端
技术分享图片
from socket import socket, AF_INET, SOCK_STREAM

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8882))

while True:
    msg = input(>>>).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))
    data = cli_socket.recv(1024)
    print(data.decode(gbk))      #  Windows系统,默认编码gbk,所以用gbk解码

cli_socket.close()
客户端

2、产生粘包原因

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

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

在上面的例子中,如果执行命令tasklist,那么就会存在粘包问题。由于TCP协议是流式协议,所以数据都以流的形式传输。假如数据大小是123456,可是已经设定了接收的大小 是1024,所以只接受了数据中的一小部分,但是,剩余部分数据并不会消失,会一直存在于操作系统中,所以下一次接收数据的时候是优先从剩余数据中接收。这样所有数据就乱套了,这就是粘包问题。

3、发生粘包的两种情况

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

技术分享图片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)

conn, addr = ser_socket.accept()

data = conn.recv(1024)
print(第一次接收:, data.decode(utf-8))
data1 = conn.recv(5)
print(第二次接收:, data1.decode(utf-8))
data2 = conn.recv(1024)
print(第三次接收:, data2.decode(utf-8))

conn.send(data.upper())

conn.close()

ser_socket.close()
服务端
技术分享图片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

cli_socket.send(hello.encode(utf-8))
cli_socket.send(world.encode(utf-8))
cli_socket.send(object.encode(utf-8))

# data = cli_socket.recv(1024)
# print(data.decode(‘utf-8‘))

cli_socket.close()
客户端

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

  例如:模拟ssh远程执行命令,若执行tasklist命令,在客户端,无法几次性全部接受执行结果,所以剩余结果会在下一次执行命令式优先接收

技术分享图片
from socket import socket, AF_INET, SOCK_STREAM
import subprocess

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8882))

ser_socket.listen(5)
while True:
    conn, addr = ser_socket.accept()
    while True:
        try:
            data = conn.recv(1024)
            obj = subprocess.Popen(data.decode(utf-8),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            conn.send(stdout + stderr)
        except ConnectionResetError:
            break
    conn.close()

ser_socket.close()
服务端
技术分享图片
from socket import socket, AF_INET, SOCK_STREAM

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8882))

while True:
    msg = input(>>>).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))
    data = cli_socket.recv(1024)
    print(data.decode(gbk))

cli_socket.close()
客户端

4、解决粘包问题的方法

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

(1)简单版本

技术分享图片
# 服务端必须满足至少三点:
# 1. 绑定一个固定的ip和port
# 2. 一直对外提供服务,稳定运行
# 3. 能够支持并发
from socket import *
import subprocess
import struct

server = socket(AF_INET, SOCK_STREAM)
server.bind((127.0.0.1, 8081))
server.listen(5)

# 链接循环
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024) #cmd=b‘dir‘
            # if len(cmd) == 0: break  # 针对linux系统
            obj=subprocess.Popen(cmd.decode(utf-8),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            # 1. 先制作固定长度的报头
            header=struct.pack(i,len(stdout) + len(stderr))
            # 2. 再发送报头
            conn.send(header)
            # 3. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break

    conn.close()

server.close()
服务端
技术分享图片
from socket import *
import struct

client = socket(AF_INET, SOCK_STREAM)
client.connect((127.0.0.1, 8081))

# 通信循环
while True:
    cmd=input(>>: ).strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode(utf-8))
    #1. 先收报头,从报头里解出数据的长度
    header=client.recv(4)
    total_size=struct.unpack(i,header)[0]
    #2. 接收真正的数据
    cmd_res=b‘‘
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data

    print(cmd_res.decode(gbk))

client.close()
客户端

(2)终极版本

技术分享图片
# 服务端必须满足至少三点:
# 1. 绑定一个固定的ip和port
# 2. 一直对外提供服务,稳定运行
# 3. 能够支持并发
from socket import *
import subprocess
import struct
import json

server = socket(AF_INET, SOCK_STREAM)
server.bind((127.0.0.1, 8081))
server.listen(5)

# 链接循环
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # cmd=b‘dir‘
            if len(cmd) == 0: break  # 针对linux系统
            obj = subprocess.Popen(cmd.decode(utf-8),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作报头
            header_dic = {
                filename: a.txt,
                md5: asdfasdf123123x1,
                total_size: len(stdout) + len(stderr)
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode(utf-8)

            # 2. 先发送4个bytes(包含报头的长度)
            conn.send(struct.pack(i, len(header_bytes)))
            # 3  再发送报头
            conn.send(header_bytes)

            # 4. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break

    conn.close()

server.close()
服务端
技术分享图片
from socket import *
import struct
import json

client = socket(AF_INET, SOCK_STREAM)
client.connect((127.0.0.1, 8081))

# 通信循环
while True:
    cmd=input(>>: ).strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode(utf-8))
    #1. 先收4bytes,解出报头的长度
    header_size=struct.unpack(i,client.recv(4))[0]

    #2. 再接收报头,拿到header_dic
    header_bytes=client.recv(header_size)
    header_json=header_bytes.decode(utf-8)
    header_dic=json.loads(header_json)
    print(header_dic)
    total_size=header_dic[total_size]

    #3. 接收真正的数据
    cmd_res=b‘‘
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data

    print(cmd_res.decode(gbk))

client.close()
客户端

 

以上是关于网络编程——通信循环链接循环粘包问题的主要内容,如果未能解决你的问题,请参考以下文章

Python网络编程03/ low版解决粘包问题

网络编程之套接字socket

Python网络编程04/recv原理/高大上版解决粘包方式

第三模块:面向对象&网络编程基础 第2章 网络编程

网络编程2

粘包待改