1. 程式人生 > >鐵樂學Python_Day34_Socket模塊2和黏包現象

鐵樂學Python_Day34_Socket模塊2和黏包現象

官方文檔 bsd tde turn 顯示 這就是 mps lin 編碼

鐵樂學Python_Day34_Socket模塊2和黏包現象

套接字

套接字是計算機網絡數據結構,它體現了C/S結構中"通信端點"的概念。
在任何類型的通信開始之前,網絡應用程序必須創建套接字。
可以將它們比作成電話插孔,沒有它將無法進行通信。
套接字最初是為同一主機上的應用程序所創建,使得主機上運行的一個程序(又名一個進程)與另一個運行的程序進行通信。
這就是所謂的進程間通信(Inter Process Communication, IPC)。

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

基於文件 AF_UNIX

地址家族(address family):UNIX(術語)
兩個進程運行在同一臺計算機上,所以這些套接字都是基於文件的,這意味著文件系統支持它們的底層基礎結構。
這是顯而易見的,因為文件系統是運行在同一主機上的多個進程之間的共享常量。

基於網絡 AF_INET

地址家族:因特網

AF_NETLINK(無連接)

python 2.5引入了對特殊類型的Linux套接字的支持。
套接字的AF_NETLINK家族(無連接)允許使用標準的BSD套接字接口進行用戶級別和內核級別代碼之間的IPC。
例如,添加新系統調用、/proc支持,或者對一個操作系統的"IOCTL"。

AF_TIPC (透明的進程間通信)

python 2.6 新增針對linux的另一種特性,支持透明的進程間通信(TIPC)協議。
TIPC允許計算機集群之中的機器相互通信,而無須使用基於IP的尋址方式。

套接字地址: 主機-端口對

一個網絡地址由主機名和端口號對組成,而這是網絡通信所需要的。

面向連接的套接字與無連接的套接字

1、面向連接的套接字

意味著在進行通信之前必須先建立一個連接,例如,使用電話系統給一個朋友打電話。
這種類型的通信也稱為虛擬電路或流套接字。
面向連接的通信提供序列化的、可靠的和不重復的數據交付,而沒有記錄邊界。
意味著每條消息可以拆分成多個片段,並且每一條消息片段都確保能夠到達目的地,
然後將它們按順序組合在一起,最後將完整消息傳遞給正在等待的應用程序。

實現這種連接類型的主要協議是傳輸控制協議(TCP)。
為了創建TCP套接字,必須使用SOCK_STREAM作為套接字類型。(staeam.溪流)

2、無連接的套接字

數據報類型,它是一種無連接的套接字。在通信開始之前並不需要建立連接。
在數據傳輸過程中並無法保證它的順序性、可靠性或重復性。
數據報保存了記錄邊界,消息是以整體發送的,而並非首先分成多個片段。
使用數據報的消息傳輸可以比作郵政服務。
信件和包裹或許並不能以發送順序到達。甚至可能不會到達。
為了將其添加到並發通信中,在網絡中甚至有可能存在重復的消息。

實現這種連接類型的主要協議是用戶數據報協議(UDP)。
為了創建UDP套接字,必須使用SOCK_DGRAM作為套接字類型。(datagram.數據報)

socket()模塊函數

要創建套接字,必須使用socket.socket()函數,它一般的語法如下:
socket(socket_family, socket_type, protocol=0)
其中,socket_family 是AF_UNIX或AF_INET, socket_type 是SOCK_STREAM 或SOCK_DGRAM, 
protocol通常省略,默認為0。

創建TCP/IP套接字:
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
創建UDP/IP套接字:
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

因為有很多socket模塊屬性,所以此時使用‘from module import *‘這種導入方式可以簡便許多,
使用‘from socket import *‘,就把socket屬性引入到命名空間中,雖然看起來有些麻煩,
但是通過這種方式將能夠大大縮短代碼,如:
tcpSock = socket(AF_INET, SOCK_STREAM)

一旦有了一個套接字對象,那麽使用套接字對象的方法將可以進行進一步的交互。

黏包

執行多條命令之後,得到的結果很可能只有一部分,在執行其他命令的時候又接收到之前執行的另外一部分結果,
這種現像就是黏包。

註意:只有TCP有黏包現象,UDP永遠不會粘包。
(udp面向數據報,是有消息邊界的。)

例:socket TCP仿ssh遠程執行命令服務器
#!/usr/bin/env python
# _*_ coding: utf-8 _*_

from socket import *
import subprocess

