lwIP TCP/IP 協議棧筆記之十八: Socket接口編程

目錄

1. Socket 概述

2. LwIP 中的socket

3. Socket API

3.1 socket()

3.2 bind()

3.3 connect()

3.4 listen()

3.5 accept()

3.6 read()、recv()、recvfrom()

3.7 sendto()

3.8 send()

3.9 write()

3.10 close()

3.11 ioctl()、ioctlsocket()

3.12 setsockopt()

3.13 getsockopt()

4. 實驗例程

4.1  TCP Server

4.2  TCP Client


1. Socket 概述

Socket 英文原意是“孔”或者“插座”的意思,在網絡編程中,通常將其稱之爲“套接字”,當前網絡中的主流程序設計都是使用Socket 進行編程的,因爲它簡單易用,更是一個標準,能在不同平臺很方便移植。

套接字(socket)是一個抽象層,應用程序可以通過它發送或接收數據,可對其進行像對文件一樣的打開、讀寫和關閉等操作。套接字允許應用程序將I/O插入到網絡中,並與網絡中的其他應用程序進行通信。網絡套接字是IP地址與端口的組合。

總之,套接字Socket=(IP地址:端口號),套接字的表示方法是點分十進制的IP地址後面寫上端口號,中間用冒號或逗號隔開。每一個傳輸層連接唯一地被通信兩端的兩個端點(即兩個套接字)所確定。

Socket最初是加利福尼亞大學Berkeley分校爲Unix系統開發的網絡通信接口。後來隨着TCP/IP網絡的發展,Socket成爲最爲通用的應用程序接口,也是在Internet上進行應用開發最爲通用的API

爲了能讓更多開發者直接上手LwIP 的編程,專門設計了LwIP 的第三種編程接口——Socket API,它兼容BSD Socket。

Socket 雖然是能在多平臺移植,但是LwIP 中的Socket 並不完善,因爲LwIP 設計之初就是爲了在嵌入式平臺中使用,它只實現了完整Socket 的部分功能,不過,在嵌入式平臺中,這些功能早已足夠。

2. LwIP 中的socket

在LwIP 中,Socket API 是基於NETCONN API 之上來實現的,系統最多提供MEMP_NUM_NETCONN 個netconn 連接結構,因此決定Socket 套接字的個數也是那麼多個。

爲了更好對netconn 進行封裝,LwIP 還定義了一個套接字結構體——lwip_sock(稱之爲Socket 連接結構),每個lwip_sock 內部都有一個netconn 的指針,實現了對netconn 的再次封裝。

LwIP 定義了一個lwip_sock 類型的sockets數組,通過套接字就可以直接索引並且訪問這個結構體了,這也是爲什麼套接字是一個整數的原因,lwip_sock 結構體是比較簡單的,因爲基本上全是依賴netconn 實現。

#define NUM_SOCKETS MEMP_NUM_NETCONN    // 默認是4

/** 全局可用套接字數組 **/
static struct lwip_sock sockets[NUM_SOCKETS];

union lwip_sock_lastdata {
  struct netbuf *netbuf;
  struct pbuf *pbuf;
};

/** 包含用於套接字的所有內部指針和狀態*/
struct lwip_sock {
  /** 套接字當前是在netconn 上構建的,每個套接字都有一個netconn*/
  struct netconn *conn;
  /** 從上一次讀取中留下的數據 */
  union lwip_sock_lastdata lastdata;
#if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL
  /** number of times data was received, set by event_callback(),
      tested by the receive and select functions */
  s16_t rcvevent;
  /** number of times data was ACKed (free send buffer), set by event_callback(),
      tested by select */
  u16_t sendevent;
  /** error happened for this socket, set by event_callback(), tested by select */
  u16_t errevent;
  /** 使用select 等待此套接字的線程數 */
  SELWAIT_T select_waiting;
#endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */
#if LWIP_NETCONN_FULLDUPLEX
  /* counter of how many threads are using a struct lwip_sock (not the 'int') */
  u8_t fd_used;
  /* status of pending close/delete actions */
  u8_t fd_free_pending;
#define LWIP_SOCK_FD_FREE_TCP  1
#define LWIP_SOCK_FD_FREE_FREE 2
#endif
};

3. Socket API

3.1 socket()

向內核申請一個套接字,在本質上該函數其實就是對netconn_new()函數進行了封裝,雖然說不是直接調用它,但是主體完成的工作就做了 netconn_new()函數的事情,而且該函數本質是一個宏定義.

/** @ingroup socket */
#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)

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

#define AF_INET         2

/* Socket protocol types (TCP/UDP/RAW) */
#define SOCK_STREAM     1
#define SOCK_DGRAM      2
#define SOCK_RAW        3

參數domain :表示該套接字使用的協議簇,對於TCP/IP 協議來說,該值始終爲AF_INET。

參數type: 指定了套接字使用的服務類型,可能的類型有3 種:

1. SOCK_STREAM:提供可靠的(即能保證數據正確傳送到對方)面向連接的Socket 服務,多用於資料(如文件)傳輸,如TCP 協議。

2. SOCK_DGRAM:是提供無保障的面向消息的Socket 服務,主要用於在網絡上發廣播信息,如UDP 協議,提供無連接不可靠的數據報交付服務。

3. SOCK_RAW:表示原始套接字,它允許應用程序訪問網絡層的原始數據包,這個套接字用得比較少,暫時不用理會它。

參數protocol: 指定了套接字使用的協議,在IPv4 中,只有TCP 協議提供SOCK_STREAM這種可靠的服務,只有UDP 協議供SOCK_DGRAM服務,對於這兩種協議,protocol 的值均爲0。

當申請套接字成功的時候,該函數返回一個int 類型的值,也是Socket 描述符,用戶通過這個值可以索引到一個Socket 連接結構——lwip_sock,當申請套接字失敗時,該函數返回-1。

3.2 bind()

該函數的功能與netconn_bind()函數是一樣的,用於服務器端綁定套接字與網卡信息,實際上就是對netconn_bind()函數進行了封裝,可以將一個申請成功的套接字與網卡信息進行綁定。

/** @ingroup socket */
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)

int
lwip_bind(int s, const struct sockaddr *name, socklen_t namelen);

參數s : 表示要綁定的Socket 套接字

參數name: 是一個指向sockaddr 結構體的指針,其中包含了網卡的IP 地址、端口號等重要的信息,LwIP 爲了更好描述這些信息,使用了sockaddr 結構體來定義了必要的信息的字段,它常被用於Socket API 的很多函數中,我們在使用bind()的時候,只需要直接填寫相關字段即可.

參數namelen: 指定了name 結構體的長度

struct sockaddr {
  u8_t        sa_len;            /* 長度 */
  sa_family_t sa_family;         /* 協議簇 */
  char        sa_data[14];       /* 連續的14字節信息 */
};

需要填寫的IP 地址與端口號等信息,都在sa_data 連續的14 字節信息裏面,但是這個數據對我們不友好,因此LwIP 還定義了另一個對開發者更加友好的結構體——sockaddr_in,我們一般也是用這個結構體.

/* members are in network byte order */
struct sockaddr_in {
  u8_t            sin_len;        // 長度
  sa_family_t     sin_family;     // 協議簇  uint8_t
  in_port_t       sin_port;       // 端口    uint16_t
  struct in_addr  sin_addr;       // 地址    uint32_t
#define SIN_ZERO_LEN 8
  char            sin_zero[SIN_ZERO_LEN]; 
};

這個結構體的前兩個字段是與sockaddr 結構體的前兩個字段一致,而剩下的字段就是sa_data 連續的14 字節信息裏面的內容,只不過從新定義了成員變量而已,sin_port 字段是我們需要填寫的端口號信息,sin_addr 字段是我們需要填寫的IP 地址信息,剩下sin_zero區域的8 字節保留未用.

使用例程:

#define PORT              5001
#define IP_ADDR        "192.168.0.181"


int sock = -1;  
struct sockaddr_in server_addr;

