IO多路復用深入淺出

Java程序員進階三條必經之路:數據庫、虛擬機、異步通信。

前言

從零單排高性能問題,這次輪到異步通信了。這個領域入門有點難,需要了解UNIX五種IO模型和TCP協議,熟練使用三大異步通信框架:Netty、NodeJS、Tornado。目前所有標榜異步的通信框架用的都不是異步IO模型,而是IO多路復用中的epoll。因為Python提供了對Linux內核API的友好封裝,所以我選擇Python來學習IO多路復用。

IO多路復用

  1. select

舉一個EchoServer的例子,客戶端發送任何內容,服務端會原模原樣返回。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 16, 2016

@author: mountain
'''
import socket
import select
from Queue import Queue

#AF_INET指定使用IPv4協議,如果要用更先進的IPv6,就指定為AF_INET6。
#SOCK_STREAM指定使用面向流的TCP協議,如果要使用面向數據包的UCP協議,就指定SOCK_DGRAM。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
#設置監聽的ip和port
server_address = ('localhost', 1234)
server.bind(server_address)
#設置backlog為5,client向server發起connect,server accept后建立長連接,
#backlog指定排隊等待server accept的連接數量,超過這個數量,server將拒絕連接。
server.listen(5)
#注冊在socket上的讀事件
inputs = [server]
#注冊在socket上的寫事件
outputs = []
#注冊在socket上的異常事件
exceptions = []
#每個socket有一個發送消息的隊列
msg_queues = {}
print "server is listening on %s:%s." % server_address
while inputs:
     #第四個參數是timeout,可選,表示n秒內沒有任何事件通知,就執行下面代碼
     readable, writable, exceptional = select.select(inputs, outputs, exceptions)
     for sock in readable:
         #client向server發起connect也是讀事件,server accept后產生socket加入讀隊列中
         if sock is server:
             conn, addr = sock.accept()
             conn.setblocking(False)
             inputs.append(conn)
             msg_queues[conn] = Queue()
             print "server accepts a conn."
         else:
             #讀取client發過來的數據,最多讀取1k byte。
             data = sock.recv(1024)
             #將收到的數據返回給client
             if data:
                 msg_queues[sock].put(data)
                 if sock not in outputs:
                     #下次select的時候會觸發寫事件通知,寫和讀事件不太一樣,前者是可寫就會觸發事件,并不一定要真的去寫
                     outputs.append(sock)
             else:
                 #client傳過來的消息為空,說明已斷開連接
                 print "server closes a conn."
                 if sock in outputs:
                     outputs.remove(sock)
                 inputs.remove(sock)
                 sock.close()
                 del msg_queues[sock]
     for sock in writable:
         if not msg_queues[sock].empty():
             sock.send(msg_queues[sock].get_nowait())
         if msg_queues[sock].empty():
             outputs.remove(sock)
     for sock in exceptional:
         inputs.remove(sock)
         if sock in outputs:
             outputs.remove(sock)
         sock.close()
         del msg_queues[sock]
[mountain@king ~/workspace/wire]$ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
1

select有3個缺點:
1. 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大。
1. 每次調用select后,都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大。
這點從python的例子里看不出來,因為python select api更加友好,直接返回就緒的socket列表。事實上linux內核select api返回的是就緒socket數目:
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1. fd數量有限,默認1024。

  1. poll

采用poll重新實現EchoServer,只要搞懂了select,poll也不難,只是api的參數不太一樣而已。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 27, 2016

@author: mountain
'''
import select
import socket
import sys
import Queue

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
server_address = ('localhost', 1234)
server.bind(server_address)
server.listen(5)
print 'server is listening on %s port %s' % server_address
msg_queues = {}
timeout = 1000 * 60
#POLLIN: There is data to read
#POLLPRI: There is urgent data to read
#POLLOUT: Ready for output
#POLLERR: Error condition of some sort
#POLLHUP: Hung up
#POLLNVAL: Invalid request: descriptor not open
READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR
READ_WRITE = READ_ONLY | select.POLLOUT
poller = select.poll()
#注冊需要監聽的事件
poller.register(server, READ_ONLY)
#文件描述符和socket映射
fd_to_socket = { server.fileno(): server}
while True:
     events = poller.poll(timeout)
     for fd, flag in events:
         sock = fd_to_socket[fd]
         if flag & (select.POLLIN | select.POLLPRI):
             if sock is server:
                 conn, client_address = sock.accept()
                 conn.setblocking(False)
                 fd_to_socket[conn.fileno()] = conn
                 poller.register(conn, READ_ONLY)
                 msg_queues[conn] = Queue.Queue()
             else:
                 data = sock.recv(1024)
                 if data:
                     msg_queues[sock].put(data)
                     poller.modify(sock, READ_WRITE)
                 else:
                     poller.unregister(sock)
                     sock.close()
                     del msg_queues[sock]
         elif flag & select.POLLHUP:
             poller.unregister(sock)
             sock.close()
             del msg_queues[sock]
         elif flag & select.POLLOUT:
             if not msg_queues[sock].empty():
                 msg = msg_queues[sock].get_nowait()
                 sock.send(msg)
             else:
                 poller.modify(sock, READ_ONLY)
         elif flag & select.POLLERR:
             poller.unregister(sock)
             sock.close()
             del msg_queues[sock]

