1. 程式人生 > >Python的socket模組詳解

Python的socket模組詳解

最近在學習Python看了一篇文章寫得不錯,是在指令碼之家裡的,原文如下,很有幫助:

一、網路知識的一些介紹

socket 是網路連線端點。例如當你的Web瀏覽器請求www.jb51.net上的主頁時,你的Web瀏覽器建立一個socket並命令它去連線www.jb51.net的Web伺服器主機,Web伺服器也對來自的請求在一個socket上進行監聽。兩端使用各自的socket來發送和接收資訊。

在使用的時候,每個socket都被繫結到一個特定的IP地址和埠。IP地址是一個由4個數組成的序列,這4個數均是範圍0~255中的值(例如,220,176,36,76);埠數值的取值範圍是0~65535。埠數小於1024的都是為眾所周知的網路服務所保留的(例如Web服務使用的80埠);最大的保留數被儲存在socket模組的IPPORT_RESERVED變數中。你也可以為你的程式使用另外的埠數值。

不是所有的IP地址都對世界的其它地方可見。實際上,一些是專門為那些非公共的地址所保留的(比如形如192.168.y.z或10.x.y.z)。地址127.0.0.1是本機地址;它始終指向當前的計算機。程式可以使用這個地址來連線執行在同一計算機上的其它程式。

IP地址不好記,你可以花點錢為特定的IP地址註冊一個主機名或域名(比如使用www.jb51.net代替222.76.216.16)。域名伺服器(DNS)處理名字到IP地址的對映。每個計算機都可以有一個主機名,即使它沒有在官方註冊。

多少資訊通過一個網路被傳送基於許多因素,其中之一就是使用的協議。許多的協議是基於簡單的、低階協議以形成一個協議棧。例如HTTP協議,它是用在Web瀏覽器與Web伺服器之間通訊的協議,它是基於TCP協議,而TCP協議又基於IP協議。

當在你自己的兩個程式間傳送資訊的時候,你通常選擇TCP或UDP協議。TCP協議在兩端間建立一個持續的連線,並且你所傳送的資訊有保證的按順序到達它們的目的地。UDP不建立連線,它的速度快但不可靠。你傳送的資訊也可能到不了另一端;或它們沒有按順序到達。有時候一個資訊的多個複製到達接收端,即使你只發送了一次。

二、使用地址和主機名

socket模組提供了幾個函式用於使用主機名和地址來工作。

gethostname()返回執行程式所在的計算機的主機名:

>>> importsocket
>>>socket.gethostname()
'lenovo'

gethostbyname(name)嘗試將給定的主機名解釋為一個IP地址。首先將檢查當前計算機是否能夠解釋。如果不能,一個解釋請求將傳送給一個遠端的DNS伺服器(遠端的DNS伺服器還可能將解釋請求轉發給另一個DNS伺服器,直到該請求可以被處理)。gethostbyname函式返回這個IP地址或在查詢失敗後引發一個異常。

>>>socket.gethostbyname('lenovo')
'192.168.1.4'
>>>socket.gethostbyname('www.jb51.net')
'222.76.216.16'

一個擴充套件的形式是gethostbyname_ex(name),它返回一個包含三個元素的元組,分別是給定地址的主要的主機名、同一IP地址的可選的主機名的一個列表、關於同一主機的同一介面的其它IP地址的一個列表(列表可能都是空的)。

