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

来源:https://www.cnblogs.com/neo-T/archive/2022/11/12/16882934.html
-Advertisement-
Play Games

什麼是Path環境變數? 在探討這個問題之前,我們需要瞭解什麼是環境變數。 “環境變數”和“path環境變數”其實是兩個東西,這一點大家一定要區分開,不要混為一談。 “環境變數”是操作系統工作環境設置的一些選項或屬性參數。每個環境變數由變數名和文件路徑組成的,可以設置很多個環境變數。 我們一般使用環 ...


4. tcp客戶端

       在協議棧源碼工程下,存在一個用vs2015建立的TcpServerForStackTesting工程。其運行在windows平臺下,模擬實際應用場景下的tcp伺服器。當tcp客戶端連接到伺服器後,伺服器會立即下發一個1100多位元組長度的控制報文到客戶端。之後在整個tcp鏈路存續期間,伺服器會每隔一段隨機的時間(90秒到120秒之間)下發控制報文到客戶端,模擬實際應用場景下伺服器主動下髮指令、數據到客戶端的情形。客戶端則連續上發數據報文到伺服器,伺服器回饋一個應答報文給客戶端。客戶端如果收不到該應答報文則會立即重發,直至收到應答報文或超過重試次數後重連伺服器。總之,整個測試場景的設計目標就是完全契合常見的商業應用需求,以此來驗證協議棧的核心功能指標是否完全達標。用vs2015打開這個工程,配置管理器指定目標平臺為x64。main.cpp文件的頭部定義了伺服器的埠號以及報文長度等信息:

#define SRV_PORT         6410 //* 伺服器埠
#define LISTEN_NUM       10   //* 最大監聽數
#define RCV_BUF_SIZE     2048 //* 接收緩衝區容量
#define PKT_DATA_LEN_MAX 1200 //* 報文攜帶的數據最大長度,凡是超過這個長度的報文都將被丟棄

我們可以依據實際情形調整上述配置並利用這個模擬伺服器測試tcp客戶端的通訊功能。

……
#include "onps.h"

#define PKT_FLAG 0xEE //* 通訊報文的頭部和尾部標誌
typedef struct _ST_COMMUPKT_HDR_ { //* 數據及控制指令報文頭部結構
    CHAR bFlag;         //* 報文頭部標誌,其值參看PKT_FLAG巨集
    CHAR bCmd;          //* 指令,0為數據報文,1為控制指令報文
    CHAR bLinkIdx;      //* tcp鏈路標識,當存在多個tcp鏈路時,該欄位用於標識這是哪一個鏈路
    UINT unSeqNum;      //* 報文序號
    UINT unTimestamp;   //* 報文被髮送時刻的unix時間戳
    USHORT usDataLen;   //* 攜帶的數據長度
    USHORT usChechsum;  //* 校驗和(crc16),覆蓋除頭部和尾部標誌字元串之外的所有欄位
} PACKED ST_COMMUPKT_HDR, *PST_COMMUPKT_HDR; 

typedef struct _ST_COMMUPKT_ACK_ { //* 數據即控制指令應答報文結構
    ST_COMMUPKT_HDR stHdr; //* 報文頭
    UINT unTimestamp;      //* unix時間戳,其值為被應答報文攜帶的時間戳
    CHAR bLinkIdx;         //* tcp鏈路標識,其值為被應答報文攜帶的鏈路標識
    CHAR bTail;            //* 報文尾部標誌,其值參看PKT_FLAG巨集
} PACKED ST_COMMUPKT_ACK, *PST_COMMUPKT_ACK;

//* 提前申請一塊靜態存儲時期的緩衝區用於tcp客戶端的接收和發送,因為接收和發送的報文都比較大,所以不使用動態申請的方式
#define RCV_BUF_SIZE     1300           //* 接收緩衝區容量
#define PKT_DATA_LEN_MAX 1200           //* 報文攜帶的數據最大長度,凡是超過這個長度的報文都將被丟棄
static UCHAR l_ubaRcvBuf[RCV_BUF_SIZE]; //* 接收緩衝區
static UCHAR l_ubaSndBuf[sizeof(ST_COMMUPKT_HDR) + PKT_DATA_LEN_MAX]; //* 發送緩衝區,ST_COMMUPKT_HDR為通訊報文頭部結構體
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 0
        emac_init(); //* ethernet網卡初始化函數,並註冊網卡到協議棧
    #else
        while(!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; 
    }
    
    //* 分配一個socket         
    if(INVALID_SOCKET == (hSocket = socket(AF_INET, SOCK_STREAM, 0, &enErr))) 
    {
        //* 返回了一個無效的socket,列印錯誤日誌
        printf("<1>socket() failed, %s\r\n", onps_error(enErr)); 
        return -1; 
    }
    
    //* 連接成功則connect()函數返回0,非0值則連接失敗
    if(connect(hSocket, "192.168.0.2", 6410, 10))
    {
        printf("connect 192.168.0.2:6410 failed, %s\r\n", onps_get_last_error(hSocket, NULL));
        close(hSocket);
        return -1; 
    }
    
    //* 等待接收伺服器應答或控制報文的時長(即recv()函數的等待時長),單位:秒。0不等待;大於0等待指定秒數;-1一直
    //* 等待直至數據到達或報錯。設置成功返回TRUE,否則返回FALSE。這裡我們設置recv()函數不等待
    //* 註意,只有連接成功後才可設置這個接收等待時長,在這裡我們設置接收不等待,recv()函數立即返回,非阻塞型
    if(!socket_set_rcv_timeout(hSocket, 0, &enErr))
        printf("socket_set_rcv_timeout() failed, %s\r\n", onps_error(enErr));
    
    INT nThIdx = 0;
    while(TRUE && nThIdx < 1000)
    {
        //* 接收,前面已經設置recv()函數不等待,有數據則讀取數據後立即返回,無數據則立即返回
        INT nRcvBytes = recv(hSocket, ubaRcvBuf, sizeof(ubaRcvBuf));
        if(nRcvBytes > 0)
        {
            //* 收到報文,處理之,報文有兩種:一種是應答報文;另一種是伺服器主動下發的控制報文
            //* 在這裡添加你的自定義代碼
            ……
        }
        
        //* 發送數據報文到伺服器,首先封裝要發送的數據報文,PST_COMMUPKT_HDR其類型為指向ST_COMMUPKT_HDR結構體的指
        //* 針,這個結構體是與TcpServerForStackTesting伺服器通訊用的報文頭部結構
        PST_COMMUPKT_HDR pstHdr = (PST_COMMUPKT_HDR)l_ubaSndBuf;
        pstHdr->bFlag = (CHAR)PKT_FLAG; 
        pstHdr->bCmd = 0x00; 
        pstHdr->bLinkIdx = (CHAR)nThIdx++; 
        pstHdr->unSeqNum = unSeqNum; 
        pstHdr->unTimestamp = time(NULL); 
        pstHdr->usDataLen = 900; //* 填充隨機數據,隨機數據長度加ST_COMMUPKT_HDR結構體長度不超過l_ubaSndBuf的長度即可
        pstHdr->usChechsum = 0; 
        pstHdr->usChechsum = crc16(l_ubaSndBuf + sizeof(CHAR), sizeof(ST_COMMUPKT_HDR) - sizeof(CHAR) + 900, 0xFFFF); 
        l_ubaSndBuf[sizeof(ST_COMMUPKT_HDR) + 900] = PKT_FLAG; 

        //* 發送上面已經封裝好的數據報文
        INT nPacketLen = sizeof(ST_COMMUPKT_HDR) + pstHdr->usDataLen + 1;
        INT nSndBytes = send(hSocket, l_ubaSndBuf, nPacketLen, 3); 
        if(nSndBytes != nPacketLen) //* 與實際要發送的數據不相等的話就意味著發送失敗了
        {
            printf("<err>sent %d bytes failed, %s\r\n", nPacketLen, onps_get_last_error(hSocket, &enErr));
            
            //* 關閉socket,斷開當前tcp連接,釋放占用的協議棧資源
            close(hSocket);
            return -1; 
        }
    }
    
    //* 關閉socket,斷開當前tcp連接,釋放占用的協議棧資源
    close(hSocket);
    
    return 0; 
}

編寫tcp客戶端的幾個關鍵步驟:

  1. 調用socket函數,申請一個數據流(tcp)類型的socket;
  2. connect()函數建立tcp連接;
  3. recv()函數等待接收伺服器下發的應答及控制報文;
  4. send()函數將封裝好的數據報文發送給伺服器;
  5. close()函數關閉socket,斷開當前tcp連接;

真實場景下,單個tcp報文攜帶的數據長度的上限基本在1K左右。所以,在上面給出的功能測試代碼中,單個通訊報文的長度也設定在這個範圍內。客戶端迴圈上報伺服器的數據報文的長度900多位元組,伺服器下發開發板的控制報文長度1100多位元組。

       與傳統的socket編程相比,除了上述幾個函數的原型與Berkeley sockets標準有細微的差別,在功能及使用方式上沒有任何改變。之所以對函數原型進行調整,原因是傳統的socket編程模型比較繁瑣——特別是阻塞/非阻塞的設計很不簡潔,需要一些看起來很“突兀”地額外編碼,比如select操作。在設計協議棧的socket模型時,考慮到類似select之類的操作細節完全可以藉助rtos的信號量機制將其封裝到底層實現,從而達成簡化用戶編碼,讓socket編程更加簡潔、優雅的目的。因此,最終呈現給用戶的協議棧socket模型部分偏離了Berkeley標準。

