十二、網路程式設計
十二、網路程式設計
網路程式設計:使用程式語言實現多臺計算機的通訊。
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。
-
SOCK_STREAM 表示面向連線的資料傳輸方式。資料可以準確無誤地到達另一臺計算機,如果損壞或丟失,可以重新發送,但效率相對較慢。常見的 http 協議就使用 SOCK_STREAM 傳輸資料,因為要確保資料的正確性,否則網頁不能正常解析。
-
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:“謝謝你受理我的請求。”
-
序號:Seq(Sequence Number)序號佔32位,用來標識從計算機A傳送到計算機B的資料包的序號,計算機發送資料時對此進行標記。
-
確認號:Ack(Acknowledge Number)確認號佔32位,客戶端和伺服器端都可以傳送,Ack = Seq + 1。
-
標誌位:每個標誌位佔用1Bit,共有6個,分別為 URG、ACK、PSH、RST、SYN、FIN,具體含義如下:
// URG:緊急指標(urgent pointer)有效。 // ACK:確認序號有效。 // PSH:接收方應該儘快將這個報文交給應用層。 // RST:重置連線。 // SYN:建立一個新連線。 // FIN:斷開一個連線。
3)TCP/IP三次握手
使用 connect() 建立連線時,客戶端和伺服器端會相互發送三個資料包,請看下圖:
客戶端呼叫 socket() 建立套接字後,因為沒有建立連線,所以套接字處於CLOSED
狀態;伺服器端呼叫 listen() 函式後,套接字進入LISTEN
狀態,開始監聽客戶端請求。這個時候,客戶端開始發起請求:
當客戶端呼叫 connect() 函式後,TCP協議會組建一個數據包,並設定 SYN 標誌位,表示該資料包是用來建立同步連線的。同時生成一個隨機數字 1000,填充“序號(Seq)”欄位,表示該資料包的序號。完成這些工作,開始向伺服器端傳送資料包,客戶端就進入了
SYN-SEND
狀態。伺服器端收到資料包,檢測到已經設定了 SYN 標誌位,就知道這是客戶端發來的建立連線的“請求包”。伺服器端也會組建一個數據包,並設定 SYN 和 ACK 標誌位,SYN 表示該資料包用來建立連線,ACK 用來確認收到了剛才客戶端傳送的資料包。 伺服器生成一個隨機數 2000,填充“序號(Seq)”欄位。2000 和客戶端資料包沒有關係。伺服器將客戶端資料包序號(1000)加1,得到1001,並用這個數字填充“確認號(Ack)”欄位。伺服器將資料包發出,進入
SYN-RECV
狀態。客戶端收到資料包,檢測到已經設定了 SYN 和 ACK 標誌位,就知道這是伺服器發來的“確認包”。客戶端會檢測“確認號(Ack)”欄位,看它的值是否為 1000+1,如果是就說明連線建立成功。接下來,客戶端會繼續組建資料包,並設定 ACK 標誌位,表示客戶端正確接收了伺服器發來的“確認包”。同時,將剛才伺服器發來的資料包序號(2000)加1,得到 2001,並用這個數字來填充“確認(Ack)”欄位。客戶端將資料包發出,進入
ESTABLISED
狀態,表示連線已經成功建立。伺服器端收到資料包,檢測到已經設定了 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
狀態。這時,客戶端發起斷開連線的請求:
客戶端呼叫 close() 函式後,向伺服器傳送 FIN 資料包,進入
FIN_WAIT_1
狀態。FIN 是 Finish 的縮寫,表示完成任務需要斷開連線。伺服器收到資料包後,檢測到設定了 FIN 標誌位,知道要斷開連線,於是向客戶端傳送“確認包”,進入
CLOSE_WAIT
狀態。注意:伺服器收到請求後並不是立即斷開連線,而是先向客戶端傳送“確認包”,告訴它我知道了,我需要準備一下才能斷開連線。客戶端收到“確認包”後進入
FIN_WAIT_2
狀態,等待伺服器準備完畢後再次傳送資料包。等待片刻後,伺服器準備完畢,可以斷開連線,於是再主動向客戶端傳送 FIN 包,告訴它我準備好了,斷開連線吧。然後進入
LAST_ACK
狀態。客戶端收到伺服器的 FIN 包後,再向伺服器傳送 ACK 包,告訴它你斷開連線吧。然後進入
TIME_WAIT
狀態。伺服器收到客戶端的 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()