网络编程(三)——通信循环、链接循环、粘包问题

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

一、通信循环

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

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到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

 补充:struct模块

 可以把一个类型,如数字,转成固定长度的bytes字节类型数据

import struct

# 打包,将数据转成固定字节长度的数据,使在接收端可以知道报头长度,接收报头
res = struct.pack('i', 12344566)
print(res, len(res))              # b'xf6\xbcx00'   4

res1 = struct.pack('i', 888888)
print(res1, len(res1))            # b'8x90
x00'    4


# 解包,将数据从固定字节的数据中解出来,获取原数据(元组格式,元组的第一个值)
res2=struct.unpack('i',res)
print(res2)             # (12344566,)

(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)终极版本

由于简单版本中,struct模块转换的原数据的大小有限制,报头只含有数据长度,所以用字典来表示报头。

json(json格式的字符串):数据以什么格式发送,接收到的还是原来的格式的数据

struct:把json格式的数据转换成固定长度的字符串(bytes)数据,使报头和真正数据不粘在一起,在接收端可以接收报头

# 服务端必须满足至少三点:
# 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()
客户端
原文地址:https://www.cnblogs.com/linagcheng/p/9579685.html