Linux网络IO模型与python实现

2020-10-09  本文已影响0人  睡不醒的大橘

一、背景知识

Socket

概念
socket交互基本流程

Linux网络IO

  1. 服务器的网络驱动接收到消息之后,向内核申请空间,并在收到完整的数据包(这个过程会产生延时,因为有可能是通过分组传送过来的)后,将其复制到内核空间;
  2. 数据从内核空间拷贝到用户空间;
  3. 用户程序进行处理。


二、Linux IO模型

阻塞IO模型

import socket

HOST = socket.gethostname()
PORT = 12345
BUFSIZ = 1024

# 模拟handle socket
def handle_socket(request):
    return '{} received'.format(request)

s = socket.socket()
s.bind((HOST, PORT))
# 开始 TCP 监听。backlog 参数指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
# 该值至少为 1,大部分应用程序设为 5 就可以了。
s.listen(5)
while True:
    print('waiting for connection...')
    conn, addr = s.accept()
    print('connecting from: {}'.format(addr))
    req = conn.recv(BUFSIZ)
    resp = handle_socket(req.decode('utf-8'))
    conn.send(resp.encode())
    conn.close()

client

import socket

HOST = socket.gethostname()
PORT = 12345
BUFSIZ = 1024

s = socket.socket()
s.connect((HOST, PORT))
s.send(b'test message')
while True:
    data = s.recv(BUFSIZ)
    if not data:
        break
    print(data.decode('utf-8'))
s.close()

非阻塞IO模型

非阻塞的recv系统调用之后,进程没有被阻塞,操作系统立马把结果返回给进程,如果数据还没准备好,则抛出异常,进程可以去做其他的事,然后在发起recv系统调用,重复上述过程(这个过程通常被称为轮询),一直到数据准备好,再拷贝数据到进程进行数据处理。需要注意,拷贝数据的整个过程,进程仍然是属于阻塞状态。

import socket

HOST = socket.gethostname()
PORT = 12345
BUFSIZ = 1024

# 模拟handle socket
def handle_socket(request):
    return '{} received'.format(request)

server = socket.socket()
server.bind((HOST, PORT))
server.listen(5)
# accept默认是阻塞的,设置后accept成为非阻塞
server.setblocking(False)

conn = None

while True:
    try:
        # print('waiting for connection...')
        conn, addr = server.accept()
        print('connecting from: {}'.format(addr))
    # accept被设置为非阻塞后,要求必须有connect来连接, 否则抛出BlockingIOError
    except BlockingIOError:
        continue
        
    try:
        client_req = conn.recv(BUFSIZ)
        resp = handle_socket(client_req.decode('utf-8'))
        conn.send(resp.encode())
        conn.close()
    except (BlockingIOError, ConnectionResetError):
        pass

多路复用IO模型

select
import socket
import select

HOST = socket.gethostname()
PORT = 12345
BUFSIZ = 1024

# 模拟handle socket
def handle_socket(request):
    return '{} received'.format(request)

server = socket.socket()
server.bind((HOST, PORT))
server.listen(5)
server.setblocking(False)

inputs = [server, ]

while True:
    print('waiting for connection...')
    # 监听第一个列表的文件描述符,如果其中有文件描述符发生改变,则捕获并放到rlist中
    # rlist-- wait until ready for reading
    # wlist -- wait until ready for writing
    # xlist -- wait for an ``exceptional condition''
    rlist, wlist, elist = select.select(inputs, [], [])

    for r in rlist:
        # 当客户端第一次连接服务端时
        if r == server:
            conn, addr = r.accept()
            inputs.append(conn)
            print('connecting from: {}'.format(addr))
        # 当客户端连接上服务端之后,再次发送数据时
        else:
            client_req = r.recv(BUFSIZ)
            resp = handle_socket(client_req.decode('utf-8'))
            r.send(resp.encode())
            inputs.remove(r)
            r.close()
  1. 每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
  2. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)
poll

poll的机制与select类似,只是poll没有最大文件描述符数量的限制。因此poll仍然有select的缺点1。

epoll

1)LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。

2)ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

import socket
import select

HOST = socket.gethostname()
PORT = 12345
BUFSIZ = 1024

def handle_socket(request):
    return '{} received'.format(request)

server = socket.socket()
server.bind((HOST, PORT))
server.listen(5)
server.setblocking(False)

#创建epoll对象
epoll = select.epoll()

#将创建的套接字添加到epoll的事件监听中
#事件类型:
#select.EPOLLIN 可读事件
#select.EPOLLOUT 可写事件
#select.EPOLLERR   错误事件
#select.EPOLLHUP   客户端断开事件
epoll.register(server.fileno(), select.EPOLLIN)

conns = {}

while True:
    print('waiting for connection...')
    #轮询注册的事件集合
    epoll_list = epoll.poll()
    for fd, events in epoll_list:
        #新连接
        if fd == server.fileno():
            conn, addr = server.accept()
            #注册新连接fd到待读事件集合
            epoll.register(conn.fileno(), select.EPOLLIN)
            conns[conn.fileno()] = conn
        #可读事件
        elif events == select.EPOLLIN:
            client_req = conns[fd].recv(BUFSIZ)
            resp = handle_socket(client_req.decode('utf-8'))
            conns[fd].send(resp.encode())
            epoll.unregister(fd)
            conns[fd].close()
            del conns[fd]
上一篇 下一篇

猜你喜欢

热点阅读