使用SocketServer实现简单的webserver
每个客户端访问webserver,webserver都需要产生一个线程去处理请求,不管是tcp请求还是udp请求的底层都是用socket实现的,这十分适合用SocketServer模块去实现
python2.x 有一个模块叫SimpleHTTPServer,能实现一个简单的展示型的webserver
[root@web socket_py]# cat simplewebserver.py #!/usr/bin/env python import SimpleHTTPServer import SocketServer host = '' port = 80 Handler = SimpleHTTPServer.SimpleHTTPRequestHandler httpd = SocketServer.TCPServer((host, port), Handler) print "(serving at port: {0})".format(port) httpd.serve_forever() -----------------------------------------------------------> [root@web socket_py]# python simplewebserver.py (serving at port: 80) DESKTOP-I55CB3B.lan - - [06/Jan/2018 13:02:32] "GET / HTTP/1.1" 200 - DESKTOP-I55CB3B.lan - - [06/Jan/2018 13:02:32] code 404, message File not found DESKTOP-I55CB3B.lan - - [06/Jan/2018 13:02:32] "GET /favicon.ico HTTP/1.1" 404 - DESKTOP-I55CB3B.lan - - [06/Jan/2018 13:02:32] code 404, message File not found DESKTOP-I55CB3B.lan - - [06/Jan/2018 13:02:32] "GET /favicon.ico HTTP/1.1" 404 -
python3下,SimpleHTTPServe模块已经被合并到http.server了
#!/usr/bin/env python #coding:utf8 # The SimpleHTTPServer module has been merged into http.server in Python 3 # SimpleHTTPServer模块在pythotn3中已经被整合到http.server中 #http是一个包 from http import server import socketserver PORT = 8000 Handler = server.SimpleHTTPRequestHandler with socketserver.TCPServer(("", PORT), Handler) as httpd: print("serving at port", PORT) httpd.serve_forever()
select模型
在默认阻塞模式下的socket编程,recv会一直阻塞,直到有数据返回
select模型是Winsock中最常见的I/O模型,使用select函数,把所有套接字加入到列表中,然后监控套接字是否发生数据的读写,如果有活动连接,select会遍历所有的套接字,找出活动的套接字。select模型是非阻塞的
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销
poll模型和select模型在本质上没有多大差别,但是poll没有最大文件描述符数量的限制
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll模型。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持
epoll模型本身没有最大文件描述符限制,受限于系统的最大文件描述符
epoll模型效率更高,例如监控100个套接字,有两个活跃(发生数据读写),epoll模型会通知这两个活跃可以操作,而select模型需要线性遍历所有的套接字
使用select模型和socket模块开发的聊天室
聊天室服务端:
- 监听和接收多个客户端的连接
- 从每个客户端读取消息并广播到其他客户端
[root@web socket_py]# cat selectserver.py #!/usr/bin/env python #coding:utf8 import socket import select class SelectServer(object): def __init__(self, host, port, backlog): self.host = host self.port = port self.backlog = backlog self.socketlist= list() self._initSocket() def _initSocket(self): self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server.bind((self.host, self.port)) self.server.listen(self.backlog) self.socketlist.append(self.server) //把主socket加入到监听列表中 print ('已开启群聊') def broadcast(self, socketobject, content): //广播消息到除了自身和主socket的其他客户端上 for i in self.socketlist: if i != socketobject and i != self.server: try: i.sendall(content) except Exception as e: i.close() self.socketlist.remove(i) def monitor(self): while 1: rlist, wlist, xlist = select.select(self.socketlist, list(), list()) //当select返回时,说明rlist有数据可读 for r in rlist: if r == self.server: //如果是主socket(self.server)上有数据可读,表示有新的客户端连接上服务端 conn, addr = self.server.accept() //服务端给该客户端分配一个专属的socket self.socketlist.append(conn) //把专属socket加入到监听列表 print ('{0}已进入群聊'.format(addr[0])) self.broadcast(conn, '{0}已进入群聊'.format(addr[0])) else: //表示已经建立连接的socket有数据可读 try: data = r.recv(2048) if data: self.broadcast(r, '\n{0}\n>>>{1}'.format(addr[0],data)) except Exception as e: //这段异常触发不了,试过换成else,触发顺序错乱且不完整,暂时不知道怎么搞 print ('{0}退出了群聊'.format(addr[0])) self.broadcast(r, '{0}退出了群聊'.format(addr[0])) r.close() self.socketlist.remove(r) self.server.close() if __name__ == '__main__': host = '192.168.123.105' port = 12345 backlog = 5 selectServer = SelectServer(host, port, backlog) selectServer.monitor()
聊天室客户端:
- 监听服务端有没有数据发送过来(self.client)
- 读取用户输入,发送到服务端
[root@web socket_py]# cat selectclient.py #!/usr/bin/env python import socket import select import sys class SelectClient(object): def __init__(self, host, port): self.host = host self.port = port self._initSocket() def _initSocket(self): self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.client.connect((self.host, self.port)) except Exception as e: print ('Unable to connect, {0}'.format(e)) sys.exit() def send(self): while 1: rlist = [sys.stdin, self.client] //这里有两个I/O事件需要监控,分别是连接到服务端的socket对象和标准输入 rlist, wlist, xlist = select.select(rlist, list(), list()) //select有返回,说明rlist有数据可读 for r in rlist: if r == self.client: //从服务端接收的数据 data = r.recv(2048) if data: sys.stdout.write(data+'\n') else: continue else: //从键盘读取的数据 data = raw_input('{0}: '.format(socket.gethostname())) self.client.sendall(data) self.client.close() if __name__ == '__main__': host = '192.168.123.105' port = 12345 selectClient = SelectClient(host, port) selectClient.send()
注意:聊天室客户端代码不能在 windows 下运行,因为代码使用 select 同时监听 socket 和输入流,在 Windows 下 select 函数是由 WinSock 库提供,不能处理不是由 WinSock 定义的文件描述符