1. 程式人生 > 其它 >十二、網路程式設計

十二、網路程式設計

十二、網路程式設計

網路程式設計:使用程式語言實現多臺計算機的通訊。

12.1、網路三要素

網路程式設計三要素:

                (1)IP地址:網路中每一臺計算機的唯一標識,通過IP地址找到指定的計算機。

                (2)埠:用於標識程序的邏輯地址,通過埠找到指定程序。

                (3)協議:定義通訊規則,符合協議則可以通訊,不符合不能通訊。一般有TCP協議和UDP協議。

(1)IP地址

計算機分佈在世界各地,要想和它們通訊,必須要知道確切的位置。確定計算機位置的方式有多種,IP 地址是最常用的,例如,114.114.114.114 是國內第一個、全球第三個開放的 DNS 服務地址,127.0.0.1 是本機地址。

其實,我們的計算機並不知道 IP 地址對應的地理位置,當要通訊時,只是將 IP 地址封裝到要傳送的資料包中,交給路由器去處理。路由器有非常智慧和高效的演算法,很快就會找到目標計算機,並將資料包傳遞給它,完成一次單向通訊。

目前大部分軟體使用 IPv4 地址,但 IPv6 也正在被人們接受,尤其是在教育網中,已經大量使用。

(2)埠

有了 IP 地址,雖然可以找到目標計算機,但仍然不能進行通訊。一臺計算機可以同時提供多種網路服務,例如Web服務、FTP服務(檔案傳輸服務)、SMTP服務(郵箱服務)等,僅有 IP 地址,計算機雖然可以正確接收到資料包,但是卻不知道要將資料包交給哪個網路程式來處理,所以通訊失敗。

為了區分不同的網路程式,計算機會為每個網路程式分配一個獨一無二的埠號(Port Number),例如,Web服務的埠號是 80,FTP 服務的埠號是 21,SMTP 服務的埠號是 25。

埠(Port)是一個虛擬的、邏輯上的概念。可以將埠理解為一道門,資料通過這道門流入流出,每道門有不同的編號,就是埠號。如下圖所示:

(3)協議

協議(Protocol)就是網路通訊的約定,通訊的雙方必須都遵守才能正常收發資料。協議有很多種,例如 TCP、UDP、IP 等,通訊的雙方必須使用同一協議才能通訊。協議是一種規範,由計算機組織制定,規定了很多細節,例如,如何建立連線,如何相互識別等。

協議僅僅是一種規範,必須由計算機軟體來實現。例如 IP 協議規定了如何找到目標計算機,那麼各個開發商在開發自己的軟體時就必須遵守該協議,不能另起爐灶。

所謂協議族(Protocol Family),就是一組協議(多個協議)的統稱。最常用的是 TCP/IP 協議族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百個互為關聯的協議,由於 TCP、IP 是兩種常用的底層協議,所以把它們統稱為 TCP/IP 協議族。

(4)資料傳輸方式

計算機之間有很多資料傳輸方式,各有優缺點,常用的有兩種:SOCK_STREAM 和 SOCK_DGRAM。

  1. SOCK_STREAM 表示面向連線的資料傳輸方式。資料可以準確無誤地到達另一臺計算機,如果損壞或丟失,可以重新發送,但效率相對較慢。常見的 http 協議就使用 SOCK_STREAM 傳輸資料,因為要確保資料的正確性,否則網頁不能正常解析。

  2. SOCK_DGRAM 表示無連線的資料傳輸方式。計算機只管傳輸資料,不作資料校驗,如果資料在傳輸中損壞,或者沒有到達另一臺計算機,是沒有辦法補救的。也就是說,資料錯了就錯了,無法重傳。因為 SOCK_DGRAM 所做的校驗工作少,所以效率比 SOCK_STREAM 高。

QQ 視訊聊天和語音聊天就使用 SOCK_DGRAM 傳輸資料,因為首先要保證通訊的效率,儘量減小延遲,而資料的正確性是次要的,即使丟失很小的一部分資料,視訊和音訊也可以正常解析,最多出現噪點或雜音,不會對通訊質量有實質的影響。

