網絡通訊中TCP出現的黏包以及解決方法 socket 模擬黏包

粘包問題概述python

 

1.1  描述背景linux

採用TCP協議進行網絡數據傳送的軟件設計中,廣泛存在粘包問題。這主要是因爲現代操做系統的網絡傳輸機制所產生的。咱們知道,網絡通訊採用的套接字(socket)技術,其實現實際是由系統內核提供一片連續緩存(流緩衝)來實現應用層程序與網卡接口之間的中轉功能。多個數據包被連續存儲於連續的緩存中,在對數據包進行讀取時因爲沒法肯定發生方的發送邊界,而採用某一估測值大小來進行數據讀出,若雙方的size不一致時就會使數據包的邊界發生錯位,致使讀出錯誤的數據分包,進而曲解原始數據含義。程序員

 

1.2  粘包的概念算法

粘包問題的本質就是數據讀取邊界錯誤所致,經過下圖能夠形象地理解其現象。shell

如圖1所示,當前的socket緩存中已經有6個數據分組到達,其大小如圖中數字。而應用程序在對數據進行收取時(如圖2),採用了300字節的要求去讀取,則會誤將pkg1和pkg2一塊兒收走當作一個包來處理。而實際上,極可能pkg1是一個文本文件的內容,而pkg2則多是一個音頻內容,這風馬牛不相及的兩個數據包卻被揉進一個包進行處理,顯然有失穩當。嚴重時可能由於丟了pkg2而致使軟件陷入異常分支產生烏龍事件。編程

所以,粘包問題必須引發全部軟件設計者(項目經理)的高度重視!json

那麼,或許會有讀者發問,爲什麼不讓接收程序按照100字節來讀取呢?我想若是您瞭解一些TCP編程的話就不會有這樣的問題。網絡通訊程序中,數據包一般是不能肯定大小的,尤爲在軟件設計階段沒法真的作到肯定爲一個固定值。好比聊天軟件客戶端若採用TCP傳輸一個用戶名和密碼到服務端進行驗證登錄,我想這個數據包不過是幾十字節,至多幾百字節便可發送完畢,而有時候要傳輸一個很大的視頻文件,即便分包發送也應該一個包在幾千字節吧。(聽說,某國電信平臺的MW中見到過一次發送1.5萬字節的電話數據)這種狀況下,發送數據的分包大小沒法固定,接收端也就沒法固定。因此通常採用一個較爲合理的預估值進行輪詢接收。(網卡的MTU都是1500字節,所以這個預估值通常爲MTU的1~3倍)。windows

 

粘包現象

  
  說粘包以前,咱們先說兩個內容,1.緩衝區、2.windows下cmd窗口調用系統指令
  9.1 緩衝區(下面粘包現象的圖裏面還有關於緩衝區的解釋)
    

 

每一個 socket 被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。 write()/send() 並不當即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由TCP協議將數據從緩衝區發送到目標機器。一旦將數據寫入到緩衝區,函數就能夠成功返回,無論它們有沒有到達目標機器,也無論它們什麼時候被髮送到網絡,這些都是TCP協議負責的事情。 TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,屢次寫入的數據被一次性發送到網絡,這取決於當時的網絡狀況、當前線程是否空閒等諸多因素,不禁程序員控制。 read()/recv() 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取。 這些I/O緩衝區特性可整理以下: 1.I/O緩衝區在每一個TCP套接字中單獨存在; 2.I/O緩衝區在建立套接字時自動生成; 3.即便關閉套接字也會繼續傳送輸出緩衝區中遺留的數據; 4.關閉套接字將丟失輸入緩衝區中的數據。 輸入輸出緩衝區的默認大小通常都是 8K,能夠經過 getsockopt() 函數獲取: 1.unsigned optVal; 2.int optLen = sizeof(int); 3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen); 4.printf("Buffer length: %d\n", optVal); socket緩衝區解釋

9.2 windows下cmd窗口調用系統指令(linux下沒有寫出來,你們仿照windows的去摸索一下吧)緩存

    a.首先ctrl+r,彈出左下角的下圖,輸入cmd指令,肯定
      

    b.在打開的cmd窗口中輸入dir(dir:查看當前文件夾下的全部文件和文件夾),你會看到下面的輸出結果。服務器

      

      另外還有ipconfig(查看當前電腦的網絡信息),在windows沒有ls這個指令(ls在linux下是查看當前文件夾下全部文件和文件夾的指令,和windows下的dir是相似的),那麼沒有這個指令就會報下面這個錯誤

      

 

