Day10 Python網絡編程 Socket編程

1、客戶端/服務器架構

1.C/S架構,包括:

  1.硬件C/S架構(打印機)python

  2.軟件C/S架構(web服務)【QQ,SSH,MySQL,FTP】linux

2.C/S架構與socket的關係:

  咱們學習socket就是爲了完成C/S架構的開發web

3.預備知識:

      須知一個完整的計算機系統是由硬件和軟件構成,軟件又分爲:操做系統和應用軟件。算法

      互聯網之間的通訊都必須遵循統一的規範,這個統一的規範就是協議,就比如全世界人通訊的標準是英語,互聯網協議就是計算機界的英語,全部的計算機都就能夠按照統一的標準去收發信息從而完成通訊了!shell

4.互聯網世界中的兩套協議:

     1.學術界:OSI七層模型編程

     2.工業界:TCP四層模型json

二者對比:windows

咱們實際生產中實際上公認的標準就是使用的其實就是TCP四層模型! 緩存

工做在上述四層的協議分別爲:安全

數據包的傳輸過程其實是:

 

普及一點知識:

TCP/IP協議是傳輸層協議,主要解決數據如何在網絡中傳輸,而HTTP是應用層協議,主要解決如何包裝數據關於TCP/IP和HTTP協議的關係,網絡有一段比較容易理解的介紹:「咱們在傳輸數據時,能夠只使用(傳輸層)TCP/IP協議,可是那樣的話,若是沒有應用層,便沒法識別數據內容,若是想要使傳輸的數據有意義,則必須使用到應用層協議,應用層協議有不少,好比HTTP、FTP、TELNET等,也能夠本身定義應用層協議。WEB使用HTTP協議做應用層協議,以封裝HTTP 文本信息,而後使用TCP/IP作傳輸層協議將它發到網絡上。

TCP/IP協議棧主要分爲四層:應用層、傳輸層、網絡層[網絡互連層]、數據鏈路層[主機到網絡層],每層都有相應的協議,以下圖:

在網絡中,一幀以太網數據包的格式:

 

二.Socket網絡編程:

咱們知道兩個進程若是須要進行通信最基本的一個前提能可以惟一的標示一個進程,在本地進程通信中咱們可使用PID來惟一標示一個進程,但PID只在本地惟一,網絡中的兩個進程PID衝突概率很大,這時候咱們須要另闢它徑了,咱們知道IP層的ip地址能夠惟一標示主機,而TCP層協議和端口號能夠惟一標示主機的一個進程,這樣咱們能夠利用ip地址+協議+端口號惟一標示網絡中的一個進程,操做系統有0-65535個端口,每一個端口均可以獨立對外提供服務。。因此socket本質上就是在2臺網絡互通的電腦之間,架設一個通道,兩臺電腦經過這個通道來實現數據的互相傳遞,也就是說:創建一個socket必須至少有2端, 一個服務端,一個客戶端, 服務端被動等待並接收請求,客戶端主動發起請求, 鏈接創建以後,雙方能夠互發數據。 好比:【QQ,微信】 

可以惟一標示網絡中的進程後,它們就能夠利用socket進行通訊了,什麼是socket呢?咱們常常把socket翻譯爲套接字,socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操做抽象爲幾個簡單的接口供應用層調用已實現進程在網絡中通訊,從而簡化咱們的編程!

以下所示:咱們更加形象的給你們展現一下socket抽象層!

 

從上面能夠知道,咱們的socket編程是基於TCP或者UDP的,基於TCP的Socket編程咱們稱之爲基於TCP的Socket網絡編程,基於UDP的Socket編程咱們稱之爲基於UDP的Socket網絡編程!因此,咱們無需深刻理解tcp/udp協議,socket已經爲咱們封裝好了,咱們只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。

關鍵點:socket通訊是兩個進程之間的通信,每一個進程對應一個端口號!【區別於:線程】 

HTTP與Socket鏈接的區別:
因爲一般狀況下Socket鏈接就是TCP鏈接,所以Socket鏈接一旦創建,通訊雙方便可開始相互發送數據內容,直到雙方鏈接斷開。但在實際網絡應用中,客戶端到服務器之間的通訊每每須要穿越多箇中間節點,例如路由器、網關、防火牆等,大部分防火牆默認會關閉長時間處於非活躍狀態的鏈接而致使 Socket 鏈接斷連,所以須要經過輪詢告訴網絡,該鏈接處於活躍狀態。因此準確的說:Socket只算是鏈接,有侷限性,適用於文件傳輸,如:FTP!不適合B/S架構,適合C/S架構。


而HTTP鏈接使用的是「請求—響應」的方式,不只在請求時須要先創建鏈接,並且須要客戶端向服務器發出請求後,服務器端才能回覆數據。適用於B/S架構!

