中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

鄭州做網(wǎng)站的公司哪家專業(yè)seo網(wǎng)站

鄭州做網(wǎng)站的公司哪家,專業(yè)seo網(wǎng)站,天津網(wǎng)站制作的公司哪家好,用wordpress建wiki目錄 lwIP 初探TCP/IP 協(xié)議棧是什么TCP/IP 協(xié)議棧架構(gòu)TCP/IP 協(xié)議棧的封包和拆包 lwIP 簡介lwIP 源碼下載lwIP 文件說明 MAC 內(nèi)核簡介PHY 芯片介紹YT8512C 簡介LAN8720A 簡介 以太網(wǎng)接入MCU 方案軟件TCP/IP 協(xié)議棧以太網(wǎng)接入方案硬件TCP/IP 協(xié)議棧以太網(wǎng)接入方案 lwIP 無操作系…

目錄

  • lwIP 初探
    • TCP/IP 協(xié)議棧是什么
      • TCP/IP 協(xié)議棧架構(gòu)
      • TCP/IP 協(xié)議棧的封包和拆包
    • lwIP 簡介
      • lwIP 源碼下載
      • lwIP 文件說明
    • MAC 內(nèi)核簡介
    • PHY 芯片介紹
      • YT8512C 簡介
      • LAN8720A 簡介
    • 以太網(wǎng)接入MCU 方案
      • 軟件TCP/IP 協(xié)議棧以太網(wǎng)接入方案
      • 硬件TCP/IP 協(xié)議棧以太網(wǎng)接入方案
  • lwIP 無操作系統(tǒng)移植
  • lwIP 帶操作系統(tǒng)移植
  • ARP 協(xié)議
    • ARP 協(xié)議的簡介
      • ARP 協(xié)議的工作流程
      • ARP 緩存表的超時(shí)處理
    • APR 報(bào)文的報(bào)文結(jié)構(gòu)
    • ARP 協(xié)議層的接收與發(fā)送原理解析
      • 發(fā)送ARP 請(qǐng)求數(shù)據(jù)包
      • 接收ARP 應(yīng)答數(shù)據(jù)包
  • IP 協(xié)議
    • IP 協(xié)議的簡介
    • IP 數(shù)據(jù)報(bào)
      • IP 數(shù)據(jù)報(bào)結(jié)構(gòu)
      • IP 數(shù)據(jù)報(bào)的分片解析
      • IP 數(shù)據(jù)報(bào)的分片重裝
    • IP 數(shù)據(jù)報(bào)的輸出
    • IP 數(shù)據(jù)報(bào)的輸入
  • ICMP 協(xié)議
    • ICMP 協(xié)議簡介
      • ICMP 報(bào)文類型
      • ICMP 報(bào)文結(jié)構(gòu)
    • ICMP 的實(shí)現(xiàn)
      • ICMP 數(shù)據(jù)結(jié)構(gòu)體
      • 發(fā)送ICMP 差錯(cuò)報(bào)文
      • ICMP 報(bào)文處理
  • RAW 編程接口TCP 客戶端實(shí)驗(yàn)
    • TCP 協(xié)議
      • TCP 協(xié)議簡介
      • TCP 的建立連接
      • TCP 終止連接
      • TCP 報(bào)文結(jié)構(gòu)
      • lwIP 的TCP 報(bào)文首部數(shù)據(jù)結(jié)構(gòu)
      • lwIP 的TCP 連接狀態(tài)圖
      • lwIP 的TCP 控制塊
      • lwIP 的TCP 編程
      • lwIP 的TCP 建立與關(guān)閉連接原理
      • lwIP 中RAW API 編程接口中與TCP 相關(guān)的函數(shù)
    • RAW 接口的TCP 實(shí)驗(yàn)
      • 硬件設(shè)計(jì)
      • 軟件設(shè)計(jì)
      • 下載驗(yàn)證
  • NETCONN 編程接口TCP 客戶端實(shí)驗(yàn)
    • NETCONN 實(shí)現(xiàn)TCP 客戶端連接步驟
    • NETCONN 接口的TCPClient 實(shí)驗(yàn)
      • 硬件設(shè)計(jì)
      • 軟件設(shè)計(jì)
      • 下載驗(yàn)證
  • Socket 編程接口TCP 客戶端實(shí)驗(yàn)
    • Socket 編程TCP 客戶端流程
    • Socket 接口的TCPClient 實(shí)驗(yàn)
  • 基于MQTT 協(xié)議連接阿里云服務(wù)器
    • MQTT 協(xié)議簡介
      • MQTT 協(xié)議實(shí)現(xiàn)原理
      • 移植MQTT 協(xié)議
      • 配置遠(yuǎn)程服務(wù)器
    • 阿里云MQTT 協(xié)議實(shí)驗(yàn)
      • 硬件設(shè)計(jì)
      • 下載驗(yàn)證
  • 基于MQTT 協(xié)議連接OneNET 服務(wù)器
    • 配置OneNET 平臺(tái)
    • 工程配置
    • 基于OneNET 平臺(tái)MQTT 實(shí)驗(yàn)
      • 硬件設(shè)計(jì)
      • 下載驗(yàn)證
  • HTTP 客戶端實(shí)驗(yàn)
    • OneNTE 的HTTP 配置
    • HTTP 客戶端實(shí)驗(yàn)
      • 硬件設(shè)計(jì)
      • 軟件設(shè)計(jì)
      • 下載驗(yàn)證

lwIP 初探

本章,先介紹計(jì)算機(jī)網(wǎng)絡(luò)相關(guān)知識(shí),然后對(duì)lwIP軟件庫進(jìn)行概述,接著介紹MAC 內(nèi)核的基本知識(shí),最后探討LAN8720A 和YT8512C 以太網(wǎng)PHY 層芯片。

TCP/IP 協(xié)議棧是什么

TCP/IP 協(xié)議棧是一系列網(wǎng)絡(luò)協(xié)議的總和,是構(gòu)成網(wǎng)絡(luò)通信的核心骨架,它定義了電子設(shè)備如何連入因特網(wǎng),以及數(shù)據(jù)如何在它們之間進(jìn)行傳輸。

TCP/IP 協(xié)議采用4層結(jié)構(gòu),分別是應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層,每一層都呼叫它的下一層所提供的協(xié)議來完成自己的需求。由于我們大部分時(shí)間都工作在應(yīng)用層,下層的事情不用我們操心;其次網(wǎng)絡(luò)協(xié)議體系本身就很復(fù)雜龐大,入門門檻高,因此很難搞清楚TCP/IP 的工作原理。如果讀者想深入了解TCP/IP 協(xié)議棧的工作原理,可閱讀《計(jì)算機(jī)網(wǎng)絡(luò)書籍》。

TCP/IP 協(xié)議棧架構(gòu)

網(wǎng)絡(luò)協(xié)議有很多,如MQTT、TCP、UDP、IP 等協(xié)議,這些協(xié)議組成了TCP/IP 協(xié)議棧,同時(shí),這些協(xié)議具有層次性,它們分布在應(yīng)用層,傳輸層和網(wǎng)絡(luò)層。

TCP/IP 協(xié)議棧的分層結(jié)構(gòu)和網(wǎng)絡(luò)協(xié)議得對(duì)應(yīng)關(guān)系如下圖所示:

在這里插入圖片描述

由于OSI 模型和協(xié)議比較復(fù)雜,所以并沒有得到廣泛的應(yīng)用。而TCP/IP 模型因其開放性和易用性在實(shí)踐中得到了廣泛的應(yīng)用,它也成為互聯(lián)網(wǎng)的主流協(xié)議。

注意:網(wǎng)絡(luò)技術(shù)的發(fā)展并不是遵循嚴(yán)格的OSI分層概念。實(shí)際上現(xiàn)在的互聯(lián)網(wǎng)使用的是TCP/IP 體系結(jié)構(gòu)有時(shí)已經(jīng)演變成為圖1.1.1.2所示那樣,即某些應(yīng)用程序可以直接使用IP 層,或甚至直接使用最下面的網(wǎng)絡(luò)接口層。

在這里插入圖片描述

無論哪種表示方法,TCP/IP 模型各個(gè)層次都分別對(duì)應(yīng)于不同的協(xié)議。TCP/IP 協(xié)議棧負(fù)責(zé)確保網(wǎng)絡(luò)設(shè)備之間能夠通信。它是一組規(guī)則,規(guī)定了信息如何在網(wǎng)絡(luò)中傳輸。

這些協(xié)議都分布在應(yīng)用層,傳輸層和網(wǎng)絡(luò)層,網(wǎng)絡(luò)接口層是由硬件來實(shí)現(xiàn)。

如Windows 操作系統(tǒng)包含了CBISC 協(xié)議棧,該協(xié)議棧就是實(shí)現(xiàn)了TCP/IP 協(xié)議棧的應(yīng)用層,傳輸層和網(wǎng)絡(luò)層的功能;網(wǎng)絡(luò)接口層由網(wǎng)卡實(shí)現(xiàn),所以CBISC 協(xié)議棧和網(wǎng)卡構(gòu)建了網(wǎng)絡(luò)通信的核心骨架。因此,無論哪一款以太網(wǎng)產(chǎn)品,都必須符合TCP/IP 體系結(jié)構(gòu),才能實(shí)現(xiàn)網(wǎng)絡(luò)通信。

注意:路由器和交換機(jī)等相關(guān)網(wǎng)絡(luò)設(shè)備只實(shí)現(xiàn)網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層的功能。

TCP/IP 協(xié)議棧的封包和拆包

TCP/IP 協(xié)議棧的封包和拆包也是一個(gè)非常重要的知識(shí),如以太網(wǎng)設(shè)備發(fā)送數(shù)據(jù)和接收數(shù)據(jù)的處理流程是怎么樣的?這個(gè)問題涉及到TCP/IP 協(xié)議棧對(duì)數(shù)據(jù)處理的流程,該流程稱之為“封包”和“拆包”?!胺獍笔菍?duì)發(fā)送數(shù)據(jù)處理的流程,而“拆包”是對(duì)接收數(shù)據(jù)處理的流程,如下圖所示。

在這里插入圖片描述

上圖中,發(fā)送端發(fā)送的數(shù)據(jù)自頂向下依次傳遞。各層協(xié)議依次在數(shù)據(jù)前添加本層的首部且設(shè)置本層首部的信息,最終將處理后的MAC幀遞交給物理層轉(zhuǎn)成光電模擬信號(hào)發(fā)送至網(wǎng)絡(luò),這個(gè)流程稱之為封包流程。

在這里插入圖片描述

上圖中,當(dāng)幀數(shù)據(jù)到達(dá)目的主機(jī)時(shí),將沿著協(xié)議棧自底向上依次傳遞。各層協(xié)議依次根據(jù)幀中本層負(fù)責(zé)的頭部信息以獲取所需數(shù)據(jù),最終將處理后的幀交給應(yīng)用層,這個(gè)流程稱之為拆包的過程。

lwIP 簡介

lwIP 是Light Weight(輕型)IP 協(xié)議,有無操作系統(tǒng)的支持都可以運(yùn)行。lwIP 實(shí)現(xiàn)的重點(diǎn)是在保持TCP/IP 協(xié)議主要功能的基礎(chǔ)上減少對(duì)RAM 的占用,它只需十幾 KB 的 RAM 和 40K左右的ROM 就可以運(yùn)行,這使lwIP 協(xié)議棧適合在低端的嵌入式系統(tǒng)中使用。lwIP 的設(shè)計(jì)理念下,既可以無操作系統(tǒng)使用,也可以帶操作系統(tǒng)使用;既可以支持多線程,也可以無線程。它可以運(yùn)行在8 位以及32 位的微處理器上,同時(shí)支持大端、小端系統(tǒng)。

?lwIP 特性參數(shù)

lwIP 的各項(xiàng)特性,如下表所示:

在這里插入圖片描述

?lwIP 與TCP/IP 體系結(jié)構(gòu)的對(duì)應(yīng)關(guān)系

在這里插入圖片描述

從上圖可以看出,lwIP 軟件庫只實(shí)現(xiàn)了TCP/IP 體系結(jié)構(gòu)的應(yīng)用層、傳輸層和網(wǎng)絡(luò)層的功能,但網(wǎng)絡(luò)接口層不能使用軟件的方式實(shí)現(xiàn),因?yàn)榫W(wǎng)絡(luò)接口層是把數(shù)據(jù)包轉(zhuǎn)成光電模擬信號(hào),并轉(zhuǎn)發(fā)至網(wǎng)絡(luò),所以網(wǎng)絡(luò)接口層只能由硬件來實(shí)現(xiàn)。

lwIP 源碼下載

lwIP 的開發(fā)托管在Savannah 上,Savannah 是軟件開發(fā)、維護(hù)和分發(fā)。每個(gè)人都可以通過使用Savannah 的界面、Git 和郵件列表下載lwIP 源碼包。lwIP 的項(xiàng)目主頁:http://savannah.nongnu.org/projects/lwip/。在這個(gè)主頁上,讀者需要關(guān)注“project homepage”和“download area”這兩個(gè)鏈接地址。

打開lwIP 項(xiàng)目主頁之后,往下找到“Quick Overview”選項(xiàng),如下圖所示:

在這里插入圖片描述

點(diǎn)擊上圖中Project Homepage 鏈接地址,讀者可以看到官方對(duì)于lwIP 的說明文檔,包括lwIP 更新日記、常見誤解、已發(fā)現(xiàn)的BUG、多線程、優(yōu)化提示和相關(guān)文件中的函數(shù)描述等內(nèi)容。

點(diǎn)擊上圖中的Domnload Area 鏈接地址,讀者可以看到lwIP 源碼和contrib 包的下載網(wǎng)頁,如下圖所示那樣。由于lwIP 版本居多,因此本教程選擇目前最新的lwIP 版本(2.1.3)。下圖中的contrib 包是提供用戶lwIP 移植文件和lwIP 相關(guān)demo 例程。注:contrib 包不屬于lwIP內(nèi)核的一部分,它只是為我們提供移植文件和學(xué)習(xí)實(shí)例。

在這里插入圖片描述

點(diǎn)擊上圖中的lwip-2.1.3.zip 和contrib-2.1.0.zip 鏈接,下載完成之后在本地上可以看到這兩個(gè)壓縮包。

lwIP 文件說明

根據(jù)上一個(gè)小節(jié)的操作,我們已經(jīng)下載了lwip-2.1.3.zip 和contrib-2.1.0.zip 這兩個(gè)壓縮包。

接下來,筆者帶大家認(rèn)識(shí)一下lwip-2.1.3 和contrib-2.1.0 文件夾內(nèi)的文件。

? lwIP 源碼包文件說明

打開lwip-2.1.3 文件夾,如下圖所示:

在這里插入圖片描述

上圖中,這個(gè)文件夾包含的文件和文件夾非常多,這些文件與文件夾描述如下表所示。

在這里插入圖片描述

上表中,src 文件夾是lwIP 源碼包中最重要的,它是lwIP 的內(nèi)核文件,也是我們移植到工程中的重要文件。接下來,筆者重點(diǎn)講解src 文件夾下的文件與文件夾,如下表所示。

在這里插入圖片描述

  • api 文件夾下的文件是實(shí)現(xiàn)應(yīng)用層與傳輸層遞交數(shù)據(jù)的接口實(shí)現(xiàn);
  • apps 文件夾下的文件實(shí)現(xiàn)了多種應(yīng)用層協(xié)議;
  • core 文件夾下的文件是構(gòu)建lwIP 內(nèi)核的源文件,對(duì)應(yīng)了TCP/IP 體系架構(gòu)的傳輸層、網(wǎng)絡(luò)層;include 文件夾包含了lwIP 軟件庫的全部頭文件;
  • netif 文件夾下的文件實(shí)現(xiàn)了網(wǎng)絡(luò)層與數(shù)據(jù)鏈路層交互接口,以及管理不同類型的網(wǎng)卡。

打開core 文件夾,我們會(huì)發(fā)現(xiàn),lwIP 是由一系列的模塊組合而成,這些模塊包括:
TCP/IP 協(xié)議棧的各種協(xié)議、內(nèi)存管理、數(shù)據(jù)包管理、網(wǎng)卡管理、網(wǎng)卡接口、基礎(chǔ)功能和API接口模塊等,每一個(gè)模塊是由幾個(gè)源文件和一個(gè)頭文件集合,這些頭文件全部放在include 文件夾下,而源文件都是放在core 文件夾下。這些模塊描述如下:

在這里插入圖片描述
在這里插入圖片描述

? lwIP 的contrib 包文件說明

contrib 包提供了lwIP 移植文件和lwIP 相關(guān)demo(應(yīng)用實(shí)例),如下圖所示:

在這里插入圖片描述

上圖中,ports 文件夾提供了lwIP 基于FreeRTOS 操作系統(tǒng)的移植文件;examples 和apps文件夾提供讀者學(xué)習(xí)lwIP 的應(yīng)用實(shí)例。至此,lwIP 源碼庫和contrib 包介紹完畢。

MAC 內(nèi)核簡介

STM32 內(nèi)置了一個(gè)MAC 內(nèi)核,它實(shí)現(xiàn)了TCP/IP 體系架構(gòu)的數(shù)據(jù)鏈路層功能。STM32 內(nèi)置以太網(wǎng)架構(gòu)如下所示:

在這里插入圖片描述

上圖,綠色框框的RX FIFO 和TX FIFO 都是2KB 的物理存儲(chǔ)器,它們分別存儲(chǔ)網(wǎng)絡(luò)層遞交的以太網(wǎng)數(shù)據(jù)和接收的以太網(wǎng)數(shù)據(jù)。以太網(wǎng)DMA 是網(wǎng)絡(luò)層和數(shù)據(jù)鏈路層的中間橋梁,是利用存儲(chǔ)器到存儲(chǔ)器方式傳輸;

紅色框框的內(nèi)容可分為兩個(gè)部分講解,RMII 與MII是 MAC內(nèi)核(數(shù)據(jù)鏈路層)與 PHY 芯片(物理層)的數(shù)據(jù)交互通道,用來傳輸以太網(wǎng)數(shù)據(jù)。

MDC 和MDIO 是MAC 內(nèi)核對(duì)PHY 芯片的管理和配置,是站管理接口(SMI)所需的通信引腳。站管理接口(SMI)允許應(yīng)用程序通過2 條線:時(shí)鐘(MDC)和數(shù)據(jù)線(MDIO)訪問任意PHY 寄存器。該接口支持訪問多達(dá)32 個(gè)PHY。應(yīng)用程序可以從32 個(gè)PHY 中選擇一個(gè)PHY,然后從任意PHY 包含的32 個(gè)寄存器中選擇一個(gè)寄存器,發(fā)送控制數(shù)據(jù)或接收狀態(tài)信息。

任意給定時(shí)間內(nèi)只能對(duì)一個(gè)PHY 中的一個(gè)寄存器進(jìn)行尋址。在MAC 對(duì)PHY 進(jìn)行讀寫操作的時(shí)候,應(yīng)用程序不能修改MII 的地址寄存器和MII 的數(shù)據(jù)寄存器。在此期間對(duì)MII 地址寄存器或MII 數(shù)據(jù)寄存器執(zhí)行的寫操作將會(huì)被忽略。例如關(guān)于SMI 接口的詳細(xì)介紹大家可以參考STM32F4xx 中文參考手冊(cè)的824 頁。

? 介質(zhì)獨(dú)立接口:MII

MII 用于MAC 層與PHY 層進(jìn)行數(shù)據(jù)傳輸。MCU 通過MII 與PHY 層芯片的連接圖如下。

圖1.3.2 MCU 與PHY 層芯片連接

從圖中可以看出,MII 介質(zhì)接口使用的引腳數(shù)量是非常多的,這也反映出引腳緊缺的MCU 不適合使用 MII 介質(zhì)接口來實(shí)現(xiàn)以太網(wǎng)數(shù)據(jù)傳輸,MII 接口引腳的作用如下所示。

?MII_TX_CLK:連續(xù)時(shí)鐘信號(hào)。該信號(hào)提供進(jìn)行TX 數(shù)據(jù)傳輸時(shí)的參考時(shí)序。標(biāo)稱頻率為:速率為10 Mbit/s 時(shí)為2.5 MHz;速率為100 Mbit/s 時(shí)為25 MHz。
?MII_RX_CLK:連續(xù)時(shí)鐘信號(hào)。該信號(hào)提供進(jìn)行RX 數(shù)據(jù)傳輸時(shí)的參考時(shí)序。標(biāo)稱頻率為:速率為10 Mbit/s 時(shí)為2.5 MHz;速率為100 Mbit/s 時(shí)為25 MHz。
?MII_TX_EN:發(fā)送使能信號(hào)。
?MII_TXD[3:0]:數(shù)據(jù)發(fā)送信號(hào)。該信號(hào)是4 個(gè)一組的數(shù)據(jù)信號(hào)。
?MII_CRS:載波偵聽信號(hào)。
?MII_COL:沖突檢測(cè)信號(hào)。
?MII_RXD[3:0]:數(shù)據(jù)接收信號(hào)。該信號(hào)是4 個(gè)一組的數(shù)據(jù)信號(hào)
?MII_RX_DV:接收數(shù)據(jù)有效信號(hào)。
?MII_RX_ER:接收錯(cuò)誤信號(hào)。該信號(hào)必須保持一個(gè)或多個(gè)周期(MII_RX_CLK),從而向MAC 子層指示在幀的某處檢測(cè)到錯(cuò)誤。

? 精簡介質(zhì)獨(dú)立接口:RMII

精簡介質(zhì)獨(dú)立接口(RMII)規(guī)范降低10/100Mbit/s 下微控制器以太網(wǎng)外設(shè)與外部PHY 間的引腳數(shù)。根據(jù)IEEE 802.3u 標(biāo)準(zhǔn),MII 包括16 個(gè)數(shù)據(jù)和控制信號(hào)的引腳,而RMII 規(guī)范將引腳數(shù)減少為7 個(gè)。

MCU 通過RMII 接口與PHY 層芯片的連接圖如下圖所示。因?yàn)镽MII 相比MII,其發(fā)送和接收都少了兩條線。因此要達(dá)到10Mbit/s 的速度,其時(shí)鐘頻率應(yīng)為5MHZ,同理要達(dá)到100Mbit/s 的速度其時(shí)鐘頻率應(yīng)為50MHz。正點(diǎn)原子開發(fā)板就是采用此接口連接PHY 芯片。

在這里插入圖片描述

可以看出,REF_CLK 引腳需要提供50MHz 時(shí)鐘頻率,它分別提供MAC 內(nèi)核和PHY 芯片,確保它們時(shí)鐘同步。

PHY 芯片介紹

PHY 芯片在TCP/IP 體系架構(gòu)中扮演著物理層的角色,它把數(shù)據(jù)轉(zhuǎn)換成光電模擬信號(hào)傳輸至網(wǎng)絡(luò)當(dāng)中。本小節(jié)為讀者介紹正點(diǎn)原子常用的PHY 芯片,它們分別為LAN8720A 和YT8512C,這兩款PHY 芯片都是支持10/100BASE-T 百兆以太網(wǎng)傳輸速率,為此筆者分兩個(gè)小節(jié)來講解這兩款以太網(wǎng)芯片的知識(shí)。

YT8512C 簡介

YT8512C 是低功耗單端口10/100Mbps 以太網(wǎng)PHY 芯片。它通過兩條標(biāo)準(zhǔn)雙絞線電纜收發(fā)器發(fā)送和接收數(shù)據(jù)所需的所有物理層功能。另外,YT8512C 通過標(biāo)準(zhǔn)MII 和RMII 接口連接到MAC 層。YT8512C 功能結(jié)構(gòu)圖如下圖所示:

在這里插入圖片描述
上圖是YT8512C 芯片的內(nèi)部總架構(gòu)示意圖,從圖中我們大概可以看出,它通過LED0\LED1 引腳的電平來設(shè)置PHY 地址,由XTAL,Clock 引腳提供PHY 內(nèi)部時(shí)鐘,同時(shí)TXP\TXN\RXP\RXN 引腳連接到RJ45(網(wǎng)口)。

? PHY 地址設(shè)置

MAC 層通過SMI 總線對(duì)PHY 芯片進(jìn)行讀寫操作,SMI 可以控制32 個(gè)PHY 芯片,通過PHY 地址的不同來配置對(duì)應(yīng)的PHY 芯片。YT8512C 芯片的PHY 地址設(shè)置如下表所示:

在這里插入圖片描述

上表中,我們可通過YT8512C 芯片的LED0/PHYADD0 和LED1/PHYADD1 引腳電平來設(shè)置PHY 地址。由于正點(diǎn)原子板載的PHY 芯片是把這兩個(gè)引腳拉低,所以它的PHY 地址為0x00。打開HAL 配置文件或者打開PHY 配置文件,我們?cè)诖宋募屡渲肞HY 地址,這些文
件如下表所示:

在這里插入圖片描述

可以看到,探索者和DMF407 開發(fā)板的PHY 地址在stm32f4xx_hal_conf.h 文件下設(shè)置的,而阿波羅和北極星開發(fā)板的PHY 地址在ethernet_chip.h 文件下設(shè)置的。因?yàn)樘剿髡吲cDMF407 使用的HAL 庫版本比阿波羅與北極星開發(fā)板所使用的HAL 庫版本舊,所以它們的移植流程存在巨大的差異。這里筆者暫且不講解這部分的內(nèi)容。

? YT8521C 的RMII 接口介紹

YT8521C 的RMII 接口提供了兩種RMII 模式,這兩種模式分別為:
?RMII1 模式:這個(gè)模式下YT8521C 的TXC 引腳不會(huì)輸出50MHz 時(shí)鐘。該模式的連接示意圖如下圖1.4.1.2 所示。
?RMII2 模式:這個(gè)模式下YT8521C 的TXC 引腳會(huì)輸出50MHz 時(shí)鐘。該模式的連接示意圖如下圖1.4.1.3 所示。

在這里插入圖片描述

對(duì)于RMII 接口而言,外部必須提供50MHz 的時(shí)鐘驅(qū)動(dòng)PHY 與MAC 內(nèi)核,該時(shí)鐘為了使PHY 芯片與MAC 內(nèi)核保持時(shí)鐘同步操作,它可以來自PHY 芯片、有源晶振或者STM32的MCO 引腳。如果我們的電路采用RMII1 模式的話,那么PHY 芯片由25MHz 晶振經(jīng)過內(nèi)部PLL 倍頻達(dá)到50MHz,但是MAC 內(nèi)核沒有被提供50MHz 與PHY 芯片保持時(shí)鐘同步,所以我們必須在此基礎(chǔ)上使用MCO 或外部接入50MHz 晶振提供時(shí)鐘給MAC 內(nèi)核,以保持時(shí)鐘同步。

在這里插入圖片描述

如果電路使用上圖模式連接的話,那么PHY 芯片經(jīng)過外接晶振25MHz 和內(nèi)部PLL 倍頻操作,最終PHY 芯片內(nèi)部的時(shí)鐘為50MHz。接著PHY 芯片外圍引腳TXC 會(huì)輸出50MHz 時(shí)鐘頻率,該時(shí)鐘頻率可輸入到MAC 內(nèi)核保持時(shí)鐘同步,這樣我們無需外接晶振或者M(jìn)CO 提供MAC 內(nèi)核時(shí)鐘。

注:RMII1 模式和RMII2 模式的選擇是由YT8521C 的RX_DV(8)和RXD3(12)引腳決定,具體如何選擇,請(qǐng)讀者參考“YT8512C.PDF”手冊(cè)的17 到18 頁的內(nèi)容。

? YT8521C 的寄存器介紹

PHY 是由IEEE 802.3 定義的,一般通過SMI 對(duì)PHY 進(jìn)行管理和控制,也就是讀寫PHY內(nèi)部寄存器。PHY 寄存器的地址空間為5 位,可以定義0 ~ 31 共32 個(gè)寄存器,但是,隨著PHY 芯片功能的增加,很多PHY 芯片采用分頁技術(shù)來擴(kuò)展地址空間,定義更多的寄存器,在
這里筆者不討論這種情況,IEEE 802.3 定義了0~15 這16 個(gè)寄存器的功能,而16~31 寄存器由芯片制造商自由定義的。

在YT8521C 中有很多寄存器,這里筆者只介紹幾個(gè)用到的寄存器(包括寄存器地址,此
處使用十進(jìn)制表示):BCR(0),BSR(1),PHY 特殊功能寄存器(17)這三個(gè)寄存器。首先我們來看一下BCR(0)寄存器,BCR 寄存器各位介紹如下表所示。

在這里插入圖片描述
在這里插入圖片描述

我們?cè)O(shè)置以太網(wǎng)速率和雙工,其實(shí)就是配置PHY 芯片的BCR 寄存器。在HAL 配置文件
或者ethernet_chip.h 文件定義了BCR 和BSR 寄存器,代碼如下:
探索者、DMF407 開發(fā)板(HAL 配置文件下):

#define PHY_BCR ((uint16_t)0x0000)
#define PHY_BSR ((uint16_t)0x0001)

阿波羅、北極星開發(fā)板(PHY 配置文件下):

#define ETH_CHIP_BCR ((uint16_t)0x0000U)
#define ETH_CHIP_BSR ((uint16_t)0x0001U)

由于探索者及DMF407 開發(fā)板的例程是使用V1.26 版本的HAL 庫,所以這兩個(gè)寄存器并不需要讀者來操作,原因就是我們調(diào)用HAL_ETH_Init 函數(shù)以后系統(tǒng)就會(huì)根據(jù)我們輸入的參數(shù)配置YT8521C 的相應(yīng)寄存器。但是,阿波羅及北極星開發(fā)板的例程使用目前最新的HAL 版本,它要求讀者手動(dòng)操作BCR 寄存器,例如自動(dòng)協(xié)商、軟復(fù)位等操作。
BSR 寄存器各個(gè)位介紹如下表所示:

在這里插入圖片描述
在這里插入圖片描述

BSR 寄存器為YT8521C 的狀態(tài)寄存器,通過讀取該寄存器的值我們可以得到當(dāng)前的連接
速度、雙工狀態(tài)和連接狀態(tài)等信息。

接下來,筆者介紹的是YT8521C 特殊功能寄存器,此寄存器的各位如下表所示:

在這里插入圖片描述
在這里插入圖片描述

在特殊功能寄存器中我們關(guān)心的是bit13~bit15 這三位,因?yàn)橄到y(tǒng)通過讀取這3 位的值來設(shè)置BCR 寄存器的bit8 和bit13。由于特殊功能寄存器不屬于IEEE802.3 規(guī)定的前16 個(gè)寄存器,所以每個(gè)廠家的可能不同,這個(gè)需要用戶根據(jù)自己實(shí)際使用的PHY 芯片去修改。
ST 提供的以太網(wǎng)驅(qū)動(dòng)文件有三個(gè)配置項(xiàng)值得讀者注意的,它們分別為PHY_SR、PHY_SPEED_STATUS 和PHY_DUPLEX_STATUS 配置項(xiàng),這些配置項(xiàng)用來描述PHY 特殊功能寄存器,根據(jù)該寄存器的值設(shè)置BCR 寄存器的第8 位和第13 位,即雙工和網(wǎng)速。
探索者、DMF407 開發(fā)板:

/* 網(wǎng)卡PHY地址設(shè)置*/
#define ETHERNET_PHY_ADDRESS 0x00
/* 選擇PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C
#if(PHY_TYPE == LAN8720)
#define PHY_SR ((uint16_t)0x1F) /* PHY狀態(tài)寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0004) /* PHY速度狀態(tài)*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0010) /* PHY雙工狀態(tài)*/
#elif(PHY_TYPE == SR8201F)
#define PHY_SR ((uint16_t)0x00) /* PHY狀態(tài)寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x2020) /* PHY速度狀態(tài)*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0100) /* PHY雙工狀態(tài)*/
#elif(PHY_TYPE == YT8512C)
#define PHY_SR ((uint16_t)0x11) /* PHY狀態(tài)寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x4010) /* PHY速度狀態(tài)*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x2000) /* PHY雙工狀態(tài)*/
#elif(PHY_TYPE == RTL8201)
#define PHY_SR ((uint16_t)0x10) /* PHY狀態(tài)寄存器地址*/
#define PHY_SPEED_STATUS ((uint16_t)0x0022) /* PHY速度狀態(tài)*/
#define PHY_DUPLEX_STATUS ((uint16_t)0x0004) /* PHY雙工狀態(tài)*/

阿波羅、北極星開發(fā)板:

/* PHY地址*/
#define ETH_CHIP_ADDR ((uint16_t)0x0000U)
/* 選擇PHY芯片*/
#define LAN8720 0
#define SR8201F 1
#define YT8512C 2
#define RTL8201 3
#define PHY_TYPE YT8512C
#if(PHY_TYPE == LAN8720)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x1F)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0004)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0010)
#elif(PHY_TYPE == SR8201F)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x00)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x2020)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0100)
#elif(PHY_TYPE == YT8512C)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x11)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x4010)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x2000)
#elif(PHY_TYPE == RTL8201)
#define ETH_CHIP_PHYSCSR ((uint16_t)0x10)
#define ETH_CHIP_SPEED_STATUS ((uint16_t)0x0022)
#define ETH_CHIP_DUPLEX_STATUS ((uint16_t)0x0004)
#endif /* PHY_TYPE */

筆者已經(jīng)適配了多款PHY 芯片,根據(jù)PHY_TYPE 配置項(xiàng)來選擇PHY_SR、PHY_SPEED_
STATUS 和PHY_DUPLEX_STATUS 配置項(xiàng)的數(shù)值。

LAN8720A 簡介

LAN8720A 是一款低功耗的10/100M 以太網(wǎng)PHY 層芯片,它通過RMII/MII 介質(zhì)接口與
以太網(wǎng)MAC 層通信,內(nèi)置10-BASE-T/100BASE-TX 全雙工傳輸模塊,支持10Mbps 和
100Mbps。LAN8720A 主要特點(diǎn)如下:

?高性能的10/100M 以太網(wǎng)傳輸模塊。
?支持RMII 接口以減少引腳數(shù)。
?支持全雙工和半雙工模式。
?兩個(gè)狀態(tài)LED 輸出。
?可以使用25M 晶振以降低成本。
?支持自協(xié)商模式。
?支持HP Auto-MDIX 自動(dòng)翻轉(zhuǎn)功能。
?支持SMI 串行管理接口。
?支持MAC 接口。

LAN8720A 功能框圖如下圖所示:

在這里插入圖片描述

? LAN8720A 中斷管理
LAN8720A 的器件管理接口支持非IEEE 802.3 規(guī)范的中斷功能。當(dāng)一個(gè)中斷事件發(fā)生并且
相應(yīng)事件的中斷位使能,LAN8720A 就會(huì)在nINT(14 腳)產(chǎn)生一個(gè)低電平有效的中斷信號(hào)。LAN8720A 的中斷系統(tǒng)提供兩種中斷模式:主中斷模式和復(fù)用中斷模式。主中斷模式是默認(rèn)中斷模式,LAN8720A 上電或復(fù)位后就工作在主中斷模式,當(dāng)模式控制/狀態(tài)寄存器(十進(jìn)制地址為17)的ALTINT 為0 是LAN8720 工作在主模式,當(dāng)ALTINT 為1 時(shí)工作在復(fù)用中斷模式。正點(diǎn)原子的STM32 系列開發(fā)板并未用到中斷功能,關(guān)于中斷的具體用法可以參考LAN8720A 數(shù)據(jù)手冊(cè)的29,30 頁。

? PHY 地址設(shè)置
MAC 層通過SMI 總線對(duì)PHY 進(jìn)行讀寫操作,SMI 可以控制32 個(gè)PHY 芯片,通過不同
的PHY 芯片地址來對(duì)不同的PHY 操作。LAN8720A 通過設(shè)置RXER/PHYAD0 引腳來設(shè)置其PHY 地址,默認(rèn)情況下為0,其地址設(shè)置如下表所示。正點(diǎn)原子的STM32 系列開發(fā)板使用的是默認(rèn)地址,也就是0X00。

在這里插入圖片描述

? nINT/REFCLKO 配置

nINTSEL 引腳(2 號(hào)引腳)用于設(shè)置nINT/REFCLKO 引腳(14 號(hào)引腳)的功能。
nINTSEL 配置如下表所示。

在這里插入圖片描述

當(dāng)工作在REF_CLK In 模式時(shí),50MHz 的外部時(shí)鐘信號(hào)應(yīng)接到LAN8720 的XTAL1/CKIN
引腳(5 號(hào)引腳)和STM32 的RMII_REF_CLK(PA1)引腳上,如下圖所示。

在這里插入圖片描述

為了降低成本,LAN8720A 可以從外部的25MHz 的晶振中產(chǎn)生REF_CLK 時(shí)鐘。到要使
用此功能時(shí)應(yīng)工作在REF_CLK Out 模式。當(dāng)工作在REF_CLO Out 模式時(shí)REF_CLK 的時(shí)鐘源如下圖所示。

在這里插入圖片描述

? LAN8720A 內(nèi)部寄存器
PHY 是由IEEE 802.3 定義的,一般通過SMI 對(duì)PHY 進(jìn)行管理和控制,也就是讀寫PHY 內(nèi)
部寄存器。PHY 寄存器的地址空間為5 位,可以定義0~ 31 共32 個(gè)寄存器,但是隨之PHY 芯片功能的增加,很多PHY 芯片采用分頁技術(shù)來擴(kuò)展地址空間,定義更多的寄存器,在這里我們不討論這種情況。IEEE 802.3 定義了0~ 15 這16 個(gè)寄存器的功能,16~31 寄存器由芯片制造商自由定義。在LAN8720A 有很多寄存器,筆者重點(diǎn)講解BCR(0),BSR(1),PHY 特殊功能寄存器(31)這三個(gè)寄存器,前面兩個(gè)寄存器筆者已經(jīng)在1.6.1 小節(jié)講解了,這里筆者無需重復(fù)講解。接下來介紹的是LAN8720A 特殊功能寄存器,此寄存器的各個(gè)位如下表所示:

在這里插入圖片描述

在特殊功能寄存器中我們關(guān)心的是bit2~bit4 這三位,因?yàn)橄到y(tǒng)通過讀取這3 位的值來設(shè)
置BCR 寄存器的bit8 和bit13。

以太網(wǎng)接入MCU 方案

以太網(wǎng)接入方案一般分為兩種,它們分別為全硬件TCP/IP 協(xié)議棧軟件TCP/IP 協(xié)議棧
其中,軟件TCP/IP 協(xié)議棧用途非常廣泛,如電腦、交換機(jī)等網(wǎng)絡(luò)設(shè)備,而全硬件TCP/IP 協(xié)議棧是近年來比較新型的以太網(wǎng)接入方案。下面筆者分別來講解這兩種接入方案的差異和優(yōu)缺點(diǎn)。

軟件TCP/IP 協(xié)議棧以太網(wǎng)接入方案

這種方案由lwIP + MAC 內(nèi)核 + PHY 層芯片實(shí)現(xiàn)以太網(wǎng)物理連接,如正點(diǎn)原子的探索者、
阿波羅、北極星以及電機(jī)開發(fā)板都是采用這類型的以太網(wǎng)接入方案,該方案的連接示意圖如下圖所示:

在這里插入圖片描述

上圖中,MCU 要求內(nèi)置 MAC 內(nèi)核,該內(nèi)核相當(dāng)TCP/IP 體系結(jié)構(gòu)的數(shù)據(jù)鏈路層,而lwIP軟件庫用來實(shí)現(xiàn)TCP/IP 體系結(jié)構(gòu)的應(yīng)用層、傳輸層和網(wǎng)絡(luò)層,同時(shí),板載PHY 層芯片用來實(shí)現(xiàn)TCP/IP 體系結(jié)構(gòu)的物理層。因此,lwIP、MAC 內(nèi)核和PHY 層芯片構(gòu)建了網(wǎng)絡(luò)通信核心骨架。

優(yōu)點(diǎn):
?移植性:可在不同平臺(tái)、不同編譯環(huán)境的程序代碼經(jīng)過修改轉(zhuǎn)移到自己的系統(tǒng)中運(yùn)行。
?可造性:可在TCP/IP協(xié)議棧的基礎(chǔ)上添加和刪除相關(guān)功能。
?可擴(kuò)展性:可擴(kuò)展到其他領(lǐng)域的應(yīng)用及開發(fā)。

缺點(diǎn):
?內(nèi)存方面分析:傳統(tǒng)的TCP/IP 方案是移植一個(gè)lwIP 的TCP/IP 協(xié)議(RAM 50K+,
ROM 80K+),造成主控可用內(nèi)存減小。
?從代碼量分析:移植lwIP可能需要的代碼量超過40KB,對(duì)于有些主控芯片內(nèi)存匱乏
來說無疑是一個(gè)嚴(yán)重的問題。
?從運(yùn)行性能方面分析:由于軟件TCP/IP協(xié)議棧方案在通信時(shí)候是不斷地訪問中斷機(jī)
制,造成線程無法運(yùn)行,如果多線程運(yùn)行,會(huì)使MCU的工作效率大大降低。
?從安全性方面分析:軟件協(xié)議棧會(huì)很容易遭受網(wǎng)絡(luò)攻擊,造成單片機(jī)癱瘓。

硬件TCP/IP 協(xié)議棧以太網(wǎng)接入方案

所謂全硬件TCP/IP 協(xié)議棧是將傳統(tǒng)的軟件協(xié)議TCP/IP 協(xié)議棧用硬件化的邏輯門電路來實(shí)
現(xiàn)。芯片內(nèi)部完成TCP、UDP、ICMP 等多種網(wǎng)絡(luò)協(xié)議,并且實(shí)現(xiàn)了物理層以太網(wǎng)控制
(MAC+PHY)、內(nèi)存管理等功能,完成了一整套硬件化的以太網(wǎng)解決方案。
該方案的連接示意圖如下圖所示:

在這里插入圖片描述

上圖中,MCU 通過串口或者SPI 進(jìn)行網(wǎng)絡(luò)通訊,無需移植協(xié)議庫,極大地減少程序的代
碼量,甚至彌補(bǔ)了網(wǎng)絡(luò)協(xié)議安全性不足的短板。硬件TCP/IP 協(xié)議棧的優(yōu)缺點(diǎn),如下所示:

優(yōu)點(diǎn):
?從代碼量方面來看:相比于傳統(tǒng)的接入已經(jīng)大大減少了代碼量。
?從運(yùn)行方面來看:極大的減少了中斷次數(shù),讓單片機(jī)更好的完成其他線程的工作。
?從安全性方面來看:硬件化的邏輯門電路來處理TCP/IP協(xié)議是不可被攻擊的,也就
是說網(wǎng)絡(luò)攻擊和病毒對(duì)它無效,這也充分彌補(bǔ)了網(wǎng)絡(luò)協(xié)議安全性不足的短板。

缺點(diǎn):
?從可擴(kuò)展性來看:雖然該芯片內(nèi)部使用邏輯門電路來實(shí)現(xiàn)應(yīng)用層和物理層協(xié)議,但是
它具有功能局限性,例如給TCP/IP協(xié)議棧添加一個(gè)協(xié)議,這樣它無法快速添加了。
?從收發(fā)速率來看:全硬件TCP/IP協(xié)議棧芯片都是采用并口、SPI以及IIC等通訊接
口來收發(fā)數(shù)據(jù),這些數(shù)據(jù)會(huì)受通信接口的速率而影響。
總的來說:全硬件TCP / IP 協(xié)議棧簡化傳統(tǒng)的軟件TCP / IP 協(xié)議棧,卸載了MCU 用于處
理TCP / IP 這部分的線程,節(jié)約MCU 內(nèi)部ROM 等硬件資源,工程師只需進(jìn)行簡單的套接字
編程和少量的寄存器操作即可方便地進(jìn)行嵌入式以太網(wǎng)上層應(yīng)用開發(fā),減少產(chǎn)品開發(fā)周期,降低開發(fā)成本。

lwIP 無操作系統(tǒng)移植

lwIP 帶操作系統(tǒng)移植

ARP 協(xié)議

ARP 協(xié)議的簡介

ARP 全稱為Address Resolution Protocol(地址解析協(xié)議),是根據(jù)IP 地址獲取物理地址的一個(gè)TCP/IP 協(xié)議。

主機(jī)發(fā)送信息時(shí)將包含目標(biāo)IP 地址的ARP 請(qǐng)求廣播到局域網(wǎng)絡(luò)上的所有主機(jī),并接收返回消息,以此確定目標(biāo)的物理地址;收到返回消息后將該IP 地址和物理地址存入本機(jī)ARP 緩存中并保留一定時(shí)間,下次請(qǐng)求時(shí)直接查詢ARP 緩存以節(jié)約資源。地址解析協(xié)議是建立在網(wǎng)絡(luò)中各個(gè)主機(jī)互相信任的基礎(chǔ)上的,局域網(wǎng)絡(luò)上的主機(jī)可以自主發(fā)送ARP 應(yīng)答消息,其他主機(jī)收到應(yīng)答報(bào)文時(shí)不會(huì)檢測(cè)該報(bào)文的真實(shí)性就會(huì)將其記入本機(jī)ARP 緩存;總的來說,ARP 協(xié)議是透過目標(biāo)設(shè)備的IP 地址,查詢目標(biāo)設(shè)備的MAC 地址,以保證通信的順利進(jìn)行。

ARP 協(xié)議的工作流程

假設(shè)由兩臺(tái)主機(jī),分別為主機(jī)A(192.168.0.10)與主機(jī)B(192.168.0.11),它們兩個(gè)都是
同一網(wǎng)段的,如果主機(jī)A 向主機(jī)B 發(fā)送信息或者數(shù)據(jù),ARP 的地址解析過程有以下幾個(gè)步驟:

  • ①主機(jī)A 首先查自己的ARP 表是否有包含主機(jī)B 的信息,例如主機(jī)B 的MAC 地址,如
    果主機(jī)A 的ARP 表包含主機(jī)B 的MAC 地址,則主機(jī)A 直接利用ARP 表的主機(jī)B 的MAC 地址對(duì)IP 數(shù)據(jù)包進(jìn)行封裝并把數(shù)據(jù)包發(fā)給主機(jī)B。

  • ②如果主機(jī)A 的ARP 表沒有包含主機(jī)B 的MAC 地址或者沒有找到主機(jī)B 的MAC 地址,
    則主機(jī)A 就把數(shù)據(jù)包緩存起來,然后以廣播的方式發(fā)送一個(gè)ARP 包的請(qǐng)求報(bào)文,該ARP 包的內(nèi)容包含主機(jī)A 的IP 地址、MAC 地址、主機(jī)B 的IP 地址和主機(jī)B 的全0 的MAC 地址,由于主機(jī)A 發(fā)送ARP 包是使用廣播形式,那么同一網(wǎng)段的主機(jī)都可以收到該ARP 包,主機(jī)B接收到這個(gè)ARP 包會(huì)進(jìn)行處理。

  • ③主機(jī)B 接收到主機(jī)A 的ARP 包之后,主機(jī)B 會(huì)對(duì)這個(gè)ARP 解析并比較自己的IP 地址
    和ARP 包的目的IP 地址是否相同,如果相同,則主機(jī)B 將ARP 請(qǐng)求報(bào)文中的發(fā)送端(即主機(jī)A)的IP 地址和MAC 地址存入自己的ARP 表中。之后以單播方式發(fā)送ARP 響應(yīng)報(bào)文給主機(jī)A,其中包含了自己的MAC 地址。

  • ④當(dāng)主機(jī)A 收到了主機(jī)B 的ARP 包也是同樣的處理,首先比較ARP 包的IP 地址是否和
    自己的IP 地址相同,如果IP 地址相同,則把ARP 包的信息存入自己的ARP 表中,最后對(duì)IP數(shù)據(jù)包進(jìn)行封裝并把數(shù)據(jù)包發(fā)給主機(jī)B。

從上述步驟的內(nèi)容,可得到ARP 包的流程圖,如下圖所示:

在這里插入圖片描述

可以看到,主機(jī)A 發(fā)送數(shù)據(jù)之前先判斷主機(jī)A 的ARP 緩存表是否包含主機(jī)B 的MAC 地
址,若主機(jī)A 的ARP 緩存表沒有主機(jī)B 的MAC 地址,則主機(jī)A 把要發(fā)送的數(shù)據(jù)掛起并發(fā)送
一個(gè)ARP 請(qǐng)求包,發(fā)送完成之后等待主機(jī)B 應(yīng)答,直到收到主機(jī)B 的應(yīng)答包之后才把掛起的
數(shù)據(jù)包添加以太網(wǎng)首部發(fā)送至主機(jī)B 當(dāng)中。

lwIP 描述ARP 緩存表和ARP 相關(guān)處理函數(shù)由etharp.c/h 文件定義,下面筆者重點(diǎn)講解
ARP 緩存表的表項(xiàng)信息和掛起流程。ARP 緩存表結(jié)構(gòu)如下所示:

struct etharp_entry {
#if ARP_QUEUEING/* 數(shù)據(jù)包緩存隊(duì)列指針*/struct etharp_q_entry *q;
#else /* ARP_QUEUEING *//* 指向此ARP表項(xiàng)上的單個(gè)掛起數(shù)據(jù)包的指針*/
struct pbuf *q;
#endif /* ARP_QUEUEING */ip4_addr_t ipaddr; /* 目標(biāo)IP 地址*/struct netif *netif; /* 對(duì)應(yīng)網(wǎng)卡信息*/struct eth_addr ethaddr; /* 對(duì)應(yīng)的MAC 地址*/u16_t ctime; /* 生存時(shí)間信息*/u8_t state; /* 表項(xiàng)的狀態(tài)*/
};static struct etharp_entry arp_table[ARP_TABLE_SIZE];

可以看出,ARP 緩存表(arp_table)最大存放10 個(gè)表項(xiàng),每一個(gè)表項(xiàng)描述符了IP 地址映
射MAC 地址的信息和表項(xiàng)生存時(shí)間與狀態(tài)。這個(gè)ARP 緩存表很小,lwIP 根據(jù)傳入的目標(biāo)IP
地址對(duì)ARP 緩存表直接采用遍歷方式查找對(duì)應(yīng)的MAC 地址。

注:每一個(gè)表項(xiàng)都有一個(gè)生存時(shí)間,若超出了自身生存時(shí)間,則lwIP 內(nèi)核會(huì)把這個(gè)表項(xiàng)
刪除,這里用到了超時(shí)處理機(jī)制。

每一個(gè)表項(xiàng)從創(chuàng)建、請(qǐng)求等都設(shè)置了一個(gè)狀態(tài),不同狀態(tài)的表項(xiàng)都需要特殊的處理,這些
狀態(tài)如下所示:

enum etharp_state {ETHARP_STATE_EMPTY = 0,ETHARP_STATE_PENDING,ETHARP_STATE_STABLE,ETHARP_STATE_STABLE_REREQUESTING_1,ETHARP_STATE_STABLE_REREQUESTING_2
};

下面筆者講解一下每一個(gè)表項(xiàng)的作用及任務(wù)。

  • (1) ETHARP_STATE_EMPTY 狀態(tài)
    這個(gè)狀態(tài)表示ARP 緩存表處于初始化的狀態(tài),所有表項(xiàng)初始化之后才可以被使用,如果需要添加表項(xiàng),lwIP 內(nèi)核就會(huì)遍歷ARP 緩存表并找到合適的表項(xiàng)進(jìn)行添加。

  • (2) ETHARP_STATE_PENDING 狀態(tài)
    該狀態(tài)表示該表項(xiàng)處于不穩(wěn)定狀態(tài),此時(shí)該表項(xiàng)只記錄到了IP 地址,但是還未記錄到對(duì)
    應(yīng)的MAC 地址。很可能的情況是:lwIP 內(nèi)核已經(jīng)發(fā)出一個(gè)關(guān)于該IP 地址的ARP 請(qǐng)求到數(shù)據(jù)鏈路上且lwIP 內(nèi)核還未收到ARP 應(yīng)答,此時(shí)ETHARP_STATE_PENDING 狀態(tài)下會(huì)設(shè)定超時(shí)時(shí)間(5 秒),當(dāng)計(jì)數(shù)超時(shí)后,對(duì)應(yīng)的表項(xiàng)將被刪除,超時(shí)時(shí)間需要宏定義
    ARP_MAXPENDING 來指定,默認(rèn)為5 秒,如果在5 秒之前收到應(yīng)答數(shù)據(jù)包,那么系統(tǒng)會(huì)更新緩存表的信息,記錄目標(biāo)IP 地址與目標(biāo)MAC 地址的映射關(guān)系并且開始記錄表項(xiàng)的生存時(shí)間,同時(shí)該表項(xiàng)的狀態(tài)會(huì)變成ETHARP_STATE_STABLE 狀態(tài)。

  • (3) ETHARP_STATE_STABLE 狀態(tài)
    當(dāng)收到應(yīng)答之前,這些數(shù)據(jù)包會(huì)暫時(shí)掛載到表項(xiàng)的數(shù)據(jù)包緩沖隊(duì)列上,收到應(yīng)答之后,系統(tǒng)已經(jīng)更新ARP 緩存表,那么系統(tǒng)發(fā)送數(shù)據(jù)就會(huì)進(jìn)入該狀態(tài)

  • (4)ETHARP_STATE_STABLE_REREQUESTING_1&&
    ETHARP_STATE_STABLE_REREQUESTING_2 狀態(tài)
    如果系統(tǒng)再一次發(fā)送ARP 請(qǐng)求數(shù)據(jù)包,則表項(xiàng)狀態(tài)會(huì)暫時(shí)被設(shè)置為ETHARP_STATE_ST
    ABLE_REREQUESTING_1,之后設(shè)置為ETHARP_STATE_STABLE_REREQUESTING_2 狀態(tài),其實(shí)這兩個(gè)狀態(tài)為過渡狀態(tài),如果5 秒之前收到ARP 應(yīng)答后,表項(xiàng)又會(huì)被設(shè)置為ETHARP_S
    TATE_STABLE 狀態(tài),這樣子能保持表項(xiàng)的有效性。
    這些狀態(tài)也是和超時(shí)處理相關(guān),在ARP 超時(shí)事件中,需要定時(shí)遍歷ARP 緩存表各個(gè)表項(xiàng)的狀態(tài)和檢測(cè)各個(gè)表項(xiàng)的生存時(shí)間。稍后筆者也會(huì)講解ARP 超時(shí)事件的作用。
    表項(xiàng)掛起數(shù)據(jù)包

之前講解過,lwIP 發(fā)送數(shù)據(jù)包時(shí)需要檢測(cè)ARP 緩存表是否包含對(duì)方主機(jī)的MAC 地址,
若ARP 緩存表沒有包含對(duì)方主機(jī)的MAC 地址,則lwIP 內(nèi)核在ARP 緩存表上創(chuàng)建一個(gè)表項(xiàng)并
且構(gòu)建一個(gè)ARP 請(qǐng)求包,發(fā)送完成之后lwIP 內(nèi)核把要發(fā)送的數(shù)據(jù)包掛載到新創(chuàng)建的表項(xiàng)當(dāng)中。在表項(xiàng)中包含了etharp_q_entry 結(jié)構(gòu)體和pbuf 結(jié)構(gòu)體指針,這兩個(gè)都是用來掛載數(shù)據(jù)包的,一般來說,lwIP 內(nèi)核不使用etharp_q_entry 結(jié)構(gòu)體掛載數(shù)據(jù)包,而是直接使用指針指向pbuf 數(shù)據(jù)包,下面筆者使用一張圖來描述上面的內(nèi)容。

在這里插入圖片描述

ARP 緩存表的超時(shí)處理

上一個(gè)小節(jié)寫了這么多,無非就是為了ARP 表項(xiàng)的ctime(生存時(shí)間)這個(gè)參數(shù)準(zhǔn)備的,
其實(shí)這個(gè)參數(shù)筆者在上面也有所涉及,因?yàn)橄到y(tǒng)以周期的形式調(diào)用函數(shù)etharp_trm。例如,5秒之前收到ARP 的應(yīng)答包就會(huì)更新ARP 緩存表,這個(gè)函數(shù)的作用就是使每個(gè)ARP 緩存表項(xiàng)ctime 字段加1 處理,如果某個(gè)表項(xiàng)的生存時(shí)間計(jì)數(shù)值大于系統(tǒng)規(guī)定的某個(gè)值,系統(tǒng)就會(huì)刪除該表項(xiàng)。etharp_trm 函數(shù)如下所示:

void etharp_tmr(void) {u8_t i;/* 第一步:ARP緩存表遍歷,ARP_TABLE_SIZE = 10 */for (i = 0; i < ARP_TABLE_SIZE; ++i) {/* 獲取表項(xiàng)的狀態(tài)*/u8_t state = arp_table[i].state;/* 第二步:判斷該狀態(tài)不等于空(初始化的狀態(tài))*/if (state != ETHARP_STATE_EMPTY) {/* ARP緩存表項(xiàng)的生存時(shí)間+1 */arp_table[i].ctime++;/* 第三步:發(fā)送ARP請(qǐng)求數(shù)據(jù)包并判斷ctime是否大于5秒*/if ((arp_table[i].ctime >= ARP_MAXAGE) ||((arp_table[i].state == ETHARP_STATE_PENDING) &&(arp_table[i].ctime >= ARP_MAXPENDING))) {/* 從ARP緩存表中刪除該表項(xiàng)*/etharp_free_entry(i);} else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_1) {/* 這是一個(gè)過度形式*/arp_table[i].state = ETHARP_STATE_STABLE_REREQUESTING_2;} else if (arp_table[i].state == ETHARP_STATE_STABLE_REREQUESTING_2) {/* 將狀態(tài)重置為穩(wěn)定狀態(tài),使下一個(gè)傳輸?shù)臄?shù)據(jù)包將重新發(fā)送一個(gè)ARP請(qǐng)求*/arp_table[i].state = ETHARP_STATE_STABLE;} else if (arp_table[i].state == ETHARP_STATE_PENDING) {/* 仍然掛起,重新發(fā)送一個(gè)ARP查詢*/etharp_request(arp_table[i].netif, & arp_table[i].ipaddr);}}}
}

此函數(shù)非常簡單,這里筆者使用一個(gè)流程圖來講解這個(gè)函數(shù)的實(shí)現(xiàn)流程,如下圖所示:

在這里插入圖片描述

從上圖可以看出,這些ARP 緩存表的表項(xiàng)都會(huì)定期檢測(cè),如果這些表項(xiàng)超時(shí)最大生存時(shí)
間,那么lwIP 內(nèi)核會(huì)把這些表項(xiàng)統(tǒng)一刪除。

APR 報(bào)文的報(bào)文結(jié)構(gòu)

典型的ARP 報(bào)文結(jié)構(gòu),該結(jié)構(gòu)如下圖所示:

在這里插入圖片描述

左邊的是以太網(wǎng)首部,數(shù)據(jù)發(fā)送時(shí)必須添加以太網(wǎng)首部,添加完成之后才能把數(shù)據(jù)發(fā)往到
網(wǎng)絡(luò)當(dāng)中(這里解答了為什么需要對(duì)方主機(jī)的MAC 地址),而右邊是ARP 報(bào)文結(jié)構(gòu),它一共定義了5 個(gè)字段,它們分別為:

  • 硬件類型:如果這個(gè)類型設(shè)置為1 表示以太網(wǎng)MAC 地址。
  • 協(xié)議類型:表示要映射的協(xié)議地址類型,0x0800–映射為IP 地址。
  • 硬件地址長度和協(xié)議地址長度:以太網(wǎng)ARP 請(qǐng)求和應(yīng)答分別設(shè)置為6 和4,它們代表M
    AC 地址長度和IP 地址長度。在ARP 協(xié)議包中留出硬件地址長度字段和協(xié)議地址長度字段可以使得ARP 協(xié)議在任何網(wǎng)絡(luò)中被使用,而不僅僅只在以太網(wǎng)中。
  • op:ARP 數(shù)據(jù)包的類型,ARP 請(qǐng)求設(shè)置為1,ARP 應(yīng)答設(shè)置為2。
  • 剩下的字段就是填入本地IP 地址與本地MAC 地址和目標(biāo)IP 地址與目標(biāo)MAC 地址。

關(guān)于ARP 報(bào)文結(jié)構(gòu)可在ethernet.h 找到一些數(shù)據(jù)結(jié)構(gòu)和宏描述,如下所示:

/**********************************ethernet.h********************************/ #
define ETH_HWADDR_LEN 6 /* 以太網(wǎng)地址長度*/
struct eth_addr { /* 一個(gè)以太網(wǎng)MAC地址*/PACK_STRUCT_FLD_8(u8_t addr[ETH_HWADDR_LEN]);
}
PACK_STRUCT_STRUCT;
struct eth_hdr { /* 以太網(wǎng)首部*/ #if ETH_PAD_SIZEPACK_STRUCT_FLD_8(u8_t padding[ETH_PAD_SIZE]);#endifPACK_STRUCT_FLD_S(struct eth_addr dest); /* 以太網(wǎng)目標(biāo)地址(6字節(jié)) */PACK_STRUCT_FLD_S(struct eth_addr src); /* 以太網(wǎng)源MAC 地址(6字節(jié)) */PACK_STRUCT_FIELD(u16_t type); /* 幀類型(2字節(jié)) */
}
PACK_STRUCT_STRUCT;
/***********************************etharp.h**********************************/
struct etharp_hdr { /* ARP 報(bào)文*//* ARP 報(bào)文首部*/PACK_STRUCT_FIELD(u16_t hwtype); /* 硬件類型(2字節(jié)) */PACK_STRUCT_FIELD(u16_t proto); /* 協(xié)議類型(2字節(jié)) */PACK_STRUCT_FLD_8(u8_t hwlen); /* 硬件地址長度(1字節(jié)) */PACK_STRUCT_FLD_8(u8_t protolen); /* 協(xié)議地址長度(2字節(jié)) */PACK_STRUCT_FIELD(u16_t opcode); /* op 字段(2字節(jié)) */PACK_STRUCT_FLD_S(struct eth_addr shwaddr); /* 源MAC 地址(6字節(jié)) */PACK_STRUCT_FLD_S(struct ip4_addr2 sipaddr); /* 源ip 地址(4字節(jié)) */PACK_STRUCT_FLD_S(struct eth_addr dhwaddr); /* 目標(biāo)MAC 地址(6字節(jié)) */PACK_STRUCT_FLD_S(struct ip4_addr2 dipaddr); /* 目標(biāo)ip 地址(4字節(jié)) */
}
PACK_STRUCT_STRUCT;
/* op 字段操作*/
enum etharp_opcode {ARP_REQUEST = 1, /* 請(qǐng)求包*/ARP_REPLY = 2 /* 應(yīng)答包*/
};

前面的eth_hdr 結(jié)構(gòu)體就是定義了以太網(wǎng)首部字段,而etharp_hdr 結(jié)構(gòu)體定義了ARP 首部
的字段信息。下面筆者使用wireshark 網(wǎng)絡(luò)抓包工具形象地講解報(bào)文格式和內(nèi)容,如下圖所示:

在這里插入圖片描述
在這里插入圖片描述

從這兩張圖可以看出,圖一的ARP 數(shù)據(jù)包是以廣播的方式發(fā)送,它的OP 字段類型為1
表示ARP 數(shù)據(jù)包為ARP 請(qǐng)求包。圖二的ARP 數(shù)據(jù)包為ARP 應(yīng)答包,因?yàn)樗腛P 字段為2,所以該包不是以廣播的方式發(fā)送。

ARP 協(xié)議層的接收與發(fā)送原理解析

發(fā)送ARP 請(qǐng)求數(shù)據(jù)包

構(gòu)建ARP 請(qǐng)求包函數(shù)是在etharp_raw 函數(shù)下實(shí)現(xiàn),該函數(shù)如下所示:

static err_t
etharp_raw(struct netif * netif, /* 發(fā)送ARP 數(shù)據(jù)包的lwip 網(wǎng)絡(luò)接口*/const struct eth_addr * ethsrc_addr, /* 以太網(wǎng)源MAC 地址*/const struct eth_addr * ethdst_addr, /* 以太網(wǎng)目標(biāo)MAC 地址*/const struct eth_addr * hwsrc_addr, /* ARP 協(xié)議源MAC 地址*/const ip4_addr_t * ipsrc_addr, /* ARP 協(xié)議源IP 地址*/const struct eth_addr * hwdst_addr, /* ARP 協(xié)議目標(biāo)MAC 地址*/const ip4_addr_t * ipdst_addr, /* ARP 協(xié)議目標(biāo)IP 地址*/const u16_t opcode) /* ARP 數(shù)據(jù)包的類型:1為請(qǐng)求包類型、2為應(yīng)答包類型*/ {struct pbuf * p;err_t result = ERR_OK;struct etharp_hdr * hdr;/* 申請(qǐng)ARP 報(bào)文的內(nèi)存池空間*/p = pbuf_alloc(PBUF_LINK, SIZEOF_ETHARP_HDR, PBUF_RAM);/* 申請(qǐng)內(nèi)存池是否成功*/if (p == NULL) {ETHARP_STATS_INC(etharp.memerr);return ERR_MEM;}/* ARP 報(bào)文的數(shù)據(jù)區(qū)域,并且強(qiáng)制將起始地址轉(zhuǎn)化成ARP 報(bào)文首部*/hdr = (struct etharp_hdr * ) p - > payload;/* ARP 數(shù)據(jù)包的op 字段*/hdr - > opcode = lwip_htons(opcode);/* 源MAC地址*/SMEMCPY( & hdr - > shwaddr, hwsrc_addr, ETH_HWADDR_LEN);/* 目的MAC地址*/SMEMCPY( & hdr - > dhwaddr, hwdst_addr, ETH_HWADDR_LEN);/* 源IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T( & hdr - > sipaddr, ipsrc_addr);/* 目的IP地址*/IPADDR_WORDALIGNED_COPY_FROM_IP4_ADDR_T( & hdr - > dipaddr, ipdst_addr);/* 硬件類型*/hdr - > hwtype = PP_HTONS(HWTYPE_ETHERNET);/* 協(xié)議類型*/hdr - > proto = PP_HTONS(ETHTYPE_IP);/* 硬件地址長度*/hdr - > hwlen = ETH_HWADDR_LEN;/* 協(xié)議地址長度*/hdr - > protolen = sizeof(ip4_addr_t);#if LWIP_AUTOIPif (ip4_addr_islinklocal(ipsrc_addr)) {ethernet_output(netif, p, ethsrc_addr, & ethbroadcast, ETHTYPE_ARP);} else# endif /* LWIP_AUTOIP */ {/* 調(diào)用底層發(fā)送函數(shù)將以太網(wǎng)數(shù)據(jù)幀發(fā)送出去*/ethernet_output(netif, p, ethsrc_addr, ethdst_addr, ETHTYPE_ARP);}ETHARP_STATS_INC(etharp.xmit);/* 發(fā)送完成釋放內(nèi)存*/pbuf_free(p);p = NULL;/* 發(fā)送完成返回結(jié)果*/return result;}/* 定義以太網(wǎng)廣播地址*/
const struct eth_addr ethbroadcast = {{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
};
/* 填寫ARP請(qǐng)求包的接收方MAC字段*/
const struct eth_addr ethzero = {{0, 0, 0, 0, 0, 0}
};
static err_t
etharp_request_dst(struct netif * netif,const ip4_addr_t * ipaddr,const struct eth_addr * hw_dst_addr) {return etharp_raw(netif, (struct eth_addr * ) netif - > hwaddr, hw_dst_addr, (struct eth_addr * ) netif - > hwaddr,netif_ip4_addr(netif), & ethzero,ipaddr, ARP_REQUEST);}/* 發(fā)送一個(gè)要求ipaddr的ARP請(qǐng)求包*/
err_t
etharp_request(struct netif * netif,const ip4_addr_t * ipaddr) {return etharp_request_dst(netif, ipaddr, & ethbroadcast);
}

發(fā)送ARP 請(qǐng)求報(bào)文之前先申請(qǐng)pbuf 內(nèi)存,接著由pbuf 的payload 指針指向的地址添加
ARP 首部,添加完成之后設(shè)置ARP 首部字段的信息,最后由ethernet_output 函數(shù)為pbuf 添加以太網(wǎng)首部和發(fā)送,發(fā)送完成之后把要發(fā)送的數(shù)據(jù)掛載到ARP 緩存表項(xiàng)當(dāng)中。

接收ARP 應(yīng)答數(shù)據(jù)包

雖然ARP 和IP 協(xié)議同屬于網(wǎng)絡(luò)層的協(xié)議,但是從分層的結(jié)構(gòu)來看,ARP 處于網(wǎng)絡(luò)層的最
底層,而IP 處于網(wǎng)絡(luò)層的頂層??偟膩碚f,ARP 最接近網(wǎng)卡驅(qū)動(dòng)文件,發(fā)送的數(shù)據(jù)經(jīng)過ARP檢測(cè)和操作發(fā)送至網(wǎng)卡驅(qū)動(dòng)文件處理,由網(wǎng)卡驅(qū)動(dòng)文件調(diào)用ETH 外設(shè)把數(shù)據(jù)發(fā)送至PHY 設(shè)備當(dāng)中。

下面筆者來講解網(wǎng)卡驅(qū)動(dòng)文件的函數(shù)如何把接收的數(shù)據(jù)發(fā)送至ARP 或者IP 處理,這個(gè)函數(shù)為ethernet_input,如下所示:

err_t
ethernet_input(struct pbuf * p, struct netif * netif) {struct eth_hdr * ethhdr;u16_t type;#if LWIP_ARP || ETHARP_SUPPORT_VLAN || LWIP_IPV6s16_t ip_hdr_offset = SIZEOF_ETH_HDR;#endif /* LWIP_ARP || ETHARP_SUPPORT_VLAN *//* 第一步:判斷數(shù)據(jù)包是否小于等于以太網(wǎng)頭部的大小如果是,則釋放內(nèi)存,直接返回*/if (p - > len <= SIZEOF_ETH_HDR) {ETHARP_STATS_INC(etharp.proterr);ETHARP_STATS_INC(etharp.drop);MIB2_STATS_NETIF_INC(netif, ifinerrors);goto free_and_return;}if (p - > if_idx == NETIF_NO_INDEX) {p - > if_idx = netif_get_index(netif);}/* 第二步:p->payload表示指向緩沖區(qū)中實(shí)際數(shù)據(jù)的指針相當(dāng)于指向以太網(wǎng)的頭部*/ethhdr = (struct eth_hdr * ) p - > payload;/* 第三步:獲取數(shù)據(jù)包的類型*/type = ethhdr - > type;#if LWIP_ARP_FILTER_NETIFnetif = LWIP_ARP_FILTER_NETIF_FN(p, netif, lwip_htons(type));#endif /* LWIP_ARP_FILTER_NETIF*//* 第四步:判斷數(shù)據(jù)包是以怎么樣的類型發(fā)來的*/if (ethhdr - > dest.addr[0] & 1) {/* 這可能是一個(gè)多播或廣播包*/if (ethhdr - > dest.addr[0] == LL_IP4_MULTICAST_ADDR_0) {#if LWIP_IPV4if ((ethhdr - > dest.addr[1] == LL_IP4_MULTICAST_ADDR_1) &&(ethhdr - > dest.addr[2] == LL_IP4_MULTICAST_ADDR_2)) {/* 將pbuf標(biāo)記為鏈路層多播*/p - > flags |= PBUF_FLAG_LLMCAST;}#endif /* LWIP_IPV4 */} else if (eth_addr_cmp( & ethhdr - > dest, & ethbroadcast)) {/* 將pbuf標(biāo)記為鏈路層廣播*/p - > flags |= PBUF_FLAG_LLBCAST;}}/* 第五步:判斷數(shù)據(jù)包的類型*/switch (type) {#if LWIP_IPV4 && LWIP_ARP/* IP數(shù)據(jù)包*/case PP_HTONS(ETHTYPE_IP):if (!(netif - > flags & NETIF_FLAG_ETHARP)) {goto free_and_return;}/* 去除以太網(wǎng)報(bào)頭*/if ((p - > len < ip_hdr_offset) ||pbuf_header(p, (s16_t) - ip_hdr_offset)) { /* 去除以太網(wǎng)首部失敗,則直接返回*/goto free_and_return;} else {/* 傳遞到IP 協(xié)議去處理*/ip4_input(p, netif);}break;/* 對(duì)于是ARP 包*/case PP_HTONS(ETHTYPE_ARP):if (!(netif - > flags & NETIF_FLAG_ETHARP)) {goto free_and_return;}/* 去除以太網(wǎng)首部*/if ((p - > len < ip_hdr_offset) ||pbuf_header(p, (s16_t) - ip_hdr_offset)) { /* 去除以太網(wǎng)首部失敗,則直接返回*/ETHARP_STATS_INC(etharp.lenerr);ETHARP_STATS_INC(etharp.drop);goto free_and_return;} else {/* 傳遞到ARP 協(xié)議處理*/etharp_input(p, netif);}break;#endif /* LWIP_IPV4 && LWIP_ARP */default:#ifdef LWIP_HOOK_UNKNOWN_ETH_PROTOCOLif (LWIP_HOOK_UNKNOWN_ETH_PROTOCOL(p, netif) == ERR_OK) {break;}#endifETHARP_STATS_INC(etharp.proterr);ETHARP_STATS_INC(etharp.drop);MIB2_STATS_NETIF_INC(netif, ifinunknownprotos);goto free_and_return;}return ERR_OK;free_and_return:pbuf_free(p);return ERR_OK;
}

為了理解整個(gè)以太網(wǎng)的數(shù)據(jù)幀在ARP 層處理,筆者就以圖形展示整個(gè)數(shù)據(jù)包遞交流程,
如下圖所示

在這里插入圖片描述

可以看出,數(shù)據(jù)包在ethernet_input 中需要判斷該數(shù)據(jù)包的類型,若該數(shù)據(jù)包的類型為IP
數(shù)據(jù)包,則lwIP 內(nèi)核把該數(shù)據(jù)包遞交給ip4_input 函數(shù)處理。若該數(shù)據(jù)包的類型為ARP 數(shù)據(jù)
包,則lwIP 內(nèi)核把該數(shù)據(jù)包遞交給etharp_input 函數(shù)處理,遞交完成之后該函數(shù)需要判斷
ARP 數(shù)據(jù)包的類型,如果它是ARP 請(qǐng)求包,則lwIP 內(nèi)核調(diào)用etharp_raw 函數(shù)構(gòu)建ARP 應(yīng)答包并且更新ARP 緩存表;如果它是ARP 應(yīng)答包,則lwip 內(nèi)核更新ARP 緩存表并且把表項(xiàng)掛載的數(shù)據(jù)包以ethernet_output 函數(shù)發(fā)送。

IP 協(xié)議

IP 指網(wǎng)際互連協(xié)議,Internet Protocol 的縮寫,是TCP/IP 體系中的網(wǎng)絡(luò)層協(xié)議。設(shè)計(jì)IP 的
目的是提高網(wǎng)絡(luò)的可擴(kuò)展性:一是解決互聯(lián)網(wǎng)問題,實(shí)現(xiàn)大規(guī)模、異構(gòu)網(wǎng)絡(luò)的互聯(lián)互通;二是分割頂層網(wǎng)絡(luò)應(yīng)用和底層網(wǎng)絡(luò)技術(shù)之間的耦合關(guān)系,以利于兩者的獨(dú)立發(fā)展。根據(jù)端到端的設(shè)計(jì)原則,IP 只為主機(jī)提供一種無連接、不可靠的、盡力而為的數(shù)據(jù)包傳輸服務(wù)。

IP 協(xié)議的簡介

IP 協(xié)議是整個(gè)TCP/IP 協(xié)議族的核心,也是構(gòu)成互聯(lián)網(wǎng)的基礎(chǔ)。IP 位于TCP/IP 模型的網(wǎng)
絡(luò)層(相當(dāng)于OSI 模型的網(wǎng)絡(luò)層),它可以向傳輸層提供各種協(xié)議的信息,例如TCP、UDP 等;

對(duì)下可將IP 信息包放到鏈路層,通過以太網(wǎng)、令牌環(huán)網(wǎng)絡(luò)等各種技術(shù)來傳送。為了能適應(yīng)異
構(gòu)網(wǎng)絡(luò),IP 強(qiáng)調(diào)適應(yīng)性、簡潔性和可操作性,并在可靠性做了一定的犧牲。這里我們不過多
深入了解IP 協(xié)議了,本章筆者重點(diǎn)講解IP 數(shù)據(jù)報(bào)的分片與重組原理。

IP 數(shù)據(jù)報(bào)

IP 層數(shù)據(jù)報(bào)也叫做IP 數(shù)據(jù)報(bào)或者IP 分組,IP 數(shù)據(jù)報(bào)組裝在以太網(wǎng)幀中發(fā)送的,它通常由
兩個(gè)部分組成,即IP 首部與數(shù)據(jù)區(qū)域,其中IP 的首部是20 字節(jié)大小,數(shù)據(jù)區(qū)域理論上可以
多達(dá)65535 個(gè)字節(jié),由于以太網(wǎng)網(wǎng)絡(luò)接口的最大傳輸單元為1500,所以一個(gè)完整的數(shù)據(jù)包不
能超出1500 字節(jié)大小。IP 數(shù)據(jù)報(bào)結(jié)構(gòu)如以下圖所示:

在這里插入圖片描述

  • (1) 版本:占4 位指IP 協(xié)議的版本。通信雙方使用的IP 協(xié)議版本必須一致。廣泛使用的
    IP 協(xié)議版本號(hào)為4(即IPv4)。

  • (2) 首部長度:占4 位可表示的最大十進(jìn)制數(shù)值是15。請(qǐng)注意,這個(gè)字段所表示數(shù)的單位是32 位字長(1 個(gè)32 位字長是4 字節(jié)),因此,當(dāng)IP 的首部長度為1111 時(shí)(即十進(jìn)制的15),首部長度就達(dá)到60 字節(jié)。當(dāng)IP 分組的首部長度不是4 字節(jié)的整數(shù)倍時(shí),必須利用最后的填充字段加以填充。因此數(shù)據(jù)部分永遠(yuǎn)在4 字節(jié)的整數(shù)倍開始,這樣在實(shí)現(xiàn)IP 協(xié)議時(shí)較為方便。
    首部長度限制為60 字節(jié)的缺點(diǎn)是有時(shí)可能不夠用。但這樣做是希望用戶盡量減少開銷。最常用的首部長度就是20 字節(jié)(即首部長度為0101),這時(shí)不使用任何選項(xiàng)。

  • (3) 區(qū)分服務(wù):占8 位,用來獲得更好的服務(wù)。這個(gè)字段在舊標(biāo)準(zhǔn)中叫做服務(wù)類型,但實(shí)際上一直沒有被使用過。

  • (4) 總長度:總長度指首部和數(shù)據(jù)之和的長度,單位為字節(jié)??傞L度字段為16 位,因此數(shù)據(jù)報(bào)的最大長度為2^16-1=65534 字節(jié)。
    在IP 層下面的每一種數(shù)據(jù)鏈路層都有自己的幀格式,其中包括幀格式中的數(shù)據(jù)字段的最
    大長度,這稱為最大傳送單元MTU。當(dāng)一個(gè)數(shù)據(jù)報(bào)封裝成鏈路層的幀時(shí),此數(shù)據(jù)報(bào)的總長度(即首部加上數(shù)據(jù)部分)一定不能超過下面的數(shù)據(jù)鏈路層的MTU 值。

  • (5) 標(biāo)識(shí)(identification):占16 位IP 軟件在存儲(chǔ)器中維持一個(gè)計(jì)數(shù)器,每產(chǎn)生一個(gè)數(shù)據(jù)報(bào),計(jì)數(shù)器就加1,并將此值賦給標(biāo)識(shí)字段。但這個(gè)“標(biāo)識(shí)”并不是序號(hào),因?yàn)镮P 是無連接服務(wù),數(shù)據(jù)報(bào)不存在按序接收的問題。當(dāng)數(shù)據(jù)報(bào)由于長度超過網(wǎng)絡(luò)的MTU 而必須分片時(shí),這個(gè)標(biāo)識(shí)字段的值就被復(fù)制到所有的數(shù)據(jù)報(bào)的標(biāo)識(shí)字段中。相同的標(biāo)識(shí)字段的值使分片后的各數(shù)據(jù)報(bào)片最后能正確地重裝成為原來的數(shù)據(jù)報(bào)。

  • (6) 標(biāo)志(flag):占3 位但只有2 位有意義的。

  1. 標(biāo)志字段中的最低位記為MF(More Fragment)。MF=1 即表示后面“還有分片”的數(shù)
    據(jù)報(bào)。MF=0 表示這已是若干數(shù)據(jù)報(bào)片中的最后一個(gè)。
  2. 標(biāo)志字段中間的一位記為DF(Don’t Fragment),意思是“不能分片”。只有當(dāng)DF=0
    時(shí)才允許分片。
  • (7) 片偏移:占13 位片偏移指出:較長的分組在分片后,某片在原分組中的相對(duì)位置。也
    就是說,相對(duì)用戶數(shù)據(jù)字段的起點(diǎn),該片從何處開始。片偏移以8 個(gè)字節(jié)為偏移單位。這就是說,除了最后一個(gè)分片,每個(gè)分片的長度一定是8 字節(jié)(64 位)的整數(shù)倍。
  • (8) 生存時(shí)間:占8 位生存時(shí)間字段常用的的英文縮寫是TTL(Time To Live),表明是數(shù)據(jù)
    報(bào)在網(wǎng)絡(luò)中的壽命。由發(fā)出數(shù)據(jù)報(bào)的源點(diǎn)設(shè)置這個(gè)字段。其目的是防止無法交付的數(shù)據(jù)報(bào)無限制地在因特網(wǎng)中兜圈子,因而白白消耗網(wǎng)絡(luò)資源。最初的設(shè)計(jì)是以秒作為TTL 的單位。每經(jīng)過一個(gè)路由器時(shí),就把TTL 減去數(shù)據(jù)報(bào)在路由器消耗掉的一段時(shí)間。若數(shù)據(jù)報(bào)在路由器消耗的時(shí)間小于1 秒,就把TTL 值減1。當(dāng)TTL 值為0 時(shí),就丟棄這個(gè)數(shù)據(jù)報(bào)。后來把TTL 字段的功能改為“跳數(shù)限制”(但名稱不變)。路由器在轉(zhuǎn)發(fā)數(shù)據(jù)報(bào)之前就把TTL 值減1.若TTL 值減少到零,就丟棄這個(gè)數(shù)據(jù)報(bào),不再轉(zhuǎn)發(fā)。因此,TTL 的單位不再是秒,而是跳數(shù)。TTL 的意義是指明數(shù)據(jù)報(bào)在網(wǎng)絡(luò)中至多可經(jīng)過多少個(gè)路由器。顯然,數(shù)據(jù)報(bào)在網(wǎng)絡(luò)上經(jīng)過的路由器的最大數(shù)值是255。若把TTL 的初始值設(shè)為1,就表示這個(gè)數(shù)據(jù)報(bào)只能在本局域網(wǎng)中傳送。
  • (9) 協(xié)議:占8 位協(xié)議字段指出此數(shù)據(jù)報(bào)攜帶的數(shù)據(jù)是使用何種協(xié)議,以便使目的主機(jī)的IP 層知道應(yīng)將數(shù)據(jù)部分上交給哪個(gè)處理過程。
  • (10) 首部檢驗(yàn)和:占16 位這個(gè)字段只檢驗(yàn)數(shù)據(jù)報(bào)的首部,但不包括數(shù)據(jù)部分。這是因?yàn)閿?shù)據(jù)報(bào)每經(jīng)過一個(gè)路由器,路由器都要重新計(jì)算一下首部檢驗(yàn)和(一些字段,如生存時(shí)間、標(biāo)志、片偏移等都可能發(fā)生變化)。不檢驗(yàn)數(shù)據(jù)部分可減少計(jì)算的工作量。
  • (11) 源地址:占32 位。
  • (12) 目的地址:占32 位。
  • (13) 數(shù)據(jù)區(qū)域:這是IP 數(shù)據(jù)報(bào)的最后的一個(gè)字段,也是最重要的內(nèi)容,lwIP 發(fā)送數(shù)據(jù)報(bào)是把該層的首部封裝到數(shù)據(jù)包里面,在IP 層也是把IP 首部封裝在其中,因?yàn)橛袛?shù)據(jù)區(qū)域才會(huì)有數(shù)據(jù)報(bào)首部的存在,在大多數(shù)情況下,IP 數(shù)據(jù)報(bào)中的數(shù)據(jù)字段包含要交付給目標(biāo)IP 地址的運(yùn)輸層(TCP 協(xié)議或UDP 協(xié)議),當(dāng)然數(shù)據(jù)區(qū)域也可承載其他類型的報(bào)文,如ICMP 報(bào)文等。

IP 數(shù)據(jù)報(bào)結(jié)構(gòu)

在lwIP 中,為了描述IP 報(bào)文結(jié)構(gòu),它在ip4.h 文件中定義了一個(gè)ip_hdr 結(jié)構(gòu)體來描述IP
數(shù)據(jù)報(bào)的內(nèi)容,該結(jié)構(gòu)體如下所示:

struct ip_hdr {/* 版本號(hào)+首部長度+服務(wù)類型*/PACK_STRUCT_FLD_8(u8_t _v_hl);/* 服務(wù)類型*/PACK_STRUCT_FLD_8(u8_t _tos);/* 總長度(IP首部+數(shù)據(jù)區(qū)) */PACK_STRUCT_FIELD(u16_t _len);/* 數(shù)據(jù)包標(biāo)識(shí)(編號(hào)) */PACK_STRUCT_FIELD(u16_t _id);/* 標(biāo)志+片偏移*/PACK_STRUCT_FIELD(u16_t _offset);/* IP首部標(biāo)志定義*/#define IP_RF 0x8000 U /* 保留*/ # define IP_DF 0x4000 U /* 是否允許分片*/ # define IP_MF 0x2000 U /* 后續(xù)是否還有更多分片*/ # define IP_OFFMASK 0x1fff U /* 片偏移域掩碼*//* 生存時(shí)間(最大轉(zhuǎn)發(fā)次數(shù))+協(xié)議類型(IGMP:1、UDP:17、TCP:6) */PACK_STRUCT_FLD_8(u8_t _ttl);/* 協(xié)議*/PACK_STRUCT_FLD_8(u8_t _proto);/* 校驗(yàn)和(IP首部) */PACK_STRUCT_FIELD(u16_t _chksum);/* 源IP地址/目的IP地址*/PACK_STRUCT_FLD_S(ip4_addr_p_t src);PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
}
PACK_STRUCT_STRUCT;
PACK_STRUCT_END

可以看出,此結(jié)構(gòu)體的成員變量和上圖9.2.1 的字段一一對(duì)應(yīng)。

IP 數(shù)據(jù)報(bào)的分片解析

TCP/IP 協(xié)議棧為什么具備分片的概念,因?yàn)閼?yīng)用程序處理的數(shù)據(jù)是不確定的,可能超出
網(wǎng)絡(luò)接口最大傳輸單元,為此TCP/IP 協(xié)議棧引入了分片概念,它是以MTU 為界限對(duì)這個(gè)大
型的數(shù)據(jù)切割成多個(gè)小型的數(shù)據(jù)包。這些小型的數(shù)據(jù)叫做IP 的分組和分片,它們?cè)诮邮辗竭M(jìn)
行重組處理,這樣,接收方的應(yīng)用程序接收到這個(gè)大型的數(shù)據(jù)了??偟膩碇v,IP 數(shù)據(jù)報(bào)的分
片概念是為了解決IP 數(shù)據(jù)報(bào)數(shù)據(jù)過大的問題而誕生。注:以太網(wǎng)最大傳輸單元MTU 為1500。

現(xiàn)在筆者舉個(gè)示例,讓大家更好的理解IP 分片的原理:

假設(shè)IP 數(shù)據(jù)報(bào)整體的大小為4000 字節(jié),IP 首部默認(rèn)為20 字節(jié),而數(shù)據(jù)區(qū)域?yàn)?980。由
于以太網(wǎng)最大傳輸單元為1500,所以lwIP 內(nèi)核會(huì)把這個(gè)數(shù)據(jù)報(bào)進(jìn)行分片處理。

  1. 第一個(gè)IP 分片:
    分片數(shù)據(jù)大小:20(IP 首部)+ 1480(數(shù)據(jù)區(qū)域)。
    標(biāo)識(shí):888。
    標(biāo)志:IP_MF = 1 后續(xù)還有分片。
    片偏移量:片偏移量是0,單位是8 字節(jié),本片偏移量相當(dāng)于0 字節(jié)。
  2. 第二片IP 數(shù)據(jù)報(bào):
    分片數(shù)據(jù)大小:20(IP 首部)+ 1480(數(shù)據(jù)區(qū)域)。
    標(biāo)識(shí):888。
    標(biāo)志:IP_MF = 1 后續(xù)還有分片。
    片偏移量:片偏移量是185(1480/8),單位是8 字節(jié),本片偏移量相當(dāng)于1480 字節(jié)。
  3. 第三片IP 數(shù)據(jù)報(bào):
    分片數(shù)據(jù)大小:20(IP 首部)+ 1020(數(shù)據(jù)區(qū)域)。
    標(biāo)識(shí):888。
    標(biāo)志:IP_MF = 0,后續(xù)沒有分片。
    片偏移量:片偏移量是370(185+185),單位是8 字節(jié),本片偏移量相當(dāng)于2960 字節(jié)。

注:這些分片的標(biāo)識(shí)都是一致的,而IP_MF 表示后續(xù)有沒有分片,若IP_MF 為0,則這
個(gè)分片為最后一個(gè)分片。

在這里插入圖片描述

從上圖可以看出,一個(gè)大型的IP 數(shù)據(jù)包經(jīng)過網(wǎng)絡(luò)層處理,它會(huì)被分成兩個(gè)或者兩個(gè)以上的IP 分片,這些分片的數(shù)據(jù)組合起來就是應(yīng)用程序發(fā)送的數(shù)據(jù)與傳輸層的首部。
至此,我們已經(jīng)明白了IP 分片的原理,下面筆者講解lwIP 內(nèi)核如何實(shí)現(xiàn)這個(gè)原理,它的
實(shí)現(xiàn)函數(shù)為ip4_frag,該函數(shù)如下所示:

/**
* 如果IP數(shù)據(jù)報(bào)對(duì)netif來說太大,則將其分片,
將數(shù)據(jù)報(bào)切成MTU大小的塊,然后按順序發(fā)送通過將pbuf_ref指向p
* @param p:要發(fā)送的IP數(shù)據(jù)包
* @param netif:發(fā)送的netif
* @param dest:目的IP地址
* @return ERR_OK:發(fā)送成功, err_t:其他
*/
err_t
ip4_frag(struct pbuf * p, struct netif * netif,const ip4_addr_t * dest) {struct pbuf * rambuf;#if !LWIP_NETIF_TX_SINGLE_PBUFstruct pbuf * newpbuf;u16_t newpbuflen = 0;u16_t left_to_copy;#endifstruct ip_hdr * original_iphdr;struct ip_hdr * iphdr;/* (1500 - 20)/8 = 偏移185 */const u16_t nfb = (u16_t)((netif - > mtu - IP_HLEN) / 8);u16_t left, fragsize;u16_t ofo;int last;u16_t poff = IP_HLEN; /* IP頭部長度*/u16_t tmp;int mf_set;original_iphdr = (struct ip_hdr * ) p - > payload; /* 指向數(shù)據(jù)報(bào)*/iphdr = original_iphdr;/* 判斷IP頭部是否為20 */if (IPH_HL_BYTES(iphdr) != IP_HLEN) {return ERR_VAL;}/* tmp變量獲取標(biāo)志和片偏移數(shù)值*/tmp = lwip_ntohs(IPH_OFFSET(iphdr));/* ofo = 片偏移*/ofo = tmp & IP_OFFMASK;/* mf_set = 分片標(biāo)志*/mf_set = tmp & IP_MF;/* left = 總長度減去IP頭部等于有效數(shù)據(jù)長度,4000 - 20 = 3980 */left = (u16_t)(p - > tot_len - IP_HLEN);/* 判斷l(xiāng)eft是否為有效數(shù)據(jù)*/while (left) {/* 判斷有效數(shù)據(jù)和偏移數(shù)據(jù)大小,fragsize = 1480 (3980 < 1480 ? 3980 : 1480) */fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));/* rambuf申請(qǐng)20字節(jié)大小的內(nèi)存塊*/rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);if (rambuf == NULL) {goto memerr;}/* 這個(gè)rambuf有效數(shù)據(jù)指針指向original_iphdr數(shù)據(jù)報(bào)*/SMEMCPY(rambuf - > payload, original_iphdr, IP_HLEN);/* iphdr指向有效區(qū)域地址rambuf->payload */iphdr = (struct ip_hdr * ) rambuf - > payload;/* left_to_copy = 偏移數(shù)據(jù)大小(1480) */left_to_copy = fragsize;while (left_to_copy) {struct pbuf_custom_ref * pcr;/* 當(dāng)前pbuf中數(shù)據(jù)的長度,plen = 3980 - 20 = 3960 */u16_t plen = (u16_t)(p - > len - poff);/* newpbuflen = 1480 (1480 < 3960 ? 1480 : 3960) */newpbuflen = LWIP_MIN(left_to_copy, plen);if (!newpbuflen) {poff = 0;p = p - > next;continue;}/* pcr申請(qǐng)內(nèi)存*/pcr = ip_frag_alloc_pbuf_custom_ref();if (pcr == NULL) {pbuf_free(rambuf);goto memerr;}/* newpbuf申請(qǐng)內(nèi)存1480字節(jié),保存了這個(gè)數(shù)據(jù)區(qū)域偏移poff字節(jié)的數(shù)據(jù)(p->payload + poff) */newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, & pcr - > pc, (u8_t * ) p - > payload + poff, newpbuflen);if (newpbuf == NULL) {/* 釋放內(nèi)存*/ip_frag_free_pbuf_custom_ref(pcr);pbuf_free(rambuf);goto memerr;}/* 增加pbuf的引用計(jì)數(shù)*/pbuf_ref(p);pcr - > original = p;pcr - > pc.custom_free_function = ipfrag_free_pbuf_custom;/* 將它添加到rambuf的鏈的末尾*/pbuf_cat(rambuf, newpbuf);/* left_to_copy = 0 (1480 - 1480) */left_to_copy = (u16_t)(left_to_copy - newpbuflen);if (left_to_copy) {poff = 0;p = p - > next;}}/* poff = 1500 (20 + 1480) */poff = (u16_t)(poff + newpbuflen);/* last = 0 (3980 <= (1500 - 20)) */last = (left <= netif - > mtu - IP_HLEN);/* 設(shè)置新的偏移量和MF標(biāo)志*/tmp = (IP_OFFMASK & (ofo));/* 判斷是否是最后一個(gè)分片*/if (!last || mf_set) {/* 最后一個(gè)片段設(shè)置了MF為0 */tmp = tmp | IP_MF;}/* 分段偏移與標(biāo)志字段*/IPH_OFFSET_SET(iphdr, lwip_htons(tmp));/* 設(shè)置數(shù)據(jù)報(bào)總長度= 1500 (1480 + 20) */IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN)));/* 校驗(yàn)為0 */IPH_CHKSUM_SET(iphdr, 0);/* 發(fā)送IP數(shù)據(jù)報(bào)*/netif - > output(netif, rambuf, dest);IPFRAG_STATS_INC(ip_frag.xmit);/* rambuf釋放內(nèi)存*/pbuf_free(rambuf);/* left = 2500 (3980 - 1480) */left = (u16_t)(left - fragsize);/* 片偏移ofo = 185(0 + 185) */ofo = (u16_t)(ofo + nfb);}MIB2_STATS_INC(mib2.ipfragoks);return ERR_OK;memerr:MIB2_STATS_INC(mib2.ipfragfails);return ERR_MEM;
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}

此函數(shù)非常簡單,首先判斷這個(gè)大型數(shù)據(jù)包的有效區(qū)域總長度,系統(tǒng)根據(jù)這個(gè)總長度劃分
數(shù)據(jù)區(qū)域,接著申請(qǐng)20+sizeof(struct pbuf)字節(jié)的rampbuf 來存儲(chǔ)IP 首部,然后根據(jù)poff 數(shù)值讓被分片數(shù)據(jù)包的payload 指針偏移poff 大小,它所指向的地址由newpbuf 數(shù)據(jù)包的payload指針指向,最后調(diào)用netif->output 函數(shù)發(fā)送該分片,其他分片一樣操作。

在這里插入圖片描述

上圖中,newpbuf 的payload 指針指向的地址由左邊的payload 指針經(jīng)過偏移得來的。

IP 數(shù)據(jù)報(bào)的分片重裝

由于IP 分組在網(wǎng)絡(luò)傳輸過程中到達(dá)目的地點(diǎn)的時(shí)間是不確定的,所以后面的分組可能比
前面的分組先達(dá)到目的地點(diǎn)。為此,lwIP 內(nèi)核需要將接收到的分組暫存起來,等所有的分組
都接收完成之后,再將數(shù)據(jù)傳遞給上層。

在lwIP 中,有專門的結(jié)構(gòu)體負(fù)責(zé)緩存這些分組,這個(gè)結(jié)構(gòu)體為ip_reassdata 重裝數(shù)據(jù)鏈表,
該結(jié)構(gòu)體在ip4_frag.h 文件中定義,如下所示:

/* 重裝數(shù)據(jù)結(jié)構(gòu)體*/
struct ip_reassdata {struct ip_reassdata *next; /* 指向下一個(gè)重裝節(jié)點(diǎn)*/struct pbuf *p; /* 指向分組的pbuf */struct ip_hdr iphdr; /* IP數(shù)據(jù)報(bào)的首部*/u16_t datagram_len; /* 已收到數(shù)據(jù)的長度*/u8_t flags; /* 標(biāo)志是否最后一個(gè)分組*/u8_t timer; /* 超時(shí)間隔*/
};

這個(gè)結(jié)構(gòu)體描述了同類型的IP 分組信息,同類型的IP 分組會(huì)掛載到該重裝節(jié)點(diǎn)上,如下
圖所示:

在這里插入圖片描述

可以看到,這些分片掛載到同一個(gè)重裝節(jié)點(diǎn)上,它們掛載之前,是把IP 首部的前8 字節(jié)
強(qiáng)制轉(zhuǎn)換成三個(gè)字段,其中next_pbuf 指針用來鏈接這些IP 分組,形成了單向鏈表,而start
和end 字段用來描述分組的順序,lwIP 系統(tǒng)根據(jù)這些數(shù)值對(duì)分組進(jìn)行排序。

lwIP 內(nèi)核的IP 重組功能由ip4_reass 函數(shù)實(shí)現(xiàn),該函數(shù)的代碼量比較長,這里筆者不深入
講解了,我們會(huì)在視頻當(dāng)中講解IP 重裝流程。

IP 數(shù)據(jù)報(bào)的輸出

無論是UDP 還是TCP,它們的數(shù)據(jù)段遞交至網(wǎng)絡(luò)層的接口是一致的,這個(gè)接口函數(shù)如下
所示:

err_t
ip4_output_if_src(struct pbuf * p,const ip4_addr_t * src,const ip4_addr_t * dest,u8_t ttl, u8_t tos,u8_t proto, struct netif * netif) {struct ip_hdr * iphdr;ip4_addr_t dest_addr;if (dest != LWIP_IP_HDRINCL) {u16_t ip_hlen = IP_HLEN;/* 第一步:生成IP報(bào)頭*/if (pbuf_header(p, IP_HLEN)) {return ERR_BUF;}/* 第二步:iphdr 指向IP頭部指針*/iphdr = (struct ip_hdr * ) p - > payload;/* 設(shè)置生存時(shí)間(最大轉(zhuǎn)發(fā)次數(shù)) */IPH_TTL_SET(iphdr, ttl);/* 設(shè)置協(xié)議類型(IGMP:1、UDP:17、TCP:6) */IPH_PROTO_SET(iphdr, proto);/* 設(shè)置目的IP地址*/ip4_addr_copy(iphdr - > dest, * dest);/* 設(shè)置版本號(hào)+設(shè)置首部長度*/IPH_VHL_SET(iphdr, 4, ip_hlen / 4);/* 服務(wù)類型*/IPH_TOS_SET(iphdr, tos);/* 設(shè)置總長度(IP首部+數(shù)據(jù)區(qū)) */IPH_LEN_SET(iphdr, lwip_htons(p - > tot_len));/* 設(shè)置標(biāo)志+片偏移*/IPH_OFFSET_SET(iphdr, 0);/* 設(shè)置數(shù)據(jù)包標(biāo)識(shí)(編號(hào)) */IPH_ID_SET(iphdr, lwip_htons(ip_id));/* 每發(fā)送一個(gè)數(shù)據(jù)包,編號(hào)加一*/++ip_id;/* 沒有指定源IP地址*/if (src == NULL) {/* 將當(dāng)前網(wǎng)絡(luò)接口IP地址設(shè)置為源IP地址*/ip4_addr_copy(iphdr - > src, * IP4_ADDR_ANY4);} else {/* 復(fù)制源IP地址*/ip4_addr_copy(iphdr - > src, * src);}} else {/* IP頭部已經(jīng)包含在pbuf中*/iphdr = (struct ip_hdr * ) p - > payload;ip4_addr_copy(dest_addr, iphdr - > dest);dest = & dest_addr;}IP_STATS_INC(ip.xmit);ip4_debug_print(p);/* 如果數(shù)據(jù)包總長度大于MTU,則分片發(fā)送*/if (netif - > mtu && (p - > tot_len > netif - > mtu)) {return ip4_frag(p, netif, dest);}/* 如果數(shù)據(jù)包總長度不大于MTU,則直接發(fā)送*/return netif - > output(netif, p, dest);
}

此函數(shù)非常簡單,這里筆者使用一個(gè)流程圖來描述該函數(shù)的實(shí)現(xiàn)原理,如下圖所示:

在這里插入圖片描述

此函數(shù)首先判斷目標(biāo)IP 地址是否為NULL,若目標(biāo)IP 地址不為空,則偏移payload 指針
添加IP 首部,偏移完成之后設(shè)置IP 首部字段信息,接著判斷該數(shù)據(jù)包的總長度是否大于以太網(wǎng)傳輸單元,若大于,則調(diào)用ip4_frag 函數(shù)對(duì)這個(gè)數(shù)據(jù)包分組并且逐一發(fā)送,否則直接調(diào)用ethrap_output 函數(shù)把數(shù)據(jù)包遞交給ARP 層處理。

IP 數(shù)據(jù)報(bào)的輸入

數(shù)據(jù)包提交給網(wǎng)絡(luò)層之前,系統(tǒng)需要判斷接收到的數(shù)據(jù)包是IP 數(shù)據(jù)包還是ARP 數(shù)據(jù)包,
若接收到的是IP 數(shù)據(jù)包,則lwIP 內(nèi)核調(diào)用ip4_input 函數(shù)處理這個(gè)數(shù)據(jù)包,該函數(shù)如下所示:

err_t
ip4_input(struct pbuf * p, struct netif * inp) {struct ip_hdr * iphdr;struct netif * netif;u16_t iphdr_hlen;u16_t iphdr_len;#if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMPint check_ip_src = 1;#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */IP_STATS_INC(ip.recv);MIB2_STATS_INC(mib2.ipinreceives);/* 識(shí)別IP報(bào)頭*/iphdr = (struct ip_hdr * ) p - > payload;/* 第一步:判斷版本是否為IPv4 */if (IPH_V(iphdr) != 4) {ip4_debug_print(p);pbuf_free(p); /* 釋放空間*/IP_STATS_INC(ip.err);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinhdrerrors);return ERR_OK;}/* 以4字節(jié)(32位)字段獲得IP頭的長度*/iphdr_hlen = IPH_HL(iphdr);/* 以字節(jié)計(jì)算IP報(bào)頭長度*/iphdr_hlen *= 4;/* 以字節(jié)為單位獲取ip長度*/iphdr_len = lwip_ntohs(IPH_LEN(iphdr));/* 修剪pbuf。這對(duì)于< 60字節(jié)的數(shù)據(jù)包尤其需要。*/if (iphdr_len < p - > tot_len) {pbuf_realloc(p, iphdr_len);}/* 第二步:標(biāo)頭長度超過第一個(gè)pbuf 長度,或者ip 長度超過總pbuf 長度*/if ((iphdr_hlen > p - > len) || (iphdr_len > p - > tot_len) || (iphdr_hlen < IP_HLEN)) {if (iphdr_hlen < IP_HLEN) {}if (iphdr_hlen > p - > len) {}if (iphdr_len > p - > tot_len) {}/* 釋放空間*/pbuf_free(p);IP_STATS_INC(ip.lenerr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipindiscards);return ERR_OK;}/* 第三步:驗(yàn)證校驗(yàn)和*/#if CHECKSUM_CHECK_IP/* 省略代碼*/#endif/* 將源IP 地址與目標(biāo)IP 地址復(fù)制到對(duì)齊的ip_data.current_iphdr_src和ip_data.current_iphdr_dest */ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr - > dest);ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr - > src);/* 第四步:匹配數(shù)據(jù)包和接口,即這個(gè)數(shù)據(jù)包是否發(fā)給本地*/if (ip4_addr_ismulticast(ip4_current_dest_addr())) {#if LWIP_IGMP/* 省略代碼*/#else /* LWIP_IGMP *//* 如果網(wǎng)卡已經(jīng)掛載了和IP 地址有效*/if ((netif_is_up(inp)) && (!ip4_addr_isany_val( * netif_ip4_addr(inp)))) {netif = inp;} else {netif = NULL;}#endif /* LWIP_IGMP */}/* 如果數(shù)據(jù)報(bào)不是發(fā)給本地*/else {int first = 1;netif = inp;do {/* 接口已啟動(dòng)并配置? */if ((netif_is_up(netif)) &&(!ip4_addr_isany_val( * netif_ip4_addr(netif)))) {/* 單播到此接口地址? */if (ip4_addr_cmp(ip4_current_dest_addr(),netif_ip4_addr(netif)) ||/* 或廣播在此接口網(wǎng)絡(luò)地址? */ip4_addr_isbroadcast(ip4_current_dest_addr(), netif)# if LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF || (ip4_addr_get_u32(ip4_current_dest_addr()) ==PP_HTONL(IPADDR_LOOPBACK))# endif /* LWIP_NETIF_LOOPBACK && !LWIP_HAVE_LOOPIF */) {break;}#if LWIP_AUTOIPif (autoip_accept_packet(netif, ip4_current_dest_addr())) {/* 跳出if循環(huán)*/break;}#endif /* LWIP_AUTOIP */}if (first) {#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF/* 檢查一下目標(biāo)IP 地址是否是環(huán)回地址*/if (ip4_addr_isloopback(ip4_current_dest_addr())) {netif = NULL;break;}#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */first = 0;netif = netif_list;} else {netif = netif - > next;}if (netif == inp) {netif = netif - > next;}} while (netif != NULL);}#if IP_ACCEPT_LINK_LAYER_ADDRESSINGif (netif == NULL) {/* 遠(yuǎn)程端口是DHCP服務(wù)器? */if (IPH_PROTO(iphdr) == IP_PROTO_UDP) {struct udp_hdr * udphdr = (struct udp_hdr * )((u8_t * ) iphdr + iphdr_hlen);if (IP_ACCEPT_LINK_LAYER_ADDRESSED_PORT(udphdr - > dest)) {netif = inp;check_ip_src = 0;}}}#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */ #if LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSINGif (check_ip_src#if IP_ACCEPT_LINK_LAYER_ADDRESSING && !ip4_addr_isany_val( * ip4_current_src_addr())# endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING */)# endif /* LWIP_IGMP || IP_ACCEPT_LINK_LAYER_ADDRESSING */ {/* 第五步:IP 地址,源IP 地址不能是多播或者廣播地址*/if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) ||(ip4_addr_ismulticast(ip4_current_src_addr()))) {/* 釋放空間*/pbuf_free(p);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);return ERR_OK;}}/* 第六步:如果還沒找到對(duì)應(yīng)的網(wǎng)卡,數(shù)據(jù)包不是給我們的*/if (netif == NULL) {/* 路由轉(zhuǎn)發(fā)或者丟棄。如果IP_FORWARD 宏定義被使能,則進(jìn)行轉(zhuǎn)發(fā)*/#if IP_FORWARD/* 非廣播包?*/if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), inp)) {/* 嘗試在(其他)網(wǎng)卡上轉(zhuǎn)發(fā)IP 數(shù)據(jù)包*/ip4_forward(p, iphdr, inp);} else# endif /* IP_FORWARD */ {IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);}/* 釋放空間*/pbuf_free(p);return ERR_OK;}/* 第七步:如果數(shù)據(jù)報(bào)由多個(gè)片段組成(分片處理)?*/if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0) {/* 重裝數(shù)據(jù)報(bào)*/p = ip4_reass(p);/* 如果重裝沒有完成*/if (p == NULL) {return ERR_OK;}/* 分片重裝完成,將數(shù)據(jù)報(bào)首部強(qiáng)制轉(zhuǎn)換為ip_hdr 類型*/iphdr = (struct ip_hdr * ) p - > payload;}#if IP_OPTIONS_ALLOWED == 0#if LWIP_IGMPif ((iphdr_hlen > IP_HLEN) && (IPH_PROTO(iphdr) != IP_PROTO_IGMP)) {#else/* 第八步:如果IP 數(shù)據(jù)報(bào)首部長度大于20 字節(jié),就表示錯(cuò)誤*/if (iphdr_hlen > IP_HLEN) {#endif /* LWIP_IGMP *//* 釋放空間*/pbuf_free(p);IP_STATS_INC(ip.opterr);IP_STATS_INC(ip.drop);/* u不受支持的協(xié)議特性*/MIB2_STATS_INC(mib2.ipinunknownprotos);return ERR_OK;}#endif /* IP_OPTIONS_ALLOWED == 0 *//* 第九步:發(fā)送到上層協(xié)議*/ip4_debug_print(p);ip_data.current_netif = netif;ip_data.current_input_netif = inp;ip_data.current_ip4_header = iphdr;ip_data.current_ip_header_tot_len = IPH_HL(iphdr) * 4;#if LWIP_RAW/* RAW API 輸入*/if (raw_input(p, inp) == 0)# endif /* LWIP_RAW */ {/* 轉(zhuǎn)移到有效載荷(數(shù)據(jù)區(qū)域),不需要檢查*/pbuf_header(p, -(s16_t) iphdr_hlen);/* 根據(jù)IP 數(shù)據(jù)報(bào)首部的協(xié)議的類型處理*/switch (IPH_PROTO(iphdr)) {#if LWIP_UDP/* UDP協(xié)議*/case IP_PROTO_UDP:#if LWIP_UDPLITEcase IP_PROTO_UDPLITE:#endif /* LWIP_UDPLITE */MIB2_STATS_INC(mib2.ipindelivers);/* IP層遞交給網(wǎng)絡(luò)層的函數(shù)*/udp_input(p, inp);break;#endif /* LWIP_UDP */ #if LWIP_TCP/* TCP協(xié)議*/case IP_PROTO_TCP:MIB2_STATS_INC(mib2.ipindelivers);/* IP層遞交給網(wǎng)絡(luò)層的函數(shù)*/tcp_input(p, inp);break;#endif /* LWIP_TCP */pbuf_free(p); /* 釋放空間*/IP_STATS_INC(ip.proterr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinunknownprotos);}}/* 全局變量清零*/ip_data.current_netif = NULL;ip_data.current_input_netif = NULL;ip_data.current_ip4_header = NULL;ip_data.current_ip_header_tot_len = 0;ip4_addr_set_any(ip4_current_src_addr());ip4_addr_set_any(ip4_current_dest_addr());return ERR_OK;}

上述的源碼篇幅很長,也不容易理解,下面筆者把上述的源碼分成十步來講解:
第一步:判斷IP 數(shù)據(jù)報(bào)的版本是否是IPv4,如果不是,那么lwIP 會(huì)掉棄該數(shù)據(jù)報(bào)。
第二步:判斷標(biāo)頭長度超過第一個(gè)pbuf 長度,或者ip 長度超過總pbuf 長度,如果是,那
么lwIP 會(huì)丟棄該數(shù)據(jù)報(bào)。
第三步:驗(yàn)證校驗(yàn)和,如果不正確,那么lwIP 會(huì)掉棄該數(shù)據(jù)報(bào)。
第四步:匹配數(shù)據(jù)包和接口,這個(gè)數(shù)據(jù)包是否發(fā)給本地。
第五步:判斷IP 數(shù)據(jù)報(bào)是否是廣播或者多播,如果是,那么lwIP 會(huì)丟棄該數(shù)據(jù)報(bào)。
第六步:如果到了這一步,沒有發(fā)現(xiàn)網(wǎng)絡(luò)接口,那么lwIP 會(huì)丟棄該數(shù)據(jù)報(bào)。
第七步:如果如IP 數(shù)據(jù)報(bào)不能分片處理,那么lwIP 會(huì)丟棄該數(shù)據(jù)報(bào)。
第八步:如果IP 數(shù)據(jù)報(bào)的IP 首部大于20 字節(jié),那么lwIP 會(huì)丟棄該數(shù)據(jù)報(bào)。
第九步:把數(shù)據(jù)包遞交給上層。
第十步:判斷該數(shù)據(jù)報(bào)的協(xié)議為TCP/UDP/ICMP/IGMP,如果不是這四個(gè)協(xié)議,則丟棄該
數(shù)據(jù)報(bào)。

ICMP 協(xié)議

ICMP(Internet Control Message Protocol)Internet 控制報(bào)文協(xié)議。它是TCP/IP 協(xié)議簇的
一個(gè)子協(xié)議,用于在IP 主機(jī)、路由器之間傳遞控制消息??刂葡⑹侵妇W(wǎng)絡(luò)通不通、主機(jī)是
否可達(dá)、路由是否可用等網(wǎng)絡(luò)本身的消息,這些控制消息雖然并不傳輸?shù)接脩魯?shù)據(jù),但是對(duì)于用戶數(shù)據(jù)的傳遞起著重要的作用。

