Python之套接字、黏包

发布时间 2023-07-05 09:06:05作者: Way*yy

了解socket

socket:
	Socket是应用层与TCP/IP协议族通信的中间抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

套接字的分类:
	AF_UNIX:用在局域网内
    AF_INET:用在互联网中

        
客户端和服务端是如何启动的?
	我们应该先启动服务端,服务端启动以后,等待客户端进行连接,然后接收客户端的信息,进行通信

套接字的工作流程

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束


服务端套接字函数
    s.bind()    绑定(主机,端口号)到套接字
    s.listen()  开始TCP监听
    s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
    s.connect()     主动初始化TCP服务器连接
    s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
    s.recv()            接收TCP数据
    s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    s.recvfrom()        接收UDP数据
    s.sendto()          发送UDP数据
    s.getpeername()     连接到当前套接字的远端的地址
    s.getsockname()     当前套接字的地址
    s.getsockopt()      返回指定套接字的参数
    s.setsockopt()      设置指定套接字的参数
    s.close()           关闭套接字

面向锁的套接字方法
    s.setblocking()     设置套接字的阻塞与非阻塞模式
    s.settimeout()      设置阻塞套接字操作的超时时间
    s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
    s.fileno()          套接字的文件描述符
    s.makefile()        创建一个与该套接字相关的文件

基于TCP的套接字

#############################服务端##############################

import socket

"""
    SOCK_STREAM:使用的是TCP协议
    SOCK_DGRAM:使用的是UDP协议
"""

# 通过实例化得到了一个socket的对象sever,相当于买了一部手机
sever = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 服务端绑定信息,相当于买了一张电话卡
sever.bind(('127.0.0.1', 8080))

# 监听消息 ,相当于手机处于待机状态
sever.listen(4)  # 4代表的是半连接池:可以等待的客户端的数量

# 接收消息 , 相当于接电话
sock, addr = sever.accept()  # 代码走到这里会停住,等待接收客户端发来的消息

"""
sock:代表的是当前客户端的连接对象
addr:代表的是客户端的信息(ip+port)
"""

# 听消息
date = sock.recv(1024)   # 获取到客户端发来的消息
print(f'这是客户端发来的消息{date}')  # 这是客户端发来的消息b'hello'

# 发消息
# 数据类型必须是字节类型
sock.send(b'BayBay')  # 服务端给客户端回话

sever.close()  # 挂电话

sock.close()  # 手机关机

#############################客户端##############################
import socket

#  相当于买了一个手机
client = socket.socket()  # 实例化出来一个对象,

#  给服务端打电话
client.connect(('127.0.0.1', 8080))  # 跟服务端进行创建连接

#  给服务端说消息
client.send(b'hello')  # 发送的必须是字节类型

#  接收服务端返回过来的消息
date = client.recv(1024)  # 一次性接受的最大量,也是字节类型
print(f'这是服务端发来的消息>>>>{date}')  # 这是服务端发来的消息>>>>b'BayBay'

# 挂掉电话
client.close()  # 断掉与服务器的链接

加上链接循环与通信循环

#############################服务端##############################
import socket

sever = socket.socket()

sever.bind(("127.0.0.1", 8000))

sever.listen(3)

while True:
    sock, addr = sever.accept()
    while True:
        date = sock.recv(1024)
        print(date)

        sock.send(b'bay bay')
    sock.close()
sever.close()

#############################客户端##############################
import socket

client = socket.socket()

client.connect(("127.0.0.1", 8000))
while True:
    msg = input('请输入你的信息>>>').strip()
    if len(msg) == 0:
        continue
    client.send(msg.encode('utf8'))

    date = client.recv(1024)
    print(date)


client.close()

基于UDP协议的套接字编程

#############################服务端##############################
import socket

sever = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

sever.bind(("127.0.0.1", 8000))

data, server_addr = sever.recvfrom(1024)
print(data, server_addr)

sever.sendto(data, server_addr)

sever.close()
#############################客户端##############################
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》UDP

client.sendto(b'hello', ("127.0.0.1", 8000))
data, server_addr = client.recvfrom(1024)
print(data)

client.close()

加上链接循环与通信循环

#############################服务端##############################
import socket

sever = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)   # 数据报协议-》UDP

sever.bind(("127.0.0.1", 8000))

while True:
    # client_addr:客户端的地址:ip+port
    
    data, client_addr = sever.recvfrom(1024)  # 接收客户端发来的消息,1024是字节数
    print(f'接收到来自客户端的{data},{server_addr}'吗)

    sever.sendto(data, server_addr)  # 给客户端发消息

sever.close()

#############################客户端##############################
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》UDP

while True:
    msg = input('请输入你发送的信息>>>>>').strip()
    if len(msg) == 0:
        continue
    client.sendto(msg.encode('utf8'), ("127.0.0.1", 8000))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()

黏包

#############################服务端##############################
import socket
import subprocess

sever = socket.socket()

sever.bind(("127.0.0.1", 8080))

sever.listen(3)

sock, addr = sever.accept()