不少狀況下,須要服務器端主動向客戶端推送數據,保持客戶端與服務器數據的實時與同步。此時若雙方創建的是Socket鏈接,服務器就能夠直接將數據傳送給客戶端;若雙方創建的是HTTP鏈接,則服務器須要等到客戶端發送一次請求後才能將數據傳回給客戶端,所以,客戶端定時向服務器端發送鏈接請求,不只能夠保持在線,同時也是在「詢問」服務器是否有新的數據,若是有就將數據傳給客戶端。
HTTP與Socket鏈接的區別

1.基於TCP的Socket網絡編程

創建一個socket必須至少有2端, 一個服務端,一個客戶端, 服務端被動等待並接收請求,客戶端主動發起請求, 鏈接創建以後,雙方能夠互發數據。

各位,咱們知道對於全部的服務端和客戶端架構的鏈接而言,都是先啓動服務端,而後客戶端發送請求,服務端處理客戶端發送的請求,而後將結果返回給客戶端,而後再繼續!

因此這裏咱們先講服務端和客戶端的通訊流程,如上圖:

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

代碼演示:

import socket  #導入socket模塊
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  #咱們這裏編寫的代碼是基於網絡類型的套接字家族(AF_INET),同時在這裏咱們指定這是TCP鏈接協議,TCP協議是流式協議
server.bind(("127.0.0.1",8080)) #這裏要注意:綁定IP、端口號的時候 要用 元組的形式!端口號位於:0-65535這個區間
server.listen(5) #這裏咱們是寫死的,其實這裏能夠從配置文件中讀取的!
conn,addr = server.accept() # #接受客戶端連接,接收客戶端鏈接(至關於TCP協議中的創建鏈接的過程【3次握手】),經過該方法能夠返回(雙方的鏈接信息,客戶端的IP地址和端口號),注意這是元組的形式!
print("tcp的鏈接:",conn)
print("客戶端的地址",addr)
data = conn.recv(1024)  #收消息,這個1024是指接收的字節數,獲得的是data返回值是bytes二進制值!
print("from client msg:%s"%data)
服務端代碼
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

client.send("hello".encode("utf-8"))  # 必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
data = client.recv(1024)
print(data)
client.close()
客戶端代碼

最後注意:運行程序時,要先運行服務器代碼,再運行客戶端代碼代碼中也必定要將server或client 給close()掉,不然會報出:一般每一個套接字地址(協議/網絡地址/端口)只容許使用一次的錯誤

socket方法說明
  2)connect()函數
     對於客戶端的 connect() 函數,該函數的功能爲客戶端主動鏈接服務器,創建鏈接是經過三次握手,而這個鏈接的過程是由內核完成,
     不是這個函數完成的,這個函數的做用僅僅是通知 Linux 內核,讓 Linux 內核自動完成 TCP 三次握手鍊接(三次握手詳情,請看《淺談 TCP 三次握手》),
     最後把鏈接的結果返回給這個函數的返回值(成功鏈接爲0, 失敗爲-1)。
 
     一般的狀況,客戶端的 connect() 函數默認會一直阻塞,直到三次握手成功或超時失敗才返回(正常的狀況,這個過程很快完成)。

   3)listen()函數
    對於服務器,它是被動鏈接的。舉一個生活中的例子,一般的狀況下,移動的客服(至關於服務器)是等待着客戶(至關於客戶端)電話的到來。
    而這個過程,須要調用listen()函數。listen() 函數的主要做用就是將數值傳遞給參數backlog,backlog 的做用是設置內核中鏈接隊列的長度。
    def listen(self, backlog=None): (可看源碼)
    
    須要注意的是:listen()函數不會阻塞,它主要作的事情爲,將該套接字和套接字對應的鏈接隊列長度告訴 Linux 內核,而後,listen()函數就結束。
    這樣的話,當有一個客戶端主動鏈接(connect()),Linux 內核就自動完成TCP 3次握手,將創建好的連接自動存儲到隊列中,如此重複。
    因此,只要 TCP 服務器調用了 listen(),客戶端就能夠經過 connect() 和服務器創建鏈接,而這個鏈接的過程是由內核完成。
          
   知識點補充:【三次握手的鏈接隊列】
        這裏詳細的介紹一下 listen() 函數的第二個參數( backlog)的做用:告訴內核鏈接隊列的長度。
        爲了更好的理解 backlog 參數,咱們必須認識到內核爲任何一個給定的監聽套接口維護兩個隊列:
        1、未完成鏈接隊列(incomplete connection queue),每一個這樣的 SYN 分節對應其中一項:已由某個客戶發出併到達服務器,
           而服務器正在等待完成相應的 TCP三次握手過程。這些套接口處於 SYN_RCVD 狀態。
        2、已完成鏈接隊列(completed connection queue),每一個已完成 TCP 三次握手過程的客戶對應其中一項。這些套接口處於 ESTABLISHED 狀態。
         
   圖解計算機的三次握手:
     當來自客戶的 SYN 到達時,TCP 在未完成鏈接隊列中建立一個新項,而後響應以三次握手的第二個分節:服務器的 SYN 響應,
     其中稍帶對客戶 SYN 的 ACK(即SYN+ACK),這一項一直保留在未完成鏈接隊列中,直到三次握手的第三個分節(客戶對服務器 SYN 的 ACK )到
     達或者該項超時爲止(曾經源自Berkeley的實現爲這些未完成鏈接的項設置的超時值爲75秒)。

    若是三次握手正常完成,該項就從未完成鏈接隊列移到已完成鏈接隊列的隊尾。
    
    backlog 參數歷史上被定義爲上面兩個隊列的大小之和,大多數實現默認值爲 5,當服務器把這個完成鏈接隊列的某個鏈接取走後,
    這個隊列的位置又空出一個,這樣來回實現動態平衡,但在高併發 web 服務器中此值顯然不夠。
    
    accept()函數
        accept()函數功能是,從處於 established 狀態的鏈接隊列頭部取出一個已經完成的鏈接,
        若是這個隊列沒有已經完成的鏈接,accept()函數就會阻塞,直到取出隊列中已完成的用戶鏈接爲止。
        
        若是,服務器不能及時調用 accept() 取走隊列中已完成的鏈接,隊列滿掉後會怎樣呢?
            UNP(《unix網絡編程》)告訴咱們,服務器的鏈接隊列滿掉後,服務器不會對再對創建新鏈接的syn進行應答,
            因此客戶端的 connect 就會返回 ETIMEDOUT。但實際上Linux的並非這樣的,TCP 的鏈接隊列滿後,
            Linux 不會如書中所說的所有拒絕鏈接,有些會延時鏈接!