sock = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;      
server_addr.sin_port = htons(PORT);   
server_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));    

if (bind(sock, (struct sockaddr *)&server_addr, 
                sizeof(struct sockaddr)) == -1) {
    ;
}

3.3 connect()

函數的作用與netconn_connect()函數的作用基本一致,因爲就是封裝了netconn_connect()函數。它用於客戶端中,將Socket 與遠端IP 地址、端口號進行綁定,在TCP 客戶端連接中,調用這個函數將發生握手過程(會發送一個TCP 連接請求),並最終建立新的TCP 連接,而對於UDP 協議來說,調用這個函數只是在UDP 控制塊中記錄遠端IP 地址與端口號,而不發送任何數據,參數信息與bind()函數是一樣的.

/** @ingroup socket */
#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)

int
lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);

3.4 listen()

函數是對netconn_listen()函數的封裝,只能在TCP 服務器中使用,讓服務器進入監聽狀態,等待遠端的連接請求,LwIP 中可以接收多個客戶端的連接,因此參數backlog 指定了請求隊列的大小.

/** @ingroup socket */
#define listen(s,backlog)                         lwip_listen(s,backlog)

int
lwip_listen(int s, int backlog);

3.5 accept()

accept()函數與netconn_accept()函數作用一樣,用於TCP 服務器中,等待着遠端主機的連接請求,並且建立一個新的TCP 連接,在調用這個函數之前需要通過調用listen()函數讓服務器進入監聽狀態。accept()函數的調用會阻塞應用線程直至與遠程主機建立TCP 連接。參數addr 是一個返回結果參數,它的值由accept()函數設置,其實就是遠程主機的地址與端口號等信息,當新的連接已經建立後,遠端主機的信息將保存在連接句柄中,它能夠唯一的標識某個連接對象。同時函數返回一個int 類型的套接字描述符,根據它能索引到連接結構,如果連接失敗則返回-1.

/** @ingroup socket */
#define accept(s,addr,addrlen)                    lwip_accept(s,addr,addrlen)

int
lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);

3.6 read()、recv()、recvfrom()

read()與recv()函數的核心是調用recvfrom()函數,而recvfrom()函數是基於netconn_recv()函數來實現的,recv()與read()函數用於從Socket 中接收數據,它們可以是TCP 協議和UDP 協議

/** @ingroup socket */
#define read(s,mem,len)                           lwip_read(s,mem,len)

ssize_t
lwip_read(int s, void *mem, size_t len)
{
  return lwip_recvfrom(s, mem, len, 0, NULL, NULL);
}

/** @ingroup socket */
#define recv(s,mem,len,flags)                     lwip_recv(s,mem,len,flags)

ssize_t
lwip_recv(int s, void *mem, size_t len, int flags)
{
  return lwip_recvfrom(s, mem, len, flags, NULL, NULL);
}

ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,
              struct sockaddr *from, socklen_t *fromlen);

men 參數記錄了接收數據的緩存起始地址,

len 用於指定接收數據的最大長度,如果函數能正確接收到數據,將會返回一個接收到數據的長度,否則將返回-1,若返回值爲0,表示連接已經終止,應用程序可以根據返回的值進行不一樣的操作。

recv()函數包含一個flags 參數,我們暫時可以直接忽略它,設置爲0 即可。注意,如果接收的數據大於用戶提供的緩存區,那麼多餘的數據會被直接丟棄.

3.7 sendto()

函數主要是用於UDP 協議傳輸數據中,它向另一端的UDP 主機發送一個UDP 報文,本質上是對netconn_send()函數的封裝,參數data 指定了要發送數據的起始地址,而size 則指定數據的長度,參數flag 指定了發送時候的一些處理,比如外帶數據等,此時我們不需要理會它,一般設置爲0 即可,參數to 是一個指向sockaddr 結構體的指針,在這裏需要我們自己提供遠端主機的IP 地址與端口號,並且用tolen 參數指定這些信息的長度

/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)


ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,
            const struct sockaddr *to, socklen_t tolen);

