以下内容基于python2.7。

(一)python非阻塞网络编程

非阻塞网络编程要求监听或等待接受不阻塞当前线程,如果资源没到就先跳过(其实是抛出IOError)继续执行后面的代码。

  • 非阻塞监听:sock.setblocking(False)
  • 非阻塞接收:conn.setblocking(False)

示例:

  • 服务端
# coding: utf-8

import socket

CONN_ADDR = ('127.0.0.1', 9999)
conn_list = []  # 连接列表
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 开启socket
sock.setblocking(False)  # 设置为非阻塞
sock.bind(CONN_ADDR)  # 绑定IP和端口到套接字
sock.listen(5)          # 监听,5表示客户端最大连接数
print('start listen')
while True:
    try:
        conn, addr = sock.accept()  # 被动接受TCP客户的连接,等待连接的到来,收不到时会报异常
        print('connect by ', addr)
        conn_list.append(conn)
        conn.setblocking(False)  # 设置非阻塞
    except IOError:
        pass    # 非阻塞connect时如果没有connect发起则抛出异常

    tmp_list = [conn for conn in conn_list]
    for conn in tmp_list:
        try:
            data = conn.recv(1024) # 接收数据1024字节
            if data:
                print u'收到的数据是%s' % data
                conn.send(data)
            else:
                print('close conn',conn)
                conn.close()
                conn_list.remove(conn)
        except IOError:
            pass    # 非阻塞recv时如果没有recv成功则抛出异常
  • 客户端
# coding: utf-8

import socket

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

while True:
	try:
	    msg = input(">>>")
	    if msg != 'q':
	        client.send(unicode(msg))
	        data = client.recv(1024)
	        print u'收到的数据%s' % data
	    else:
	        client.close()
	        print('close client socket')
	        break
    except SyntaxError:	# 空输入
        pass


(二)多路复用IO

阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作。
epoll是Linux效率最高的IO多路复用技术,而Windows只有默认的select。
注意:python2没有selectors模块,需要先pip install selectors2再使用selectors2模块替代。
使用IO多路复用的服务器示例:

# coding: utf-8

import socket
import selectors2

# 注册一个epllo事件
# 1. socket
# 2.事件可读
# 3.回调函数 把一个函数当成变量传到函数里

def recv_data(conn):
	try:
	    data = conn.recv(1024)
	    if data:
	        print u'接收的数据是:%s' % data
	        conn.send(data)
	    else:
	        e_poll.unregister(conn)
	        conn.close()
	except IOError:
		pass

def acc_conn(p_server):
    conn, addr = p_server.accept()
    print 'Connected by', addr
    # 也有注册一个epoll
    e_poll.register(conn, selectors2.EVENT_READ, recv_data)


CONN_ADDR = ('127.0.0.1', 9999)
server = socket.socket()
server.setblocking(False)
server.bind(CONN_ADDR)
server.listen(6) # 表示一个客户端最大的连接数

# 生成一个epllo选择器实例 I/O多路复用,监控多个socket连接
# e_poll = selectors2.EpollSelector()     # linux特有
e_poll = selectors2.DefaultSelector()   # window没有epoll
e_poll.register(server, selectors2.EVENT_READ, acc_conn)

# 事件循环
while True:
    # 事件循环不断地调用select获取被激活的socket
    events = e_poll.select(timeout=1)
    print events
    for key, _ in events:
        call_back = key.data
        call_back(key.fileobj)

如果不喜欢多路复用IO阻塞当前线程,可以给select方法传递一个timeout参数设置等待时间。

(三)异步非阻塞网络编程asyncroe

具体API文档可参考17.6. asyncore — Asynchronous socket handler — Python 2.7.18 documentation。

  • 基类:dispatcher
  • 提供方法:accept, read, write, close
  • 可重写方法:readable, writable, handle_accept, handle_read, handle_write, handle_close
  • 原理:
  • 模块维护一个全局变量socket_map字典保存所有的dispatcher实例,并在loop方法中轮询。
def loop(timeout=30.0, use_poll=False, map=None, count=None):
    if map is None:
        map = socket_map

    if use_poll and hasattr(select, 'poll'):
        poll_fun = poll2
    else:
        poll_fun = poll

    if count is None:
        while map:
            poll_fun(timeout, map)

    else:
        while map and count > 0:
            poll_fun(timeout, map)
            count = count - 1
  • 综合每个dispatcher对象的writable, readable等方法和属性,分别得出做好读、写、异常的dispatcher列表,并使用IO多路复用进行相应的操作(非阻塞),得到最终进行了读、写、异常的dispatcher列表,执行相对应的handle响应方法。以下仅列出read作为示例,writeexcept等其他过程类似。
def poll(timeout=0.0, map=None):
	r, w, e = select.select(r, w, e, timeout)
	
	for fd in r:
        obj = map.get(fd)
        if obj is None:
            continue
        read(obj)
        
def read(obj):
    try:
        obj.handle_read_event()
    except _reraised_exceptions:
        raise
    except:
        obj.handle_error()