connect、listen、accept方法說明

演變一:一次鏈接,交流屢次[通訊循環]

import socket  #導入socket模塊
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  #咱們這裏編寫的代碼是基於網絡類型的套接字家族(AF_INET),同時在這裏咱們指定這是TCP鏈接協議,TCP協議是流式協議
server.bind(("127.0.0.1",8080)) #這裏要注意:綁定IP、端口號的時候 要用 元組的形式!端口號位於:0-65535這個區間
server.listen(5) #這裏咱們是寫死的,其實這裏能夠從配置文件中讀取的!
conn,addr = server.accept() # #接受客戶端連接,接收客戶端鏈接(至關於TCP協議中的創建鏈接的過程【3次握手】),經過該方法能夠返回(雙方的鏈接信息,客戶端的IP地址和端口號),注意這是元組的形式!
print("tcp的鏈接:",conn)
print("客戶端的地址",addr)
while True: #通信循環
    data = conn.recv(1024)  #收消息,這個1024是指接收的字節數,獲得的是data返回值是bytes二進制值!
    print("from client msg:%s"%data)
    conn.send(data.upper()) #給客戶端發送消息 ,由於客戶端發送過來的是二進制的數據,將數據變成大寫以後依舊是二進制數據!

conn.close()  #關閉鏈接  只是將tcp鏈接關掉
server.close() #關閉服務器,把socket套接字給關掉!
服務端代碼
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080))

while True: #通信循環
    msg = input(">>: ")
    client.send(msg.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
    data = client.recv(1024)
    print(data)

client.close()
客戶端代碼

演變二:屢次鏈接【鏈接循環】

import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5)
while True: #鏈接循環
    conn,addr = server.accept()
    print("tcp的鏈接:",conn)
    print("客戶端的地址",addr)
    while True:#//通信循環
        data = conn.recv(1024)
        print("from client msg:%s"%data)
        conn.send(data.upper())

    conn.close()  #鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼

客戶端代碼不變,和上面同樣;

可是這裏實際是有問題的,也就是說,上面的服務端代碼只是形式上的屢次鏈接,實際上當客戶端代碼鏈接關閉以後,在服務器端的conn鏈接再去調用recv方法就會出異常,ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的鏈接。緣由:客戶端1忽然關閉鏈接,致使服務端出現異常,從而終止了服務端程序的正常運行!

那怎麼辦呢?出異常能咋辦,異常處理唄,以下:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8081))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)

    while True:  # //通信循環
        try:
            data = conn.recv(1024)
            print("from client msg:%s" % data)
            conn.send(data.upper())
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端異常處理

演變三:多客戶端鏈接

