學會Zynq(11)RAW API的TCP和UDP程式設計

FPGADesigner發表於2019-03-20

RAW API

RAW API(有時稱作native API)是一種事件驅動型的API,在沒有作業系統的情況下使用。核心棧通過這個API完成不同協議間的互動。

使用lwIP棧的應用程式通過一組回撥函式實現。當某些“事件”發生時,會lwIP核會呼叫這些回撥函式,比如傳入資料、傳出資料、錯誤通知、連線關閉等。應用程式中的回撥函式執行對這些事件的處理操作。

RAW API支援多種協議,下面介紹如何對TCP和UDP進行程式設計。在Xilinx平臺中使用lwIP的RAW API,部分細節會有所不同,但大部分函式用法都一樣。


TCP實現

1. 初始化

在使用任何TCP函式前,必須先呼叫**lwip_init()函式。此後必須每隔TCP_TMR_INTERVAL(通常取250ms)呼叫一次tcp_tmr()函式。某些版本的lwIP只需要將sys_check_timeouts()**函式新增到主迴圈中,它會處理棧中所有協議的定時器。Xilinx中還是需要通過配置處理器的定時器來呼叫tcp_tmr()。

**tcp_arg()**函式指定傳給一個連線的所有回撥函式的引數,原型介面如下:

void tcp_arg(struct tcp_pcb * pcb, void * arg)

“pcb”引數指定TCP連線的控制塊;“arg”引數是指向使用者設定的一些資料的指標。大多數情況下,使用者使用這個引數來標識應用程式中的特定例項。

2. TCP連線步驟

一個TCP連線由一個協議控制塊(Protocol Control Block,PCB)做標識。有兩種建立連線的方法。被動連線(監聽)方法,相當於作為服務端:

  1. 呼叫pcb_new建立一個pcb。
  2. (可選)呼叫tcp_arg將應用程式中特定的值於PCB關聯在一起。
  3. 呼叫tcp_bind函式指定本地IP地址和埠。
  4. 呼叫tcp_listentcp_listen_with_backlog,這些函式將釋放作為引數的PCB,並返回一個更小的監聽PCB,如“tcp_new = tcp_listen(tpcb);”。
  5. 呼叫tcp_accept指定新連線到來時要呼叫的函式。

主動連線方法,相當於作為客戶端:

  1. 呼叫pcb_new建立一個pcb。
  2. (可選)呼叫tcp_arg將應用程式中特定的值於PCB關聯在一起。
  3. (可選)呼叫tcp_bind函式指定本地IP地址和埠
  4. 呼叫tcp_connect函式。

3. TCP連線函式

struct tcp_pcb * tcp_new(void)

該函式建立一個新的連線控制塊PCB,連線的初始狀態為“close”。如果沒有可用的記憶體建立新的PCB,將會返回NULL。

err_t tcp_bind(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

該函式將PCB與本地IP地址和埠號繫結在一起。IP地址可設定為IP_ADDR_ANY,連線繫結到所有本地IP地址。如果埠設定為0,函式會自動選擇一個可用埠。繫結時連線必須處於“close”狀態。繫結成功後會返回“ERR_OK”;如果連線試圖繫結到被佔用的埠,會返回“ERR_USE”。

struct tcp_pcb * tcp_listen(struct tcp_pcb * pcb)

該函式中的PCB引數用於指定要監聽的連線,此時該連線必須處於“close”狀態,並已經使用tcp_bind繫結到了本地埠。該函式設定本地埠來監聽傳入的連線。

tcp_listen函式會返回一個新的PCB,作為引數傳遞給函式的PCB會被釋放。這是因為監聽需要的記憶體更少,因此tcp_listen會為監聽連線分配一個更小的記憶體。如果可用記憶體不夠,則返回NULL,作為引數的PCB相關聯的記憶體也不會被釋放。

呼叫tcp_listen()之後必須呼叫tcp_accept(),否則此埠的傳入連線會被中止。

struct tcp_pcb * tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog)

該函式功能與tcp_listen相同,但將監聽佇列中未完成連線的數量限制為backlog引數的值。使用該函式需要在lwipop.h中設定TCP_LISTEN_BACKLOG=1。

void tcp_accept(struct tcp_pcb * pcb,
          err_t (* accept)(void * arg, struct tcp_pcb * newpcb, err_t err))

該函式命令PCB開始監聽傳入的連線。當新連線到達本地埠時,PCB會呼叫設定的回撥函式來完成新連線。