linux shell中一個運行多個命令,命令間用;隔開便可 windows的命令提示符中運行多條命令用的是:&&、||、& aa && bb 就是執行aa,成功後再執行bb aa || bb 先執行aa,若執行成功則再也不執行bb,若失敗則執行bb a & b 表示執行a再執行b,不管a是否成功 「執行成功」的意思是返回的errorlevel=0 windows下執行多條指令
windows下執行多條指令

 爲何要說這個系統指令呢,是但願藉助系統指令和指令輸出的結果來模擬一下粘包現象,那什麼是粘包呢?

9.3 粘包現象(兩種)

    先上圖:(本圖是我作出來爲了讓小白同窗有個大體的瞭解用的,其中不少地方更加的複雜,那就須要未來你們有多餘的精力的時候去作一些深刻的研究了,這裏我就不帶你們搞啦)

    

 

    關於MTU你們能夠看看這篇文章 https://yq.aliyun.com/articles/222535  還有百度百科 MTU百科

    MTU簡單解釋:

MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500個字節,也就是1500B。若是本機一次須要發送的數據比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度

 

    關於上圖中提到的Nagle算法等建議你們去看一看Nagle算法、延遲ACK、linux下的TCP_NODELAY和TCP_CORK,這些內容等大家把python學好之後再去研究吧,網絡的內容實在太多啦,也就是說你們須要努力的過程還很長,加油!

  

  超出緩衝區大小會報下面的錯誤,或者udp協議的時候,你的一個數據包的大小超過了你一次recv能接受的大小,也會報下面的錯誤,tcp不會,可是超出緩存區大小的時候,確定會報這個錯誤。

 
   

  9.4 模擬一個粘包現象

    在模擬粘包以前,咱們先學習一個模塊subprocess。
import subprocess cmd = input('請輸入指令>>>') res = subprocess.Popen( cmd, #字符串指令:'dir','ipconfig',等等
    shell=True,              #使用shell,就至關於使用cmd窗口
    stderr=subprocess.PIPE,  #標準錯誤輸出,凡是輸入錯誤指令,錯誤指令輸出的報錯信息就會被它拿到
    stdout=subprocess.PIPE,  #標準輸出,正確指令的輸出結果被它拿到
) print(res.stdout.read().decode('gbk')) print(res.stderr.read().decode('gbk')) subprocess的簡單使用
subprocess的簡單使用

注意:

        若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼

        且只能從管道里讀一次結果,PIPE稱爲管道。

 

     下面是subprocess和windows上cmd下的指令的對應示意圖:subprocess的stdout.read()和stderr.read(),拿到的結果是bytes類型,因此須要轉換爲字符串打印出來看。
    

    

    好,既然咱們會使用subprocess了,那麼咱們就經過它來模擬一個粘包,終於到模擬粘包現象了

    tcp粘包演示(一):

      先從上面粘包現象中的第一種開始:接收方沒有及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 
      server端代碼示例:
 
