Socket 是程式間通訊的一種方式,它與其他程式間通訊的一個主要不同是:它能實現不同主機間的程式間通訊,我們網路上各種各樣的服務大多都是基於 Socket 來完成通訊的,例如我們每天瀏覽網頁、QQ 聊天、收發 email 等等。要解決網路上兩臺主機之間的程式通訊問題,首先要唯一標識該程式,在 TCP/IP 網路協議中,就是通過 (IP地址,協議,埠號) 三元組來標識程式的,解決了程式標識問題,就有了通訊的基礎了。
本文主要介紹使用 Python 進行 TCP Socket 網路程式設計,假設你已經具有初步的網路知識及 Python 基本語法知識。
TCP 是一種面向連線的傳輸層協議,TCP Socket 是基於一種 Client-Server 的程式設計模型,服務端監聽客戶端的連線請求,一旦建立連線即可以進行傳輸資料。那麼對 TCP Socket 程式設計的介紹也分為客戶端和服務端:
客戶端程式設計
建立 socket
首先要建立 socket,用 Python 中 socket 模組的函式 socket
就可以完成:
1 2 3 4 5 6 7 8 |
#Socket client example in python import socket #for sockets #create an AF_INET, STREAM socket (TCP) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print 'Socket Created' |
函式 socket.socket
建立一個 socket,返回該 socket 的描述符,將在後面相關函式中使用。該函式帶有兩個引數:
- Address Family:可以選擇
AF_INET
(用於 Internet 程式間通訊) 或者AF_UNIX
(用於同一臺機器程式間通訊) - Type:套接字型別,可以是
SOCKET_STREAM
(流式套接字,主要用於 TCP 協議)或者SOCKET_DGRAM
(資料包套接字,主要用於 UDP 協議)
注:由於本文主要概述一下 Python Socket 程式設計的過程,因此不會對相關函式引數、返回值進行詳細介紹,需要了解的可以檢視相關手冊
錯誤處理
如果建立 socket 函式失敗,會丟擲一個 socket.error
的異常,需要捕獲:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#handling errors in python socket programs import socket #for sockets import sys #for exit try: #create an AF_INET, STREAM socket (TCP) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, msg: print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1] sys.exit(); print 'Socket Created' |
那麼到目前為止已成功建立了 socket,接下來我們將用這個 socket 來連線某個伺服器,就連 www.google.com 吧。
連線伺服器
本文開始也提到了,socket 使用 (IP地址,協議,埠號) 來標識一個程式,那麼我們要想和伺服器進行通訊,就需要知道它的 IP地址以及埠號。
獲得遠端主機的 IP 地址
Python 提供了一個簡單的函式 socket.gethostbyname
來獲得遠端主機的 IP 地址:
1 2 3 4 5 6 7 8 9 10 11 12 |
host = 'www.google.com' port = 80 try: remote_ip = socket.gethostbyname( host ) except socket.gaierror: #could not resolve print 'Hostname could not be resolved. Exiting' sys.exit() print 'Ip address of ' + host + ' is ' + remote_ip |
現在我們知道了伺服器的 IP 地址,就可以使用連線函式 connect
連線到該 IP 的某個特定的埠上了,下面例子連線到 80 埠上(是 HTTP 服務的預設埠):
1 2 3 4 |
#Connect to remote server s.connect((remote_ip , port)) print 'Socket Connected to ' + host + ' on ip ' + remote_ip |
執行該程式:
1 2 3 4 |
$ python client.py Socket created Ip of remote host www.google.com is 173.194.38.145 Socket Connected to www.google.com on ip 173.194.38.145 |
傳送資料
上面說明連線到 www.google.com 已經成功了,接下面我們可以向伺服器傳送一些資料,例如傳送字串 GET / HTTP/1.1rnrn
,這是一個 HTTP 請求網頁內容的命令。
1 2 3 4 5 6 7 8 9 10 11 12 |
#Send some data to remote server message = "GET / HTTP/1.1rnrn" try : #Set the whole string s.sendall(message) except socket.error: #Send failed print 'Send failed' sys.exit() print 'Message send successfully' |
傳送完資料之後,客戶端還需要接受伺服器的響應。
接收資料
函式 recv
可以用來接收 socket 的資料:
1 2 3 4 |
#Now receive data reply = s.recv(4096) print reply |
一起執行的結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Socket created Ip of remote host www.google.com is 173.194.38.145 Socket Connected to www.google.com on ip 173.194.38.145 Message send successfully HTTP/1.1 302 Found Cache-Control: private Content-Type: text/html; charset=UTF-8 Location: http://www.google.com.sg/?gfe_rd=cr&ei=PlqJVLCREovW8gfF0oG4CQ Content-Length: 262 Date: Thu, 11 Dec 2014 08:47:58 GMT Server: GFE/2.0 Alternate-Protocol: 80:quic,p=0.02 <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> <TITLE>302 Moved</TITLE></HEAD><BODY> <H1>302 Moved</H1> The document has moved <A HREF="http://www.google.com.sg/?gfe_rd=cr&ei=PlqJVLCREovW8gfF0oG4CQ">here</A>. </BODY></HTML> |
關閉 socket
當我們不想再次請求伺服器資料時,可以將該 socket 關閉,結束這次通訊:
1 |
s.close() |
小結
上面我們學到了如何:
- 建立 socket
- 連線到遠端伺服器
- 傳送資料
- 接收資料
- 關閉 socket
當我們開啟 www.google.com 時,瀏覽器所做的就是這些,知道這些是非常有意義的。在 socket 中具有這種行為特徵的被稱為CLIENT,客戶端主要是連線遠端系統獲取資料。
socket 中另一種行為稱為SERVER,伺服器使用 socket 來接收連線以及提供資料,和客戶端正好相反。所以 www.google.com 是伺服器,你的瀏覽器是客戶端,或者更準確地說,www.google.com 是 HTTP 伺服器,你的瀏覽器是 HTTP 客戶端。
那麼上面介紹了客戶端的程式設計,現在輪到伺服器端如果使用 socket 了。
伺服器端程式設計
伺服器端主要做以下工作:
- 開啟 socket
- 繫結到特定的地址以及埠上
- 監聽連線
- 建立連線
- 接收/傳送資料
上面已經介紹瞭如何建立 socket 了,下面一步是繫結。
繫結 socket
函式 bind
可以用來將 socket 繫結到特定的地址和埠上,它需要一個 sockaddr_in
結構作為引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import socket import sys HOST = '' # Symbolic name meaning all available interfaces PORT = 8888 # Arbitrary non-privileged port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print 'Socket created' try: s.bind((HOST, PORT)) except socket.error , msg: print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] sys.exit() print 'Socket bind complete' |
繫結完成之後,接下來就是監聽連線了。
監聽連線
函式 listen
可以將 socket 置於監聽模式:
1 2 |
s.listen(10) print 'Socket now listening' |
該函式帶有一個引數稱為 backlog,用來控制連線的個數。如果設為 10,那麼有 10 個連線正在等待處理,此時第 11 個請求過來時將會被拒絕。
接收連線
當有客戶端向伺服器傳送連線請求時,伺服器會接收連線:
1 2 3 4 5 |
#wait to accept a connection - blocking call conn, addr = s.accept() #display client information print 'Connected with ' + addr[0] + ':' + str(addr[1]) |
執行該程式的,輸出結果如下:
1 2 3 4 |
$ python server.py Socket created Socket bind complete Socket now listening |
此時,該程式在 8888 埠上等待請求的到來。不要關掉這個程式,讓它一直執行,現在客戶端可以通過該埠連線到 socket。我們用 telnet 客戶端來測試,開啟一個終端,輸入 telnet localhost 8888
:
1 2 3 4 5 |
$ telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Connection closed by foreign host. |
這時服務端輸出會顯示:
1 2 3 4 5 |
$ python server.py Socket created Socket bind complete Socket now listening Connected with 127.0.0.1:59954 |
我們觀察到客戶端已經連線上伺服器了。在建立連線之後,我們可以用來與客戶端進行通訊。下面例子演示的是,伺服器建立連線之後,接收客戶端傳送來的資料,並立即將資料傳送回去,下面是完整的服務端程式:
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 |
import socket import sys HOST = '' # Symbolic name meaning all available interfaces PORT = 8888 # Arbitrary non-privileged port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print 'Socket created' try: s.bind((HOST, PORT)) except socket.error , msg: print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] sys.exit() print 'Socket bind complete' s.listen(10) print 'Socket now listening' #wait to accept a connection - blocking call conn, addr = s.accept() print 'Connected with ' + addr[0] + ':' + str(addr[1]) #now keep talking with the client data = conn.recv(1024) conn.sendall(data) conn.close() s.close() |
在一個終端中執行這個程式,開啟另一個終端,使用 telnet 連線伺服器,隨便輸入字串,你會看到:
1 2 3 4 5 6 7 |
$ telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. happy happy Connection closed by foreign host. |
客戶端(telnet)接收了伺服器的響應。
我們在完成一次響應之後伺服器立即斷開了連線,而像 www.google.com 這樣的伺服器總是一直等待接收連線的。我們需要將上面的伺服器程式改造成一直執行,最簡單的辦法是將 accept
放到一個迴圈中,那麼就可以一直接收連線了。
保持服務
我們可以將程式碼改成這樣讓伺服器一直工作:
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 |
import socket import sys HOST = '' # Symbolic name meaning all available interfaces PORT = 5000 # Arbitrary non-privileged port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print 'Socket created' try: s.bind((HOST, PORT)) except socket.error , msg: print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] sys.exit() print 'Socket bind complete' s.listen(10) print 'Socket now listening' #now keep talking with the client while 1: #wait to accept a connection - blocking call conn, addr = s.accept() print 'Connected with ' + addr[0] + ':' + str(addr[1]) data = conn.recv(1024) reply = 'OK...' + data if not data: break conn.sendall(reply) conn.close() s.close() |
現在在一個終端下執行上面的伺服器程式,再開啟三個終端,分別用 telnet 去連線,如果一個終端連線之後不輸入資料其他終端是沒辦法進行連線的,而且每個終端只能服務一次就斷開連線。這從程式碼上也是可以看出來的。
這顯然也不是我們想要的,我們希望多個客戶端可以隨時建立連線,而且每個客戶端可以跟伺服器進行多次通訊,這該怎麼修改呢?
處理連線
為了處理每個連線,我們需要將處理的程式與主程式的接收連線分開。一種方法可以使用執行緒來實現,主服務程式接收連線,建立一個執行緒來處理該連線的通訊,然後伺服器回到接收其他連線的邏輯上來。
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 47 48 49 50 51 52 |
import socket import sys from thread import * HOST = '' # Symbolic name meaning all available interfaces PORT = 8888 # Arbitrary non-privileged port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print 'Socket created' #Bind socket to local host and port try: s.bind((HOST, PORT)) except socket.error , msg: print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] sys.exit() print 'Socket bind complete' #Start listening on socket s.listen(10) print 'Socket now listening' #Function for handling connections. This will be used to create threads def clientthread(conn): #Sending message to connected client conn.send('Welcome to the server. Type something and hit entern') #send only takes string #infinite loop so that function do not terminate and thread do not end. while True: #Receiving from client data = conn.recv(1024) reply = 'OK...' + data if not data: break conn.sendall(reply) #came out of loop conn.close() #now keep talking with the client while 1: #wait to accept a connection - blocking call conn, addr = s.accept() print 'Connected with ' + addr[0] + ':' + str(addr[1]) #start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function. start_new_thread(clientthread ,(conn,)) s.close() |
再次執行上面的程式,開啟三個終端來與主伺服器建立 telnet 連線,這時候三個客戶端可以隨時接入,而且每個客戶端可以與主伺服器進行多次通訊。
telnet 終端下可能輸出如下:
1 2 3 4 5 6 7 8 9 10 11 |
$ telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Welcome to the server. Type something and hit enter hi OK...hi asd OK...asd cv OK...cv |
要結束 telnet 的連線,按下 Ctrl-]
鍵,再輸入 close
命令。
伺服器終端的輸出可能是這樣的:
1 2 3 4 5 6 |
$ python server.py Socket created Socket bind complete Socket now listening Connected with 127.0.0.1:60730 Connected with 127.0.0.1:60731 |
總結
到目前為止,我們學習了 Python 下基本的 socket 程式設計。