err_t tcp_connect(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port,
         err_t (* connected)(void * arg, struct tcp_pcb * tpcb, err_t err));

該函式設定PCB連線到遠端主機,併傳送初始SYN段來開啟連線。如果連線沒有繫結到本地埠,則會為其自動分配一個埠。

tcp_connect會立即返回值,不會等待正確設定連線。SYN成功入列則返回ERR_OK;若沒有可用記憶體用來SYN段的排隊,則返回ERR_MEM。當連線建立時,會呼叫第四個引數所指定的回撥函式。如果由於主機拒絕連線或沒有應答,導致連線未能建立,會呼叫一個錯誤處理函式。

4. 傳送TCP資料

在TCP連線上傳送資料的步驟如下:

  1. 呼叫tcp_sent()函式設定回撥函式;
  2. 呼叫tcp_sendbuf()函式查詢可以傳送的最大資料量;
  3. 呼叫tcp_write()函式把資料排隊;
  4. 呼叫tcp_output()函式強制傳送資料。

上述步驟是隻是個大致流程,不同情況下用法會有所不同。

void tcp_sent(struct tcp_pcb * pcb,
                   err_t (* sent)(void * arg, struct tcp_pcb * tpcb, u16_t len))

tcp_sent指定當遠端主機確實收到資料時,應該呼叫的回撥函式。len引數為主機確認的位元組數。

u16_t tcp_sndbuf(struct tcp_pcb * pcb)

tcp_sendbuf返回輸出佇列中可用空間的位元組數。

err_t tcp_write(struct tcp_pcb * pcb, void * dataptr, u16_t len, u8_t apiflags)

tcp_write讓引數dataptr指向的資料排隊,資料長度為len。apiflags的取值包括兩個bit位:TCP_WRITE_FLAG_COPY指示lwIP應該分配新記憶體並將資料分配到其中,未選此bit則不會分配新記憶體;TCP_WRITE_FLGA_MORE指示不在TCP段中設定push標誌。

如果資料長度超過了當前傳送緩衝區的大小,或者輸出段佇列長度大於lwipopt.h中為TCP_SND_QUEUELEN定義的上限,tcp_write()函式會失敗並返回ERR_MEM。此時應用程式應該等待其它主機成功接收到排在前面的資料後,再重試。

err_t tcp_output(struct tcp_pcb * pcb)

tcp_output強制傳送目前所有進入佇列的資料。

5. 接收TCP資料

TCP資料接收是基於回撥的,當新資料到達時,將呼叫應用程式指定的回撥函式。TCP協議設定了一個視窗(window),該視窗告訴傳送主機它可以在連線上傳送多少資料。所有連線的視窗大小都是lwipopts.h中設定的TCP_WND值。當應用程式處理了傳入的資料後,必須呼叫tcp_recved()函式,以指示TCP可以增加接收視窗。

void tcp_recv(struct tcp_pcb * pcb,
 err_t (* recv)(void * arg, struct tcp_pcb * tpcb, struct pbuf * p, err_t err))

tcp_recv設定新資料到達時將呼叫的回撥函式。如果沒有錯誤發生且回撥函式返回了ERR_OK,tcp_recv會釋放佔用的pbuf;否則不會釋放pbuf,等待lwIP核做進一步處理。如果遠端主機關閉了連線,將會呼叫帶NULL pbuf的回撥函式,告知應用程式連線已關閉。

void tcp_recved(struct tcp_pcb * pcb, u16_t len)

應用程式已經處理完資料並準備接收更多資料時,必須呼叫tcp_recved函式。其目的是在處理資料是有更大的視窗。len參數列示處理資料的長度。

6. 應用程式輪詢(polling)

當連線處於空閒狀態時(即沒有傳送或接收資料),lwIP將呼叫指定的回撥函式重複輪詢應用程式。這種機制可以用作看門狗計時器,以解決長時間處於空閒狀態的連線;也可以作為一種等待記憶體變為可用狀態的方法。比如當記憶體不可用導致tcp_write()的呼叫失敗時,應用程式可以在連線空閒了一段時間後,使用輪詢功能再次呼叫tcp_write()。

void tcp_poll(struct tcp_pcb * pcb,
          err_t (* poll)(void * arg, struct tcp_pcb * tpcb), u8_t interval)

tcp_toll指定輪詢應用程式時應呼叫的回撥函式和輪詢間隔。TCP有一個粗略的計時器,大概一秒鐘發生兩次訊號,輪詢間隔時間和此有關。比如設定為10,則表示應用程式每(10/2=)5秒輪詢一次。