poll解決了select的第三個缺點,fd數量不受限制,但是失去了select的跨平臺特性,它的linux內核api是這樣的:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd { 
     int fd; /* file descriptor */
     short events; /* requested events to watch */
     short revents; /* returned events witnessed */
};
  1. epoll

用法與poll幾乎一樣。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Created on Feb 28, 2016

@author: mountain
'''
import select
import socket
import Queue

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
server_address = ('localhost', 1234)
server.bind(server_address)
server.listen(5)
print 'server is listening on %s port %s' % server_address
msg_queues = {}
timeout = 60
READ_ONLY = select.EPOLLIN | select.EPOLLPRI
READ_WRITE = READ_ONLY | select.EPOLLOUT
epoll = select.epoll()
#注冊需要監聽的事件
epoll.register(server, READ_ONLY)
#文件描述符和socket映射
fd_to_socket = { server.fileno(): server}
while True:
     events = epoll.poll(timeout)
     for fd, flag in events:
         sock = fd_to_socket[fd]
         if flag & READ_ONLY:
             if sock is server:
                 conn, client_address = sock.accept()
                 conn.setblocking(False)
                 fd_to_socket[conn.fileno()] = conn
                 epoll.register(conn, READ_ONLY)
                 msg_queues[conn] = Queue.Queue()
             else:
                 data = sock.recv(1024)
                 if data:
                     msg_queues[sock].put(data)
                     epoll.modify(sock, READ_WRITE)
                 else:
                     epoll.unregister(sock)
                     sock.close()
                     del msg_queues[sock]
         elif flag & select.EPOLLHUP:
             epoll.unregister(sock)
             sock.close()
             del msg_queues[sock]
         elif flag & select.EPOLLOUT:
             if not msg_queues[sock].empty():
                 msg = msg_queues[sock].get_nowait()
                 sock.send(msg)
             else:
                 epoll.modify(sock, READ_ONLY)
         elif flag & select.EPOLLERR:
             epoll.unregister(sock)
             sock.close()
             del msg_queues[sock]

epoll解決了select的三個缺點,是目前最好的IO多路復用解決方案。為了更好地理解epoll,我們來看一下linux內核api的用法。

int epoll_create(int size)//創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)//注冊事件,每個fd只拷貝一次。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)/*等待IO事件,事件發生時,
內核調用回調函數,把就緒fd放入就緒鏈表中,并喚醒epoll_wait,epoll_wait只需要遍歷就緒鏈表即可,
而select和poll都是遍歷所有fd,這效率高下立判。*/
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 看到網上有不少討論epoll,但大多不夠詳細準確,以前面試有被問到這個問題。不去更深入的了解,只能停留在知其然...
    電臺_Fang閱讀 11,941評論 0 8
  • 大綱 一.Socket簡介 二.BSD Socket編程準備 1.地址 2.端口 3.網絡字節序 4.半相關與全相...
    y角閱讀 2,581評論 2 11
  • 我有一位好閨蜜,從小一起長大,大學在一個城市,工作后也會經常見面的那種,反正就是大家所謂的‘’后天親人”。剛剛我的...
    哆啦的夢閱讀 464評論 0 1
  • 這一篇文章是寫在14年11月30日,在我看來沒有什么文筆可言,字字句句皆出本心。我仍然記得,當時的我是留著淚把它寫...
    Ta_nG閱讀 782評論 0 1
  • A財富目標 1,公司四個月以內收入50萬 2,自己可以獨立承擔自己的責任。 B,伴侶的目標 身高一米六到一米七,長...
    雪痕情閱讀 96評論 0 1