Python开发-034_网络编程

Python开发人员可以使用各种第三方工具来创建网络客户端和服务端,但这些第三方工具的核心其实都是socket模块,这个模块提供了所有必须的接口,我们可以利用它来快速开发对应的TCP/UDP客户端、服务端

1 网络编程入门

网络编程涉及到计算机之间的通信,我们以本地回环地址为案例,整个程序分为服务端service.py 客户端client.py,在运行时,服务端会监听127.0.0.1:8001地址,等待客户端的连接

service.py

import socket

# 1.监听本地的IP地址和端口
    # socket.AF_INET 指的是 使用IPV4地址或主机名
    # socket.SOCK_STREAM 指的是 TCP客户端
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001)) # 监听IP和端口
sock.listen(5) # 支持5人排队

while True: # 服务端要服务多个客户端,故需要循环处理
    # 2.等待客户端连接,此处阻塞程序运行等待连接
    conn,addr = sock.accept() # conn是对象 addr是ip地址
    # 3.等待客户端发送信息,此处阻塞程序运行等待客户端发送
    client_data = conn.recv(1024) # 等待接受客户端发来的数据,单次接收最大1024个字节
    print(client_data.decode("utf-8")) # 接收到的都是字节数据,需要使用decode解码为字符串
    # 4.给客户端回信息
    conn.send("Hello World".encode('utf-8'))  # 发送的时候也需要使用encode编码为字节类型
    # 5.关闭连接
    conn.close()

# 6.停止监听端口
sock.close()

client.py

import socket

# 1.向指定IP发送连接请求
client = socket.socket()
client.connect(('127.0.0.1',8001)) # 向服务端发送连接请求
# 2.连接成功之后,发送消息
client.send("Hello,I am client".encode('utf-8')) # 发送字符串需要编码
# 3.阻塞程序进行,等待消息回复
reply = client.recv(1024) # 单次接受1024字节
print(reply.decode('utf-8')) # 解码为字符串
# 4.关闭连接 -> 关闭连接后,会发送一个空字符到服务端
client.close()

案例1:马冬梅大爷模拟器

service.py

import socket,random

def Inquire(item):
    item_len = len(item) # 测量名字长度
    item_random = random.randint(0,item_len-1)
    name = item[item_random] # 提取被更改数
    item = item.replace(name,"什么") # 随即替换名字为什么
    return item

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001))
sock.listen(5)

while True:
    conn,addr = sock.accept()
    print("用户已连接")
    conn.sendall("你找谁呀!".encode('utf-8'))
    while True:
        # 循环马冬梅
        client_data = conn.recv(1024) # 接收信息
        if not client_data: # 如果是空,则说明对方close
            break
        answer = Inquire(client_data.decode('utf-8')) # 将马...梅编码传输过去
        conn.sendall(answer.encode('utf-8'))
    # 关闭与此人的连接
    print("对方断开连接")
    conn.close()
# 停止监听服务
sock.close()

client.py

import socket

# 1. 向指定IP发送连接请求
client = socket.socket()
client.connect(('127.0.0.1',8001)) # 向服务端发送连接请求

# 2.连接成功后,获取系统登录信息
message = client.recv(1024)
print(message.decode("utf-8"))

while True:
    content = input("请输入(q/Q退出):")
    if content.upper() == 'Q':
        break
    client.sendall(content.encode("utf-8"))

    # 3. 等待,消息的回复
    reply = client.recv(1024)
    print(reply.decode("utf-8"))

# 关闭连接,关闭连接时会向服务端发送空数据。
client.close()

image-20220706230256767

案例2:文件上传

server.py

import os
import socket

def file_path(filename):
    abs = os.path.abspath(__file__)
    path = os.path.dirname(abs)
    file_path = os.path.join(path,'file',filename)
    return file_path

# 网络连接
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001))
sock.listen(1)