注意:SOCK_DGRAM 沒有想象中的糟糕,不會頻繁的丟失資料,資料錯誤只是小概率事件。

有可能多種協議使用同一種資料傳輸方式,所以在 socket 程式設計中,需要同時指明資料傳輸方式和協議。

綜上所述:IP地址和埠能夠在廣袤的網際網路中定位到要通訊的程式,協議和資料傳輸方式規定了如何傳輸資料,有了這些,兩臺計算機就可以通訊了。

12.2、TCP協議

(1)OSI模型

如果你讀過計算機專業,或者學習過網路通訊,那你一定聽說過 OSI 模型,它曾無數次讓你頭大。OSI 是 Open System Interconnection 的縮寫,譯為“開放式系統互聯”。 OSI 模型把網路通訊的工作分為 7 層,從下到上分別是物理層、資料鏈路層、網路層、傳輸層、會話層、表示層和應用層。

這個網路模型究竟是幹什麼呢?簡而言之就是進行資料封裝的。

當另一臺計算機接收到資料包時,會從網路介面層再一層一層往上傳輸,每傳輸一層就拆開一層包裝,直到最後的應用層,就得到了最原始的資料,這才是程式要使用的資料。

2)TCP報文格式

TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連線的、可靠的、基於位元組流的通訊協議,資料在傳輸前要建立連線,傳輸完畢後還要斷開連線。

客戶端在收發資料前要使用 connect() 函式和伺服器建立連線。建立連線的目的是保證IP地址、埠、物理鏈路等正確無誤,為資料的傳輸開闢通道。

TCP建立連線時要傳輸三個資料包,俗稱三次握手(Three-way Handshaking)。可以形象的比喻為下面的對話:

  • [Shake 1] 套接字A:“你好,套接字B,我這裡有資料要傳送給你,建立連線吧。”
  • [Shake 2] 套接字B:“好的,我這邊已準備就緒。”
  • [Shake 3] 套接字A:“謝謝你受理我的請求。”
  1. 序號:Seq(Sequence Number)序號佔32位,用來標識從計算機A傳送到計算機B的資料包的序號,計算機發送資料時對此進行標記。

  2. 確認號:Ack(Acknowledge Number)確認號佔32位,客戶端和伺服器端都可以傳送,Ack = Seq + 1。

  3. 標誌位:每個標誌位佔用1Bit,共有6個,分別為 URG、ACK、PSH、RST、SYN、FIN,具體含義如下:

// URG:緊急指標(urgent pointer)有效。
// ACK:確認序號有效。
// PSH:接收方應該儘快將這個報文交給應用層。
// RST:重置連線。
// SYN:建立一個新連線。
// FIN:斷開一個連線。

3)TCP/IP三次握手

使用 connect() 建立連線時,客戶端和伺服器端會相互發送三個資料包,請看下圖:

 

客戶端呼叫 socket() 建立套接字後,因為沒有建立連線,所以套接字處於CLOSED狀態;伺服器端呼叫 listen() 函式後,套接字進入LISTEN狀態,開始監聽客戶端請求。這個時候,客戶端開始發起請求:

  1. 當客戶端呼叫 connect() 函式後,TCP協議會組建一個數據包,並設定 SYN 標誌位,表示該資料包是用來建立同步連線的。同時生成一個隨機數字 1000,填充“序號(Seq)”欄位,表示該資料包的序號。完成這些工作,開始向伺服器端傳送資料包,客戶端就進入了SYN-SEND狀態。

  2. 伺服器端收到資料包,檢測到已經設定了 SYN 標誌位,就知道這是客戶端發來的建立連線的“請求包”。伺服器端也會組建一個數據包,並設定 SYN 和 ACK 標誌位,SYN 表示該資料包用來建立連線,ACK 用來確認收到了剛才客戶端傳送的資料包。 伺服器生成一個隨機數 2000,填充“序號(Seq)”欄位。2000 和客戶端資料包沒有關係。伺服器將客戶端資料包序號(1000)加1,得到1001,並用這個數字填充“確認號(Ack)”欄位。伺服器將資料包發出,進入SYN-RECV狀態。

  3. 客戶端收到資料包,檢測到已經設定了 SYN 和 ACK 標誌位,就知道這是伺服器發來的“確認包”。客戶端會檢測“確認號(Ack)”欄位,看它的值是否為 1000+1,如果是就說明連線建立成功。接下來,客戶端會繼續組建資料包,並設定 ACK 標誌位,表示客戶端正確接收了伺服器發來的“確認包”。同時,將剛才伺服器發來的資料包序號(2000)加1,得到 2001,並用這個數字來填充“確認(Ack)”欄位。客戶端將資料包發出,進入ESTABLISED狀態,表示連線已經成功建立。

  4. 伺服器端收到資料包,檢測到已經設定了 ACK 標誌位,就知道這是客戶端發來的“確認包”。伺服器會檢測“確認號(Ack)”欄位,看它的值是否為 2000+1,如果是就說明連線建立成功,伺服器進入ESTABLISED狀態。至此,客戶端和伺服器都進入了ESTABLISED狀態,連線建立成功,接下來就可以收發資料了。

