標籤:python
半開放socket
通常socket是雙向的,有時你會想建立一個單向的socket,單向的socket被稱為半開放socket。為此,需要調用shutdown()函數,對於socket,這個操作是無法復原的。半開放socket可以用於一下情況:
你想要確保所有寫好的資料都已經被傳送出去。當為了關閉socket的輸出頻道而調用shutdown()函數的時候,只有在緩衝裡面的資料都被成功發送出去後,才會有返回。
你想有個辦法來捕獲潛在的程式錯誤,這些錯誤是由數圖寫一個不能寫的socket,或者讀一個不該讀的socket引起的。
你的程式使用了fork()或多線程,而你想防止其他進程或線程的某些操作,或者你像立刻關閉一個socket。
使用shutdown()的一個原因是協助你確保代碼的正確。例如,若你結束了寫操作並使用了shutdown()來防止將來再進行寫操作,一旦你將來試圖再進行寫操作,你將得到一個異常。通常捕獲異常要比跟蹤死結或協議錯誤容易。
當程式使用fork()或多線程的時候,還會產生一種情況。若使用fork(),對socket的close()函數調用,只能保證對那個特別的進程來說串連是關閉的。只有所有的進程或者調用了close(),或者socket超過範圍,或者socket被刪除和終止,該串連才會真正地關閉。可以通過調用shutdown()來終止雙向通訊,進而關閉socket。
shutdown()的參數:0 表示禁止將來讀;1 表示禁止將來寫;2 表示禁止將來讀和寫
逾時
逾時在很多情況下對發現錯誤或串連問題很有用。一些用於主動串連中的程式,如果沒有任何活動超過了一分鐘,該程式就會收到警告。而不是等待來自作業系統關於失敗的通知。在讀資料的時候,逾時可以用於強迫斷開不活動的用戶端。無論是讀還是寫資料,都可以用逾時來檢查斷掉的串連。
為了讓Python的socket具有逾時檢查功能,需要調用socket的settimeout()函數,向傳遞給它的參數表明,經過多少秒數就算是逾時。稍後,當訪問一個socket,如果經過參數設定的一定的時間後,什麼都沒有發生,就會產生一個socket.timeout異常。例子:
#!/usr/bin/env pythonfrom socket import *import tracebackHOST=‘‘PORT=51234ADDR=(HOST,PORT)s=socket(AF_INET,SOCK_STREAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.bind(ADDR)s.listen(1)while True: try: client,addr=s.accept() except KeyboardInterrupt: raise except: traceback.print_exc() continue client.settimeout(5) try: print ‘Got connection from :‘,client.getpeername() while True: data=client.recv(4096) if not len(data): break client.sendall(data) except (KeyboardInterrupt,SystemExit): raise except timeout: print ‘Time out‘ except: traceback.print_exc() try: client.close() except KeyboardInterrupt: raise except: traceback.print_exc()
當使用socket逾時時,需要特別注意的是讀和寫操作都會產生逾時。對於讀操作,如果用戶端沒有發送任何資料,就可能會產生逾時。對於寫操作,用戶端也許還沒有開始試圖讀取資料。在這兩種情況下,網路擁擠或出現問題都會引起逾時。這就是在這個例子中,捕獲socket.timeout的語句覆蓋了recv()和sendall()的調用。
傳輸字串
在網路傳輸資料有一個普遍的問題,就是傳輸不確定長度的字串。不知道什麼時候資料發送結束。有兩種方法可以解決這個問題:一個唯一的字串結束標識符和一個在開始部分的固定長度字串大小的指標。
在第一種方法中,發送方會在發送本文後附加一個字串結束標識符。這個標識符是一個NULL字元或是分行符號。若使用分行符號,就能方便地使用檔案類對象的方法readline()來得到資料。但是,分行符號也經常用在本文中,所以若一小塊單獨的資料卻有很多行,就有問題。這時可以用NULL字元。但是,若字串中包含位元據,它可能會包含NULL字元。這種情況下,沒有可用的唯一字串結束標識符。這時可以用轉義符,資料編碼,或是直接用第二種方法。
第二種方法中,首先發送一個固定寬度的數字,表示要發送資料的長度,接收方會根據這個數字接受該長度的資料。
理解網路位元組順序
在網路上發送整型資料的時候,有兩種選擇:
一個ASCII碼的字串,接收方需解析;
一個二進位字,一般是16或32位長
在發送一個二進位整數之前,該整數被轉換成網路位元組順序。接收方收到後,在使用該資料之前,會先把網路位元組順序轉換成本地的表示方法。Python的struct模組提供了把資料在Python和位元據之間轉換的支援。struct模組是以一個格式化字串為基礎。在這裡主要使用兩種基本的格式:H,適用於16位整數;I,適用於32位的整數。驚歎號表示struct模組使用網路位元組順序來進行編碼和解碼。例子:
#!/usr/bin/env pythonimport struct,sysdef htons(num): return struct.pack(‘!H‘,num)def htonl(num): return struct.pack(‘!I‘,num)def ntohs(data): return struct.unpack(‘!H‘,data)[0]def ntohl(data): return struct.unpack(‘!I‘,data)[0]def sendstring(data): return htonl(len(data))+dataprint ‘Enter a string:‘str=sys.stdin.readline().rstrip()print repr(sendstring(str))
使用廣播資料
廣播資料不能用TCP實現,多數是用UDP來實現的。發送方廣播一個UDP資訊包給區域網路上的所有機器。在接收方,當收到一個廣播資訊包後,檢查目的地和連接埠號碼,若正有一個進程在偵聽該連接埠,則資訊包會發送給該進程,否則就丟棄。
為了使socket支援廣播,必須首先調用setsockopt(SOL_SOCKET,SO_BROADCAST,1)。要發送一個廣播,可以使用特殊的地址“<broadcast>”。在廣播中,發送方和接收方都要進行配置。例子:
接收方(broadcast server):
#!/usr/bin/env pythonfrom socket import *import tracebackHOST=‘‘PORT=51234ADDR=(HOST,PORT)s=socket(AF_INET,SOCK_DGRAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.setsockopt(SOL_SOCKET,SO_BROADCAST,1)s.bind(ADDR)while True: try: message,addr=s.recvfrom(8192) print ‘Got data from‘,addr s.sendto(‘I am here‘,addr) except (KeyboardInterrupt,SystemExit): raise except: traceback.print_exc()
發送方(broadcast sender):
#!/usr/bin/env pythonfrom socket import *import sysdest=(‘<broadcast>‘,51234)s=socket(AF_INET,SOCK_DGRAM)s.setsockopt(SOL_SOCKET,SO_BROADCAST,1)s.sendto(‘Hello‘,dest)print ‘Looking for replies‘while True: (buf,addr)=s.recvfrom(2048) if not len(buf): break print ‘Received from %s:%s‘%(addr,buf)
關於廣播需要注意的地方是盡量少用廣播。
IPv6
家族AF_INET6。socket.getaddrinfo()函數可以使用一種或兩種協議來尋找地址。
使用poll()或select()實現事件通知
通常情況下,socket上的I/O會阻塞。也就是說,除非一個操作結束,否則程式是不會繼續執行的。select()和poll()可以通知作業系統是哪個socket對你的程式“感興趣”。當某個socket上有事件發生的時候,作業系統會通知你發生了什麼,這樣就可以進行處理。select()是早期介面,在同時觀察多個socket的時候比較笨重。。下面是一個使用poll()的例子:
伺服器程式(每5秒向用戶端發送一段文本的一行):
#!/usr/bin/env pythonfrom socket import *import traceback,timeHOST=‘‘PORT=51234ADDR=(HOST,PORT)s=socket(AF_INET,SOCK_STREAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.bind(ADDR)s.listen(1)while True: try: client,addr=s.accept() except KeyboardInterrupt: raise except: traceback.print_exc() continue try: print "Got connection from",client.getpeername() while True: try: client.sendall(time.asctime()+‘\n‘) except: break time.sleep(5) except (KeyboardInterrupt,SystemExit): raise except: traceback.print_exc()try: client.close()except KeyboardInterrupt: raiseexcept: traceback.print_exc()
用戶端程式:
#!/usr/bin/env pythonfrom socket import *import sys,selectHOST=‘localhost‘PORT=51234ADDR=(HOST,PORT)spinsize=10spinpos=0spindir=1def spin(): global spinsize,spinpos,spindir spinstr=‘.‘*spinpos+‘|‘+‘.‘*(spinsize-spinpos-1) sys.stdout.write(‘\r‘+spinstr+‘ ‘) sys.stdout.flush() spinpos+=spindir if spinpos<0: spindir=1 spinpos=1 elif spinpos>=spinsize: spinpos-=2 spindir=-1s=socket(AF_INET,SOCK_STREAM)s.connect(ADDR)p=select.poll()p.register(s.fileno(),select.POLLIN|select.POLLERR|select.POLLHUP)#對這些事情使用poll,對到來的資料感興趣while True: results=p.poll(50)#表示等待50毫秒後可能會發生某件事情,若什麼都沒有發生,則p.poll()返回一個空的列表 if len(results): if results[0][1]==select.POLLIN: data=s.recv(4096) if not len(data): print (‘\rRemote end closed connection;exiting.‘) break sys.stdout.write(‘\rReceived: ‘+data) sys.stdout.flush() else: print ‘\rProblem occurred;exiting‘ sys.exit(0) spin()
最後的結果非常有趣。。
本文出自 “蓮的思念” 部落格,請務必保留此出處http://liandesinian.blog.51cto.com/7737219/1554969
第5章 進階網路操作