1. 程式人生 > 其它 >ART-PI之UDP非阻塞客戶端

ART-PI之UDP非阻塞客戶端

服務端控制客戶端的小電機(PWM 方式)

客戶端: ART-PI,向服務端傳送天氣資訊和客戶端狀態,訊息格式s:%d;v:%d;n:%d;l:%s

服務端:自制Python服務端,埠繫結8887,傳送電機控制命令 60/61/62/63/64 (hex 0x36 0x30...)

遇到的問題:雖然使用的是UDP 連線, 預設狀態下recvfrom是阻塞的, 如果服務端沒有傳送指令,socket並沒有收到資料,客戶端任務阻塞等待在這個接收函式。

如果這個任務還有其他任務要求,比如傳送狀態,那就成了問題。那麼,是否有一種方法為UDP的RECV設定超時。

方法一:設定socket超時時間, timeout後, 讀取recvfrom的返回值,判斷狀態並繼續loop

 struct timeval tv_out;
 tv_out.tv_sec = 3;
 tv_out.tv_usec  = 0;
 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv_out,izeof(tv_out));

方法二:使用select實現非阻塞通訊

/*
 int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout)

 返回值:就緒描述符的數目,超時返回0,出錯返回 - 1
*/ /* 引數列表: int maxfdp是一個整數值,是指集合中所有檔案描述符的範圍,即所有檔案描述符的最大值加1,不能錯! fd_set* readfds是指向fd_set結構的指標,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的讀變化的,即我們關心是否可以從這些檔案中讀取資料了,如果這個集合中有一個檔案可讀,select就會返回一個大於0的值,表示有檔案可讀,如果沒有可讀的檔案,則根據timeout引數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何檔案的讀變化。 d_set* writefds是指向fd_set結構的指標,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的寫變化的,即我們關心是否可以向這些檔案中寫入資料了,如果這個集合中有一個檔案可寫,select就會返回一個大於0的值,表示有檔案可寫,如果沒有可寫的檔案,則根據timeout引數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何檔案的寫變化。 fd_set* errorfds同上面兩個引數的意圖,用來監視檔案錯誤異常。 struct timeval* timeout是select的超時時間,這個引數至關重要,它可以使select處於三種狀態: 第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視檔案描述符集合中某個檔案描述符發生變化為止; 第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函式,不管檔案描述符是否有變化,都立刻返回繼續執行,檔案無變化返回0,有變化返回一個正值; 第三,timeout的值大於0,這就是等待的超時時間,即 select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。 * / /* 說明兩個結構體: 第一,struct fd_set可以理解為一個集合,這個集合中存放的是檔案描述符(file descriptor),即檔案控制代碼,這可以是我們所說的普通意義的檔案,當然Unix下任何裝置、管道、FIFO等都是檔案形式,全部包括在內,所以毫無疑問一個socket就是一個檔案,socket控制代碼就是一個檔案描述符。fd_set集合可以通過一些巨集由人為來操作。比如清空集合 FD_ZERO(fd_set*),將一個給定的檔案描述符加入集合之中FD_SET(int, fd_set*),將一個給定的檔案描述符從集合中刪除FD_CLR(int, fd_set*),檢查集合中指定的檔案描述符是否可以讀寫FD_ISSET(int, fd_set*)。 第二,struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個是毫秒數。
*/
void udp_demo(void)
{
     int ret;
    int sock = -1;
    struct hostent *host;
    struct sockaddr_in server_addr, local_addr;
    int bytes_received;
    char *recv_data;
    fd_set readset;
    int maxfdp1,i;

    int state = 0;
    int sn = 0;
    char sl[1024] = {0};
    int vol = 0;
    char buff[128] = {0};
    char ip_addr_buf[64];
    recv_data = rt_calloc(1, TEST_BUFSZ);
    if (recv_data == RT_NULL)
    {
        rt_kprintf("calloc failed. No memory!");
        return;
    }
   // rt_thread_mdelay(5000);
   //udp 收發的一些心得
   //https://www.cnblogs.com/wf-skylark/p/9008138.html
    /* 建立一個socket,型別是SOCK_DGRAM,UDP型別 */
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        rt_kprintf("Socket error\n");
        return;
    }
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(udp_remote_port);
    server_addr.sin_addr.s_addr = inet_addr(udp_remote_ip);
    rt_kprintf("ip: %s, port: %d \n",udp_remote_ip, udp_remote_port);
    if ((ret = connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) < 0)
    {
        LOG_E("Connect <%d> fail! ret:%d", sock, ret);
        goto __exit;
    }

    LOG_I("connect  success\n");
    //https://blog.csdn.net/songjun007/article/details/119987087
    //udp receive timeout
   struct timeval tv_out;
   tv_out.tv_sec = 3;
   tv_out.tv_usec  = 0;
   // setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv_out, sizeof(tv_out));
   struct timeval tv;                    //設定select 超時時間
   tv.tv_sec = 3;
   tv.tv_usec =0;

   int fromlen =sizeof(struct sockaddr_in);
   while(1)
   {
       //rt_thread_mdelay(20);
       wifi_audio_infor(&state, &sn, &vol,sl);
       rt_sprintf(req_data, "s:%d;v:%d;n:%d;l:%s", state, vol, sn,sl);
       // ret = send(sock, req_data, strlen(req_data), 0);
       LOG_I("send: %s \n",req_data );
       sendto(sock, req_data, strlen(req_data), 0, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
       rt_thread_mdelay(2);
       LOG_I("select");
       FD_ZERO(&readset);                                //清除這個集合
       FD_SET(sock,&readset);                            //增加網路描述符
       i = select(sock+1, &readset,0,0,&tv);
       if(i < 0)
       {
          LOG_I("select error \n ");
       }
       else if( i ==0 )
       {
           LOG_I("time out \n ");      //等於零代表超時
       }
        else if (i > 0)
       {
         //bytes_received = recv(sock, recv_data, TEST_BUFSZ - 1, 0); //等待收取資料
         //檢測套接字的檔案描述符是否在該集合中可讀寫
         if(FD_ISSET(sock,&readset))
         {
            bytes_received = recvfrom(sock, recv_data, TEST_BUFSZ - 1, 0, (struct sockaddr *)&server_addr,&fromlen);
            if (bytes_received < 0)
           {
             LOG_I("no receive, socket <%d>.", sock);
            // goto __exit;
            }
         else {

            recv_data[bytes_received] = '\0' ;
            LOG_I("\n(%s,%d) say:", inet_ntoa(server_addr.sin_addr),ntohs(server_addr.sin_port));
            LOG_I("recv data: %s \n", recv_data);
            process_server_data(recv_data);
            }
        }
       }


   }
__exit:
   LOG_I(" @@ exit \r\n");
    if (recv_data)
        rt_free(recv_data);

    if (sock >= 0)
    {
        closesocket(sock);
        sock = -1;
    }
}

發現ulog用起來蠻舒服的,幾個MSH常用命令來設定日誌級別、標籤、輸出關鍵字、過濾器。相信以後除錯一定還要用。.

ulog_lvl 0

ulog_lvl 7
ulog_lvl 4
ulog_tag udp
ulog_kw wifi
ulog_filter
ulog_flush

總結:

對於UDP而言,伺服器也不知道客戶端的狀態(面向非連線,沒有連線這個東西,因此只剩下判斷那個客戶端控制代碼需要收資料)。

“大多數情況下,TCP伺服器是併發的,UDP的伺服器是迭代的“。人說,UDP沒有必要使用多路複用。

這個例子也只是起到了超時控制的作用,就當做以後TCP服務端程式設計做個練習實驗吧。