LwIP之socket應用--WebServer和Modbus TCP


1. 引言

     LwIP是嵌入式領域一個流行的以太網協議棧, LwIP開放源碼,用C寫成非常方便移植,並且支持socket接口,使用者可以集中精力處理應用功能。

     本文是LwIP socket的一個使用小結,使用的測試平台是stm32+enc28j60+lwip+uc/OS-II。

2. 使用socket

一個基本的socket建立順序是:

Server端:

  • socket()
  • bind()
  • listen()
  • accept()
  • recv()

Client端:

  • socket()
  • connect()
  • send()

      lwip的socket和PC上的socket接口一致,只是底層實現用lwip的API進行了封裝,可以參考lwip\src\include\lwip\sockets.h。

#if LWIP_COMPAT_SOCKETS
#define accept(a,b,c)         lwip_accept(a,b,c)
#define bind(a,b,c)           lwip_bind(a,b,c)
#define shutdown(a,b)         lwip_shutdown(a,b)
#define closesocket(s)        lwip_close(s)
#define connect(a,b,c)        lwip_connect(a,b,c)
#define getsockname(a,b,c)    lwip_getsockname(a,b,c)
#define getpeername(a,b,c)    lwip_getpeername(a,b,c)
#define setsockopt(a,b,c,d,e) lwip_setsockopt(a,b,c,d,e)
#define getsockopt(a,b,c,d,e) lwip_getsockopt(a,b,c,d,e)
#define listen(a,b)           lwip_listen(a,b)
#define recv(a,b,c,d)         lwip_recv(a,b,c,d)
#define recvfrom(a,b,c,d,e,f) lwip_recvfrom(a,b,c,d,e,f)
#define send(a,b,c,d)         lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f)   lwip_sendto(a,b,c,d,e,f)
#define socket(a,b,c)         lwip_socket(a,b,c)
#define select(a,b,c,d,e)     lwip_select(a,b,c,d,e)
#define ioctlsocket(a,b,c)    lwip_ioctl(a,b,c)

#if LWIP_POSIX_SOCKETS_IO_NAMES
#define read(a,b,c)           lwip_read(a,b,c)
#define write(a,b,c)          lwip_write(a,b,c)
#define close(s)              lwip_close(s)
#define fcntl(a,b,c)          lwip_fcntl(a,b,c)
#endif /* LWIP_POSIX_SOCKETS_IO_NAMES */

#endif /* LWIP_COMPAT_SOCKETS */

int socket(int domain, int type, int protocol);

服務器根據地址類型(ipv4,ipv6)、socket類型、協議創建socket。

domain:協議族,常用的有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE其中AF_INET代表使用ipv4地址

type:socket類型,常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等

protocol:協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等

 

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

把一個地址族中的特定地址賦給socket

sockfd:socket描述字,也就是socket引用

addr:要綁定給sockfd的協議地址

addrlen:地址的長度

通常服務器在啟動的時候都會綁定一個地址(如ip地址+端口號),用於提供服務。有些端口號是約定俗成的不能亂用,如80用作http,502用作modbus。

 

int listen(int sockfd, int backlog);

監聽socket

sockfd:要監聽的socket描述字

backlog:相應socket可以排隊的最大連接個數 

 

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

連接某個socket

sockfd:客戶端的socket描述字

addr:服務器的socket地址

addrlen:socket地址的長度

 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP服務器監聽到客戶端請求之后,調用accept()函數取接收請求

sockfd:服務器的socket描述字

addr:客戶端的socket地址

addrlen:socket地址的長度

 

size_t read(int fd, void *buf, size_t count);

讀取socket內容

fd:socket描述字

buf:緩沖區

count:緩沖區長度

 

size_t write(int fd, const void *buf, size_t count);

向socket寫入內容,其實就是發送內容

fd:socket描述字

buf:緩沖區

count:緩沖區長度

 

int close(int fd);

socket標記為以關閉 ,使相應socket描述字的引用計數-1,當引用計數為0的時候,觸發TCP客戶端向服務器發送終止連接請求。

 

 3. 使用socket創建嵌入式WebServer

 要使用socket的前提是已經做好lwip和rtos的移植,如果低層驅動移植完畢,就可以使用socket快速創建應用。

   本例是一個簡單的WebServer。

const unsigned char htmldata[] = "\
      <html>\
          <head><title> LWIP</title></head>\
           <center><p>A WebServer Based on LwIP v1.4.1 Hello world!</center>\
     </html>";
const unsigned char errhtml[] = "\
        <html>\
            <head>\
               <title>Error!</title>\
            </head>\
            <body>\
               <h1>404 - Page not found</h1>\
            </body>\
        </html>"; 

/**
  * @brief serve tcp connection  
  * @param conn: connection socket 
  * @retval None
  */
void http_server(int conn) 
{
  int buflen = 1500;
  int ret;
  unsigned char recv_buffer[1500];
                
  /* Read in the request */
  ret = read(conn, recv_buffer, buflen); 
  if(ret <= 0)
  {
    close(conn);
    Printf("read failed\r\n");
    return;
  }
    
    Printf("http server response!\r\n");
    if(strncmp((char *)recv_buffer, "GET /lwip", 9) == 0)
    {
        write(conn, htmldata, sizeof(htmldata)-1);
    }
    else
    {
        write(conn, errhtml, sizeof(errhtml)-1);
    }
    /* Close connection socket */
    close(conn);
}

/**
  * @brief  http_task
  * @param arg: pointer on argument(not used here) 
  * @retval None
  */