上面的代碼雖然一個客戶端能夠開啓、關閉鏈接,再開啓、再關閉鏈接,可是不能同時開啓多個客戶端鏈接【併發問題】,由於服務端的代碼會卡在一個鏈接裏面,也就是說:當兩個客戶端同時和一個服務器通訊的時候,只有一個客戶端能夠得到響應,只有這個客戶端關閉鏈接的時候,另外一個客戶端纔可以獲得響應!固然除此以外還有一個問題,就是客戶端程序啥都不輸入直接回車的問題:綜上所述咱們的服務端代碼仍是有問題的,主要有如下兩個問題:

1.不能處理併發問題
2.當客戶端什麼都不輸入的時候,直接回車,那麼服務端的conn.recv(1024)這句代碼會卡住,阻塞代碼執行[服務器和客戶端都在等着收數據];
針對上面的第2個問題,咱們能夠在客戶端解決,以下所示:

import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8081))

while True:
    msg = input(">>: ").strip()
    if not msg: continue  #python經常使用的判斷字符串爲空的方法
    client.send(msg.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
    data = client.recv(1024)
    print(data)

client.close()
客戶端代碼

咱們如今客戶端代碼是沒問題的,可是此時客戶端的代碼若是是在MAC系統或者Linux系統上,若是咱們把客戶端忽然關閉,服務器端代碼會進入死循環,一直輸出爲空,

緣由就是:服務端代碼data = conn.recv(1024) 會接收到空數據,不會報異常,一直輸出空!因此這時服務器代碼還須要加一個判斷,以下所示:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("192.168.222.130", 8081))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)

    while True:  # //通信循環
        try:
            data = conn.recv(1024)
            if not data:break #針對Mac或者Linux系統上的客戶端忽然斷開鏈接的異常處理
            print("from client msg:%s" % data)
            conn.send(data.upper())
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼爲空判斷

案例:寫一個類ssh服務,將客戶端命令在服務器上執行,並將結果返回給客戶端!

import socket
import subprocess
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8081))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)

    while True:  # //通信循環
        try:
            cmd = conn.recv(1024)
            if not cmd:break #針對Mac或者Linux系統上的客戶端忽然斷開鏈接的異常處理
            print("from client msg:%s" % cmd)
            res = subprocess.Popen(cmd.decode("utf-8"), #注意:windows系統上運行的subprocess.Popen()方法,因此默認是以GBK編碼的
                                   shell = True,
                                   stdout = subprocess.PIPE,
                                   stderr = subprocess.PIPE)
            error = res.stderr.read()
            if error:
                back_msg = error
            else:
                back_msg = res.stdout.read()
            #conn.send(len(back_msg))
            conn.send(back_msg)
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8081))

while True:
    cmd = input(">>: ").strip()
    if not cmd: continue  #python經常使用的判斷字符串爲空的方法
    client.send(cmd.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
    res = client.recv(1024)
    print(res.decode("gbk")) #注意:這裏必定要用gbk格式的解碼

client.close()
客戶端代碼

運行代碼,輸入正確命令dir就會輸出正確結果,若是輸出的是錯誤命令,就會返回錯誤信息!

res=subprocess.Popen(cmd.decode('utf-8'),
                    shell=True,
                    stderr=subprocess.PIPE,
                    stdout=subprocess.PIPE)
的結果的編碼是以當前所在的系統爲準的,若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端須要用GBK解碼
且只能從管道里讀一次結果
subprocess注意點

2.粘包

 注意:上面有個問題,就是當輸入ipconfig命令的時候顯示沒問題,可是一旦接着輸入下一個命令的時候,那麼就會出現顯示的不是本條命令的結果,而是顯示上一條命令的結果,這樣程序就亂了,這就是粘包的現象!

 1. 什麼是粘包?

須知:只有TCP有粘包現象,UDP永遠不會粘包,爲什麼,且聽我娓娓道來

首先須要掌握一個socket收發消息的原理:

從上面咱們客戶端和服務端的進行數據傳輸的時候,實際上咱們從服務端發送到客戶端的數據並無直接發送給客戶端,而是發送到了服務端的緩存中,而後操做系統再將服務端的緩存中的數據又到了客戶端的緩存中,因此在客戶端接收的數據也是從客戶端本身的緩存中拿到的,而不是直接從服務端獲取的!那麼操做系統是怎麼發送服務端緩存中數據的呢?是經過TCP協議去發的,你這裏不是基於TCP的Socket網絡編程麼,那麼它就根據TCP去發,因此你會看到大家的操做系統上都有TCP/IP服務這個模塊,只有存在這個服務,你才能發送TCP協議的數據!

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

上面問題ipconfig命令的問題解釋:

當咱們服務器端發送了ipconfig命令以後,接收方設置1024個字節的時候,這個大小是能夠將整個ipconfig命令都接收過來的,而後咱們的應用程序,將在應用程序裏執行ipconfig命令,並將結果寫回到客戶端,可是此時客戶端咱們設置的是1024個字節,致使ipconfig的命令結果咱們沒法在客戶端所有接收,剩下的數據就保存在服務器的緩存中,這樣客戶端就將客戶端緩存中的1024個字節所有輸出了,此時計算機程序會執行下一次循環,執行輸入,輸入以後執行下面的程序就是從服務端的緩存中讀數據,這樣咱們就看到了輸入的命令與輸出結果不一致,輸出的是上一次命令的結果,此時實際上咱們第二次命令的結果已經輸入到客戶端的緩存中了,可是此時咱們的程序又進入了下一次循環,先要輸入才能看到數據,這就是一個惡性循環了!

小Demo演示:

import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5)
conn,addr = server.accept()
cmd = conn.recv(1)
print(cmd)
data = conn.recv(10)
print(data)
conn.close()  #鏈接循環的時候,要記得將這個鏈接也關閉了!
server.close()
服務端代碼
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080))
client.send("hello".encode("utf-8"))
client.send("world".encode("utf-8"))
client.close()
客戶端代碼

