开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
select函数
select 函数如下所示:
int select(int maxfdp1, fdset *readset, fd_set *write_set, fd_set *exceptset,
const struct timeval *timeout);
-
*readset, *write_set, *exceptset: 需要select帮我们处理的set集合,当我们想处理某 socket 某些事件(读、写、异常)的时候,把socket加入到这些集合当中去,当这些事件准备就绪的时候,select 会帮我们做好标记,等待我们处理。我们对哪个时间不感兴趣就把对应的参数设置成为空指针
-
*timeout: select 阻塞等待的时间;
1. 永远等待(timeout=NULL);
2. 等待一段时间,在一个描述符准备好 I/O 时返回;
3. 立即返回,此时 timeout 设置的时间应该为0; -
maxfdp1: select 需要检测 fdset 中所有的文件描述符,但是超过 maxfdp1 的文件描述符会被忽略。
在unix网络变成卷三中这样描述 maxfdp1:maxfdpl参数指定待测试的描述符个数,它的值是待测试的最大描述符加1(因此我们把该参数命名为maxfdp1),描述符01,2.··一直到maxfdp1-1均将被测试。
linux man文档这样描述 maxfdp1: According to POSIX, select() should check all specified file descriptors in the three file descriptor sets, up to the limit nfds-1. However, the current implementation ignores any file descriptor in these sets that is greater than the maximum file descriptor number that the process currently has open.(PS:个人觉得这个描述应该更加准确,因为按上面的 卷三 当中的描述需要便利的 fd 也太多了)
fd_set 处理
select 使用 fd_set 来管理我们需要处理的 socket
-
void FD_ZERO(fd_set *fdset); // fdset 清除为0
-
void FD_SET(int fd, fd_set *fdset); // 把 socket 加入到 fdset
-
void FD_ISSET(int fd, fd_set *fdset); // 对应的 socket 是否准备就绪
-
void FD_CLR(int fd, fd_set *fdset); // 清除 fdset 准备就绪的标记
描述符就绪的条件
- 满足下列四个条件中的任何一个时,一个套接字准备好读。
1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻寒塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。
2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
3. 该套接字是一·个监听套接字且已完成的连接数不为0。对这样的套接字的accept通常不会阻塞,不过我们将在15.6节讲解accept可能阻塞的一种时序条件。
4. 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误 (pending error)也可以通过指定SO ERROR套接字选项调用getsockopt获取并清除。
- 下列四个条件中的任何一个满足时,一个套接字准备好写。
1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接)。这意味着如果我们把这样的套接字设置成非阻塞写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。
2. 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。
3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
4. 其上有一个套接字错误待处。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
-
如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
select 代码示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/select.h>
#define SERV_PORT 8989
int main(int argc, const char* argv[])
{
int lfd, cfd;
struct sockaddr_in serv_addr, clien_addr;
int serv_len, clien_len;
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(SERV_PORT); // 设置端口
serv_len = sizeof(serv_addr);
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
int ret;
int maxfd = lfd;
// reads 实时更新,temps 内核检测
fd_set reads, temps;
/*===============================================================*/
// 记录要检测的文件描述符的数组
int allfd[FD_SETSIZE]; // 1024
// 记录数组中最后一个元素的下标
int last_index = 0;
// 初始化数组
for(int i=0; i<FD_SETSIZE; ++i)
{
allfd[i] = -1; // 无效文件描述符值
}
allfd[0] = lfd; // 监听的文件描述符添加到数组中
/*===============================================================*/
// 初始化监听的读集合
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
// 每次都需要更新,否则select不会重新检测
temps = reads;
ret = select(maxfd+1, &temps, NULL, NULL, NULL);
if(ret == -1)
{
perror("select error");
exit(1);
}
int i = 0;
char bufip[64];
// 判断是否有新连接
if(FD_ISSET(lfd, &temps))
{
// 接受连接请求
clien_len = sizeof(clien_len);
int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
printf("client ip: %s, port: %d\n",
inet_ntop(AF_INET, &clien_addr.sin_addr.s_addr, bufip, sizeof(bufip)),
ntohs(clien_addr.sin_port));
// 文件描述符放入检测集合
FD_SET(cfd, &reads);
// 更新最大文件描述符
maxfd = maxfd < cfd ? cfd : maxfd;
// cfd添加到检测数组中
for(i=0; i<FD_SETSIZE; ++i)
{
if(allfd[i] == -1)
{
allfd[i] = cfd;
break;
}
}
// 更新数组最后一个有效值下标
last_index = last_index < i ? i : last_index;
}
// 遍历检测的文件描述符是否有读操作
for(i=lfd+1; i<=maxfd; ++i)
{
if(FD_ISSET(i, &temps))
{
// 读数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1)
{
perror("read error");
exit(1);
}
else if(len == 0)
{
// 对方关闭了连接
FD_CLR(i, &reads);
close(i);
if(maxfd == i)
{
maxfd--;
}
allfd[i] = -1;
printf("对方已经关闭了连接。。。。。。\n");
}
else
{
printf("read buf = %s\n", buf);
for(int j=0; j<len; ++j)
{
buf[j] = toupper(buf[j]);
}
printf("--buf toupper: %s\n", buf);
write(i, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}