‘‘‘
socket TCP仿ssh遠程執行命令服務器
‘‘‘

HOST = ‘localhost‘
PORT = 9527
ADDR = (HOST, PORT)
BUFSIZ = 1024

tcpss = socket(AF_INET, SOCK_STREAM)
tcpss.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
tcpss.bind(ADDR)
tcpss.listen(5)

while True:
    conn, addr = tcpss.accept()
    print(‘接受客戶端連接中...:‘, addr)
    while True:
        # 接收客戶端發過來的cmd命令
        cmd = conn.recv(BUFSIZ)
        # 命令為空時退出循環
        if not cmd:break
        ‘‘‘
        subprocess通過子進程來執行外部指令,shell參數為True時,直接傳入命令;
        Popen方法用於先進入到某個輸入環境,再執行一系列指令;
        stdin 程序的標準輸入,stdout 標準輸出 stderr 標準錯誤。
        ‘‘‘
        result = subprocess.Popen(cmd.decode(‘utf-8‘), shell=True,
                                  stdout = subprocess.PIPE,
                                  stdin  = subprocess.PIPE,
                                  stderr = subprocess.PIPE )

        stderr = result.stderr.read()
        stdout = result.stdout.read()
        # 將標準輸出和標準錯誤發送給客戶端
        conn.send(stderr)
        conn.send(stdout)

conn.close()
tcpss.close()

socket TCP仿ssh遠程執行命令客戶端

#!/usr/bin/env python
# _*_ coding: utf-8 _*_

from socket import *

‘‘‘socket TCP仿ssh遠程執行命令客戶端‘‘‘

HOST = ‘localhost‘
PORT = 9527
BUFSIZ = 1024
ADDR = (HOST, PORT)

tcpsc = socket(AF_INET, SOCK_STREAM)
tcpsc.connect(ADDR)

while True:
    cmd = input(‘請輸入要對服務端操作的命令>>>‘).strip()
    if not cmd:break
    # 當客戶端輸入quit時退出循環
    if cmd == ‘quit‘:break

    tcpsc.send(cmd.encode(‘utf-8‘))
    result = tcpsc.recv(BUFSIZ)
    # 註意,服務器操作系統為windows,所以解碼的時候使用gbk
    print(result.decode(‘gbk‘), end=‘‘)

tcpsc.close()

正常命令輸出短消息時不黏包如下:
D:\PortableSoft\Python35\python.exe E:/Python/重要的代碼/socket-遠程執行命令/TCPscCmd.py
請輸入要對服務端操作的命令>>>dir
 驅動器 E 中的卷是 VM
 卷的序列號是 4AE6-716D

 E:\Python\重要的代碼\socket-遠程執行命令 的目錄

2018-05-10  17:24    <DIR>          .
2018-05-10  17:24    <DIR>          ..
2018-05-10  17:24               532 TCPscCmd.py
2018-05-10  17:24             1,375 TCPssCmd.py
               2 個文件          1,907 字節
               2 個目錄 26,977,878,016 可用字節

命令輸出長消息時(1024字節一次顯示不全)黏包如下:
D:\PortableSoft\Python35\python.exe E:/Python/重要的代碼/socket-遠程執行命令/TCPscCmd.py
請輸入要對服務端操作的命令>>>help
有關某個命令的詳細信息,請鍵入 HELP 命令名
ASSOC          顯示或修改文件擴展名關聯。
ATTRIB         顯示或更改文件屬性。
BREAK          設置或清除擴展式 CTRL+C 檢查。
BCDEDIT        設置啟動數據庫中的屬性以控制啟動加載。
CACLS          顯示或修改文件的訪問控制列表(ACL)。
CALL           從另一個批處理程序調用這一個。
CD             顯示當前目錄的名稱或將其更改。
CHCP           顯示或設置活動代碼頁數。
CHDIR          顯示當前目錄的名稱或將其更改。
CHKDSK         檢查磁盤並顯示狀態報告。
CHKNTFS        顯示或修改啟動時間磁盤檢查。
CLS            清除屏幕。
CMD            打開另一個 Windows 命令解釋程序窗口。
COLOR          設置默認控制臺前景和背景顏色。
COMP           比較兩個或兩套文件的內容。
COMPACT        顯示或更改 NTFS 分區上文件的壓縮。
CONVERT        將 FAT 卷轉換成 NTFS。您不能轉換
               當前驅動器。
