3 onps棧使用說明——tcp、udp通訊測試( 三 )

編寫tcp服務器的幾個主要步驟:

  1. 調用socket函數,申請一個數據流(tcp)類型的socket;
  2. bind()函數綁定一個ip地址和端口號;
  3. listen()函數啟動監聽;
  4. accept()函數接受一個tcp連接請求;
  5. 調用tcpsrv_recv_poll()函數利用協議棧提供的poll模型(非傳統的select模型)等待客戶端數據到達;
  6. 調用recv()函數讀取客戶端數據并處理之 , 直至所有數據讀取完畢返回第5步 , 獲取下一個已送達數據的客戶端socket;
  7. 定期檢查不活躍的客戶端 , 調用close()函數關閉tcp鏈路,釋放客戶端占用的協議棧資源;
與傳統的tcp服務器編程并沒有兩樣 。
協議棧實現了一個poll模型用于服務器的數據讀取 。poll模型利用了rtos的信號量機制 。當某個tcp服務器端口有一個或多個客戶端有新的數據到達時,協議棧會立即投遞一個或多個信號到用戶層 。注意,協議棧投遞信號的數量取決于新數據到達的次數(tcp層每收到一個攜帶數據的tcp報文記一次) , 與客戶端數量無關 。用戶通過tcpsrv_recv_poll()函數得到這個信號,并得到最先送達數據的客戶端socket,然后讀取該客戶端送達的數據 。注意這里一定要把所有數據讀取出來 。因為信號被投遞的唯一條件就是有新的數據到達 。沒有信號 ,  tcpsrv_recv_poll()函數無法得到一個有效的客戶端socket,那么剩余數據就只能等到該客戶端再次送達新數據時再讀了 。
其實,poll模型的運作機制非常簡單 。tcp服務器每收到一組新的數據,就會將該數據所屬的客戶端socket放入接收隊列尾部,然后投信號 。所以 , 數據到達、獲取socket與投遞信號是一系列的連鎖反應,且一一對應 。tcpsrv_recv_poll()函數則在用戶層接著完成連鎖反應的后續動作:等信號、摘取接收隊列首部節點、取出首部節點保存的socket、返回該socket以告知用戶立即讀取數據 。非常簡單明了,沒有任何拖泥帶水 。從這個運作機制我們可以看出:
  1. poll模型的運轉效率取決于rtos的信號量處理效率;
  2. tcpsrv_recv_poll()函數每次返回的socket有可能是同一個客戶端的,也可能是不同客戶端;
  3. 單個客戶端已送達的數據長度與信號并不一一對應 , 一一對應的是該客戶端新數據到達的次數與信號投遞的次數,所以當數據讀取次數小于信號數時,存在讀取數據長度為0的情形;
  4. tcpsrv_recv_poll()函數返回有效的sokcet后,盡量讀取全部數據到用戶層進行處理,否則會出現剩余數據無法讀取的情形,如果客戶端不再上發新的數據的話;
6. udp通訊相比tcp,udp通訊功能的實現相對簡單很多 。為udp綁定一個固定端口其就可以作為服務器使用,反之則作為一個客戶端使用 。
……#include "onps.h"#define RUDPSRV_IP"192.168.0.2" //* 遠端udp服務器的地址#define RUDPSRV_PORT 6416//* 遠端udp服務器的端口#define LUDPSRV_PORT 6415//* 本地udp服務器的端口//* udp通訊用緩沖區(接收和發送均使用)static UCHAR l_ubaUdpBuf[256];int main(void){EN_ONPSERR enErr;SOCKET hSocket = INVALID_SOCKET;if(open_npstack_load(&enErr)){printf("The open source network protocol stack (ver %s) is loaded successfully. \r\n", ONPS_VER);//* 協議棧加載成功,在這里初始化ethernet網卡或等待ppp鏈路就緒#if 0emac_init(); //* ethernet網卡初始化函數 , 并注冊網卡到協議棧#elsewhile(!netif_is_ready("ppp0")) //* 等待ppp鏈路建立成功os_sleep_secs(1);#endif}else{printf("The open source network protocol stack failed to load, %s\r\n", onps_error(enErr));return -1;}//* 分配一個socketif(INVALID_SOCKET == (hSocket = socket(AF_INET, SOCK_STREAM, 0, &enErr))){//* 返回了一個無效的socket , 打印錯誤日志printf("<1>socket() failed, %s\r\n", onps_error(enErr));return -1;}#if 0//* 如果是想建立一個udp服務器,這里需要調用bind()函數綁定地址和端口if(bind(hSocket, NULL, LUDPSRV_PORT)){printf("bind() failed,%s\r\n", onps_get_last_error(hSocket, NULL));//* 關閉socket釋放占用的協議棧資源close(hSocket);return -1;}#else//* 建立一個udp客戶端,在這里可以調用connect()函數綁定一個固定的目標服務器,接下來就可以直接使用send()函數發送//* 數據 , 當然在這里你也可以什么都不做(不調用connect()),但接下來你需要使用sendto()函數指定要發送的目標地址if(connect(hSocket, RUDPSRV_IP, RUDPSRV_PORT, 0)){printf("connect %s:%d failed, %s\r\n", RUDPSRV_IP, RUDPSRV_PORT, onps_get_last_error(hSocket, NULL));//* 關閉socket釋放占用的協議棧資源close(hSocket);return -1;}#endif//* 與tcp客戶端測試一樣,接收數據之前要設定udp鏈路的接收等待的時間 , 單位:秒,這里設定recv()函數等待1秒if(!socket_set_rcv_timeout(hSocket, 1, &enErr))printf("socket_set_rcv_timeout() failed, %s\r\n", szNowTime, onps_error(enErr));INT nCount = 0;while(TRUE && nCount < 1000){//* 發緩沖區填充一段字符串然后得到其填充長度sprintf((char *)l_ubaUdpBuf, "U#%d#%d#>1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", time(NULL), nCount++);INT nSendDataLen = strlen((const char *)l_ubaUdpBuf);//* 調用send()函數發送數據,如果實際發送長度與字符串長度不相等則說明發送失敗if(nSendDataLen != send(hSocket, l_ubaUdpBuf, nSendDataLen, 0))printf("send failed, %s\r\n", onps_get_last_error(hSocket, NULL));//* 接收對端數據之前清0,以便本地能夠正確輸出收到的對端回饋的字符串memset(l_ubaUdpBuf, 0, sizeof(l_ubaUdpBuf));//* 調用recv()函數接收數據,如果想知道對端地址調用recvfrom()函數,在這里recv()函數為阻塞模式,最長阻塞1秒(如果未收到任何udp報文的話)INT nRcvBytes = recv(hSocket, l_ubaUdpBuf, sizeof(l_ubaUdpBuf));if(nRcvBytes > 0)printf("recv %d bytes, Data = https://www.huyubaike.com/biancheng/

推薦閱讀