網絡編程的基本組件是socket[1],包括:服務端socket和客戶端socket。其中客戶端socket只是簡單的鏈接、完成事務、斷開鏈接,而服務端socket流程多一些。html
一個小型服務器python
v1-server編程
#!/usr/bin/env python # coding=utf-8 import socket s=socket.socket() host=socket.gethostname() port=8889 s.bind((host,port)) s.listen(10) while True: conn,addr=s.accept() print "got conn from ",addr conn.send("thx for conn") conn.close()
服務端的accept方法會阻塞直到客戶端連上。ubuntu
一個小型客戶機數組
v1-client服務器
#!/usr/bin/env python # coding=utf-8 import socket s=socket.socket() host=socket.gethostname() port=8889 s.connect((host,port)) print s.recv(1024)
複雜一些的場景可使用SocketServer模塊。在其設計中,server每收到一個請求,就會實例化一個請求處理程序。網絡
v2-SocketServerapp
# 和v1-client對應 from SocketServer import TCPServer,StreamRequestHandler class Handler(StreamRequestHandler): def handle(self): addr=self.request.getpeername() print 'got connectino from ',addr self.wfile.write('thx for you connecting') server=TCPServer(('',8889),Handler) server.serve_forever()
上述兩種方案server一次只能處理一個鏈接,框架
要想同時處理多個鏈接,有三種方法:less
其中threading方式下,每一個socket交給一個線程處理,不一樣的socket通訊過程互不干擾。
v3-forking.py
from SocketServer import TCPServer,StreamRequestHandler, ForkingMixIn class Server(ForkingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): addr=self.request.getpeername() print 'got connectino from ',addr self.wfile.write('thx for you connecting') server=Server(('',8889),Handler) server.serve_forever()
v3-threading.py
from SocketServer import TCPServer,StreamRequestHandler, ThreadingMixIn class Server(ThreadingMixIn,TCPServer): pass class Handler(StreamRequestHandler): def handle(self): addr=self.request.getpeername() print 'got connectino from ',addr self.wfile.write('thx for you connecting') server=Server(('',8889),Handler) server.serve_forever()
thread方式下,每次一個客戶成功鏈接到服務器,服務器都會建立一個新線程,若是黑客控制許多client惡意鏈接服務器那極可能耗盡系統資源。怎麼辦呢?
[6]給出了一個簡單的方案:線程池(pre-allocated pool)
from socketserver import StreamRequestHandler, TCPServer class EchoHandler(StreamRequestHandler): def handle(self): print('Got connection from', self.client_address) # self.rfile is a file-like object for reading for line in self.rfile: # self.wfile is a file-like object for writing self.wfile.write(line) '''old: spawn a new process or thread on each client connection if __name__ == '__main__': serv = TCPServer(('', 20000), EchoHandler) serv.serve_forever() ''' if __name__ == '__main__': from threading import Thread NWORKERS = 16 serv = TCPServer(('', 20000), EchoHandler) for n in range(NWORKERS): t = Thread(target=serv.serve_forever) t.daemon = True t.start() serv.serve_forever()
在上面的代碼中,限定了線程數量爲NWORKERS個。避免了前面的問題。
服務器:v4-select.py
#!/usr/bin/env python # coding=utf-8 import socket, select s=socket.socket() host=socket.gethostname() port=8889 s.bind((host,port)) s.listen(5) inputs=[s] print 's',s while True: rs,ws,es=select.select(inputs,[],[]) # read write except result #rs,ws,es=select.select(inputs,[],[],0) print 'inputs ',inputs for r in rs: print 'rs ',len(rs),rs if r is s: c, addr = s.accept() print 'Got connection from',addr c.send('connect to server successful') inputs.append(c) print 'r,c ',r,c else: try: data = r.recv(1024) disconnected = not data except: disconnected = True if disconnected: print r.getpeername(),'disconnected' inputs.remove(r) else: print data
客戶端:v4-client.py
#!/usr/bin/env python # coding=utf-8 import socket s=socket.socket() host=socket.gethostname() port=8889 s.connect((host,port)) print socket.gethostname() s.send("hi, i am "+socket.gethostname()) print s.recv(1024)
運行:
啓動server
$ python v4-select.py s <socket._socketobject object at 0xb74d9d84> inputs [<socket._socketobject object at 0xb74d9d84>] rs 1 [<socket._socketobject object at 0xb74d9d84>] Got connection from ('127.0.0.1', 44464) r,c <socket._socketobject object at 0xb74d9d84> <socket._socketobject object at 0xb74d9d4c> inputs [<socket._socketobject object at 0xb74d9d84>, <socket._socketobject object at 0xb74d9d4c>] rs 1 [<socket._socketobject object at 0xb74d9d4c>] hi, i am ubuntu inputs [<socket._socketobject object at 0xb74d9d84>, <socket._socketobject object at 0xb74d9d4c>] rs 1 [<socket._socketobject object at 0xb74d9d4c>] ('127.0.0.1', 44464) disconnected
啓動client
$ python v4-client.py ubuntu connect to server successful
python中提供的select.select(rlist, wlist, xlist[, timeout])
是Unix中select()
系統調用的一個接口。[4]
rlist、wlist、xlist都是waitable對象序列,分別表示:輸入、輸出以及異常狀況的序列。
'waitable objects’: either integers representing file descriptors or objects with a parameterless method named fileno() returning such an integer
rlist: wait until ready for reading wlist: wait until ready for writing xlist: wait for an 「exceptional condition」
當不提供可選參數timeout的時候,select會阻塞,直到至少一個文件描述符爲行動作好了準備,纔會繼續運行。
當給定timeout秒數,select阻塞時間不會超過timeout秒。若是超時仍然沒有準備好的文件描述符就會返回空結果。
返回值是3個list組成的tuple,這3個list分別是3個輸入list的子集。
當timeout爲0,會給出一個連續的poll(不阻塞),設置爲0後能夠看到while True
函數體瘋狂循環。
[1]中有些話都是直接翻譯文檔的,看完跟不看同樣,略坑。
從執行的輸出能夠看到:
server端的,inputs列表開始只包含socket對象4c
第一次循環中,select從inputs中找到準備好的子集:4c
4c調用accept成功以後生成了socket對象84,而且被inputs列表添加.
第二次循環中,select從inputs中找到準備好的子集:84
84接收消息
這只是一個最簡單的演示,因此
[1]認爲:
同步的服務器解決方案:server一次只能鏈接處理一個client
要實現多個鏈接,能夠藉助forking分出多個進程,並行處理多個socket鏈接。缺點是浪費資源。
或者用threading分多個線程,每一個線程處理一個socket鏈接。缺點是須要進程同步。
異步IO能夠避免上述問題,基本機制是select模塊的select函數。
對應的[2]中認爲:
同步/異步主要針對C端,阻塞/非阻塞主要針對S端。 同步IO和異步IO的區別就在於:數據訪問的時候進程是否阻塞! 阻塞IO和非阻塞IO的區別就在於:應用程序的調用是否當即返回! Linux下的五種I/O模型 1)阻塞I/O(blocking I/O) 2)非阻塞I/O (nonblocking I/O) 3) I/O複用(select 和poll) (I/O multiplexing) 4)信號驅動I/O (signal driven I/O (SIGIO)) 5)異步I/O (asynchronous I/O (the POSIX aio_functions)) 前四種都是同步,只有最後一種纔是異步IO。
概念混亂不清,挖坑待辨析。
v4-poll.py
#!/usr/bin/env python # coding=utf-8 import socket, select s=socket.socket() host=socket.gethostname() port=8889 s.bind((host,port)) fdmap={s.fileno():s} s.listen(5) # 獲得poll對象 p=select.poll() p.register(s) while True: events=p.poll() for fd,event in events: if fd==s.fileno(): c,addr = s.accept() print 'got connection from ',addr p.register(c) fdmap[c.fileno()] = c elif event & select.POLLIN: data=fdmap[fd].recv(1024) if not data: print fdmap[fd].getpeername(),'disconnected' p.unregister(fd) del fdmap[fd] else: print data
[4]中提到:
The poll() system call, supported on most Unix systems, provides better scalability for network servers that service many, many clients at the same time. poll() scales better because the system call only requires listing the file descriptors of interest, while select() builds a bitmap, turns on bits for the fds of interest, and then afterward the whole bitmap has to be linearly scanned again. select() is O(highest file descriptor), while poll() is O(number of file descriptors).
區別在於poll() syscall的scalability更好。 poll()只維護感興趣的fd列表,可是select() syscall經過bitmap來維護整個fd列表。poll()和select()的時間複雜度分別爲O(fd數目)和O(最大fd數目).
[5]中認爲:
select最先於1983年出如今4.2BSD中,它經過一個select()系統調用來監視多個文件描述符的數組。當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程能夠得到這些文件描述符從而進行後續的讀寫操做。
poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差異,可是poll沒有最大文件描述符數量的限制。
??說好的scalability區別呢?
poll和select一樣存在一個缺點就是,包含大量文件描述符的數組被總體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增長而線性增大。
直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll,它幾乎具有了以前所說的一切優勢,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。相比select和poll有以下改進: