Socket 通信之 TCP 通信

上篇文章主要總結了 UDP 套接字的通信方式,這篇文章主要講解 TCP 套接字的通信方式。

TCP 通信流程

一臺主機可以即作為服務端又作為客戶端,因此在 UDP 通信流程中將二者串聯起來了,沒有進行區分。由于 TCP 通信相對于 UDP 通信要復雜一點,這里將 TCP 通信拆分為服務端和客戶端,方便理解。
首先是服務端的通信流程:

  • 創建服務端套接字
  • 綁定本機 IP 地址和端口(不必須)
  • 將主動套接字轉換為被動套接字(不必須)
  • 監聽客戶端請求
  • 接收客戶端請求
  • 發送/接受消息
  • 關閉客戶端套接字
  • 關閉服務端套接字

客戶端的通信流程如下:

  • 創建客戶端套接字
  • 綁定本機 IP 地址和端口(不必須)
  • 連接服務端
  • 發送/接受消息
  • 關閉客戶端

下面依次進行講解。

服務端通信流程

首先看一下服務端的通信流程。

創建服務端套接字

使用 TCP 通信,需要創建 TCP 的套接字:

sSocket = socket(AF_INET, SOCK_STREAM)

綁定服務器的 IP 和端口

sSocket.bind((IP, PORT))

同使用 UDP 套接字,綁定 IP 和端口是不必須的,如果要綁定本機上所有合法的 IP,可以這樣寫:

sSocket.bind(("",PORT))

將主動套接字轉換為被動套接字

主動套接字是創建 TCP 套接字對象后的默認行為,只能向其他的主機發送消息,如果需要接收其他主機的消息,就需要將其轉換為被動套接字,轉換后就可以同時進行收發了,如果我們的主機不想接收其他主機的消息,就不需要轉換為被動套接字,因此這項也不是必須的。
將主動套接字轉換為被動套接字很簡單,只需要調用套接字對象的 listen 方法:

sScoket.listen( maxConnect )

listen 函數接受一個參數,表示最大連接數。

接收客戶端請求

執行 accept 方法以接收客戶端的請求,該方法是一個阻塞方法:

clientSocket, clientInfo = sSocket.accept()

accept 方法返回一個元組,元組的第一項一個新的套接字對象,專門用來處理和相應的客戶端的通信,元組的第二項是客戶端的 IP 和端口信息。

發送/接受消息

發送消息使用 send 方法,接受消息使用 recv 方法:

# 發送消息
clientSocket.send(byte)

# 接收消息
recvData = clientSocket.recv( maxLen )

send 方法用來發送信息,接受一個 byte 類型的消息。
recv 方法用來接收信息,接受一個最大接收長度作為參數。
注意:以上兩個方法都由特定的客戶端套接字對象調用。

關閉套接字

通信完成后需要關閉套接字,首先需要關閉客戶端套接字,最后當所有的客戶端消息處理完成后,需要關閉服務端套接字:

# 關閉客戶端套接字
clientSocket.close()

# 關閉服務端套接字
sScoket.close()

客戶端通信流程

下面講解客戶端的通信流程,由于前面已有介紹,這里就不再贅述綁定和關閉套接字了。

創建套接字

首先需要在客戶端創建一個套接字對象:

cSocket = socket( AF_INET, SOCK_STREAM)

連接服務端

連接服務端需要使用 connect 方法,該方法接受服務端的 IP 地址和對應的端口作為參數:

cSocket.connect( IP, PORT )

該方法也是阻塞的,連接過程會耗費一定時間。連接成功后,會觸發客戶端 Socket 對象的 accept 方法。

發送/接收消息

使用 send 方法向服務端發送消息,使用 recv 從服務端接收消息:

# 發送消息
cSocket.connect( msg )

# 接收消息
cSocket.recv( maxLen )

由于前面已經使用 connect 將客戶端和服務端進行了連接,因此在使用 send 方法的時候不必再傳入服務端相關的 IP 和端口信息了。

簡單實例

