IO多路复用

文章目录

  • 1 socket 缓冲区
  • 2 IO多路复用
    • 2.1 listen
    • 2.2 accept
    • 2.3 send
    • 2.4 recv
    • 2.5 总结
  • 3 建立socket链接
    • 3.1 创建一个基础的客户端和服务器
    • 3.2 使用多线程来实现处理多个客户端请求
    • 3.3 使用IO多路复用改写程序

1 socket 缓冲区

Linux系统中一切皆文件,socket在操作系统层面看,也是一个文件,每个建立链接的socket内核都会为其创建发送缓冲区和接收缓冲区。

socket 四元组 确定一个链接的过程

IO多路复用_第1张图片

socket在内核中维护了两个队列,分别是发送缓冲区和接收缓冲区

IO多路复用_第2张图片

机器A与机器B已经建立了socket链接
当机器A给机器B发送数据时

  1. 机器A用户态执行send()方法
  2. 机器A将发送的数据从用户态拷贝到内核态中的发送缓冲区中,等待内核发送
  3. 机器B的内核的接收缓冲区接收到数据
  4. 机器B执行recv()方法从内核的接收缓冲区拷贝数据到用户态中

至此一个发送数据的流程就走完了。

2 IO多路复用

IO多路复用机制是在一个线程中处理多个IO流,内核负责监听套接字上面的可读事件、可写事件和错误事件,触发相应事件的回调函数。

可读事件、可写事件

当socket的接收缓冲区中有数据时,在用户态的角度看就是可读的
当socket的发送缓冲区没有数据或者数据没有满时,在用户态的角度看就是可写的

2.1 listen

socket调用listen方法会将socket变成监听套接字,监听套接字等待客户端的链接

2.2 accept

当有客户端想要与监听套接字建立连接时,accept()函数会被触发,返回一个已连接套接字和一个远程客户端地址。

客户端发送握手报文给服务端,报文会发送到服务端内核的接收缓冲区中,对于服务端用户态代码来讲,是可读的,可以把数据从接收缓冲区拷贝到用户态中。

2.3 send

发送数据到发送缓冲区,发送缓冲区没有满,对于用户态来说就是可写的。

2.4 recv

从内核的接收缓冲区获取数据,接收缓冲区有数据,就是可读事件

2.5 总结

accept 可读事件
recv 可读事件
send 可写事件

事件的可读还是可写是站在用户态的角度说的,如果内核的接收缓冲区有数据,那么就是可读的,如果内核的发送缓冲区没有满,那么就是可写的。

3 建立socket链接

IO多路复用_第3张图片

3.1 创建一个基础的客户端和服务器

服务端

import socket

# 创建一个套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置套接字对象端口释放后直接可以使用,默认是False
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

# 绑定地址和端口,
server_socket.bind(('127.0.0.1', 9001))

# 开启监听
server_socket.listen(5)
print(f"server running .... at 9001")

# 阻塞等待客户端链接
client_socket, remote_addr = server_socket.accept()

print(f"欢迎{remote_addr[0]}:{remote_addr[1]}访问服务端~~~")

# 接收客户端发送过来的数据
recv_msg = client_socket.recv(8096).decode("utf-8")
print(f"从客户端{remote_addr[0]}接收数据: {recv_msg}")

# 给客户端返回数据
send_msg = recv_msg + "呵呵哈哈哈"
client_socket.send(send_msg.encode("utf-8"))

# 服务端主动断开链接
client_socket.close()

客户端

import socket

# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 发起远程链接
client_socket.connect(("127.0.0.1", 9001))

# 发送数据
client_socket.send("我来了".encode('utf-8'))

# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')

print(f"从服务端接收数据:{recv_msg}")

3.2 使用多线程来实现处理多个客户端请求

服务端

import socket
import threading


def handle_request(client_socket, remote_addr):
    """ 开启一个线程,处理请求 """
    print(f"欢迎客户端访问:{remote_addr[0]}at{remote_addr[1]}")
    recv_msg = client_socket.recv(8096).decode("utf-8")
    print(f"从客户端接收到信息:{recv_msg}")
    send_msg = recv_msg + "哈哈哈哈哈哈!!!"
    client_socket.send(send_msg.encode('utf-8'))
    client_socket.close()


# 创建socket_server服务端
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置socket属性
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

# 绑定地址
socket_server.bind(("127.0.0.1", 9002))

# 开启监听
socket_server.listen(128)

print(f"server listening at 9002......")

while True:
    client_socket, remote_addr = socket_server.accept()
    threading.Thread(target=handle_request, args=(client_socket, remote_addr)).start()

客户端

import socket

# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 发起远程链接
client_socket.connect(("127.0.0.1", 9002))

# 发送数据
client_socket.send("我来了".encode('utf-8'))

# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')

print(f"从服务端接收数据:{recv_msg}")

client_socket.close()

3.3 使用IO多路复用改写程序

大家都知道,多线程的切换会导致一些系统的开销,线程的切换,导致系统的开销比较大,使用单线程IO多路复用技术实现多任务处理,是一种很好的方式。

前面介绍的socket都是阻塞的socket,开启监听之后,服务端阻塞等待客户端链接,

服务端

import socket
import threading