cket import *
import subprocess ip_port=('127.0.0.1',8080) BUFSIZE=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: conn,addr=tcp_socket_server.accept() print('客戶端>>>',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(cmd.decode('gbk'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() conn.send(stderr) conn.send(stdout) tcp_server.py

client端代碼示例:

import socket ip_port = ('127.0.0.1',8080) size = 1024 tcp_sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = tcp_sk.connect(ip_port) while True: msg=input('>>: ').strip() if len(msg) == 0:continue
    if msg == 'quit':break tcp_sk.send(msg.encode('utf-8')) act_res=tcp_sk.recv(size) print('接收的返回結果長度爲>',len(act_res)) print('std>>>',act_res.decode('gbk')) #windows返回的內容須要用gbk來解碼,由於windows系統的默認編碼爲gbk
 tcp_client.py

 

 

tcp粘包演示(二):發送數據時間間隔很短,數據也很小,會合到一塊兒,產生粘包

      server端代碼示例:(若是兩次發送有必定的時間間隔,那麼就不會出現這種粘包狀況,試着在兩次發送的中間加一個time.sleep(1))

from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() tcp_server.py

client端代碼示例

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # res=s.connect_ex(ip_port)
res=s.connect(ip_port) s.send('hi'.encode('utf-8')) s.send('meinv'.encode('utf-8')) tcp_server.py

 

示例二的結果:所有被第一個recv接收了

    

 

    udp粘包演示:注意:udp是面向包的,因此udp是不存在粘包的
      server端代碼示例:
import socket from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF sk = socket.socket(type=socket.SOCK_DGRAM) # sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090)) msg,addr = sk.recvfrom(1024) while True: cmd = input('>>>>') if cmd == 'q': break sk.sendto(cmd.encode('utf-8'),addr) msg,addr = sk.recvfrom(1032) # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg)) print(msg.decode('utf-8')) sk.close() udp_server.py

 client端代碼示例:

import socket from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF sk = socket.socket(type=socket.SOCK_DGRAM) # sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090)) msg,addr = sk.recvfrom(1024) while True: cmd = input('>>>>') if cmd == 'q': break sk.sendto(cmd.encode('utf-8'),addr) msg,addr = sk.recvfrom(1024) # msg,addr = sk.recvfrom(1218)
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg)) print(msg.decode('utf-8')) sk.close() udp_server.py

在udp的代碼中,咱們在server端接收返回消息的時候,咱們設置的recvfrom(1024),那麼當我輸入的執行指令爲‘dir’的時候,dir在我當前文件夾下輸出的內容大於1024,而後就報錯了,報的錯誤也是下面這個:

  

    解釋緣由:是由於udp是面向報文的,意思就是每一個消息是一個包,你接收端設置接收大小的時候,必需要比你發的這個包要大,否則一次接收不了就會報這個錯誤,而tcp不會報錯,這也是爲何ucp會丟包的緣由之一,這個和咱們上面緩衝區那個錯誤的報錯緣由是不同的。  

 
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。 例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束 所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。 此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。 1.TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。 2.UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。 3.tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略 udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠 tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。 解釋緣由
解釋緣由

 

補充兩個問題:

補充問題一:爲什麼tcp是可靠傳輸,udp是不可靠傳輸 tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的。 而udp發送數據,對端是不會返回確認信息的,所以不可靠 補充問題二:send(字節流)和sendall send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失,通常的小數據就用send,由於小數據也用sendall的話有些影響代碼性能,簡單來說就是還多while循環這個代碼呢。    用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送) 用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比較短,可能會等待和下一次數據一塊兒發送。

 

粘包的緣由:主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的

 

 

 粘包的解決方案

  解決方案(一):
     問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端發一個確認消息給發送端,而後發送端再發送過來後面的真實內容,接收端再來一個死循環接收完全部數據。
     

    看代碼示例:

      server端代碼 
import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客戶端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True,\ stdin=subprocess.PIPE,\ stderr=subprocess.PIPE,\ stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close() tcp_server.py

    client端代碼示例

import socket,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue
    if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b''
    while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8')) tcp_server.py

解決方案(二):

    經過struck模塊將須要發送的內容的長度進行打包,打包成一個4字節長度的數據發送到對端,對端只要取出前4個字節,而後對這四個字節的數據進行解包,拿到你要發送的內容的長度,而後經過這個長度來繼續接收咱們實際要發送的內容。不是很好理解是吧?哈哈,不要緊,看下面的解釋~~
       爲何要說一下這個模塊呢,由於解決方案(一)裏面你發現,我每次要先發送一個個人內容的長度,須要接收端接收,並切須要接收端返回一個確認消息,我發送端才能發後面真實的內容,這樣是爲了保證數據可靠性,也就是接收雙方能順利溝通,可是多了一次發送接收的過程,爲了減小這個過程,咱們就要使struck來發送你須要發送的數據的長度,來解決上面咱們所說的經過發送內容長度來解決粘包的問題
    關於struck的介紹:
      瞭解c語言的人,必定會知道struct結構體在c語言中的做用,不瞭解C語言的同窗也不要緊,不影響,其實它就是定義了一種結構,裏面包含不一樣類型的數據(int,char,bool等等),方便對某一結構對象進行處理。而在網絡通訊當中,大多傳遞的數據是以二進制流(binary data)存在的。當傳遞字符串時,沒必要擔憂太多的問題,而當傳遞諸如int、char之類的基本數據的時候,就須要有一種機制將某些特定的結構體類型打包成二進制流的字符串而後再網絡傳輸,而接收端也應該能夠經過某種機制進行解包還原出原始的結構體數據。python中的struct模塊就提供了這樣的機制,該模塊的主要做用就是對python基本類型值與用python字符串格式表示的C struct類型間的轉化(This module performs conversions between Python values and C structs represented as Python strings.)。
    
    struck模塊的使用:struct模塊中最重要的兩個函數是pack()打包, unpack()解包。
 
    

    pack():#我在這裏只介紹一下'i'這個int類型,上面的圖中列舉除了能夠打包的全部的數據類型,而且struck除了pack和uppack兩個方法以外還有好多別的方法和用法,你們之後找時間能夠去研究一下,這裏我就不作介紹啦,網上的教程不少~~

