LwIP是嵌入式領域一個流行的以太網協議棧, LwIP開放源碼,用C寫成很是方便移植,而且支持socket接口,使用者能夠集中精力處理應用功能。html
本文是LwIP socket的一個使用小結,使用的測試平臺是stm32+enc28j60+lwip+uc/OS-II。服務器
一個基本的socket創建順序是:網絡
Server端:dom
Client端:socket
lwip的socket和PC上的socket接口一致,只是底層實現用lwip的API進行了封裝,能夠參考lwip\src\include\lwip\sockets.h。tcp
#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_INET六、AF_LOCAL、AF_ROUTE其中AF_INET表明使用ipv4地址spa
type:socket類型,經常使用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等debug
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客戶端向服務器發送終止鏈接請求。
要使用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中實現 }
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應用。