Python-網絡編程(二)

一.網絡通信原理python

  1.互聯網的本質就是一系列的網絡協議linux

  咱們是在瀏覽器上輸入了一個網址,可是咱們都知道,互聯網鏈接的電腦互相通訊的是電信號,咱們的電腦是怎麼將咱們輸入的網址變成了電信號而後發送出去了呢,而且咱們發送出去的消息是否是應該讓對方的服務器可以知道,咱們是在請求它的網站呢,也就是說京東是否是應該知道我發送的消息是什麼意思呢。是否是發送的消息應該有一些固定的格式呢?讓全部電腦都能識別的消息格式,他就像英語成爲世界上全部人通訊的統一標準同樣,若是把計算機當作分佈於世界各地的人,那麼鏈接兩臺計算機之間的internet實際上就是一系列統一的標準,這些標準稱之爲互聯網協議,互聯網的本質就是一系列的協議,總稱爲‘互聯網協議’(Internet Protocol Suite)。程序員

  互聯網協議的功能:定義計算機如何接入internet,以及接入internet的計算機通訊的標準。算法

  網絡通訊的流程昨天已經說過了,出門左轉就能看到,這裏就再也不說了.shell

  2.osi七層協議json

  互聯網協議按照功能不一樣分爲osi七層或tcp/ip五層或tcp/ip四層windows

  咱們如今只須要了解五層的協議就行了,ok嗎?咱們寫的程序屬於哪一層呢,屬於應用層。設計模式

  每層運行常見物理設備瀏覽器

 

  3.tcp/ip五層模型講解緩存

   咱們將應用層,表示層,會話層並做應用層,從tcp/ip五層協議的角度來闡述每層的由來與功能,搞清楚了每層的主要協議

    就理解了整個互聯網通訊的原理。

    首先,用戶感知到的只是最上面一層應用層,自上而下每層都依賴於下一層,因此咱們從最下一層開始切入,比較好理解

    每層都運行特定的協議,越往上越靠近用戶,越往下越靠近硬件

     3.1 物理層

 

      物理層功能:主要是基於電器特性發送高低電壓(電信號),高電壓對應數字1,低電壓對應數字0

    3.2 數據鏈路層

      數據鏈路層的功能:定義了電信號的分組方式

      以太網協議:

      早期的時候各個公司都有本身的分組方式,後來造成了統一的標準,即以太網協議ethernet

      ethernet規定

    一組電信號構成一個數據包,叫作‘幀’

    每一數據幀分紅:報頭head和數據data兩部分

       mac地址:

        mac地址:每塊網卡出廠時都被燒製上一個世界惟一的mac地址,長度爲48位2進制,一般由12位16進制數表示(前六位是廠商編號,後六位是流水線號)

     3.3 網絡層

      IP協議:

      規定網絡地址的協議叫ip協議,它定義的地址稱之爲ip地址,普遍採用的v4版本即ipv4,它規定網絡地址由32位2進製表示

      範圍0.0.0.0-255.255.255.255 (4個點分十進制,也就是4個8位二進制數)

      一個ip地址一般寫成四段十進制數,例:172.16.10.1

      ipv6,經過上面能夠看出,ip緊缺,因此爲了知足更多ip須要,出現了ipv6協議:6個冒號分割的16進制數表示,這個應該是未來的趨勢,可是ipv4仍是用的最多的,由於咱們通常一個公司就一個對外的IP地址,咱們全部的機器上網都走這一個IP出口。

      ip數據包

      ip數據包也分爲head和data部分,無須爲ip包定義單獨的欄位,直接放入以太網包的data部分

      head:長度爲20到60字節

      data:最長爲65,515字節。

      而以太網數據包的」數據」部分,最長只有1500字節。所以,若是IP數據包超過了1500字節,它就須要分割成幾個以太網數據包,分開發送了。

    3.4 傳輸層

      tcp協議:(TCP把鏈接做爲最基本的對象,每一條TCP鏈接都有兩個端點,這種端點咱們叫做套接字(socket),它的定義爲端口號拼接到IP地址即構成了套接字,例如,若IP地址爲192.3.4.16 而端口號爲80,那麼獲得的套接字爲192.3.4.16:80。)

      當應用程序但願經過 TCP 與另外一個應用程序通訊時,它會發送一個通訊請求。這個請求必須被送到一個確切的地址。在雙方「握手」以後,TCP 將在兩個應用程序之間創建一個全雙工 (full-duplex,雙方均可以收發消息) 的通訊。

      這個全雙工的通訊將佔用兩個計算機之間的通訊線路,直到它被一方或雙方關閉爲止。

      它是可靠傳輸,TCP數據包沒有長度限制,理論上能夠無限長,可是爲了保證網絡的效率,一般TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包沒必要再分割。

      udp協議:不可靠傳輸,」報頭」部分一共只有8個字節,總長度不超過65,535字節,正好放進一個IP數據包。

      tcp三次握手和四次揮手

      咱們知道網絡層,能夠實現兩個主機之間的通訊。可是這並不具體,由於,真正進行通訊的實體是在主機中的進程,是一個主機中的一個進程與另一個主機中的一個進程在交換數據。IP協議雖然能把數據報文送到目的主機,可是並無交付給主機的具體應用進程。而端到端的通訊才應該是應用進程之間的通訊。

      UDP,在傳送數據前不須要先創建鏈接,遠地的主機在收到UDP報文後也不須要給出任何確認。雖然UDP不提供可靠交付,可是正是由於這樣,省去和不少的開銷,使得它的速度比較快,好比一些對實時性要求較高的服務,就經常使用的是UDP。對應的應用層的協議主要有 DNS,TFTP,DHCP,SNMP,NFS 等。

      TCP,提供面向鏈接的服務,在傳送數據以前必須先創建鏈接,數據傳送完成後要釋放鏈接。所以TCP是一種可靠的的運輸服務,可是正由於這樣,不可避免的增長了許多的開銷,好比確認,流量控制等。對應的應用層的協議主要有 SMTP,TELNET,HTTP,FTP 等。

       三次握手:

      1.TCP服務器進程先建立傳輸控制塊TCB,時刻準備接受客戶進程的鏈接請求,此時服務器就進入了 LISTEN(監聽)狀態;

      2.TCP客戶進程也是先建立傳輸控制塊TCB,而後向服務器發出鏈接請求報文,這是報文首部中的同部位SYN=1,同時選擇一個初始序列號 seq=x ,此時,TCP客戶端進程進入了 SYN-SENT(同步已發送狀態)狀態。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶數據,但須要消耗掉一個序號。

      3.TCP服務器收到請求報文後,若是贊成鏈接,則發出確認報文。確認報文中應該 ACK=1,SYN=1,確認號是ack=x+1,同時也要爲本身初始化一個序列號 seq=y,此時,TCP服務器進程進入了SYN-RCVD(同步收到)狀態。這個報文也不能攜帶數據,可是一樣要消耗一個序號。

      4.TCP客戶進程收到確認後,還要向服務器給出確認。確認報文的ACK=1,ack=y+1,本身的序列號seq=x+1,此時,TCP鏈接創建,客戶端進入ESTABLISHED(已創建鏈接)狀態。TCP規定,ACK報文段能夠攜帶數據,可是若是不攜帶數據則不消耗序號。

      5.當服務器收到客戶端的確認後也進入ESTABLISHED狀態,此後雙方就能夠開始通訊了。 

      四次揮手:

      數據傳輸完畢後,雙方均可釋放鏈接。最開始的時候,客戶端和服務器都是處於ESTABLISHED狀態,而後客戶端主動關閉,服務器被動關閉。服務端也能夠主動關閉,一個流程。

      1.客戶端進程發出鏈接釋放報文,而且中止發送數據。釋放數據報文首部,FIN=1,其序列號爲seq=u(等於前面已經傳送過來的數據的最後一個字節的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即便不攜帶數據,也要消耗一個序號。

      2.服務器收到鏈接釋放報文,發出確認報文,ACK=1,ack=u+1,而且帶上本身的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,可是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。

      3.客戶端收到服務器的確認請求後,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送鏈接釋放報文(在這以前還須要接受服務器發送的最後的數據)。

      4.服務器將最後的數據發送完畢後,就向客戶端發送鏈接釋放報文,FIN=1,ack=u+1,因爲在半關閉狀態,服務器極可能又發送了一些數據,假定此時的序列號爲seq=w,此時,服務器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。

      5.客戶端收到服務器的鏈接釋放報文後,必須發出確認,ACK=1,ack=w+1,而本身的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP鏈接尚未釋放,必須通過2∗MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。

      6.服務器只要收到了客戶端發出的確認,當即進入CLOSED狀態。一樣,撤銷TCB後,就結束了此次的TCP鏈接。能夠看到,服務器結束TCP鏈接的時間要比客戶端早一些。

 

    3.5 應用層

      應用層由來:用戶使用的都是應用程序,均工做於應用層,互聯網是開發的,你們均可以開發本身的應用程序,數據多種多樣,必須規定好數據的組織形式 

      應用層功能:規定應用程序的數據格式。

  五層通訊流程:

 

二. socket

  結合上圖來看,socket在哪一層呢,咱們繼續看下圖

  socket在內的五層通信流程:

  Socket又稱爲套接字,它是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。當咱們使用不一樣的協議進行通訊時就得使用不一樣的接口,還得處理不一樣協議的各類細節,這就增長了開發的難度,軟件也不易於擴展(就像咱們開發一套公司管理系統同樣,報帳、會議預約、請假等功能不須要單獨寫系統,而是一個系統上多個功能接口,不須要知道每一個功能如何去實現的)。因而UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通訊細節,使得程序員無需關注協議自己,直接使用socket提供的接口來進行互聯的不一樣主機間的進程的通訊。這就比如操做系統給咱們提供了使用底層硬件功能的系統調用,經過系統調用咱們能夠方便的使用磁盤(文件操做),使用內存,而無需本身去進行磁盤讀寫,內存管理。socket其實也是同樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就能夠統1、方便的使用tcp/ip協議的功能了。

  其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。

 三.套接字socket的發展史及分類

   基於文件類型的套接字家族

  套接字家族的名字:AF_UNIX

  基於網絡類型的套接字家族

  套接字家族的名字:AF_INET

 四.基於TCP和UDP兩個協議下socket的通信流程

   1.TCP和UDP對比

    TCP(Transmission Control Protocol)可靠的、面向鏈接的協議(eg:打電話)、傳輸效率低全雙工通訊(發送緩存&接收緩存)、面向字節流。使用TCP的應用:Web瀏覽器;文件傳輸程序。

    UDP(User Datagram Protocol)不可靠的、無鏈接的服務,傳輸效率高(發送前時延小),一對1、一對多、多對1、多對多、面向報文(數據包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP)。

    直接看圖對比其中差別

    繼續往下看

    TCP和UDP下socket差別對比圖:

   2.TCP協議下的socket

    基於TCP的socket通信流程圖片:

    雖然上圖將通信流程中的大體描述了一下socket各個方法的做用,可是仍是要總結一下通信流程(下面一段內容)

    先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束

    上代碼感覺一下,須要建立兩個文件,文件名稱隨便起,爲了方便看,個人兩個文件名稱爲tcp_server.py(服務端)和tcp_client.py(客戶端),將下面的server端的代碼拷貝到tcp_server.py文件中,將下面client端的代碼拷貝到tcp_client.py的文件中,而後先運行tcp_server.py文件中的代碼,再運行tcp_client.py文件中的代碼,而後在pycharm下面的輸出窗口看一下效果。

    server端代碼示例(若是比喻成打電話)

1
2
3
4
5
6
7
8
9
10
import  socket
sk  =  socket.socket()
sk.bind(( '127.0.0.1' , 8898 ))   #把地址綁定到套接字
sk.listen()           #監聽連接
conn,addr  =  sk.accept()  #接受客戶端連接
ret  =  conn.recv( 1024 )   #接收客戶端信息
print (ret)        #打印客戶端信息
conn.send(b 'hi' )         #向客戶端發送信息
conn.close()        #關閉客戶端套接字
sk.close()         #關閉服務器套接字(可選)

    client端代碼示例

1
2
3
4
5
6
7
import  socket
sk  =  socket.socket()            # 建立客戶套接字
sk.connect(( '127.0.0.1' , 8898 ))     # 嘗試鏈接服務器
sk.send(b 'hello!' )
ret  =  sk.recv( 1024 )          # 對話(發送/接收)
print (ret)
sk.close()             # 關閉客戶套接字

    socket綁定IP和端口時可能出現下面的問題:

     解決辦法:

1
2
3
4
5
6
7
8
9
10
11
12
13
#加入一條socket配置,重用ip和端口
import  socket
from  socket  import  SOL_SOCKET,SO_REUSEADDR
sk  =  socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR, 1 #在bind前加,容許地址重用
sk.bind(( '127.0.0.1' , 8898 ))   #把地址綁定到套接字
sk.listen()           #監聽連接
conn,addr  =  sk.accept()  #接受客戶端連接
ret  =  conn.recv( 1024 )    #接收客戶端信息
print (ret)               #打印客戶端信息
conn.send(b 'hi' )         #向客戶端發送信息
conn.close()        #關閉客戶端套接字
sk.close()         #關閉服務器套接字(可選)

    可是若是你加上了上面的代碼以後仍是出現這個問題:OSError: [WinError 10013] 以一種訪問權限不容許的方式作了一個訪問套接字的嘗試。那麼只能換端口了,由於你的電腦不支持端口重用。

    記住一點,用socket進行通訊,必須是一收一發對應好。

  提一下:網絡相關或者須要和電腦上其餘程序通訊的程序才須要開一個端口。

  

  在看UDP協議下的socket以前,咱們還須要加一些內容來說:看代碼

    server端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     import  socket
from  socket  import  SOL_SOCKET,SO_REUSEADDR
sk  =  socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk.bind(( '127.0.0.1' , 8090 ))
sk.listen()
conn,addr  =  sk.accept()   #在這阻塞,等待客戶端過來鏈接
while  True :
     ret  =  conn.recv( 1024 )   #接收消息  在這仍是要阻塞,等待收消息
     ret  =  ret.decode( 'utf-8' )   #字節類型轉換爲字符串中文
     print (ret)
     if  ret  = =  'bye' :         #若是接到的消息爲bye,退出
         break
     msg  =  input ( '服務端>>' )   #服務端發消息
     conn.send(msg.encode( 'utf-8' ))
     if  msg  = =  'bye' :
         break
 
conn.close()
sk.close()

    client端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     import  socket
sk  =  socket.socket()
sk.connect(( '127.0.0.1' , 8090 ))  #鏈接服務端
 
while  True :
     msg  =  input ( '客戶端>>>' )   #input阻塞,等待輸入內容
     sk.send(msg.encode( 'utf-8' ))
     if  msg  = =  'bye' :
         break
     ret  =  sk.recv( 1024 )
     ret  =  ret.decode( 'utf-8' )
     print (ret)
     if  ret  = =  'bye' :
         break
sk.close()

  你會發現,第一個鏈接的客戶端能夠和服務端收發消息,可是第二個鏈接的客戶端發消息服務端是收不到的

    緣由解釋:
      tcp屬於長鏈接,長鏈接就是一直佔用着這個連接,這個鏈接的端口被佔用了,第二個客戶端過來鏈接的時候,他是能夠鏈接的,可是處於一個佔線的狀態,就只能等着去跟服務端創建鏈接,除非一個客戶端斷開了(優雅的斷開能夠,若是是強制斷開就會報錯,由於服務端的程序還在第一個循環裏面),而後就能夠進行和服務端的通訊了。什麼是優雅的斷開呢?看代碼。
    server端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
     import  socket
from  socket  import  SOL_SOCKET,SO_REUSEADDR
sk  =  socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #容許地址重用,這個東西都說能解決問題,我很是不建議你們這麼作,容易出問題
sk.bind(( '127.0.0.1' , 8090 ))
sk.listen()
# 第二步演示,再加一層while循環
while  True :     #下面的代碼所有縮進進去,也就是循環創建鏈接,可是無論怎麼聊,只能和一個聊,也就是另一個優雅的斷了以後才能和另一個聊
                 #它不能同時和好多人聊,仍是長鏈接的緣由,一直佔用着這個端口的鏈接,udp是能夠的,而後咱們學習udp
     conn,addr  =  sk.accept()   #在這阻塞,等待客戶端過來鏈接
     while  True :
         ret  =  conn.recv( 1024 )   #接收消息  在這仍是要阻塞,等待收消息
         ret  =  ret.decode( 'utf-8' )   #字節類型轉換爲字符串中文
         print (ret)
         if  ret  = =  'bye' :         #若是接到的消息爲bye,退出
             break
         msg  =  input ( '服務端>>' )   #服務端發消息
         conn.send(msg.encode( 'utf-8' ))
         if  msg  = =  'bye' :
             break
     conn.close()

    client端代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     import  socket
sk  =  socket.socket()
sk.connect(( '127.0.0.1' , 8090 ))  #鏈接服務端
 
while  True :
     msg  =  input ( '客戶端>>>' )   #input阻塞,等待輸入內容
     sk.send(msg.encode( 'utf-8' ))
     if  msg  = =  'bye' :
         break
     ret  =  sk.recv( 1024 )
     ret  =  ret.decode( 'utf-8' )
     print (ret)
     if  ret  = =  'bye' :
         break
# sk.close()

  強制斷開鏈接以後的報錯信息:

    

   3.UDP協議下的socket

    老樣子!先上圖!

    基於UDP的socket通信流程:

     總結一下UDP下的socket通信流程

      先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),recvform接收消息,這個消息有兩項,消息內容和對方客戶端的地址,而後回覆消息時也要帶着你收到的這個客戶端的地址,發送回去,最後關閉鏈接,一次交互結束

      上代碼感覺一下,須要建立兩個文件,文件名稱隨便起,爲了方便看,個人兩個文件名稱爲udp_server.py(服務端)和udp_client.py(客戶端),將下面的server端的代碼拷貝到udp_server.py文件中,將下面cliet端的代碼拷貝到udp_client.py的文件中,而後先運行udp_server.py文件中的代碼,再運行udp_client.py文件中的代碼,而後在pycharm下面的輸出窗口看一下效果。

      server端代碼示例