這樣就會看出問題了,若是咱們設置的接收字節數小於發送到緩存中的數據,那麼一次接收數據的時候就接收不徹底,等下次再接收的時候就會出現粘包的問題!

2.粘包產生的兩大緣由:

1.先說TCP:因爲TCP協議自己的機制(面向鏈接的可靠地協議-三次握手機制)客戶端與服務器會維持一個鏈接(Channel),數據在鏈接不斷開的狀況下,能夠持續不斷地將多個數據包發往服務器,可是若是發送的網絡數據包過小,那麼他自己會啓用Nagle算法(可配置是否啓用)對較小的數據包進行合併(基於此,TCP的網絡延遲要UDP的高些)而後再發送(超時或者包大小足夠)。那麼這樣的話,服務器在接收到消息(數據流)的時候就沒法區分哪些數據包是客戶端本身分開發送的,這樣產生了粘包;

2.服務器在接收到數據後,放到緩衝區中,若是消息沒有被及時從緩存區中所有取走,下次在取數據的時候可能就會出現取出的是上一個數據包中的數據的狀況,形成粘包現象(確切來說,對於基於TCP協議的應用,不該用包來描述,而應用 流來描述),我的認爲服務器接收端產生的粘包應該與linux內核處理socket的方式 select輪詢機制的線性掃描頻度無關。
 
再說UDP:自己做爲無鏈接的不可靠的傳輸協議(適合頻繁發送較小的數據包),他不會對數據包進行合併發送(也就沒有Nagle算法之說了),他直接是一端發送什麼數據,直接就發出去了,既然他不會對數據合併,每個數據包都是完整的(數據+UDP頭+IP頭等等發一次數據封裝一次)也就沒有粘包一說了。
 
[TCP協議的內部優化機制]:形成粘包的第一種方式咱們若是在發送的時候,不讓它將小數據連續發送【等一下子再發】,那麼粘包問題就能夠解決了,以下演示:
import socket
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5)
conn,addr = server.accept()
cmd = conn.recv(104)
print(cmd)
data = conn.recv(1024)
print(data)
conn.close()  #鏈接循環的時候,要記得將這個鏈接也關閉了!
server.close()
服務端代碼
import socket
import time
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080))
client.send("hello".encode("utf-8"))
time.sleep(5)
client.send("world".encode("utf-8"))
client.close()
客戶端代碼

這種問題固然咱們能夠手動控制發送的速度,這是能夠的,可是問題是若是個人程序在作交互的時候,就是程序來完成的,那麼這種人爲控制速度的方式就有點不適合了(固然咱們仍是能夠經過在客戶端程序中import time ,而後在屢次發送數據請求之間使用time.sleep(5)代碼),可是若是按照這種方式咱們的高併發也就作不了了!

那還有沒有別的方式呢?有的,咱們能夠在客戶端發送數據的時候,將發送數據的大小也發送過去,讓服務器端知道咱們要發送的數據有多長就OK了,以下所示:

import socket
import subprocess  # subprocess最簡單的用法就是調用shell命令了

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8000))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    while True:  # //通信循環
        try:
            cmd = conn.recv(1024)
            if not cmd: break  # 解決當recv方法接收爲空,linux或者mac進入死循環問題
            print("from client msg:%s" % cmd)
            res = subprocess.Popen(cmd.decode("utf-8"),  # 注意:windows系統上運行的subprocess.Popen()方法,因此默認是以GBK編碼的
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            error = res.stderr.read()
            if error:
                back_msg = error
            else:
                back_msg = res.stdout.read()
                print("===",back_msg)
            conn.send(str(len(back_msg)).encode("utf-8")) #將數據的長度編碼成utf-8發過去!
            conn.send(back_msg)
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼

上述代碼在發送數據以前咱們先把數據的長度發送過去,這樣問題就解決了,可是這裏的問題是,這個長度的大小是多少呢?

以下所示:當數據變化的時候,數據的長度也是變化的,因此數據的長度是不固定的!

那有沒有什麼方法能把一串數字打包成一個二進制,而且長度是固定的,這個問題就解決了,有這麼一個模塊【struct模塊】

那麼python中正好提供了一個struct模塊,它能夠將一個數字編碼成二進制,而且這串二進制的長度是固定的,這個問題就解決了!

上述的i,表示將後面的數據打包成4個字節;

接收端在拿到數據以後,只須要解碼就OK,以下所示:

解碼以後拿到的是一個元組,咱們取出第一個值就是咱們要接收的數據長度,以下所示:

並且數值是整形的!

 這時候客戶端實際上也就不能直接接收數據了,它須要在接收數據以前先將數據的長度接收了,長度就是固定的4個字節:
因此客戶端代碼是:
import socket
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8000))

while True: #客戶端只須要通信循環就好
    cmd = input(">>: ").strip()
    if not cmd: continue
    client.send(cmd.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
    data = client.recv(4)
    datasize=struct.unpack("i",data)[0]
    res = client.recv(datasize)
    print(res.decode("gbk"))  #注意:解碼的時候是gbk解碼的

client.close()
引入struct模塊以後的客戶端代碼
import socket
import struct
import subprocess  # subprocess最簡單的用法就是調用shell命令了

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8000))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("bb")
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)
    while True:  # //通信循環
        try:
            cmd = conn.recv(1024)
            if not cmd: break  # 解決當recv方法接收爲空,linux或者mac進入死循環問題
            print("from client msg:%s" % cmd)
            res = subprocess.Popen(cmd.decode("utf-8"),  # 注意:windows系統上運行的subprocess.Popen()方法,因此默認是以GBK編碼的
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            error = res.stderr.read()
            if error:
                back_msg = error
            else:
                back_msg = res.stdout.read()
                print(back_msg)
            conn.send(struct.pack("i", len(back_msg)))
            conn.send(back_msg)
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
引入struct以後的server端代碼

這樣發送和接收數據就沒問題了,就是在發送數據以前咱們先把數據的長度發送過去!

 

struct模塊詳解:

爲字節流加上自定義固定長度報頭報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據

struct模塊 

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

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

。。。。。。。。。

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

 

固然,不用struct模塊將數據的長度打包成固定大小的數據發送過去,也能夠採用其它方式,好比,連續多個\r\n,具體參考TCP/IP中的解決方式!

 

還存在什麼問題呢?
若是咱們send的數據比較大,當緩存放滿的時候,send的數據尚未發完,那麼用send函數發送數據的時候是否是就會丟數據啊,那咱們怎麼解決呢?咱們可使用在服務器端使用sendall方法,sendall方法能夠循環的調用send方法,一直到數據都發送完爲止,避免了在發送數據的時候遇到服務器的緩存滿的問題,這是在服務器端的解決方案,那麼在客戶端也不能直接接收全部的數據了,因此客戶端代碼也須要改動,以下所示:

import socket
import struct
import subprocess  # subprocess最簡單的用法就是調用shell命令了

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8000))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("bb")
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)
    while True:  # //通信循環
        try:
            cmd = conn.recv(1024)
            if not cmd: break  # 解決當recv方法接收爲空,linux或者mac進入死循環問題
            print("from client msg:%s" % cmd)
            res = subprocess.Popen(cmd.decode("utf-8"),  # 注意:windows系統上運行的subprocess.Popen()方法,因此默認是以GBK編碼的
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            error = res.stderr.read()
            if error:
                back_msg = error
            else:
                back_msg = res.stdout.read()
                print(back_msg)
            conn.send(struct.pack("i", len(back_msg)))
            conn.sendall(back_msg)  #循環調用send方法,直到將大數據發送完畢!
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼
import socket
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8000))

