我的python学习笔记之select模块

简介

Python中的select模块专注于I/O多路复用,提供了select poll epoll三个方法(其中后两个在Linux中可用,windows仅支持select),另外也提供了kqueue方法(freeBSD系统)

博文说明:
1,本文中的代码都已运行成功;
2,所有截图都是博主自己截取的,写一篇完整的博客确实很辛苦,整理素材输出文档;
3,如果转载,请注明出处。

运行环境说明:
	OS发行版:CentOS7.4
	python版本:
		【epoll实例服务端1】使用python3.6和python2.7.5
		【epoll实例服务端2】使用python2.7.5
		【所有客户端】使用python2.7.5

select方法

进程指定内核监听哪些文件描述符(最多监听1024个fd)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一个或者多个文件描述符事件发生时,进程被唤醒。

当我们调用select()时:

  1. 上下文切换转换为内核态
  2. 将fd从用户空间复制到内核空间
  3. 内核遍历所有fd,查看其对应事件是否发生
  4. 如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
  5. 返回遍历后的fd
  6. 将fd从内核空间复制到用户空间

fd:file descriptor 文件描述符

fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])

参数: 可接受四个参数(前三个必须)

  • rlist: wait until ready for reading
  • wlist: wait until ready for writing
  • xlist: wait for an “exceptional condition”
  • timeout: 超时时间

返回值:三个列表

select方法用来监视文件描述符(当文件描述符条件不满足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表

  • 当参数1 序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中
  • 当参数2 序列中含有fd时,则将该序列中所有的fd添加到 fd_w_list中
  • 当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中
  • 当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化,当超时时间 = n(正整数)时,那么如果监听的句柄均无任何变化,则select会阻塞n秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

select实例:利用select实现一个可并发的服务端

服务端:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import select

ip_port = ("192.168.12.172", 9999)
sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk1.bind(ip_port)
sk1.listen(5)
sk1.setblocking(0)

inputs = [sk1,]

print "inputs: ", inputs
while True:
    #要不断的调用select函数来检查给定的类文件对象是否有数据就绪,当且仅当有新客户端连接进来的时候,sk1服务端这个套接字对象才是读就绪的,也就是才会存在于readable_list列表中;
    readable_list, writeable_list, error_list = select.select(inputs, [], inputs, 1)
    print "readable_list: ", readable_list
    for r in readable_list:
        #当客户端第一次连接服务端时,sk1服务端套接字对象,通过调用accept方法获取到新进来的客户端套接字,然后把新进来的客户端套接字对象追加到,待select检测的inputs列表中。
        if sk1 == r:
            print "r=sk1: ", r
            print "first accept"
            request, address = r.accept()
            request.setblocking(0)
            inputs.append(request)
            print "first inputs: ", inputs
        #当客户端连接上服务器端之后,再次发送数据时
        else:
            print "myr: ", r
            received = r.recv(1024)
            print "received: ", received
            #当正常接收客户端发送的数据时
            if received:
                print "received data: ", received
            #当客户端关闭程序时
            else:
                inputs.remove(r)
sk1.close()

客户端:

#!/usr/bin/env python
#-*- coding: utf-8 -*-

import socket

ip_port = ("192.168.12.172", 9999)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)

while True:
    inp = raw_input("Please enter:")
    client.sendall(inp)
client.close()

在服务端我们可以看到,我们需要不停的调用select, 这就意味着:

  • 当文件描述符过多时,文件描述符在用户空间与内核空间进行copy会很费时
  • 当文件描述符过多时,内核对文件描述符的遍历也很浪费时间
  • select最大仅仅支持1024个文件描述符

poll与select相差不大,本文不作介绍

epoll方法

epoll很好的改进了select:

  1. epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  2. epoll会在epoll_ctl时把指定的fd遍历一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd
  3. epoll对文件描述符没有额外限制
select.epoll(sizehint=-1, flags=0) 创建epoll对象


epoll.close()
Close the control file descriptor of the epoll object.关闭epoll对象的文件描述符

epoll.closed
True if the epoll object is closed.检测epoll对象是否关闭

epoll.fileno()
Return the file descriptor number of the control fd.返回epoll对象的文件描述符

epoll.fromfd(fd)
Create an epoll object from a given file descriptor.根据指定的fd创建epoll对象

epoll.register(fd[, eventmask])
Register a fd descriptor with the epoll object.向epoll对象中注册fd和对应的事件

epoll.modify(fd, eventmask)
Modify a registered file descriptor.修改fd的事件

epoll.unregister(fd)
Remove a registered file descriptor from the epoll object.取消注册

epoll.poll(timeout=-1, maxevents=-1)
Wait for events. timeout in seconds (float)阻塞,直到注册的fd事件发生,会返回一个dict,格式为:{(fd1,event1),(fd2,event2),……(fdn,eventn)}