注意:三次握手的關鍵是要確認對方收到了自己的資料包,這個目標就是通過“確認號(Ack)”欄位實現的。計算機會記錄下自己傳送的資料包序號 Seq,待收到對方的資料包後,檢測“確認號(Ack)”欄位,看Ack = Seq + 1是否成立,如果成立說明對方正確收到了自己的資料包

(4)TCP/IP四次揮手

建立連線非常重要,它是資料正確傳輸的前提;斷開連線同樣重要,它讓計算機釋放不再使用的資源。如果連線不能正常斷開,不僅會造成資料傳輸錯誤,還會導致套接字不能關閉,持續佔用資源,如果併發量高,伺服器壓力堪憂。

建立連線需要三次握手,斷開連線需要四次握手,可以形象的比喻為下面的對話:

  • [Shake 1] 套接字A:“任務處理完畢,我希望斷開連線。”
  • [Shake 2] 套接字B:“哦,是嗎?請稍等,我準備一下。”
  • 等待片刻後……
  • [Shake 3] 套接字B:“我準備好了,可以斷開連線了。”
  • [Shake 4] 套接字A:“好的,謝謝合作。”

下圖演示了客戶端主動斷開連線的場景:

建立連線後,客戶端和伺服器都處於ESTABLISED狀態。這時,客戶端發起斷開連線的請求:

  1. 客戶端呼叫 close() 函式後,向伺服器傳送 FIN 資料包,進入FIN_WAIT_1狀態。FIN 是 Finish 的縮寫,表示完成任務需要斷開連線。

  2. 伺服器收到資料包後,檢測到設定了 FIN 標誌位,知道要斷開連線,於是向客戶端傳送“確認包”,進入CLOSE_WAIT狀態。注意:伺服器收到請求後並不是立即斷開連線,而是先向客戶端傳送“確認包”,告訴它我知道了,我需要準備一下才能斷開連線。

  3. 客戶端收到“確認包”後進入FIN_WAIT_2狀態,等待伺服器準備完畢後再次傳送資料包。

  4. 等待片刻後,伺服器準備完畢,可以斷開連線,於是再主動向客戶端傳送 FIN 包,告訴它我準備好了,斷開連線吧。然後進入LAST_ACK狀態。

  5. 客戶端收到伺服器的 FIN 包後,再向伺服器傳送 ACK 包,告訴它你斷開連線吧。然後進入TIME_WAIT狀態。

  6. 伺服器收到客戶端的 ACK 包後,就斷開連線,關閉套接字,進入CLOSED狀態。

注意:關於 TIME_WAIT 狀態的說明

客戶端最後一次傳送 ACK包後進入 TIME_WAIT 狀態,而不是直接進入 CLOSED 狀態關閉連線,這是為什麼呢?