while True: #客戶端只須要通信循環就好
    cmd = input(">>: ").strip()
    if not cmd: continue
    client.send(cmd.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據
    data = client.recv(4)
    datasize=struct.unpack("i",data)[0]
    # res = client.recv(datasize)
    recv_size = 0  #存放已經接收的數據大小
    recv_bytes = b""  #存放接收的字節
    while recv_size < datasize:
        res = client.recv(1024)
        recv_bytes += res
        recv_size +=len(res)#這裏注意,不是每次都接收1024哦【最後一次】,因此加的是res的真實長度,而不是1024
    print(recv_bytes.decode("gbk"))  #注意:解碼的時候是gbk解碼的

client.close()
客戶端代碼

還有沒有問題呢?

可是上面實際上仍是有問題的,就是服務端設置struct包的 struct.pack('i',len(back_msg))時候,咱們設置的是"i"這個格式的!這個表示的是int類型,標準大小是4個字節,也就是說,這是有大小限制的,當超過這個大小的時候就會出問題,並且咱們發送的其實是由報頭+數據兩部分組成的,報頭中包含數據大小,文件名等信息,因此咱們在服務端的代碼就變成了以下:這樣報頭咱們就能夠設置成爲字典類型(鍵對應的值是沒有大小限制的)的就能夠了,可是字典類型的數據若是要在網絡中傳輸而且在接收端接收到字典以後還能直接使用,咱們就須要將字典序列化,因此在服務端還須要導入json,用來序列化它,轉換成json格式以後【JSON本質就相似於鍵值對形式的字符串】,而後咱們還須要進一步編碼可是此時服務器端的代碼就須要先報頭的長度給客戶端,再發報頭頭信息給客戶端,再發報文信息給客戶端!

 

import socket
import struct
import json
import subprocess  # subprocess最簡單的用法就是調用shell命令了

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8000))
server.listen(5)
while True:  # 鏈接循環
    conn, addr = server.accept()
    print("bb")
    print("tcp的鏈接:", conn)
    print("客戶端的地址", addr)
    while True:  # //通信循環
        try:
            cmd = conn.recv(1024)
            if not cmd: break  # 解決當recv方法接收爲空,linux或者mac進入死循環問題
            print("from client msg:%s" % cmd)
            res = subprocess.Popen(cmd.decode("utf-8"),  # 注意:windows系統上運行的subprocess.Popen()方法,因此默認是以GBK編碼的
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            error = res.stderr.read()
            if error:
                back_msg = error
            else:
                back_msg = res.stdout.read()
            header_dict={"datasize":len(back_msg)}
            header_json = json.dumps(header_dict)
            header_bytes = header_json.encode("utf-8");

            conn.send(struct.pack("i", len(header_bytes)))
            conn.send(header_bytes)
            conn.sendall(back_msg)
        except Exception:
            break
    conn.close()  # 鏈接循環的時候,要記得將這個鏈接也關閉了!

server.close()
服務端代碼

 由於服務端是分三次發送的,客戶端相應的也要作三次接收【報頭長度直接取出4個字節就OK,報頭數據也不是很大,因此咱們直接取出來就好,最後獲取數據自己】:

import socket
import struct
import json
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8000))

while True: #客戶端只須要通信循環就好
    cmd = input(">>: ").strip()
    if not cmd: continue
    client.send(cmd.encode("utf-8"))  #必定要注意:發送的數據要是Bytes格式的,即二進制形式的數據

    #收報頭長度信息
    head = client.recv(4)
    headsize=struct.unpack("i",head)[0]
    #收報頭信息(根據報頭長度)
    head_bytes = client.recv(headsize)
    head_json = head_bytes.decode("utf-8")
    #反序列化
    head_dict = json.loads(head_json)
    datasize = head_dict["datasize"] #取出真實數據的長度大小!

   #收真實的數據
    recv_size = 0
    recv_bytes = b""
    while recv_size < datasize:
        res = client.recv(1024)
        recv_bytes += res
        recv_size +=len(res)
    print(recv_bytes.decode("gbk","ignore"))  #注意:解碼的時候是gbk解碼的

client.close()
客戶端代碼

提示:若是在寫代碼的時候報這個錯誤UnicodeDecodeError: ‘XXX' codec can't decode bytes in position 2-5: illegal multibyte sequence 

錯誤緣由:

這是由於遇到了非法字符,例如:全角空格每每有多種不一樣的實現方式,好比\xa3\xa0,或者\xa4\x57,
這些字符,看起來都是全角空格,但它們並非「合法」的全角空格
真正的全角空格是\xa1\xa1,所以在轉碼的過程當中出現了異常。 
而以前在處理新浪微博數據時,遇到了非法空格問題致使沒法正確解析數據。

解決辦法:

#將獲取的字符串strTxt作decode時,指明ignore,會忽略非法字符,

#固然對於gbk等編碼,處理一樣問題的方法是相似的

strTest = strTxt.decode('utf-8', 'ignore')

return strTest

[補充]

默認的參數就是strict,表明遇到非法字符時拋出異常; 
若是設置爲ignore,則會忽略非法字符; 
若是設置爲replace,則會用?號取代非法字符; 
若是設置爲xmlcharrefreplace,則使用XML的字符引用。 

3.基於UDP的套接字

#_*_coding:utf-8_*_
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server_client.bind(ip_port)

while True:
    msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(msg,addr)
    udp_server_client.sendto(msg.upper(),addr)