while True:
    # 等待用户连接
    conn,addr = sock.accept()
    # 获取传输文件名和大小 : 需要文件大小判断是否传输完毕
    data = conn.recv(1024)
    data = data.decode('utf-8')
    file_name,file_size = data.split("-")
    file_size = int(file_size)
    print("用户已连接,传输文件名{}-{}kb".format(file_name,file_size/1024))
    file_object = open(file_path(file_name),mode='wb')
    recv_size = 0 # 初始化已接受到的文件大小
    while True:
        data = conn.recv(1024) # 单次接受1024个字符
        file_object.write(data)
        file_object.flush() # 写入硬盘
        recv_size += len(data)
        if recv_size == file_size:
            print("\r{}传输完成".format(file_name))
            break
        else:
            print("\r{}已传输{}%".format(file_name,recv_size/file_size*100,end=''))
    file_object.close()
    conn.close()
sock.close()

client.py

import os
import socket

client = socket.socket()
client.connect(('127.0.0.1',8001))

file_path = input("请输入需要上传的文件:")

# 首先发送文件大小
file_size = os.stat(file_path).st_size # 获取文件大小
file_name = os.path.basename(file_path)
recv_name = "{}-{}".format(file_name,file_size).encode('utf-8')
client.sendall(recv_name)

# 开始上传文件
print("开始上传文件")
file_object = open(file_path,mode='rb')
read_size = 0
while True:
    chunk = file_object.read(1024) # 单次读取1024
    client.sendall(chunk)
    read_size += len(chunk)
    if read_size == file_size:
        break
file_object.close()
client.close()

案例3:远程登陆系统

客户端:
	1. 运行程序,连接服务端并获取服务端发送的欢迎使用xx系统信息。
    2. 输入用户名和密码,并将用户名和密码发送到服务端去校验。
    3. 登录成功,进入系统,提示登录成功
服务端:
    1. 等待客户端发送用户名和密码进行校验(用户名和密码在文件中)
    2. 登录失败,返回错误信息。
    3. 登录成功,返回成功提示的内容。

service.py

import socket,hashlib,json

def md5(origin,salt="sdasdfagfawds"):
    hash_object = hashlib.md5(salt.encode('utf-8'))
    hash_object.update(origin.encode('utf-8'))
    result = hash_object.hexdigest()
    return result

def user_login(uname,pwd):
    with open("user",mode='rt',encoding='utf-8') as user_object:
        for user in user_object:
            username,passwd = user.split("|")
            if username == uname and passwd == pwd:
                return "登陆成功"
        else:
            return "账号或密码错误"

def run():
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.bind(('127.0.0.1',8001))
    sock.listen(1)
    while True:
        conn,addr = sock.accept()
        conn.sendall("欢迎使用XX系统".encode('utf-8'))
        info_json = conn.recv(1024)
        info = json.loads(info_json.decode('utf-8')) # 账号和密码采用json格式
        username = info['username']
        password = info['password']
        login_status = user_login(username,password)
        conn.sendall(login_status.encode('utf-8'))
        conn.close()
    sock.close()

if __name__ == '__main__':
    run()

client.py

import socket,hashlib,json

def md5(origin,salt="sdasdfagfawds"):
    hash_object = hashlib.md5(salt.encode('utf-8'))
    hash_object.update(origin.encode('utf-8'))
    result = hash_object.hexdigest()
    return result

def run():
    client = socket.socket()
    client.connect(('127.0.0.1',8001))
    print(client.recv(1024).decode('utf-8'))
    username = input("请输入账号名:").strip()
    password = input("请输入密码:").strip()
    login_data = {'username':username,'password':md5(password)}
    client.sendall(json.dumps(login_data).encode('utf-8')) # 采用Json格式发送
    login_status = client.recv(1024).decode('utf-8')
    print(login_status)

if __name__ == "__main__":
    run()

2 UDP和TCP编程

在经历传输层的时候,网络交互除了定义端口信息,还需要指定UDP协议和TCP协议,协议不同连接和传输数据的细节也会不同

  • UDP(User Data Protocol)用户数据报协议, 是?个?连接的简单的?向数据报的传输层协议。 UDP不提供可靠性, 它只是把应?程序传给IP层的数据报发送出去, 但是并不能保证它们能到达?的地。 由于UDP在传输数据报前不?在客户和服务器之间建??个连接, 且没有超时重发等机制, 故?传输速度很快。

    常见的有:语音通话、视频通话、实时游戏画面 等。
    
  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。

    常见有:网站、手机APP数据获取等。
    

2.1 TCP代码示例

服务端

import socket

