引言:
网络编程中最常用的就是Socket编程,即网络套接字编程。Socket API不仅提供了连网、接收数据的接口,也提供了灵活地查看、修改Socket option,即配置选项的接口。想充分发挥Socket编程的作用,了解这些配置选项将是你开发出更好用的网络工程代码。
1.代码:
下面是查看、修改socket相关配置选项的代码,其中的代码都是可以化为己用的,我尽量添加了相关注释:
实验用的代码模板是ESP8266 RTOS-SDKv3.3以上版本的Socket TCP Example,开发板是ESP8266DevkitC.
/* BSD Socket API Example*/
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>
#define PORT CONFIG_EXAMPLE_PORT
static const char *TAG = "example";
/* use to get the socket options */
union val {
int i_val;
long l_val;
struct linger linger_val;
struct timeval timeval_val;
} val;
static char *sock_str_flag(union val *, int);
static char *sock_str_int(union val *, int);
static char *sock_str_linger(union val *, int);
static char *sock_str_timeval(union val *, int);
struct sock_opts {
const char *opt_str;
int opt_level;
int opt_name;
char *(*opt_val_str)(union val *, int);
} sock_opts[] = {
{ "SO_KEEPALIVE", SOL_SOCKET, SO_KEEPALIVE, sock_str_flag },
{ "SO_LINGER", SOL_SOCKET, SO_LINGER, sock_str_linger },
{ "SO_RCVBUF", SOL_SOCKET, SO_RCVBUF, sock_str_int },
{ "SO_SNDBUF", SOL_SOCKET, SO_SNDBUF, sock_str_int },
{ "SO_RCVTIMEO", SOL_SOCKET, SO_RCVTIMEO, sock_str_timeval },
{ "SO_SNDTIMEO", SOL_SOCKET, SO_SNDTIMEO, sock_str_timeval },
{ "SO_REUSEADDR", SOL_SOCKET, SO_REUSEADDR, sock_str_flag }
};
/* Some Conversion function ,include checkopts3 */
static char strres[128];
static char *
sock_str_flag(union val *ptr, int len)
{
/* *INDENT-OFF* Attention:ESP32 long type is 4Bytes,long long type is 8Bytes*/
if (len != sizeof(long long)){
snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
}
else
snprintf(strres, sizeof(strres),
"%s", (ptr->l_val == 0) ? "off" : "on");
return(strres);
/* *INDENT-ON* */
}
static char *
sock_str_int(union val *ptr, int len)
{
if (len != sizeof(long long))
snprintf(strres, sizeof(strres), "size (%d) not sizeof( long long)", len);
else
snprintf(strres, sizeof(strres), "%ld", ptr->l_val);
return(strres);
}
static char *
sock_str_linger(union val *ptr, int len)
{
struct linger *lptr = &ptr->linger_val;
if (len != sizeof(struct linger))
snprintf(strres, sizeof(strres),
"size (%d) not sizeof(struct linger)", len);
else
snprintf(strres, sizeof(strres), "l_onoff = %d, l_linger = %d",
lptr->l_onoff, lptr->l_linger);
return(strres);
}
static char *
sock_str_timeval(union val *ptr, int len)
{
struct timeval *tvptr = &ptr->timeval_val;
if (len != sizeof(struct timeval))
snprintf(strres, sizeof(strres),
"size (%d) not sizeof(struct timeval)", len);
else
snprintf(strres, sizeof(strres), "%ld sec, %ld usec",
tvptr->tv_sec, tvptr->tv_usec);
return(strres);
}/*end for checkopt*/
static void tcp_server_task(void *pvParameters)
{
char rx_buffer[128];
char addr_str[128];
int addr_family;
int ip_protocol;
while (1) {
#ifdef CONFIG_EXAMPLE_IPV4
struct sockaddr_in destAddr;
destAddr.sin_addr.s_addr = htonl(INADDR_ANY);
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
inet_ntoa_r(destAddr.sin_addr, addr_str, sizeof(addr_str) - 1);
#else // IPV6
struct sockaddr_in6 destAddr;
bzero(&destAddr.sin6_addr.un, sizeof(destAddr.sin6_addr.un));
destAddr.sin6_family = AF_INET6;
destAddr.sin6_port = htons(PORT);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
inet6_ntoa_r(destAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
#endif
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (listen_sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
/* Now, let's get the socket's option */
socklen_t len;
struct sock_opts *ptr;
for (ptr = sock_opts; ptr->opt_str != NULL; ptr++) {
printf("%s: ", ptr->opt_str);
if (ptr->opt_val_str == NULL)
printf("(undefined)\n");
else {
len = sizeof(val);
if (getsockopt(listen_sock, ptr->opt_level, ptr->opt_name,
&val, &len) == -1) {
printf("getsockopt error\n");
} else {
printf("default = %s\n", (*ptr->opt_val_str)(&val, len));
}
}
}
/* Now ,let's set some options*/
// ptr = &sock_opts[4];
struct timeval set_my_timeval = {10,20};
len = sizeof(struct timeval);
if (setsockopt(listen_sock, SOL_SOCKET, SO_RCVTIMEO,
&set_my_timeval, len) == -1) {
printf("getsockopt error\n");
} else {
printf("setoption_accept_timeout = %s\n", sock_str_timeval((union val *)(&set_my_timeval), len));
}
/*check the changed value*/
vTaskDelay(100);
ptr = &sock_opts[4];
if (getsockopt(listen_sock, ptr->opt_level, ptr->opt_name,
&val, &len) == -1) {
printf("getsockopt error\n");
} else {
printf("nowoption_accept_timeout = %s\n", (*ptr->opt_val_str)(&val, len));
}
/* bind() listen() */
int err = bind(listen_sock, (struct sockaddr *)&destAddr, sizeof(destAddr));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket binded");
err = listen(listen_sock, 1);
if (err != 0) {
ESP_LOGE(TAG, "Error occured during listen: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket listening");
#ifdef CONFIG_EXAMPLE_IPV6
struct sockaddr_in6 sourceAddr; // Large enough for both IPv4 or IPv6
#else
struct sockaddr_in sourceAddr;
#endif
uint addrLen = sizeof(sourceAddr);
int sock = accept(listen_sock, (struct sockaddr *)&sourceAddr, &addrLen);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket accepted");
/* Now, let's get the socket's option again */
for (ptr = sock_opts; ptr->opt_str != NULL; ptr++) {
printf("%s: ", ptr->opt_str);
if (ptr->opt_val_str == NULL)
printf("(undefined)\n");
else {
len = sizeof(val);
if (getsockopt(sock, ptr->opt_level, ptr->opt_name,
&val, &len) == -1) {
printf("getsockopt error\n");
} else {
printf("default = %s\n", (*ptr->opt_val_str)(&val, len));
}
}
}
/* Now ,let's set some options again*/
// ptr = &sock_opts[4];
set_my_timeval.tv_sec = 12;
set_my_timeval.tv_usec = 12;
len = sizeof(struct timeval);
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
&set_my_timeval, len) == -1) {
printf("getsockopt error\n");
} else {
printf("setoption_recv_timeout= %s\n", sock_str_timeval((union val *)(&set_my_timeval), len));
}
/*check the changed value*/
vTaskDelay(100);
ptr = &sock_opts[4];
if (getsockopt(sock, ptr->opt_level, ptr->opt_name,
&val, &len) == -1) {
printf("getsockopt error\n");
} else {
printf("nowoption_ recv_timeout= %s\n", (*ptr->opt_val_str)(&val, len));
}
// /*set a socket as nonbloacking */
// int flags;
// if ((flags = fcntl(sock, F_GETFL, 0)) < 0)
// printf("error");
// flags |= O_NONBLOCK;
// if ((fcntl(sock, F_SETFL, flags)) < 0)
// printf("error");
while (1) {
set_my_timeval.tv_sec = 12;
set_my_timeval.tv_usec = 12;
len = sizeof(struct timeval);
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
&set_my_timeval, len) == -1) {
printf("getsockopt error\n");
} else {
printf("setoption = %s\n", sock_str_timeval((union val *)(&set_my_timeval), len));
}
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
// Error occured during receiving
if (len < 0) {
ESP_LOGE(TAG, "recv failed: errno %d", errno);
break;
}
// Connection closed
else if (len == 0) {
ESP_LOGI(TAG, "Connection closed");
break;
}
// Data received
else {
#ifdef CONFIG_EXAMPLE_IPV6
// Get the sender's ip address as string
if (sourceAddr.sin6_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
} else if (sourceAddr.sin6_family == PF_INET6) {
inet6_ntoa_r(sourceAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
}
#else
inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
#endif
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
ESP_LOGI(TAG, "%s", rx_buffer);
int err = send(sock, rx_buffer, len, 0);
if (err < 0) {
ESP_LOGE(TAG, "Error occured during sending: errno %d", errno);
break;
}
}
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
void app_main()
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
xTaskCreate(tcp_server_task, "tcp_server", 4096, NULL, 5, NULL);
}
2.实验结果分析:
由于实验结果较长,我分为以下几个部分分别分析,以得出一些有用的结论。
(1)开发板编译烧录上述程序后,打印信息如下,首先是Socket原来的一些option的值。此外,我修改了其SO_RCVTIMEO的值,其原来是0 sec, 0usec.我设置的值是10 sec, 20usec. 再次查询其值是10 sec, 0usec(因为该开发板计时精度有限,因此自动舍去了微妙级别的数值20usec),证明修改成功。同时注意到,开发板上运行的TCP Server的IP地址为192.168.47.100.
(2)剩下的实验结果如下:
在Ubuntu下打开一个Terminal终端,输入如下命令连接开发板上的TCP Server:
nc 192.168.47.100 3333(注意将命令中的IP替换为你的串口打印的IP地址和端口号,不了解的可以参考我的上一篇博客,Socket编程(1))。连接后,开发板打印信息如下:
连接成功提示“Socket accept".之后打印出当前Socket的配置选项值。此外,需要提醒的是在(1)中设置的SO_RCVTIMEO的值是10sec,这意味这accept()必须在10秒内收到连接请求,超出10秒,就会抛出异常,终止程序。因此必须在10秒内输入上述nc命令,否则accept()就会在超出时间后,停止程序,提示超时异常。(这点可以试一试,看看实验结果就明白了)
最最重要的是,我们在上述代码中生成了两个套接字:Listen_sock、sock,前者经过Socket()产生,后者经过accept()产生,尽管accept()函数生成的新的套接字sock是向accept()传入Listen_sock加工而来的,但,sock并未继承Listen的option,这点可以通过比较(1)、(2)打印的结果得来,显而易见,sock的初始SO_RCVTIMEO是0sec 0usec,并不是(1)中设置的10sec.
实际上,Socket()生成的套接字、accept()生成的套接字是完全不同的属性,前者属性默认是发起连接的,后者的属性是接收连接请求的,因此,在设置Socket的option时,务必区分开来这两个套接字的属性。
从上面的打印信息来看,我再一次修改了SO_RCVTIMEO的值为12sec。那么这个12sec将影响接下来的哪些行为呢?它将影响的是recv()函数的行为,即12秒内服务器必须收到客户端发来的数据,否则就会触发超时异常,程序终止。
假设你在12秒内在终端输入以下信息:
向开发板的Server端发送消息“you got me", 开发板会回复同样的“you got me"作为回复。
(3)接到上述数据后,开发板的串口输出信息如下,提示,受到11Byetes的数据,以及收到的数据内容。
同样的,接收完数据后,将再一次进入循环,等待12秒内下一次数据的到来,如果没有数据发来,就会打印红色部分的串口信息这一段信息,可自行分析,因为未设置Socket的地址/端口可复用,因此触发超时异常就会停止程序运行。
值得一提的是,Socket的SO_RCVTIMEO选项,在accept()之前设置为10sec,其决定的是accept()的超时时间,而在accept()之后设置为12sec,其决定的是recv()的超时时间。此外,Socket的计时机制是累计计时,除非重新设置超时(即程序中的setsockopt()语句)或者有超时异常抛出,否则超时到达,不会重新开始计时。因此在循环中为使得每次recv()都反复设置超时时间为12sec,程序中将setsockopt()语句写在循环体内的recv()函数前。
最后,接收超时选项SO_RCVTIMEO作为Socket的配置选项中最基本的option,用的实在是多,但上述通过Setsockopt()语句的设置超时值的方法有些繁琐,下面我将再写一篇博客,介绍Socket编程中的利器-Select()的用法,通过Select机制设置Socket的接收超时时间。