基於UDP的服務端代碼
#_*_coding:utf-8_*_
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    udp_server_client.sendto(msg.encode('utf-8'),ip_port)
    back_msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'),addr)
基於UDP的客戶端代碼

UDP和TCP的區別就是:UDP是無鏈接的,因此UDP雖然是有端口的,可是UDP是不須要監聽的【無鏈接的】,也不須要accept的,並且接收和發送的方法也變成了recvfrom、sendto方法了,而且recvfrom方法的返回值再也不是鏈接、地址,而是接收的 數據、地址,sendto('data',IPADDR_PORT)方法裏面的參數也成了數據和IP地址_端口號,

UDP不會發生粘包現象

UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息)[UDP協議底層支持的],這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。

 

TCP的三次握手和四次揮手

TCP之因此是數據安全的,是由於在TCP創建鏈接以後,每次都是須要進行數據確認的,可是UDP在數據傳輸的時候,沒有數據確認這個環節,只管着發,無論對方是否可以接收到,因此說UDP是數據不安全的!

 

Tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭!

UDP不可靠的鏈接,應用場景在於QQ,TCP與UDP的區別主要是在創建鏈接以後,TCP在數據傳輸的時候是有數據確認功能的,而UDP是沒有數據確認功能的!

4.用SocketServer實現高併發

上面講的知識點都是單線程的,實現不了併發,可是咱們這裏又沒有學習多線程,還好python給咱們提供了一個socketserver模塊,該模塊能夠將單線程的套接字作成多線程的,實現併發,代碼以下所示:

import socketserver
class FtpServer(socketserver.BaseRequestHandler):#這個類不能隨便定義,要繼承socketserver下面的BaseRequestHandler
    def handle(self):                            #BaseRequestHandler處理通訊
        print(self.request) #其實就是conn
        print(self.client_address) #其實addr
        while True:#相似於通訊循環!
            data = self.request.recv(1024)
            self.request.send(data.upper())

if __name__=="__main__":
    s= socketserver.ThreadingTCPServer(("127.0.0.1",8000),FtpServer) #處理鏈接
    s.serve_forever() #相似與鏈接循環
socketserver服務端代碼
from socket import *
client = socket(AF_INET,SOCK_STREAM)
client.connect(("127.0.0.1",8000))
while True:
    msg = input(">>:")
    client.send(msg.encode("utf-8"))
    data = client.recv(1024)
    print(data)
socketserver客戶端代碼

上述代碼就相似於qq聊天,能夠同時多個客戶端去跟服務端通訊!

 

5.做業:多用戶在線的FTP程序

1.FTP是什麼?FTP是文件傳輸協議

2.具體細節

 

import os
import json
import struct
from socket import *
class FtpClient:
    def __init__(self,ip,port,Family=AF_INET,Type=SOCK_STREAM):
        self.client=socket(AF_INET,SOCK_STREAM)
        self.client.connect((ip,port))

    def run(self):
        while True:
            inp=input('>>: ').strip()
            if not cmd:continue
            cmd,attr=inp.split() #put /a/b/c/a.txt
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(attr)

    def put(self,filepath):
        filename=os.path.basename(filepath)
        filesize=os.path.getsize(filepath)
        head_dict={
            'cmd':'put',
            'filesize':filesize,
            'filename':filename
        }
        head_json=json.dumps(head_dict)
        head_bytes=head_json.encode('utf-8')

        self.client.send(struct.pack('i',len(head_bytes)))
        self.client.send(head_bytes)
        with open(filepath,'rb') as f:
            for line in f:
                self.client.send(line)




if __name__ == '__main__':
    f=FtpClient('127.0.0.1',8080)
    f.run()
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()






#下列代碼與本題無關
class MYUDPServer:

    """UDP server class."""
    address_family = socket.AF_INET
    socket_type = socket.SOCK_DGRAM
    allow_reuse_address = False
    max_packet_size = 8192
    coding='utf-8'
    def get_request(self):
        data, client_addr = self.socket.recvfrom(self.max_packet_size)
        return (data, self.socket), client_addr
    def server_activate(self):
        # No need to call listen() for UDP.
        pass
    def shutdown_request(self, request):
        # No need to shutdown anything.
        self.close_request(request)
    def close_request(self, request):
        # No need to close anything.
        pass
FTP服務端代碼

因此寫FTP要去客戶端有什麼方法,服務端就有什麼方法就OK!

 

 socket起源於UNIX,在Unix一切皆文件哲學的思想下,socket是一種"打開—讀/寫—關閉"模式的實現,服務器和客戶端各自維護一個"文件",在創建鏈接打開後,能夠向本身文件寫入內容供對方讀取或者讀取對方內容,通信結束時關閉文件。

相關文章
相關標籤/搜索