socket網絡編程

1、網絡知識的一些介紹
socket是網絡鏈接端點。例如當你的Web瀏覽器請求www.pythontik.com上的主頁時,你的Web瀏覽器建立一個socket並命令它去鏈接www.pythontik.com的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.pythontik.com代替222.76.216.16)。域名服務器(DNS)處理名字到IP地址的映射。每一個計算機均可以有一個主機名,即便它沒有在官方註冊。
多少信息經過一個網絡被傳送基於許多因素,其中之一就是使用的協議。許多的協議是基於簡單的、低級協議以造成一個協議棧。例如HTTP協議,它是用在Web瀏覽器與Web服務器之間通訊的協議,它是基於TCP協議,而TCP協議又基於IP協議。
當在你本身的兩個程序間傳送信息的時候,你一般選擇TCP或UDP協議。TCP協議在兩端間創建一個持續的鏈接,而且你所發送的信息有保證的按順序到達它們的目的地。UDP不創建鏈接,它的速度快但不可靠。你發送的信息也可能到不了另外一端;或它們沒有按順序到達。有時候一個信息的多個複製到達接收端,即便你只發送了一次。
2、使用地址和主機名
socket模塊提供了幾個函數用於使用主機名和地址來工做。
gethostname()返回運行程序所在的計算機的主機名:
>>> import socket
>>> socket.gethostname()
'lenovo'
gethostbyname(name)嘗試將給定的主機名解釋爲一個IP地址。首先將檢查當前計算機是否可以解釋。若是不能,一個解釋請求將發送給一個遠程的DNS服務器(遠程的DNS服務器還可能將解釋請求轉發給另外一個DNS服務器,直到該請求能夠被處理)。gethostbyname函數返回這個IP地址或在查找失敗後引起一個異常。
>>> socket.gethostbyname('lenovo')
'192.168.1.4'
>>> socket.gethostbyname('www.pythontik.com')
'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])函數返回關於給定主機名的全域名(若是省略,則返回本機的全域名)。
3、使用低級的socket通訊
儘管Python提供了一些封裝,使得使用socket更容易,可是你也能夠直接使用socket來工做。
一、建立和銷燬socket
socket模塊中的socket(family,type[,proto])函數建立一個新的socket對象。family的取值一般是AF_INET。type的取值一般是SOCK_STREAM(用於定向的鏈接,可靠的TCP鏈接)或SOCK_DGRAM(用於UDP):
>>> from socket import *
>>> 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阻止接收和發送。
二、鏈接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,i come from pythontik.com')
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。
三、發送和接收數據
函數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_INET socket的返回值是(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選項
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
五、數值轉換
因爲不一樣平臺的字節順序不同,因此當在網絡中傳輸數據時咱們使用標準的網絡字節順序。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鏈接的細節。
一、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自身。
二、請求處理器
請求處理器有setup()、handle()和finish()方法,你能夠覆蓋它們來定製你本身的行爲。通常狀況下,你只須要覆蓋handle方法。BaseRequestHandler的__init__函數調用setup()方法來作初始化的工做,handle()服務於請求,finish()用於執行清理工做,若是handle或setup致使一個異常,finish不會被調用。記住,你的請求處理器會爲每一個請求建立一個新的實例。
request成員變量有關於流(TCP)服務的最近接受的socket;對於數據報服務,它是一個包含進入消息和監聽socket的元組。client_address包含發送者的地址,server有對SocketServer的一個引用(經過這你能夠訪問它的成員,如server_address)。
下面的例子實現了一個EchoRequestHandler,這做爲一個服務端它將客戶端所發送的數據再發送回客戶端:
>>> import SocketServer
>>> class EchoRequestHandler(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 socket import *
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.connect(('120.0.0.1',12321))
>>> s.send('Hello!')
6
>>> print s.recv(1024)
Hello!
>>> s.send('I like Tuesdays!')
16
>>> print s.recv(1024)
I like Tuesdays!
>>> s.close()
SocketServer模塊也定義了BaseRequestHandler的兩個子類:StreamRequestHandler和DatagramRequestHandler。它們覆蓋了setup和finish方法並建立了兩個文件對象rfile和wfile,你能夠用這兩個文件對象來向客戶端讀寫數據,從而代替使用socket方法。
相關文章
相關標籤/搜索