# 1.监听本机的IP和端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
while True:
    # 2.等待,有人来连接(阻塞)
    conn, addr = sock.accept()
    # 3.等待,连接者发送消息(阻塞)
    client_data = conn.recv(1024)
    print(client_data)
    # 4.给连接者回复消息
    conn.sendall(b"hello world")
    # 5.关闭连接
    conn.close()
# 6.停止服务端程序
sock.close()

客户端

import socket

# 1. 向指定IP发送连接请求
client = socket.socket()
client.connect(('127.0.0.1', 8001))
# 2. 连接成功之后,发送消息
client.sendall(b'hello')
# 3. 等待,消息的回复(阻塞)
reply = client.recv(1024)
print(reply)
# 4. 关闭连接
client.close()

2.2 UDP代码示例

服务端

import socket
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # SOCK_DGRAM指UDP协议
server.bind(('127.0.0.1',8001))

while True:
    data,(host,port) = server.recvfrom(1024) # 等待接受数据
    # 由于UDP不是持续连接,故需要获得主机地址和端口才能回信息
    print(data,host,port)
    server.sendto("已收到".encode('utf-8'),(host,port))
client.close()

客户端

import socket

client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
    text = input("请输入要发送的内容(Q退出):").strip()
    if text.upper() == "Q":
        break
    client.sendto(text.encode('utf-8'),('127.0.0.1',8001))
    data,(host,port) = client.recvfrom(1024)
    print(data.decode('utf-8'))
client.close()

3 粘包

在两台电脑进行数据收发的时候,其实并不是将数据直接传输给对方

  • 对于发送者,执行 sendall/send 发送消息时,是将数据先发送至自己网卡的写缓冲区,再由缓冲区将数据发送给到对方网卡的读缓冲区

    • send可能存在网卡缓冲区空间不足导致发送内容被截取的情况(发不全),故推荐使用sendall
  • 对于接受者,执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。例如:

client.py

import socket

client = socket.socket()
client.connect(('127.0.0.1',8001)) # TCP连接建立

# 这里发送了两次
client.sendall('kinght love '.encode('utf-8'))
client.sendall('aym'.encode('utf-8'))

client.close()

service.py

import socket

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001))
sock.listen(1)
conn,addr = sock.accept() # 等待连接
client_data = conn.recv(1024) # 接收数据
print(client_data.decode('utf-8'))
conn.close()
sock.close()

本次连接,client.py向server.py连续发送了两个数据包,由于两次数据包间隔时间太短,被接收端一次就完成接收,被认为是同一个数据包内容,导致输出结果

image-20220709163626287

3.1 解决粘包问题的思路

解决粘包问题的思路非常简单,只需要在每次发送消息的时候,告诉接收方这个消息有多长即可,通常方案有两种:

  • 单独发送字节长度,再发送数据,接收端获得长度后,根据长度读取缓冲区内数据
  • 发送数据时,将数据载荷分为两个部分
    • 头部(固定字节长度一般为4字节,内容为数据的字节长度)
    • 数据

? 接收端根据头部获取的数据字节长度来读取缓冲区数据

client.py

import socket

client = socket.socket()
client.connect(('127.0.0.1',8001))

text = ['kinght love ','aym']
for item in text:
    item_encode = item.encode('utf-8')
    
    # 计算数据长度并发送给服务端
        # 传输数据必须是字节码,而整数不能转字节码,所以需要先转字符串
    item_len = str(len(item_encode)).encode('utf-8')
    client.sendall(item_len) # 首先发送数据长度
    
    client.sendall(item_encode) # 然后在发送数据
client.close()

service.py

import socket

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001))
sock.listen(1)
conn,addr = sock.accept() # 等待连接

# 接收数据长度
client_len = conn.recv(4) # 首先接收数据长度
client_len = client_len.decode('utf-8')
client_len = int(client_len)

client_data = conn.recv(client_len) # 使用长度接收数据
print(client_data.decode('utf-8'))
conn.close()
sock.close()

3.2 使用struct解决数字转字节

上述案例中,我们发的先传输数据必须是字节码,而整数不能直接转换为字节码,需要先转换为字符串,显得特别麻烦,这里就可以使用python的struct包来实现

import struct

