Python3之socket編程

Python3之socket編程解決粘包問題

什麼是粘包
當發送網絡數據時,tcp協議會根據Nagle算法將時間間隔短,數據量小的多個數據包打包成一個數據包,先發送到自己操作系統的緩存中,然後操作系統將數據包發送到目標程序所對應操作系統的緩存中,最後將目標程序從緩存中取出,而第一個數據包的長度,應用程序並不知道,所以會直接取出數據或者取出部分數據,留部分數據在緩存中,取出的數據可能第一個數據包和第二個數據包粘到一起。

粘包解決方案
由於應用程序自己發送的數據可以進行打包處理,自己製作協議,對數據進行封裝添加報頭,然後發送數據部分。而報頭必須是固定長度,對方接受時可以先接受報頭,對報頭進行解析,然後根據報頭內的封裝的數據的長度對數據進行讀取,這樣收取的數據就是一個完整的數據包

具體代碼實現
struct模塊的使用

被struct打包的數據會變成bytes格式,這樣便於數據的網絡傳輸

服務端

import socket
import struct
import subprocess

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))

phone.listen(5)
while 1:
    conn, addr = phone.accept()
    while 1:
        try:
            data = conn.recv(1024)
            res = subprocess.Popen(data.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = res.stdout.read()
            stderr = res.stderr.read()
            # 發送報頭
            res = struct.pack('i', len(stdout) + len(stderr))
            conn.send(res)
            # 發送數據部分
            conn.send(stdout)
            conn.send(stderr)
        except Exception:
            break
    conn.close()
phone.close()

客戶端

import socket
import struct

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))

while 1:
    cmd = input('請輸入>>:').strip()
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    # 接受報頭
    res = phone.recv(4)
    # 對報頭解壓
    data_size = struct.unpack('i', res)[0]
    # 根據報頭數據長度對數據進行接收
    recv_size = 0
    total_data = b''
    while recv_size < data_size:
        data_recv = phone.recv(1024)
        if (data_size - len(data_recv)) < 1024:
            left_data = phone.recv(data_size - len(data_recv))
            total_data += left_data
        total_data += data_recv
        recv_size += len(data_recv)

    data = phone.recv(1024)
    print(data.decode('gbk'))

phone.close()

Python之解讀Socketserver & Tcpserver部分問題記錄
在解析socketserver是如工作之前,我們先看看socektserver類的繼承關係圖:

請求類繼承關係:
  在這裏插入圖片描述

server類繼承關係:
  在這裏插入圖片描述

有了上面的繼承關係圖後,我們解析socketserver就輕鬆多了,下面,我們從代碼開始,慢慢揭開socketserver面紗:

import socketserver
import struct, json, os

class FtpServer(socketserver.BaseRequestHandler):
    coding = 'utf-8'
    server_dir = 'file_upload'
    max_packet_size = 1024
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))

    def handle(self):
        print(self.request)
        while True:
            data = self.request.recv(4)
            data_len = struct.unpack('i', data)[0]
            head_json = self.request.recv(data_len).decode(self.coding)
            head_dic = json.loads(head_json)
            cmd = head_dic['cmd']
            if hasattr(self, cmd):
                func = getattr(self, cmd)
                func(head_dic)

    def put(self):
        pass

    def get(self):
        pass

if __name__ == '__main__':
    HOST, PORT = "localhost", 9999
    with socketserver.ThreadingTCPServer((HOST, PORT), FtpServer) as server:
        server.serve_forever()

Socket定長通訊讀取消息長度頭(java)

###一、前言
數據在網絡傳輸時使用的都是字節流,Socket也不例外,所以我們發送數據的時候需要轉換爲字節發送,讀取的時候也是以字節爲單位讀取。
那麼問題就在於socket通訊時,接收方並不知道此次數據有多長,因此無法精確地創建一個緩衝區(字節數組)用來接收,在不定長通訊中,通常使用的方式時每次默認讀取8*1024長度的字節,若輸入流中仍有數據,則再次讀取,一直到輸入流沒有數據爲止。但是如果發送數據過大時,發送方會對數據進行分包發送,這種情況下或導致接收方判斷錯誤,誤以爲數據傳輸完成,因而接收不全。
所以,大部分情況下,雙方使用socket通訊時都會約定一個定長頭放在傳輸數據的最前端,用以標識數據體的長度,通常定長頭有整型int,短整型short,字符串Strinng三種形式。
###二、整型與短整型
int型的長度數據會以4個byte存放,所以可以讀取前四個字節的數據,然後轉換爲int類型:

InputStream is = socket.getInputStream();
byte[] datalen = new byte[4];
is.read(datalen);//讀取前四個字節數據存放到datalen中
int length = byteArrayToInt(datalen)//將字節數組轉換爲int型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//將獲得數據轉爲字符串類型

byteArrayToInt的方法實現如下:

public static int byteArrayToInt(byte[] b){
	return b[3]&0xFF | (b[2]&0xFF) << 8 | (b[1]&0xFF) << 16 | (b[0]&0xFF) << 24 
}

當然還有一個更簡單的方法,使用DataInputStream:

DataInputStream is = new DataInputStream(socket.getInputStream());
int datalen = is.readInt();//讀取前四位字節並返回int類型數據
byte[] data = new byte[datalen];
is.readFully(data);
//使用阻塞的方式讀取完整的datalen長度的數據
String recvMsg = new String(data);//將獲得數據轉爲字符串類型

使用DataInputStream不但可以簡化代碼,而且它的readFully方法會以阻塞等待的形式讀取完整datalen長度的數據,而上面的read方法有可能出現沒有讀取完整的情況,因此對於定長通訊推薦使用DataInputStream和readFully方法;
同樣,對於短整型short,DataInputStream也帶有對應的方法:

int datalen = is.readShort();//讀取前兩個字節數據並返回short型數據

同樣也可以使用read讀取前兩位再轉爲int,再次不多贅述。
###三、字符串類型
字符串類型的好處是可以自行約定長度,將長度轉爲字符串,然後通過左補零的形式達到約定長度。假設約定定長頭爲8位,數據長度爲1480,則這筆數據傳輸的前八位定長頭爲“00001480”。
處理方式爲先讀取約定長度字節,然後直接轉爲String類型,再轉爲int型

InputStream is = socket.getInputStream();
byte[] datalen = new byte[8];
//假設約定8位
is.read(datalen);
int length = Integer.parseInt(new String(datalen));//將字節數組轉爲字符串,再轉爲int類型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//將獲得數據轉爲字符串類型

總結:
對於socket短鏈接通訊,建議使用定長頭標識通訊內容長度,並且不宜發送過大的報文,以免通訊過程中產生內容丟失,比如網絡丟包等情況。然後接收時儘量使用DataInputStream的readFully方法讀取確定長度的數據

簡單理解socket(AF_INET&SOCK_STREAM,SOCK_DGRAM)

套接字
在任何類型的通信開始之前,網絡應用程序都必須創建套接字。

*套接字最初是爲同一主機上的應用程序所創建,使得主機上運行的一個程序(又名一個進程)與另一個運行的程序進行通信。這就是所謂的進程間通信(Inter Process Communication,IPC)*

有兩種類型的套接字:基於文件的和麪向網絡的。

基於文件的
家族名:AF_UNIX

(又名AF_LOCAL,在POSIX1.g標準中指定),它代表地址家族(addressfamily):UNIX。其他比較舊的系統可能會將地址家族表示成域(domain)或協議家族(protocolfamily),並使用其縮寫PF而非AF。類似地,AF_LOCAL(在2000~2001年標準化)將代替AF_UNIX

面向網絡的
家族名:AF_INET

或者地址家族:因特網。另一個地址家族AF_INET6用於第6版因特網協議(IPv6)尋址。此外,還有其他的地址家族,這些要麼是專業的、過時的、很少使用的,要麼是仍未實現的。在所有的地址家族之中,目前AF_INET是使用得最廣泛的

總的來說,Python只支持AF_INET、AF_UNIX、AF_NETLINK和AF_TIPC家族

套接字地址:主機-端口對
做個比喻,套接字就像一個電話插孔,主機名和端口號就像區號和號碼。
當程序之間需要通信時,需要知道對端的主機名(IP)和端口號。
有效的端口號範圍爲0~65535(小於1024的端口號預留給了系統)

面向連接的套接字與無連接的套接字
面向連接的套接字
TCP套接字的名字SOCK_STREAM。
特點:可靠,開銷大。
在進行通信之前必須先建立一個連接,該連接的通信提供序列化的、可靠的和不重複的數據交付,而沒有記錄邊界。這種類型的通信也稱爲虛擬電路或流套接字。
實現這種連接類型的主要協議是傳輸控制協議(縮寫 TCP)
爲了創建 TCP套接字,必須使用 SOCK_STREAM 作爲套接字類型。

無連接的套接字
UDP套接字的名字SOCK_DGRAM
特點:不可靠(局網內還是比較可靠的),開銷小。
與虛擬電路形成鮮明對比的是數據報類型的套接字,它是一種無連接的套接字。
在通信開始之前並不需要建立連接。此時,在數據傳輸過程中並無法保證它的順序性、可靠性或重複性。數據報確實保存了記錄邊界,這就意味着消息是以整體發送的,而並非首先分成多個片段。
實現這種連接類型的主要協議是用戶數據報協議(縮寫 UDP)。爲
了創建UDP套接字,必須使用SOCK_DGRAM作爲套接字類型。
UDP套接字的SOCK_DGRAM名字來自於單詞“datagram”(數據報)。

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