static void http_task(void *arg)
{
  int sock, newconn, size;
  struct sockaddr_in address, remotehost;

 /* create a TCP socket */
  if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
  {
    Printf("can not create socket");
    return;
  }
  
  /* bind to port 80 at any interface */
  address.sin_family = AF_INET;
  address.sin_port = htons(80);
  address.sin_addr.s_addr = INADDR_ANY;
  if (bind(sock, (struct sockaddr *)&address, sizeof (address)) < 0)
  {
    Printf("can not bind socket");
    close(sock);
    return;
  }
/* listen for connections (TCP listen backlog = 1) */ listen(sock, 1); size = sizeof(remotehost); while (1) { newconn = accept(sock, (struct sockaddr *)&remotehost, (socklen_t *)&size); if (newconn >= 0) { http_server(newconn); } else { close(newconn); } } } /************************************************************** * void http_task_init(void) * * This function initializes the service. **************************************************************/ void http_task_init(void) { sys_thread_new( CHARGEN_THREAD_NAME, http_task, 0, 0, TCPIP_THREAD_PRIO+1); //函數棧在移植sys_thread_new中實現 }

 

  4. 使用socket創建Modbus TCP應用

   Modbus TCP在網絡傳輸層次,就是一串有特定含義的數據包的交互,LwIP層次並不識別是什么數據。所以從這個角度來講,Modbus TCP移植和其他TCP應用的移植沒有任何差別。

    透過表面看本質,只有撥開外層重重包裝看本質,我們才能從紛雜的事件中找到問題的重點,然后剝離不相關的部分,一次解決一個問題。基於這個思想,Modbus TCP應用可以直接划分為2個層次,底層是驅動部分,負責一包數據從網絡上接收上來或發送出去,上層是Modbus的協議部分,就是Modbus寄存器的操作等。從這個角度來說,只要數據傳輸正確了,那么怎么處理就是另一個問題了,比如可以共用Modbus RS485的代碼等。

   下面測試了Modbus TCP的數據傳輸。

    Modbus TCP設計有幾個重要的點:

  1)Modbus是連續通信,不能和http一樣完成一次連接后就斷開,所以要不停的read,當讀出錯時在close(conn)關閉連接。

  2)Modbus可能存在通信失敗情況,需要關閉socket后再重新建立socket。

  3)Modbus作為工業協議,應用場景下一般不會多個客戶端連接一台機器,並且多個客戶端連接一台機器,寄存器的讀寫互斥會是一個大問題,所以常見的做法是一旦連接成功,就關閉socket禁止其他連接進來。客戶端主動斷開后再重新建立socket然后進入listen狀態。

/**
  * @brief serve modbus_tcp connection  
  * @param conn: connection socket 
  * @retval None
  */
void modbus_tcp_server(int conn) 
{
  int buflen = 1500;
  int ret;
  unsigned char recv_buffer[1500];
  int i;
    
Printf("start modbus tcp\r\n"); ret = read(conn, recv_buffer, buflen); while ( ret > 0 ) { ret = read(conn, recv_buffer, buflen); Printf("\r\n>:"); // debug print for(i=0; i<ret; i++) { Printf("%x ", recv_buffer[i]); } Printf("\r\n>"); } close(conn); Printf("close modbus tcp\r\n"); } /** * @brief modbus_task * @param arg: pointer on argument(not used here) * @retval None */ static void modbus_task(void *arg) { int sock, newconn, size; struct sockaddr_in address, remotehost; while(1) { /* create a TCP socket */ if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { Printf("can not create socket\r\n"); OSTimeDlyHMSM(0, 0, 1, 0); continue; } address.sin_family = AF_INET; address.sin_port = htons(502); // mosbus tcp port address.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&address, sizeof (address)) < 0) { Printf("can not bind socket\r\n"); close(sock); OSTimeDlyHMSM(0, 0, 2, 0); continue; } /* listen for incoming connections (TCP listen backlog = 1) */ listen(sock, 1); size = sizeof(remotehost); newconn = accept(sock, (struct sockaddr *)&remotehost, (socklen_t *)&size); if (newconn >= 0) { close(sock); //一次只接受一個連接
Printf("connect socket\r\n");
modbus_tcp_server(newconn); } else { close(sock); close(newconn); } }
}
/************************************************************** * void modbus_task_init(void) * * This function initializes the service. **************************************************************/ void modbus_task_init(void) { sys_thread_new( CHARGEN_THREAD_NAME, modbus_task, 0, 0, TCPIP_THREAD_PRIO+2); //函數棧在sys_thread_new中實現 }

 本例旨在測試LwIP的socket,所以並沒有完整的實現modbus TCP,但是其中的幾行測試代碼足以說明Mosbus TCP通信正常與否。

      ret = read(conn, recv_buffer, buflen);
      Printf("\r\n>:");  // debug print
      for(i=0; i<ret; i++)
      {
         Printf("%x ", recv_buffer[i]);
      }
      Printf("\r\n>");

可以啟動一個modbus poll來測試這段代碼。

如下,軟件連接成功說明已經完成socket連接,但是有通信error這是因為沒有實現協議處理導致的。

modbus poll的通信數據監控,沒有做回復處理所以此處看到的全是Tx:

再看上面Printf函數的串口輸出,其中LED ON/OFF是另外一個task在運行。

可以看到,所有modbus poll發送的數據包都被modbus_tcp_server函數正確接收,如果加上協議處理,那么就是一個完整的modbus TCP應用。

 

 

WebServer


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM