python網絡編程中socket、select和poll的簡單使用

網絡編程的基本組件是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)

1 使用SocketServer框架的服務器

複雜一些的場景可使用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()

2 多個鏈接

上述兩種方案server一次只能處理一個鏈接,框架

要想同時處理多個鏈接,有三種方法:less

  • forking
  • threading
  • asnchronous I/O

其中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個。避免了前面的問題。

3 使用select的簡單服務器

服務器: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接收消息

這只是一個最簡單的演示,因此

[2]中的一些概念和[1]不一樣,這使人困惑。

[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。

概念混亂不清,挖坑待辨析。

4 使用poll的簡單服務器

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.1 select和poll的異同

[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有以下改進:

  1. 省掉文件描述符於用戶態和內核的地址空間之間來回複製的開銷
  2. 採用基於事件的就緒通知方式

References

  1. pyhton基礎教程(第2版)
  2. socket阻塞與非阻塞,同步與異步、I/O模型
  3. https://docs.python.org/2/library/socket.html
  4. https://docs.python.org/2/library/select.html
  5. http://www.cnblogs.com/coser/archive/2012/01/06/2315216.html
  6. http://chimera.labs.oreilly.com/books/1230000000393/ch11.html#_discussion_1
相關文章
相關標籤/搜索