[Python] Simple Socket Server


最近要利用Python來monitor一些東西, 需要做個telnet daemon

特地寫一下如何用 python 做一個 socket server

這個我當初到是真的花了不少時間找尋資料, 發現有兩種方法, 一種是比較傳統的 socket

然而 python 也有做一個 module - SocketServer, 可以簡化不少自己用 socket 做 server 的麻煩

不過 client 端當然還是要用 socket 自己做

官方文件 Socket: http://docs.python.org/2/howto/sockets.html

網路上有很多教學, 這裡到是有一個不錯的英文教學: http://www.binarytides.com/python-socket-programming-tutorial/

先從server端開始 (socket 的功用不是只有當 server, 只是這邊用 server - client 角度當範例)
import socket

host = ''
port = 12345

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.bind((host, port))

sock.listen(5)
 
第一步, 要先import socket, 然後要先建立socket, 用socket.socket去create

create有些基本設定, 首先是有socket.AF_INETsocket.AF_UNIX兩種

AF_INET: 跟外面網路, 也就是IPv4, TCP, UDP要用這種 (AF_INET6 for IPv6)

AF_UNIX: 本地機器自己內部的溝通, 不對外

至於socket.SOCK_STREAMsocket.SOCK_DGRAM

SOCK_STREAM: 會確保資料正確的流到對方, 像是資料串流的用法, 此法是用TCP

SOCK_DGRAM: 無確保, 像是廣播訊息的用法, 此法是用UDP

第二步要讓這個socket要綁到某個位址(ip/port), 讓要求可以用此位址傳送訊息進來
sock.bind((host, port))

第三步設定最多可以讓幾個連線數
sock.listen(5) # 最多5個

最後就是要啟動server準備讓client傳送訊息
sock.accept()

注意, 使用accept()的時候, server會處在wait狀態, 等到有client連線的時候

他會將整個連線跟client資訊回傳出來, 所以最好改成
(clientsocket, address) = serversocket.accept()

clientsocket就是存放這次的連線, 之後可以透過clientsocket來跟client做其他溝通

address就會有連過來IP/PORT相關資訊

那如果今天server只聽一次accept(), 做完就會結束了, 可是要做成Server, 要一直不停的聆聽client端傳過來的東西

所以簡單一點可以把accept做在while loop裡面
while True:
    (clientsocket, address) = serversocket.accept()

那server端跟client端彼此溝通是透過socket.recv & socket.send來傳送資料
while True:
    clientsocket.recv(1024) #接收1024字元
    clientsocket.send(data) #傳送data出去

那要是怕等待時間過久浪費connection, 可以設置timeout
while True:
    sock.settimeout(10) # timeout 10s

簡單的完整例子
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.bind(('', 54321))
sock.listen(5)
sock.settimeout(10)


#while True:
    (csock, adr) = sock.accept()
    print "Client Info: ", csock, adr

那從client端來看, 一樣是要做socket.socket create, 但是他之後不用綁住位置, 他要的是給他位置去連線
sock.connect((HOST, PORT))

只要連上成功, 就可以開始跟server傳送訊息了, 下面給一個根據上述所寫出來的簡易流程

Server

import socket, sys

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
    sys.stderr.write("[ERROR] %s\n" % msg[1])
    sys.exit(1)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #reuse tcp
sock.bind(('', 54321))
sock.listen(5)
sock.settimeout(10)


while True:
    (csock, adr) = sock.accept()
    print "Client Info: ", csock, adr
    msg = csock.recv(1024)
    if not msg:
        pass
    else:
        print "Client send: " + msg
        csock.send("Hello I'm Server.\r\n")
    csock.close()

server端我稍微加了一點的try&except做偵測, 後面accept()之後, 做的事情很簡單, 但是要注意, 收到的客戶端連線叫做csock, 所以如果你要收或者是傳對方的訊息記得要用csock接收, 我看過有bug寫出來還是用sock去做sock.recv跟sock.send之類的, 要小心

Client

import socket, sys

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
    sys.stderr.write("[ERROR] %s\n" % msg[1])
    sys.exit(1)

try:
    sock.connect(('', 54321))
except socket.error, msg:
    sys.stderr.write("[ERROR] %s\n" % msg[1])
    exit(1)

sock.send("Hello I'm Client.\r\n")
print sock.recv(1024)
sock.close()

客戶端就單純多了, connect之後, 就利用connect後好的sock做傳輸動作(send/recv)即可

那這邊會看到一個很笨的事情, 當client做個一傳一回的動作之後, 就中斷了, 因為 server 端並沒有後續的處理, server 端因為 while loop 的關係, 所以又重新開始回到 accept() 接收狀態, 所以這時候就需要做threading的機制來幫你分別處理每個連上的 client 所需要的工作, 這留到後面仔細解說