import struct a=12
# 將a變爲二進制
bytes=struct.pack('i',a) ------------------------------------------------------------------------------- struct.pack('i',1111111111111) 若是int類型數據太大會報錯struck.error struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍

 

pack方法圖解:

      

 

    unpack():

# 注意,unpack返回的是tuple !!
 a,=struct.unpack('i',bytes) #將bytes類型的數據解包後,拿到int類型數據

 

好,到這裏咱們將struck這個模塊將int類型的數據打包成四個字節的方法了,那麼咱們就來使用它解決粘包吧。

  先看一段僞代碼示例:

import json,struct #假設經過客戶端上傳1T:1073741824000的文件a.txt

#爲避免粘包,必須自定製報頭
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值

#爲了該報頭能傳送,須要序列化而且轉爲bytes,由於bytes只能將字符串類型的數據轉換爲bytes類型的,全部須要先序列化一下這個字典,字典不能直接轉化爲bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化並轉成bytes,用於傳輸

#爲了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節裏只包含了一個數字,該數字是報頭的長度

#客戶端開始發送
conn.send(head_len_bytes) #先發報頭的長度,4個bytes
conn.send(head_bytes) #再發報頭的字節格式
conn.sendall(文件內容) #而後發真實內容的字節格式

#服務端開始接收
head_len_bytes=s.recv(4) #先收報頭4個bytes,獲得報頭長度的字節格式
x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度
 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
header=json.loads(json.dumps(header)) #提取報頭

#最後根據報頭的內容提取真實的數據,好比
real_data_len=s.recv(header['file_size']) s.recv(real_data_len) 僞代碼(含解釋)

下面看正式的代碼:

  server端代碼示例:報頭:就是消息的頭部信息,咱們要發送的真實內容爲報頭後面的內容。

import socket,struct,json 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 True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break
        print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() if err: back_msg=err else: back_msg=res.stdout.read() conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度
        conn.sendall(back_msg) #在發真實的內容
        #其實就是連續的將長度和內容一塊兒發出去,那麼整個內容的前4個字節就是咱們打包的後面內容的長度,對吧
 conn.close( tcp_server.py(自定製報頭)

  client端代碼示例:

import socket,time,struct s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue
    if msg == 'quit':break s.send(msg.encode('utf-8'))  #發送給一個指令
    l=s.recv(4)     #先接收4個字節的數據,由於咱們將要發送過來的內容打包成了4個字節,因此先取出4個字節
    x=struct.unpack('i',l)[0]  #解包,是一個元祖,第一個元素就是咱們的內容的長度
    print(type(x),x) # print(struct.unpack('I',l))
    r_s=0 data=b''
    while r_s < x:    #根據內容的長度來繼續接收4個字節後面的內容。
        r_d=s.recv(1024) data+=r_d r_s+=len(r_d) # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默認gbk編碼
 tcp_client.py(自定製報頭)

 

 

複雜一些的代碼示例

  server端:

import socket,struct,json 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 True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break
        print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers) head_json_bytes=bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) #先發報頭的長度
        conn.send(head_json_bytes) #再發報頭
        conn.sendall(back_msg) #在發真實的內容
 conn.close() tcp_server.py

  client端:

from socket import *
import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head=client.recv(4) head_json_len=struct.unpack('i',head)[0] head_json=json.loads(client.recv(head_json_len).decode('utf-8')) data_len=head_json['data_size'] recv_size=0 recv_data=b''
    while recv_size < data_len: recv_data+=client.recv(1024) recv_size+=len(recv_data) #print(recv_data.decode('utf-8'))
    print(recv_data.decode('gbk')) #windows默認gbk編碼
 tcp_client.py

 

