引言:
select()函数是Socket编程中实现I/O多路复用的基本函数,通过select机制,我们可以实现同时监控多个I/O描述符,控制多个I/O的输入输出。
作为基本的,我们可以通过select机制,代替上一篇博客的setsockopt()函数设置Socket的接收超时时间。作为开始我简单介绍下很好理解的select机制的基本应用方法,与相关的宏(或者函数)。
1.Select基本使用步骤:
(1)fd_set read_set;//声明待监控的集合read_set
(2)FD_ZERO(&read_set);//将待监控的集合read_set中的监控位全部变为0,即初始态。
(3)FD_SET(1, &read_set);//将集合read_set中的bit1位置1,即要监控文件描述符fd为1的I/0口。
(4)FD_SET(5, &read_set);//同上,将集合read_set中的bit5位置1,即要监控文件描述符fd为5的I/0口。(即我们监控的对象是fd为1,5的文件I/O)
(5)ret = select(5+1, &read_set, NULL, NULL, &timeout);//启动监控,若集合read_set中有可操作的文件描述符,即1,5可操作(即已就绪),就返回可操作的数量给ret.
(6)FD_ISSET(1, read_set);//测试是否是fd=1的文件可读了(可读的话,相应的bit位会在调用select时清零,我们不用管这种机制的实现,只要会用即可)
(7)FD_ISSET(5, read_set);//测试是否是fd=5的文件可读了
(8)如果1,5可读了,就执行对应的读操作。
(9)如果不想再监控fd=1的文件I/O,就调用FD_CLR(1, read_set),将其对应的监控bit位清零,select()就不会再监控之了。
(10)特别注意的是,调用select()函数被调用后,任何与未就绪读/写描述符相关的bit均被清0,为此,为保证,我们始终能够监控所关心的文件描述符(即通过FD_SET(fd, &read_set);将关心的bit置位为1),应总是在重新调用select()前,调用FD_SET(fd, &read_set);语句加入关心的文件描述符。
2.使用select设置Socket TCP接收超时时间
实验用的代码模板是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";
-
-
int readable_timeo(int fd, int sec);
-
int Readable_timeo(int fd, int sec);
-
int ret;
-
-
int
-
readable_timeo
(int fd, int sec)
-
{
-
fd_set rset;
-
struct
timeval tv;
-
-
FD_ZERO(&rset);
-
FD_SET(fd, &rset);
-
-
tv.tv_sec = sec;
-
tv.tv_usec =
0;
-
ESP_LOGI(TAG,
"Timeout will occur after %d sec", sec);
-
return(
select(fd+
1, &rset,
NULL,
NULL, &tv));
-
/* > 0 if descriptor is readable */
-
}
-
/* end readable_timeo */
-
-
int
-
Readable_timeo
(int fd, int sec)
-
{
-
int n;
-
-
if ( (n =
readable_timeo(fd, sec)) <
0)
-
ESP_LOGI(TAG,
"readable_timeo error");
-
return(n);
-
}
-
-
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");
-
-
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");
-
-
while (
1) {
-
ret =
Readable_timeo(sock,
10);
//set timeout and add if
-
-
if (ret >
0){
-
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;
-
}
-
}
-
}
-
else {
-
ESP_LOGE(TAG,
"timeout or error: ret of readable_timeo= %d", ret);
-
break;
-
}
-
}
-
-
if (sock !=
-1 || ret <=
0) {
-
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);
-
}
实现上主要是参考《UNIX 网络编程 卷1,套接字编程》一书相关介绍,修改了其中的
int readable_timeo(int fd, int sec);
int Readable_timeo(int fd, int sec);
两个函数,并在while()的第一句代码添加了设置超时的语句:ret = Readable_timeo(sock, 10);//set timeout
3.实验结果与分析:
(1)编译烧录上述程序,包含接收延时的TCP server程序就设置好了。运行该程序,其串口输出信息如下:
(2)在终端下运行以下命令,启动终端下建立的模拟TCP Client:(注意替换(1)中打印的自己的开发板IP地址)
(3)连接后,会提示Timeout will occur after 10 sec,即10秒后会触发接收超时,我们在10s内发送字符串“you got me"则终端就会收到开发板回复的消息“you got me",同时串口提示收到PC端的终端发来的消息,并再次提示接收超时Timeout will occur after 10 sec。
(4)如果,10秒内未向开发板发出任何消息,将触发接收超时,串口打印信息如下:
提示超时发生,并关闭开发板的Socket TCP Server.
(附:如果对上述nc命令以及建立TCP Server/Client不太熟悉,可以参考我博客的Socket编程(1)中的介绍)
当然,Select()仅仅用来设置Socket的接收超时时间实在是大材小用,作为I/O多路复用的基本实现机制,它的作用可是非同小可,下一篇博客,将介绍用select机制在ESP8266上实现单任务处理多客户端交互的实现,感兴趣的可以看一看哦。