1. 程式人生 > >Python基礎 - 第八天 - Socket編程進階

Python基礎 - 第八天 - Socket編程進階

python

本篇內容:

1.解決socket粘包問題

2.通過socket傳輸大數據

3.socketserver的使用



一、解決socket粘包問題

1.粘包現象怎麽出現的

粘包是通過socket傳輸數據時不可避免的問題,也是我們要註意的問題。

當上次發送的數據和本次發送的數據是通過一次發送動作發送出去的,這樣就出現了粘包情況。

什麽情況下會將兩次發送操作合並成一次發送操作?在寫代碼時將兩次send寫在一起、緊挨著,中間沒有其它動作的情況下,就會合並發送次數。如下圖:

技術分享圖片


2.解決粘包問題的方法

當兩次發送的數據用途不一樣,並且兩次的數據是粘在一起發送給對端的,對端接收數據後就無法對數據做處理,這時我們就要解決粘包問題。

①一發一收的方式解決粘包問題

send發送數據後,緊接著再通過recv接收數據,這樣就會強制清空緩沖區,將緩沖區中的數據全部發送出去後再接收數據。如下圖:

技術分享圖片


②增加兩次發送操作的間隔時間

增加兩次send操作的間隔時間,這樣就會導致上一次緩沖區超時,從而不會等下一條了。註意,兩次send操作的間隔時間最少要有0.5秒;



二、通過socket傳輸大數據

通過socket不僅能傳輸容量小的數據,也能傳遞容量大的數據或文件。

下面是通過socket傳輸文件,並且驗證文件一致性的例子:

服務端:

import socket
import os
import hashlib


# 聲明socket類型,同時生成socket連接對象
# family默認是AF_INET,type默認是SOCK_STREAM,可以不用再寫了
server = socket.socket()

server.bind(("0.0.0.0", 55555))  # 綁定ip地址和端口號

server.listen(5)  # 監聽連接。和連接通信後最多可以掛起5個連接

while True:  # 服務端能接收多個連接
    conn, addr = server.accept()
    print("新連接", addr)

    while True:  # 能和任意的一個連接互相通信多次
        print("等待的新指令")
        cmd = conn.recv(1024)  # 接收數據,設置最大只能接收1K數據

        if not cmd:  # 當客戶端斷開後,會一直接收空數據,防止進入死循環
            print("客戶端斷開")
            break

        # 獲取動作和文件名
        action, filename = cmd.decode(encoding="utf-8").split()
        print("客戶端需要下載的文件是", filename)

        if os.path.isfile(filename):  # 文件存在
            file_size = os.stat(filename).st_size  # 獲取文件大小
            conn.send(str(file_size).encode(encoding="utf-8"))  # 向客戶端發送文件大小

            # 接收客戶端的確認信息。服務端一發一收從而解決粘包問題
            receive_client_ack = conn.recv(1024)
            print("接收到客戶端反饋的確認信息為", receive_client_ack.decode(encoding="utf-8"))

            m = hashlib.md5()  # 生成md5對象

            with open(filename, "rb") as f:
                for line in f:
                    conn.send(line)  # 按照每次只發送一整行內容的方式向客戶端發送
                    m.update(line)  # 將文件每一行的內容添加到md5中,註意hashlib模塊只能處理bytes類型的內容

            md5_value = m.hexdigest()  # 按照16進制格式顯示
            print("服務端計算的文件md5值為", md5_value)

            conn.send(md5_value.encode(encoding="utf-8"))  # 將文件的md5值發送給客戶端

        else:
            print("文件%s不存在" % filename)

        print("發送完成")


客戶端:

import socket
import hashlib


# 聲明socket類型,同時生成socket連接對象
# family默認是AF_INET,type默認是SOCK_STREAM,可以不用再寫了
client = socket.socket()

client.connect(("localhost", 55555))  # 連接的ip地址和端口號