5. tcp伺服器

       常見的tcp伺服器要完成的工作無外乎就是接受連接請求,接收客戶端上發的數據,下發應答或控制報文,清除不活躍的客戶端以釋放其占用的系統資源。因此,tcp伺服器的功能測試代碼分為兩部分實現:一部分在主線程完成啟動tcp伺服器、等待接受連接請求這兩項工作(為了突出主要步驟,清除不活躍客戶端的工作在這裡省略);另一部分單獨建立一個線程完成讀取客戶端數據並下發應答報文的工作。

……
#include "onps.h"

#define LTCPSRV_PORT        6411 //* tcp測試伺服器埠
#define LTCPSRV_BACKLOG_NUM 5    //* 排隊等待接受連接請求的客戶端數量
static SOCKET l_hSockSrv;        //* tcp伺服器socket,這是一個靜態存儲時期的變數,因為伺服器數據接收線程也要使用這個變數

//* 啟動tcp伺服器
SOCKET tcp_server_start(USHORT usSrvPort, USHORT usBacklog)
{
    EN_ONPSERR enErr;
    SOCKET hSockSrv; 
    
    do {
        //* 申請一個socket
        hSockSrv = socket(AF_INET, SOCK_STREAM, 0, &enErr); 
        if(INVALID_SOCKET == hSockSrv)
            break; 
        
        //* 綁定地址和埠,功能與Berkeley sockets提供的bind()函數相同
        if(bind(hSockSrv, NULL, usSrvPort))
            break;
        
        //* 啟動監聽,同樣與Berkeley sockets提供的listen()函數相同
        if(listen(hSockSrv, usBacklog))
            break;         
        return hSockSrv;
    } while(FALSE); 
    
    //* 執行到這裡意味著前面出現了錯誤,無法正常啟動tcp伺服器了
    if(INVALID_SOCKET != hSockSrv)
        close(hSockSrv); 
    printf("%s\r\n", onps_error(enErr)); 
    
    //* tcp伺服器啟動失敗,返回一個無效的socket句柄
    return INVALID_SOCKET;
}

//* 完成tcp伺服器的數據讀取工作
static void THTcpSrvRead(void *pvData)
{
  SOCKET hSockClt; 
  EN_ONPSERR enErr; 
  INT nRcvBytes; 
  UCHAR ubaRcvBuf[256]; 

  while(TRUE)
  {
      //* 等待客戶端有新數據到達
      hSockClt = tcpsrv_recv_poll(l_hSockSrv, 1, &enErr); 
      if(INVALID_SOCKET != hSockClt) //* 有效的socket
      {
          //* 註意這裡一定要儘量讀取完畢該客戶端的所有已到達的數據,因為每個客戶端只有新數據到達時才會觸發一個信號到用戶
          //* 層,如果你沒有讀取完畢就只能等到該客戶端送達下一組數據時再讀取了,這可能會導致數據處理延遲問題
          while(TRUE)
          {
              //* 讀取數據
              nRcvBytes = recv(hSockClt, ubaRcvBuf, 256);
              if(nRcvBytes > 0)
              {
                  //* 原封不動的回送給客戶端,利用回顯來模擬伺服器回饋應答報文的場景
                  send(hSockClt, ubaRcvBuf, nRcvBytes, 1);       
              }
              else //* 已經讀取完畢
              {
                  if(nRcvBytes < 0)
                  {
                      //* 協議棧底層報錯,這裡需要增加你的容錯代碼處理這個錯誤並列印錯誤信息
                      printf("%s\r\n", onps_get_last_error(hSocket, NULL));
                  }
                  break; 
              }
          }  
      }
      else //* 無效的socket
      {
          //* 返回一個無效的socket時需要判斷是否存在錯誤,如果不存在則意味著1秒內沒有任何數據到達,否則列印這個錯誤
          if(ERRNO != enErr)
          {
              printf("tcpsrv_recv_poll() failed, %s\r\n", onps_error(enErr)); 
              break; 
          }
      }
  }
}