3.8 send()

send()函數可以用於UDP 協議和TCP 連接發送數據。在調用send()函數之前,必須使用connect()函數將遠端主機的IP 地址、端口號與Socket 連接結構進行綁定。對於UDP 協議,send()函數將調用lwip_sendto()函數發送數據,而對於TCP 協議,將調用netconn_write_partly()函數發送數據。相對於sendto()函數,參數基本是沒啥區別的,但無需我們設置遠端主機的信息,更加方便操作,因此這個函數在實際中使用也是很多的

/** @ingroup socket */
#define send(s,dataptr,size,flags)                lwip_send(s,dataptr,size,flags)

ssize_t
lwip_send(int s, const void *data, size_t size, int flags);

3.9 write()

這個函數一般用於處於穩定的TCP 連接中傳輸數據,當然也能用於UDP 協議中,它也是基於lwip_send 上實現的,但是無需我們設置flag 參數

/** @ingroup socket */
#define write(s,dataptr,len)                      lwip_write(s,dataptr,len)


ssize_t
lwip_write(int s, const void *data, size_t size)
{
  return lwip_send(s, data, size, 0);
}

3.10 close()

close()函數是用於關閉一個指定的套接字,在關閉套接字後,將無法使用對應的套接字描述符索引到連接結構,該函數的本質是對netconn_delete()函數的封裝(真正處理的函數是netconn_prepare_delete()),如果連接是TCP 協議,將產生一個請求終止連接的報文發送到對端主機中,如果是UDP 協議,將直接釋放UDP 控制塊的內容

/** @ingroup socket */
#define close(s)                                  lwip_close(s)

int
lwip_close(int s);

3.11 ioctl()、ioctlsocket()

兩個函數,其實是一樣的,本質是宏定義,都是調用lwip_ioctl()函數,它用於獲取與設置套接字相關的操作參數.

s:一個標識套接口的描述字。

cmd:對套接口s的操作命令。

argp:指向cmd命令所帶參數的指針

參數cmd 指明對套接字的操作命令,在LwIP中只支持FIONREAD 與FIONBIO 命令:

  • FIONREAD 命令確定套接字s 自動讀入的數據量,這些數據已經被接收,但應用線程並未讀取的,所以可以使用這個函數來獲取這些數據的長度,在這個命令狀態下,argp 參數指向一個無符號長整型,用於保存函數的返回值(即未讀數據的長度)。如果套接字是SOCK_STREAM類型,則FIONREAD 命令會返回recv()函數中所接收的所有數據量,這通常與在套接字接收緩存隊列中排隊的數據總量相同;而如果套接字是SOCK_DGRAM類型的,則FIONREAD 命令將返回在套接字接收緩存隊列中排隊的第一個數據包大小。
  • FIONBIO 命令用於允許或禁止套接字的非阻塞模式。在這個命令下,argp 參數指向一個無符號長整型,如果該值爲0 則表示禁止非阻塞模式,而如果該值非0 則表示允許非阻塞模式則。當創建一個套接字的時候,它就處於阻塞模式,也就是
    說非阻塞模式被禁止,這種情況下所有的發送、接收函數都會是阻塞的,直至發送、接收成功才得以繼續運行;而如果是非阻塞模式下,所有的發送、接收函數都是不阻塞的,如果發送不出去或者接收不到數據,將直接返回錯誤代碼給用戶,這就需要用戶對這些“意外”情況進行處理,保證代碼的健壯性,這與BSD Socket 是一致的。
/** @ingroup socket */
#define ioctlsocket(s,cmd,argp)                   lwip_ioctl(s,cmd,argp)

/** @ingroup socket */
#define ioctl(s,cmd,argp)                         lwip_ioctl(s,cmd,argp)


int
lwip_ioctl(int s, long cmd, void *argp);

3.12 setsockopt()

/** @ingroup socket */
#define setsockopt(s,level,optname,opval,optlen) lwip_setsockopt(s,level,optname,opval,optlen)

int
lwip_setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