# 数值转换为固定4个字节
v1 = struct.pack('i',199)
## i是参数,表示转换前是int类型,转换为4个字节
## 199是数值,被转换的数值
print(v1) # 转换后的结果 b'\xc7\x00\x00\x00'

# 字节转换为数值
v2 = struct.unpack('i', v1)
print(v2) # (199,) 转弯完成后是元组类型

使用何种参数进行转换,可以参照一下表格进行。例如使用int转换为字节码,表格显示占用大小4个字节,即参考表格第八行使用i进行转换,在接收端我们也要接收4个字节的数据

image-20210215090446549

案例:

service.py

import socket,struct

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(('127.0.0.1',8001))
sock.listen(5)
conn,addr = sock.accept()
while True:
    # 读取4个字节,获得数据长度
    header = conn.recv(4)
    if not header:
        print("对方断开连接")
        conn.close()
        break # 对方断开连接会发送空字符
    data_lenth = struct.unpack('i',header)[0]
    
    # 分段传输:保证数据长度大于1024时能够完成传输
    has_recv_len = 0 # 初始化已接收的数据长度
    data = b"" # 初始化数据接收空间,数据接收后存放于此
    while True:
        lenth = data_lenth - has_recv_len # 总数据长度 - 已接受数据长度 = 还需传输的数据长度
        if lenth > 1024: # 单次数据传输量为1024 故如果还需传输的数据长度大于1024 则需要分段传输
            lth = 1024 # 需要分段传输,设置本次接收长度为1024
        else:
            lth = lenth # 不需要分段传输,设置本次接收长度为剩余总长度
        chunk = conn.recv(lth)
        data += chunk
        has_recv_len = len(data)
        if has_recv_len == data_lenth: # 已接收的长度等于数据总长度则完成接收
            break
    print(data.decode('utf-8'))

sock.close()

tips:系统默认单次接收最多1024*8 = 8196字节

client.py

import socket,struct

client = socket.socket()
client.connect(('127.0.0.1',8001))

data1 = 'xin正在学习'.encode('utf-8')
heard1 = struct.pack('i',len(data1))
client.sendall(heard1)
client.sendall(data1)

data2 = '应该没有'.encode('utf-8')
heard2 = struct.pack('i',len(data2))
client.sendall(heard2)
client.sendall(data2)

client.close()

3.3 案例:发送消息和文件上传

service.py

import os,json,socket,struct

def recv_data(conn,chunk_size=1024):
    # 获取信息长度
    data_lenth = conn.recv(4) # 接收struct固定转换整形4个字节
    data_lenth = struct.unpack('i',data_lenth)[0] # 将字节码转为整数
    # 获取数据
    data_list = []
    has_read_data_size = 0 # 接收数据长度初始化
    while has_read_data_size < data_lenth:
        # 剩余数据长度大于chunk_size默认值1024 则分段传输
        size = chunk_size if (data_lenth - has_read_data_size) > chunk_size else data_lenth - has_read_data_size
        chunk = conn.recv(size)
        data_list.append(chunk)
        has_read_data_size += len(chunk)
    data = b"".join(data_list)
    return data

def recv_file(conn,save_file_name,chunk_size=1024):
    # 获取信息长度
    data_lenth = conn.recv(4)
    data_lenth = struct.unpack('i',data_lenth)[0]
    # 获取当前文件位置
    abs_path = os.path.abspath(__file__)
    dir_path = os.path.dirname(abs_path)
    file_path = os.path.join(dir_path,'file',save_file_name)
    # 获取文件句柄
    file_object = open(file_path,mode='wb')
    # 开始接收文件
    has_read_data_size = 0
    while has_read_data_size < data_lenth:
        size = chunk_size if (data_lenth - has_read_data_size) > chunk_size else data_lenth - has_read_data_size
        chunk = conn.recv(size)
        file_object.write(chunk)
        file_object.flush()
        has_read_data_size += len(chunk)

