基於TCP協議的網絡通信


第二節:基於TCP協議的網絡通信

 

本節具體內容如下:

  1. 對上一節內容補充總結

  2. 單個客戶端與服務端通信

  3. 通信循環

  4. 通信,連接循環

  5. 遠程執行命令示例

  6. 提出粘包現象

     

    1.對上一節內容補充總結

    上一節我們通篇講的是網絡相關的知識,接觸了很多專業名詞以及各種協議,在本節開篇先對上一屆內容進行簡單的匯總梳理。

    • 相關名詞解釋

      • 互聯網協議:就是制定一系列全世界范圍內都公認的通信標准,讓全世界各地的人通過計算機都可以通信。(比如英語)

      • mac地址:網卡燒制的48位二進制一串數字,計算機上的唯一標識,全世界的每台計算機的mac都是不同的,唯一的,由12位16進制數表示,根據它可以查找局域網內計算機的位置。

      • 以太網協議:數據鏈路層的協議,可以將bit流分組,分成固定的頭18個字節(源mac地址,目標mac地址,數據類型)和數據兩部分 ,用於查找局域網內的目標計算機。

      • 廣播:計算機在局域網內通信方式,一個計算機發出數據,同一局域網內的其他計算機都可以接受到數據。

      • 單播:數據可以單獨發送,比如交換機接受到一個計算機來的數據之后,可以通過mac表查找目標mac地址對應的網口,然后單獨發送。

      • 局域網內的通信是通過廣播+以太網協議完成的。

      • 局域網,網段,子網:都是一個概念,都是局域網的意思。什么是局域網呢?

        局域網將一定區域內的各種計算機、外部設備和數據庫連接起來形成計算機通信網,通過專用數據線路與其他地方的局域網或數據庫連接,形成更大范圍的信息處理系統。局域網可以實現文件管理、應用軟件共享、打印機共享等功能,在使用過程當中,通過維護局域網網絡安全,能夠有效地保護資料安全,保證局域網網絡能夠正常穩定的運行。 局域網自身的組成大體由計算機設備、網絡連接設備、網絡傳輸介質3大部分構成,其中,計算機設備又包括服務器,工作站,網絡連接設備則包含了網卡、集線器、交換機,網絡傳輸介質簡單來說就是網線,由同軸電纜、雙絞線及光纜3大原件構成。

         

      • 集線器:存在局域網中,就是擴充網線端口,它沒有mac地址學習功能,只能廣播的形式進行通信。也就是low版的"交換機"。

         

         

      • 交換機:存在局域網中,也是擴充網線端口,但是能夠利用mac地址學習功能繪制mac地址~網線口表,可以通過單播的形式收發數據。常見的標准的固定端口數量有8、12、16、24、48等幾種。

        交換機與集線器的區別也就是集線器必須通過廣播的形式發送數據,而交換機存在mac地址~網口表,可以單播的形式發送數據。

         

         

      • 交換機mac地址學習功能:交換機存儲一張mac地址~端口對照表,作用是可以對照表快速定位目標mac的端口通過單播的形式傳遞數據。

      • ARP協議:通過計算機的IP地址獲取其mac地址。我們上一節只講了同一個局域網內ARP協議是如何工作的,本節會講到不同的局域網內ARP協議如何工作。

      • 路由器:簡單描述一下,路由器又可以稱為網關設備,它就是連接為外網與不同的子網傳遞數據,他的包含了很多協議,其中有幾個技術點需要我們清楚:

        1. DHCP協議,給局域網內的計算機自動分配IP地址。

        2. 路由器也有mac地址(下面會說到他的作用)。

        3. 路由協議,包含多個協議,主要目的就是選取到達目的路由的最優路徑。

        4. 默認網關:計算機A以廣播的形式發數據,當發現子網內都沒有找到目標mac時,就會將數據發送到路由器上的默認網關,然后由默認網關再將數據發送出去。一般默認網關的ip地址為xxx.xxx.xxx.1。

      • IP地址:標示的一個計算機的網絡地址一般都是四段十進制。

        • 公網IP:也可以直接稱為外網IP,可以直接訪問因特網,公網IP時唯一的。

        • 私網IP:就是路由器給你自動分配虛擬的IP,同一個局域網內的私網IP唯一,但是不可直接訪問因特網。

      • 子網掩碼:表示子網絡的一個參數,有兩個作用:

        • 與IP地址and運算,確定子網網段。

        • 不同種類的子網掩碼限定了局域網內ip地址的數量也就是限定了局域網內承載計算機的上限。

          A類子網掩碼:255.0.0.0

          B類子網掩碼:255.255.0.0

          C類子網掩碼:255.255.255.0

          由於我們國家引入計算機技術相對較晚,所以給我們國家分配的大部分都是C類子網掩碼,這就意味我們如果創建一個局域網,ip地址取值范圍0~255(0,255不能使用).

      • 端口協議:簡單說就是數據到傳輸層需要封裝客戶端與服務端的端口號,有兩種協議分別是UDP與TCP。

         

    • 同一局域網,計算機通信流程:

      https://www.processon.com/view/link/5d784083e4b04a19501d5ddb

      提取密碼:taibai

    • 不同局域網,計算機通信流程:

      https://www.processon.com/view/link/5d78ab9ae4b03461a3a4d184

      提取密碼:taibai

       

    • 額外解疑

      1. 有人常說數據鏈路層對應的設備除了計算機之外還有以太網(二層)交換機(就是我們講的交換機)為什么?

        因為以太網交換機會將計算機發出的數據進行拆包,但是只能拆到數據鏈路層,他需要查看源mac地址以及目標mac地址。

      2. 網絡層對應的設備有路由器,為什么?

        網絡層除了計算機在拆封數據時需要封裝或者解析IP地址,mac地址,路由器也是需要拆封數據直至網絡層,因為它需要查看源ip 目標ip,源mac,目標mac。

      3. 家用路由器與企業路由器的區別?

        家用路由器與企業路由器區別很多,對我們有幫助的區別就是:

        家用路由器(比如大學宿舍中一個路由器可以連接幾台電腦)只有一個外網IP,只能設置一個網段,每個連接電腦的端口都是內網IP,它既有路由的功能,也具有交換機的功能,因為電腦數量很少,不需要用交換機分端口。

        企業級路由器可以連接多個外網IP,可以設置多個網段,每個端口對應一個網段,每個端口都可以連接一個交換機,然后交換機在連接其他交換機...... 理論上,一個端口就可以分流出去255左右個IP地址。

      4. 路由器都必須有路由表(每個端口對應那個網段),路由協議(計算機給另一個網段發消息,計算最優路徑),ARP協議(存儲周圍獲着經常發送的路由器的mac地址與ip),可以具有交換機的功能(比如家用路由一般都有交換機的功能即有mac地址與端口的對照表)。

      5. 交換機主要就是分多個端口,而且可以泛洪(廣播的功能),具有mac地址表。

      6. ARP請求以廣播發送、以單播回應

      7. 路由器隔離廣播。每一個網段都是獨立的廣播域。

      8. 跨越網段通信必須使用網關的mac地址。

      9. 上面不同網段通信時,源IP與目的IP始終不變,但是只要經過路由就需要mac地址置換。

        如想深入分析,建議模擬環境,進行抓包操作,可以看到具體經歷了哪些過程,有助於深入了解。

       

      看socket之前,先來回顧一下五層通訊流程:

      img

      但實際上從傳輸層開始以及以下,都是操作系統以及各個硬件設備幫咱們完成的,下面的各種包頭封裝的過程,用咱們去一個一個做么?NO!

      img

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

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

      套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一台主機上多個應用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於文件型的和基於網絡型的。

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

      套接字家族的名字:AF_UNIX

      unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信

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

      套接字家族的名字:AF_INET

      (還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要么是只用於某個平台,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由於我們只關心網絡編程,所以大部分時候我么只使用AF_INET)

       

      2.單個客戶端與服務端通信

        • 服務端代碼示例:

          import socket
          ​
          # 1. 創建socket對象(買電話)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 參數可以默認不寫
          # 2. 綁定IP地址和端口  # 安裝電話卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 監聽(開機)
          phone.listen(5)
          ​
          # 4. 等待電話連接(等別人給你打電話)
          phone.accept()
          # 此時運行就會一直阻塞住。
          import socket
          

          參數詳解:

          socket.AF_INET:基於網絡的socket套接字。

          socket.SOCK_STREAM:基於TCP協議的socket套接字。

          phone.listen:這個知識點有一些不容易理解,服務端開啟之后,等待客戶端連接,listen做了一個客戶端數量的限定,listen(n)只有n+1的客戶端可以連接上我的服務端,但是連接上之后,只有第一個客戶端可以與服務端進行互相通信,其他的n個客戶端已經成功建立鏈接但是需要等待第一個客戶端結束之后,逐一進行通信,通信之前的狀態都是阻塞狀態;n+1以外的客戶端雖然也是阻塞,但是是連鏈接都建立不成的,就是單純的阻塞。只有第一客戶端結束之后,剩余的才可以逐一建立鏈接等待。這個其實與服務端開啟的半鏈接池相關,什么叫半鏈接池?服務端開啟之后,只要有客戶端鏈接我,理論上來說都可以與我建立鏈接的,但是只要建立鏈接,在我服務端就會占有一定的內存,暫存這些鏈接數據,試想一下,如果1000萬個鏈接進入我的服務端的內存,這樣會極大的浪費內存資源,所以服務端設置一個半鏈接池,只允許n+1個客戶端與我服務端建立鏈接,剩下的客戶端也是處於阻塞狀態,但是不會進入我的內存,這樣可以控制客戶端的數量,節省內存。

          accept:服務端會處於阻塞狀態,直至有客戶端鏈接我,服務端代碼才會向下執行。

          服務端代碼示例:此時運行一下就會一直阻塞住,我們繼續完善。

        • # 1. 創建socket對象(買電話)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 參數可以默認不寫
          # 2. 綁定IP地址和端口  # 安裝電話卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 監聽(開機)
          phone.listen(5)
          ​
          # 4. 等待電話連接(等別人給你打電話)
          print('start...')
          conn,client_addr = phone.accept()  # 此時運行就會一直阻塞住。
          print('連接來了:',conn,client_addr)
          ​
          # 5. 接受消息
          msg = conn.recv(1024)  # 每次至多讀取1024個字節
          print('客戶端的消息:',msg)
          conn.send(msg.upper())
          ​
          # 6. 關閉連接
          conn.close()
          ​
          # 7. 關機
          phone.close()

           

        • 客戶端代碼示例:

          import socket
          ​
          # 1. 創建socket對象(買電話)
          phone = socket.socket()
          ​
          # 2. 與服務端建立鏈接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 發消息
          phone.send('hello'.encode('utf-8'))
          from_server_data = phone.recv(1024)
          print(from_server_data)
          ​
          phone.close()
          ​

          同學們可以簡單練習一下。

          先開啟客戶端就會報錯,應該是先啟動服務端,然后在開啟客戶端。進行一個錯誤演示,先開啟客戶端,就會報錯: 

           

      3.通信循環

      真實場景是互相溝通,有收有發,通訊循環。

      • server端代碼示例:

        import socket
        ​
        # 1. 創建socket對象(買電話)
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 參數可以默認不寫
        # 2. 綁定IP地址和端口  # 安裝電話卡
        phone.bind(('127.0.0.1',8848))
        ​
        # 3. 監聽(開機)
        phone.listen(5)
        ​
        # 4. 等待電話連接(等別人給你打電話)
        print('start...')
        conn,client_addr = phone.accept()  # 此時運行就會一直阻塞住。
        print('連接來了:',conn,client_addr)
        ​
        # 5. 循環收發接受消息
        while 1:
            msg = conn.recv(1024)  # 每次至多讀取1024個字節
            print(f'來自客戶{client_addr}的消息:{msg.decode("utf-8")}')
            to_client = input('>>>')
            conn.send(to_client.encode('utf-8'))
        ​
        # 6. 關閉連接
        conn.close()
        # 7. 關機
        phone.close()

         

         

        • 客戶端代碼示例:

          import socket
          ​
          # 1. 創建socket對象(買電話)
          phone = socket.socket()
          ​
          # 2. 與服務端建立鏈接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 循環收發消息
          while 1:
              to_server = input('>>>')
              phone.send(to_server.encode('utf-8'))
              from_server_data = phone.recv(1024)
              print(from_server_data.decode('utf-8'))
          ​
          phone.close()

          此時如果你直接關閉客戶端,服務端就會出現如下的錯誤:

          所以,無論你的客戶端是合理關閉,或者強制關閉,你的服務端最起碼是正常關閉的。我們應該怎么解決?在服務端加上異常處理!

          服務端:

          import socket
          ​
          # 1. 創建socket對象(買電話)
          phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 參數可以默認不寫
          # 2. 綁定IP地址和端口  # 安裝電話卡
          phone.bind(('127.0.0.1',8848))
          ​
          # 3. 監聽(開機)
          phone.listen(5)
          ​
          # 4. 等待電話連接(等別人給你打電話)
          print('start...')
          conn,client_addr = phone.accept()  # 此時運行就會一直阻塞住。
          print('連接來了:',conn,client_addr)
          ​
          # 5. 循環收發接受消息
          while 1:
              try:
                  msg = conn.recv(1024)  # 每次至多讀取1024個字節
                  print(f'來自客戶{client_addr}的消息:{msg.decode("utf-8")}')
                  to_client = input('>>>')
                  conn.send(to_client.encode('utf-8'))
              except ConnectionResetError:
                  break# 6. 關閉連接
          conn.close()
          # 7. 關機
          phone.close()

          客戶端

          import socket
          ​
          # 1. 創建socket對象(買電話)
          phone = socket.socket()
          ​
          # 2. 與服務端建立鏈接
          phone.connect(('127.0.0.1',8848))
          ​
          # 3. 循環收發消息
          while 1:
              to_server = input('>>>')
              phone.send(to_server.encode('utf-8'))
              from_server_data = phone.recv(1024)
              print(from_server_data.decode('utf-8'))
          ​
          phone.close()
          但是上面也不是非常合理,因為你的客戶端無論是不是正常關閉,服務端一定是一直開啟狀態,等待其他人鏈接的。

      4.通信,連接循環

      • 服務端:

        import socket
        ​
        # 1. 創建socket對象(買電話)
        phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 參數可以默認不寫
        # 2. 綁定IP地址和端口  # 安裝電話卡
        phone.bind(('127.0.0.1',8848))
        ​
        # 3. 監聽(開機)
        phone.listen(3)
        ​
        # 4. 等待電話連接(等別人給你打電話)
        while 1:
            conn,client_addr = phone.accept()  # 此時運行就會一直阻塞住。
        # 5. 循環收發接受消息
            while 1:
                try:
                    msg = conn.recv(1024)  # 每次至多讀取1024個字節
                    print(f'來自客戶{client_addr}的消息:{msg.decode("utf-8")}')
                    to_client = input('>>>')
                    conn.send(to_client.encode('utf-8'))
                except ConnectionResetError:
                    break
            # 6. 關閉連接
            conn.close()
        # 7. 關機
        phone.close()
      • 客戶端:
        import socket
        ​
        # 1. 創建socket對象(買電話)
        phone = socket.socket()
        ​
        # 2. 與服務端建立鏈接
        phone.connect(('127.0.0.1',8848))
        ​
        # 3. 循環收發消息
        while 1:
            to_server = input('>>>')
            phone.send(to_server.encode('utf-8'))
            from_server_data = phone.recv(1024)
            print(from_server_data.decode('utf-8'))
        ​
        phone.close()

          5.遠程執行命令示例

      •  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM