select函数的作用是检测一组socket中某个或者某几个是否有"事件"就绪。
事件分类
读事件就绪
- socket内核中,接收缓冲区中的子节数大于等于低水位标记SO_RCVLOWAT, 此时调用recv或者read函数可以无阻塞的读该描述符,并且返回值大于0
- TCP对端关闭连接,此时调用recv或read函数对socket进行读,返回值为0
- 监听fd上有新连接请求
- socket上有未处理的错误
写事件就绪
- socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置)大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写,并且返回值大于0
- socket的写操作被关闭(调用了close或者shutdown函数),对于一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
- socket有非阻塞的connect连接成功或者失败之后
异常事件就绪
- socket上收到带外数据
接口API
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
select参数说明:
- nfds: 所有需要使用select函数监听fd中最大fd值加1
- readfds: 需要监听可读事件的fd集合
- writefds: 需要监听可写事件的fd集合
- exceptfds: 需要监听异常事件的fd集合
- timeout: 超时时间,即在这个参数设定的时间内检测这些fd事件,超过这个时间后select函数将立即返回。
相关结构体:
timeout超时时间的结构体为struct timeval
:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
select函数超时总时间是timeout->tv_sec和timeout->tv_usec之后,前者单位是秒,后者单位是微秒
readfds/writefds/exceptfds的类型都是fd_set。其结构如下:
// sys/select.h
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Some versions of <linux/posix_types.h> define this macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask)) // 64
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; // 1024/64 = 16
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE // 1024
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
extern int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
/* We don't use `memset' because this would require a prototype and
the array isn't too big. */
#define __FD_ZERO(s) \
do { \
unsigned int __i; \
fd_set *__arr = (s); \
for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \
__FDS_BITS (__arr)[__i] = 0; \
} while (0)
#define __FD_SET(d, s) \
((void) (__FDS_BITS (s)[__FD_ELT(d)] |= __FD_MASK(d)))
#define __FD_CLR(d, s) \
((void) (__FDS_BITS (s)[__FD_ELT(d)] &= ~__FD_MASK(d)))
#define __FD_ISSET(d, s) \
((__FDS_BITS (s)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
- FD_SET(fd, fdsetp)宏 将一个fd添加到fdsetp这个集合中
- FD_CLR(fd, fdsetp)宏 从fdsetp集合中删除一个fd
- FD_ISSET(fd, fdsetp)宏 检测对应位置上是否置1
- FD_ZERO(fdsetp)宏 将fdsetp中所有fd都清除掉
select示例
/**
* @brief select示例
*/
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <vector>
#include <mutex>
std::mutex g_mutex;
#ifndef LOG_MSG
#define LOG_MSG(x) do { \
std::lock_guard<std::mutex> locker(g_mutex);\
std::cout << x << std::endl; \
} while(0)
#endif //LOG_MSG
constexpr int32_t kInvalidFd = -1;
int main(int argc, char *argv[])
{
if (argc < 3)
{
LOG_MSG("Usage: ");
LOG_MSG(" " << argv[0] << " $port $ip");
return -1;
}
// 创建监听套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
LOG_MSG("create socket fail");
return -1;
}
// init server addr
sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = inet_addr(argv[2]); // htonl(INADDR_ANY);
bindaddr.sin_port= htons(::atoi(argv[1]));
if(bind(listenfd,(struct sockaddr*) &bindaddr, sizeof(bindaddr)) == -1)
{
LOG_MSG("bind error");
close(listenfd);
return -1;
}
// start listen
if(listen(listenfd, SOMAXCONN) == -1)
{
LOG_MSG("listen error");
close(listenfd);
return -1;
}
std::vector<int> clientfds;
char recvbuf[256];
int maxfd = listenfd;
do
{
fd_set readset;
FD_ZERO(&readset);
FD_SET(listenfd, &readset);
for (auto& item : clientfds)
{
if (item != kInvalidFd)
{
FD_SET(item, &readset);
}
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
const auto ret = select(maxfd + 1, &readset, nullptr, nullptr, &tm);
if (ret < 0)
{
if (errno != EINTR)
break;
}
else if (ret == 0)
{
// timeout
continue;
}
else
{
if (FD_ISSET(listenfd, &readset))
{
// 检查监听fd上是否有读事件
struct sockaddr_in clientaddr;
socklen_t client_len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client_len);
if (clientfd < 0)
{
// 连接出错,退出
break;
}
LOG_MSG("accept a client connection, fd " << clientfd << " : " << inet_ntoa(clientaddr.sin_addr));
clientfds.emplace_back(clientfd);
if (clientfd > maxfd)
maxfd = clientfd;
}
else
{
for (auto& clifd : clientfds)
{
if (clifd != kInvalidFd && FD_ISSET(clifd, &readset))
{
memset(recvbuf, 0, sizeof(recvbuf));
int length = recv(clifd, recvbuf, 256, 0);
if (length <= 0 && errno != EINTR)
{
LOG_MSG("recv data error, clienfd: " << clifd);
close(clifd);
clifd = kInvalidFd;
continue;
}
LOG_MSG("clientfd: " << clifd << ", recv data: " << recvbuf);
}
}
}
}
} while (true);
for (const auto& clifd : clientfds)
{
if (clifd != kInvalidFd)
{
close(clifd);
}
}
close(listenfd);
return 0;
}
程序执行后输出结果如下:
# 启动服务端
➜ build ./test_select 8899 127.0.0.1
accept a client connection, fd 4 : 127.0.0.1
clientfd: 4, recv data: 33312222
clientfd: 4, recv data: 455533322332
clientfd: 4, recv data: 4444
recv data error, clienfd: 4
# 启动客户端
➜ ~ nc -v 127.0.0.1 8899
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:8899.
33312222
455533322332
4444
由于nc发送的数据是按换行符来区分的,每个数据包默认的换行符以\n结束,所以服务端收到数据后,显示出来的数据每一行下面都一个空白行。
nc安装和使用
nc安装
在CentOS上,可以使用以下命令安装nc:
yum update
yum install nc
nc常用命令
- 打开一个终端并启动nc监听器:nc -lnvp <端口号>
# 监听端口号为1234的端口
nc -lnvp 1234
- 在另一个终端中,输入以下命令以连接到nc监听器:nc <IP地址> <端口号>
# 连接到IP地址为192.168.1.100的计算机上的端口号为1234
nc 192.168.1.100 1234
select函数使用注释事项
- select函数调用前后会修改readfds/writefds/exceptfds这3个集合中的内容,所以下次调用select复用这个变量,需要再次调用FD_ZERO将集合清理,然后调用FD_SET将需要检测的事件fd再次添加进去。
- select函数也会修改timeval结构体的值,如果我们需要复用这个变量,需要给timeval变量重新设置值。
- select函数的timeval结构体的tv_sec和tv_usec这个两个值设置为0, 即检测事件设置超时时间为0. 其行为是select会检测相关集合中的fd, 如果没有就绪的事件,则立即返回。