def handle_request(client_socket, remote_addr):
    """ 开启一个线程,处理请求 """
    print(f"欢迎客户端访问:{remote_addr[0]}at{remote_addr[1]}")
    recv_msg = client_socket.recv(8096).decode("utf-8")
    print(f"从客户端接收到信息:{recv_msg}")
    send_msg = recv_msg + "哈哈哈哈哈哈!!!"
    client_socket.send(send_msg.encode('utf-8'))
    client_socket.close()


# 创建socket_server服务端
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置socket属性
socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

# 绑定地址
socket_server.bind(("127.0.0.1", 9002))

# 开启监听
socket_server.listen(128)

print(f"server listening at 9002......")

while True:
    client_socket, remote_addr = socket_server.accept()
    threading.Thread(target=handle_request, args=(client_socket, remote_addr)).start()

客户端

import socket

# 创建一个socket客户端
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 发起远程链接
client_socket.connect(("127.0.0.1", 9003))

# 发送数据
client_socket.send("我来了".encode('utf-8'))

# 接收数据
recv_msg = client_socket.recv(8096).decode('utf-8')

print(f"从服务端接收数据:{recv_msg}")

client_socket.close()

服务端

import socket
import select
from queue import Queue
from typing import Dict

# 创建server_socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定IP和port
server_socket.bind(("127.0.0.1", 9005))
# 设置重用地址
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

# 必须设置成非阻塞,IO多路复用必须用非阻塞套接字
server_socket.setblocking(False)
server_socket.listen(128)

# 以上创建监听套接字,负责监听是否有客户端链接
# 当事件发生时,代表客户端过来了,属于可读事件
# 所以把server_socket放到rlist里面

rlist = [server_socket]
wlist = []
xlist = rlist
# 因为可以监听多个套接字,所以rlist,wlist,xlist,都是列表
# 但在初始状态下,select只监听一个监听套接字(server_socket)即可


message_queues: Dict[socket.socket, Queue] = {}

while True:
    # 开启select返回三个元素
    # readable:     rlist中发生可读事件的socket对象
    # writeable:     wlist中发生可写事件的socket对象
    # exceptional:   xlist中发生异常的socket
    readable, writeable, exceptional = select.select(rlist, wlist, xlist)
    # 遍历readable
    for r in readable:
        # 如果r是server_socket, 则代表监听套接字有事件发生
        # 显然是客户端链接来了
        if r is server_socket:
            # 监听套接字是非阻塞的,那么已链接套接字默认也是非阻塞的
            client_socket, remote_addr = server_socket.accept()
            print(f"欢迎客户端:{remote_addr[0]}:{remote_addr[1]}到来!")
            # 客户端将数据发送到接收缓冲区,接收缓冲区的数据对于用户来说是可读的
            # 将客户端socket放到可读文件列表中,当客户端给服务端发消息时
            # 遍历的时候会处理这个已连接的套接字
            rlist.append(client_socket)
            # 负责保存客户端发送过来的消息,以socket对象为键,以Queue()对象为值
            message_queues[client_socket] = Queue()
        else:
            # 如果不是server_socket 则表示已链接套接字有事件发生
            data = r.recv(1024)
            if data:
                # 这里的r就是活跃的已连接的socket
                addr = r.getpeername()
                # 数据没有接收完,继续接收
                print(f"接收到{addr}发来的数据{data.decode('utf-8')}")
                # message_queues保存了所有已连接的字符串
                # 每一个套接字都对应一个队列,找到该活跃套接字对应的队列
                message_queues[r].put(data) # 将消息放进去
                # 消息放进去之后,服务器需要回复客户端消息,属于可写事件
                # 还要将r放到wlist中接受select检查
                if r not in wlist:
                    wlist.append(r)
            else:
                # 走到这里,说明data为假
                # 客户端断开链接了,给服务器发送一个b''
                addr = r.getpeername()
                print(f"客户端{addr}断开链接了!")
                # 我们要将套接字从rlist, wlist中移除
                # 客户端都断开链接了,那么select也就不需要再监听了
                rlist.remove(r)
                if r in wlist:
                    wlist.remove(r)

                # 把message_queues里面的已连接套接字移除
                message_queues.pop(r)
                # 关闭服务端套接字链接
                r.close()
    # 以上是遍历可读事件,可读事件可以发生在监听套接字上面(和客户端建立连接)
    # 也可以发生在已连接套接字上面(客户端发信息了)
    # 如果没有事件发生或者处理完毕,那么接下来就要遍历可写事件
    # 而可写事件一定发生在已连接套接字上面(要回消息给客户端)
    for w in writeable:
        message_queue = message_queues[w]
        # 一开始队列里面肯定是有消息的,因为我们手动往里面放了一条
        # 但如果队列为空,说明服务端已经回复过了,那么要将该套接字从 wlist 里面移除
        # 让它变为非活跃状态,不再满足可写
        # 等到下一次客户端发消息时,再将它添加到 wlist 中,让它变得可写
        if message_queue.empty():
            wlist.remove(w)
            continue
        # 获取消息
        data = message_queue.get()
        # 发送给客户端
        w.send("服务端收到,你发的消息是: ".encode("utf-8") + data)

    for x in exceptional:
        addr = x.getpeername()
        print(f"和客户端 {addr[0]}:{addr[1]} 通信出现错误")
        rlist.remove(x)
        if x in wlist:
            wlist.remove(x)
        message_queues.pop(x)
        x.close()

你可能感兴趣的:(网络,linux,服务器)