學會Zynq(11)RAW API的TCP和UDP程式設計
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)做標識。有兩種建立連線的方法。被動連線(監聽)方法,相當於作為服務端:
- 呼叫pcb_new建立一個pcb。
- (可選)呼叫tcp_arg將應用程式中特定的值於PCB關聯在一起。
- 呼叫tcp_bind函式指定本地IP地址和埠。
- 呼叫tcp_listen或tcp_listen_with_backlog,這些函式將釋放作為引數的PCB,並返回一個更小的監聽PCB,如“tcp_new = tcp_listen(tpcb);”。
- 呼叫tcp_accept指定新連線到來時要呼叫的函式。
主動連線方法,相當於作為客戶端:
- 呼叫pcb_new建立一個pcb。
- (可選)呼叫tcp_arg將應用程式中特定的值於PCB關聯在一起。
- (可選)呼叫tcp_bind函式指定本地IP地址和埠
- 呼叫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連線上傳送資料的步驟如下:
- 呼叫tcp_sent()函式設定回撥函式;
- 呼叫tcp_sendbuf()函式查詢可以傳送的最大資料量;
- 呼叫tcp_write()函式把資料排隊;
- 呼叫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。
相關文章
- Java 網路程式設計(TCP程式設計 和 UDP程式設計)Java程式設計TCPUDP
- 【網路程式設計】Tcp/Udp程式設計TCPUDP
- 網路程式設計中TCP與UDP程式設計TCPUDP
- Python網路程式設計實現TCP和UDP連線Python程式設計TCPUDP
- 好程式設計師大資料學習路線分享TCP和UDP學習筆記程式設計師大資料TCPUDP筆記
- TCP 和 UDPTCPUDP
- TCP和UDPTCPUDP
- 基本TCP套接字程式設計APITCP程式設計API
- Android程式設計師必知必會的網路通訊傳輸層協議——UDP和TCPAndroid程式設計師協議UDPTCP
- TCP會被UDP取代麼?TCPUDP
- UDP和TCP的差異UDPTCP
- tcp和udp的區別TCPUDP
- 基於TCP/UDP的Socket程式設計,HTTP/HTTPS協議TCPUDP程式設計HTTP協議
- 好程式設計師Python培訓分享udp和tcp協議介紹程式設計師PythonUDPTCP協議
- 網路遊戲程式設計師須知 UDP vs TCP(轉)遊戲程式設計師UDPTCP
- UDP和TCP以及HTTPUDPTCPHTTP
- TCP和UDP比較TCPUDP
- TCP和UDP協議TCPUDP協議
- TCP和UDP對比TCPUDP
- Java&Python的TCP&UDP通訊-網路程式設計JavaPythonTCPUDP程式設計
- TCP和UDP是如何工作的TCPUDP
- UDP&TCP Linux網路應用程式設計詳解UDPTCPLinux程式設計
- UDP程式設計(八)UDP程式設計
- tcp和udp使用總結TCPUDP
- 學會Zynq(3)Zynq的軟體開發基礎知識
- 學會Zynq(2)Zynq-7000處理器的配置詳解
- php中TCP和UDP的區別PHPTCPUDP
- python 中的UDP和TCP(1)PythonUDPTCP
- 網路程式設計協議(TCP和UDP協議,黏包問題)以及socketserver模組程式設計協議TCPUDPServer
- python 學習-- TCP程式設計PythonTCP程式設計
- 學會Zynq(10)lwIP簡介
- 計算機網路 之 TCP和UDP的埠號解析計算機網路TCPUDP
- 學會Zynq(1)搭建Zynq-7000 AP SoC處理器
- udp程式設計的那些事與golang udp的實踐UDP程式設計Golang
- 網路程式設計-UDP程式設計UDP
- 基於UDP程式設計UDP程式設計
- TCP 和 UDP 協議簡介TCPUDP協議
- 學會Zynq(6)固化程式到SD卡或QSPI FlashSD卡