>>>socket.gethostbyname('www.163.com')
'60.191.81.49'
>>>socket.gethostbyname_ex('www.163.com')
('www.cache.split.netease.com', ['www.163.com'], ['60.191.81.48','60.191.81.49
, '60.191.81.50', '60.191.81.51', '60.191.81.52', '60.191.81.53','60.191.81.54
, '220.181.28.50', '220.181.28.51', '220.181.28.52','220.181.28.53', '220.181.
8.54', '220.181.31.182', '220.181.31.183', '220.181.31.184'])

gethostbyaddr(address)函式的作用與gethostbyname_ex相同,只是你提供給它的引數是一個IP地址字串:

>>>socket.gethostbyaddr('202.165.102.205')
('homepage.vip.cnb.yahoo.com', ['www.yahoo.com.cn'],['202.165.102.205'])

getservbyname(service,protocol)函式要求一個服務名(如'telnet'或'ftp')和一個協議(如'tcp'或'udp'),返回服務所使用的埠號:

>>>socket.getservbyname('http','tcp')
80
>>>socket.getservbyname('telnet','tcp)
23

通常,非Python程式以32位位元組包的形式儲存和使用IP地址。inet_aton(ip_addr)和inet_ntoa(packed)函式在這個形式和IP地址間作轉換:

>>>socket.inet_aton('222.76.216.16')
'\xdeL\xd8\x10'
>>>socket.inet_ntoa('\xdeL\xd8\x10')
'222.76.216.16'

socket也定義了一些變數來代表保留的IP地址。INADDR_ANY和INADDR_BROADCAST是被保留的IP地址分別代表任意IP地址和廣播地址;INADDR_LOOPBACK代表loopback裝置,總是地址127.0.0.1。這些變數是32位位元組數字形式的。

getfqdn([name])函式返回關於給定主機名的全域名(如果省略,則返回本機的全域名)。

三、使用低階的socket通訊

儘管Python提供了一些封裝,使得使用socket更容易,但是你也可以直接使用socket來工作。

1、建立和銷燬socket

socket模組中的socket(family,type[,proto])函式建立一個新的socket物件。family的取值通常是AF_INET。type的取值通常是SOCK_STREAM(用於定向的連線,可靠的TCP連線)或SOCK_DGRAM(用於UDP):

>>> from socketimport *
>>>s=socket(AF_INET,SOCK_STREAM)

family和type引數暗指了一個協議,但是你可以使用socket的第三個可選的引數(proto的取值如IPPROTO_TCP或IPPROTO_RAW)來指定所使用的協議。代替使用IPPROTO_XX變數,你可以使用函式getprotobyname:

>>>getprotobyname('tcp')
6
>>> IPPROTO_TCP
6

fromfd(fd,type[,proto])是一個很少被使用的函式,它用來從開啟的一個檔案描述符建立一個socket物件(檔案描述符由檔案的fileno()方法返回)。檔案描述符與一個真實的socket連線,而非一個檔案。socket物件的fileno()方法返回關於這個socket的檔案描述符。

當你使用完工socket物件時,你應呼叫close()方法顯式的關閉socket以儘快釋放資源(儘管socket被垃圾回收器回收時將自動被關閉)。另外,你也可以使用shutdown(how)方法來關閉連線一邊或兩邊。引數0阻止socket接收資料,1阻止傳送,2阻止接收和傳送。

2、連線socket

當兩個socket連線時(例如使用TCP),一端監聽和接收進來的連線,而另一端發起連線。臨聽端建立一個socket,呼叫bind(address)函式去繫結一個特定的地址和埠,呼叫listen(backlog)來臨聽進來的連線,最後呼叫accept()來接收這個新的,進來的連線,下面是在伺服器端的程式碼:

>>>s=socket(AF_INET,SOCK_STREAM)
>>>s.bind(('127.0.0.1',44444))
>>> s.listen(1)
>>> q,v=s.accept()#返回socket q和地址v

注意:上面的程式碼將一直處於等待直到連線被建立。下面我們再開啟另一個Python直譯器,用作客戶端;然後鍵入如下程式碼:
>>> from socket import*
>>>s=socket(AF_INET,SOCK_STREAM)
>>>s.connect(('127.0.0.1',44444) #發起連線

好了,我們驗證一下連線是否建立了。我們在伺服器端鍵入以下程式碼來發送一條資訊:

>>> q.send('hello,icome from pythontik.com') 注:有時可能出現send() argument 1 must be stringor buffer,not str 錯誤,原因可能是您的機器不支援UTF-8字符集,臨時解決方案是q.send(b'hello...')
31 #傳送的位元組數

在客戶端鍵入以下程式碼來接收資訊:
>>> s.recv(1024)
'hello,i come from pythontik.com'

你傳遞給bind和connect的地址是一個關於AF_INET的socket的元組(ipAddress,port)。代替connect,你也可以呼叫connect_ex(address)方法。如果背後對C的connect的呼叫返回一個錯誤,那麼connect_ex也將返回一個錯誤(否則返回0代表成功),代替引發一個異常。

當你呼叫listen時,你給了它一個引數,這個數值表示在等待佇列中允許放置的進來的連線總數。當等待佇列已滿時,如果有更多的連線到達,那麼遠端端將被告知連線被拒絕。在socket模組中的SOMAXCONN變量表明瞭等待佇列所能容納的最大量。

accept()方法返回形如bind和connect的一個地址,代表遠端socket的地址。下面顯示變數v的值:

>>> v
('127.0.0.1', 1334)

UDP是不定向的連線,但是你仍然可以使用給定的目的地址和埠來呼叫connect去關聯一個socket。

3、傳送和接收資料

函式send(string[,flags])傳送給定的字串到遠端socket。sendto(string[,flags],address)傳送給定的字串到一個特定的地址。通常,send方法用於可靠連線的socket,sendto方法用於不可靠連線的socket,但是如果你在一個UDP socket上呼叫connect來使它與一個特定的目標建立聯絡,那麼這時你也可以使用send方法來代替sendto。

send和sendto都返回實際傳送的位元組數。當你快速傳送大量的資料的時候,你可能想去確保全部資訊已被髮送,那麼你可以使用如下的一個函式:

def safeSend(sock,msg):
sent=0
while msg:
i=sock.send(msg)
if i==-1: #發生了錯誤
return -1
sent+=i
msg=msg[i:]
time.sleep(25)
return sent

recv(bufsize[,flags])方法接收一個進來的訊息。如果有大量的資料在等待,它只返回前面的bufsize位元組數的資料。recvfrom(bufsize[,flags])做同樣的事,除了它使用AF_INETsocket的返回值是(data,(ipAddress,port)),這便於你知道訊息來自哪兒(這對於非連線的socket是有用的)。

send,sendto,recv和recvfrom方法都有一個可選的引數flags,預設值為0。你可以通過對socket.MSG_*變數進行組合(按位或)來建立flags的值。這些值因平臺而有所不同,但是最通用的值如下所示:

MSG_OOB:處理帶外資料(既TCP緊急資料)。
MSG_DONTROUTE:不使用路由表;直接傳送到介面。
MSG_PEEK:返回等待的資料且不把它們從佇列中刪除。

例如,如果你有一個開啟的socket,它有一個訊息等待被接收,你可以接收這個訊息後並不把它從進來的資料的佇列中刪除:

>>>q.recv(1024,MSG_PEEK)
'hello'
>>>q.recv(1024,MSG_PEEK) #因為沒有刪除,所以你可以再得到它。
'hello'

makefile([mode[,bufsize]])方法返回一個檔案類物件,其中封裝了socket,以便於你以後將它傳遞給要求引數為一個檔案的程式碼(或許你喜歡使用檔案的方法來代替send和recv)。這個可選的mode和bufsize引數的取值和內建的open函式一樣。

4、使用socket選項

socket物件的getpeername()和getsockname()方法都返回包含一個IP地址和埠的二元組(這個二元組的形式就像你傳遞給connect和bind的)。getpeername返回所連線的遠端socket的地址和埠,getsockname返回關於本地socket的相同資訊。

在預設情況下,socket是阻塞式的,意思就是socket的方法的呼叫在任務完成之前是不會返回的。例如,如果儲存向外傳送的資料的快取已滿,你又企圖傳送更多的資料,那麼你對send的呼叫將被阻塞直到它能夠將更多的資料放入快取。你可以通過呼叫setblocking(flag)方法(其中flag取值是0,setblocking(0))來改變這個預設行為,以使socket為非阻塞式。當socket為非阻塞式的時候,如果所做的動作將導致阻塞,將會引起error異常。下面一段程式碼將試圖不斷地接受新的連線並使用函式processRequest來處理。如果一個新連線無效,它將間隔半秒再試。另一方法是在你的監聽socket上呼叫select或poll來檢測一個新的連線的到達。

別的socket的選項可以使用setsockopt(level,name,value)和getsockopt(level,name[,buflen])方法來設定和獲取。socket代表了一個協議棧的不同層,level引數指定了選項應用於哪一層。level的取值以SOL_開頭(SOL_SOCKET,SOL_TCP等等)。name表明你涉及的是哪個選項。對於value,如果該選項要求數值的值,value只能傳入數字值。你也可以傳遞入一個快取(一個字串),但你必須使用正確的格式。對getsockopt,不指定buflen引數意味你要求一個數字值,並返回這個值。如果你提供了buflen,getsockopt返回代表一個快取的字串,它的最大長度是buflen的位元組數。下面的例子設定了一個socket的用於傳送的快取尺寸為64KB:

>>>s=socket(AF_INET,SOCK_STREAM)
>>>s.setsockopt(SOL_SOCKET,SO_SNDBUF,65535)

要得到一個包在被路由丟棄前所能有的生命週期(TTL)和跳數,你可以使用如下程式碼:

>>>s.getsockopt(SOL_IP,IP_TTL)
32

5、數值轉換

由於不同平臺的位元組順序不一樣,所以當在網路中傳輸資料時我們使用標準的網路位元組順序。nthol(x)和ntohs(x)函式要求一個網路位元組順序的數值並把它轉換為當前主機位元組順序的相同數值,而htonl(x)和htons(x)則相反:

>>>import.socket
>>>socket.htons(20000) #轉換為一個16位的值
8270
>>>socket.htonl(20000) #轉換為一個32位的值
541982720
>>>socket.ntohl(541982720)
20000

使用SocketServers

SocketServers模組為一組socket服務類定義了一個基類,這組類壓縮和隱藏了監聽、接受和處理進入的socket連線的細節。

1、SocketServers家族
TCPServer和UDPServer都是SocketServer的子類,它們分別處理TCP和UDP資訊。
注意:SocketServer也提供UnixStreamServer(TCPServer的子類)和UNIXdatagramServer(UDPServer的子類),它們都如同其父類一樣除了在建立監聽socket時使用AF_UNIX代替了AF_INET。

默 認情況下,socket服務一次處理一個連線,但是你可以使用ThreadingMixIN和ForkingMixIn類來建立任一SocketServer的執行緒和子程序。實際上,SocketServer模組提供了一些對些有用的類來解決你的麻煩,它們是:ForkingUDPServer、ForkingTCPServer、ThreadingUDPServer、ThreadingTCPServer、ThreadingUnixStreamServer和ThreadingUnixDatagramServer。

SocketServer以通常的方法處理進入的連線;要使它更有用,你應該提供你自己的請求處理器類給它以便它傳遞一個socket去處理。SocketServer模組中的BaseRequestHandler類是所有請求處理器的父類。假設,例如你需要寫一個多執行緒的電子郵件伺服器,首先你要建立一個MailRequestHandler,它是BaseRequestHandler的子類,然後把它傳遞給一個新建立的SocketServer:
import SocketServer
...#建立你的MailRequestHandler
addr=('220.172.20.6',25) #監聽的地址和埠
server=SocketServer.ThreadingTCPServer(addr,MailRequestHandler)
server.serve_forever()

每次一個新的連線到來時,這個server建立一個新的MailRequestHandler例項並呼叫它的handle()方法來處理這個新的請求。因為server繼承自ThreadingTCPServer,對於每個新的請求它都啟動一個單獨的執行緒來處理這個請求,以便於多個請求能夠被同時處理。如果用handle_request()代替server_forever,它將一個一個的處理連線請求。server_forever只是反覆呼叫 handle_request而已。

一般來說,你只需使用socket服務之一,但是如果你需要建立你自己的子類的話,你可以覆蓋我們下面提到的方法來定製它。

當服務被第一次建立的時候,__init__函式呼叫server_bind()方法來繫結監聽socket(self.socket)到正確的地址(self.server_address)。然後呼叫server_activate()來啟用這個服務(預設情況下,呼叫socket的listen方法)。

這個socket服務不做任何事情直到呼叫了handle_request或serve_forever方法。handle_request呼叫get_request()去等待和接收一個新的socket連線,然後呼叫verify_request(request,client_address)去看服務是否會處理這個連線(你可以在訪問控制中使用這個,預設情況下verify_request總是返回true)。如果會處理這個請求,handle_request然後呼叫process_request(request,client_address),如果process_request(request,client_address)導致一個異常的話,將呼叫handle_error(request,client_address)。預設情況下,process_request簡單地呼叫finish_request(request,client_address);子程序和執行緒類覆蓋了這個行為去開始一新的程序或執行緒,然後呼叫finish_request。finish_request例項化一個新的請求處理器,請求處理器輪流呼叫它們的handle()方法。

當SocketServer建立一個新的請求處理器時,它傳遞給這個處理器的__init__函式的self變數,以便於這個處理器能夠訪問關於這個服務的資訊。

SocketServer的fileno()方法返回監聽socket的檔案描述符。address_family成員變數指定了監聽socket的socket族(如AF_INET),server_address包含了監聽socket被繫結到的地址。socket變數包含監聽socket自身。

2、請求處理器

請求處理器有setup()、handle()和finish()方法,你可以覆蓋它們來定製你自己的行為。一般情況下,你只需要覆蓋handle方法。BaseRequestHandler的__init__函式呼叫setup()方法來做初始化的工作,handle()服務於請求,finish()用於執行清理工作,如果handle或setup導致一個異常,finish不會被呼叫。記住,你的請求處理器會為每個請求建立一個新的例項。

request成員變數有關於流(TCP)服務的最近接受的socket;對於資料報服務,它是一個包含進入訊息和監聽socket的元組。client_address包含傳送者的地址,server有對SocketServer的一個引用(通過這你可以訪問它的成員,如server_address)。

下面的例子實現了一個EchoRequestHandler,這作為一個服務端它將客戶端所傳送的資料再發送回客戶端:

>>> importSocketServer
>>> classEchoRequestHandler(SocketServer.BaseRequestHandler):
... def handle(self):
... print 'Got new connection!'
... while 1:
... mesg=self.request.recv(1024)
... if not msg:
... break
... print 'Received:',msg
... self.request.send(msg)
... print 'Done with connection'
>>>server=SocketServer.ThreadingTCPServer(('127.0.0.1',12321),EchoReuestHandler)
>>>server.handle_request() #執行後將等待連線
Got new connection!
Received: Hello!
Received: I like Tuesdays!
Done with connection

開啟另一個Python直譯器作為客戶端,然後執行如下程式碼:

>>> from socketimport *
>>>s=socket(AF_INET,SOCK_STREAM)
>>>s.connect(('120.0.0.1',12321))
>>>s.send('Hello!')
6
>>> prints.recv(1024)
Hello!
>>> s.send('I likeTuesdays!')
16
>>> prints.recv(1024)
I like Tuesdays!
>>> s.close()

SocketServer 模組也定義了BaseRequestHandler的兩個子類:StreamRequestHandler和DatagramRequestHandler。它們覆蓋了setup和finish方法並建立了兩個檔案物件rfile和wfile,你可以用這兩個檔案物件來向客戶端讀寫資料,從而代替使用socket方法。

socket的阻塞或同步程式設計


一、使用socket

網 絡程式設計中最基本的部分就是socket(套接字)。socket有兩種:服務端socket和客戶端socket。在你建立了一個服務端socket之後,你告訴它去等待連線。然後它將監聽某個網路地址(形如:xxx.xxx.xxx.xxx:xxx)直到客戶端連線。然後這兩端就可以通訊了。

處理客戶端socket通常比處理服務端socket要容易一點,因為服務端必須時刻準備處理來自客戶端的連線,並且它必須處理多個連線,而客戶端只需要簡單的連線,然後做點什麼,然後斷開連線。

實 例化一個socket時,可以指定三個引數:地址系列(預設為socket.AF_INET)、流socket(這是個預設 值:socket.SOCK_STREAM)或資料報socket(socket.SOCK_DGRAM)、協議(預設值是0)。對於簡單的socket,你可以不指定任何引數而全部使用預設值。

服務端socket在使用bind方法之後呼叫listen方法去監聽一個給定的地址。然後,客戶端socket就可以通過使用connect方法(connect方法所使用的地址引數與bind相同)去連線服務端。listen方法要求一個引數,這個引數就是等待連線佇列中所能包含的連線數。

一旦服務端socket呼叫了listen方法,就進入了臨聽狀態,然後通常使用一個無限的迴圈:1、開始接受客房端的連線,這通過呼叫accept方法來實現。呼叫了這個方法後將處於阻塞狀態(等待客戶端發起連線)直到一個客戶端連線,連線後,accept返回形如(client,address)的一個元組,其中client是一個用於與客戶端通訊的socket,address是客戶端的形如xxx.xxx.xxx.xxx:xxx的地址;2、然後服務端處理客戶端的請求;3、處理完成之後又呼叫1。

關於傳輸資料,socket有兩個方法:send和recv。send使用字串引數傳送資料;recv引數是位元組數,表示一次接受的資料量,如果你不確定一次該接受的資料量的話,最好使用1024。

下面給出一個最小的伺服器/客戶機的例子:

服務端:

import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
s.listen(5)
while True:
c, addr = s.accept()
print 'Got connection from', addr
c.send('Thank you for connecting')
c.close()

客戶端:

import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.connect((host, port))
print s.recv(1024)

注意:如果你使用Ctrl-C來停止服務端的話,如果再次使用相同的埠可能需要等待一會兒。

二、使用SocketServer

SocketServer模組簡單化了編寫網路伺服器的工作。
它提供了四個基本的服務類:TCPServer(使用TCP協議)、UDPServer(使用資料報)、UnixStreamServer、

UnixDatagramServer。UnixStreamServer和UnixDatagramServer用於類Unix平臺。
這四個類處理請求都使用同步的方法,也就是說,在下一個請求處理開始之前當前的請求處理必須已完成

用SocketServer建立一個伺服器需要四步:

1、通過子類化BaseRequestHandler類和覆蓋它的handle()方法來建立一個請求處理器類,用於處理進來

的請求;
2、例項化服務類如TCPServer,並傳遞給它引數:伺服器地址和請求處理器類;
3、呼叫服務例項物件的handle_request()或serve_forever()方法去處理請求。

下面使用SocketServer用同步的方法寫一個最簡單的伺服器:

from SocketServer import TCPServer, StreamRequestHandler
#第一步。其中StreamRequestHandler類是BaseRequestHandler類的子類,它為流socket定義了
#rfile和wfile方法
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print 'Got connection from', addr
self.wfile.write('Thank you for connecting')

#第二步。其中''代表執行伺服器的主機
server = TCPServer(('', 1234), Handler)
#第三步。serve_forever()導致進入迴圈狀態
server.serve_forever()

注意:使用阻塞或同步的方法一次只能連線一個客戶端,處理完成後才能連線下一個客戶端。

非阻塞或非同步程式設計