COPY           將至少一個文件復制到另一個位置。
DATE           顯示或設置日期。
DEL            刪除至少一個文件。
DIR            顯示一個目錄中的文件和子目錄。
DISKCOMP      請輸入要對服務端操作的命令>>>

此時繼續輸入一條比較短消息的命令會怎樣,答案是之前未顯示全的消息會繼續發送過來接收:
DISKCOMP      請輸入要對服務端操作的命令>>>DATE
 比較兩個軟盤的內容。
DISKCOPY       將一個軟盤的內容復制到另一個軟盤。
DISKPART       顯示或配置磁盤分區屬性。
DOSKEY         編輯命令行、調用 Windows 命令並創建宏。
DRIVERQUERY    顯示當前設備驅動程序狀態和屬性。
ECHO           顯示消息,或將命令回顯打開或關上。
ENDLOCAL       結束批文件中環境更改的本地化。
ERASE          刪除一個或多個文件。
EXIT           退出 CMD.EXE 程序(命令解釋程序)。
FC             比較兩個文件或兩個文件集並顯示它們之間的不同。
FIND           在一個或多個文件中搜索一個文本字符串。
FINDSTR        在多個文件中搜索字符串。
FOR            為一套文件中的每個文件運行一個指定的命令。
FORMAT         格式化磁盤,以便跟 Windows 使用。
FSUTIL         顯示或配置文件系統的屬性。
FTYPE          顯示或修改用在文件擴展名關聯的文件類型。
GOTO           將 Windows 命令解釋程序指向批處理程序
               中某個帶標簽的行。
GPRESULT       顯示機器或用戶的組策略信息。
GRAFTABL       啟用 Windows 在圖形模式顯示擴展字符集。
HELP           提供 Windows 命令的幫助信息。
ICACLS         請輸入要對服務端操作的命令>>>

----------------------

黏包成因

tcp協議的拆包機制

當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。
MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。
大部分網絡設備的MTU都是1500。
如果本機的MTU比網關的MTU大,
大的數據包就會被拆開來傳送,這樣會產生很多數據包碎片,增加丟包率,降低網絡速度。

面向流的通信特點和Nagle算法

TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,
因此,發送端為了將多個發往接收端的包,更有效的發到對方,
使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。

面向流的通信是無消息保護邊界的。

對於空消息:
tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,
防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,
udp協議會幫你封裝上消息頭發送過去。

可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。

基於tcp協議特點的黏包現象成因

發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,
當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。

也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),
一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。

而UDP是面向消息的協議,每個UDP段都是一條消息,
應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。
怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,
需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,
TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩沖區。

基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,
在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束。

此外,發送方引起的粘包是由TCP協議本身造成的,
TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。
若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,
這樣接收方就收到了粘包數據。

UDP不會發生黏包

UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。
不會使用塊的合並優化算法, 由於UDP支持的是一對多的模式,
所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,
在每個UDP包中就有了消息頭(消息來源地址,端口等信息),
這樣,對於接收端來說,就容易進行區分處理了。

即面向消息的通信是有消息保護邊界的。

對於空消息:
tcp是基於數據流的,於是收發的消息不能為空,
這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,
而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,
udp協議會幫你封裝上消息頭發送過去。

不可靠不黏包的udp協議:

udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),
收完了x個字節的數據就算完成,若是y;x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠。

用UDP協議發送時,用sendto函數最大能發送數據的長度為:65535- IP頭(20) – UDP頭(8)=65507字節。
用sendto函數發送數據時,如果發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送)

用TCP協議發送時,由於TCP是數據流協議,因此不存在包大小的限制(暫不考慮緩沖區的大小),
這是指在用send函數時,數據長度參數不受限制。
而實際上,所指定的這段數據並不一定會一次性發送出去,如果這段數據比較長,會被分段發送,
如果比較短,可能會等待和下一次數據一起發送。

會發生黏包的兩種情況

情況一 發送方的緩存機制

發送端需要等緩沖區滿才發送出去,造成黏包
(發送數據時間間隔很短,數據了很小,會合到一起,產生黏包)。

情況二 接收方的緩存機制

接收方不及時接收緩沖區的包,造成多個包接收
(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生黏包)

總結

黏包現象只發生在tcp協議中:
1.從表面上看,黏包問題主要是因為發送方和接收方的緩存機制、tcp協議面向流通信的特點。
2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

黏包的解決方案

解決方案一

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,
所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,
然後接收端來一個死循環接收完所有數據。

技術分享圖片

存在的問題:
程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,
這種方式會放大網絡延遲帶來的性能損耗。