1
2
3
4
5
6
7
import  socket
udp_sk  =  socket.socket( type = socket.SOCK_DGRAM)    #建立一個服務器的套接字
udp_sk.bind(( '127.0.0.1' , 9000 ))         #綁定服務器套接字
msg,addr  =  udp_sk.recvfrom( 1024 )
print (msg)
udp_sk.sendto(b 'hi' ,addr)                  # 對話(接收與發送)
udp_sk.close()                          # 關閉服務器套接字

      client端代碼示例

1
2
3
4
5
6
import  socket
ip_port = ( '127.0.0.1' , 9000 )
udp_sk = socket.socket( type = socket.SOCK_DGRAM)
udp_sk.sendto(b 'hello' ,ip_port)
back_msg,addr = udp_sk.recvfrom( 1024 )
print (back_msg.decode( 'utf-8' ),addr)

 五.粘包現象

  說粘包以前,咱們先說兩個內容,1.緩衝區、2.windows下cmd窗口調用系統指令

   5.1  緩衝區(下面粘包現象的圖裏面還有關於緩衝區的解釋)
    

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

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

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

      

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

      

   5.3 粘包現象(兩種)

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

    

     MTU簡單解釋:

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

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

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

   

  5.4 模擬一個粘包現象

    在模擬粘包以前,咱們先學習一個模塊subprocess。