ICMP 協(xié)議簡介

IP 協(xié)議雖然是TCP/IP 協(xié)議中的核心部分,但是它是一種無連接的不可靠數(shù)據(jù)報(bào)交付,這
個(gè)協(xié)議本身沒有任何錯(cuò)誤檢驗(yàn)和恢復(fù)機(jī)制,為了彌補(bǔ)IP 協(xié)議中的缺陷,ICMP 協(xié)議登場(chǎng)了,
ICMP 協(xié)議是一種面向無連接的協(xié)議,用于傳輸出錯(cuò)報(bào)告控制信息。它是一個(gè)非常重要的協(xié)議,它對(duì)于網(wǎng)絡(luò)安全具有極其重要的意義。

它屬于網(wǎng)絡(luò)層協(xié)議,主要用于在主機(jī)與路由器之間傳遞控制信息,包括報(bào)告錯(cuò)誤、交換受限控制和狀態(tài)信息等。當(dāng)遇到IP 數(shù)據(jù)無法訪問目標(biāo)、IP 路由器無法按當(dāng)前的傳輸速率轉(zhuǎn)發(fā)數(shù)據(jù)包等情況時(shí),會(huì)自動(dòng)發(fā)送ICMP 消息。

ICMP 協(xié)議用于IP 主機(jī)、路由器之間遞交控制消息,在網(wǎng)絡(luò)中,控制消息分為很多種,例
如數(shù)據(jù)報(bào)錯(cuò)信息、網(wǎng)絡(luò)狀況信息和主句狀況信息等,雖然這些信息不會(huì)遞交給用戶數(shù)據(jù),但對(duì)于用戶來說數(shù)據(jù)報(bào)有效性得到提高。

ICMP 應(yīng)用場(chǎng)景

IP 協(xié)議本身不提供差錯(cuò)報(bào)告和差錯(cuò)控制機(jī)制來保證數(shù)據(jù)報(bào)遞交的有效性,如果在路由器
無法遞交一個(gè)數(shù)據(jù)報(bào)或者數(shù)據(jù)報(bào)生存時(shí)間為0 時(shí),那么路由器會(huì)直接掉棄這個(gè)數(shù)據(jù)報(bào),雖然
IP 層這樣處理是合理的,但是對(duì)于源主機(jī)來說,比較希望得到數(shù)據(jù)報(bào)遞交過程中出現(xiàn)異常相
關(guān)信息,以便重新遞交數(shù)據(jù)報(bào)或者其他處理。

IP 協(xié)議不能進(jìn)行主機(jī)管理與查詢機(jī)制,簡單來說:不知道對(duì)方主機(jī)或者路由器的活躍,
對(duì)于不活躍的主機(jī)和路由器就沒有必要發(fā)送數(shù)據(jù)報(bào),所以對(duì)于主機(jī)管理員來說:更希望得到對(duì)方主機(jī)和路由器的信息,這樣可以根據(jù)相關(guān)的信息對(duì)自身配置、數(shù)據(jù)報(bào)發(fā)送控制。

為了解決上述的兩個(gè)問題,TCP/IP 設(shè)計(jì)人員在協(xié)議上引入了特殊用途報(bào)文,這個(gè)報(bào)文為
網(wǎng)際報(bào)文控制協(xié)議簡稱ICMP,從TCP/IP 的協(xié)議結(jié)構(gòu)來看,它是和IP’協(xié)議一樣,都是處于網(wǎng)
絡(luò)層,但是ICMP 協(xié)議有自己一套報(bào)文結(jié)構(gòu),這樣數(shù)據(jù)報(bào)就變成了IP 首部+ICMP 首部+數(shù)據(jù)
區(qū)域,ICMP 協(xié)議不為任何的應(yīng)用程序服務(wù),它的目的是目的主機(jī)的網(wǎng)絡(luò)層處理軟件。

ICMP 報(bào)文類型

在沒有引入ICMP 報(bào)文之前,IP 數(shù)據(jù)報(bào)一般分為IP 首部+IP 數(shù)據(jù)區(qū)域,現(xiàn)在添加了ICMP
協(xié)議,則IP 數(shù)據(jù)報(bào)分為IP 首部+ICMP 首部+數(shù)據(jù)區(qū)域。ICMP 報(bào)文分為兩類:一類是ICMP 差錯(cuò)報(bào)告報(bào)文,另一類是ICMP 查詢報(bào)文,這兩類報(bào)文分別解決上小節(jié)的兩個(gè)問題。

①ICMP 差錯(cuò)報(bào)告報(bào)文主要用來向IP 數(shù)據(jù)報(bào)源主機(jī)返回一個(gè)差錯(cuò)報(bào)告信息,這個(gè)信息就
是判斷路由器和主機(jī)對(duì)當(dāng)前的數(shù)據(jù)報(bào)進(jìn)行正常處理,例如無法將數(shù)據(jù)報(bào)遞交給上層處理,或者數(shù)據(jù)報(bào)因?yàn)樯鏁r(shí)間而被刪除。

②ICMP 查詢報(bào)文用于一臺(tái)主機(jī)向另一臺(tái)主機(jī)查詢特定的信息,這個(gè)類型的報(bào)文是成對(duì)出
現(xiàn)的,例如源主機(jī)發(fā)送查詢報(bào)文,當(dāng)目標(biāo)主機(jī)收到該報(bào)文之后,它會(huì)根據(jù)查詢報(bào)文的約定的格式為源主機(jī)放回應(yīng)答報(bào)文。

ICMP 差錯(cuò)報(bào)告報(bào)文和ICMP 查詢報(bào)文常見類型如下表所示:

在這里插入圖片描述
注:lwIP 只實(shí)現(xiàn)差錯(cuò)報(bào)文的類型3 和11,而查詢報(bào)文只處理回顯請(qǐng)求。

ICMP 報(bào)文結(jié)構(gòu)

ICMP 報(bào)文有8 字節(jié)首部和可變長度的數(shù)據(jù)部分組成,因?yàn)镮CMP 有兩種類型的報(bào)文,其
中不同的報(bào)文其首部的格式也會(huì)有點(diǎn)差異,當(dāng)然也有通用的地方,例如首部的前4 個(gè)字節(jié)是通用的,ICMP 報(bào)文結(jié)構(gòu)如下圖所示:

在這里插入圖片描述

類型字段:表示使用ICMP 的兩類類型中的哪一個(gè)。
代碼字段:產(chǎn)生ICMP 報(bào)文的具體原因。
校驗(yàn)和字段:用于記錄包括ICMP 報(bào)文數(shù)據(jù)部分在內(nèi)的整個(gè)ICMP 數(shù)據(jù)報(bào)的校驗(yàn)和。
首部剩余的4 字節(jié)在每種類型的報(bào)文有特殊的定義,總的看來說:不同類型的報(bào)文,數(shù)據(jù)
部分長度和含義存在差異,例如差錯(cuò)報(bào)文會(huì)引起差錯(cuò)的據(jù)報(bào)的信息,而查詢報(bào)文攜帶查詢請(qǐng)求和查詢結(jié)果數(shù)據(jù)。

  1. ICMP 差錯(cuò)報(bào)文
    (1) 目的站不可到達(dá)
    當(dāng)路由器發(fā)送的數(shù)據(jù)報(bào)不能發(fā)送到指定目的地時(shí),或者說當(dāng)路由器不能夠給數(shù)據(jù)報(bào)找到路由或主機(jī)不能夠交付數(shù)據(jù)報(bào)時(shí),就丟棄這個(gè)數(shù)據(jù)報(bào),然后向發(fā)送數(shù)據(jù)報(bào)的源主機(jī)設(shè)備發(fā)回一個(gè)終點(diǎn)不可達(dá)數(shù)據(jù)報(bào)文。如下圖所示:

在這里插入圖片描述

舉個(gè)例子:主機(jī)A 給主機(jī)B 發(fā)送一個(gè)數(shù)據(jù)報(bào),在網(wǎng)絡(luò)中傳輸時(shí)中間可能要經(jīng)過很多臺(tái)路
由器,主機(jī)A 先把這個(gè)數(shù)據(jù)報(bào)發(fā)送給路由器,路由器收到這個(gè)數(shù)據(jù)報(bào)后,此時(shí)路由R1 發(fā)生了故障,它不知道這個(gè)數(shù)據(jù)報(bào)下一步該發(fā)給哪個(gè)路由設(shè)備或者那臺(tái)主機(jī)設(shè)備,也就是說這個(gè)數(shù)據(jù)報(bào)不能發(fā)送到目的地主機(jī)B,這時(shí)路由器會(huì)把這個(gè)數(shù)據(jù)報(bào)丟棄并向主機(jī)A 發(fā)回一個(gè)終點(diǎn)不可達(dá)的數(shù)據(jù)報(bào)文。

ICMP 目的不可達(dá)差錯(cuò)報(bào)告報(bào)文產(chǎn)生差錯(cuò)的原因有很多,如網(wǎng)絡(luò)不可達(dá)、主機(jī)不可達(dá)、協(xié)
議不可達(dá)、端口不可達(dá)等,引起差錯(cuò)的原因會(huì)在ICMP 報(bào)文中的代碼字段(Code)記錄。對(duì)
于不同的差錯(cuò)代碼字段的值是不一樣的,但是lwIP 實(shí)現(xiàn)的只有前6 種,如下圖所示:

在這里插入圖片描述

當(dāng)然ICMP 目的不可達(dá)報(bào)文首部剩下的4 字節(jié)是未使用,而ICMP 報(bào)文數(shù)據(jù)區(qū)裝載了IP
數(shù)據(jù)報(bào)首部及IP 數(shù)據(jù)報(bào)的數(shù)據(jù)區(qū)域前8 字節(jié),為什么需要裝載IP 數(shù)據(jù)報(bào)的數(shù)據(jù)區(qū)域中前8 個(gè)
字節(jié)的數(shù)據(jù)呢?因?yàn)镮P 數(shù)據(jù)報(bào)的數(shù)據(jù)區(qū)域前8 個(gè)字節(jié)剛好覆蓋了傳輸層協(xié)議中的端口號(hào)字段,而IP 數(shù)據(jù)報(bào)首部就擁有目標(biāo)IP 地址與源IP 地址,當(dāng)源主機(jī)收到這樣子的ICMP 報(bào)文后,它能根據(jù)ICMP 報(bào)文的數(shù)據(jù)區(qū)域判斷出是哪個(gè)數(shù)據(jù)包出現(xiàn)問題,并且IP 層能夠根據(jù)端口號(hào)將報(bào)文傳遞給對(duì)應(yīng)的上層協(xié)議處理,差錯(cuò)報(bào)文結(jié)構(gòu)如下圖所示:

在這里插入圖片描述

可以看出:首部剩下的4 個(gè)字節(jié)是未使用的,而數(shù)據(jù)區(qū)域保存的是引起差錯(cuò)IP 首部和引
起差錯(cuò)數(shù)據(jù)包的數(shù)據(jù)區(qū)域前8 字節(jié)數(shù)據(jù)。準(zhǔn)確來說,就是把引起差錯(cuò)IP 數(shù)據(jù)包的IP 部和數(shù)據(jù)
區(qū)域的前8 字節(jié)數(shù)據(jù)拷貝到差錯(cuò)報(bào)文的數(shù)據(jù)區(qū)域。

(2) 源站抑制
由于IP 協(xié)議是面向無連接的,沒有流量控制機(jī)制,數(shù)據(jù)在傳輸過程中是非常容易造成擁
塞的現(xiàn)象。而ICMP 源點(diǎn)抑制報(bào)文就是給IP 協(xié)議提供一種流量監(jiān)控的機(jī)制,因?yàn)镮CMP 源點(diǎn)
抑制機(jī)制并不能控制流量的大小,但是能根據(jù)流量的使用情況,給源主機(jī)提供一些建議。這個(gè)報(bào)文的作用就是通知數(shù)據(jù)報(bào)在擁塞時(shí)被丟棄了,另外還會(huì)警告源主機(jī)流量出現(xiàn)了擁塞的情況,然后源主機(jī)根據(jù)反饋的ICMP 源點(diǎn)抑制報(bào)文信息作出處理,至于源主機(jī)怎么就不關(guān)它的事了。

如下圖所示:

在這里插入圖片描述

(3) 端口不可達(dá)
當(dāng)目標(biāo)系統(tǒng)收到一個(gè)IP 數(shù)據(jù)報(bào)的某個(gè)服務(wù)請(qǐng)求時(shí),如果本地沒有此服務(wù),則本地會(huì)向源
頭返回ICMP 端口不可達(dá)信息。常見的端口不可達(dá)有:主機(jī)A 向主機(jī)B 發(fā)起一個(gè)ftp 的傳輸請(qǐng)
求,從主機(jī)B 傳輸一個(gè)文件到主機(jī)A,由于主機(jī)B 設(shè)備沒有開啟ftp 服務(wù)的69 端口,因此主
機(jī)A 在請(qǐng)求主機(jī)B 時(shí),會(huì)收到主機(jī)B 回復(fù)的一個(gè)ICMP 端口不可達(dá)的差錯(cuò)報(bào)文。

(4) 超時(shí)
ICMP 差錯(cuò)報(bào)告報(bào)文主要在以下幾種情況中,會(huì)發(fā)送ICMP 超時(shí)報(bào)文:

  1. 當(dāng)路由器接收到的數(shù)據(jù)報(bào)的TTL 生命周期字段值為0 時(shí),路由器會(huì)把該數(shù)據(jù)報(bào)丟棄掉,
    并向源主機(jī)發(fā)回一個(gè)ICMP 超時(shí)報(bào)文。
  2. 另外,當(dāng)目標(biāo)主機(jī)在規(guī)定時(shí)間內(nèi)沒有收到所有的數(shù)據(jù)分片時(shí),會(huì)把已經(jīng)收到的所有數(shù)據(jù)
    分片丟棄,并向源主機(jī)發(fā)回一個(gè)ICMP 超時(shí)報(bào)文。在超時(shí)報(bào)文中,代碼0 只能給路由器使用,表示生存周期字段值為0,代碼1 只能給目的主機(jī)使用,它表示在規(guī)定的時(shí)間內(nèi),目的主機(jī)沒有收到所有的數(shù)據(jù)分片。

(5) 參數(shù)錯(cuò)誤
當(dāng)數(shù)據(jù)報(bào)在因特網(wǎng)上傳送時(shí),在其首部中出現(xiàn)的任何二義性或者首部字段值被修改都可能
會(huì)產(chǎn)生非常嚴(yán)重的問題。如果路由器或目的主機(jī)發(fā)現(xiàn)了這種二義性,或在數(shù)據(jù)報(bào)的某個(gè)字段中缺少某個(gè)值,就丟棄這個(gè)數(shù)據(jù)報(bào),并回送參數(shù)問題報(bào)文。

  1. ICMP 查詢報(bào)文
    ping 程序利用ICMP 回顯請(qǐng)求報(bào)文和回顯應(yīng)答報(bào)文(而不用經(jīng)過傳輸層)來測(cè)試目標(biāo)主機(jī)是否可達(dá)。它是一個(gè)檢查系統(tǒng)連接性的基本診斷工具。
    ICMP 回顯請(qǐng)求和ICMP 回顯應(yīng)答報(bào)文是配合工作的。當(dāng)源主機(jī)向目標(biāo)主機(jī)發(fā)送了ICMP
    回顯請(qǐng)求數(shù)據(jù)包后,它期待著目標(biāo)主機(jī)的回答。目標(biāo)主機(jī)在收到一個(gè)ICMP 回顯請(qǐng)求數(shù)據(jù)包后,它會(huì)交換源、目的主機(jī)的地址,然后將收到的ICMP 回顯請(qǐng)求數(shù)據(jù)包中的數(shù)據(jù)部分原封不動(dòng)地封裝在自己的ICMP 回顯應(yīng)答數(shù)據(jù)包中,然后發(fā)回給發(fā)送ICMP 回顯請(qǐng)求的一方。如果校驗(yàn)正確,發(fā)送者便認(rèn)為目標(biāo)主機(jī)的回顯服務(wù)正常,也即物理連接暢通。查詢報(bào)文結(jié)構(gòu)如下圖所示:

在這里插入圖片描述

類型字段是指請(qǐng)求報(bào)文(8)和回答報(bào)文(0),代碼段在ICMP 查詢報(bào)文沒有特殊取值,
其值為0,首部中的標(biāo)識(shí)符和序號(hào)在ICMP 中沒有正式定義該值的范圍,所以發(fā)送方可以自由定義這兩個(gè)字段,可以用來記錄源主機(jī)發(fā)送出去的請(qǐng)求報(bào)文編號(hào)。數(shù)據(jù)可選區(qū)域標(biāo)識(shí)回送請(qǐng)求報(bào)文包含數(shù)據(jù)和長度是可選的,發(fā)送放應(yīng)該選擇適合的長度和填充數(shù)據(jù)。在接收方它可以根據(jù)這個(gè)回送請(qǐng)求產(chǎn)生一個(gè)回送回答報(bào)文,回送報(bào)文的數(shù)據(jù)與回送請(qǐng)求報(bào)文的數(shù)據(jù)是相同的。

ICMP 的實(shí)現(xiàn)

我們可以總結(jié)一下ICMP 協(xié)議的作用,ICMP 協(xié)議是IP 協(xié)議的輔助協(xié)議,為什么ICMP 協(xié)
議是IP 協(xié)議的輔助協(xié)議呢?由于IP 協(xié)議本身不提供差錯(cuò)報(bào)告和差錯(cuò)控制機(jī)制來保證數(shù)據(jù)報(bào)遞交的有效性和進(jìn)行主機(jī)管理與查詢機(jī)制,簡單來說:ICMP 協(xié)議為了解決IP 協(xié)議的缺陷而誕生的,ICMP 報(bào)文分為差錯(cuò)報(bào)文和查詢報(bào)文,這兩個(gè)報(bào)文分別解決IP 協(xié)議的兩大缺陷,本小節(jié)主要講解lwIP 是怎么樣實(shí)現(xiàn)ICMP 協(xié)議發(fā)送及處理的。

ICMP 數(shù)據(jù)結(jié)構(gòu)體

在講述IP 協(xié)議時(shí),它是有自己的數(shù)據(jù)結(jié)構(gòu),同樣ICMP 也有它自己的數(shù)據(jù)結(jié)構(gòu)icmp_echo
_hdr,該數(shù)據(jù)結(jié)構(gòu)在lwIP 的icmp.h 文件中定義,該結(jié)構(gòu)體如下源碼所示:

PACK_STRUCT_BEGIN
struct icmp_echo_hdr {PACK_STRUCT_FLD_8(u8_t type); /* ICMP類型*/PACK_STRUCT_FLD_8(u8_t code); /* ICMP代碼號(hào)*/PACK_STRUCT_FIELD(u16_t chksum); /* ICMP校驗(yàn)和*/PACK_STRUCT_FIELD(u16_t id); /* ICMP的標(biāo)識(shí)符*/PACK_STRUCT_FIELD(u16_t seqno); /* 序號(hào)*/
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

此外lwIP 還定義了很多宏與枚舉類型的變量對(duì)ICMP 的類型及代碼字段進(jìn)行描述,如下
源碼所示:

#define ICMP_ER 0 /* 回送應(yīng)答*/ # define ICMP_DUR 3 /* 目標(biāo)不可達(dá)*/ # define ICMP_SQ 4 /* 源站抑制*/ # define ICMP_RD 5 /* 重定向*/ # define ICMP_ECHO 8 /* 回送*/ # define ICMP_TE 11 /* 超時(shí)*/ # define ICMP_PP 12 /* 參數(shù)問題*/ # define ICMP_TS 13 /* 時(shí)間戳*/ # define ICMP_TSR 14 /* 時(shí)間戳應(yīng)答*/ # define ICMP_IRQ 15 /* 信息請(qǐng)求*/ # define ICMP_IR 16 /* 信息應(yīng)答*/ # define ICMP_AM 17 /* 地址掩碼請(qǐng)求*/ # define ICMP_AMR 18 /* 地址掩碼應(yīng)答*/
/* ICMP目標(biāo)不可到達(dá)的代碼*/
enum icmp_dur_type {/* 網(wǎng)絡(luò)不可到達(dá)*/ICMP_DUR_NET = 0,/* 主機(jī)不可達(dá)*/ICMP_DUR_HOST = 1,/* 協(xié)議不可到達(dá)*/ICMP_DUR_PROTO = 2,/* 端口不可達(dá)*/ICMP_DUR_PORT = 3,/* 需要進(jìn)行分片但設(shè)置不分片比特*/ICMP_DUR_FRAG = 4,/* 源路由失敗*/ICMP_DUR_SR = 5
};
/* ICMP時(shí)間超時(shí)代碼*/
enum icmp_te_type {/* 在運(yùn)輸過程中超出了生存時(shí)間*/ICMP_TE_TTL = 0,/* 分片重組時(shí)間超時(shí)*/ICMP_TE_FRAG = 1
};

可以看出,這些宏定義描述了ICMP 數(shù)據(jù)報(bào)文的類型字段,下面的icmp_dur_type 和
icmp_te_type 枚舉用來描述ICMP 數(shù)據(jù)報(bào)文的代碼字段,它們分別為目的不可到達(dá)和超時(shí)差錯(cuò)報(bào)文。

lwIP 的作者為了快速讀取和填寫ICMP 報(bào)文首部,在icmp.h 文件還定義了ICMP 報(bào)文首
部的宏定義,如下源碼所示:

#define ICMPH_TYPE(hdr) ((hdr)->type) /* 讀取類型字段*/
#define ICMPH_CODE(hdr) ((hdr)->code) /* 讀取代碼字段*/
#define ICMPH_TYPE_SET(hdr, t) ((hdr)->type = (t)) /* 填寫類型字段*/
#define ICMPH_CODE_SET(hdr, c) ((hdr)->code = (c)) /* 填寫代碼字段*/

使用這些宏定義能快速設(shè)置ICMP 各個(gè)字段的數(shù)值。

發(fā)送ICMP 差錯(cuò)報(bào)文

lwIP 只實(shí)現(xiàn)目的不可到達(dá)和超時(shí)差錯(cuò)報(bào)文,它們的實(shí)現(xiàn)函數(shù)分別為icmp_dest_unreach 和i
cmp_time_exceeded,這兩個(gè)函數(shù)轉(zhuǎn)入的參數(shù)與icmp_dur_type 和icmp_te_type 枚舉相關(guān)。如目的不可到達(dá)報(bào)文的代碼字段由icmp_dur_type 枚舉描述,而超時(shí)報(bào)文的代碼字段由icmp_te_type 枚舉描述。

打開icmp.c 文件查看icmp_dest_unreach 和icmp_time_exceeded 這兩
個(gè)函數(shù),如下所示:

/* 發(fā)送目標(biāo)不可達(dá)報(bào)文,該函數(shù)實(shí)際調(diào)用函數(shù)
icmp_send_response來發(fā)送ICMP差錯(cuò)報(bào)文
ICMP_DUR 為目的不可到達(dá)*/
void
icmp_dest_unreach(struct pbuf * p, enum icmp_dur_type t) {MIB2_STATS_INC(mib2.icmpoutdestunreachs);icmp_send_response(p, ICMP_DUR, t);}/* 發(fā)送超時(shí)報(bào)文,該函數(shù)實(shí)際調(diào)用函數(shù)icmp_send_response來發(fā)送ICMP差錯(cuò)報(bào)文ICMP_TE 為超時(shí)*/
void
icmp_time_exceeded(struct p buf * p, enum icmp_te_type t) {MIB2_STATS_INC(mib2.icmpouttimeexcds);icmp_send_response(p, ICMP_TE, t);
}

從上述源碼可以看出,差錯(cuò)報(bào)文的類型已經(jīng)固定為目的不可到達(dá)或者超時(shí),它們唯一不同
的是差錯(cuò)報(bào)文的代碼值,這個(gè)代碼值就是由icmp_dur_type 和icmp_te_type 枚舉定義的,最后調(diào)用相同的icmp_send_response 函數(shù)發(fā)送差錯(cuò)報(bào)文,這個(gè)發(fā)送函數(shù)如下所示:

static void
icmp_send_response(struct pbuf * p, u8_t type, u8_t code) {struct pbuf * q;struct ip_hdr * iphdr;struct icmp_echo_hdr * icmphdr;ip4_addr_t iphdr_src;struct netif * netif;MIB2_STATS_INC(mib2.icmpoutmsgs);/* 為差錯(cuò)報(bào)文申請(qǐng)pbuf,pbuf預(yù)留以太網(wǎng)首部和ip首部,申請(qǐng)數(shù)據(jù)長度為icmp首部長度+icmp數(shù)據(jù)長度(ip首部長度+8) */q = pbuf_alloc(PBUF_IP, sizeof(struct icmp_echo_hdr) + IP_HLEN +ICMP_DEST_UNREACH_DATASIZE, PBUF_RAM);if (q == NULL) {MIB2_STATS_INC(mib2.icmpouterrors);return;}/* 指向IP 數(shù)據(jù)報(bào)首部*/iphdr = (struct ip_hdr * ) p - > payload;/* 指向帶填寫的icmp首部*/icmphdr = (struct icmp_echo_hdr * ) q - > payload;/* 填寫類型字段*/icmphdr - > type = type;/* 填寫代碼字段*/icmphdr - > code = code;icmphdr - > id = 0;icmphdr - > seqno = 0;/* 從原始數(shù)據(jù)包中復(fù)制字段,IP 數(shù)據(jù)報(bào)首部+8 字節(jié)的數(shù)據(jù)區(qū)域*/SMEMCPY((u8_t * ) q - > payload + sizeof(struct icmp_echo_hdr), (u8_t * ) p - > payload,IP_HLEN + ICMP_DEST_UNREACH_DATASIZE);/* 得到源IP 地址*/ip4_addr_copy(iphdr_src, iphdr - > src);/* 判斷是否同一網(wǎng)段*/netif = ip4_route( & iphdr_src);if (netif != NULL) {/* 計(jì)算校驗(yàn)和*/icmphdr - > chksum = 0;ICMP_STATS_INC(icmp.xmit);/* 發(fā)送ICMP差錯(cuò)報(bào)文*/ip4_output_if(q, NULL, & iphdr_src, ICMP_TTL, 0, IP_PROTO_ICMP, netif);}/* 釋放icmp pbuf */pbuf_free(q);
}

可以看到,此函數(shù)申請(qǐng)了一個(gè)pbuf 內(nèi)存,它的數(shù)據(jù)區(qū)域存儲(chǔ)了ICMP 首部,接著對(duì)這個(gè)
首部各個(gè)字段設(shè)置數(shù)值,然后在ICMP 首部后面添加引起差錯(cuò)數(shù)據(jù)包的IP 首部和引起差錯(cuò)的
前8 字節(jié)數(shù)據(jù)區(qū)域,這樣lwIP 內(nèi)核構(gòu)建差錯(cuò)報(bào)文完成,最后調(diào)用ip4_output_if 函數(shù)發(fā)送該差
錯(cuò)報(bào)文。

ICMP 報(bào)文處理

IP 層把數(shù)據(jù)報(bào)遞交至傳輸層之前,lwIP 內(nèi)核會(huì)判斷IP 首部的上層協(xié)議字段,若這個(gè)上層
協(xié)議字段不為TCP 和UDP,則該數(shù)據(jù)報(bào)不會(huì)遞交給傳輸層處理;若上層協(xié)議字段為ICMP,
則該數(shù)據(jù)報(bào)遞交給icmp_input 函數(shù)處理,該函數(shù)如下所示:

void
icmp_input(struct pbuf * p, struct netif * inp) {u8_t type;struct icmp_echo_hdr * iecho;const struct ip_hdr * iphdr_in;u16_t hlen;const ip4_addr_t * src;ICMP_STATS_INC(icmp.recv);MIB2_STATS_INC(mib2.icmpinmsgs);iphdr_in = ip4_current_header();hlen = IPH_HL_BYTES(iphdr_in);/* 判斷IP首部的大小*/if (hlen < IP_HLEN) {goto lenerr;}/* 判斷pbud的大小*/if (p - > len < sizeof(u16_t) * 2) {goto lenerr;}/* 獲取ICMP的類型字段*/type = * ((u8_t * ) p - > payload);switch (type) {case ICMP_ER:/* 回送應(yīng)答*/MIB2_STATS_INC(mib2.icmpinechoreps);break;case ICMP_ECHO:/* 回送*/MIB2_STATS_INC(mib2.icmpinechos);src = ip4_current_dest_addr();/* 判斷是否為多播*/if (ip4_addr_ismulticast(ip4_current_dest_addr())) {goto icmperr;}/* 判斷是否為廣播*/if (ip4_addr_isbroadcast(ip4_current_dest_addr(),ip_current_netif())) {goto icmperr;}if (p - > tot_len < sizeof(struct icmp_echo_hdr)) {goto lenerr;}if (pbuf_header(p, (s16_t)(hlen + PBUF_LINK_HLEN +PBUF_LINK_ENCAPSULATION_HLEN))) {struct pbuf * r;r = pbuf_alloc(PBUF_LINK, p - > tot_len + hlen, PBUF_RAM);if (r == NULL) {goto icmperr;}if (r - > len < hlen + sizeof(struct icmp_echo_hdr)) {pbuf_free(r);goto icmperr;}MEMCPY(r - > payload, iphdr_in, hlen);if (pbuf_header(r, (s16_t) - hlen)) {pbuf_free(r);goto icmperr;}if (pbuf_copy(r, p) != ERR_OK) {pbuf_free(r);goto icmperr;}pbuf_free(p);p = r;} else {if (pbuf_header(p, -(s16_t)(hlen + PBUF_LINK_HLEN +PBUF_LINK_ENCAPSULATION_HLEN))) {goto icmperr;}}/* 強(qiáng)制將數(shù)據(jù)區(qū)域轉(zhuǎn)換為ICMP 報(bào)文首部*/iecho = (struct icmp_echo_hdr * ) p - > payload;if (pbuf_header(p, (s16_t) hlen)) {} else {err_t ret;struct ip_hdr * iphdr = (struct ip_hdr * ) p - > payload;/* 拷貝源IP 地址*/ip4_addr_copy(iphdr - > src, * src);/* 拷貝目標(biāo)IP 地址*/ip4_addr_copy(iphdr - > dest, * ip4_current_src_addr());/* 填寫報(bào)文類型*/ICMPH_TYPE_SET(iecho, ICMP_ER);iecho - > chksum = 0;/* 設(shè)置正確的TTL并重新計(jì)算頭校驗(yàn)和。*/IPH_TTL_SET(iphdr, ICMP_TTL);IPH_CHKSUM_SET(iphdr, 0);ICMP_STATS_INC(icmp.xmit);MIB2_STATS_INC(mib2.icmpoutmsgs);MIB2_STATS_INC(mib2.icmpoutechoreps);/* 發(fā)送一個(gè)應(yīng)答ICMP數(shù)據(jù)包*/ret = ip4_output_if(p, src, LWIP_IP_HDRINCL,ICMP_TTL, 0, IP_PROTO_ICMP, inp);if (ret != ERR_OK) {}}break;default:/* 對(duì)于其他類型的報(bào)文,直接丟掉*/if (type == ICMP_DUR) {MIB2_STATS_INC(mib2.icmpindestunreachs);} else if (type == ICMP_TE) {MIB2_STATS_INC(mib2.icmpintimeexcds);} else if (type == ICMP_PP) {MIB2_STATS_INC(mib2.icmpinparmprobs);} else if (type == ICMP_SQ) {MIB2_STATS_INC(mib2.icmpinsrcquenchs);} else if (type == ICMP_RD) {MIB2_STATS_INC(mib2.icmpinredirects);} else if (type == ICMP_TS) {MIB2_STATS_INC(mib2.icmpintimestamps);} else if (type == ICMP_TSR) {MIB2_STATS_INC(mib2.icmpintimestampreps);} else if (type == ICMP_AM) {MIB2_STATS_INC(mib2.icmpinaddrmasks);} else if (type == ICMP_AMR) {MIB2_STATS_INC(mib2.icmpinaddrmaskreps);}ICMP_STATS_INC(icmp.proterr);ICMP_STATS_INC(icmp.drop);}pbuf_free(p);return;lenerr:pbuf_free(p);ICMP_STATS_INC(icmp.lenerr);MIB2_STATS_INC(mib2.icmpinerrors);return;icmperr:pbuf_free(p);ICMP_STATS_INC(icmp.err);MIB2_STATS_INC(mib2.icmpinerrors);return;
}

可以看出,lwIP 接收到回顯請(qǐng)求報(bào)文時(shí),系統(tǒng)會(huì)把這個(gè)回顯請(qǐng)求報(bào)文的ICMP 類型字段修
改為0(回顯應(yīng)答類型),接著偏移payload 指針添加IP 首部并設(shè)置IP 首部的各個(gè)字段,最后調(diào)用ip4_output_if 函數(shù)發(fā)送這個(gè)回顯應(yīng)答報(bào)文。注:lwIP 只處理回顯請(qǐng)求報(bào)文,而其他類型的請(qǐng)求報(bào)文一律不處理。

RAW 編程接口TCP 客戶端實(shí)驗(yàn)

本章,我們學(xué)習(xí)傳輸層的另一個(gè)協(xié)議,它是TCP 協(xié)議,TCP 協(xié)議對(duì)于UDP 協(xié)議來說,可
能有點(diǎn)晦澀難懂,讀者可以參考相關(guān)網(wǎng)絡(luò)書籍,來學(xué)習(xí)TCP 協(xié)議。

TCP 協(xié)議

TCP 協(xié)議簡介

TCP(Transmission Control Protocol 傳輸控制協(xié)議)是一種面向連接的、可靠的、基于字
節(jié)流的傳輸層通信協(xié)議。

TCP 為了保證數(shù)據(jù)包傳輸?shù)目煽啃?#xff0c;會(huì)給每個(gè)包一個(gè)序號(hào),同時(shí)此序號(hào)也保證了發(fā)送到
接收端主機(jī)能夠按序接收。然后接收端主機(jī)對(duì)成功接收到的數(shù)據(jù)包發(fā)回一個(gè)相應(yīng)的確認(rèn)字符
(ACK,Acknowledgement),如果發(fā)送端主機(jī)在合理的往返時(shí)延(RTT)內(nèi)未收到確認(rèn)字符
ACK,那么對(duì)應(yīng)的數(shù)據(jù)包就被認(rèn)為丟失并將被重傳。TCP 協(xié)議,它是基于連接的一種傳輸層
協(xié)議,在發(fā)送數(shù)據(jù)之前要求系統(tǒng)需要在不可靠的信道上建立可靠連接,我們稱之為“三次握
手”。建立連接完成之后客戶端與服務(wù)器才能互發(fā)數(shù)據(jù),不需要發(fā)送數(shù)據(jù)時(shí),可以可以斷開連
接,這里我們稱之為“四次揮手”。下面筆者帶大家了解一下TCP 協(xié)議建立連接的過程和斷開
連接的過程,即三次握手和四次揮手的過程。

