上篇文章主要總結了 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
方法在運行時會產生異常,我們可以通過異常的捕獲來判斷客戶端是否斷開了連接。
下面是運行效果:
實例改進
上面的實例有個問題:服務器一次只能處理一個 Socket 通信,只有在上一個 Socket 通信處理完成后,才能進行下一次通信的處理。這是因為外層的 while
循環需要內層的 while
循環執行完成后再執行,其他時間它都是阻塞的,只有這一次的 Socket 的通信處理完了,外層的 while
循環才能進行下一次的處理。兩個客戶端同時請求服務端的運行效果如下:
要解決這個問題也很簡單,既然是多連接多任務處理,我們只需將內層的
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()
看下效果:
完。