1
2
3
4
5
6
7
8
9
10
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' ))

      注意:

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

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

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

    

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

    tcp粘包演示(一):

      先從上面粘包現象中的第一種開始: 接收方沒有及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 
      server端代碼示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)

      client端代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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粘包演示(二):發送數據時間間隔很短,數據也很小,會合到一塊兒,產生粘包

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

      client端代碼示例:

1
2
3
4
5
6
7
8
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' ))

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

    

 

     udp粘包演示:注意:udp是面向包的,因此udp是不存在粘包的
      server端代碼示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

       client端代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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端接收返回消息的時候,咱們設置的recvfrom(1024),那麼當我輸入的執行指令爲‘dir’的時候,dir在我當前文件夾下輸出的內容大於1024,而後就報錯了,報的錯誤也是下面這個:

  

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

  補充兩個問題:

1
2
3
4
5
6
7
8
9
10
11
12
補充問題一:爲什麼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端代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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()

      client端代碼示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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' ))

  解決方案(二):

    經過struck模塊將須要發送的內容的長度進行打包,打包成一個4字節長度的數據發送到對端,對端只要取出前4個字節,而後對這四個字節的數據進行解包,拿到你要發送的內容的長度,而後經過這個長度來繼續接收咱們實際要發送的內容。不是很好理解是吧?哈哈,不要緊,看下面的解釋~~
       爲何要說一下這個模塊呢,由於解決方案(一)裏面你發現,我每次要先發送一個個人內容的長度,須要接收端接收,並切須要接收端返回一個確認消息,我發送端才能發後面真實的內容,這樣是爲了保證數據可靠性,也就是接收雙方能順利溝通,可是多了一次發送接收的過程,爲了減小這個過程,咱們就要使struck來發送你須要發送的數據的長度,來解決上面咱們所說的經過發送內容長度來 解決粘包的問題

    struck模塊的使用:struct模塊中最重要的兩個函數是pack()打包, unpack()解包。

    pack():#我在這裏只介紹一下'i'這個int類型

1
2
3
4
5
6
7
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  #這個是範圍

    unpack():

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

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

  先看一段僞代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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端代碼示例:報頭:就是消息的頭部信息,咱們要發送的真實內容爲報頭後面的內容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()

  client端代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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編碼
相關文章
相關標籤/搜索