/*
TCP 是面向連線的傳輸方式,必須保證資料能夠正確到達目標機器,不能丟失或出錯,而網路是不穩定的,隨時可能會毀壞資料,所以機器A每次向機器B傳送資料包後,都要求機器B”確認“,回傳ACK包,告訴機器A我收到了,這樣機器A才能知道資料傳送成功了。
如果機器B沒有回傳ACK包,機器A會重新發送,直到機器B回傳ACK包。
客戶端最後一次向伺服器回傳ACK包時,有可能會因為網路問題導致伺服器收不到,伺服器會再次傳送 FIN 包,如果這時客戶端完全關閉了連線,那麼伺服器無論如何也收不到ACK包了,所以客戶端需要等待片刻、確認對方收到ACK包後才能進入CLOSED狀態。
那麼,要等待多久呢?資料包在網路中是有生存時間的,超過這個時間還未到達目標主機就會被丟棄,並通知源主機。
這稱為報文最大生存時間(MSL,Maximum Segment Lifetime)。
TIME_WAIT 要等待 2MSL 才會進入 CLOSED 狀態。ACK 包到達伺服器需要 MSL 時間,伺服器重傳 FIN 包也需要 MSL 時間,2MSL 是資料包往返的最大時間,如果 2MSL 後還未收到伺服器重傳的 FIN 包,就說明伺服器已經收到了 ACK 包。
*/

12.3、socket介紹

12.3.1、什麼是 socket?

socket 的原意是“插座”,在計算機通訊領域,socket 被翻譯為“套接字”,它是計算機之間進行通訊的一種約定或一種方式。通過 socket 這種約定,一臺計算機可以接收其他計算機的資料,也可以向其他計算機發送資料。 我們把插頭插到插座上就能從電網獲得電力供應,同樣,為了與遠端計算機進行資料傳輸,需要連線到因特網,而 socket 就是用來連線到因特網的工具

12.3.2、socket緩衝區與阻塞

1、socket緩衝區

每個 socket 被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。write()/send() 並不立即向網路中傳輸資料,而是先將資料寫入緩衝區中,再由TCP協議將資料從緩衝區傳送到目標機器。一旦將資料寫入到緩衝區,函式就可以成功返回,不管它們有沒有到達目標機器,也不管它們何時被髮送到網路,這些都是TCP協議負責的事情。

TCP協議獨立於 write()/send() 函式,資料有可能剛被寫入緩衝區就傳送到網路,也可能在緩衝區中不斷積壓,多次寫入的資料被一次性發送到網路,這取決於當時的網路情況、當前執行緒是否空閒等諸多因素,不由程式設計師控制。read()/recv() 函式也是如此,也從輸入緩衝區中讀取資料,而不是直接從網路中讀取。

這些I/O緩衝區特性可整理如下:

  • I/O緩衝區在每個TCP套接字中單獨存在;
  • I/O緩衝區在建立套接字時自動生成;
  • 即使關閉套接字也會繼續傳送輸出緩衝區中遺留的資料;
  • 關閉套接字將丟失輸入緩衝區中的資料。

輸入輸出緩衝區的預設大小一般都是 8K!

2、阻塞模式

對於TCP套接字(預設情況下),當使用send() 傳送資料時:

(1) 首先會檢查緩衝區,如果緩衝區的可用空間長度小於要傳送的資料,那麼 send() 會被阻塞(暫停執行),直到緩衝區中的資料被髮 送到目標機器,騰出足夠的空間,才喚醒 send() 函式繼續寫入資料。

(2) 如果TCP協議正在向網路傳送資料,那麼輸出緩衝區會被鎖定,不允許寫入,send() 也會被阻塞,直到資料傳送完畢緩衝區解鎖, send() 才會被喚醒。

(3) 如果要寫入的資料大於緩衝區的最大長度,那麼將分批寫入。

(4) 直到所有資料被寫入緩衝區 send() 才能返回。

當使用recv() 讀取資料時:

(1) 首先會檢查緩衝區,如果緩衝區中有資料,那麼就讀取,否則函式會被阻塞,直到網路上有資料到來。

(2) 如果要讀取的資料長度小於緩衝區中的資料長度,那麼就不能一次性將緩衝區中的所有資料讀出,剩餘資料將不斷積壓,直到有 recv() 函式再次讀取。