常見問題補充一下, 如果在實作的時候, 因為常常中斷又重新啟動server, 而發生
socket.error: [Errno 98] Address already in use

這跟TCP的設計有關, 網路有一篇有詳細解釋

解釋: http://stackoverflow.com/questions/337115/setting-time-wait-tcp

那簡單一點說明就是 TCP connection建立的時候, 會有一組tuple - (source IP, source port, destination IP, destination port)

這組就算你的server shutdown了, TCP connection都還是會在TIME_WAIT狀態, 因為怕還有其他live packets還沒傳送過來

所以要嘛是改TIME_WAIT時間(不安全), 要嘛你設定之後可以這個TCP connection可以再度重複使用

重複使用的方法就是, create socket完之後, 加上下列這行
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

這樣不管你強行中斷了多少次 server, 它都會再次接收原本的 TCP connection

上述我簡單說明了如何使用 Socket 建立 server & client

但是其實不是很實用, 因為 server 只能循序一個一個做對應的工作, 而且也不能同時處理多個clients

這時候就需要靠線程(thread& threading)來幫忙解決問題了

python thread: http://docs.python.org/2/library/thread.html

其實python的thread算是比較低階的用法, 很單純, 但是功能性較少, 較複雜的要自己想辦法寫

所以python還有一個叫high-level的threading, 去幫助需要更多複雜thread動作的人加速開發(lock相關之類的)

先來簡單講講thread在上述socket server的修改

首先import thread, 為了方便, 用 from thread import *

那, 簡易的用法是, 可以直接呼叫start_new_thread(function, args), 就可以叫一個thread去做工作

所以簡單修改一下上一個範例
import socket, sys 
from thread import *

def threadWork(client):
    while True:
        msg = client.recv(1024)
        if not msg:
            pass
        else:
            print "Client send: " + msg 
            client.send("You say: " + msg + "\r\n")
    client.close()

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
    sys.stderr.write("[ERROR] %s\n" % msg[1])
    sys.exit(1)

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 54321))
sock.listen(5)

while True:
    (csock, adr) = sock.accept()
    print "Client Info: ", csock, adr 
    start_new_thread(threadWork, (csock,))

sock.close()

開頭新增了import thread, 不看threadwork function的話, 下面程式碼幾乎跟先前的如出一轍, 不過while loop裡面內容稍微不一樣了

本來是直接接收client端傳過來的訊息, 變成直接呼叫thread(start_new_thread), 並且給了兩個參數

首先第一個參數, 要給他一個function, 這個function就是你呼叫thread要做的事情, 第二個參數則是給要呼叫這個function所需要的arguments

所以簡單啦, 就把原本要跟client做的溝通工作, 寫到thread要呼叫的functiion就好
(這邊範例則是 threadwork )

那因為我想要叫一個 thread 去幫我接收 client 端所傳過來的內容, 再傳回去, 然後無止盡的做, 就完成啦

而 server 端還是可以不停的接收新進的client繼續工作, 既不會影響 client 又可以保持 server 流暢