事件:

  • EPOLLIN Available for read 可读 状态符为1
  • EPOLLOUT Available for write 可写 状态符为4
  • EPOLLPRI Urgent data for read
  • EPOLLERR Error condition happened on the assoc. fd 发生错误 状态符为8
  • EPOLLHUP Hang up happened on the assoc. fd 挂起状态
  • EPOLLET Set Edge Trigger behavior, the default is Level Trigger behavior 默认为水平触发,设置该事件后则边缘触发
  • EPOLLONESHOT Set one-shot behavior. After one event is pulled out, the fd is internally disabled
  • EPOLLRDNORM Equivalent to EPOLLIN
  • EPOLLRDBAND Priority data band can be read.
  • EPOLLWRNORM Equivalent to EPOLLOUT
  • EPOLLWRBAND Priority data may be written.
  • EPOLLMSG Ignored.

水平触发和边缘触发:

  • Level_triggered(水平触发,有时也称条件触发):
    • 当被监控的文件描述符上有可读写事件发生时,epoll.poll()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll.poll()时,它还会通知你在上次没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
    • 优点很明显:稳定可靠
  • Edge_triggered(边缘触发,有时也称状态触发):
    • 当被监控的文件描述符上有可读写事件发生时,epoll.poll()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll.poll()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
    • 缺点:某些条件下不可靠

epoll实例服务端1:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import select
import datatime

s = socket.socket()
s.bind(('192.168.12.172',9999))
s.listen(5)

#创建一个epoll对象
epoll_obj = select.epoll()
# 在服务端socket上面注册对读事件(event)的关注。一个读event随时会触发服务端socket去接收一个socket连接
epoll_obj.register(s,select.EPOLLIN)

connections = {}

while True:
    # 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。
    # 如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回,返回一个元组列表[(fd1, events1), (...)]
    events = epoll_obj.poll(1)
    
    # event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
    for fd, event in events:
        help(event)
        # 如果是服务端产生event,表示有一个新的连接进来
        if fd == s.fileno():
            #创建一个新套接字,并且连接到对应到客户端(ip:port)
            conn, addr = s.accept()

            print('client connected: ', addr)
            #将上述新增服务端套接字注册到epoll_obj对象中
            epoll_obj.register(conn,select.EPOLLIN)
            connections[conn.fileno()] = conn
            print("connections: {0}".format(connections))

            print("Starting receving data...")
            
            #将客户端发送的数据接收进来
            msg = conn.recv(200)
            
            #给客户端响应
            conn.sendall('Server OK'.encode())
        else:
            #当fd不是上面声明的服务端套接字s的时候,执行以下操作
            try:
                #说明该客户端是已经来过的,则直接操作数据
                fd_obj = connections[fd]#获取文件描述符指向的套接字对象
                msg = fd_obj.recv(200)
                fd_obj.sendall('Client OK'.encode())
            except BrokenPipeError:
                print("ending up ....")
                # 注销对此socket连接的关注
                epoll_obj.unregister(fd)
                # 关闭socket连接
                connections[fd].close()
                del connections[fd]
s.close()
epoll_obj.close()

epoll实例服务端2:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import select
import socket
 
response = b''
 
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
# 因为socket默认是阻塞的,所以需要使用非阻塞(异步)模式。
serversocket.setblocking(0)
 
# 创建一个epoll对象
epoll = select.epoll()
# 在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket连接
epoll.register(serversocket.fileno(), select.EPOLLIN)
 
try:
    # 字典connections映射文件描述符(整数)到其相应的网络连接对象
    connections = {}
    requests = {}
    responses = {}
    while True:
        # 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。
        # 如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回
        events = epoll.poll(1)
        # event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
        for fileno, event in events:
            # 如果是服务端产生event,表示有一个新的连接进来
            if fileno == serversocket.fileno():
            #注意该处于epoll实例服务端1代码的区别是,这部分代码块中没有使用recv()方法,具体介绍请看本文最后小结。
                connection, address = serversocket.accept()
                print('client connected:', address)
                # 设置新的socket为非阻塞模式
                connection.setblocking(0)
                # 为新的socket注册对读(EPOLLIN)event的关注
                epoll.register(connection.fileno(), select.EPOLLIN)
                connections[connection.fileno()] = connection
                # 初始化接收的数据
                requests[connection.fileno()] = b''
 
            # 如果发生一个读event,就读取从客户端发送过来的新数据
            elif event & select.EPOLLIN:
                print("------recvdata---------")
                # 接收客户端发送过来的数据
                requests[fileno] += connections[fileno].recv(1024)
                # 如果客户端退出,关闭客户端连接,取消所有的读和写监听
                if not requests[fileno]:
                    connections[fileno].close()
                    # 删除connections字典中的监听对象
                    del connections[fileno]
                    # 删除接收数据字典对应的句柄对象
                    del requests[connections[fileno]]
                    print(connections, requests)
                    epoll.modify(fileno, 0)
                else:
                    # 一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端
                    epoll.modify(fileno, select.EPOLLOUT)
                    # 打印完整的请求,证明虽然与客户端的通信是交错进行的,但数据可以作为一个整体来组装和处理
                    print('-' * 40 + '\n' + requests[fileno].decode())
 
            # 如果一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端
            elif event & select.EPOLLOUT:
                print("-------send data---------")
                # 每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端
                byteswritten = connections[fileno].send(requests[fileno])
                requests[fileno] = requests[fileno][byteswritten:]
                if len(requests[fileno]) == 0:
                    # 一旦完整的响应数据发送完成,就不再关注写event
                    epoll.modify(fileno, select.EPOLLIN)
 
            # HUP(挂起)event表明客户端socket已经断开(即关闭),所以服务端也需要关闭。
            # 没有必要注册对HUP event的关注。在socket上面,它们总是会被epoll对象注册
            elif event & select.EPOLLHUP:
                print("end hup------")
                # 注销对此socket连接的关注
                epoll.unregister(fileno)
                # 关闭socket连接
                connections[fileno].close()
                del connections[fileno]
