目錄
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與單板連接的時候,網絡調試助手,必須保證防火牆是允許通信的,否則可能被攔截而造成失敗。