7. 關閉與中止連線

err_t tcp_close(struct tcp_pcb * pcb)

tcp_close用於關閉連線。如果沒有可用記憶體來關閉連線時,tcp_close返回ERR_MEM。此時應用程式應該使用應答回撥函式或輪詢功能,等待並再次嘗試關閉。成功關閉後返回ERR_OK。呼叫tcp_close後,TCP會釋放PCB。但是在遠端主機確認關閉連線之前,仍然可以在該連線上收到資料。

void tcp_abort(struct tcp_pcb * pcb)

tcp_abort向遠端主機傳送RST段(復位)來中止連線,PCB會被釋放。

8. 其餘函式

如果連線由於錯誤而中止,或者連線嘗試失敗、超時或重置,應用程式將通過err回撥函式對此類事件發出警報。記憶體不足也會導致連線中止。要呼叫的回撥函式通過tcp_err()函式設定。

void tcp_err(struct tcp_pcb * pcb, void (* err)(void * arg, err_t err))

錯誤回撥函式不會將PCB作為引數,因為此時PCB可能已經被釋放了。

tcp_nagle_enable ( struct tcp_pcb * aPcb ); 
tcp_nagle_disable ( struct tcp_pcb * aPcb ); 
tcp_nagle_disabled ( struct tcp_pcb * aPcb );

Nagle演算法會自動連線許多小的訊息,減少傳送包的個數來增加網路效率。TCP/IP協議中無論傳送多少資料,總需要在資料前面加上協議頭;對方接收到資料也需要傳送應答。Nagle演算法儘可能傳送大塊資料,避免小資料塊,從而充分利用網路頻寬。

但有時我們又不需要Nagle演算法。tcp_nagle_enable()函式用於開啟nagle演算法;tcp_nagle_disable()函式用於禁用nagle演算法;tcp_nagle_disabled()用於檢測,未啟用nagle演算法時返回ture。


RAW TCP示例序列圖

RAW TCP主要是通過執行回撥函式來實現的,它的操作往往與接收和處理單個訊息緊密相關。因此如果熟悉底層的TCP協議對程式設計會有一定幫助,否則會不知道該在何時呼叫哪個函式。下表給出了遠端客戶機與本地lwIP伺服器之間互動的序列圖。
遠端

客戶端 TCP 訊息 lwIP棧操作 lwIP伺服器操作 簡述
- - - <= tcp_new() 建立TCP PCB
- - - <= tcp_bind() 繫結埠號
- - - <= tcp_listen_with_backlog() 建立監聽端點(分配新的PCB)
- - - <= tcp_accept() 設定accept回撥
- - - <= tcp_arg() 設定回撥函式引數
connect => - - - 客戶端連線伺服器
- SYN => - - 遠端棧傳送SYN
- - 分配新PCB - lwIP“掛起”會話
- <= SYN/ACK - - SYN/ACK響應
<= (連線返回) - - - 遠端棧通知客戶端連線成功
- ACK => - - 遠端棧傳送完成“三次握手”的應答
- - (呼叫 accept 回撥函式) => - lwIP通知應用程式有新會話
- - - <= tcp_accepted() 伺服器接受連線,減少掛起會話的計數
- - - <= tcp_arg() 設定新的回撥引數
- - - <= tcp_recv() 伺服器設定接收回撥
- - - <= tcp_err() 伺服器設定錯誤/中止的回撥函式
- - - <= tcp_sent() 伺服器設定傳送回撥
- - - <= (伺服器從accept回撥函式返回OK狀態)
- - (把PCB標記為活躍) - -
已建立連線(任何一方都可以傳送資料)
send => TCP資料=> - - 客戶端傳送請求資料
- - (lwIP呼叫伺服器的接收回撥函式) => - -
- - - <= tcp_write(response_data, len) 伺服器向客戶機寫入回覆的資料
- - (lwIP對TCP段進行排隊) - -
- - - <=tcp_write(response_data2, len2) 伺服器寫入更多資料
- - (lwIP對TCP段進行排隊) - 段與段之間可以合併
- - - <= tcp_recved() 伺服器通知lwIP 使用更大的視窗
- - - <= (伺服器從接收回撥函式返回OK狀態) -
- <= TCP 資料 (lwIP查詢要傳送的佇列段) - lwIP向客戶端傳送資料段,包括接收客戶端資料的應答