(3) 直到讀取到資料後 recv() 函式才會返回,否則就一直被阻塞。

TCP套接字預設情況下是阻塞模式,也是最常用的。當然你也可以更改為非阻塞模式,後續我們會講解。

12.3.3、TCP的粘包問題

上節我們講到了socket緩衝區和資料的傳遞過程,可以看到資料的接收和傳送是無關的,read()/recv() 函式不管資料傳送了多少次,都會盡可能多的接收資料。也就是說,read()/recv() 和 write()/send() 的執行次數可能不同。

例如,write()/send() 重複執行三次,每次都發送字串"abc”,那麼目標機器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分兩次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字串"abcabcabc"。

這就是資料的“粘包”問題,客戶端傳送的多個數據包被當做一個數據包接收。也稱資料的無邊界性,read()/recv() 函式不知道資料包的開始或結束標誌(實際上也沒有任何開始或結束標誌),只把它們當做連續的資料流來處理。

12.4、Python的socket模組

12.4.1、建立套接字物件

Linux 中的一切都是檔案,每個檔案都有一個整數型別的檔案描述符;socket 也可以視為一個檔案物件,也有檔案描述符。

import socket
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# <socket.socket fd=496, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
print(sock)

1、AF 為地址族(Address Family),也就是 IP 地址型別,常用的有 AF_INET 和 AF_INET6。AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。大家需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址。

2、 type 為資料傳輸方式/套接字型別,常用的有 SOCK_STREAM(流格式套接字/面向連線的套接字) 和 SOCK_DGRAM(資料報套接字/無連線的套接字)。

3、sock = socket.socket()預設建立TCP套接字。

12.4.2、 套接字物件方法

(1)服務端:bind方法

socket 用來建立套接字物件,確定套接字的各種屬性,然後伺服器端要用 bind() 方法將套接字與特定的 IP 地址和埠繫結起來,只有這樣,流經該 IP 地址和埠的資料才能交給套接字處理。類似地,客戶端也要用 connect() 方法建立連線。

import socket

sock = socket.socket()
sock.bind(("127.0.0.1",8899))

(2)服務端:listen方法

通過 listen() 方法可以讓套接字進入被動監聽狀態,sock 為需要進入監聽狀態的套接字,backlog 為請求佇列的最大長度。所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字才會被“喚醒”來響應請求。當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再從緩衝區中讀取出來處理。如果不斷有新的請求進來,它們就按照先後順序在緩衝區中排隊,直到緩衝區滿。這個緩衝區,就稱為請求佇列(Request Queue)。

緩衝區的長度(能存放多少個客戶端請求)可以通過 listen() 方法的 backlog 引數指定,但究竟為多少並沒有什麼標準,可以根據你的需求來定,併發量小的話可以是10或者20。

如果將 backlog 的值設定為SOMAXCONN ,就由系統來決定請求佇列長度,這個值一般比較大,可能是幾百,或者更多。當請求佇列滿時,就不再接收新的請求。

注意:listen() 只是讓套接字處於監聽狀態,並沒有接收請求。接收請求需要使用 accept() 函式。

sock.listen(5)

(3)服務端:accept方法

當套接字處於監聽狀態時,可以通過 accept() 函式來接收客戶端請求。accept() 返回一個新的套接字來和客戶端通訊,addr 儲存了客戶端的IP地址和埠號,而 sock 是伺服器端的套接字,大家注意區分。後面和客戶端通訊時,要使用這個新生成的套接字,而不是原來伺服器端的套接字。

最後需要說明的是:listen() 只是讓套接字進入監聽狀態,並沒有真正接收客戶端請求,listen() 後面的程式碼會繼續執行,直到遇到 accept()。accept() 會阻塞程式執行(後面程式碼不能被執行),直到有新的請求到來。

conn,addr=sock.accept()

print("conn:",conn) # conn: <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 64915)>

print("addr:",addr) # addr: ('127.0.0.1', 64915)

(4)客戶端:connect方法

connect() 是客戶端程式用來連線服務端的方法,:

import socket
ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)