int main(void)
{
    EN_ONPSERR enErr; 
    
    if(open_npstack_load(&enErr))
    {    
        printf("The open source network protocol stack (ver %s) is loaded successfully. \r\n", ONPS_VER);
        
        //* 協議棧載入成功,在這裡初始化ethernet網卡,並註冊網卡到協議棧
        emac_init();
    }
    else
    {
        printf("The open source network protocol stack failed to load, %s\r\n", onps_error(enErr));
        return -1; 
    }
    
    //* 啟動tcp伺服器
    l_hSockSrv = tcp_server_start(LTCPSRV_PORT, LTCPSRV_BACKLOG_NUM); 
    if(INVALID_SOCKET != l_hSockSrv)
    {
        //* 在這裡添加工作線程啟動代碼,啟動tcp伺服器數據讀取線程THTcpSrvRead
        ……
    }
    
    //* 進入主線程的主邏輯處理迴圈,等待tcp客戶端連接請求到來
    while(TRUE)
    {
        //* 接受連接請求
        in_addr_t unCltIP; 
        USHORT usCltPort; 
        SOCKET hSockClt = accept(l_hSockSrv, &unCltIP, &usCltPort, 1, &enErr); 
        if(INVALID_SOCKET != hSockClt)
        {
            //* 在這裡你自己的代碼處理新到達的客戶端
            ……
        }
        else
        {
            printf("accept() failed, %s\r\n", onps_error(enErr));
            break;
        }
    }
    
    //* 關閉socket,釋放占用的協議棧資源
    close(l_hSockSrv);
    
    return 0; 
}

編寫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 0
        emac_init(); //* ethernet網卡初始化函數,並註冊網卡到協議棧
    #else
        while(!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; 
    }
    
    //* 分配一個socket         
    if(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 = <%s>\r\n", nRcvBytes, (const char *)l_ubaUdpBuf);
        else
        {
            //* 小於0則意味著recv()函數報錯
            if(nRcvBytes < 0)
            {
                printf("recv failed, %s\r\n", onps_get_last_error(hSocket, NULL)); 
                
                //* 關閉socket釋放占用的協議棧資源
                close(hSocket);
                break; 
            }
        }
    }
    
    //* 關閉socket,斷開當前tcp連接,釋放占用的協議棧資源
    close(hSocket);
    
    return 0; 
}

udp通訊編程依然遵循了傳統習慣,主要編程步驟還是那些:

  1. 調用socket函數,申請一個SOCK_DGRAM(udp)類型的socket;
  2. 如果想建立伺服器,調用bind()函數;想與單個目標地址通訊,調用connect()函數;與任意目標地址通訊則什麼都不用做;
  3. 調用send()或sendto()函數發送udp報文;
  4. 調用recv()或recvfrom()函數接收udp報文;
  5. close()函數關閉socket釋放當前占用的協議棧資源;

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 註釋 給別人看的,機器並不會執行這行語句 1.單行註釋 // 我是單行註釋 2.多行註釋 /* 我是多行註釋 我是多行註釋 我是多行註釋 我是多行註釋 */ // 這是一個main函數,這個是go語言啟動的入口 func main() { //fmt.Println :列印一句話,然後執行完畢後,進 ...
  • JZ55 二叉樹的深度 描述 輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度為樹的深度,根節點的深度視為 1 。 方法1 遞歸 思路: 最大深度是所有葉子節點的深度的最大值,深度是指樹的根節點到任一葉子節點路徑上節點的數量,因此從根節點每 ...
  • 我們常見的併發鎖ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基於AQS實現的,所以說不懂AQS實現原理的,就不能說瞭解Java鎖。 上篇文章講了AQS的加鎖流程,這篇文章再一塊看一下AQS具體源碼實現。 ...
  • 您好,我是湘王,這是我的博客園,歡迎您來,歡迎您再來~ 多數位農在開發的時候,要麼處理同步應用,要麼處理非同步。但是如果能學會使用CompletableFuture,就會具備一種神奇的能力:將同步變為非同步(有點像用了月光寶盒後同時穿梭在好幾個時空的感覺)。怎麼做呢?來看看代碼。 新增一個商店類Shop ...
  • 目錄 一. EGL 前言 二. EGL 繪製流程簡介 三.eglDestroySurface 函數簡介 四.eglDestroySurface 使用 四.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL ES ...
  • C#11添加了文件作用域類型功能:一個新的file修飾符,可以應用於任何類型定義以限制其只能在當前文件中使用。 這樣,我們可以在一個項目中擁有多個同名的類。 通過下麵的項目顯示,該項目包含兩個名為Answer的類。 文件File1.cs中 namespace ConsoleApp11 { file ...
  • 1、前言 對於簡單的系統而言模型與數據可以進行直接的映射,比如說三層模型就足夠支撐項目的需求了。對於這種簡單的系統我們過度設計說白了無異於增加成本,因為對於一般的CRUD來說我們不用特別區分查詢和增刪改的程式結構。高射炮打蚊子那就有點大材小用了。但是我們的系統具有一定複雜性的時候,可能源於訪問頻次、 ...
  • .NET for Apache Spark 入門演練 微軟官方文檔: .NET for Apache Spark 入門 | Microsoft Learn 註意:由於本次在windows平臺下進行演練,以下在未標註操作系統平臺處,預設為windows。 1. 環境準備 推薦安裝工具: visual ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...