其實上面複雜的代碼作了個什麼事情呢,就是自定製了報頭:

  

 你爲啥屢次send啊,其實屢次send和將數據拼接起來send一次是同樣的,由於咱們約定好了,你接收的時候先接收4個字節,而後再接收後面的內容。

整個流程的大體解釋:
咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的描述信息(大小啊之類的),而後json序列化,而後用struck將序列化後的數據長度打包成4個字節。
咱們在網絡上傳輸的全部數據 都叫作數據包,數據包裏的全部數據都叫作報文,報文裏面不止有你的數據,還有ip地址、mac地址、端口號等等,其實全部的報文都有報頭,這個報頭是協議規定的,看一下

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

接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,而後解碼,反序列化
從反序列化的結果中取出待取數據的描述信息,而後去取真實的數據內容

 

FTP上傳下載文件的代碼(簡易版)

import socket import struct import json sk = socket.socket() # buffer = 4096 # 當雙方的這個接收發送的大小比較大的時候,就像這個4096,就會丟數據,這個等我查一下再告訴你們,改小了就ok的,在linux上也是ok的。
buffer = 1024 #每次接收數據的大小
sk.bind(('127.0.0.1',8090)) sk.listen() conn,addr = sk.accept() #接收
head_len = conn.recv(4) head_len = struct.unpack('i',head_len)[0] #解包
json_head = conn.recv(head_len).decode('utf-8') #反序列化
head = json.loads(json_head) filesize = head['filesize'] with open(head['filename'],'wb') as f: while filesize: if filesize >= buffer: #>=是由於若是恰好等於的狀況出現也是能夠的。
            content = conn.recv(buffer) f.write(content) filesize -= buffer else: content = conn.recv(buffer) f.write(content) break conn.close() sk.close() tcp_server.py

 

import os import json import socket import struct sk = socket.socket() sk.connect(('127.0.0.1',8090)) buffer = 1024 #讀取文件的時候,每次讀取的大小
head = { 'filepath':r'D:\打包程序', #須要下載的文件路徑,也就是文件所在的文件夾
            'filename':'xxx.mp4',  #改爲上面filepath下的一個文件
            'filesize':None, } file_path = os.path.join(head['filepath'],head['filename']) filesize = os.path.getsize(file_path) head['filesize'] = filesize # json_head = json.dumps(head,ensure_ascii=False) #字典轉換成字符串
json_head = json.dumps(head)  #字典轉換成字符串
bytes_head = json_head.encode('utf-8') #字符串轉換成bytes類型
print(json_head) print(bytes_head) #計算head的長度,由於接收端先接收咱們本身定製的報頭,對吧
head_len = len(bytes_head) #報頭長度
pack_len = struct.pack('i',head_len) print(head_len) print(pack_len) sk.send(pack_len) #先發送報頭長度
sk.send(bytes_head) #再發送bytes類型的報頭

#即使是視頻文件,也是能夠按行來讀取的,也能夠readline,也能夠for循環,可是讀取出來的數據大小就不固定了,影響效率,有可能讀的比較小,也可能很大,像視頻文件通常都是一行的二進制字節流。 #全部咱們能夠用read,設定一個一次讀取內容的大小,一邊讀一邊發,一邊收一邊寫
with open(file_path,'rb') as f: while filesize: if filesize >= buffer: #>=是由於若是恰好等於的狀況出現也是能夠的。
            content = f.read(buffer) #每次讀取出來的內容
 sk.send(content) filesize -= buffer #每次減去讀取的大小
        else: #那麼說明剩餘的不夠一次讀取的大小了,那麼只要把剩下的讀取出來發送過去就好了
            content = f.read(filesize) sk.send(content) break sk.close() tcp_client.py

 

 

FTP上傳下載文件的代碼(升級版)(注:我們學完網絡編程就留FTP做業,這個代碼能夠參考,當你用函數的方式寫完以後,再用面向對象進行改版卻沒有思路的時候再來看,別騙本身昂~~)

 

import socket import struct import json import subprocess import os class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload'

    def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise

    def server_bind(self): """Called by constructor to bind the socket. """
        if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """
        return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
                    cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break

    def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize)) tcpserver1=MYTCPServer(('127.0.0.1',8080)) tcpserver1.run() server.py
server.py

 

import socket import struct import json import os class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5

    def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise

    def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return
        else: filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful') client=MYTCPClient(('127.0.0.1',8080)) client.run() client.py
client.py

 

相關文章
相關標籤/搜索