finally:
    # 打开的socket连接不需要关闭,因为Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯
    epoll.unregister(serversocket.fileno())
    epoll.close()
    serversocket.close()

epoll实例客户端:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket

ip_port = ("192.168.12.172", 9999)
#ip_port = ("192.168.12.172", 8080)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)

while True:
    inp = raw_input("Please input: ")
    client.sendall(inp)
    data = client.recv(1024)
    print "received: ", data
    if inp == 'exit':
        break
client.close()

小结:

  1. 使用epoll机制的脚本只能在linux系统运行,windows下不行;

  2. socket对象的setblocking方法和recv方法

    setblocking方法:

    • socket.``setblocking(flag)

      Set blocking or non-blocking mode of the socket: if flag is 0, the socket is set to non-blocking, else to blocking mode. Initially all sockets are in blocking mode. In non-blocking mode, if a recv() call doesn’t find any data, or if a send() call can’t immediately dispose of the data, an error exception is raised; in blocking mode, the calls block until they can proceed. s.setblocking(0) is equivalent to s.settimeout(0.0); s.setblocking(1) is equivalent to s.settimeout(None).

描述翻译:
setblocking方法用于设置socket对象为阻塞或非阻塞模式:如果flag的值为0,则socket对象被设置为非阻塞,否则就是阻塞模式。最初所有的socket都是阻塞模式。非阻塞模式下,如果一个recv()调用没有发现任何数据,或者一个send()调用不能立即处理数据,那么就会触发一个error异常;在阻塞模式下,调用阻塞直到它们可以继续;

演示1:

异常演示服务端,直接修改epoll实例服务端1的代码即可,修改位置如下:

s = socket.socket()
s.bind(('192.168.12.172',9999))
s.listen(5)
s.setblocking(0)#添加代码##

.....

#创建一个新套接字,并且连接到对应到客户端(ip:port)
            conn, addr = s.accept()
            conn.setblocking(0)#添加代码#
            print('client connected: ', addr)
            

报错信息:

[root@localhost ~]# python3.6 ./select_epoll_server1.py 
1
client connected:  ('192.168.12.172', 21589)
connections: {5: }
Starting receving data...
Traceback (most recent call last):
  File "./select_epoll_server1.py", line 42, in 
    msg = conn.recv(200)
BlockingIOError: [Errno 11] Resource temporarily unavailable

演示2:

服务端直接运行【epoll实例服务端1】代码即可:

演示客户端,就直接用【epoll实例客户端】代码即可;
运行服务端

运行第一个客户端:
客户端1

查看服务端输出1:

服务端输出1

运行第二个客户端:

客户端2

查看服务端输出2:

此时界面还是和输出1完全一样,原因就是这个时候epoll实例服务端1的代码阻塞在了第一个客户端的recv()方法处:

   #将客户端发送的数据接收进来
            msg = conn.recv(200)

在客户端1输入字符串:

客户端1输入1

查看服务端输出3:

此时发现走到了第二轮,打印出来的connections字典已经将客户端2加进去了。

服务端输出3

我将客户端就保持在2个,不再有新的客户端连接,只是发送和接收数据,这个时候没有任何问题,因为没有新客户连接,所以【epoll实例服务端1】的代码就不会再执行到if fd == s.fileno():

recv方法:

  • socket.``recv(bufsize[, flags])

Receive data from the socket. The return value is a string representing the data received. The maximum amount of data to be received at once is specified by bufsize. See the Unix manual page recv(2) for the meaning of the optional argument flags; it defaults to zero. Note For best match with hardware and network realities, the value of bufsize should be a relatively small power of 2, for example, 4096.

help(socket.socket().recv)命令显示的信息:

Receive up to buffersize bytes from the socket. For the optional flags argument, see the Unix manual. When no data is available, block until at least one byte is available or until the remote end is closed. When the remote end is closed and all data is read, return the empty string.

描述翻译:
主要是help信息,从socket接收数据达到buffersize指定的字节数。至于可选参数flags,请查阅Unix手册。当没有数据可以获取的时候,recv方法会阻塞到至少有一个字节的数据可以获取到或一直到远程终端关闭。当远程终端关闭的时候或者所有数据都被读取的时候,recv会返回一个空字符串。

也就是说recv方法是会阻塞的。

python查询信息查询
有3个地方,但是内容却可能不相同,查手册的时候务必注意。

  • https://docs.python.org
  • help()方法
  • 下载的官方pdf文档

参考文档:
python系列之-select

你可能感兴趣的:(Python)