while True:
    try:
        date = sock.recv(1024)
        obj = subprocess.Popen(date.decode('utf8'),
                               shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)

        user_stdout = obj.stdout.read()
        user_stderr = obj.stderr.read()
        print(len(user_stdout) + len(user_stderr))
        sock.send(user_stdout)
        sock.send(user_stderr)
    except Exception:
        break

    sock.close()

#############################客户端##############################
import socket

client = socket.socket()

client.connect(("127.0.0.1", 8080))

while True:
    cmd = input('请输入指令>>>>>').strip()
    client.send(cmd.encode('gbk'))

    date = client.recv(1024)
    print(date.decode('gbk'))
    
    
"""上述程序是基于tcp的socket,在运行时会发生粘包"""

什么是黏包

须知:只有TCP有粘包现象,UDP永远不会粘包

因为TCP在传输数据之前需要先建立一个双向通道,然而在传输数据的时候又会设定一个最大传输的字节,一旦传输的数据大于这个字节(假如我们把最大字节设置为1024)以后,多余出来的数据就会滞留在通道内,下次再调用又会吐出来1024个字节的数据,就这样一直循环下去,直到所有的数据都传输完毕
"这样的一个问题就是我们传输的数据会乱掉"

UDP协议的话是不用建立双向通道的,所以也就不会存在数据滞留的问题,然后也就不会出现黏包的现象

解决黏包现象:struct模块

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

如果说一个数据的大小是2000000,那么我们就可以通过使用struct模块将他变为一个固定大小的字节,详情请参照下图

import struct

res = struct.pack('i', 1000000)

print(len(res))  # 4

res1 = struct.unpack('i', res)
print(res1)  # (1000000,)
print(res1[0])  # 1000000
#  但是被打包的数据是有限制的
"""
    struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
"""


如果超过这个范围又该如何解决呢?

#############################服务端##############################
import socket
import json
import struct

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)

while True:
    sock, address = server.accept()
    while True:
        # 1.先接收固定长度为4的字典报头数据
        recv_first = sock.recv(4)
        # 2.解析字典报头
        dict_length = struct.unpack('i', recv_first)[0]
        # 3.接收字典数据
        real_data = sock.recv(dict_length)
        # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化)
        real_dict = json.loads(real_data)
        # 5.获取字典中的各项数据
        data_length = real_dict.get('size')
        file_name = real_dict.get("file_name")

        recv_size = 0
        with open(file_name,'wb') as f:
            while recv_size < data_length:
                data = sock.recv(1024)
                recv_size += len(data)
                f.write(data)




        # data = sock.recv(1024)  # 接收cmd命令
        # command_cmd = data.decode('utf8')
        # sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        # res = sub.stdout.read() + sub.stderr.read()  # 结果可能很大
        # # 1.定义一个字典数据
        # data_dict = {
        #     'desc': '这是非常重要的数据',
        #     'size': len(res),
        #     'info': '下午挺困的 ...提神醒脑'
        # }
        # data_json = json.dumps(data_dict)
        # # 2.制作字典报头
        # data_first = struct.pack('i', len(data_json))
        # # 3.发送字典报头
        # sock.send(data_first)
        # # 4.发送字典
        # sock.send(data_json.encode('utf8'))
        # # 5.发送真实数据
        # sock.send(res)

#############################客户端##############################
import json
import socket
import struct
import os

client = socket.socket()  # 买手机
client.connect(('127.0.0.1', 8080))  # 拨号

while True:
    data_path = os.path.dirname(os.path.abspath(__file__))
    # print(os.listdir(data_path))  # [文件名称1 文件名称2 ]
    movie_name_list = os.listdir(data_path)
    for i, j in enumerate(movie_name_list, 1):
        print(i, j)
    choice = input('请选择您想要上传的电影编号>>>:').strip()
    if choice.isdigit():
        choice = int(choice)
        if choice in range(1, len(movie_name_list) + 1):
            # 获取文件名称
            movie_name = movie_name_list[choice - 1]
            # 拼接文件绝对路径
            movie_path = os.path.join(data_path, movie_name)
            # 1.定义一个字典数据
            data_dict = {
                'file_name': 'XXX老师合集.mp4',
                'desc': '这是非常重要的数据',
                'size': os.path.getsize(movie_path),
                'info': '下午挺困的,可以提神醒脑'
            }
            data_json = json.dumps(data_dict)
            # 2.制作字典报头
            data_first = struct.pack('i', len(data_json))
            # 3.发送字典报头
            client.send(data_first)
            # 4.发送字典
            client.send(data_json.encode('utf8'))
            # 5.发送真实数据
            with open(movie_path,'rb') as f:
                for line in f:
                    client.send(line)


    # msg = input('请输入cmd命令>>>:').strip()
    # if len(msg) == 0:
    #     continue
    # client.send(msg.encode('utf8'))
    # # 1.先接收固定长度为4的字典报头数据
    # recv_first = client.recv(4)
    # # 2.解析字典报头
    # dict_length = struct.unpack('i',recv_first)[0]
    # # 3.接收字典数据
    # real_data = client.recv(dict_length)
    # # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化)
    # real_dict = json.loads(real_data)
    # print(real_dict)
    # # 5.获取字典中的各项数据
    # data_length = real_dict.get('size')
    # data_bytes = client.recv(data_length)
    # print(data_bytes.decode('gbk'))