注意:只有經過connect連線成功後的套接字物件才能呼叫傳送和接受方法(send/recv),所以服務端的sock物件不能send or recv。

(5)收發資料方法:send和recv

方法 解析
s.recv() 接收 TCP 資料,資料以字串形式返回,bufsize 指定要接收的最大資料量。flag 提供有關訊息的其他資訊,通常可以忽略。
s.send() 傳送 TCP 資料,將 string 中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於 string 的位元組大小。

12.4.3、聊天案例

服務端:

import socket

sock = socket.socket()
sock.bind(("127.0.0.1", 8899))
sock.listen(5)

while 1:
    client_sock, addr = sock.accept()
    print("客戶端%s建立連線" % str(addr))
    while 1:
        try:
            data = client_sock.recv(1024) # data位元組傳
        except Exception:
            client_sock.close()
            print("客戶端%s退出"%str(addr))
            break
        print(data.decode())
        res = input(">>>")
        client_sock.send(res.encode())

客戶端:

import socket
ip_port=("127.0.0.1",8899)
sk = socket.socket()
sk.connect(ip_port)


while 1:
    data = input(">>>")
    sk.send(data.encode())
    res = sk.recv(1024)
    print("服務端:%s"%res.decode())

12.4.5、粘包案例

import socket
import time

s = socket.socket()
s.bind(("127.0.0.1",8080))
s.listen(5)

client,addr = s.accept()
time.sleep(10)
data = client.recv(1024)
print(data)

client.send(data)
import socket


s = socket.socket()
s.bind(("127.0.0.1",8080))

data = input(">>>")
s.send(data.encode())
s.send(data.encode())
s.send(data.encode())

res = s.recv(1024)
print(res)

12.4.6、ssh案例

服務端程式:

import socket
import subprocess
import time
import struct

sock = socket.socket()
sock.bind(("127.0.0.1", 8899))
sock.listen(5)

while 1:
    client_sock, addr = sock.accept()
    print("客戶端%s建立連線" % str(addr))
    while 1:
        try:
            cmd = client_sock.recv(1024)  # data位元組串
        except Exception:
            print("客戶端%s退出" % str(addr))
            client_sock.close()
            break
        print("執行命令:", cmd.decode("gbk"))

        # 版本1:記憶體問題
        # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
        # client_sock.send(cmd_res_bytes)

        # 版本2:粘包問題
        # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
        # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8")
        # client_sock.sendall(cmd_res_bytes_len)
        # client_sock.sendall(cmd_res_bytes)

        # 版本3:粘包解決方案

        # result_str = subprocess.getoutput(cmd.decode("gbk"))
        # result_bytes = bytes(result_str, encoding='utf8')
        # res_len = struct.pack('i',len(result_bytes))
        # client_sock.sendall(res_len)
        # client_sock.sendall(result_bytes)

        # cmd_res_bytes = subprocess.getoutput(cmd.decode("gbk")).encode()
        # cmd_res_bytes_len = bytes(str(len(cmd_res_bytes)),"utf8")
        # res_len = struct.pack('i', len(cmd_res_bytes))
        # client_sock.sendall(res_len)
        # client_sock.sendall(cmd_res_bytes)

客戶端程式:

import socket
import time
import struct

ip_port=("127.0.0.1",8899)
sk = socket.socket()
sk.connect(ip_port)

while 1:
    data = input("輸入執行命令>>>")
    sk.send(data.encode())

    # 版本1 記憶體問題
    # res = sk.recv(1024)
    # print("位元組長度:",len(res))
    # print("執行命令結果:%s"%(res.decode()))

    # 版本2 粘包問題
    # # time.sleep(5)
    # res_len = sk.recv(1024)
    # data = sk.recv(int(res_len.decode()))
    # print(res_len)
    # print(data.decode())

    # 版本3:粘包解決方案

    # length_msg = sk.recv(4)
    # length = struct.unpack('i', length_msg)[0]
    # msg = sk.recv(length).decode()
    # print("執行命令結果:",msg)

測試命令

ipconfig
netstat -an

12.4.7、案例之檔案上傳

服務端程式碼

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

import socket
import struct
import json
import os