TCP 的建立連接

握手之前主動(dòng)打開連接的客戶端結(jié)束CLOSED 階段,被動(dòng)打開的服務(wù)器端也結(jié)束
CLOSED 階段,并進(jìn)入LISTEN 階段。隨后開始“三次握手”:
①TCP 服務(wù)器進(jìn)程先創(chuàng)建傳輸控制塊TCB,時(shí)刻準(zhǔn)備接受客戶進(jìn)程的連接請(qǐng)求,此時(shí)服
務(wù)器就進(jìn)入了LISTEN(監(jiān)聽)狀態(tài)。
②TCP 客戶進(jìn)程也是先創(chuàng)建傳輸控制塊TCB,然后向服務(wù)器發(fā)出連接請(qǐng)求報(bào)文,這是報(bào)
文首部中的同部位SYN=1,同時(shí)選擇一個(gè)初始序列號(hào)seq=x ,此時(shí),TCP 客戶端進(jìn)程進(jìn)入了
SYN-SENT(同步已發(fā)送狀態(tài))狀態(tài)。TCP 規(guī)定,SYN 報(bào)文段(SYN=1 的報(bào)文段)不能攜帶
數(shù)據(jù),但需要消耗掉一個(gè)序號(hào)。
③TCP 服務(wù)器收到請(qǐng)求報(bào)文后,如果同意連接,則發(fā)出確認(rèn)報(bào)文。確認(rèn)報(bào)文中應(yīng)該
ACK=1,SYN=1,確認(rèn)號(hào)是ack=x+1,同時(shí)也要為自己初始化一個(gè)序列號(hào)seq=y,此時(shí),TCP
服務(wù)器進(jìn)程進(jìn)入了SYN-RCVD(同步收到)狀態(tài)。這個(gè)報(bào)文也不能攜帶數(shù)據(jù),但是同樣要消
耗一個(gè)序號(hào)。
④TCP 客戶進(jìn)程收到確認(rèn)后,還要向服務(wù)器給出確認(rèn)。確認(rèn)報(bào)文的ACK=1,ack=y+1,
自己的序列號(hào)seq=x+1,此時(shí),TCP 連接建立,客戶端進(jìn)入ESTABLISHED(已建立連接)狀
態(tài)。TCP 規(guī)定,ACK 報(bào)文段可以攜帶數(shù)據(jù),但是如果不攜帶數(shù)據(jù)則不消耗序號(hào)。
當(dāng)服務(wù)器收到客戶端的確認(rèn)后也進(jìn)入ESTABLISHED 狀態(tài),此后雙方就可以開始通信了。
這就是“三次握手”的過程,如下圖所示。

在這里插入圖片描述

TCP 終止連接

建立一個(gè)連接需要三次握手而終止一個(gè)連接需要四次揮手,終止連接有以下過程。
(1) 第一次揮手:客戶端發(fā)送釋放報(bào)文,并停止發(fā)送數(shù)據(jù)。釋放數(shù)據(jù)報(bào)文首部,FIN=1,其
序列號(hào)為seq=u,此時(shí),客戶端進(jìn)入FIN-WAIT1(等待服務(wù)器應(yīng)答FIN 報(bào)文)。
(2) 第二次揮手:服務(wù)器收到客戶端的FIN 報(bào)文后,發(fā)出確認(rèn)報(bào)文ACK=1、ack=u+1,并
攜帶自己的序列號(hào)seq=v。此時(shí),服務(wù)器進(jìn)入CLOSE-WAIT(關(guān)閉等待)狀態(tài)??蛻舳耸盏椒?br /> 務(wù)端確認(rèn)請(qǐng)求,此時(shí),客戶端進(jìn)入FIN-WAIT2(終止等待2)狀態(tài),等待服務(wù)器發(fā)送連接釋放
報(bào)文。
(3) 第三次揮手:服務(wù)器向客戶端發(fā)送連接釋放報(bào)文FIN=1、ack=u+1,此時(shí),服務(wù)器進(jìn)入
了LAST-ACK(最后確認(rèn))等待客戶端的確認(rèn)。客戶端接收到服務(wù)器的連接釋放報(bào)文后,必
須發(fā)送確認(rèn)ack=1、ack=w+1,客戶端的序列號(hào)為seq=u+1,此時(shí),客戶端進(jìn)入TIME-WAIT(時(shí)
間等待)。
(4) 第四次揮手:服務(wù)器接收到客戶端的確認(rèn)報(bào)文,立刻進(jìn)入CLOSED 狀態(tài)。
這四次揮手就是終止TCP 協(xié)議連接,如下圖所示:

在這里插入圖片描述

上圖的終止連接由客戶端發(fā)起,當(dāng)然服務(wù)器也可以發(fā)起終止連接。

TCP 報(bào)文結(jié)構(gòu)

在傳輸層中,TCP 的數(shù)據(jù)包稱為數(shù)據(jù)段,TCP 報(bào)文段與UDP 報(bào)文段一樣都是封裝在IP 數(shù)
據(jù)報(bào)中發(fā)送。TCP 首部包含建立與斷開、數(shù)據(jù)確認(rèn)、窗口大小通告、數(shù)據(jù)發(fā)送相關(guān)的所有標(biāo)
志和控制信息,TCP 報(bào)文結(jié)構(gòu)如下圖所示:

在這里插入圖片描述

(1) 源、目標(biāo)端口號(hào)字段:占16 比特。TCP 協(xié)議通過使用”端口”來標(biāo)識(shí)源端和目標(biāo)端的
應(yīng)用進(jìn)程。端口號(hào)可以使用0 到65535 之間的任何數(shù)字。在收到服務(wù)請(qǐng)求時(shí),操作系統(tǒng)動(dòng)態(tài)地
為客戶端的應(yīng)用程序分配端口號(hào)。在服務(wù)器端,每種服務(wù)在”眾所周知的端口”(Well-Know
Port)為用戶提供服務(wù)。
(2) 序列號(hào)字段:占32 比特。用來標(biāo)識(shí)從TCP 源端向TCP 目標(biāo)端發(fā)送的數(shù)據(jù)字節(jié)流,它
表示在這個(gè)報(bào)文段中的第一個(gè)數(shù)據(jù)字節(jié)。
(3) 確認(rèn)號(hào)字段:占32 比特。只有ACK 標(biāo)志為1 時(shí),確認(rèn)號(hào)字段才有效。它包含目標(biāo)端
所期望收到源端的下一個(gè)數(shù)據(jù)字節(jié)。
(4) 頭部長度字段:占4 比特。給出頭部占32 比特的數(shù)目。沒有任何選項(xiàng)字段的TCP 頭
部長度為20 字節(jié);最多可以有60 字節(jié)的TCP 頭部。
(5) 標(biāo)志位字段(U、A、P、R、S、F):占6 比特。各比特的含義如下:
①URG:緊急指針有效。
②ACK:為1 時(shí),確認(rèn)序號(hào)有效。
③PSH:為1 時(shí),接收方應(yīng)該盡快將這個(gè)報(bào)文段交給應(yīng)用層。
④RST:為1 時(shí),重建連接。
⑤SYN:為1 時(shí),同步程序,發(fā)起一個(gè)連接。
⑥FIN:為1 時(shí),發(fā)送端完成任務(wù),釋放一個(gè)連接。
(6) 窗口大小字段:占16 比特。此字段用來進(jìn)行流量控制。單位為字節(jié)數(shù),這個(gè)值是本機(jī)
期望一次接收的字節(jié)數(shù)。
(7) TCP 校驗(yàn)和字段:占16 比特。對(duì)整個(gè)TCP 報(bào)文段,即TCP 頭部和TCP 數(shù)據(jù)進(jìn)行校驗(yàn)
和計(jì)算,并由目標(biāo)端進(jìn)行驗(yàn)證。
(8) 緊急指針字段:占16 比特。它是一個(gè)偏移量,和序號(hào)字段中的值相加表示緊急數(shù)據(jù)最
后一個(gè)字節(jié)的序號(hào)。
(9) 選項(xiàng)字段:占32 比特。可能包括”窗口擴(kuò)大因子”、”時(shí)間戳”等選項(xiàng)。
上述的內(nèi)容講解的是TCP 首部信息,這些信息被封裝在一個(gè)IP 數(shù)據(jù)報(bào)中,該數(shù)據(jù)報(bào)結(jié)構(gòu)
如下圖所示。

在這里插入圖片描述

lwIP 的TCP 報(bào)文首部數(shù)據(jù)結(jié)構(gòu)

實(shí)現(xiàn)TCP 協(xié)議的文件有tcp.h、tcp.c、tcp_in.c 和tcp_out.c,這四個(gè)文件實(shí)現(xiàn)了TCP 協(xié)議
全部數(shù)據(jù)結(jié)構(gòu)和函數(shù),其中tcp.c 文件包含了與TCP 編程、TCP 定時(shí)器相關(guān)的函數(shù),而
tcp_in.c 文件包含了TCP 報(bào)文段輸入處理函數(shù),而tcp_out.c 文件包含了TCP 報(bào)文輸出處理函
數(shù),當(dāng)然tcp.h 定義了宏和結(jié)構(gòu)體。首先我們看一下TCP 首部結(jié)構(gòu),這個(gè)結(jié)構(gòu)為tcp_hdr,如
下源碼所示:

struct tcp_hdr {PACK_STRUCT_FIELD(u16_t src); /* 源端口*/PACK_STRUCT_FIELD(u16_t dest); /* 目的端口*/PACK_STRUCT_FIELD(u32_t seqno); /* 序號(hào)*/PACK_STRUCT_FIELD(u32_t ackno); /* 確認(rèn)序號(hào)*/PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); /* 首部長度+保留位+標(biāo)志位*/PACK_STRUCT_FIELD(u16_t wnd); /* 窗口大小*/PACK_STRUCT_FIELD(u16_t chksum); /* 校驗(yàn)位*/PACK_STRUCT_FIELD(u16_t urgp); /* 緊急指針*/
} PACK_STRUCT_STRUCT;

可見,lwIP 使用tcp_hdr 結(jié)構(gòu)體描述TCP 首部各個(gè)字段,值得注意的是,該結(jié)構(gòu)體的
_hdrlen_rsvd_flags 變量用來描述下圖黃色部分的內(nèi)容。

在這里插入圖片描述

lwIP 的TCP 連接狀態(tài)圖

根據(jù)圖12.1.2.1 和12.1.2.2 所示,發(fā)送端與接收端發(fā)送的指令會(huì)進(jìn)入不同的狀態(tài),因此,
lwIP 在tcpbase.h 文件中定義了枚舉類型tcp_state,它是用來描述TCP 的狀態(tài),該枚舉
tcp_state 如下源碼所示:

enum tcp_state {CLOSED = 0, /* 關(guān)閉狀態(tài)*/LISTEN = 1, /* 監(jiān)聽狀態(tài)*/SYN_SENT = 2, /* 發(fā)送請(qǐng)求連接*/SYN_RCVD = 3, /* 接收請(qǐng)求連接*/ESTABLISHED = 4, /* 連接狀態(tài)已建立*/FIN_WAIT_1 = 5, /* 程序已關(guān)閉該連接*/FIN_WAIT_2 = 6, /* 另一端已關(guān)閉連接*/CLOSE_WAIT = 7, /* 等待程序關(guān)閉連接*/CLOSING = 8, /* 兩端同時(shí)收到對(duì)方的關(guān)閉請(qǐng)求*/LAST_ACK = 9, /* 服務(wù)器等待對(duì)方接收關(guān)閉操作*/TIME_WAIT = 10 /* 關(guān)閉成功*/
};

下面筆者使用TCP 狀態(tài)轉(zhuǎn)換圖來描述連接可能在各個(gè)狀態(tài)之間的轉(zhuǎn)換關(guān)系,如下圖所示:

在這里插入圖片描述

如果TCP 需要建立連接,則系統(tǒng)需要三次握手;如果TCP 中斷連接,則系統(tǒng)需要四次揮
手,現(xiàn)在筆者以上圖12.1.4.2 的TCP 狀態(tài)變遷圖來繪制三次握手與四次揮手的狀態(tài)圖,不得不
說圖片讓我們更直觀了解TCP 連接和關(guān)閉,如下圖所示:

在這里插入圖片描述

lwIP 的TCP 控制塊

到目前為此,筆者已經(jīng)講解了太多TCP 協(xié)議理論的知識(shí),這一小節(jié)我們正式踏入lwIP 的
TCP 協(xié)議大門。在此之前我們先了解一下TCP 控制塊,這個(gè)控制塊定義了TCP 協(xié)議運(yùn)作過程
中的參數(shù),例如發(fā)送窗口、數(shù)據(jù)緩沖區(qū)等,如下源碼所示:

/** TCP協(xié)議控制塊*/
struct tcp_pcb {/** common PCB members */IP_PCB;TCP_PCB_COMMON(struct tcp_pcb);/* 遠(yuǎn)端端口號(hào)*/u16_t remote_port;/*附加狀態(tài)信息,如連接是快速恢復(fù)、一個(gè)被延遲的ACK 是否被發(fā)送等*/tcpflags_t flags;#define TF_ACK_DELAY 0x01 U /* 延遲發(fā)送ACK. */ # define TF_ACK_NOW 0x02 U /* 延遲發(fā)送ACK. */ # define TF_INFR 0x04 U /* 在快速恢復(fù). */ # define TF_CLOSEPEND 0x08 U /* 關(guān)閉掛起*/ # define TF_RXCLOSED 0x10 U /* rx 由tcp_shutdown 關(guān)閉*/ # define TF_FIN 0x20 U /* 連接在本地關(guān)閉(FIN段入隊(duì)) */ # define TF_NODELAY 0x40 U /* 納格爾禁用算法*/ # define TF_NAGLEMEMERR 0x80 U /* nagle啟用,本地緩沖區(qū)溢出*//* Timers */u8_t polltmr, pollinterval;/* 控制塊被最后一次處理的時(shí)間*/u8_t last_timer;/* 該字段記錄該P(yáng)CB 被創(chuàng)建的時(shí)刻*/u32_t tmr;/* 接收變量*/u32_t rcv_nxt; /* 下一個(gè)期望收到的序號(hào)*/tcpwnd_size_t rcv_wnd; /* 當(dāng)前接收窗口的大小,會(huì)隨著數(shù)據(jù)的接收與遞交動(dòng)態(tài)變化*/tcpwnd_size_t rcv_ann_wnd; /* 將向?qū)Ψ酵ǜ娴拇翱诖笮?#xff0c;隨著數(shù)據(jù)的接收與遞交動(dòng)態(tài)變化*/u32_t rcv_ann_right_edge; /* 上一次窗口通告時(shí)窗口的右邊界值*//* 重傳定時(shí)器,該值隨時(shí)間遞增,當(dāng)大于rto 的值時(shí)重傳報(bào)文*/s16_t rtime;u16_t mss; /* 對(duì)方可接收的最大報(bào)文段大小*//* RTT(往返時(shí)間)估計(jì)變量*/u32_t rttest; /* RTT估計(jì)每秒500毫秒*/u32_t rtseq; /* 序列號(hào)定時(shí)*/s16_t sa, sv; /* RTT 估計(jì)得到的平均值與時(shí)間差*/s16_t rto; /* 重新傳輸超時(shí)(以TCP_SLOW_INTERVAL為單位) */u8_t nrtx; /* 重新發(fā)送的*//* 快速重新傳輸/恢復(fù)*/u8_t dupacks; /* 上述最大確認(rèn)號(hào)被重復(fù)收到的次數(shù)*/u32_t lastack; /* 接收到的最大確認(rèn)序號(hào)*//* 擁塞避免/控制變量*/tcpwnd_size_t cwnd; /* 連接當(dāng)前的窗口大小*/tcpwnd_size_t ssthresh; /* 擁塞避免算法啟動(dòng)的閾值*//* 第一個(gè)字節(jié)后面最后一個(gè)rto字節(jié)*/u32_t rto_end;/* 發(fā)送變量*/u32_t snd_nxt; /* 下一個(gè)要發(fā)送的序號(hào)*/u32_t snd_wl1, snd_wl2; /* 上一次收到的序號(hào)和確認(rèn)號(hào)*/u32_t snd_lbb; /* 要緩沖的下一個(gè)字節(jié)的序列號(hào)*/tcpwnd_size_t snd_wnd; /* 發(fā)送窗口*/tcpwnd_size_t snd_wnd_max; /* 對(duì)方的最大發(fā)送方窗口*//* 可用的緩沖區(qū)空間*/tcpwnd_size_t snd_buf;#define TCP_SNDQUEUELEN_OVERFLOW(0xffff U - 3)u16_t snd_queuelen; /* 可用的發(fā)送包數(shù)*/ #if TCP_OVERSIZE/* Extra bytes available at the end of the last pbuf in unsent. */u16_t unsent_oversize;#endif /* TCP_OVERSIZE */tcpwnd_size_t bytes_acked;/* These are ordered by sequence number: */struct tcp_seg * unsent; /* 未發(fā)送的報(bào)文段*/struct tcp_seg * unacked; /* 已發(fā)送但未收到確認(rèn)的報(bào)文段. */struct pbuf * refused_data; /* 以前收到但上層尚未取得的數(shù)據(jù)*/ #if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOGstruct tcp_pcb_listen * listener;#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG *//* TCP 協(xié)議相關(guān)的回調(diào)函數(shù)*/#if LWIP_CALLBACK_API/* 當(dāng)數(shù)據(jù)發(fā)送成功后被調(diào)用. */tcp_sent_fn sent;/* 接收數(shù)據(jù)完成后被調(diào)用*/tcp_recv_fn recv;/* 建立連接后被調(diào)用. */tcp_connected_fn connected;/* 該函數(shù)被內(nèi)核周期調(diào)用. */tcp_poll_fn poll;/* 發(fā)送錯(cuò)誤時(shí)候被調(diào)用. */tcp_err_fn errf;#endif /* LWIP_CALLBACK_API *//* 保持活性*/u32_t keep_idle;/* 堅(jiān)持計(jì)時(shí)器計(jì)數(shù)器值*/u8_t persist_cnt;/* 堅(jiān)持計(jì)時(shí)器關(guān)閉*/u8_t persist_backoff;/* 持續(xù)探測(cè)數(shù)*/u8_t persist_probe;/* 保持活性報(bào)文發(fā)送次數(shù)*/u8_t keep_cnt_sent;
};

TCP 協(xié)議控制塊的成員變量有點(diǎn)多,由于TCP 協(xié)議在lwIP 源碼中占了50%之多,所以深
入的去了解TCP 協(xié)議可能會(huì)花很多精力和時(shí)間,這里筆者講解重要的知識(shí)即可。首先我們先
講解一下接收數(shù)據(jù)相關(guān)的字段rcv_nxt,rcv_wnd,rcv_ann_wnd 和數(shù)據(jù)發(fā)送的相關(guān)字段
snd_nxt,snd_max,snd_wnd,acked,這些字段和TCP 中滑動(dòng)窗口協(xié)議有密切關(guān)系的。
聲明:下面的內(nèi)容參考自《嵌入式網(wǎng)絡(luò)那些事LWIP 協(xié)議深度剖析與實(shí)戰(zhàn)演練》,作者朱
升林!。

  1. TCP 控制塊接收窗口
    在TCP 控制塊中,關(guān)于接收窗口有四個(gè)變量來描述,如下圖所示:
    ①rcv_nxt:是自己期望收到的下一個(gè)數(shù)據(jù)字節(jié)編號(hào)。
    ②rcv_wnd:表示接收窗口的大小。
    ③rcv_ann_wnd:表示將向?qū)Ψ酵ǜ娴拇翱诖笮≈?#xff0c;這個(gè)值在報(bào)文發(fā)送時(shí)會(huì)被填在首部中
    的窗口大小字段。
    ④rcv_ann_right_edge:記錄了上一次窗口通告時(shí)窗口右邊界取值,該字段在窗口滑動(dòng)過
    程中經(jīng)常被用到。

在這里插入圖片描述

在上圖中綠色框是窗口大小(rcv_wnd = 9 ),也就是說可以發(fā)送9 個(gè)數(shù)據(jù),而
rcv_ann_wnd = 9 就是通知對(duì)方窗口大小的值,而rcv_ann_right_edge 記錄了上一次窗口通告時(shí)
窗口右邊界取值(14),當(dāng)然下一次發(fā)送時(shí),這四個(gè)變量就不一定是上述圖中的值了,它們會(huì)
隨著數(shù)據(jù)的發(fā)送與接收動(dòng)態(tài)改變。當(dāng)接收到數(shù)據(jù)后,數(shù)據(jù)會(huì)被放在接收窗口中等待上層調(diào)用,
rcv_nxt 字段會(huì)指向下一個(gè)期望接收的編號(hào),同時(shí)窗口值rcv_wnd 值會(huì)減少,當(dāng)上層取走相關(guān)
的數(shù)據(jù)后,窗口的值會(huì)增加;rcv_ann_wnd 在整個(gè)過程中都是動(dòng)態(tài)計(jì)算的,當(dāng)rcv_wnd 值改變
時(shí),內(nèi)核會(huì)計(jì)算一個(gè)合理的窗口值rcv_ann_wnd(并不一定與rcv_wnd 相等),在下一次報(bào)文
發(fā)送時(shí),通告窗口的值(rcv_ann_wnd )會(huì)被填入報(bào)文的首部,同時(shí)右邊界值
rcv_ann_right_edge 也在報(bào)文發(fā)送后更新數(shù)值。
2. TCP 控制塊發(fā)送窗口
在lwIP 源碼描述TCP 的發(fā)送窗口涉及4 個(gè)變量,它們之間的關(guān)系如下圖所示:
①lastack:字段記錄了被接收方確認(rèn)的最高序列號(hào)。
②snd_nxt:表示自己將要發(fā)送的下一個(gè)數(shù)據(jù)的起始編號(hào)。
③snd_wnd:記錄了當(dāng)前的發(fā)送窗口大小,它常被設(shè)置為接收方通告的接收窗口值。
④snd_lbb:記錄了下一個(gè)將被應(yīng)用程序緩存的數(shù)據(jù)的起始編號(hào)。

在這里插入圖片描述

可以看出,左邊部分是已經(jīng)發(fā)送并確認(rèn)的數(shù)據(jù),綠色框是已經(jīng)發(fā)送但未確認(rèn)的數(shù)據(jù)(需要
等待對(duì)方確認(rèn)),紅色框可以發(fā)送的數(shù)據(jù),最右邊的是不能發(fā)送的。上面這四個(gè)字段的值也是
動(dòng)態(tài)變化的,每當(dāng)收到接收方的一個(gè)有效ACK 后,lastack 的值就做相應(yīng)的增加,指向下一個(gè)
待確認(rèn)數(shù)據(jù)的編號(hào),當(dāng)發(fā)送一個(gè)報(bào)文后,snd_nxt 的值就做相應(yīng)的增加,指向下一個(gè)待發(fā)送數(shù)
據(jù)。snd_nxt 和lastack 之間的差值不能超過sndwnd 的大小。由于實(shí)際數(shù)據(jù)發(fā)送時(shí)是按照?qǐng)?bào)文
段的形式組織的,因此可能存在這樣的情況:即使發(fā)送窗口允許,但并不是窗口內(nèi)的所有數(shù)據(jù)
都能被發(fā)送以填滿窗口,如上圖中編號(hào)為11~13 的數(shù)據(jù),可能因?yàn)樗鼈兲〔荒芙M織成一個(gè)有
效的報(bào)文段,因此不會(huì)被發(fā)送。發(fā)送方會(huì)等到新的確認(rèn)到來,從而使發(fā)送窗口向右滑動(dòng),使得
更多的數(shù)據(jù)被包含在窗口中,這樣再啟動(dòng)下一個(gè)報(bào)文段的發(fā)送。
3. 監(jiān)聽控制塊
lwIP 除了定義結(jié)構(gòu)體tcp_pcb,它還定義了結(jié)構(gòu)體tcp_pcb_listen,前者我們知道有這個(gè)就
行,后者結(jié)構(gòu)體tcp_pcb_listen 主要描述LISTEN 狀態(tài)的連接,一般用于描述處于監(jiān)聽狀態(tài)的
連接,在處于LISTEN 狀態(tài)的連接只記錄本地端口的信息,不記錄任何遠(yuǎn)程端口的信息,當(dāng)然
處于該狀態(tài)不會(huì)進(jìn)行數(shù)據(jù)發(fā)送、連接握手之類的服務(wù),主要是分配完整的TCP 控制塊是比較
消耗內(nèi)存資源的,在TCP 協(xié)議在連接之初,是無法進(jìn)行數(shù)據(jù)交互,那么在監(jiān)聽的時(shí)候只需要
把對(duì)方主機(jī)的相關(guān)信息得到,然后無縫切換到完整的TCP 控制塊中,這樣子就能節(jié)省不少資
源,tcp_pcb_listen 的廬山真面目,如下源碼所示:

/** 用于監(jiān)聽pcb的TCP協(xié)議控制塊*/
struct tcp_pcb_listen {/** 該宏包含源IP 地址、目的IP 地址兩個(gè)重要字段*/IP_PCB;/** 兩種控制塊都具有的字段*/TCP_PCB_COMMON(struct tcp_pcb_listen);#if LWIP_CALLBACK_API/* 函數(shù)在連接偵聽器時(shí)調(diào)用*/tcp_accept_fn accept;#endif /* LWIP_CALLBACK_API */
};
  1. 控制塊鏈表:
    為了描述TCP 控制塊,lwIP 內(nèi)核定義了四條鏈表來鏈接處于不同狀態(tài)下的控制塊,TCP
    操作一般對(duì)于鏈表上的控制塊進(jìn)行查找,這四個(gè)控制塊鏈表在tcp.c 文件中,如下源碼所示:
/*連接所有進(jìn)行了端口號(hào)綁定,但是還沒有發(fā)起連接(主動(dòng)連接)或進(jìn)入偵聽狀態(tài)(被動(dòng)連接)的控制塊*/
struct tcp_pcb *tcp_bound_pcbs;
/* 連接所有進(jìn)入偵聽狀態(tài)(被動(dòng)連接)的控制塊*/
union tcp_listen_pcbs_t tcp_listen_pcbs;
/* 連接所有處于其他狀態(tài)的控制塊. */
struct tcp_pcb *tcp_active_pcbs;
/* 連接所有處于TIME-WAIT 狀態(tài)的控制塊*/
struct tcp_pcb *tcp_tw_pcbs;
  1. TCP 報(bào)文段緩沖
    在內(nèi)核中,所有待發(fā)送的數(shù)據(jù)或者已經(jīng)接收的數(shù)據(jù)都會(huì)以報(bào)文的形式保存,一般都是保存
    在pbuf 中,為了很好的管理報(bào)文段的pbuf,內(nèi)核引用了一個(gè)tcp_seg 的結(jié)構(gòu)體,該結(jié)構(gòu)體的作
    用就是把所有報(bào)文段連接起來,當(dāng)然這些報(bào)文段可以是無發(fā)送、已發(fā)送并未確認(rèn)的或者是以收
    到的報(bào)文,它們都保存在TCP 控制塊緩沖區(qū)中,該結(jié)構(gòu)體如下源碼所示:
/* 定義組織TCP 報(bào)文段的結(jié)構(gòu)*/
struct tcp_seg {struct tcp_seg *next; /* 該指針用于將報(bào)文段組織為隊(duì)列的形式*/struct pbuf *p; /* 指向裝載報(bào)文段的pbuf */u16_t len; /* 報(bào)文段中的數(shù)據(jù)長度*/u8_t flags;
#define TF_SEG_OPTS_MSS (u8_t)0x01U /* 包含了最大報(bào)文段大小選項(xiàng)*/
#define TF_SEG_OPTS_TS (u8_t)0x02U /* 包含了時(shí)間戳選項(xiàng)*/
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* 所有數(shù)據(jù)(不是header)都是校驗(yàn)和為*/
#define TF_SEG_OPTS_ WND_SCALE (u8_t)0x08U /* 包括WND規(guī)模選項(xiàng)(僅用于SYN段) */
#define TF_SEG_OPTS_SACK_PERM (u8_t)0x10U/*包括SACK允許選項(xiàng)(僅在SYN段中使用)*//* 指向報(bào)文段中的TCP 首部*/struct tcp_hdr *tcphdr; /* TCP報(bào)頭*/
};

每個(gè)控制塊中都維護(hù)了三個(gè)緩沖隊(duì)列,unsent、unacked、ooseq 三個(gè)字段(這三個(gè)字段已
經(jīng)在TCP 控制塊時(shí)候講解了)。unsent 用于連接還未被發(fā)送出去的報(bào)文段、unacked 用于連接
已經(jīng)發(fā)送出去但是還未被確認(rèn)的報(bào)文段、ooseq 用于連接接收到的無序報(bào)文段,如下圖所示:

在這里插入圖片描述

lwIP 的TCP 編程

(2) TCP 報(bào)文段的接收
報(bào)文段的接收函數(shù)是tcp_input,該函數(shù)位于tcp_inc.c 文件中,如下源碼所示:

void
tcp_input(struct pbuf * p, struct netif * inp) {struct tcp_pcb * pcb, * prev;struct tcp_pcb_listen * lpcb;u8_t hdrlen_bytes;err_t err;/* 指向TCP首部*/tcphdr = (struct tcp_hdr * ) p - > payload;/* 第一步:檢查TCP報(bào)頭是否少于20 */if (p - > len < TCP_HLEN) {/* 釋放空間掉棄報(bào)文段*/goto dropped;}/* 第二步:判斷是否是廣播與多播類型*/if (ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif()) ||ip_addr_ismulticast(ip_current_dest_addr())) {/* 釋放空間掉棄報(bào)文段*/goto dropped;}/* 獲取tcphdr首部字節(jié)*/hdrlen_bytes = TCPH_HDRLEN_BYTES(tcphdr);/* 第三步:檢測(cè)TCP報(bào)文長度*/if ((hdrlen_bytes < TCP_HLEN) || (hdrlen_bytes > p - > tot_len)) {/* 釋放空間掉棄報(bào)文段*/goto dropped;}/* 移動(dòng)pbuf中的有效負(fù)載指針,使其指向TCP數(shù)據(jù)*//* tcphdr_optlen = TCP報(bào)頭選項(xiàng)長度(TCP報(bào)頭總長度- TCP標(biāo)準(zhǔn)報(bào)頭20字節(jié)) */tcphdr_optlen = (u16_t)(hdrlen_bytes - TCP_HLEN);tcphdr_opt2 = NULL; /* tcphdr_opt2 指向NULL *//* 判斷TCP報(bào)頭是否在一個(gè)pbuf中*/if (p - > len >= hdrlen_bytes) {/* 若TCP報(bào)頭在第一個(gè)pbuf中*/tcphdr_opt1len = tcphdr_optlen; /* tcphdr_opt1len = TCP報(bào)頭選項(xiàng)長度*/pbuf_remove_header(p, hdrlen_bytes); /* 將指針移動(dòng)到pbuf數(shù)據(jù)中*/} else {u16_t opt2len;/* 刪除TCP首部*/pbuf_remove_header(p, TCP_HLEN);/* 確定選項(xiàng)的第一部分和第二部分長度*/tcphdr_opt1len = p - > len;opt2len = (u16_t)(tcphdr_optlen - tcphdr_opt1len);/* 移除tcphdr_opt1len選項(xiàng)*/pbuf_remove_header(p, tcphdr_opt1len);/* 檢查TCP報(bào)頭選項(xiàng)部分是否在第二個(gè)pbuf中*/if (opt2len > p - > next - > len) {/* 丟棄過短的報(bào)文*/goto dropped;}/* 記住指向TCP報(bào)頭選項(xiàng)的第二部分的指針(有部分選項(xiàng)在第二個(gè)pbuf中,記錄TCP報(bào)頭選項(xiàng)的開始部分) */tcphdr_opt2 = (u8_t * ) p - > next - > payload;/* 將第二個(gè)pbuf的指針指向pbuf 的數(shù)據(jù)部分*/pbuf_remove_header(p - > next, opt2len);p - > tot_len = (u16_t)(p - > tot_len - opt2len);}/* 提取源端口*/tcphdr - > src = lwip_ntohs(tcphdr - > src);/* 提取目標(biāo)端口*/tcphdr - > dest = lwip_ntohs(tcphdr - > dest);/* 提取序號(hào)*/seqno = tcphdr - > seqno = lwip_ntohl(tcphdr - > seqno);/* 提取確認(rèn)號(hào)*/ackno = tcphdr - > ackno = lwip_ntohl(tcphdr - > ackno);/* 提取窗口*/tcphdr - > wnd = lwip_ntohs(tcphdr - > wnd);/* 6位標(biāo)志位*/flags = TCPH_FLAGS(tcphdr);/* TCP數(shù)據(jù)包中數(shù)據(jù)的總長度,對(duì)于有FIN或SYN標(biāo)志的數(shù)據(jù)包,該長度要加1 */tcplen = p - > tot_len;if (flags & (TCP_FIN | TCP_SYN)) {tcplen++;if (tcplen < p - > tot_len) {/* 釋放空間掉棄報(bào)文段*/goto dropped;}}/* ****************************省略代碼********************************* *//* 如果pcb在回調(diào)中被中止(通過調(diào)用tcp_abort()),則跳轉(zhuǎn)目標(biāo)。*/aborted:tcp_input_pcb = NULL;recv_data = NULL;if (inseg.p != NULL) {pbuf_free(inseg.p);inseg.p = NULL;}
} else {/*如果在3張鏈表里都未找到匹配的pcb,則調(diào)用tcp_rst向源主機(jī)發(fā)送一個(gè)TCP復(fù)位數(shù)據(jù)包*/if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) {TCP_STATS_INC(tcp.proterr);TCP_STATS_INC(tcp.drop);tcp_rst(NULL, ackno, seqno + tcplen, ip_current_dest_addr(),ip_current_src_addr(), tcphdr - > dest, tcphdr - > src);}pbuf_free(p);
}
return;
dropped:pbuf_free(p);
}

上述的源碼大概400 多行,該函數(shù)可以分為上部分與下部分,上部分主要講述了對(duì)IP 層
遞交傳輸層的數(shù)據(jù)報(bào)檢驗(yàn),例如檢驗(yàn)數(shù)據(jù)報(bào)是否正常操作、是否包含數(shù)據(jù)、該數(shù)據(jù)報(bào)是否為廣
播或者多播,如果以上檢驗(yàn)成立,則系統(tǒng)把該數(shù)據(jù)報(bào)掉棄處理,并釋放pbuf。下部分主要對(duì)
tcp_active_pcbs 鏈表尋找對(duì)應(yīng)的TCP 控制塊,如果找到了TCP 控制塊,則調(diào)用tcp_process 函
數(shù)處理;如果找不到TCP 控制塊,則內(nèi)核轉(zhuǎn)換到tcp_tw_pcbs 鏈表中查找;如果在
tcp_tw_pcbs 鏈表中找到TCP 控制塊,則內(nèi)核調(diào)用tcp_timewait_input 函數(shù)處理它;如果這兩個(gè)
鏈表沒有找到TCP 控制塊,則系統(tǒng)會(huì)進(jìn)入tcp_listen_pcbs 鏈表中查找;如果找到了就調(diào)用
tcp_listen_input 函數(shù)處理;如果三個(gè)鏈表都找不到的話,則系統(tǒng)就釋放pbuf 內(nèi)存。
(3) TCP 報(bào)文段的發(fā)送
傳輸層與網(wǎng)絡(luò)層的交互函數(shù)為tcp_output,它在tcp_output.c 文件中定義,如下源碼所示:

/* 發(fā)送控制塊緩沖隊(duì)列中的報(bào)文段*/
err_t
tcp_output(struct tcp_pcb * pcb) {struct tcp_seg * seg, * useg;u32_t wnd, snd_nxt;err_t err;struct netif * netif;/* 如果控制塊當(dāng)前正有數(shù)據(jù)被處理,這里不做任何輸出,直接返回*/if (tcp_input_pcb == pcb) /* 在控制塊的數(shù)據(jù)處理完成后,內(nèi)核會(huì)再次調(diào)用*/ {return ERR_OK; /* 調(diào)用tcp_output 發(fā)送數(shù)據(jù),見函數(shù)tcp_input */}/* 從發(fā)送窗口和阻塞窗口取小者得到有效發(fā)送窗口,擁塞避免會(huì)講解到這個(gè)原理*/wnd = LWIP_MIN(pcb - > snd_wnd, pcb - > cwnd);/* 未發(fā)送隊(duì)列*/seg = pcb - > unsent;if (seg == NULL) {/* 若要求立即確認(rèn),但該ACK 又不能被捎帶出去,則只發(fā)送一個(gè)純ACK 的報(bào)文段*/if (pcb - > flags & TF_ACK_NOW) {return tcp_send_empty_ack(pcb); /* 發(fā)送只帶ACK 的報(bào)文段*/}/* 沒什么可送的*/goto output_done;} else {}/* 判斷本地IP地址與遠(yuǎn)程IP地址是否同一網(wǎng)段*/netif = tcp_route(pcb, & pcb - > local_ip, & pcb - > remote_ip);if (netif == NULL) {return ERR_RTE;}/* 如果沒有本地IP地址,從netif獲得一個(gè)*/if (ip_addr_isany( & pcb - > local_ip)) {const ip_addr_t * local_ip = ip_netif_get_local_ip(netif, & pcb - > remote_ip);if (local_ip == NULL) {return ERR_RTE;}ip_addr_copy(pcb - > local_ip, * local_ip);}/* 處理窗口中不匹配的當(dāng)前段*/if (lwip_ntohl(seg - > tcphdr - > seqno) - pcb - > lastack + seg - > len > wnd) {/* 開始持續(xù)定時(shí)器*/if (wnd == pcb - > snd_wnd && pcb - > unacked == NULL && \pcb - > persist_backoff == 0) {pcb - > persist_cnt = 0;pcb - > persist_backoff = 1;pcb - > persist_probe = 0;}/* 我們需要一個(gè)ACK,但是現(xiàn)在不能發(fā)送數(shù)據(jù),所以發(fā)送一個(gè)空ACK */if (pcb - > flags & TF_ACK_NOW) {return tcp_send_empty_ack(pcb);}goto output_done;}/* 停止持續(xù)計(jì)時(shí)器,如果以上條件不滿足*/pcb - > persist_backoff = 0;/* useg應(yīng)該指向未處理隊(duì)列的最后一個(gè)tcp_seg 結(jié)構(gòu)*/useg = pcb - > unacked;if (useg != NULL) {for (; useg - > next != NULL; useg = useg - > next);}/* 可用數(shù)據(jù)和窗口允許它發(fā)送報(bào)文段,直到把數(shù)據(jù)全部發(fā)送出去或者填滿發(fā)送窗口*/while (seg != NULL &&lwip_ntohl(seg - > tcphdr - > seqno) - pcb - > lastack + seg - > len <= wnd) {/* 如果nagle算法可以阻止發(fā)送,就停止發(fā)送*/if ((tcp_do_output_nagle(pcb) == 0) &&((pcb - > flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {break;}if (pcb - > state != SYN_SENT) /* 當(dāng)前不為SYN_SENT 狀態(tài)*/ {TCPH_SET_FLAG(seg - > tcphdr, TCP_ACK); /* 填寫首部中的ACK 標(biāo)志*/}/* 調(diào)用函數(shù)發(fā)送報(bào)文段*/err = tcp_output_segment(seg, pcb, netif);if (err != ERR_OK) {/* segment could not be sent, for whatever reason */tcp_set_flags(pcb, TF_NAGLEMEMERR);return err;}/* 得到下一個(gè)未發(fā)送的tcp_seg */pcb - > unsent = seg - > next;if (pcb - > state != SYN_SENT) {tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW);}/* 計(jì)算snd_nxt 的值*/snd_nxt = lwip_ntohl(seg - > tcphdr - > seqno) + TCP_TCPLEN(seg);/* 更新下一個(gè)要發(fā)送的數(shù)據(jù)編號(hào)*/if (TCP_SEQ_LT(pcb - > snd_nxt, snd_nxt)) {pcb - > snd_nxt = snd_nxt;}/* 如果發(fā)送出去的報(bào)文段數(shù)據(jù)長度不為0,或者帶有SYN、FIN 標(biāo)志,則將該報(bào)文段加入到未確認(rèn)隊(duì)列中以便超時(shí)后重傳*/if (TCP_TCPLEN(seg) > 0) {seg - > next = NULL; /* 空?qǐng)?bào)文段next 字段*//* 若未確認(rèn)隊(duì)列為空,則直接掛接*/if (pcb - > unacked == NULL) {pcb - > unacked = seg;useg = seg; /* 變量useg 指向未確認(rèn)隊(duì)列尾部*/} else {/* 如果未確認(rèn)隊(duì)列不為空,則需要把當(dāng)前報(bào)文按照順序組織在隊(duì)列中*/if (TCP_SEQ_LT(lwip_ntohl(seg - > tcphdr - > seqno),lwip_ntohl(useg - > tcphdr - > seqno))) {/* 如果當(dāng)前報(bào)文的序列號(hào)比隊(duì)列尾部報(bào)文的序列號(hào)低,則從隊(duì)列首部開始查找合適的位置,插入報(bào)文段*/struct tcp_seg * * cur_seg = & (pcb - > unacked);while ( * cur_seg &&TCP_SEQ_LT(lwip_ntohl(( * cur_seg) - > tcphdr - > seqno),lwip_ntohl(seg - > tcphdr - > seqno))) {cur_seg = & (( * cur_seg) - > next);} /* 找到插入位置,將報(bào)文段插入到隊(duì)列中*/seg - > next = ( * cur_seg);( * cur_seg) = seg;} else {/* 報(bào)文段序號(hào)最高,則放在未確認(rèn)隊(duì)列尾部*/useg - > next = seg;useg = useg - > next;}}} else /* 報(bào)文段長度為0,不需要重傳,直接刪除*/ {tcp_seg_free(seg);}seg = pcb - > unsent; /* 發(fā)送下一個(gè)報(bào)文段*/}#if TCP_OVERSIZEif (pcb - > unsent == NULL) {/* 清0 已發(fā)送的窗口探測(cè)包數(shù)目*/pcb - > unsent_oversize = 0;}#endif /* TCP_OVERSIZE */output_done:tcp_clear_flags(pcb, TF_NAGLEMEMERR);return ERR_OK;
}

從整體來看,此函數(shù)首先檢測(cè)報(bào)文是否滿足發(fā)送要求,接著判斷控制塊的flags 字段是否
被設(shè)置為TF_ACK_NOW 狀態(tài),如果是,則發(fā)送一個(gè)純粹ACK 報(bào)文段,因此,此時(shí)unsent 隊(duì)
列中無數(shù)據(jù)發(fā)送或者發(fā)送窗口不允許發(fā)送數(shù)據(jù)。如果內(nèi)核能發(fā)送數(shù)據(jù),則就將ACK 應(yīng)答捎帶
發(fā)送出去,同時(shí)在發(fā)送的時(shí)候先找到未發(fā)送鏈表,然后調(diào)用tcp_output_segment()-> ip_output_if()函數(shù)進(jìn)行發(fā)送,直到把未發(fā)送鏈表的數(shù)據(jù)完全發(fā)送出去或者直到填滿發(fā)送窗口,
并且更新發(fā)送窗口相關(guān)字段,當(dāng)然也要將這些已發(fā)送但是未確認(rèn)的數(shù)據(jù)存儲(chǔ)在未確認(rèn)鏈表中,
以防丟失數(shù)據(jù)進(jìn)行重發(fā)操作,放入未確認(rèn)鏈表的時(shí)候是按序號(hào)升序進(jìn)行排序的。

lwIP 的TCP 建立與關(guān)閉連接原理

下面筆者來講解一下lwIP 如何實(shí)現(xiàn)TCP 客戶端以及服務(wù)器連接,這里我們可以根據(jù)TCP
連接示意圖來講解lwIP 源碼是如何實(shí)現(xiàn)TCP 連接的。在講解之前,我們先了解TCP 客戶端的
配置流程,如下所示:

  1. TCP 客戶端建立連接原理:
    ①創(chuàng)建TCP 控制塊
    調(diào)用函數(shù)tcp_new 創(chuàng)建TCP 控制塊。
    ②連接指定的IP 地址和端口號(hào)
    調(diào)用函數(shù)tcp_connect 連接到目的地址的指定端口上,注意:當(dāng)連接成功后進(jìn)入回調(diào)
    tcp_client_connected 函數(shù)。
    ③接收數(shù)據(jù)
    調(diào)用函數(shù)tcp_recved 接收數(shù)據(jù)。
    ④發(fā)送數(shù)據(jù)
    調(diào)用函數(shù)tcp_write 發(fā)送數(shù)據(jù)。
    從上述步驟可知,我們主要調(diào)用函數(shù)tcp_connect 連接遠(yuǎn)程服務(wù)器,這個(gè)函數(shù)和TCP 連接
    圖存在某種聯(lián)系,下面筆者簡單的講解這個(gè)函數(shù)到底如何連接服務(wù)器,該函數(shù)如下所示:
err_t
tcp_connect(struct tcp_pcb * pcb,const ip_addr_t * ipaddr, u16_t port,tcp_connected_fn connected) {/*.....................前面省略大部分代碼......................*//* 發(fā)送SYN與MSS選項(xiàng)一起發(fā)送*/ret = tcp_enqueue_flags(pcb, TCP_SYN);(1)if (ret == ERR_OK) {/* 設(shè)置當(dāng)前TCP控制塊為SYN_SENT狀態(tài)*/pcb - > state = SYN_SENT;(2)if (old_local_port != 0) {TCP_RMV( & tcp_bound_pcbs, pcb);}TCP_REG_ACTIVE(pcb);MIB2_STATS_INC(mib2.tcpactiveopens);tcp_output(pcb);(3)}return ret;
}

可見,上述的(1)表示程序調(diào)用函數(shù)tcp_enqueue_flags 構(gòu)建連接請(qǐng)求報(bào)文(TCP_SYN);上
述的(2)表示當(dāng)前TCP 控制塊設(shè)置為SYN_SENT 狀態(tài);上述的(3)表示程序調(diào)用函數(shù)tcp_output
向服務(wù)器發(fā)送連接請(qǐng)求報(bào)文。下面筆者使用一個(gè)示意圖來描述上述的內(nèi)容,如下圖所示:

在這里插入圖片描述

上圖中紅色框框的是tcp_connect 函數(shù)實(shí)現(xiàn)流程,這里可以稱之為TCP 第一次握手,此時(shí)
客戶端等待服務(wù)器的連接應(yīng)答報(bào)文(TCP_ACK)。當(dāng)客戶端接收服務(wù)器應(yīng)答報(bào)文(TCP_ACK)
時(shí),系統(tǒng)會(huì)在tcp_input 這個(gè)函數(shù)處理該應(yīng)答報(bào)文。這個(gè)函數(shù)在上小節(jié)也講解過,這里我們無
需重復(fù)講解了,該連接應(yīng)答報(bào)文會(huì)在tcp_input–>tcp_process 函數(shù)下處理,注意:tcp_input 函
數(shù)中flags 的全局變量是獲取接收數(shù)據(jù)報(bào)的首部標(biāo)志位(TCP_ACK+ TCP_SYN),這個(gè)過程請(qǐng)
看tcp_in.c 文件234 行的代碼,如下源碼所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {/*..................此處省略了很多代碼..................... */switch (pcb - > state) {case SYN_SENT:/* 收到SYN ACK與預(yù)期的序列號(hào)? */if ((flags & TCP_ACK) && (flags & TCP_SYN)(1) && (ackno == pcb - > lastack + 1)) {pcb - > rcv_nxt = seqno + 1;pcb - > rcv_ann_right_edge = pcb - > rcv_nxt;pcb - > lastack = ackno;pcb - > snd_wnd = tcphdr - > wnd;pcb - > snd_wnd_max = pcb - > snd_wnd;pcb - > snd_wl1 = seqno - 1;pcb - > state = ESTABLISHED;(2)}/*..................此處省略了很多代碼..................... */}/*..................此處省略了很多代碼..................... */
}

上述的的(1)就是為了判斷服務(wù)器應(yīng)答報(bào)文的標(biāo)志位是否包含TCP_ACK 和TCP_SYN,如
果該應(yīng)答報(bào)文包含這些標(biāo)志位,則系統(tǒng)執(zhí)行上述(2)的代碼設(shè)置TCP 控制塊為ESTABLISHED
狀態(tài)。這里筆者也是使用一個(gè)示意圖來描述上述的內(nèi)容,如下圖所示:

在這里插入圖片描述

上圖的紅色框框就是上述內(nèi)容實(shí)現(xiàn)的過程,這里可以稱之為TCP 第二次握手,此時(shí)客戶端必須發(fā)送TCP_ACK 應(yīng)答報(bào)文給服務(wù)器才能實(shí)現(xiàn)第三次握手。上面的函數(shù)tcp_process 執(zhí)行
完成之后返回到tcp_input 函數(shù),該函數(shù)的553 行代碼調(diào)用了tcp_output 函數(shù)發(fā)送應(yīng)答報(bào)文,該
函數(shù)如下所示:

err_t
tcp_output(struct tcp_pcb * pcb) {/*..................此處省略了很多代碼..................... */if (pcb - > state != SYN_SENT) {TCPH_SET_FLAG(seg - > tcphdr, TCP_ACK);}/* 發(fā)送應(yīng)答包*/err = tcp_output_segment(seg, pcb, netif);/*..................此處省略了很多代碼..................... */
}

因?yàn)門CP 控制塊已經(jīng)是ESTABLISHED 狀態(tài)了,所以這個(gè)if 語句判斷為真且執(zhí)行if 語句
內(nèi)的代碼,這個(gè)代碼主要添加該數(shù)據(jù)報(bào)的首部標(biāo)志位TCP_ACK ,接著系統(tǒng)調(diào)用
tcp_output_segmen 發(fā)送該應(yīng)答包,這里就完成了三次握手的動(dòng)作。下面筆者使用一個(gè)示意圖
來講解這個(gè)過程,如下圖所示:

在這里插入圖片描述

  1. TCP 服務(wù)器建立連接原理
    TCP 服務(wù)器的配置流程,如下步驟所示:
    ①創(chuàng)建TCP 控制塊
    調(diào)用函數(shù)tcp_new 創(chuàng)建TCP 控制塊。
    ②綁定本地IP 地址和端口號(hào)
    調(diào)用函數(shù)tcp_bind 綁定本地IP 地址和端口號(hào)。
    ③連接請(qǐng)求
    調(diào)用函數(shù)tcp_accept 等待連接。注意:有連接時(shí),會(huì)調(diào)用函數(shù)lwip_tcp_server_accept 處理
    ④接收數(shù)據(jù)
    調(diào)用函數(shù)tcp_recved 接收數(shù)據(jù)。
    ⑤發(fā)送數(shù)據(jù)
    調(diào)用函數(shù)tcp_write 發(fā)送數(shù)據(jù)。
    首先我們調(diào)用tcp_listen 函數(shù)讓服務(wù)器進(jìn)去監(jiān)聽狀態(tài),簡單來說,TCP 服務(wù)器控制塊從
    CLOSER 轉(zhuǎn)換成LISTEN 狀態(tài),如下源碼所示:
#
define tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog) {LWIP_ASSERT_CORE_LOCKED();return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);}
struct tcp_pcb *tcp_listen_with_backlog_and_err(struct tcp_pcb * pcb, u8_t backlog, err_t * err) {/* ..............省略代碼.............. */lpcb - > callback_arg = pcb - > callback_arg;lpcb - > local_port = pcb - > local_port;lpcb - > state = LISTEN;(1)lpcb - > prio = pcb - > prio;lpcb - > so_options = pcb - > so_options;lpcb - > netif_idx = pcb - > netif_idx;lpcb - > ttl = pcb - > ttl;lpcb - > tos = pcb - > tos;/* ..............省略代碼.............. */}

上述的(1)就是讓TCP 服務(wù)器控制塊從CLOSER 狀態(tài)轉(zhuǎn)換成LISTEN 狀態(tài),下面筆者使用
一個(gè)圖來描述上述的內(nèi)容,如下圖所示:

在這里插入圖片描述

上圖的紅色框框就是由tcp_listen 函數(shù)實(shí)現(xiàn)的,下面開始講解TCP 第一次握手流程,對(duì)于服務(wù)器而言,它是先接收客戶端發(fā)來的連接請(qǐng)求包并判斷該請(qǐng)求報(bào)文的首部標(biāo)志位是否包含T
CP_SYN,這個(gè)請(qǐng)求報(bào)文的處理是由tcp_input→tcp_listen_input 函數(shù)處理的,該函數(shù)與客戶端
請(qǐng)求包相關(guān)的源碼如下所示:

#
define tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog) {LWIP_ASSERT_CORE_LOCKED();return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);}
struct tcp_pcb *tcp_listen_with_backlog_and_err(struct tcp_pcb * pcb, u8_t backlog, err_t * err) {/* ..............省略代碼.............. */lpcb - > callback_arg = pcb - > callback_arg;lpcb - > local_port = pcb - > local_port;lpcb - > state = LISTEN;(1)lpcb - > prio = pcb - > prio;lpcb - > so_options = pcb - > so_options;lpcb - > netif_idx = pcb - > netif_idx;lpcb - > ttl = pcb - > ttl;lpcb - > tos = pcb - > tos;/* ..............省略代碼.............. */}

可見,lwIP 內(nèi)核首先判斷連接請(qǐng)求報(bào)文的首部標(biāo)志位是否包含TCP_SYN,顯然這個(gè)符合
第一次TCP 握手,然后系統(tǒng)把服務(wù)器控制塊的狀態(tài)從LISTEN 轉(zhuǎn)換成SYN-RCVD,這個(gè)過程
請(qǐng)看上述的(1),其次系統(tǒng)構(gòu)建連接應(yīng)答TCP_ACK| TCP_SYN 報(bào)文(上述源碼中的(2)),最后
系統(tǒng)調(diào)用函數(shù)tcp_output 發(fā)送該連接應(yīng)答TCP_ACK| TCP_SYN 報(bào)文到客戶端當(dāng)中(上述源碼
中的(3))。至此我們已經(jīng)實(shí)現(xiàn)了TCP 第二次握手了,下面筆者使用一個(gè)示意圖來講解上述的內(nèi)
容,如下圖所示:

在這里插入圖片描述

上圖的紅色框框就是服務(wù)器接收客戶端的連接請(qǐng)求報(bào)文之后發(fā)送連接應(yīng)答報(bào)文,到了這里
服務(wù)器必須接收客戶端的確認(rèn)連接應(yīng)答TCP_ACK 報(bào)文才能實(shí)現(xiàn)TCP 第三次握手,下面筆者
帶大家講解一下最后一次握手,它是在tcp_input→ tcp_process 函數(shù)下處理的,該函數(shù)如下所
示:

static err_t
tcp_process(struct tcp_pcb * pcb) {/* ...........此處省略多行代碼....... */case SYN_RCVD:if (flags & TCP_ACK) {if (TCP_SEQ_BETWEEN(ackno, pcb - > lastack + 1, pcb - > snd_nxt)) {pcb - > state = ESTABLISHED;/* ...........此處省略多行代碼....... */} else {/* ...........此處省略多行代碼....... */}} else if ((flags & TCP_SYN) && (seqno == pcb - > rcv_nxt - 1)) {/* ...........此處省略多行代碼....... */}break;/* ...........此處省略多行代碼....... */
}

服務(wù)器接收到客戶端的應(yīng)答ACK 報(bào)文之后會(huì)把自身的狀態(tài)SYN-RCVD 轉(zhuǎn)換成
ESTABLISHED,至此客戶端和服務(wù)器可以相互發(fā)送數(shù)據(jù)了。TCP 客戶端和服務(wù)器握手流程已
經(jīng)很詳細(xì)講解了,如有疑問,請(qǐng)大家聯(lián)系筆者,我們可以一起討論研究。
3. TCP 關(guān)閉連接原理:
(1) 客戶端發(fā)送FIN 報(bào)文
程序關(guān)閉TCP 連接是調(diào)用tcp_close 函數(shù)實(shí)現(xiàn)的,在調(diào)用這個(gè)函數(shù)之前,我們必須把
tcp_pcb 的recv 回調(diào)函數(shù)指針設(shè)置為NULL(應(yīng)用層不再接收數(shù)據(jù),所有數(shù)據(jù)直接被丟棄,協(xié)
議層的處理仍按正常流程走,認(rèn)為應(yīng)用層已經(jīng)接收到數(shù)據(jù)),tcp_close 函數(shù)主要作用是發(fā)送
FIN 報(bào)文,進(jìn)入FIN_WAIT_1 狀態(tài)(第一次揮手),下面我們來看一下第一次揮手的源碼,注
意:以下源碼的路徑:tcp_close→tcp_close_shutdown→tcp_close_shutdown_fin 函數(shù)下,該函數(shù)
如下所示:

static err_t
tcp_close_shutdown_fin(struct tcp_pcb * pcb) {err_t err;/* 省略多余的代碼行*/switch (pcb - > state) {case SYN_RCVD:err = tcp_send_fin(pcb);if (err == ERR_OK) {tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb - > state = FIN_WAIT_1;}break;case ESTABLISHED:err = tcp_send_fin(pcb);if (err == ERR_OK) {MIB2_STATS_INC(mib2.tcpestabresets);/* 設(shè)置TCP控制塊的狀態(tài)為FIN_WAIT_1 */pcb - > state = FIN_WAIT_1;}break;case CLOSE_WAIT:err = tcp_send_fin(pcb);if (err == ERR_OK) {MIB2_STATS_INC(mib2.tcpestabresets);pcb - > state = LAST_ACK;}break;default:return ERR_OK;}/* 發(fā)送關(guān)閉連接請(qǐng)求包*/if (err == ERR_OK) {tcp_output(pcb);} else if (err == ERR_MEM) {tcp_set_flags(pcb, TF_CLOSEPEND);return ERR_OK;}return err;
}

大家請(qǐng)看上述有注釋的代碼,這些代碼是客戶端發(fā)送關(guān)閉連接請(qǐng)求報(bào)文過程,該包的首部
包含F(xiàn)IN 標(biāo)志位并調(diào)用函數(shù)tcp_output 發(fā)送到服務(wù)器當(dāng)中。由此可見,客戶端從
ESTABLISHED 狀態(tài)轉(zhuǎn)換成FIN-WAIT-1 狀態(tài),下面筆者使用一個(gè)示意圖來描述上述的內(nèi)容,
如下圖所示:

在這里插入圖片描述

上圖紅色框框表示tcp_close 函數(shù)處理過程,這里也可以稱之為TCP 第一次揮手的動(dòng)作。
(2) 服務(wù)器接收到FIN 報(bào)文并發(fā)送ACK 報(bào)文
當(dāng)服務(wù)器接收到客戶端的FIN 報(bào)文時(shí),它會(huì)進(jìn)入到CLOSE_WAIT 狀態(tài),這個(gè)FIN 報(bào)文交
由tcp_process 函數(shù)處理,當(dāng)然它接收到的數(shù)據(jù)可以發(fā)送給應(yīng)用層,但是它遞交一個(gè)空的EOF
數(shù)據(jù)給應(yīng)用層(應(yīng)用層知道接收數(shù)據(jù)已經(jīng)完成,不需要再從協(xié)議棧讀數(shù)據(jù)),最后系統(tǒng)發(fā)送客
戶端ACK 報(bào)文給客戶端(第二次揮手),進(jìn)入CLOSE_WAIT 狀態(tài),如下源碼所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {/* ...........省略多行代碼........... */switch (pcb - > state) {/* ...........省略多行代碼........... */case ESTABLISHED:tcp_receive(pcb);if (recv_flags & TF_GOT_FIN) { /* 收到FIN被動(dòng)關(guān)閉*/tcp_ack_now(pcb); /* 構(gòu)建ACK報(bào)文*/pcb - > state = CLOSE_WAIT; /* 進(jìn)入CLOSE_WAIT狀態(tài)*/}break;}/* ...........省略多行代碼........... */
}

上述源碼是服務(wù)器接收到客戶端的FIN 報(bào)文時(shí),它構(gòu)建了一個(gè)ACK 報(bào)文發(fā)送到客戶端當(dāng)
中,然后它的狀態(tài)從ESTABLISHED 轉(zhuǎn)換成CLOSE-WAIT,下面筆者也是使用一個(gè)示意圖來
描述上述的內(nèi)容,如下圖所示:

在這里插入圖片描述

上圖紅色框框就是上述源碼運(yùn)行的流程,為了理解,筆者沒有把全部的代碼列舉出來。
(3) 客戶端接收到ACK 報(bào)文并轉(zhuǎn)換成FIN-WAIT-2 狀態(tài)
當(dāng)FIN_WAIT_1 狀態(tài)的客戶端收到服務(wù)器的ACK 報(bào)文時(shí),它的狀態(tài)從FIN-WAIT-1 轉(zhuǎn)換
成FIN-WAIT-2 狀態(tài),這個(gè)過程的源碼如下所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {/* ...........省略多行代碼........... */switch (pcb - > state) {/* ...........省略多行代碼........... */case FIN_WAIT_1:/* 接收數(shù)據(jù)*/tcp_receive(pcb);/* 服務(wù)器還沒有確認(rèn)FIN報(bào)文*/if (recv_flags & TF_GOT_FIN) {/* 非同時(shí)關(guān)閉*/if ((flags & TCP_ACK) && (ackno == pcb - > snd_nxt)) {/* ...........省略多行代碼........... *//* 發(fā)送ACK應(yīng)答對(duì)端的FIN報(bào)文*/tcp_ack_now(pcb);TCP_RMV( & tcp_active_pcbs, pcb); /* 從tcp_active_pcbs刪除tcp_pcb *//* tcp_timewait_input處理,所有數(shù)據(jù)都丟棄,不發(fā)送給應(yīng)用層,直接確認(rèn)當(dāng)前收到的報(bào)文,rcv_nxt設(shè)置為當(dāng)前報(bào)文的下一個(gè)字節(jié)*/pcb - > state = TIME_WAIT;TCP_REG( & tcp_tw_pcbs, pcb); /* 添加tcp_pcb到tcp_tw_pcbs */} else { /* (客戶端、服務(wù)器同時(shí)調(diào)用tcp_close,都在FIN_WAIT_1狀態(tài)收到對(duì)方的FIN報(bào)文)*/tcp_ack_now(pcb); /* 發(fā)送FIN報(bào)文的ACK */pcb - > state = CLOSING; /* 進(jìn)入CLOSING狀態(tài)*/}} /* 服務(wù)器確認(rèn)了FIN報(bào)文*/else if ((flags & TCP_ACK) && (ackno == pcb - > snd_nxt)) {pcb - > state = FIN_WAIT_2; /* 進(jìn)入FIN_WAIT_2狀態(tài)*/}}/* ...........省略多行代碼........... */
}

上述源碼可分為兩個(gè)部分講解,第一部分:處于FIN_WAIT_1 客戶端會(huì)判斷服務(wù)器有沒
有確認(rèn)FIN 報(bào)文,如果它沒有發(fā)送ACK 報(bào)文,則系統(tǒng)進(jìn)入if 語句執(zhí)行,該if 語句的代碼主要
為了判斷服務(wù)器和客戶端是否同時(shí)調(diào)用tcp_close 函數(shù)關(guān)閉連接,如果不同時(shí),則將TCP 控制
塊從tcp_active_pcbs 隊(duì)列移除并設(shè)置該控制塊的狀態(tài)為TIME_WAIT。最后把該控制塊掛在
tcp_tw_pcbs 隊(duì)列當(dāng)中;如果客戶端和服務(wù)器同時(shí)關(guān)閉連接,則系統(tǒng)發(fā)送一個(gè)ACK 報(bào)文到服務(wù)
器當(dāng)中并設(shè)置TCP 控制塊的狀態(tài)為CLOSING;第二部分:服務(wù)器發(fā)送ACK 報(bào)文給客戶端了,
顯然它直接設(shè)置TCP 控制塊的狀態(tài)為FIN_WAIT_2,下面我們使用一個(gè)示意圖來描述上述的
內(nèi)容,如下圖所示:

在這里插入圖片描述

從上圖可知:服務(wù)器的狀態(tài)從FIN-WAIT-1 轉(zhuǎn)換成FIN-WAIT-2 狀態(tài),FIN-WAIT-2 狀態(tài)的
客戶端需要等待服務(wù)器發(fā)送FIN 報(bào)文。
(4) CLOSE-WAIT 狀態(tài)的服務(wù)器發(fā)送FIN 報(bào)文
CLOSE-WAIT 狀態(tài)的服務(wù)器發(fā)送FIN 報(bào)文如下源碼所示:

static err_t
tcp_close_shutdown_fin(struct tcp_pcb * pcb) {err_t err;/* 省略多余的代碼行*/switch (pcb - > state) {case SYN_RCVD:err = tcp_send_fin(pcb);if (err == ERR_OK) {tcp_backlog_accepted(pcb);MIB2_STATS_INC(mib2.tcpattemptfails);pcb - > state = FIN_WAIT_1;}break;case ESTABLISHED:err = tcp_send_fin(pcb);if (err == ERR_OK) {MIB2_STATS_INC(mib2.tcpestabresets);pcb - > state = FIN_WAIT_1;}break;case CLOSE_WAIT:/* 發(fā)送FIN報(bào)文*/err = tcp_send_fin(pcb);if (err == ERR_OK) {MIB2_STATS_INC(mib2.tcpestabresets);/* 設(shè)置狀態(tài)為LAST_ACK */pcb - > state = LAST_ACK;}break;default:return ERR_OK;}/* 發(fā)送關(guān)閉連接請(qǐng)求包*/if (err == ERR_OK) {tcp_output(pcb);} else if (err == ERR_MEM) {tcp_set_flags(pcb, TF_CLOSEPEND);return ERR_OK;}return err;
}

此函數(shù)很簡單,主要發(fā)送FIN 報(bào)文以及設(shè)置CLOSE_WAIT 狀態(tài)的服務(wù)器為LAST_ACK
狀態(tài)。下面筆者也是使用一個(gè)示意圖來描述上述的內(nèi)容,如下圖所示:

在這里插入圖片描述

這里稱之為TCP 第三次揮手,最后就是FIN-WAIT-2 狀態(tài)的客戶端接收服務(wù)器的FIN 報(bào)文
并發(fā)送ACK 報(bào)文確認(rèn)關(guān)閉。
(5) FIN-WAIT-2 狀態(tài)的客戶端接收FIN 報(bào)文并發(fā)送ACK 報(bào)文確認(rèn)
這個(gè)過程是在tcp_input→tcp_ process 函數(shù)下處理,該函數(shù)如下所示:

static err_t
tcp_process(struct tcp_pcb * pcb) {/* ...........省略多行代碼........... */switch (pcb - > state) {/* ...........省略多行代碼........... */case FIN_WAIT_2:/* 接收?qǐng)?bào)文*/tcp_receive(pcb);if (recv_flags & TF_GOT_FIN) {/* 構(gòu)建ACK報(bào)文*/tcp_ack_now(pcb);tcp_pcb_purge(pcb);TCP_RMV_ACTIVE(pcb);/* 設(shè)置狀態(tài)為TIME_WAIT */pcb - > state = TIME_WAIT;TCP_REG( & tcp_tw_pcbs, pcb);}break;}/* ...........省略多行代碼........... */
}

此函數(shù)主要判斷FIN_WAIT_2 狀態(tài)的客戶端是否接收到FIN 報(bào)文,如果系統(tǒng)接收的報(bào)文
是FIN 報(bào)文,則系統(tǒng)發(fā)送ACK 報(bào)文給服務(wù)器并設(shè)置客戶端的狀態(tài)為TIME_WAIT,這里就是
TCP 第四次揮手。

lwIP 中RAW API 編程接口中與TCP 相關(guān)的函數(shù)

tcp.c、tcp.h、tcp_in.c 和tcp_out.c 是lwIP 中關(guān)于TCP 協(xié)議的文件,TCP 層中函數(shù)的關(guān)系
如下圖所示。

在這里插入圖片描述

lwIP 提供了很多關(guān)于TCP 協(xié)議的的RAW 編程API 函數(shù),我們可以使用這些函數(shù)來完成
有關(guān)TCP 的實(shí)驗(yàn),我們?cè)谙卤砹谐隽艘徊糠趾瘮?shù)。

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

RAW 接口的TCP 實(shí)驗(yàn)

硬件設(shè)計(jì)

  1. 例程功能
    本章實(shí)驗(yàn)的目標(biāo)是PC 端和開發(fā)板通過TCP 協(xié)議連接起來,開發(fā)板做TCP 客戶端,PC 端
    的網(wǎng)絡(luò)調(diào)試助手配置成服務(wù)器。開發(fā)板接收服務(wù)器發(fā)送的數(shù)據(jù)在LCD 上顯示,我們也可以通
    過開發(fā)板上的按鍵發(fā)送數(shù)據(jù)給PC。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程3 lwIP_RAW_TCPClient 實(shí)驗(yàn)》。

軟件設(shè)計(jì)

12.2.2.1 TCP 客戶端配置步驟

  1. 創(chuàng)建TCP 控制塊
    調(diào)用函數(shù)tcp_new 創(chuàng)建TCP 控制塊。
  2. 連接指定的IP 地址和端口號(hào)
    調(diào)用函數(shù)tcp_connect 連接到目的地址的指定端口上。
  3. 接收數(shù)據(jù)
    調(diào)用函數(shù)tcp_recved 接收數(shù)據(jù)。
  4. 發(fā)送數(shù)據(jù)
    調(diào)用函數(shù)tcp_write 發(fā)送數(shù)據(jù)。
    12.2.2.2 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示:

在這里插入圖片描述

12.2.2.3 程序解析
本章實(shí)驗(yàn)只講解lwip_demo.c 文件,該文件定義了9 個(gè)函數(shù),這些函數(shù)的作用如下所示:

在這里插入圖片描述

程序首先執(zhí)行l(wèi)wip_demo 函數(shù),此函數(shù)為lwip_demo.c 文件的入口處,如下源碼所示:

/*** @brief lwip_demo程序入口* @param 無* @retval 無*/
void lwip_demo(void) {struct tcp_pcb * tcppcb; /* 定義一個(gè)TCP服務(wù)器控制塊*/ip_addr_t rmtipaddr; /* 遠(yuǎn)端ip地址*/char * tbuf;uint8_t key;uint8_t res = 0;uint8_t t = 0;uint8_t connflag = 0; /* 連接標(biāo)記*/lwip_tcp_client_set_remoteip(); /* 先選擇IP */lcd_clear(BLACK); /* 清屏*/g_point_color = WHITE;lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);lcd_show_string(30, 50, 200, 16, 16, "TCP Client Test", g_point_color);lcd_show_string(30, 70, 200, 16, 16, "ATOM@正點(diǎn)原子", g_point_color);lcd_show_string(30, 90, 200, 16, 16, "KEY0:Send data", g_point_color);lcd_show_string(30, 110, 200, 16, 16, "KEY1:Quit", g_point_color);tbuf = mymalloc(SRAMIN, 200); /* 申請(qǐng)內(nèi)存*/if (tbuf == NULL) return; /* 內(nèi)存申請(qǐng)失敗了,直接退出*/sprintf((char * ) tbuf, "Local IP:%d.%d.%d.%d", lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]); /* 服務(wù)器IP */lcd_show_string(30, 130, 210, 16, 16, tbuf, g_point_color);/* 遠(yuǎn)端IP */sprintf((char * ) tbuf, "Remote IP:%d.%d.%d.%d", lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);sprintf((char * ) tbuf, "Remote Port:%d", TCP_CLIENT_PORT); /* 客戶端端口號(hào)*/lcd_show_string(30, 170, 210, 16, 16, tbuf, g_point_color);g_point_color = BLUE;lcd_show_string(30, 190, 210, 16, 16, "STATUS:Disconnected", g_point_color);tcppcb = tcp_new(); /* 創(chuàng)建一個(gè)新的pcb */if (tcppcb) /* 創(chuàng)建成功*/ {IP4_ADDR( & rmtipaddr, lwipdev.remoteip[0], lwipdev.remoteip[1],lwipdev.remoteip[2], lwipdev.remoteip[3]);/* 連接到目的地址的指定端口上,當(dāng)連接成功后回調(diào)tcp_client_connected()函數(shù)*/tcp_connect(tcppcb, & rmtipaddr, TCP_CLIENT_PORT,lwip_tcp_client_connected);} else res = 1;while (res == 0) {key = key_scan(0);if (key == KEY1_PRES) break;if (key == KEY0_PRES) /* KEY0按下了,發(fā)送數(shù)據(jù)*/ {lwip_tcp_client_usersent(tcppcb); /* 發(fā)送數(shù)據(jù)*/}if (lwip_client_flag & 1 << 6) /* 是否收到數(shù)據(jù)*/ {/* 清上一次數(shù)據(jù)*/lcd_fill(30, 230, lcddev.width - 1, lcddev.height - 1, BLACK);/* 顯示接收到的數(shù)據(jù)*/lcd_show_string(30, 230, lcddev.width - 30, lcddev.height - 230, 16,lwip_client_recvbuf, g_point_color);lwip_client_flag &= ~(1 << 6); /* 標(biāo)記數(shù)據(jù)已經(jīng)被處理了*/}if (lwip_client_flag & 1 << 5) /* 是否連接上*/ {if (connflag == 0) {lcd_show_string(30, 190, lcddev.width - 30,lcddev.height - 190, 16,"STATUS:Connected ",g_point_color); /* 提示消息*/g_point_color = WHITE;lcd_show_string(30, 210, lcddev.width - 30,lcddev.height - 190, 16,"Receive Data:", g_point_color); /* 提示消息*/g_point_color = BLUE;connflag = 1; /* 標(biāo)記連接了*/}} else if (connflag) {lcd_show_string(30, 190, 190, 16, 16, "STATUS:Disconnected",g_point_color);lcd_fill(30, 210, lcddev.width - 1,lcddev.height - 1, BLACK); /* 清屏*/connflag = 0; /* 標(biāo)記連接斷開了*/}lwip_periodic_handle();delay_ms(2);t++;if (t == 200) {/* 未連接上,則嘗試重連*/if (connflag == 0 && (tcp_client_flag & 1 << 5) == 0) {lwip_tcp_client_connection_close(tcppcb, 0); /* 關(guān)閉連接*/tcppcb = tcp_new(); /* 創(chuàng)建一個(gè)新的pcb */if (tcppcb) /* 創(chuàng)建成功*/ {/* 連接到目的地址的指定端口上,當(dāng)連接成功后回調(diào)tcp_client_connected()函數(shù)*/tcp_connect(tcppcb, & rmtipaddr, TCP_CLIENT_PORT,tcp_client_connected);}}t = 0;LED0_TOGGLE();}}lwip_tcp_client_connection_close(tcppcb, 0); /* 關(guān)閉TCP Client連接*/myfree(SRAMIN, tbuf);
}

可見,此函數(shù)和UDP 實(shí)驗(yàn)一樣,根據(jù)開發(fā)板上的KEY0 和KEY1 設(shè)置遠(yuǎn)程IP 地址,接著
調(diào)用RAW 接口函數(shù)配置TCP 客戶端,配置完成之后連接服務(wù)器。
設(shè)置遠(yuǎn)程IP 地址的函數(shù)lwip_tcp_client_set_remoteip,如下源碼所示:

/*** @brief 設(shè)置遠(yuǎn)端IP地址* @param 無* @retval 無*/
void lwip_tcp_client_set_remoteip(void) {char * tbuf;uint16_t xoff;uint8_t key;lcd_clear(BLACK);g_point_color = WHITE;lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);lcd_show_string(30, 50, 200, 16, 16, "TCP Client Test", g_point_color);lcd_show_string(30, 70, 200, 16, 16, "Remote IP Set", g_point_color);lcd_show_string(30, 90, 200, 16, 16, "KEY0:+ KEY2:-", g_point_color);lcd_show_string(30, 110, 200, 16, 16, "KEY_UP:OK", g_point_color);tbuf = mymalloc(SRAMIN, 100); /* 申請(qǐng)內(nèi)存*/if (tbuf == NULL) return;/* 前三個(gè)IP保持和DHCP得到的IP一致*/lwipdev.remoteip[0] = lwipdev.ip[0];lwipdev.remoteip[1] = lwipdev.ip[1];lwipdev.remoteip[2] = lwipdev.ip[2];/* 遠(yuǎn)端IP */sprintf((char * ) tbuf, "Remote IP:%d.%d.%d.", lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2]);lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);g_point_color = BLUE;xoff = strlen((char * ) tbuf) * 8 + 30;lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0, g_point_color);while (1) {key = key_scan(0);if (key == KEY1_PRES) break;else if (key) {if (key == KEY0_PRES) lwipdev.remoteip[3] ++; /* IP增加*/if (key == KEY2_PRES) lwipdev.remoteip[3] --; /* IP減少*//* 顯示新IP */lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0X80,g_point_color);}}myfree(SRAMIN, tbuf);
}

此函數(shù)根據(jù)開發(fā)板上的按鍵設(shè)置遠(yuǎn)程IP 地址,設(shè)置完成之后按下KEY1 退出設(shè)置。
TCP 連接建立后的回調(diào)函數(shù)lwip_tcp_client_connected,如下源碼所示:

/*** @brief lwIP TCP連接建立后回調(diào)函數(shù)* @param arg : 回調(diào)函數(shù)傳入的參數(shù)* @param tpcb : TCP控制塊* @param err : 錯(cuò)誤碼* @retval 返回錯(cuò)誤碼*/
err_t lwip_tcp_client_connected(void * arg, struct tcp_pcb * tpcb, err_t err) {struct tcp_client_struct * es = NULL;if (err == ERR_OK) {es = (struct tcp_client_struct * ) mem_malloc(sizeof(struct tcp_client_struct)); /* 申請(qǐng)內(nèi)存*/if (es) /* 內(nèi)存申請(qǐng)成功*/ {es - > state = ES_TCPCLIENT_CONNECTED; /* 狀態(tài)為連接成功*/es - > pcb = tpcb;es - > p = NULL;tcp_arg(tpcb, es); /* 使用es更新tpcb的callback_arg *//* 初始化LwIP的tcp_recv回調(diào)功能*/tcp_recv(tpcb, lwip_tcp_client_recv);tcp_err(tpcb, lwip_tcp_client_error); /* 初始化tcp_err()回調(diào)函數(shù)*//* 初始化LwIP的tcp_sent回調(diào)功能*/tcp_sent(tpcb, lwip_tcp_client_sent);/* 初始化LwIP的tcp_poll回調(diào)功能*/tcp_poll(tpcb, lwip_tcp_client_poll, 1);tcp_client_flag |= 1 << 5; /* 標(biāo)記連接到服務(wù)器了*/err = ERR_OK;} else {lwip_tcp_client_connection_close(tpcb, es); /* 關(guān)閉連接*/err = ERR_MEM; /* 返回內(nèi)存分配錯(cuò)誤*/}} else {lwip_tcp_client_connection_close(tpcb, 0); /* 關(guān)閉連接*/}return err;
}

這個(gè)回調(diào)函數(shù)由用戶編寫,由tcp_connect 函數(shù)注冊(cè)此函數(shù)。簡單來講,就是讓TCP 控制
塊內(nèi)的函數(shù)指針指向該函數(shù)。
lwip_tcp_client_recv 函數(shù)是當(dāng)接收到數(shù)據(jù)時(shí)的回調(diào)函數(shù),在這個(gè)函數(shù)中我們根據(jù)不同的狀
態(tài)有不同的處理,這里最重要的就是當(dāng)處于連接狀態(tài)并且接收到數(shù)據(jù)時(shí)的處理,這個(gè)時(shí)候我們
將遍歷完接收數(shù)據(jù)的pbuf 鏈表,將鏈表中的所有數(shù)據(jù)拷貝到lwip_tcp_client_recvbuf 中,這個(gè)
過程和UDP 的接收處理過程相似。數(shù)據(jù)接收成功以后我們將lwip_client_flag 的bit5 置1,表
示接收到數(shù)據(jù),lwip_tcp_client_recv 函數(shù)代碼如下。

/*** @brief lwIP tcp_recv()函數(shù)的回調(diào)函數(shù)* @param arg : 回調(diào)函數(shù)傳入的參數(shù)* @param tpcb : TCP控制塊* @param p : 網(wǎng)絡(luò)數(shù)據(jù)包* @param err : 錯(cuò)誤碼* @retval 返回錯(cuò)誤碼*/
err_t lwip_tcp_client_recv(void * arg, struct tcp_pcb * tpcb,struct pbuf * p, err_t err) {uint32_t data_len = 0;struct pbuf * q;struct tcp_client_struct * es;err_t ret_err;LWIP_ASSERT("arg != NULL", arg != NULL);es = (struct tcp_client_struct * ) arg;if (p == NULL) /* 如果從服務(wù)器接收到空的數(shù)據(jù)幀就關(guān)閉連接*/ {es - > state = ES_TCPCLIENT_CLOSING; /* 需要關(guān)閉TCP 連接了*/es - > p = p;ret_err = ERR_OK;} else if (err != ERR_OK) /* 當(dāng)接收到一個(gè)非空的數(shù)據(jù)幀,但是err!=ERR_OK */ {if (p) pbuf_free(p); /* 釋放接收pbuf */ret_err = err;} else if (es - > state == ES_TCPCLIENT_CONNECTED) /* 當(dāng)處于連接狀態(tài)時(shí)*/ {if (p != NULL) /* 當(dāng)處于連接狀態(tài)并且接收到的數(shù)據(jù)不為空時(shí)*/ {/* 數(shù)據(jù)接收緩沖區(qū)清零*/memset(lwip_client_recvbuf, 0, TCP_CLIENT_RX_BUFSIZE);for (q = p; q != NULL; q = q - > next) /* 遍歷完整個(gè)pbuf鏈表*/ {/* 判斷要拷貝到TCP_CLIENT_RX_BUFSIZE中的數(shù)據(jù)是否大于TCP_CLIENT_RX_BUFSIZE的剩余空間,如果大于*//* 的話就只拷貝TCP_CLIENT_RX_BUFSIZE中剩余長度的數(shù)據(jù),否則的話就拷貝所有的數(shù)據(jù)*/if (q - > len > (LWIP_DEMO_RX_BUFSIZE - data_len)) memcpy(lwip_client_recvbuf + data_len, q - > payload, (LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷貝數(shù)據(jù)*/else memcpy(lwip_client_recvbuf + data_len, q - > payload, q - > len);data_len += q - > len;/* 超出TCP客戶端接收數(shù)組,跳出*/if (data_len > LWIP_DEMO_RX_BUFSIZE) break;}tcp_client_flag |= 1 << 6; /* 標(biāo)記接收到數(shù)據(jù)了*//*用于獲取接收數(shù)據(jù),通知LWIP可以獲取更多數(shù)據(jù)*/tcp_recved(tpcb, p - > tot_len);pbuf_free(p); /* 釋放內(nèi)存*/ret_err = ERR_OK;}} else /* 接收到數(shù)據(jù)但是連接已經(jīng)關(guān)閉*/ {/* 用于獲取接收數(shù)據(jù),通知LWIP可以獲取更多數(shù)據(jù)*/tcp_recved(tpcb, p - > tot_len);es - > p = NULL;pbuf_free(p); /* 釋放內(nèi)存*/ret_err = ERR_OK;}return ret_err;
}

lwip_tcp_client_error 函數(shù)是控制塊中errf 字段的回調(diào)函數(shù),當(dāng)出現(xiàn)知名錯(cuò)誤的時(shí)候就會(huì)被
調(diào)用,這里我們沒有實(shí)現(xiàn)這個(gè)函數(shù),用戶可以根據(jù)自己的實(shí)際情況來實(shí)現(xiàn)這個(gè)函數(shù)。
lwip_tcp_client_poll 函數(shù)為控制塊中poll 字段的回調(diào)函數(shù),這個(gè)函數(shù)會(huì)被周期調(diào)用,因此
在這個(gè)函數(shù)中我們可以將要發(fā)送的數(shù)據(jù)發(fā)送出去。通過lwip_client_flag 的bit7 來判斷是否有
數(shù)據(jù)要發(fā)送,因?yàn)閘wIP 中處理數(shù)據(jù)用的是pbuf 結(jié)構(gòu)體組成的鏈表,因此如果有數(shù)據(jù)要發(fā)送的
話就將發(fā)送緩沖區(qū)lwip_tcp_client_sendbuf 中的待發(fā)送數(shù)據(jù)放進(jìn)pbuf 鏈表中,這個(gè)我們使用
pbuf_take 來實(shí)現(xiàn)這個(gè)過程,然后我們調(diào)用lwip_tcp_client_senddata 函數(shù)將數(shù)據(jù)發(fā)送出去,發(fā)送
完成以后記得將lwip_client_flag 的bit7 清零,如下源碼所示:

/*** @brief lwIP tcp_poll的回調(diào)函數(shù)* @param arg : 回調(diào)函數(shù)傳入的參數(shù)* @param tpcb: TCP控制塊* @retval ERR_OK*/
err_t lwip_tcp_client_poll(void * arg, struct tcp_pcb * tpcb) {err_t ret_err;struct tcp_client_struct * es;es = (struct tcp_client_struct * ) arg;if (es - > state == ES_TCPCLIENT_CLOSING) /* 連接斷開*/ {lwip_tcp_client_connection_close(tpcb, es); /* 關(guān)閉TCP連接*/}ret_err = ERR_OK;return ret_err;
}

lwip_tcp_client_sent 函數(shù)為控制塊中的sent 字段的回調(diào)函數(shù),這個(gè)函數(shù)中主要調(diào)用了我們
下面要講的lwip_tcp_client_senddata 這個(gè)函數(shù),lwip_tcp_client_sent 函數(shù)源碼如下。

/*** @brief lwIP tcp_sent的回調(diào)函數(shù)(當(dāng)從遠(yuǎn)端主機(jī)接收到ACK信號(hào)后發(fā)送數(shù)據(jù))* @param arg : 回調(diào)函數(shù)傳入的參數(shù)* @param tpcb: TCP控制塊* @param len : 長度* @retval ERR_OK*/
err_t lwip_tcp_client_sent(void * arg, struct tcp_pcb * tpcb, u16_t len) {struct tcp_client_struct * es;es = (struct tcp_client_struct * ) arg;if (es - > p) lwip_tcp_client_senddata(tpcb, es); /* 發(fā)送數(shù)據(jù)*/return ERR_OK;
}

lwip_tcp_client_senddata 函數(shù)用來發(fā)送數(shù)據(jù),在這個(gè)函數(shù)中我們使用tcp_write 函數(shù)將要發(fā)
送的數(shù)據(jù)加入到發(fā)送緩沖隊(duì)列中,最后調(diào)用tcp_output 函數(shù)將發(fā)送緩沖隊(duì)列中的數(shù)據(jù)發(fā)送出去,
這個(gè)函數(shù)的代碼如下。

/*** @brief 用來發(fā)送數(shù)據(jù)* @param tpcb: TCP控制塊* @param es : LWIP回調(diào)函數(shù)使用的結(jié)構(gòu)體* @retval 無*/
void lwip_tcp_client_senddata(struct tcp_pcb * tpcb,struct tcp_client_struct * es) {struct pbuf * ptr;err_t wr_err = ERR_OK;/* 將要發(fā)送的數(shù)據(jù)加入到發(fā)送緩沖隊(duì)列中*/while ((wr_err == ERR_OK) && es - > p && (es - > p - > len <= tcp_sndbuf(tpcb))) {ptr = es - > p;wr_err = tcp_write(tpcb, ptr - > payload, ptr - > len, 1);if (wr_err == ERR_OK) {es - > p = ptr - > next; /* 指向下一個(gè)pbuf */if (es - > p) pbuf_ref(es - > p); /* pbuf的ref加一*/pbuf_free(ptr); /* 釋放ptr */} else if (wr_err == ERR_MEM) es - > p = ptr;tcp_output(tpcb); /* 將發(fā)送緩沖隊(duì)列中的數(shù)據(jù)立即發(fā)送出去*/}
}

lwip_tcp_client_connection_close 函數(shù)的功能是關(guān)閉與服務(wù)器的連接,通過調(diào)用tcp_abort
函數(shù)來關(guān)閉與服務(wù)器的連接,然后注銷掉控制塊中的回調(diào)函數(shù),將lwip_client_flag 的bit5 置1,
標(biāo)記連接斷開,lwip_tcp_client_connection_close 函數(shù)源碼如下。

/*** @brief 關(guān)閉與服務(wù)器的連接* @param tpcb: TCP控制塊* @param es : LWIP回調(diào)函數(shù)使用的結(jié)構(gòu)體* @retval 無*/
void lwip_tcp_client_connection_close(struct tcp_pcb * tpcb,struct tcp_client_struct * es) {/* 移除回調(diào)*/tcp_abort(tpcb); /* 終止連接,刪除pcb控制塊*/tcp_arg(tpcb, NULL);tcp_recv(tpcb, NULL);tcp_sent(tpcb, NULL);tcp_err(tpcb, NULL);tcp_poll(tpcb, NULL, 0);if (es) mem_free(es);tcp_client_flag &= ~(1 << 5); /* 標(biāo)記連接斷開了*/lcd_show_string(30, 30, 200, 16, 16, "STM32", g_point_color);lcd_show_string(30, 50, 200, 16, 16, "TCPclient Test", g_point_color);lcd_show_string(30, 70, 200, 16, 16, "ATOM@正點(diǎn)原子", g_point_color);lcd_show_string(30, 90, 200, 16, 16, "KEY1:Connect", g_point_color);lcd_show_string(30, 190, 210, 16, 16, "STATUS:Disconnected", g_point_color);
}

至此,lwip_demo.c 文件就講完了,接下來就是編寫main 函數(shù),main 函數(shù)基本和UDP 實(shí)
驗(yàn)的相同。

下載驗(yàn)證

代碼編譯成功之后下載代碼到開發(fā)板中。打開網(wǎng)絡(luò)調(diào)試助手軟件設(shè)置為如下圖的信息。
在這里插入圖片描述

開發(fā)板上電,等待出現(xiàn)12.3.3.2 所示畫面,我們?cè)O(shè)置遠(yuǎn)端IP 地址為電腦的IP 地址,也就
是圖12.3.1 中的本地IP 地址,設(shè)置好以后按KEY_UP 鍵確認(rèn),確認(rèn)后進(jìn)入圖12.2.3.3 所示界
面,當(dāng)STATUS 為Connected 的時(shí)候就可以和網(wǎng)絡(luò)調(diào)試助手互相發(fā)送數(shù)據(jù)了。

在這里插入圖片描述
圖12.2.3.2 設(shè)置服務(wù)器IP 地址

在這里插入圖片描述

圖12.2.3.3 連接到服務(wù)器
我們通過網(wǎng)絡(luò)調(diào)試助手向開發(fā)板發(fā)送:http://www.openedv.com,此時(shí)開發(fā)板LCD 上顯示
接收到的數(shù)據(jù)如圖12.2.3.4 所示,按下KEY0 鍵向網(wǎng)絡(luò)調(diào)試助手發(fā)送數(shù)據(jù)。
在這里插入圖片描述

NETCONN 編程接口TCP 客戶端實(shí)驗(yàn)

本章實(shí)驗(yàn)中開發(fā)板做TCP 客戶端,網(wǎng)絡(luò)調(diào)試助手為TCP 服務(wù)器。開發(fā)板連接到TCP 服務(wù)
器(網(wǎng)絡(luò)調(diào)試助手),網(wǎng)絡(luò)調(diào)試助手給開發(fā)板發(fā)送數(shù)據(jù),開發(fā)板接收數(shù)據(jù)并通過串口將接收到的
數(shù)據(jù)發(fā)送到串口調(diào)試助手上,也可以通過按鍵從開發(fā)板向網(wǎng)絡(luò)調(diào)試助手發(fā)送數(shù)據(jù)。

NETCONN 實(shí)現(xiàn)TCP 客戶端連接步驟

NETCONN 實(shí)現(xiàn)TCP 客戶端連接有以下幾步:
①調(diào)用函數(shù)netconn_new 創(chuàng)建TCP 控制塊。
②調(diào)用函數(shù)netconn_connect 連接服務(wù)器。
③設(shè)置接收超時(shí)時(shí)間tcp_clientconn->recv_timeout。
④調(diào)用函數(shù)netconn_getaddr 獲取遠(yuǎn)端IP 地址和端口號(hào)。
⑤調(diào)用函數(shù)netconn_write 和netconn_recv 收發(fā)數(shù)據(jù)。
至于TCP 協(xié)議的知識(shí),請(qǐng)讀者擦看第十二章的內(nèi)容。

NETCONN 接口的TCPClient 實(shí)驗(yàn)

硬件設(shè)計(jì)

  1. 例程功能
    本實(shí)驗(yàn)使用NETCONN 編程接口實(shí)現(xiàn)TCPClient 連接,我們可通過按下KEY0 按鍵發(fā)送數(shù)
    據(jù)至網(wǎng)絡(luò)調(diào)試助手,還可以接收網(wǎng)絡(luò)調(diào)試助手發(fā)送的數(shù)據(jù),并在LCD 顯示屏上顯示。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程8 lwIP_NETCONN_TCPClient 實(shí)驗(yàn)》。

軟件設(shè)計(jì)

17.2.2.1 netconn 的TCPClient 連接步驟

  1. 創(chuàng)建TCP 控制塊
    調(diào)用函數(shù)netconn_new 創(chuàng)建TCP 控制塊。
  2. 綁定遠(yuǎn)程IP 地址與端口號(hào)
    調(diào)用函數(shù)netconn_connect 綁定遠(yuǎn)程IP 地址和遠(yuǎn)程端口號(hào)。
  3. 接收數(shù)據(jù)
    netconn_recv 接收數(shù)據(jù)。
  4. 發(fā)送數(shù)據(jù)
    調(diào)用函數(shù)netconn_write 發(fā)送數(shù)據(jù)。
    17.2.2.2 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示:

在這里插入圖片描述

17.2.2.3 程序解析
打開我們的例程,找到lwip_demo.c 和lwip_demo.h 兩個(gè)文件,這兩個(gè)文件就是我本章實(shí)
驗(yàn)的源碼,在lwip_demo.c 中我們實(shí)現(xiàn)了一個(gè)函數(shù)lwip_demo,同上一章一樣,都有操作系統(tǒng)
的支持下,如下源碼所示:

void lwip_demo(void) {uint32_t data_len = 0;struct pbuf * q;err_t err, recv_err;ip4_addr_t server_ipaddr, loca_ipaddr;static uint16_t server_port, loca_port;char * tbuf;server_port = LWIP_DEMO_PORT;IP4_ADDR( & server_ipaddr, DEST_IP_ADDR0, DEST_IP_ADDR1,DEST_IP_ADDR2, DEST_IP_ADDR3); /* 構(gòu)造目的IP地址*/tbuf = mymalloc(SRAMIN, 200); /* 申請(qǐng)內(nèi)存*/sprintf((char * ) tbuf, "Port:%d", LWIP_DEMO_PORT); /* 客戶端端口號(hào)*/lcd_show_string(5, 150, 200, 16, 16, tbuf, BLUE);myfree(SRAMIN, tbuf);while (1) {tcp_clientconn = netconn_new(NETCONN_TCP); /*創(chuàng)建一個(gè)TCP鏈接*//*連接服務(wù)器*/err = netconn_connect(tcp_clientconn, & server_ipaddr, server_port);if (err != ERR_OK) {printf("接連失敗\r\n");/*返回值不等于ERR_OK,刪除tcp_clientconn連接*/netconn_delete(tcp_clientconn);} else if (err == ERR_OK) /*處理新連接的數(shù)據(jù)*/ {struct netbuf * recvbuf;tcp_clientconn - > recv_timeout = 10;/*獲取本地IP主機(jī)IP地址和端口號(hào)*/netconn_getaddr(tcp_clientconn, & loca_ipaddr, & loca_port, 1);printf("連接上服務(wù)器%d.%d.%d.%d,本機(jī)端口號(hào)為:%d\r\n",DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3, loca_port);while (1) {/*有數(shù)據(jù)要發(fā)送*/if ((tcp_client_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) {/* 發(fā)送tcp_server_sentbuf中的數(shù)據(jù)*/err = netconn_write(tcp_clientconn, tcp_client_sendbuf,strlen((char * ) tcp_client_sendbuf), NETCONN_COPY);if (err != ERR_OK) {printf("發(fā)送失敗\r\n");}tcp_client_flag &= ~LWIP_SEND_DATA;}/*接收到數(shù)據(jù)*/if ((recv_err = netconn_recv(tcp_clientconn, & recvbuf)) == ERR_OK) {taskENTER_CRITICAL(); /*進(jìn)入臨界區(qū)*//*數(shù)據(jù)接收緩沖區(qū)清零*/memset(lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE);for (q = recvbuf - > p; q != NULL; q = q - > next) /*遍歷完整個(gè)pbuf鏈表*/ {if (q - > len > (LWIP_DEMO_RX_BUFSIZE - data_len)) {memcpy(lwip_demo_recvbuf + data_len, q - > payload, (LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷貝數(shù)據(jù)*/} else {memcpy(lwip_demo_recvbuf + data_len, q - > payload,q - > len);}data_len += q - > len;if (data_len > TCP_CLIENT_RX_BUFSIZE) {break; /*超出TCP客戶端接收數(shù)組,跳出*/}}taskEXIT_CRITICAL(); /*退出臨界區(qū)*/data_len = 0; /*復(fù)制完成后data_len要清零*/printf("%s\r\n", lwip_demo_recvbuf);netbuf_delete(recvbuf);} else if (recv_err == ERR_CLSD) /*關(guān)閉連接*/ {netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);printf("服務(wù)器%d.%d.%d.%d斷開連接\r\n", DEST_IP_ADDR0,DEST_IP_ADDR1, DEST_IP_ADDR2, DEST_IP_ADDR3);lcd_fill(5, 89, lcddev.width, 110, WHITE);break;}}}}
}

上述的源碼結(jié)構(gòu)和上一章節(jié)的UDP 實(shí)驗(yàn)非常相似,它們唯一不同的是連接步驟以及發(fā)送函數(shù)不同,注意:上述函數(shù)做了一個(gè)判斷服務(wù)器與客戶端的連接狀態(tài),如果這個(gè)連接狀態(tài)是斷
開狀態(tài),則系統(tǒng)不斷的調(diào)用函數(shù)netconn_connect 連接服務(wù)器,直到連接成功才進(jìn)入第二個(gè)
while 循環(huán)執(zhí)行發(fā)送接收工作。

下載驗(yàn)證

代碼編譯完成后下載到開發(fā)板中,初始化完成之后我們來看一下LCD 顯示的內(nèi)容,如下
圖所示。

在這里插入圖片描述

我們?cè)趤砜匆幌麓谡{(diào)試助手如圖17.2.3.2 所示,在串口調(diào)試助手上也輸出了我們開發(fā)板
的IP 地址,子網(wǎng)掩碼、默認(rèn)網(wǎng)關(guān)等信息。

在這里插入圖片描述

我們通過網(wǎng)絡(luò)調(diào)試助手發(fā)送數(shù)據(jù)到開發(fā)板當(dāng)中,結(jié)果如圖17.2.3.3 所示,當(dāng)然我們可以通
過開發(fā)板上的KEY0 發(fā)送數(shù)據(jù)到網(wǎng)絡(luò)調(diào)式助手當(dāng)中,如圖17.2.3.4 所示:

在這里插入圖片描述

圖17.2.3.3 LCD 顯示

在這里插入圖片描述
圖17.2.3.4 網(wǎng)絡(luò)調(diào)試助手接收數(shù)據(jù)

Socket 編程接口TCP 客戶端實(shí)驗(yàn)

關(guān)于TCP 協(xié)議的相關(guān)知識(shí),請(qǐng)參考第12 章的內(nèi)容。本章,筆者重點(diǎn)講解lwIP 的Socket
接口如何配置TCP 客戶端,并在此基礎(chǔ)上實(shí)現(xiàn)收發(fā)功能。本章分為如下幾個(gè)部分:
21.1 Socket 編程TCP 客戶端流程
21.2 Socket 接口的TCPClient 實(shí)驗(yàn)

Socket 編程TCP 客戶端流程

實(shí)現(xiàn)TCP 客戶端之前,用戶必須先配置結(jié)構(gòu)體sockaddr_in 的成員變量才能實(shí)現(xiàn)
TCPClient 連接,該配置步驟如下所示:
①sin_family 設(shè)置為AF_INET 表示IPv4 網(wǎng)絡(luò)協(xié)議。
②sin_port 為設(shè)置端口號(hào)。
③sin_addr.s_addr 設(shè)置遠(yuǎn)程IP 地址。
④調(diào)用函數(shù)Socket 創(chuàng)建Socket 連接,注意:該函數(shù)的第二個(gè)參數(shù)SOCK_STREAM 表
示TCP 連接,SOCK_DGRAM 表示UDP 連接。
⑤調(diào)用函數(shù)connect 連接遠(yuǎn)程IP 地址。
⑥調(diào)用收發(fā)函數(shù)實(shí)現(xiàn)遠(yuǎn)程通訊。

Socket 接口的TCPClient 實(shí)驗(yàn)

21.2.1 硬件設(shè)計(jì)

  1. 例程功能
    本實(shí)驗(yàn)使用Socket 編程接口實(shí)現(xiàn)TCPClient 客戶端,并可通過按鍵向所連接的TCP 服務(wù)
    器發(fā)送數(shù)據(jù),也能夠接收來自TCP 服務(wù)器的數(shù)據(jù),并實(shí)時(shí)顯示至LCD 屏幕上。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程11 lwIP_SOCKET_TCPClient 實(shí)驗(yàn)》。
    21.2.2 軟件設(shè)計(jì)
    21.2.2.1 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示:

在這里插入圖片描述

1.2.2.2 程序解析
本實(shí)驗(yàn),我們著重講解lwip_demo.c 文件,該文件實(shí)現(xiàn)了三個(gè)函數(shù),它們分別為
lwip_data_send、lwip_demo 和lwip_send_thread 函數(shù),下面筆者分別地講解它們的實(shí)現(xiàn)功能。

/*** @brief 發(fā)送數(shù)據(jù)線程* @param 無* @retval 無*/
void lwip_data_send(void) {sys_thread_new("lwip_send_thread", lwip_send_thread, NULL,512, LWIP_SEND_THREAD_PRIO);
}

此函數(shù)調(diào)用sys_thread_new 函數(shù)創(chuàng)建發(fā)送數(shù)據(jù)線程,它的線程函數(shù)為lwip_send_thread,
稍后我們重點(diǎn)會(huì)講解。

/*** @brief lwip_demo實(shí)驗(yàn)入口* @param 無* @retval 無*/
void lwip_demo(void) {struct sockaddr_in atk_client_addr;err_t err;int recv_data_len;BaseType_t lwip_err;char * tbuf;lwip_data_send(); /* 創(chuàng)建發(fā)送數(shù)據(jù)線程*/while (1) {sock_start: lwip_connect_state = 0;atk_client_addr.sin_family = AF_INET; /* 表示IPv4網(wǎng)絡(luò)協(xié)議*/atk_client_addr.sin_port = htons(LWIP_DEMO_PORT); /* 端口號(hào)*/atk_client_addr.sin_addr.s_addr = inet_addr(IP_ADDR); /* 遠(yuǎn)程IP地址*/sock = Socket(AF_INET, SOCK_STREAM, 0); /* 可靠數(shù)據(jù)流交付服務(wù)既是TCP協(xié)議*/memset( & (atk_client_addr.sin_zero), 0,sizeof(atk_client_addr.sin_zero));tbuf = mymalloc(SRAMIN, 200); /* 申請(qǐng)內(nèi)存*/sprintf((char * ) tbuf, "Port:%d", LWIP_DEMO_PORT); /* 客戶端端口號(hào)*/lcd_show_string(5, 150, 200, 16, 16, tbuf, BLUE);/* 連接遠(yuǎn)程IP地址*/err = connect(sock, (struct sockaddr * ) & atk_client_addr,sizeof(struct sockaddr));if (err == -1) {printf("連接失敗\r\n");sock = -1;closeSocket(sock);myfree(SRAMIN, tbuf);vTaskDelay(10);goto sock_start;}printf("連接成功\r\n");lwip_connect_state = 1;while (1) {recv_data_len = recv(sock, lwip_demo_recvbuf,LWIP_DEMO_RX_BUFSIZE, 0);if (recv_data_len <= 0) {closeSocket(sock);sock = -1;lcd_fill(5, 89, lcddev.width, 110, WHITE);lcd_show_string(5, 90, 200, 16, 16, "State:Disconnect", BLUE);myfree(SRAMIN, tbuf);goto sock_start;}/* 接收的數(shù)據(jù)*/lwip_err = xQueueSend(Display_Queue, & lwip_demo_recvbuf, 0);if (lwip_err == errQUEUE_FULL) {printf("隊(duì)列Key_Queue已滿,數(shù)據(jù)發(fā)送失敗!\r\n");}vTaskDelay(10);}}
}

根據(jù)21.1 小節(jié)的流程配置server_addr 結(jié)構(gòu)體的字段,配置完成之后調(diào)用connect 連接遠(yuǎn)程
服務(wù)器,接著調(diào)用recv 函數(shù)接收客戶端的數(shù)據(jù),并且把數(shù)據(jù)以消息的方式發(fā)送至其他線程當(dāng)
中。

/*** @brief 發(fā)送數(shù)據(jù)線程函數(shù)* @param pvParameters : 傳入?yún)?shù)(未用到)* @retval 無*/
void lwip_send_thread(void * pvParameters) {pvParameters = pvParameters;err_t err;while (1) {while (1) {if (((lwip_send_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) && (lwip_connect_state == 1)) /* 有數(shù)據(jù)要發(fā)送*/ {err = write(sock, lwip_demo_sendbuf, sizeof(lwip_demo_sendbuf));if (err < 0) {break;}lwip_send_flag &= ~LWIP_SEND_DATA;}vTaskDelay(10);}closeSocket(sock);}
}

此線程函數(shù)非常簡單,它主要判斷l(xiāng)wip_send_flag 變量的狀態(tài),若該變量的狀態(tài)為發(fā)送狀
態(tài),則程序調(diào)用write 函數(shù)發(fā)送數(shù)據(jù),并且清除lwip_send_flag 變量的狀態(tài)。

21.2.3 下載驗(yàn)證
初始化完成之后LCD 顯示以下信息,如下圖所示:

在這里插入圖片描述

我們通過網(wǎng)絡(luò)調(diào)試助手發(fā)送數(shù)據(jù)至開發(fā)板,開發(fā)板接收完成之后LCD 在指定位置顯示接
收的數(shù)據(jù),如下圖所示:

在這里插入圖片描述
當(dāng)然,讀者可通過KEY0 按鍵發(fā)送數(shù)據(jù)至網(wǎng)絡(luò)調(diào)試助手。

基于MQTT 協(xié)議連接阿里云服務(wù)器

本章主要學(xué)習(xí)lwIP 提供的MQTT 協(xié)議文件使用,通過MQTT 協(xié)議將設(shè)備連接到阿里云服
務(wù)器,實(shí)現(xiàn)遠(yuǎn)程互通。由于MQTT 協(xié)議是基于TCP 的協(xié)議實(shí)現(xiàn)的,所以我們只需要在單片機(jī)端實(shí)現(xiàn)TCP 客戶端程序并使用lwIP 提供的MQTT 文件來連接阿里云服務(wù)器。

MQTT 協(xié)議簡介

(1) MQTT 是什么?
MQTT(Message Queuing Telemetry Transport,消息隊(duì)列遙測(cè)傳輸協(xié)議),是一種基于發(fā)布/訂閱(Publish/Subscribe)模式的輕量級(jí)通訊協(xié)議,該協(xié)議構(gòu)建于TCP/IP 協(xié)議上,由IBM 在1999 年發(fā)布,目前最新版本為v3.1.1。MQTT 最大的優(yōu)點(diǎn)在于可以以極少的代碼和有限的帶寬,為遠(yuǎn)程設(shè)備提供實(shí)時(shí)可靠的消息服務(wù)。做為一種低開銷、低帶寬占用的即時(shí)通訊協(xié)議,MQTT在物聯(lián)網(wǎng)、小型設(shè)備、移動(dòng)應(yīng)用等方面有廣泛的應(yīng)用,MQTT 協(xié)議屬于應(yīng)用層。
(2) MQTT 協(xié)議特點(diǎn)
MQTT 是一個(gè)基于客戶端與服務(wù)器的消息發(fā)布/訂閱傳輸協(xié)議。MQTT 協(xié)議是輕量、簡單
開放和易于實(shí)現(xiàn)的,這些特點(diǎn)使它適用范圍非常廣泛。在很多情況下,包括受限境中,如:機(jī)器與機(jī)器(M2M)通信和物聯(lián)網(wǎng)(IoT)。其在,通過衛(wèi)星鏈路通信傳感器、醫(yī)療設(shè)備、智能家居、及一些小型化設(shè)備中已廣泛使用。
(3) MQTT 協(xié)議原理及實(shí)現(xiàn)方式
實(shí)現(xiàn)MQTT 協(xié)議需要:客戶端和服務(wù)器端MQTT 協(xié)議中有三種身份:發(fā)布者(Publish)、
代理(Broker)(服務(wù)器)、訂閱者(Subscribe)。其中,消息的發(fā)布者和訂閱者都是客戶端,消息代理是服務(wù)器,消息發(fā)布者可以同時(shí)是訂閱者,如下圖所示。

在這里插入圖片描述

MQTT 傳輸?shù)南⒎譃?#xff1a;主題(Topic)和消息的內(nèi)容(payload)兩部分。
Topic:可以理解為消息的類型,訂閱者訂閱(Subscribe)后,就會(huì)收到該主題的消息內(nèi)
容(payload)。
Payload:可以理解為消息的內(nèi)容,是指訂閱者具體要使用的內(nèi)容。

MQTT 協(xié)議實(shí)現(xiàn)原理

  1. 要在客戶端與代理服務(wù)端建立一個(gè)TCP 連接,建立連接的過程是由客戶端主動(dòng)發(fā)起的,
    代理服務(wù)一直是處于指定端口的監(jiān)聽狀態(tài),當(dāng)監(jiān)聽到有客戶端要接入的時(shí)候,就會(huì)立刻去處理。
    客戶端在發(fā)起連接請(qǐng)求時(shí),攜帶客戶端ID、賬號(hào)、密碼(無賬號(hào)密碼使用除外,正式項(xiàng)目不會(huì)允許這樣)、心跳間隔時(shí)間等數(shù)據(jù)。代理服務(wù)收到后檢查自己的連接權(quán)限配置中是否允許該賬號(hào)密碼連接,如果允許則建立會(huì)話標(biāo)識(shí)并保存,綁定客戶端ID 與會(huì)話,并記錄心跳間隔時(shí)間(判斷是否掉線和啟動(dòng)遺囑時(shí)用)和遺囑消息等,然后回發(fā)連接成功確認(rèn)消息給客戶端,客戶端收到連接成功的確認(rèn)消息后,進(jìn)入下一步(通常是開始訂閱主題,如果不需要訂閱則跳過)。如下圖所示:

在這里插入圖片描述

  1. 客戶端將需要訂閱的主題經(jīng)過SUBSCRIBE 報(bào)文發(fā)送給代理服務(wù),代理服務(wù)則將這個(gè)主
    題記錄到該客戶端ID 下(以后有這個(gè)主題發(fā)布就會(huì)發(fā)送給該客戶端),然后回復(fù)確認(rèn)消息SUBACK 報(bào)文,客戶端接到SUBACK 報(bào)文后知道已經(jīng)訂閱成功,則處于等待監(jiān)聽代理服務(wù)推送的消息,也可以繼續(xù)訂閱其他主題或發(fā)布主題,如下圖所示:

在這里插入圖片描述

  1. 當(dāng)某一客戶端發(fā)布一個(gè)主題到代理服務(wù)后,代理服務(wù)先回復(fù)該客戶端收到主題的確認(rèn)消
    息,該客戶端收到確認(rèn)后就可以繼續(xù)自己的邏輯了。但這時(shí)主題消息還沒有發(fā)給訂閱了這個(gè)主題的客戶端,代理要根據(jù)質(zhì)量級(jí)別(QoS)來決定怎樣處理這個(gè)主題。所以這里充分體現(xiàn)了是MQTT 協(xié)議是異步通信模式,不是立即端到端反應(yīng)的,如下圖所示:

在這里插入圖片描述

如果發(fā)布和訂閱時(shí)的質(zhì)量級(jí)別QoS 都是至多一次,那代理服務(wù)則檢查當(dāng)前訂閱這個(gè)主題
的客戶端是否在線,在線則轉(zhuǎn)發(fā)一次,收到與否不再做任何處理。這種質(zhì)量對(duì)系統(tǒng)壓力最小。

如果發(fā)布和訂閱時(shí)的質(zhì)量級(jí)別QoS 都是至少一次,那要保證代理服務(wù)和訂閱的客戶端都
有成功收到才可以,否則會(huì)嘗試補(bǔ)充發(fā)送(具體機(jī)制后面討論)。這也可能會(huì)出現(xiàn)同一主題多次重復(fù)發(fā)送的情況。這種質(zhì)量對(duì)系統(tǒng)壓力較大。

如果發(fā)布和訂閱時(shí)的質(zhì)量級(jí)別QoS 都是只有一次,那要保證代理服務(wù)和訂閱的客戶端都
有成功收到,并只收到一次不會(huì)重復(fù)發(fā)送(具體機(jī)制后面討論)。這種質(zhì)量對(duì)系統(tǒng)壓力最大。

移植MQTT 協(xié)議

其實(shí)移植lwIP 的MQTT 文件是非常簡單的,只將lwip\src\apps\mqt 路徑下的mqtt.c 文件
添加到工程當(dāng)中,這里我們?cè)诠こ讨刑砑右粋€(gè)名為Middlewares/lwip/src/apps 分組,該分組用來添加lwIP 應(yīng)用層的文件,如下圖所示所示:

在這里插入圖片描述

mqtt.c 文件是lwIP 根據(jù)MQTT 協(xié)議規(guī)則編寫而來的,如果用戶不使用這個(gè)文件,請(qǐng)自行
移植MQTT 協(xié)議包。

在Middlewares/lwip/lwip_app 分組添加hmac_sha1 和sha1 文件,這些文件用來計(jì)算核心密
鑰,這兩個(gè)文件可在阿里云官方下載。

配置遠(yuǎn)程服務(wù)器

配置阿里云服務(wù)器步驟
第一步:注冊(cè)阿里云平臺(tái),打開產(chǎn)品分類/物聯(lián)網(wǎng)Iot/物聯(lián)網(wǎng)應(yīng)用開發(fā),如下圖所示。

在這里插入圖片描述

點(diǎn)擊上圖中的“立刻使用”按鍵進(jìn)去物聯(lián)網(wǎng)應(yīng)用開發(fā)頁面。

第二步:在物聯(lián)網(wǎng)應(yīng)用開發(fā)頁面下點(diǎn)擊項(xiàng)目管理/新建項(xiàng)目/新建空白項(xiàng)目,在此界面下填
寫項(xiàng)目名稱等相關(guān)信息,如下圖所示:

在這里插入圖片描述

創(chuàng)建項(xiàng)目完成之后在項(xiàng)目管理頁面下點(diǎn)擊項(xiàng)目進(jìn)去子項(xiàng)目管理界面,如下圖所示:

在這里插入圖片描述

第三步:在上圖中點(diǎn)擊產(chǎn)品,如下圖所示:

在這里插入圖片描述

注:上圖中的節(jié)點(diǎn)類型、連網(wǎng)方式、數(shù)據(jù)格式以及認(rèn)證模式的選擇,其他產(chǎn)品參數(shù)根據(jù)用
戶愛好設(shè)置。

第三步:創(chuàng)建產(chǎn)品之后點(diǎn)擊圖26.1.3.3 中的設(shè)備選項(xiàng)添加設(shè)備,如下圖所示。

在這里插入圖片描述

第五步:在設(shè)備頁面下找到我們剛剛創(chuàng)建的設(shè)備,如下圖所示。

在這里插入圖片描述

這三個(gè)參數(shù)非常重要!!!!!!!!!!,在本章實(shí)驗(yàn)中會(huì)用到。
第六步:打開“產(chǎn)品/查看/功能定義”路徑,在該路徑下添加功能定義,如下圖所示。
在這里插入圖片描述

第七步:打開自定義功能并發(fā)布上線,這里我們添加了兩個(gè)CurrentTemperature 和
RelativeHumidity 標(biāo)簽。

阿里云MQTT 協(xié)議實(shí)驗(yàn)

硬件設(shè)計(jì)

  1. 例程功能
    本章的目標(biāo)是,lwIP 連接阿里云實(shí)現(xiàn)數(shù)據(jù)上存。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程17 lwIP_Aliyun_MQTT 實(shí)驗(yàn)》。
    26.2.2 軟件設(shè)計(jì)
    26.2.2.1 MQTT 配置步驟
  1. 配置MCU 為TCP 客戶端模式
    配置為TCP 客戶端等步驟,請(qǐng)參考第21 章。
  2. DNS 解析阿里云網(wǎng)頁轉(zhuǎn)成IP 地址
    調(diào)用函數(shù)gethostbyname 獲取DNS 解析的IP 地址。
  3. MQTT 連接
    調(diào)用函數(shù)mqtt_client_connect 連接服務(wù)器。
  4. 連接狀態(tài)
    對(duì)服務(wù)器發(fā)布和訂閱操作。
  5. 循環(huán)發(fā)布數(shù)據(jù)到服務(wù)器當(dāng)中
    在lwip_demo 函數(shù)的while()語句中定時(shí)1s 調(diào)用函數(shù)mqtt_publish 發(fā)布數(shù)據(jù)至服務(wù)器。
    26.2.2.2 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示。

在這里插入圖片描述

程序解析
我們打開lwip_deom.h 文件,在這個(gè)文件中我們定義了阿里云服務(wù)器創(chuàng)建設(shè)備的配置項(xiàng),
另外還聲明了lwip_demo 函數(shù),關(guān)于阿里云服務(wù)器的MQTT 主題請(qǐng)大家查看阿里云相關(guān)手冊(cè)。

重點(diǎn)關(guān)注的是lwip_deom.c 這個(gè)文件,在這個(gè)文件定義了8 個(gè)函數(shù),如下表所示。

在這里插入圖片描述
我們首先看一下lwip_demo 函數(shù),該函數(shù)的代碼如下。

/*** @brief lwip_demo進(jìn)程* @param 無* @retval 無*/
void lwip_demo(void) {struct hostent * server;static struct mqtt_connect_client_info_t mqtt_client_info;server = gethostbyname((char * ) HOST_NAME); /* 對(duì)oneNET服務(wù)器地址解析*//* 把解析好的地址存放在mqtt_ip變量當(dāng)中*/memcpy( & mqtt_ip, server - > h_addr, server - > h_length);char * PASSWORD;PASSWORD = mymalloc(SRAMIN, 300); /* 為密碼申請(qǐng)內(nèi)存*//* 通過hmac_sha1算法得到password */lwip_ali_get_password(DEVICE_SECRET, CONTENT, PASSWORD);/* 設(shè)置一個(gè)空的客戶端信息結(jié)構(gòu)*/memset( & mqtt_client_info, 0, sizeof(mqtt_client_info));/* 設(shè)置客戶端的信息量*/mqtt_client_info.client_id = (char * ) CLIENT_ID; /* 設(shè)備名稱*/mqtt_client_info.client_user = (char * ) USER_NAME; /* 產(chǎn)品ID */mqtt_client_info.client_pass = (char * ) PASSWORD; /* 計(jì)算出來的密碼*/mqtt_client_info.keep_alive = 100; /* ?;顣r(shí)間*/mqtt_client_info.will_msg = NULL;mqtt_client_info.will_qos = NULL;mqtt_client_info.will_retain = 0;mqtt_client_info.will_topic = 0;myfree(SRAMIN, PASSWORD); /* 釋放內(nèi)存*//* 創(chuàng)建MQTT客戶端控制塊*/mqtt_client = mqtt_client_new();/* 連接服務(wù)器*/mqtt_client_connect(mqtt_client, /* 服務(wù)器控制塊*/ & mqtt_ip, MQTT_PORT, /* 服務(wù)器IP與端口號(hào)*/mqtt_connection_cb,/* 設(shè)置服務(wù)器連接回調(diào)函數(shù)*/LWIP_CONST_CAST(void * , & mqtt_client_info), & mqtt_client_info); /* MQTT連接信息*/while (1) {if (publish_flag == 1) {temp = 30 + rand() % 10 + 1; /* 溫度的數(shù)據(jù)*/humid = 54.8 + rand() % 10 + 1; /* 濕度的數(shù)據(jù)*/sprintf((char * ) payload_out,"{\"params\":{\"CurrentTemperature\":+ % 0.1 f, \"RelativeHumidity\":%0.1f},\"method\":\"thing.event.property.post\"}", temp, humid);payload_out_len = strlen((char * ) payload_out);mqtt_publish(mqtt_client, DEVICE_PUBLISH, payload_out,payload_out_len, 1, 0, mqtt_publish_request_cb, NULL);}vTaskDelay(1000);}
}

此函數(shù)非常簡單,首先我們調(diào)用gethostbyname 函數(shù)解析阿里云的域名,根據(jù)這個(gè)域名來
連接阿里云服務(wù)器,其次使用一個(gè)結(jié)構(gòu)體配置MQTT 客戶端的信息并調(diào)用mqtt_client_new 函數(shù)創(chuàng)建MQTT 服務(wù)器控制塊,接著我們調(diào)用mqtt_client_connect 函數(shù)連接阿里云服務(wù)器并添加mqtt_connection_cb 連接回調(diào)函數(shù),最后在while()語句中判斷是否訂閱操作成功,如果系統(tǒng)訂閱成功,則構(gòu)建MQTT 消息,并調(diào)用mqtt_publish 函數(shù)發(fā)布。

接下來我們來講解一下mqtt_client_connect 函數(shù)的作用,如下源碼所示:

/*** @brief mqtt連接回調(diào)函數(shù)* @param client:客戶端控制塊* @param arg:傳入的參數(shù)* @param status:連接狀態(tài)* @retval 無*/
static void mqtt_connection_cb(mqtt_client_t * client, void * arg,mqtt_connection_status_t status) {err_t err;const struct mqtt_connect_client_info_t * client_info =(const struct mqtt_connect_client_info_t * ) arg;LWIP_UNUSED_ARG(client);printf("\r\nMQTT client \"%s\" connection cb: status %d\r\n",client_info - > client_id, (int) status);/* 判斷是否連接*/if (status == MQTT_CONNECT_ACCEPTED) {/* 判斷是否連接*/if (mqtt_client_is_connected(client)) {/* 設(shè)置傳入發(fā)布請(qǐng)求的回調(diào)*/mqtt_set_inpub_callback(mqtt_client,mqtt_incoming_publish_cb,mqtt_incoming_data_cb,NULL);/* 訂閱操作,并設(shè)置訂閱響應(yīng)會(huì)回調(diào)函數(shù)mqtt_sub_request_cb */err = mqtt_subscribe(client, DEVICE_SUBSCRIBE, 1,mqtt_request_cb, arg);if (err == ERR_OK) {printf("mqtt_subscribe return: %d\n", err);lcd_show_string(5, 170, 210, 16, 16,"mqtt_subscribe succeed", BLUE);}}} else /* 連接失敗*/ {printf("mqtt_connection_cb: Disconnected, reason: %d\n", status);}
}

此函數(shù)也是非常簡單,它主要調(diào)用函數(shù)mqtt_client_is_connected 判斷是否已經(jīng)連接服務(wù)器,如果連接成功,則程序調(diào)用函數(shù)mqtt_set_inpub_callback 添加mqtt_incoming_publish_cb 和mqtt_incoming_data_cb 回調(diào)函數(shù),這些回調(diào)函數(shù)需要根據(jù)客戶端以及服務(wù)器的發(fā)布操作才能進(jìn)去該回調(diào)函數(shù),最后我們調(diào)用函數(shù)mqtt_subscribe 對(duì)服務(wù)器進(jìn)行訂閱操作并且添加mqtt_request_cb 訂閱響應(yīng)回調(diào)函數(shù)。

下載驗(yàn)證

下載完代碼后,在瀏覽器上打開阿里云平臺(tái),并在指定的網(wǎng)頁查看上存數(shù)據(jù),如下圖所示。

在這里插入圖片描述

基于MQTT 協(xié)議連接OneNET 服務(wù)器

本章主要介紹lwIP 如何通過MQTT 協(xié)議將設(shè)備連接到OneNET 平臺(tái),并通過MQTT 協(xié)議
遠(yuǎn)程互通。關(guān)于MQTT 協(xié)議的知識(shí),請(qǐng)參考第二十六章節(jié)的內(nèi)容。

配置OneNET 平臺(tái)

配置OneNET 服務(wù)器步驟:
第一步:首先打開OneNET 服務(wù)器并注冊(cè)賬號(hào),注冊(cè)之后在主界面下打開產(chǎn)品服務(wù)頁面下
的MQTT 物聯(lián)網(wǎng)套件,如下圖所示:

在這里插入圖片描述

第二步:在上圖中點(diǎn)擊“立刻使用”選項(xiàng),頁面跳轉(zhuǎn)完成之后點(diǎn)擊“添加產(chǎn)品”選項(xiàng),此
時(shí)該頁面會(huì)彈出產(chǎn)品信息小界面,這里我們根據(jù)自己的項(xiàng)目填寫相關(guān)的信息,如下圖所示:

在這里插入圖片描述

上圖中,我們重點(diǎn)添加的選項(xiàng)有聯(lián)網(wǎng)方式和設(shè)備接入?yún)f(xié)議,這里筆者選擇移動(dòng)蜂窩網(wǎng)絡(luò)以
及MQTT 協(xié)議接入,至于其他選項(xiàng)根據(jù)愛好選擇。創(chuàng)建MQTT 產(chǎn)品之后用戶可以得到該產(chǎn)品
的信息,如下圖所示:

在這里插入圖片描述

本實(shí)驗(yàn)會(huì)用到上述的產(chǎn)品信息,例如產(chǎn)品ID(366007)、“access_key”產(chǎn)品密鑰以及產(chǎn)品
名稱(MQTT_TSET)等。
第三步:在產(chǎn)品頁面下點(diǎn)擊設(shè)備列表添加設(shè)備,如下圖所示:

在這里插入圖片描述

第四步:在上圖創(chuàng)建的設(shè)備中,點(diǎn)擊右邊的詳情標(biāo)簽進(jìn)入標(biāo)簽的鏈接頁面,在這個(gè)頁面下
我們得到以下設(shè)備信息,如下圖所示:

在這里插入圖片描述

本實(shí)驗(yàn)會(huì)用到上圖中的設(shè)備ID(617747917)、設(shè)備名稱MQTT 以及“key”設(shè)備的密鑰。
下面我們打開OneNET 在線開發(fā)指南,在這個(gè)指南中找到服務(wù)器地址,這些服務(wù)器地址就
是MQTT 服務(wù)器地址,如下圖所示:

在這里插入圖片描述

上圖中,OneNTE 的MQTT 服務(wù)器具有兩個(gè)連接方式,一種是加密接口連接,而另一種
是非加密接口連接,本章實(shí)驗(yàn)使用的是非加密接口連接MQTT 服務(wù)器。
注:MQTT 物聯(lián)網(wǎng)套件采用安全鑒權(quán)策略進(jìn)行訪問認(rèn)證,即通過核心密鑰計(jì)算的token 進(jìn)
行訪問認(rèn)證,簡單來講,用戶想連接OneNET 的MQTT 服務(wù)器必須計(jì)算核心密鑰,這個(gè)密鑰
是根據(jù)我們前面創(chuàng)建的產(chǎn)品和設(shè)備相關(guān)的信息計(jì)算得來的,密鑰的計(jì)算方法可以使用OneNET
提供的token 生成工具計(jì)算,該軟件可在這個(gè)網(wǎng)址下載:https://open.iot.10086.cn/doc/v5/develo
p/detail/242。
下面筆者簡單講解一下token 生成工具的使用,如圖27.1.1.7 所示:

在這里插入圖片描述

res:輸入格式為“products/{pid}/devices/{device_name}”,這個(gè)輸入格式中的“pid”就是
我們MQTT 產(chǎn)品ID,而“device_name”就是設(shè)備的名稱。根據(jù)前面創(chuàng)建的產(chǎn)品和設(shè)備來填寫
res 選項(xiàng)的參數(shù),如下圖所示:

在這里插入圖片描述

et:訪問過期時(shí)間(expirationTime,unix)時(shí)間,這里筆者選擇參考文檔中的數(shù)值
(1672735919),如下圖所示:

在這里插入圖片描述
key:指選擇設(shè)備的key 密鑰,如下圖所示:

在這里插入圖片描述

最后按下上圖中的“Generate”按鍵生成核心密鑰,如下圖所示。

在這里插入圖片描述

這個(gè)核心密鑰會(huì)在MQTT 客戶端的結(jié)構(gòu)體client_pass 成員變量保存。

工程配置

小上節(jié)我們使用token 生成工具根據(jù)產(chǎn)品信息以及設(shè)備信息來計(jì)算核心密鑰,這樣的方式
導(dǎo)致每次創(chuàng)建一個(gè)設(shè)備都必須根據(jù)這個(gè)設(shè)備信息再一次計(jì)算核心密鑰才能連接,這種方式會(huì)大
大地降低我們的開發(fā)效率,為了解決這個(gè)問題,筆者使用另一個(gè)方法,那就是使用代碼的方式
計(jì)算核心密鑰,它和上一章節(jié)中的方式不一樣,因?yàn)榘⒗镌坪蚈neNET 計(jì)算的方式不同,所
以不能使用阿里云的那兩個(gè)文件來計(jì)算OneNET 的密鑰。OneOS 源碼中有幾個(gè)文件是用來計(jì)
算MQTT 協(xié)議連接OneNET 平臺(tái)的核心密鑰,這些文件在oneos2.0\components\cloud\onenet\m
qtt-kit\authorization 路徑下,大家先下載OneOS 源碼并在該路徑下復(fù)制這些文件到工程當(dāng)中。
打開工程并在Middlewares/lwip/lwip_app 分組下添加以下文件,如下圖所示:

在這里插入圖片描述

這些文件都在oneos2.0\components\cloud\onenet\mqtt-kit\authorization 路徑下獲取。

基于OneNET 平臺(tái)MQTT 實(shí)驗(yàn)

硬件設(shè)計(jì)

  1. 例程功能
    本章目標(biāo)是開發(fā)板使用MQTT 協(xié)議連接OneNET 服務(wù)器,并實(shí)現(xiàn)數(shù)據(jù)上存更新。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程18 lwIP_OneNET_MQTT 實(shí)驗(yàn)》。
    27.3.2 軟件設(shè)計(jì)
    27.3.2.1 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示。

在這里插入圖片描述
程序解析

我們打開lwip_deom.h 文件,在這個(gè)文件中我們定義了OneNET 服務(wù)器創(chuàng)建設(shè)備的配置項(xiàng),
另外還聲明了lwip_demo 函數(shù),關(guān)于OneNET 服務(wù)器的MQTT 主題請(qǐng)大家查看OneNET 相關(guān)
手冊(cè),該手冊(cè)地址為https://open.iot.10086.cn/doc/v5/develop/detail/251,這個(gè)地址里面已經(jīng)說明

了OneNET 的MQTT 服務(wù)器相關(guān)主題信息。至于lwip_deom.c 文件前面我們已經(jīng)講解過了,它
們唯一不同的是計(jì)算核心密鑰方式。

下載驗(yàn)證

我們編譯代碼,并把下載到開發(fā)板上運(yùn)行,打開OneNET 的MQTT 服務(wù)器查看數(shù)據(jù)流展
示,如下圖所示。

在這里插入圖片描述

HTTP 客戶端實(shí)驗(yàn)

HTTP 客戶端用于實(shí)現(xiàn)平臺(tái)與應(yīng)用服務(wù)器之間的單向數(shù)據(jù)通信。平臺(tái)作為客戶端,通過
HTTP/HTTPS 請(qǐng)求方式,將項(xiàng)目下應(yīng)用數(shù)據(jù)、設(shè)備數(shù)據(jù)推送給用戶指定服務(wù)器。本章主要介
紹lwIP 如何通過HTTP 協(xié)議將設(shè)備連接到OneNET 平臺(tái),并實(shí)現(xiàn)遠(yuǎn)程互通。

OneNTE 的HTTP 配置

關(guān)于OneNET 平臺(tái)HTTP 接入方式可參考該官方的文檔手冊(cè),該文檔手冊(cè)地址為https://op
en.iot.10086.cn/,本實(shí)驗(yàn)主要參考官方文檔的多協(xié)議接入/HTTP/上傳數(shù)據(jù)點(diǎn)的內(nèi)容。
OneNTE 的HTTP 服務(wù)器流程
第一步:注冊(cè)O(shè)neNTE 服務(wù)器賬號(hào),注冊(cè)完成之后打開右上角的控制臺(tái)/ 全部產(chǎn)品服務(wù)/多
協(xié)議接入,如下圖所示。

在這里插入圖片描述

第二步:選擇HTTP 協(xié)議/添加產(chǎn)品。
第三步:填寫產(chǎn)品信息,如下圖所示。

在這里插入圖片描述

上圖中的幾個(gè)技術(shù)參數(shù)非常重要,剩下的技術(shù)參數(shù)根據(jù)用戶的愛好填寫。

第四步:雙擊創(chuàng)建的產(chǎn)品并點(diǎn)擊設(shè)備列表且在設(shè)備列表中添加設(shè)備,如下圖所示。

在這里插入圖片描述

這些參數(shù)用戶可以隨便填寫。
第五步:打開數(shù)據(jù)流,如下圖所示。

在這里插入圖片描述

第六步:打開數(shù)據(jù)流管理/添加數(shù)據(jù)流模板,如下圖所示。

在這里插入圖片描述

注意:上圖的數(shù)據(jù)名稱必須與程序發(fā)送數(shù)據(jù)的標(biāo)志一樣。
第七步:打開設(shè)備列表/設(shè)備詳情查看設(shè)備信息,如下圖所示。

在這里插入圖片描述

上圖中的設(shè)備ID 和APIKey 是我們需要的信息。

HTTP 客戶端實(shí)驗(yàn)

硬件設(shè)計(jì)

  1. 例程功能
    本章目標(biāo)是開發(fā)板使用HTTP 協(xié)議連接OneNET 服務(wù)器,并實(shí)現(xiàn)溫濕度上報(bào)。
    該實(shí)驗(yàn)的實(shí)驗(yàn)工程,請(qǐng)參考《lwIP 例程19 lwIP_OneNET_HTTP 實(shí)驗(yàn)》。

軟件設(shè)計(jì)

28.3.2.1 HTTP 配置步驟

  1. 配置MCU 為TCP 客戶端模式
    配置為TCP 客戶端等步驟,請(qǐng)參考第21 章。
  2. 數(shù)據(jù)合并操作
    調(diào)用函數(shù)lwip_onehttp_postpkt 把OneNET 產(chǎn)品的設(shè)備ID 和OneNET 設(shè)備的設(shè)備api 參數(shù)
    合拼成一個(gè)字符串。
  3. 發(fā)送數(shù)據(jù)
    調(diào)用函數(shù)netconn_write 把上述的postpkt 發(fā)送到OneNET 服務(wù)器平臺(tái)。
    28.3.2.2 程序流程圖
    本實(shí)驗(yàn)的程序流程圖,如下圖所示。

在這里插入圖片描述

程序解析
本章實(shí)驗(yàn)中我們重點(diǎn)講解lwip_demo.c 和lwip_demo.h。lwip_demo.h 文件很簡單,主要聲
明OneNET 平臺(tái)的設(shè)備ID 和設(shè)備密鑰,而lwip_demo.c 文件定義了2 個(gè)函數(shù),這些函數(shù)的作
用如下表所示。

在這里插入圖片描述

  1. lwip_onehttp_postpkt 函數(shù)
    把數(shù)值封裝至HTTP 數(shù)據(jù)包中,如下源碼所示:
uint32_t lwip_onehttp_postpkt(char * pkt, /* 保存的數(shù)據(jù)*/char * key, /* 連接onenet的apikey */char * devid, /* 連接onenet的onenet_id */char * dsid, /* onenet的顯示字段*/char * val) /* 該字段的值*/ {char dataBuf[100] = {0};char lenBuf[10] = {0}; * pkt = 0;sprintf(dataBuf, ",;%s,%s", dsid, val); /* 采用分割字符串格式:type = 5 */sprintf(lenBuf, "%d", strlen(dataBuf));strcat(pkt, "POST /devices/");strcat(pkt, devid);strcat(pkt, "/datapoints?type=5 HTTP/1.1\r\n");strcat(pkt, "api-key:");strcat(pkt, key);strcat(pkt, "\r\n");strcat(pkt, "Host:api.heclouds.com\r\n");strcat(pkt, "Content-Length:");strcat(pkt, lenBuf);strcat(pkt, "\r\n\r\n");strcat(pkt, dataBuf);return strlen(pkt);
}

上述源碼主要采用典型的C 語言基礎(chǔ),調(diào)用函數(shù)strcat 把兩個(gè)字符串拼接成一個(gè)字符串,
如果我們使用網(wǎng)絡(luò)調(diào)試助手接收該數(shù)據(jù)包,那么我們發(fā)現(xiàn)該數(shù)據(jù)與OneNET 平臺(tái)HTTP 協(xié)議
接入文檔描述是一致,該數(shù)據(jù)如下所示:

POST /devices/655766336/datapoints?type=5 HTTP/1.1
api-key:rw2p2Fq=VW4fhhhkj4CwpVcqJq8=
Host:api.heclouds.com
Content-Length:13
,;humidity,00
POST /devices/655766336/datapoints?type=5 HTTP/1.1
api-key:rw2p2Fq=VW4fhhhkj4CwpVcqJq8=
Host:api.heclouds.com
Content-Length:16
,;temperature,00
  1. lwip_demo 函數(shù)
    此函數(shù)非常簡單,它用來配置網(wǎng)絡(luò)環(huán)境即以TCP 協(xié)議連接OneNET 服務(wù)器。連接完成之后發(fā)送HTTP 數(shù)據(jù)包至服務(wù)器當(dāng)中。
/*** @brief lwip_demo程序入口* @param 無* @retval 無*/
void lwip_demo(void) {uint32_t data_len = 0;struct pbuf * q;err_t err;ip4_addr_t server_ipaddr, loca_ipaddr;static uint16_t server_port, loca_port;server_port = TCP_DEMO_PORT;netconn_gethostbyname(DEST_MANE, & server_ipaddr);while (1) {atk_start: tcp_clientconn = netconn_new(NETCONN_TCP); /* 創(chuàng)建一個(gè)TCP鏈接*//* 連接服務(wù)器*/err = netconn_connect(tcp_clientconn, & server_ipaddr, server_port);if (err != ERR_OK) {printf("接連失敗\r\n");/* 返回值不等于ERR_OK,刪除tcp_clientconn連接*/netconn_delete(tcp_clientconn);} else if (err == ERR_OK) /* 處理新連接的數(shù)據(jù)*/ {struct netbuf * recvbuf;tcp_clientconn - > recv_timeout = 10;/* 獲取本地IP主機(jī)IP地址和端口號(hào)*/netconn_getaddr(tcp_clientconn, & loca_ipaddr, & loca_port, 1);lcd_show_string(5, 170, 200, 16, 16, "link succeed", BLUE);while (1) {temp_rh[0] = 30 + rand() % 10 + 1; /* 溫度的數(shù)據(jù)*/temp_rh[1] = 54.8 + rand() % 10 + 1; /* 濕度的數(shù)據(jù)*/tempStr[0] = temp_rh[0] / 10 + 0x30; /* 上傳溫度*/tempStr[1] = temp_rh[0] % 10 + 0x30;;humiStr[0] = temp_rh[1] / 10 + 0x30; /* 上傳濕度*/humiStr[1] = temp_rh[1] % 10 + 0x30;len = lwip_onehttp_postpkt(buffer, apikey,onenet_id, "temperature", tempStr);/* 發(fā)送tcp_server_sentbuf中的數(shù)據(jù)*/netconn_write(tcp_clientconn, buffer, len, NETCONN_COPY);len = lwip_onehttp_postpkt(buffer, apikey,onenet_id, "humidity", humiStr);/* 發(fā)送tcp_server_sentbuf中的數(shù)據(jù)*/netconn_write(tcp_clientconn, buffer, len, NETCONN_COPY);vTaskDelay(1000);/* 接收到數(shù)據(jù)*/if (netconn_recv(tcp_clientconn, & recvbuf) == ERR_OK) {taskENTER_CRITICAL(); /* 進(jìn)入臨界區(qū)*//* 數(shù)據(jù)接收緩沖區(qū)清零*/memset(tcp_client_recvbuf, 0, TCP_CLIENT_RX_BUFSIZE);/*遍歷完整個(gè)pbuf鏈表*/for (q = recvbuf - > p; q != NULL; q = q - > next) {if (q - > len > (TCP_CLIENT_RX_BUFSIZE - data_len)) {memcpy(tcp_client_recvbuf + data_len, q - > payload, (TCP_CLIENT_RX_BUFSIZE - data_len));} else {memcpy(tcp_client_recvbuf + data_len,q - > payload, q - > len);}data_len += q - > len;if (data_len > TCP_CLIENT_RX_BUFSIZE) {break; /* 超出TCP客戶端接收數(shù)組,跳出*/}}taskEXIT_CRITICAL(); /* 退出臨界區(qū)*/data_len = 0; /* 復(fù)制完成后data_len要清零*/printf("%s\r\n", tcp_client_recvbuf);netbuf_delete(recvbuf);} else /*關(guān)閉連接*/ {netconn_close(tcp_clientconn);netconn_delete(tcp_clientconn);goto atk_start;}}}}
}

下載驗(yàn)證

我們編譯代碼下載到開發(fā)板并運(yùn)行,打開數(shù)據(jù)流展示,如下圖所示。

在這里插入圖片描述

http://www.risenshineclean.com/news/53077.html

相關(guān)文章:

  • 做特賣的網(wǎng)站愛庫存seo診斷優(yōu)化方案
  • 企業(yè)做網(wǎng)站的流程深圳網(wǎng)絡(luò)營銷推廣渠道
  • 建筑人才網(wǎng)招聘官網(wǎng)登錄深圳seo優(yōu)化排名公司
  • 智能建站系統(tǒng)怎么更換網(wǎng)站模板東莞互聯(lián)網(wǎng)推廣
  • 手機(jī)版網(wǎng)站開發(fā)框架關(guān)鍵詞怎么找出來
  • 天津單位網(wǎng)站建設(shè)獲取排名
  • wordpress 性能分析凱里seo排名優(yōu)化
  • 新鄉(xiāng)河南網(wǎng)站建設(shè)頂尖文案
  • 山東mip網(wǎng)站建設(shè)網(wǎng)絡(luò)營銷屬于哪個(gè)專業(yè)
  • 網(wǎng)站免費(fèi)建設(shè)seo外鏈優(yōu)化培訓(xùn)
  • 廊坊網(wǎng)站建設(shè)公司哪家好建站公司哪家好
  • 做去態(tài)網(wǎng)站要學(xué)什么語言故事型軟文廣告
  • 肇慶網(wǎng)站建設(shè)公司哪個(gè)好谷歌推廣怎么做
  • 負(fù)面信息網(wǎng)站全國疫情最新情報(bào)
  • 新媒體運(yùn)營師考試報(bào)名官網(wǎng)優(yōu)化大師的功能有哪些
  • 在阿里云域名可以做網(wǎng)站嗎百度海南分公司
  • 網(wǎng)站建設(shè)計(jì)劃 文庫原畫培訓(xùn)班一般學(xué)費(fèi)多少
  • 企業(yè)網(wǎng)站建設(shè)開發(fā)注意事項(xiàng)深圳優(yōu)化怎么做搜索
  • asp網(wǎng)站程序下載今日軍事新聞?lì)^條視頻
  • 網(wǎng)站費(fèi)用估算信息流廣告代理商排名
  • 關(guān)于醫(yī)院建設(shè)網(wǎng)站的請(qǐng)示外鏈吧
  • 網(wǎng)站開發(fā)招標(biāo)文件范本互聯(lián)網(wǎng)推廣招聘
  • 網(wǎng)站建設(shè) 大公司網(wǎng)站代搭建維護(hù)
  • 語言做網(wǎng)站免費(fèi)設(shè)計(jì)模板網(wǎng)站
  • 注銷建設(shè)工程規(guī)劃許可證在哪個(gè)網(wǎng)站營銷策劃精準(zhǔn)營銷
  • 賽扶做網(wǎng)站推廣拉新任務(wù)的平臺(tái)
  • 網(wǎng)站開發(fā)總結(jié)文檔百度問答官網(wǎng)
  • 設(shè)置個(gè)網(wǎng)站要多少錢黑馬培訓(xùn)是正規(guī)學(xué)校嗎
  • 網(wǎng)站如何做導(dǎo)航條下拉菜單收錄入口在線提交
  • 建網(wǎng)站要學(xué)什么手機(jī)優(yōu)化大師官方免費(fèi)下載