多路复用(select、epoll)实现tcp服务
-------------------------------多路复用的服务器(select)-------------------------------
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。
缺点:
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。
应用:
#import select:对应import包的引用操作
#readable,writeable,exceptinal=select.select(inputs,[],[]):使用select对套接字组成的列表进行遍历,过滤出对应的未堵塞的套接字
python代码示例:
1 #coding=utf-8 2 3 #引用对应的包 4 from socket import * 5 6 import select 7 8 import sys 9 10 #函数:main 11 def main(): 12 #创建套接字 13 serTcpSocket=socket(AF_INET,SOCK_STREAM) 14 15 #绑定端口和ip,sys.argv[1] 运行时传递的参数 16 serTcpSocket.bind(("",int(sys.argv[1]))) 17 18 #打开被动监听 19 serTcpSocket.listen(5) 20 21 print("-----服务器开启-----") 22 23 #创建列表,存储对应的套接字 24 inputs=[serTcpSocket] 25 26 #循环 27 while True: 28 #使用select进行对应套接字的处理过滤 29 readList,writeList,exceptList=select.select(inputs,[],[]) 30 31 #遍历readList列表进行操作 32 for sockItem in readList: 33 #如果为服务器套接字,进行accept()数据的接收 34 if sockItem==serTcpSocket: 35 #监听接受客户端传递过来的数据信息 36 newSocket,destAddr=sockItem.accept() 37 38 print("客户端(%s)以上线"%str(destAddr)) 39 40 #将用于与客户端通信的套接字进行存储 41 inputs.append(newSocket) 42 43 else: 44 #进行客户端发送过来的数据的接收 45 recvData=sockItem.recv(1024) 46 47 #进行判断,如果传递过来的数据不为空 48 if len(recvData)>0: 49 #进行客户端发送过来的数据的打印操作 50 print("客户端(%s):%s"%(str(destAddr),recvData)) 51 else: 52 #表示客户端下线 53 print("客户端(%s)以下线!"%str(destAddr)) 54 55 #进行对应该客户端套接字的关闭操作 56 sockItem.close() 57 58 #将该套接字从对应的列表中移除 59 inputs.remove(sockItem) 60 61 #程序入口 62 if __name__=="__main__": 63 main()
-------------------------------多路复用的服务器(epoll)-------------------------------
优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select。
应用:
#import select:引用select包
#epoll=select.epoll():创建epoll对象
#soc.fileno():获取套接字对应的文件描述符
#epoll.register(soc.fileno(),select.EPOLLIN|select.EPOLLET):将创建的套接字添加到epoll的事件监听
EPOLLIN (可读)
EPOLLOUT (可写)
EPOLLET (ET模式)
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。
ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。
#epollList=epoll.poll():进行已注册套接字的扫描操作
#epoll.unregister(soc.fileno()):从epoll对象中注销该套接字
1 #coding=utf-8 2 3 #引用对应的包 4 from socket import * 5 6 import select 7 8 import sys 9 10 11 #函数:main 12 def main(): 13 #创建服务器的套接字 14 serTcpSocket=socket(AF_INET,SOCK_STREAM) 15 16 #进行端口和ip的绑定操作 17 serTcpSocket.bind(("",int(sys.argv[1]))) 18 19 #开启被动,进行监听 20 serTcpSocket.listen(10) 21 22 #创建一个epoll对象 23 epoll=select.epoll() 24 25 #使用epoll对套接字在操作系统中进行注册 26 epoll.register(serTcpSocket.fileno(),select.EPOLLIN|select.EPOLLET) 27 28 #创建两个字典 29 #根据套接字的文件标识符对应套接字 30 connection={} 31 32 #根据套接字的文件标识符对应ip和端口元组信息 33 address={} 34 35 #提示服务开启 36 print("------服务开启-----") 37 38 #进行循环遍历 39 while True: 40 #接收对象中,未阻塞的套接字 41 epollList = epoll.poll() 42 43 for fd,event in epollList: 44 #判断是否为服务器的套接字 45 if fd==serTcpSocket.fileno(): 46 #接收客户端传递过来的数据信息 47 newSocket,destAddr=serTcpSocket.accept() 48 49 print("客户端(%s)以上线"%str(destAddr)) 50 51 #将对应的数据向字典中进行存储 52 connection[newSocket.fileno()]=newSocket 53 address[newSocket.fileno()]=destAddr 54 55 #将套接字在对应的epoll对象中进行注册 56 epoll.register(newSocket.fileno(),select.EPOLLIN|select.EPOLLET) 57 58 elif event==select.EPOLLIN: 59 #在字典中取出对应的套接字对象和地址(ip和端口) 60 soc=connection[fd] 61 addr=address[fd] 62 63 #接收客户端发送过来的数据信息 64 recvData=soc.recv(1024) 65 66 #判断客户端是否下线 67 if len(recvData)>0: 68 #进行打印 69 print("客户端(%s):%s"%(str(addr),recvData)) 70 71 #echo服务,将信息回发到客户端 72 soc.send(recvData) 73 else: 74 print("客户端(%s)以下线"%str(addr)) 75 76 #进行该套接字在epoll中的注销操作 77 epoll.unregister(fd) 78 79 #关闭该套接字 80 soc.close() 81 82 83 84 85 #程序入口 86 if __name__=="__main__": 87 main()