對於python網路程式設計來說,免不了要用到socket模組。下面主要分享一下個人對python socket的一些理解。
socket程式設計步驟
- 服務端建立一個socket,繫結地址和埠,然後監聽埠上傳入的連線,一旦有連線進來,就通過accept函式接收傳入的連線。
- 客戶端也是建立一個socket。繫結遠端地址和埠,然後建立連線,傳送資料。
服務端socket
下面通過一段例項程式碼來詳細說明
服務端 socker_server.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import socket import sys HOST = "127.0.0.1" PORT = 10000 s = None for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): af, socktype, proto, canonname, sa = res try: s = socket.socket(af, socktype, proto) except socket.error as msg: s = None continue try: s.bind(sa) s.listen(5) except socket.error as msg: s.close() s = None continue break if s is None: print 'could not open socket' sys.exit(1) conn, addr = s.accept() print 'Connected by', addr while 1: data = conn.recv(1024) if not data: break conn.send(data) conn.close() |
首先我們通過socket.getaddrinnfo函式將host/port轉換成一個包含5元組的序列。這個5元組包含我們建立一個socket連線所需要的所有必要引數。返回的5元組分別是 (family, sockettype, proto, canonname, sockaddr)
family 地址簇,用與socket()函式的第一個引數。主要有以下幾個
- socket.AF_UNIX 用與單一機器下的程式通訊
- socket.AF_INET 用與伺服器之間相互通訊,通常都用這個。
- socket.AF_INET6 支援IPv6
sockettype socket型別,用與socket()函式的第二個引數,常用的有
- socket.SOCK_STREAM 預設,用於TCP協議
- socket.SOCK_DGRAM 用於UDP協議
proto 協議,用於socket()函式的第三個引數。 getaddrinnfo函式會根據地址格式和socket型別,返回合適的協議
canonname 一個規範化的host name。
sockaddr 描述了一個socket address .是一個二元組,主要用於bind()和connect()函式
接下來建立一個socket物件,傳入getaddrinnfo函式返回的af,sockettype,proto。
1 |
s = socket.socket(af, socktype, proto) |
然後繫結socket address
1 |
s.bind(sa) |
開啟監聽模式
1 |
s.listen(5) |
listen函式會監聽連線到socket上的連線,參數列示在拒絕連線之前系統可以掛起的最大連線佇列數量為5。這些連線還沒有被accept處理。數量不能無限大,通常指定5。
一旦我們監聽到了連線,就會呼叫accept函式接收連線
1 |
conn, addr = s.accept() |
accept函式返回一個二元組,conn是一個新的socket物件,用來接收和傳送資料。addr表示另一端的socket地址。
接下來我們就可以用conn物件傳送和接收資料了
1 2 |
data = conn.recv(1024) # 接收資料, 這裡指定一次最多接收的字元數量為1024 conn.send(data) # 傳送資料 |
這裡我們接收到一個連線socket就會停止執行,所以如果要迴圈連線的話,將accept函式放入到一個死迴圈裡。
客戶端socket
客戶端socket程式設計相對比較簡單,通過connect和服務端建立連線之後,就可以相互通訊了。socket_client.py如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: s = socket.socket(af, socktype, proto) except socket.error as msg: s = None continue try: s.connect(sa) except socket.error as msg: s.close() s = None continue break if s is None: print 'could not open socket' sys.exit(1) s.sendall('Hello, world') data = s.recv(1024) s.close() print 'Received', repr(data) |
備註: 對於getaddrinfo函式,可以參考下http://baike.baidu.com/link?url=TOO9akvwu0BLqTRQKNC5NW6GgWTyuhXhAqHmKZOyoaqUub-2RP7LUUALaV-Kd_dZAWVDZS5z-uDvgXrHeMSg8N3YtY0h-iOVQuHgpUMiA7a 這個函式的作用是把協議相關性安全隱藏在這個底層庫函式內部。應用程式只要處理由getaddrinfo函式返回的物件即可。
以上主要是針對TCP流資料的socket程式設計。對於UDP協議的資料,處理略有不同。譬如傳送接收UDP資料包處理函式為:
1 2 |
socket.sendto(string, flags, address) socket.recvfrom(bufsize[, flags]) #返回(string, address),string是返回的資料,address是傳送方的socket地址 |
其他詳細內容可以參考 http://python.usyiyi.cn/translate/python_278/library/socket.html
SocketServer模組
python中網路程式設計除了socket模組還提供了SocketServer模組,這一模組主要是對socket模組進行了封裝,將socket的物件的建立,繫結,連線,接收,傳送,關閉都封裝在裡面,大大簡化了網路服務的程式設計。
此模組提供了以下2個主要的網路服務類,用於建立相應的套接字流
- TCPServer 建立TCP協議的套接字流
- UDPServer 建立UDP協議的套接字流
我們有了套接字流物件,還需要一個請求處理類。SocketServer模組提供了請求處理類有BaseRequestHandler,以及它的派生類StreamRequestHandler和DatagramRequestHandler。所以只要繼承這3個類中的一個,然後重寫handle函式,此函式將用來處理接收到的請求。下面看一個服務端的程式碼示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import SocketServer class MyTCPHandler(SocketServer.StreamRequestHandler): """建立請求處理類,重寫handle方法。此外也可以重寫setup()和finish()來做一些請求處理前和處理後的一些工作""" def handle(self): # self.request is the TCP socket connected to the client self.data = self.request.recv(1024).strip() print "{} wrote:".format(self.client_address[0]) print self.data # just send back the same data, but upper-cased self.request.sendall(self.data.upper()) if __name__ == "__main__": HOST, PORT = "localhost", 10000 server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler) # Activate the server; this will keep running until you # interrupt the program with Ctrl-C # server.shutdown() server.serve_forever() # 一直迴圈接收請求 # server.handle_request() # 只處理一次請求就退出 |
看著是不是程式碼簡單了很多,而且SocketServer模組內部使用了多路複用IO技術,可以實現更好的連線效能。看serve_forever函式的原始碼用到了select模組。通過傳入socket物件呼叫select.select()來監聽socket物件的檔案描述符,一旦發現socket物件就緒,就通知應用程式進行相應的讀寫操作。原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def serve_forever(self, poll_interval=0.5): """Handle one request at a time until shutdown. Polls for shutdown every poll_interval seconds. Ignores self.timeout. If you need to do periodic tasks, do them in another thread. """ self.__is_shut_down.clear() try: while not self.__shutdown_request: # XXX: Consider using another file descriptor or # connecting to the socket to wake this up instead of # polling. Polling reduces our responsiveness to a # shutdown request and wastes cpu at all other times. r, w, e = _eintr_retry(select.select, [self], [], [], poll_interval) if self in r: self._handle_request_noblock() finally: self.__shutdown_request = False self.__is_shut_down.set() |
即使使用了select技術,TCPServer,UDPServer處理請求仍然是同步的,意味著一個請求處理完,才能處理下一個請求。但SocketServer模組提供了另外2個類用來支援非同步的模式。
- ForkingMixIn 利用多程式實現非同步
- ThreadingMixIn 利用多執行緒實現非同步
看名字就知道用mixin模式來實現非同步。而mixin模式可以通過多繼承來實現,所以通過對網路服務類進行多繼承的方式就可以實現非同步模式
1 2 |
class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass |
針對ThreadindMixIn,實現非同步的原理也就是在內部對每個請求建立一個執行緒來處理。看原始碼
1 2 3 4 5 6 |
def process_request(self, request, client_address): """Start a new thread to process the request.""" t = threading.Thread(target = self.process_request_thread, args = (request, client_address)) t.daemon = self.daemon_threads t.start() |
下面提供一個非同步模式的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import socket import threading import SocketServer class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): def handle(self): data = self.request.recv(1024) cur_thread = threading.current_thread() response = "{}: {}".format(cur_thread.name, data) self.request.sendall(response) class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass def client(ip, port, message): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((ip, port)) try: sock.sendall(message) response = sock.recv(1024) print "Received: {}".format(response) finally: sock.close() if __name__ == "__main__": # Port 0 means to select an arbitrary unused port HOST, PORT = "localhost", 0 server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler) ip, port = server.server_address # Start a thread with the server -- that thread will then start one # more thread for each request server_thread = threading.Thread(target=server.serve_forever) # Exit the server thread when the main thread terminates server_thread.daemon = True server_thread.start() print "Server loop running in thread:", server_thread.name client(ip, port, "Hello World 1") client(ip, port, "Hello World 2") client(ip, port, "Hello World 3") server.shutdown() server.server_close() |
更多對SocketServer模組的瞭解參考https://docs.python.org/2/library/socketserver.html 本文所使用的示例就來自官網。畢竟官網的例子實在太好了。
以上是本人對socket相關的理解,有什麼不當或錯誤之處,還請指出。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式