def run():
    # 监听连接
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # TCP连接
    # IP可复用,有时候非正常退出会有IP被占用的报错,下方代码可以重复使用这个IP
    sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    # 绑定监听IP和端口
    sock.bind(('127.0.0.1',8001))
    sock.listen(5)

    # 监听端口等待连接
    while True:
        conn,addr = sock.accept()
        while True:
            # 获取消息类型
            message_type = recv_data(conn).decode('utf-8') # 接收到的是json数据或者是关闭的close
            print(message_type)
            if message_type == "close":
                print("客户端已关闭连接")
                break
            # 反序列化之后
            # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" }
            # 消息:{'msg_type':'msg'}
            message_type_info = json.loads(message_type) # 进行json数据进行反序列化
            if message_type_info['msg_type'] == 'msg':
                data = recv_data(conn)
                print("接收到消息:{}".format(data.decode('utf-8')))
            elif message_type_info['msg_type'] == 'file':
                recv_file(conn,message_type_info['file_name'])
            else:
                print("格式错误,请验证后输入")
        conn.close()
    sock.close()

if __name__ == '__main__':
    run()

client.py

import os,json,socket,struct

def send_data(conn,content):
    data = content.encode('utf-8') # 编码
    header = struct.pack('i',len(data)) # 发送数据长度
    conn.sendall(header)
    conn.sendall(data)
    print(data,len(data))

def send_file(conn,file_path):
    file_size = os.stat(file_path).st_size
    header = struct.pack('i',file_size)
    conn.sendall(header)
    # 发送文件数据
    has_send_size = 0 # 已发送的文件大小
    file_object = open(file_path,mode='rb')
    while file_size > has_send_size:
        chunk = file_object.read(1024)
        conn.sendall(chunk)
        has_send_size += len(chunk)
    file_object.close()

def run():
    client = socket.socket()
    client.connect(('127.0.0.1',8001))
    while True:
        """
        请发送消息,格式为:
            - 消息:msg|你好呀
            - 文件:file|xxxx.png
        """
        content = input(">>>")
        if content.upper() == "Q":
            send_data(client,"close") # 使用client连接对象发送close数据表示断开连接
            # TCP协议断开连接时会进行四次挥手,从代码层面看就是发送空字符,不过这并不准确,毕竟有可能就是发送的空字符,所以,通常的做法时定义一个断开连接的字符串进行发送,服务端接收到之后进行断开关闭连接
            break
        input_text_list = content.split('|') # 用户输入内容分割
        if len(input_text_list) != 2: # 多个竖线会导致分割元素大于2
            print("格式输入错误")
            continue
        message_type,info = input_text_list
        if message_type == 'msg':
            send_data(client,json.dumps({'msg_type':'msg'})) # 传输信息属性
            send_data(client,info) # 传输信息内容
        elif message_type == 'file':
            filename = info.rsplit(os.sep,maxsplit=1)[-1] # 获得文件名
                        # os.sep 指路径分割符 可以通过系统不同区分/或者\
            send_data(client,json.dumps({'msg_type':'file','file_name':filename}))
            send_file(client,info)
        else:
            print('格式输入错误')
            continue
    client.close()
if __name__ == '__main__':
    run()

4 阻塞与非阻塞

在默认情况下,我们编写网络编程代码的时候,等待连接和等待传输的过程中都会阻塞程序的运行

# ################### socket服务端(接收者)###################
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
# 阻塞
conn, addr = sock.accept()
# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()

# ################### socket客户端(发送者) ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
client.sendall('ABCDEFGH'.encode('utf-8'))
client.close()

我们可以使用sock.setblocking(False)将程序变为非阻塞状态,让他没有接受到连接和数据的时候,继续往下执行,不过由于也没有接收到参数,所以这样遇到 acceptrecvconnect 就会抛出 BlockingIOError 的异常。

这不是代码编写的错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误

这时使用try.except语句,让他没有得到客户端连接的时候先去执行下方的代码

try:
    conn,addr = sock.accept()
except BlockingIOError as e:
    pass

非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用

5 IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作

r,w,e = select.select([],[],[],0.05)
# r对应第一个中括号,检测是否收到数据
# w对应第二个中括号,检测是否连接成功
# e对应第三个中括号,检测是否发送异常

5.1 TCP的服务端同时处理多个客户端

IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:

server.py

import select
import socket