解決方案二(進階)

我們可以借助struct模塊,這個模塊可以把要發送的數據長度轉換成固定長度的字節。
這樣客戶端每次接收消息之前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,
那麽最終接受的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了。

struct模塊

該模塊可以把一個類型,如數字,轉成固定長度的bytes。

>>> struct.pack(‘i‘,1111111111111)

struct.error: ‘i‘ format requires -2147483648 <= number <= 2147483647 
#這個是範圍

技術分享圖片

使用struct解決黏包

借助struct模塊,我們知道長度數字可以被轉換成一個標準大小的4字節數字。
因此可以利用這個特點來預先發送數據長度。

發送時 先發送struct轉換好的數據長度4字節 再發送數據
接收時 先接受4個字節,使用struct轉換成數字來獲取要接收的數據長度 再按照長度接收數據

我們還可以把報頭做成字典,字典裏包含將要發送的真實數據的詳細信息,
然後json序列化,用struck將序列化後的數據長度打包成4個字節(4個足夠用了)

發送時 先發報頭長度 再編碼報頭內容然後發送 最後發真實內容。

接收時 先收報頭長度,用struct取出來 根據取出的長度收取報頭內容,然後解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,最後去取真實的數據內容。

例:傳輸大文件,解決黏包

#!/usr/bin/env python
# _*_ coding: utf-8 _*_
‘‘‘
socket struct 傳輸大文件 解決黏包 TCP服務端
‘‘‘
import os
import json
import socket
import struct

server = socket.socket()
server.bind((‘127.0.0.1‘, 9527))
server.listen()
conn, addr = server.accept()

# 要傳輸的文件路徑,文件名,文件大小
filepath = r‘E:\Python\file\三體.txt‘
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
dic = {‘filename‘: filename,
       ‘filesize‘: filesize}
# 字典json並轉碼bytes字節
str_dic = json.dumps(dic).encode(‘utf-8‘)
# 計算出json的字節長度並固定為struct模塊的四位模式
len_dic = struct.pack(‘i‘, len(str_dic))

conn.send(len_dic) # 發送json的長度
conn.send(str_dic) # 發送json

with open(filepath, ‘rb‘) as f:
    while filesize:
        content = f.read(4096)
        conn.send(content)
        filesize -= len(content)

conn.close()
server.close()

客戶端:
#!/usr/bin/env python
# _*_ coding: utf-8 _*_

‘‘‘
socket struct 傳輸大文件 解決黏包 TCP客戶端
‘‘‘

import json
import struct
import socket

client =socket.socket()
client.connect((‘127.0.0.1‘, 9527))

dic_len = client.recv(4)
dic_len = struct.unpack(‘i‘, dic_len)[0]
dic = client.recv(dic_len)
str_dic = dic.decode(‘utf-8‘)
dic = json.loads(str_dic)

with open(dic[‘filename‘], ‘wb‘) as f:
    while dic[‘filesize‘]:
        content = client.resv(4096)
        dic[‘filesize‘] -= len(content)
        f.write(content)

client.close()

socket的方法

服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來

客戶端套接字函數
s.connect() 主動初始化TCP服務器連接
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常

公共用途的套接字函數
s.recv() 接收TCP數據
s.send() 發送TCP數據
s.sendall() 發送TCP數據

s.recvfrom() 接收UDP數據
s.sendto() 發送UDP數據

s.getpeername() 連接到當前套接字的遠端的地址
s.getsockname() 當前套接字的地址
s.getsockopt() 返回指定套接字的參數
s.setsockopt() 設置指定套接字的參數
s.close() 關閉套接字

面向鎖的套接字方法
s.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操作的超時時間
s.gettimeout() 得到阻塞套接字操作的超時時間

面向文件的套接字的函數
s.fileno() 套接字的文件描述符
s.makefile() 創建一個與該套接字相關的文件

官方文檔對socket模塊下的socket.send()和socket.sendall()解釋如下:

socket.send(string[, flags])
Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data.

send()的返回值是發送的字節數量,這個數量值可能小於要發送的string的字節數,也就是說可能無法發送string中所有的數據。如果有錯誤則會拋出異常。

socket.sendall(string[, flags])
Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent.

嘗試發送string的所有數據,成功則返回None,失敗則拋出異常。

end
2018-5-10

參考:
http://www.cnblogs.com/Eva-J/
《python核心編程第四版》

鐵樂學Python_Day34_Socket模塊2和黏包現象