下面做一個客戶端和服務端通信的例子。首先是服務端代碼 server.py:

from socket import *

def main():
    # 創建服務端 socket 對象
    sSocket = socket(AF_INET, SOCK_STREAM)
    # 綁定本機端口
    sSocket.bind(("",3001))
    # 轉換為被動套接字
    sSocket.listen(5)
    # 下面是一個輪詢,在每次有客戶端請求時進行處理
    while True:
        # 監聽客戶端請求
        clientSocket,clientAddr = sSocket.accept()
        print("%s 已連入,正在接受消息..."%clientAddr[0])
        while True:
            # 接受客戶端消息
            try:
                recvMsg = clientSocket.recv(1024)
            except:
                # 如果觸發異常,說明客戶端斷開了連接
                print("%s 已斷開連接~"%clientAddr[0])
                break
            print("%s:%s"%(clientAddr[0],recvMsg.decode("utf-8")))
            # 回復消息
            clientSocket.send("ding~".encode("utf-8"))
        # 關閉客戶端套接字
        clientSocket.close()

if __name__ == '__main__':
    main()

接著是客戶端的代碼,client.py:

from socket import *

def main():
    cSocket = socket(AF_INET, SOCK_STREAM)
    cSocket.connect(("192.168.2.142",3001))
    while True:
        msg = input("Enter Message:")
        if msg == "q!":
            break
        else:
            cSocket.send(msg.encode("utf-8"))

    cSocket.close()

if __name__ == '__main__':
    main()

我們看到 server.py 中有兩個死循環,最外層的死循環用于和其他主機進行連接,連接成功后調用內存循環,用來和客戶機通信。通信結束后,跳出內層循環,等待下一次連接。
如果客戶端關閉了連接,服務端的 recv 方法在運行時會產生異常,我們可以通過異常的捕獲來判斷客戶端是否斷開了連接。
下面是運行效果:

tcp通信.gif

實例改進

上面的實例有個問題:服務器一次只能處理一個 Socket 通信,只有在上一個 Socket 通信處理完成后,才能進行下一次通信的處理。這是因為外層的 while 循環需要內層的 while 循環執行完成后再執行,其他時間它都是阻塞的,只有這一次的 Socket 的通信處理完了,外層的 while 循環才能進行下一次的處理。兩個客戶端同時請求服務端的運行效果如下:

tcp通信02.gif

要解決這個問題也很簡單,既然是多連接多任務處理,我們只需將內層的 while 循環放在進程中,此后每新增一個連接,就為其開一個進程進行處理,實現并發操作。
修改 server.py:

from socket import *
from multiprocessing import Process

class Server():
    @classmethod
    def __prepareSocket(cls):
        # 將服務端的 Socket 對象作為類成員
        cls.sSocket = socket(AF_INET, SOCK_STREAM)
        cls.sSocket.bind(("",3001))
        cls.sSocket.listen(5)

    @classmethod
    def startServer(cls):
        cls.__prepareSocket()
        while True:
            # 監聽客戶端請求
            clientSocket,clientAddr = cls.sSocket.accept()
            print("%s 已連入,正在接受消息..."%clientAddr[1])
            cp = SocketHander(clientSocket,clientAddr)
            cp.start()


class SocketHander(Process):
    def __init__(self,clientSocket,clientAddr):
        Process.__init__(self)
        self.clientSocket = clientSocket
        self.clientAddr = clientAddr

    def run(self):
        # 將內層循環至于 run 方法中
        while True:
            try:
                recvMsg = self.clientSocket.recv(1024)
            except:
                # 如果觸發異常,說明客戶端斷開了連接
                print("%s 已斷開連接~"%self.clientAddr[0])
                break
            print("%s:%s"%(self.clientAddr[0],recvMsg.decode("utf-8")))
            # 回復消息
            self.clientSocket.send("ding~".encode("utf-8"))
        # 關閉客戶端套接字
        self.clientSocket.close()

if __name__ == '__main__':
    Server.startServer()

看下效果:

tcp通信03.gif

完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容