while True:  # 客戶端能多次給服務端發送消息
    cmd = input("輸入命令\n>>>").strip()

    if len(cmd) == 0:  # send不能發送空內容,如果用send發送空內容會卡住
        print("輸入的命令為空")
        continue

    if cmd.startswith("get"):  # 解析命令的動作
        client.send(cmd.encode(encoding="utf-8"))  # 將動作和文件名發送給服務端

        server_response = client.recv(1024)  # 接收服務端發送的文件大小,設置最大只能接收1K數據
        file_size = server_response.decode(encoding="utf-8")  # 把文件大小從二進制轉換成字符串
        print("服務端發送的文件大小為 %s" % file_size)

        # 客戶端一收一發,為了解決粘包問題
        client.send("已經準備好接收文件了,可以開始發送了".encode(encoding="utf-8"))  # 給服務端發送確認信息

        file_total_size = int(file_size)  # 文件總大小,從字符串轉換成整型
        receive_size = 0  # 累計接收到的內容大小
        filename = cmd.split()[1]  # 獲取文件名

        m = hashlib.md5()  # 生成md5對象

        with open(filename + ".new", "wb") as f:
            while receive_size < file_total_size:  # 累計接收到的內容大小小於文件總大小就一直接收

                # 剩余的內容大小大於1024,代表需要接收的次數不止一次
                if file_total_size - receive_size > 1024:
                    size = 1024  # 接收大小為1024
                else:  # 剩余的內容大小小於1024,代表一次就可以接收完剩余數據
                    size = file_total_size - receive_size  # 接收大小為剩余數據的大小

                data = client.recv(size)  # 接收服務端發送的內容
                f.write(data)  # 寫入文件中
                receive_size += len(data)  # 將每次接收到數據的長度累加起來
                m.update(data)  # 將接收到的所有內容添加到md5中,註意hashlib模塊只能處理bytes類型的內容
            else:
                print("客戶端文件接收完成,客戶端接收到的文件大小為", receive_size)

                client_md5_value = m.hexdigest()  # 按照16進制格式顯示
                print("客戶端計算的文件md5值為", client_md5_value)

        # 這裏兩個recv是連在一起的,代表服務端就是兩個send連在一起,這樣就有可能出現粘包情況
        # 上面在接收文件時,在最後一次接收時,將接收大小設置為文件剩余內容的大小,這樣就解決了粘包的問題
        server_md5_value = client.recv(1024)  # 接收服務端發送的文件md5值
        print("服務端計算的文件md5值為", server_md5_value.decode(encoding="utf-8"))

    else:
        print("命令動作出錯,無法識別")



三、socketserver的使用

1.socketserver的類型

①TCPServer

使用因特網TCP協議,它提供了客戶端和服務器之間連續的數據流

語法:

socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)


②UDPServer

使用數據報,這是離散的信息包,到達順序可能不對或是在傳輸過程中出現故障或丟失。參數與TCPServer相同;

語法:

socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)


③UnixStreamServer和UnixDatagramServer

這些較不常用的類類似於TCP和UDP類,但是使用Unix域套接字,它們在非unix平臺上是不可用的。參數與TCPServer相同;

語法:

socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)

socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)


這幾個類的繼承關系是:

TCPServer繼承了BaseServer;

UnixStreamServer和UDPServer繼承了TCPServer;

UnixDatagramServer繼承了UDPServer;

技術分享圖片


2.創建一個socketserver至少需要遵循以下幾步(支持並發的類和不支持並發的類都需要遵循)

1.創建一個請求處理類,這個類要繼承socketserver.BaseRequestHandler類,並且還要重寫父類中的handle()方法(socketserver.BaseRequestHandler類中的handle()方法是空的。跟客戶端所有交互都是在handle()方法中完成的);


2.必須實例化一個服務器類(TCPServer或UnixStreamServer或UDPServer或UnixDatagramServer或ForkingTCPServer或ForkingUDPServer或ThreadingTCPServer或ThreadingUDPServer),並且將server地址(是一個包含ip地址和端口號的元組,和socket的地址一樣)和上面創建的請求處理類傳遞給這個服務器類;


3.