base_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.join(base_dir, 'download')


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):
        print('server is running .......')
        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)
                    cmd = head_dic['cmd']
                    if hasattr(self, cmd):
                        func = getattr(self, cmd)
                        func(head_dic)
                except Exception:
                    break

    def put(self, args):
        """
        檔案長傳
        :param args:
        :return:
        """
        file_path = os.path.normpath(os.path.join(
            base_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(2048)
                f.write(recv_data)
                recv_size += len(recv_data)
            else:
                print('recvsize:%s filesize:%s' % (recv_size, filesize))

    def get(self, args):
        """ 下載檔案
        1 檢測服務端檔案是不是存在
        2 檔案資訊 打包發到客戶端
        3 傳送檔案
        """
        filename = args['filename']
        dic = {}
        if os.path.isfile(base_dir + '/' + filename):
            dic['filesize'] = os.path.getsize(base_dir + '/' + filename)
            dic['isfile'] = True
        else:
            dic['isfile'] = False
        str_dic = json.dumps(dic)  # 字典轉str
        bdic = str_dic.encode(self.coding)  # str轉bytes
        dic_len = len(bdic)  # 計算bytes的長度
        bytes_len = struct.pack('i', dic_len)  #
        self.conn.send(bytes_len)  # 傳送長度
        self.conn.send(bdic)  # 傳送字典
        # 檔案存在傳送真實檔案
        if dic['isfile']:
            with open(base_dir + '/' + filename, 'rb') as f:
                while dic['filesize'] > 2048:
                    content = f.read(2048)
                    self.conn.send(content)
                    dic['filesize'] -= len(content)
                else:
                    content = f.read(2048)
                    self.conn.send(content)
                    dic['filesize'] -= len(content)
            print('下載完成')


tcpserver1 = MYTCPServer(('127.0.0.1', 8083))

tcpserver1.run()

客戶端程式碼

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


import socket
import struct
import json
import os
import time

base_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.join(base_dir, 'local_dir')


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]
        filename = base_dir + '/' + filename
        print(filename)
        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
        t1 = time.time()
        # with open(filename,'rb') as f:
        #     for line in f:
        #         self.socket.send(line)
        #         send_size+=len(line)
        #     else:
        #         print('upload successful')
        #         t2 = time.time()
        with open(filename, 'rb') as f:
            while head_dic['filesize'] > 2048:
                content = f.read(2048)
                self.socket.send(content)
                head_dic['filesize'] -= len(content)
            else:
                content = f.read(2048)
                self.socket.send(content)
                head_dic['filesize'] -= len(content)
            t2 = time.time()

        print(t2 - t1)

    def get(self, args):
        cmd = args[0]
        filename = args[1]
        dic = {'cmd': cmd, 'filename': filename}
        """傳送dic的步驟
        字典轉str
        str轉bytes
        計算bytes的長度
        傳送長度
        傳送字典
        """
        str_dic = json.dumps(dic)  # 字典轉str
        bdic = str_dic.encode(self.coding)  # str轉bytes
        dic_len = len(bdic)  # 計算bytes的長度
        bytes_len = struct.pack('i', dic_len)  #
        self.socket.send(bytes_len)  # 傳送長度
        self.socket.send(bdic)  # 傳送字典

        # 接受 準備下載的檔案資訊
        dic_len = self.socket.recv(4)
        dic_len = struct.unpack('i', dic_len)[0]
        dic = self.socket.recv(dic_len).decode(self.coding)
        dic = json.loads(dic)
        # 檔案存在準備下載
        if dic['isfile']:
            t1 = time.time()
            with open(base_dir + '/' + filename, 'wb') as f:
                while dic['filesize'] > 2048:
                    content = self.socket.recv(2048)
                    f.write(content)
                    dic['filesize'] -= len(content)
                else:
                    while dic['filesize']:
                        content = self.socket.recv(2048)
                        f.write(content)
                        dic['filesize'] -= len(content)
                    t2 = time.time()
            print(t2 - t1)

        else:
            print('檔案不存在!')


client = MYTCPClient(('127.0.0.1', 8083))

client.run()