def run():
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.setblocking(False) # 开启非阻塞
    server.bind(('127.0.0.1',8001))
    server.listen(5)

    # inputs列表 使用 selet.selet监控列表的对象是否有变化
    inputs = [server,]
    # socket对象->[server(初始只有它)]
    # socket对象->[server(初始只有它),conn(第一次连接添加到)]
    # socket对象->[server(初始只有它),conn(第一次连接添加到),conn(第二次连接添加到)....]
    # 通过不断添加conn达到同时监听多个客户端的目的
    while True:
        # 循环监听 r检测被连接 w检测发起的连接是否成功
        r,w,e = select.select(inputs,[],[],0.05) # 固定搭配,单次监控读取0.05秒
        # 将有变化的socket对象交给r
        for sock in r:
            # server 建立连接
            if sock == server: # 发现有变化的是列表中的server
                conn,addr = sock.accept()
                print("有新的连接")
                inputs.append(conn) # 将conn对象加入到inputs中
            else:
                data = sock.recv(1024) # 如果sock是conn,就用来接收数据
                if data:
                    print("收到消息:",data)
                else:
                    print("关闭连接")
                    inputs.remove(sock) # 如果要关闭连接,就将sock从inputs列表中去除,不再监测
if __name__ == '__main__':
    run()

client.py

import socket

def run():
    client = socket.socket()
    client.connect(('127.0.0.1',8001))

    while True:
        content = input(">>>")
        if content.upper() == "Q":
            break
        client.sendall(content.encode('utf-8'))
    client.close()
if __name__ == '__main__':
    run()

通过多路复用,我们可以让服务端同时处理来自多个客户端的网络请求

22020716001

5.2 IO复用完成客户端基础伪并发请求

客户端在对多个网络资源进行请求的时候,通常的做法是依次请求,如果遇到较大资源或者是需要服务端等待的资源时,问题就出现了

service.py

# 服务端使用IO编程模拟多个请求状态
import select,socket,time

def run():
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.setblocking(False)
    server.bind(('127.0.0.1',8001))
    server.listen(5)

    inputs = [server,]
    while True:
        r,w,e = select.select(inputs,[],[],0.05)
        for sock in r:
            if sock == server:
                conn,addr = sock.accept()
                print("有新的连接")
                inputs.append(conn)
            else:
                data = sock.recv(1024).decode('utf-8')
                print(data)
                if data == 'sleep':
                    print('收到延时信息',data)
                    time.sleep(5)
                    sock.sendall(('已收到延时反馈信息:{}'.format(data)).encode('utf-8'))
                elif data:
                    print('收到普通信息',data)
                    sock.sendall(('已收到普通信息:{}'.format(data)).encode('utf-8'))
                else:
                    print('连接已被关闭')
                    inputs.remove(sock)

if __name__ == '__main__':
    run()

client.py

import select,socket

inp_text = ["first",'sleep','second','third'] # 发送消息列表
client_list = [] # 请求连接的socket对象列表
recv_list = [] # 存放已成功连接的对象conn
inp_number = 0
# 生成多个请求对象
for item in inp_text:
    client = socket.socket()
    client.setblocking(False) # 非阻塞
    try:
        client.connect(('127.0.0.1',8001))
    except BlockingIOError as e:
        pass
    client_list.append(client) # 将生成的请求对象放于client_list列表


while True:
    # r监听recv_list列表 是否接收到信息
    # w监听client_list列表 是否成功连接
    r,w,e = select.select(recv_list,client_list,[],0.01)
    # w成功监听到连接,则需要发送数据,如果需要返回数据,则创建连接conn,将其放于recv_list列表中,等待监听
    for sock in w:
        data = inp_text[inp_number]
        print(data)
        sock.sendall(data.encode('utf-8')) # 发送inp_text中的元素
        inp_number += 1
        recv_list.append(sock) # 将连接成功的对象添加到recv_list列表中,等待监听返回数据
        client_list.remove(sock) # socket请求连接已经成功连接上,故不在监听是否连接成功
    for sock in r:
        data = sock.recv(1024)
        print(data)
        sock.close()
        recv_list.remove(sock)

5.3 多路复用的三种模式

在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)

监测socket对象是否新连接到来 or 新数据到来

5.3.1 select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

5.3.2 poll

poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

5.3.3 epoll

直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

5.3.4 IO多路复用不止socket

实际上IO多路复用适用于所有的IO交互操作


Python开发-034_网络编程
http://localhost:8080/archives/VoYOJPvW
作者
kinght
发布于
2024年11月11日
更新于
2024年11月11日
许可协议