通過實例化生成的實例來調用handle_request() ,它只能處理一個連接的請求,處理完該連接請求後就會退出;(不管使用的是支持多並發的類,還是不支持多並發的類,它都只能處理一個連接,處理完該連接後就會退出)

通過實例化生成的實例來調用serve_forever(),它可以處理多個連接的請求(服務端能接收多個客戶端的連接,但如果使用的是不支持多並發的類的話,除正在交互的連接外,其它連接都被掛起),處理完連接的請求後不會退出,會永遠執行著;


4.通過實例化生成的實例來調用server_close()就關閉了socket;


3.socketserver的語法

承放數據內容的變量 = self.request.recv(數據量大小):接收消息。數據量大小的默認單位是字節。

self.request.send(二進制類型的消息):發送消息;

self.client_address:是一個具有兩個元素的小元組(host, port),self.client_address[0]代表客戶端的ip地址,self.client_address[1]代表客戶端使用的端口號;


server_close():清理服務器端,關閉服務器端;


request_queue_size:代表請求隊列的大小。如果需要很長時間來處理單個請求,那麽在服務器繁忙時接收到的任何請求都被放置到隊列中。一旦隊列滿了,來自客戶端的請求將獲得“拒絕連接”錯誤。默認值通常是5,但它可以被子類覆蓋。(相當於普通socket的object.listen(backlog)中的backlog參數)


allow_reuse_address:服務器是否允許重用地址,也就是地址重用功能。這默認為false,並且可以在子類中設置以更改策略。(可以解決地址被占用的問題)


4.socketserver實例,不支持多並發

服務端能接收多個客戶端的連接,但同時只能和一個客戶端的連接進行通信交互,其它連接被掛起。每個客戶端都能向服務端多次發送消息。

服務端:

使用的是socketserver模塊

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    """創建MyTCPHandler請求處理類,並且MyTCPHandler類繼承了BaseRequestHandler類"""

    # 重寫handle()方法
    def handle(self):
        """處理跟客戶端交互的方法函數"""

        while True:  # 使服務端能和一個連接進行多次通信

            # 當客戶端斷開後,服務端不會出現一直接收空數據從而進入死循環狀態
            # 當客戶端斷開後,服務端會拋出ConnectionResetError錯誤,所以這裏要抓取異常
            try:

                # 接收客戶端發送的消息
                # 這裏是self,不再是conn了,代表每接收到一個客戶端的請求,都會實例化MyTCPHandler類
                self.data = self.request.recv(1024).strip()

                print("{} 發的消息是: ".format(self.client_address[0]))
                print(self.data.decode(encoding="utf-8"))

                # 向客戶端發送消息
                self.request.send(self.data.upper())

            except ConnectionResetError as e:
                print("錯誤信息為", e)
                break  # 和客戶端斷開連接


if __name__ == "__main__":

    # 定義連接服務端的ip地址和要訪問服務端的服務端口
    HOST, PORT = "localhost", 55555

    # 實例化TCPServer類,並且在實例化時將地址、MyTCPHandler類當作參數傳遞進去
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()  # 可以處理多個請求,會永遠執行著


客戶端:

使用的是socket模塊

import socket


# 聲明socket類型,同時生成socket連接對象
# family默認是AF_INET,type默認是SOCK_STREAM,可以不用再寫了
client = socket.socket()

client.connect(("localhost", 55555))  # 連接服務端的ip地址和要訪問服務端的服務端口

while True:  # 客戶端能多次給服務端發送消息

    message = input("輸入你想要發送的消息\n>>>").strip()

    if len(message) == 0:  # send不能發送空內容,如果用send發送空內容會卡住
        print("輸入的命令為空")
        continue

    # socket只能發送二進制類型的內容
    client.send(message.encode(encoding="utf-8"))  # 只能發送bytes類型,比特流的bytes類型

    data = client.recv(1024)  # 接收服務器端響應的數據,設置最多可以接收1K的數據,單位默認為字節
    print("服務器端響應的數據為: %s" % data.decode(encoding="utf-8"))


5.通過socketserver實現多並發

①讓socketserver並發起來,必須要使用以下的一個多並發的類:

socketserver.ForkingTCPServer