留言

  1. 有些 accept 寫成 accpet 了

    回覆刪除
  2. 感謝分享~ 我是新手,剛好需要,看到這篇內容,受益頗多,幾個關鍵點都有提醒,真的是細心。

    回覆刪除
  3. 您好我想詢問,我在linux上面run這程式,我想詢問是否要再檔名後
    再輸入ip和port(python server.py 192.xxx.xxx.xxx 54321 )還是需要在程式內的sock.bind(('', 54321))
    就直接將ip輸入呢?

    回覆刪除
    回覆
    1. 理論上來說, 當你使用 bind('', 54321), 就是 listen 在 127.0.0.1:54321 這個位址上, 而且也包含了上面有設定的 ip address , 如果你防火牆沒有開, 這台電腦網卡有設定 ip 是 192.168.1.100, 那別人也可以用 192.168.1.100:54321 來連線

      刪除
    2. 謝謝您,給我這一個資訊。
      我想冒昧在詢問一個問題,就是如何讓client的
      sock.send("Hello I'm Client.\r\n")
      print sock.recv(1024)
      改成使用任何字傳送給server端呢?目前似乎還是找不太到,麻煩您給我相關協助,謝謝您

      刪除
    3. 不太好意思, 我不太清楚你這問題, 如果要改文字不就是把 Hello I'm client 改掉就好了嗎??

      刪除
    4. bind('', 54321),不是指listen在0.0.0.0:54321嗎?

      刪除
  4. 您好,我在測試socket程式時候發現我得在client發送訊息才能取得server的訊息,我需要server能夠
    任何時候都能傳訊息給client端,但是我在修改程式還是有問題
    def threadWork(client):
    while True:
    msg = client.recv(1024)
    if not msg:
    pass
    else:
    print "Client send: " + msg
    client.send("You say: " + msg + "\r\n")
    client.close()
    想詢問該怎麼做修改,才能夠讓server與client互傳資訊

    回覆刪除
    回覆
    1. 你好, 這邊你的問題我困惑的地方是, 你目前是打算怎麼讓 server 傳訊息出去呢? 因為在 threadWork 階段, 只要沒離開, 這邊都會一直連接著, 所以如果 client 端已經連線, server 端照理講無論如何都可以傳訊息給 client 端, 在這個情況, 不就是互傳資訊了嗎? 還是你有簡單的情景可以說出你想要程式互相溝通的方式?

      刪除
    2. 您好,我的問題在於,run程式的時候發現需要client 端發送訊息後,才能收到 server 端發送的資訊。
      但是""我希望的是 server 端發送資訊時client端都能顯示"",而""不是需要等到client 端發送訊息後才能接收到server 端的訊息""。很抱歉的打擾您!

      刪除
    3. 嗯, 我想要先確定一下問題, 看看我有沒有理解對, 由於我的範例都是等到 client 連過去之後, server 才回剛剛跟 client 一樣的訊息, 所以你想要的是, 可不可以 client 一連線, server 就傳訊息過去對嗎? 如果是這樣的話, (假設使用threadWork) 你可以在 client.recv(1024) 之前就 client.send("Hello\r\n"), 這樣 client 一連線就會有訊息了, 希望這是你想要的, 有問題可以再提出, 沒有關係的, 只是我有時候剛好忙沒看到會回的晚一點點

      刪除
    4. 謝謝您協助我,我會試著嘗試這個問題,非常的感謝您協助

      刪除
    5. 您好,請問您的意思是否是
      def threadWork(client):
      while True:
      ***client.send("Hello\r\n")*** 這樣呢?
      msg = client.recv(1024)
      if not msg:
      pass
      else:

      刪除
    6. 我的意思接近這樣, 但是我不知道你是否是要這樣的效果

      刪除
  5. 您好我想再次跟您做詢問,有沒有方法是當兩個client都連上server後,可以由server發送資訊給兩個,因為我連上後只能發送給單個ip資訊,而沒辦法在同時發送後一次給兩個或兩個以上iP發送資訊。
    我想應該是這裡"def threadWork(client):"但我卻不知道該如何將資料送給第一個IP或送給第二個IP,或者同時發送,很抱歉造成您的困擾,請求協助

    回覆刪除
    回覆
    1. 這個問題其實不難, 只是你沒有注意到, 每次要傳訊息給對方的時候, 會需要 client 這個變數, 那每個連線的使用者都會出現一次, 所以你想要一次傳訊息給全部的人, 就是要在 server 上維護一個 user list, 想要傳就是一次 loop all user array 然後一個一個傳送, 只是要很小心當使用者斷線之後的一些錯誤處理

      刪除
    2. 您好,我想詢問如何更改" client 這個變數",然後如何修改程式成 loop all user array,這裡我比較不了解!
      我想到一種方法是用UDP傳訊息給大家,但是我再跑程式的時候 我將socket.socket(socket.AF_INET, socket.SOCK_STREAM)改成socket.socket(socket.AF_INET, socket.SOCK_DGRAM),也改正成sendto與recvfrom,會跑不出來,是需要注意哪裡呢?還是說thread是不支援UDP

      刪除
    3. 我前面的意思是, 你需要先宣告一個全域變數, 例如 usr_list = [], 每當 threadWork 收到一個 client 的時候, 你就會存起來, 例如 usr_list.append(client), 所以進來多少就會增加多少, 但是每當有 client 斷線你就自己要維護這個 usr_list. 那你想要跟全部的 client 傳訊息, 就 loop usr_list array 然後一個一個使用 send 回傳你要回傳的訊息即可. UDP 故事不太一樣, 而且也要根據你的需求才能確定是否適合 UDP, 如果你今天要傳給每個人是要明確收到, 那你就還是必須要使用TCP, 如果這些人有些人訊息偶而收不到沒關係, 那就用 UDP, 所以使用 TCP/UDP 跟你的想要的需求比較有關, 但是兩者都應該可以做出你詢問的功能才對, 只是 UDP 在我的範例還需要改動一定程度

      刪除
    4. 您好,我在def threadWork程式外宣告一個全域變數
      global usr_list

      def threadWork(client):
      usr_list = []
      usr_list.append(client)
      while True:
      msg = client.recv(1024)
      if not msg:
      pass
      else:
      print "Client send: " + msg
      client.send("You say: " + msg + "\r\n")
      client.close()
      我遇到一個問題請問loop usr_list array這個的loop是指while的意思嗎?
      我在run程式會跑出無效的語法(invalid syntax),我該如何寫這段呢?
      請您協助我一下,很抱歉又麻煩您了,請您幫助我

      刪除
    5. 我將loop視為while然後將"while True:"改為"while usr_list array:"就會出現無效語法,是哪裡出了錯呢?

      刪除
    6. Hi, 不好意思, 其實照這樣看你可能要多練一點 python 的基礎語法喔, 我意思像是這篇的使用 http://pydoing.blogspot.tw/2011/01/python-for.html 你再看看看不看得懂

      刪除
    7. 應該說, 你是要一直傳訊息嗎? 如果你不是要拼命的回傳訊息你不應該用 while True, 應該用一般的 for loop

      刪除
    8. 很不好意思,我需要再去讀基礎語法!
      其實我想要做的是我有一個server,想送訊息給兩個不同的client,但是我卻只能依照加入server端的順序,發送訊息(依照順序),可是我還無法做到說我想指定送到其中一個client,
      因為我想要一直送資料給client所以使用while,for迴圈會限定次數。

      刪除
    9. 痾, 所以這邊有點誤會, 我意思是, 要傳給client 要使用 for-loop 去 loop usr_list array, 但至於你要多久傳一次訊息則可能會是用 while True, 但是這邊有被動跟主動的使用, 就是說, 你是要人為的去叫 server 傳訊息給 client 還是自動的會固定時間傳訊息給 client? 如果是第一點, 那你還需要實作一些 server request handler, 這個我建議要看的不是只是 socket server 最好還要看一下 BaseHttpRequest 怎使用會較方便, 如果是第二點, 那 threadWork 只是接收 client, 例如做登入之類的流程, 確認過後就加入到 global 的 usr_list, 然後會有一個 function 會去 for-loop 這個 usl_list 去做你要跟 client 傳輸的事情, 那這時候你還要決定你是要寫single thread 還是 muti-thread 的運作, 因為兩個設計會差異很大, 當然這是因為我不知道你要寫的東西有多複雜, 所以我只能給相對的建議, 那我覺得這些前提示你要對 python 認識至少一定程度再去做會比較保險, 不然花在 google search 上的功夫可能會將當相當多, 還不一定找的到答案

      刪除
  6. 作者已經移除這則留言。

    回覆刪除
  7. 不好意思,我再次做個詢問,如果我有兩個client進入到server中,並且我都已經知道client的ip,我想要傳送不同訊息給不同的client端,我該如何去修正呢?是需要將
    def threadWork(client1,client2)修正嗎?
    def threadWork(client):
    while True:
    msg = client.recv(1024)
    client.close()

    回覆刪除
    回覆
    1. 你如果想要把訊息傳給不同的 client 端, 簡單的方式是你要先在 server 有一個 list 去儲存使用者的 connection, 例如: client_list = [] 每次有 client 連線你都會把這些 client 儲存進 client_list, 所以假如第一個存起來的 client 你想要回給他 hello 你就只要 client_list[0].send("hello"), 第二個存起來的 client 你想要回給他 world, 你就 client_list[1].send("world"), 當然這樣的缺點是你根本不知道第一個 client 是不是你要傳送的使用者, 所以要再詳細一點就要比較複雜的操作, 舉例連線的時候 client 會送出自己的 id, 然後你會根據這個 id 去建立 dict, 假設 client 連線的時候也傳了
      id=9c96f6ac51aaf5deb8043d8c2330da69
      這樣就可以建立
      client_dict['9c96f6ac51aaf5deb8043d8c2330da69'] = client
      所以如果你知道 9c96f6ac51aaf5deb8043d8c2330da69 是哪個要回傳的客戶, 你就會利用這方式回傳訊息回去, 希望這樣不會太複雜

      刪除
    2. 謝謝您的回覆。
      我能否將我已經知道的兩個client的ip預先寫到我的server程式中。
      利用raw_input=1的時候傳hello給第一個client端,
      當raw_input=2的時候,傳hihi第二個client端,
      前提是我已經知道"兩個client的ip預先寫到我的server程式中"
      我想針對這兩個ip給予不同的資訊!
      是否可行呢?

      刪除
    3. 所以你的問題是要得知連線過來的 client 的 ip address 是嗎? 如果是的話, 使用 client.getpeername() 就可以得到連過來的 ip, port, 這是一個 tuple, 你可以看到 ip 之後回應你要回應的內容給對方

      刪除
    4. 您好,似乎不是。
      我的預想是
      client1=192.xxx.xxx.xx1
      client2=192.xxx.xxx.xx2
      def threadWork(client1,client2)
      while True:
      msg=raw_input()
      if msg=1
      client1.send('hello')
      msg = client1.recv(1024)
      else if msg =2
      client2.send('hihi')
      msg = client2.recv(1024)
      client1.close()
      client2.close()
      這樣是否可行呢??

      刪除

張貼留言

這個網誌中的熱門文章

[Linux] Linux下查詢硬體記憶體資訊 Memory Information

[Other] Chrome 重新整理所有開啟頁面