Socket編程

Socket編程


前言

在網絡編程裏總會涉及到socket編程,或者說,網絡編程是基於socket之上的。通過socket,我們可以建立tcp連接,或是udp通訊方式。虧得Python的完美封裝,Socket編程變得容易上手。

接下來會寫一個基於tcp方式的簡易終端聊天系統。

實例一個socket

在Python中創建一個socket對象需要導入socket包,還需要指定協議。

import socket

socketObj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  • AF_INET 表示使用ipv4協議;
  • SOCK_STREAM 表示連接基於tcp,如果想用udp,則爲SOCK_DGRAM

此時socketObj是程序中的關鍵。關於它的常用方法有以下:

  • bind() 綁定地址(ip和端口),接收參數爲元組類型。如:socketObj.bind(("0.0.0.0", 8080))
  • listen() 設置監聽,只有服務器端需要設置。接收參數爲int類型。socketObj.listen(10)表示最多接收10個客戶端的連接;
  • accept() 等待連接,只有服務器端需要設置。返回一個元組,包含了與客戶端連接的socket和客戶端的地址信息(這個地址信息也是個元組,與bind()函數接收的參數格式一樣);
  • connect() 用於客戶端連接服務器,參數爲服務器的地址信息。如:client.connect(("0.0.0.0", 8080))
  • recv() 接收數據。接收參數爲int型,表示一次接收數據的字節長度;返回接收到的數據,此時data爲byte型。存在**recvfrom()**方法,不但返回接收到的數據信息,還會返回發送端的地址信息;
  • send() 發送數據。接收參數爲byte型,返回發送出去的字節長度。這個方法不需要指定接收端的地址信息,但存在**sendto()**方法,需要指定接收端的地址信息:client.sendto(data, address)
  • close() 關閉套接字。

說明:accept()和recv()都會阻塞程序,通過socket.setblocking(False)可以設置爲非阻塞,但程序會拋BlockingIOError異常,可以用try-except忽略掉。

服務器與客戶端

作爲服務器所需步驟:
第一步綁定地址(bind),第二步設置監聽(listen),第三步等待連接(accept),第四步循環接收數據與發送數據操作(send、recv)。

作爲客戶端所需步驟:
第一步連接服務器(connect),第二步循環接收數據與發送數據操作( send、recv)。

這裏在網上找了一張流程圖方便理解,侵刪。
在這裏插入圖片描述

簡易聊天系統

在這個聊天系統中,有以下幾個要求:

  1. 服務器只能被一個客戶端連接;
  2. 服務器與客戶端可以任意時候發送數據和接收數據;
  3. 客戶端通過輸入q! 或者Q! 命令,實現退出聊天系統的操作;
  4. 客戶端退出後,服務器進入等待連接狀態,直到下一個客戶端進入連接。

要求1很好實現,只需要listen(1)即可。

對於要求2,不論是客戶端還是服務器,因爲需要 “任意時候可以發送數據和接收數據”,涉及到的inputrecv都會阻塞程序,所以需要抽象出兩個方法send_data()recv_data() ,由兩個線程執行,防止終端被霸佔。

def send_data(sock):
    while True:
        words = input(">>")
        ...
        sock.send(words.encode("utf-8"))
        ...

def recv_data(sock, addr):
    while True:
	    ...
        data = sock.recv(1024)
		...
		
		# 格式化打印接收到的數據
        dataUTF8 = data.decode("utf-8")
        print("\r【時間:{time}】【來自:{ip}】\n【內容:{content}\n{separate}".format(
            time=time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()),
            ip="%s:%d" % addr,
            content=dataUTF8,
            separate="-"*50
        ))
        print(">>", end="")
        sys.stdout.flush()  # 強刷管道,不然`>>`可能打印不出來

這裏需要注意,多線程時print(">>", end="")會打印不出來,但print(">>")這種格式可以正常輸出到屏幕。我知道是因爲內容被緩存到了管道中,導致終端沒有輸出,然而根本原因不明確。不過sys.stdout.flush()可以刷新管道,數據能被正常打印了。

要求3不難,只需要在send_data()中增加標準輸入的檢查,當輸入的內容是q!或者Q!,退出循環,結束此線程。

# client.py
def send_data(sock):
    while True:
        words = input(">>")
        if words in ("q!", "Q!"):
            break
        sock.send(words.encode("utf-8"))

執行send_data()的線程結束後,需要程序正常往下執行,所以主線程只等待send_data() ,而不等待執行recv_data() 函數的線程:

# client.py
sendthreading = threading.Thread(target=send_data, args=(client, ))
recvthreading = threading.Thread(target=recv_data, args=(client, ("127.0.0.1", 8080)))

sendthreading.start()
recvthreading.start()

sendthreading.join()  # 只等待send_data()的線程結束
client.close()

win與linux的差別:

接下來需要區分win與linux中的區別。當send_data()結束,程序接下來會執行client.close(),同時還有個子線程負責recv_data(),也就是說這個函數中還使用着套接字client。然而,套接字卻被主線程關閉了——注意這個大前提。現在,在win中,當套接字被關閉,程序客戶端(client.py)會拋異常ConnectionAbortedError,但linux中什麼也不會發生;此時服務器與客戶端之間的連接斷開,win中,服務器端會在recv句中拋異常ConnectionResetError,而在linux中服務器的recv此時將一直接收空數據,導致程序服務器一直打印空數據。爲了兼容兩個系統,在server.py中需要判斷接收到的數據是否爲有效數據:

def recv_data(addr):
    global clientsock
    while True:
        try:
            data = clientsock.recv(1024)
            # 兼容linux
            if not data:  # 如果不是有效數據,拋出異常
                raise ConnectionResetError
        except ConnectionResetError:
            ...

客戶端裏,當拋出ConnectionAbortedError時,跳出接收線程的循環:

# client.py
def recv_data(sock, addr):
    while True:
        try:
            data = sock.recv(1024)
        except ConnectionAbortedError:
            break
		...

當然還要考慮到服務器異常退出時,此時客戶端——如果在linux系統,會一直接收到空數據,如果在win系統,會引發ConnectionResetError異常,所以繼續完善上面代碼:

# client.py
def recv_data(sock, addr):
    while True:
        try:
            data = sock.recv(1024)
            # 兼容linux
            if not data:
                raise ConnectionResetError("遠程主機強迫關閉了一個現有的連接。")
        except ConnectionAbortedError:
            break

認爲服務器退出之後,客戶端應該拋錯提示,所以不對ConnectionResetError做異常處理。

又因爲在linux中客戶端套接字被關掉,recv不會報錯,它所在線程(負責recv_data()的線程)不會終止。正常情況下,主線程運行結束後等待子線程結束。但我們希望主線程結束後子線程也會跟着死亡,所以把該線程設置爲守護。表示主線程結束,子線程跟着結束:

recvthreading.setDaemon(True)  # 兼容linux

爲滿足要求4,在文件server.py中做以下準備:

# server.py
def recv_data():
    global clientsock
    while True:
        try:
            data = clientsock.recv(1024)
            # 兼容linux
            if not data:
                raise ConnectionResetError
        except ConnectionResetError:
            # 用戶退出後進入等待模式
            print("\r【用戶已退出】")
            print("【等待用戶連接】")
            clientsock, addr = server.accept()
            print("【接入用戶】")
            print(">>", end="")
            continue
		...

運行效果

在這裏插入圖片描述

此次完整代碼已經上傳Github,見簡易聊天系統-多線程目錄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章