socketserver.ForkingUDPServer

socketserver.ThreadingTCPServer

socketserver.ThreadingUDPServer


②修改方法

把下面這句代碼
  server = socketserver.TCPServer((HOST, PORT), 創建的請求處理類的類名)
修改成下面的代碼,就可以多並發了
  server = socketserver.ThreadingTCPServer((HOST, PORT), 創建的請求處理類的類名)
  server = socketserver.ThreadingUDPServer((HOST, PORT), 創建的請求處理類的類名)
  server = socketserver.ForkingTCPServer((HOST, PORT), 創建的請求處理類的類名)
  server = socketserver.ForkingUDPServer((HOST, PORT), 創建的請求處理類的類名)

只用修改類,實例化時要求傳遞的參數都是一樣的;


③進程和線程的詳解

● Threading:線程(生成一個線程的開銷非常小,推薦使用)

server = socketserver.ThreadingTCPServer((HOST, PORT), 創建的請求處理類的類名)
server = socketserver.ThreadingUDPServer((HOST, PORT), 創建的請求處理類的類名)

客戶端每連進一個連接,服務器端就會分配一個新的線程來處理這個客戶端的請求;

● Forking:進程(生成一個進程的開銷非常大)

server = socketserver.ForkingTCPServer((HOST, PORT), 創建的請求處理類的類名)
server = socketserver.ForkingUDPServer((HOST, PORT), 創建的請求處理類的類名)

客戶端每連進一個連接,服務器端就會分配一個新的進程來處理這個客戶端的請求;

註意,在windows上使用Forking會出現問題,要在linux上使用Forking;


④多並發的例子

服務端:

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    """創建MyTCPHandler請求處理類,並且MyTCPHandler類繼承了BaseRequestHandler類"""

    # 重寫handle()方法
    def handle(self):
        """處理跟客戶端交互的方法函數"""

        while True:  # 使服務端能和一個連接進行多次通信

            # 當客戶端斷開後,服務端不會出現一直接收空數據從而進入死循環狀態
            # 當客戶端斷開後,服務端會拋出ConnectionResetError錯誤
            # 所以這裏要抓取異常
            try:

                # 接收客戶端發送的消息
                # 這裏是self,不再是conn了,代表每接收到一個客戶端的請求,都會實例化MyTCPHandler類
                self.data = self.request.recv(1024).strip()

                print("{} 發的消息是: ".format(self.client_address[0]))
                print(self.data.decode(encoding="utf-8"))

                # 向客戶端發送消息
                self.request.send(self.data.upper())

            except ConnectionResetError as e:
                print("錯誤信息為", e)
                break  # 和客戶端斷開連接


if __name__ == "__main__":

    # 定義連接服務端的ip地址和要訪問服務端的服務端口
    HOST, PORT = "localhost", 55555

    # 實例化ThreadingTCPServer類,支持多並發,並且在實例化時將地址、MyTCPHandler類當作參數傳遞進去
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()  # 可以處理多個請求,會永遠執行著
    # server.handle_request()  # 只能處理一個連接的請求,處理完該連接請求後就會退出


客戶端:

import socket


# 聲明socket類型,同時生成socket連接對象
# family默認是AF_INET,type默認是SOCK_STREAM,可以不用再寫了
client = socket.socket()

client.connect(("localhost", 55555))  # 連接服務端的ip地址和要訪問服務端的服務端口

while True:  # 客戶端能多次給服務端發送消息

    message = input("輸入你想要發送的消息\n>>>").strip()

    if len(message) == 0:  # send不能發送空內容,如果用send發送空內容會卡住
        print("輸入的命令為空")
        continue

    client.send(message.encode(encoding="utf-8"))  # 只能發送bytes類型,比特流的bytes類型

    data = client.recv(1024)  # 接收服務器端響應的數據,設置最多可以接收1K的數據,單位默認為字節
    print("服務器端響應的數據為: %s" % data.decode(encoding="utf-8"))

本文出自 “12031302” 博客,謝絕轉載!

Python基礎 - 第八天 - Socket編程進階