一、select函数
1. 用途
在编程的过程中,经常会遇到许多阻塞的函数,好像read和网络编程时使用的recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这是就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。
select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
2.概念理解
在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)
四种调用模式:
同步:所谓同步,就是在发出一个功能调用时,在没有得到结果前,该调用就不返回。也就是必须一件
一件做事,等前一件做完了才能做另一件。
例如在C/S模式的某个流程中,你服务器提交了某个请求,在服务器处理完毕返回结果期间客户端什么
也不能做。
异步:异步概念和同步相对。当一个异步过程调用发出后,调用者不会立刻得到结果。调用者在发出
调用后可以继续做自己的事,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。
阻塞:阻塞调用是指调用结果返回前,当前线程会被挂起(当前线程处于非可执行状态,在这个状态下,
CPU不会给线程分配时间片,即线程暂停运行),函数只有在得到结果后才回返回。
非阻塞:非阻塞和阻塞的概念相对,是指不能立刻得到借过前,该函数不会阻塞当前进程,而回立刻返回。
区别:有人会把同步和阻塞调用等同起来,实际上他们是不同的,对于同步调用来说,很多时候当前调用
还是激活的,只是从逻辑上当前函数没有返回而已。阻塞的话当前线程会被挂起
3. select函数大致原理
select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。
4. 函数定义
该函数声明如下:
select模型中需要一个结构体fd_set,该结构体是一个socket的集合,我们可以看到该结构体的定义:
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
从这个定义中可以看到,结构体中主要保存了一个socket的数组和一个保存数组的大小变量;
使用select模型主要使用函数select,该函数原型如下:
int select (
int nfds, //系统保留,无意义
fd_set FAR * readfds,//可读的socket集合
fd_set FAR * writefds,//可写的socket集合
fd_set FAR * exceptfds,//外带socket集合
const struct timeval FAR * timeout//该函数的超时值
);
返回值:
返回fd的总数,错误时返回SOCKET_ERROR
二、 fd_set结构体
fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
系统提供了FD_SET, FD_CLR, FD_ISSET, FD_ZERO进行操作,声明如下:
FD_SET(int fd, fd_set *fdset); //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何f
实例代码:
std::vector<SOCKET> g_clients;//声明SOCKET类型容器,用来存储客户端_Csocket
int main()
{
//启动Windows socket 2.x环境
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
WSAStartup(ver, &dat);
//------------
//-- 用Socket API建立简易TCP服务端
// 1 建立一个socket 套接字
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2 bind 绑定用于接受客户端连接的网络端口
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567);//host to net unsigned short
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1");
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
printf("错误,绑定网络端口失败...\n");
}
else {
printf("绑定网络端口成功...\n");
}
// 3 listen 监听网络端口
if (SOCKET_ERROR == listen(_sock, 5))
{
printf("错误,监听网络端口失败...\n");
}
else {
printf("监听网络端口成功...\n");
}
while (true)
{
//伯克利 socket
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
for (int n = (int)g_clients.size()-1; n >= 0 ; n--)
{
FD_SET(g_clients[n], &fdRead);//设置整个容器都是可读的
}
///nfds 是一个整数值 是指fd_set集合中所有描述符(socket)的范围,而不是数量
///既是所有文件描述符最大值+1 在Windows中这个参数可以写0
timeval t = {0,0};
int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExp, &t);//如果t为NULL时是阻塞模式,如果是0时即可返回,可以设置时间
if (ret < 0)
{
printf("select任务结束。\n");
break;
}
if (FD_ISSET(_sock, &fdRead))//判断创建的_sock是否在fdRead队列中
{
FD_CLR(_sock, &fdRead);//从fdRead中清除_sock
// 4 accept 等待接受客户端连接
sockaddr_in clientAddr = {};
int nAddrLen = sizeof(sockaddr_in);
SOCKET _cSock = INVALID_SOCKET;
_cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == _cSock)
{
printf("错误,接受到无效客户端SOCKET...\n");
}
g_clients.push_back(_cSock);//从尾部压入容器中
printf("新客户端加入:socket = %d,IP = %s \n", (int)_cSock, inet_ntoa(clientAddr.sin_addr));
}
//如果客户端退出,从g_clients容器中查找要退出的客户端(find()),并删除(erase(iter))
for (size_t n = 0; n < fdRead.fd_count; n++)
{
if (-1 == processor(fdRead.fd_array[n]))
{
auto iter = find(g_clients.begin(), g_clients.end(), fdRead.fd_array[n]);
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
//关闭g_clients所有的socket
for (size_t n = g_clients.size() - 1; n >= 0; n--)
{
closesocket(g_clients[n]);
}
// 8 关闭套节字closesocket
closesocket(_sock);