這個函數是用於設置套接字的一些選項的,參數level 有多個常見的選項,如:

SOL_SOCKET:表示在Socket 層。

IPPROTO_TCP:表示在TCP 層。

IPPROTO_IP: 表示在IP 層。

參數optname 表示該層的具體選項名稱,比如:

  • 1. 對於SOL_SOCKET 選項,可以是SO_REUSEADDR(允許重用本地地址和端口)、SO_SNDTIMEO(設置發送數據超時時間)、SO_SNDTIMEO(設置接收數據超時時間)、SO_RCVBUF(設置發送數據緩衝區大小)等等。
  • 2. 對於IPPROTO_TCP 選項,可以是TCP_NODELAY(不使用Nagle 算法)、TCP_KEEPALIVE(設置TCP 保活時間)等等。
  • 3. 對於IPPROTO_IP 選項,可以是IP_TTL(設置生存時間)、IP_TOS(設置服務類型)等等。

3.13 getsockopt()

這個函數與setsockopt()函數的選項參數及名稱都是差不多的,只不過是作用是獲得這些選項信息在這裏就不過多講解

4. 實驗例程

4.1  TCP Server

#define PORT              5001
#define RECV_DATA         (1024)


static void 
tcpecho_thread(void *arg)
{
  int sock = -1,connected;
  char *recv_data;
  struct sockaddr_in server_addr,client_addr;
  socklen_t sin_size;
  int recv_data_len;
  
  recv_data = (char *)pvPortMalloc(RECV_DATA);
  if (recv_data == NULL)
  {
      printf("No memory\n");
      goto __exit;
  }
  
  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0)
  {
      printf("Socket error\n");
      goto __exit;
  }
  
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = INADDR_ANY;
  server_addr.sin_port = htons(PORT);

  memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
  
  if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
  {
      printf("Unable to bind\n");
      goto __exit;
  }
  
  if (listen(sock, 5) == -1)
  {
      printf("Listen error\n");
      goto __exit;
  }
  
  while(1)
  {
    sin_size = sizeof(struct sockaddr_in);

    connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);

    printf("new client connected from (%s, %d)\n",
            inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    {
      int flag = 1;
      
      setsockopt(connected,
                 IPPROTO_TCP,     /* set option at TCP level */
                 TCP_NODELAY,     /* name of option */
                 (void *) &flag,  /* the cast is historical cruft */
                 sizeof(int));    /* length of option value */
    }
    
    while(1)
    {
      recv_data_len = recv(connected, recv_data, RECV_DATA, 0);
      
      if (recv_data_len <= 0) 
        break;
      
      printf("recv %d len data\n",recv_data_len);
      
      write(connected,recv_data,recv_data_len);
      
    }
    if (connected >= 0) 
      closesocket(connected);
    
    connected = -1;
  }
__exit:
  if (sock >= 0) closesocket(sock);
  if (recv_data) free(recv_data);
}

4.2  TCP Client

#define PORT              5001
#define IP_ADDR        "192.168.0.100"

static void client(void *thread_param)
{
  int sock = -1;
  struct sockaddr_in client_addr;
  
  uint8_t send_buf[]= "This is a TCP Client test...\n";
  
  while(1)
  {
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
      printf("Socket error\n");
      vTaskDelay(10);
      continue;
    } 

    client_addr.sin_family = AF_INET;      
    client_addr.sin_port = htons(PORT);   
    client_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
    memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));    

    if (connect(sock, 
               (struct sockaddr *)&client_addr, 
                sizeof(struct sockaddr)) == -1) 
    {
        printf("Connect failed!\n");
        closesocket(sock);
        vTaskDelay(10);
        continue;
    }                                           
    
    printf("Connect to iperf server successful!\n");
    
    while (1)
    {
      if(write(sock,send_buf,sizeof(send_buf)) < 0)
        break;
   
      vTaskDelay(1000);
    }
    
    closesocket(sock);
  }

}

 注意: PC與單板連接的時候,網絡調試助手,必須保證防火牆是允許通信的,否則可能被攔截而造成失敗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章