注意,tcp_write()只對TCP資料進行排隊,以便稍後傳輸,它實際上並沒有開始傳輸。當。但是如果是在接收回撥中使用tcp_write(),如上表中的示例,則不需要呼叫tcp_output()來傳輸要傳送的資料。如果在接收回撥中使用了tcp_output,它不會執行任何操作。

當接收回撥函式返回時,lwIP棧會自動啟動資料的傳送,遠端客戶端的前一個資料包的應答將於第一個傳出的資料段相結合。如果在其它地方呼叫tcp_write,則可能需要呼叫tcp_output來啟動資料傳輸。

下面再給出一個lwIP作為客戶端連線遠端伺服器的序列圖:

遠端伺服器 TCP 訊息 lwIP棧操作 lwIP客戶端操作 簡述
- - - <= tcp_new() 建立TCP PCB
- - - <= [tcp_bind()] 繫結埠號
- - - <= tcp_arg() 建立監聽端點(分配新的PCB)
- - - <= tcp_err()
- - - <= tcp_recv() 設定接收回撥函式
- - - <= tcp_sent() 設定傳送回撥函式
- - - <= tcp_connect() 連線並提供連線回撥函式
- <= SYN <= (lwIP生成SYN) - lwIP生成到遠端伺服器的SYN包
- SYN/ACK => - - 遠端伺服器棧傳送SYN/ACK
- - (lwIP呼叫連線回撥) => - 從lwIP的角度看,會話已經建立,回撥返回時會生成三次TCP握手的應答
建立連線(任何一方都可以傳送資料)
- - - <=tcp_write(request_data, len) 客戶端向伺服器寫請求資料
- - (lwIP對TCP段排隊) - -
- - - <=tcp_write(request_data2, len2) 客戶端向伺服器寫更多資料
- - (lwIP對TCP段排隊) - 段與段之間可以合併
- - - <= tcp_output() 客戶端向lwIP傳送訊號以生成輸出包
- - - <= (客戶端從連線回撥函式中返回) -
- <= TCP 資料 - - lwIP生成一個或多個資料包
send => TCP資料 => - - 伺服器傳送應答資料
- - (呼叫客戶端接收回撥)=> - 做相應處理

上表中在連線建立前便建立了接收和傳送的回撥函式,該操作也可以在連線建立後進行。如果連線失敗,客戶端可以通過tcp_err()設定的回撥函式得到失敗的通知。


UDP實現

struct udp_pcb * udp_new(void)

udp_new()函式建立一個新的UDP PCB,用於UDP通訊。PCB在繫結到本地地址或連線到遠端地址之前都處於不活躍狀態。

void udp_remove(struct udp_pcb * pcb)

udp_remove()函式移除並釋放PCB。

err_t udp_bind(struct udp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

udp_bind()函式將PCB與本地地址繫結。IP地址引數ipaddr可以是“IP_ADDR_ANY”,表示監聽任一本地IP地址。如果指定的埠port已被佔用,會返回ERR_USE;否則返回ERR_OK。

err_t udp_connect(struct udp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

udp_connect()函式設定PCB的遠端端。這個函式只是設定PCB的遠端地址,不會產生任何網路流量。如果連線的埠不可用則返回ERR_USE;如何沒有到目標的路由則返回ERR_RTE;連線成功則返回ERR_OK。

只有使用udp_send()函式時才需要進行連線。對於未連線的PCB,可以使用udp_sendto()函式將其傳送到任何指定的遠端地址。已連線的PCB只從連線的遠端地址處接受資料;未連線的PCB可以從任意地址接受資料包。

void udp_disconnect(struct udp_pcb * pcb)

udp_disconnect()函式移除PCB連線的遠端端。這個函式只會移除PCB的遠端地址,不會產生任何網路流量。

err_t udp_send(struct udp_pcb * pcb, struct pbuf * p)

udp_send()函式將pbuf型別的變數p傳送到遠端地址集。pbuf不會被釋放。

err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p,  
struct ip_addr *dst_ip, u16_t dst_port); 

udp_sendto()函式功能和udp_send相同,但是可以傳送到任意指定的遠端地址。

void udp_recv(struct udp_pcb * pcb,
               void (* recv)(void * arg, struct udp_pcb * upcb,
                                         struct pbuf * p,
                                         struct ip_addr * addr,
                                         u16_t port),
               void * recv_arg)

udp_recv()函式設定特定連線上接收到資料包時應呼叫的回撥函式。回撥函式負責釋放pbuf。

相關文章