1、API详解
(1)int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。
参数说明:
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
d_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
(2)fd_set类型通过下面四个宏来操作:
fd_set set;
FD_ZERO(&set); /*将set清零,使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd在fdset集合中的状态是否变化返回整型,当检测到fd状态发生变化时返回真,否则,返回假(0)*/
(3)理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(a)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(b)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(c)若再加入fd=2,fd=1,则set变为0001,0011
(d)执行select(6,&set,0,0,0)阻塞等待
(e)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
(4)基于上面的讨论,可以轻松得出select模型的特点:
(a)可监控的文件描述符个数取决与sizeof(fd_set)的值。
(b)可以有效突破select可监控的文件描述符上限。
(c)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。
(d)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
2、源码
#include <stdio.h>
#include <winsock.h> //winsock.h (2种套接字版本)
#include <process.h> //for beginthread
#pragma comment(lib,"ws2_32.lib") //wsock32.lib
#define PORT 5150
#define MSGSIZE 1024
#define MAX_LISTEN_NUM 5 //最多可同时监听的客户连接数
#define CONNECT_PORT 1027
#define CONNECT_IP "127.0.0.1"
#define DEFAULT_BUFF_SIZE 512
#pragma comment(lib, "ws2_32.lib")
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];
DWORD WINAPI WorkerThread(LPVOID lpParameter);
int main()
{
WSAData wsdata;
SOCKET sockServer;
SOCKET sockRecvClient;
SOCKADDR_IN addrServer;
SOCKADDR_IN addrRecvClient;
int addrlenClient;
HANDLE hThread; //处理客户连接用的线程Handle
DWORD nThreadID;
int nError = -1;
printf(">>>>>>>>server start Startup<<<<<<<<\n");
//初始化一个服务进程
nError = WSAStartup( MAKEWORD(2,2), &wsdata);
if( nError != 0 )
{
printf("WSAStartup Failed: %d\n",nError);
return -1;
}
//设定本地地址信息
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(CONNECT_PORT);
addrServer.sin_addr.S_un.S_addr = inet_addr(CONNECT_IP);
// Create listening socket
sockServer = socket( AF_INET, SOCK_STREAM, 0 );
// Bind
nError = bind(sockServer, (const sockaddr *)(&addrServer), sizeof(addrServer) );
nError = listen(sockServer, MAX_LISTEN_NUM );
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &nThreadID);
while (TRUE)
{ // Accept a connection
addrlenClient = sizeof( addrRecvClient );
sockRecvClient = accept( sockServer, (sockaddr *)(&addrRecvClient), &addrlenClient );
if( sockRecvClient == INVALID_SOCKET )
{
printf("accept Failed: %d\n",WSAGetLastError());
break; //jump while
}
printf("A accep new client,socketRecvClient = %d\n",sockRecvClient);
// Add socket to g_CliSocketArr
g_CliSocketArr[g_iTotalConn] = sockRecvClient;
g_iTotalConn++;
printf("g_iTotalConn = %d\n",g_iTotalConn);
}
return 0;
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int i;
fd_set fdread;
int ret;
struct timeval tv = {1, 0};
char szMessage[MSGSIZE];
while (TRUE)
{
FD_ZERO(&fdread);
for (i = 0; i < g_iTotalConn; i++)
{
if (0 != g_CliSocketArr[i])//表示socket已经关闭嘞
{
FD_SET(g_CliSocketArr[i], &fdread);
}
}
// We only care read event
ret = select(0, &fdread, NULL, NULL, &tv);
if (ret == 0)
{ // Time expired
continue;
}
for (i = 0; i < g_iTotalConn; i++)
{
//select函数成功返回时会将未准备好的描述符位清零。
//使用FD_ISSET是为了检查在select函数返回后,某个描述符是否准备好,以便进行接下来的处理操作。
//当描述符fd在描述符集fdset中,返回非零值,否则,返回零。
if (g_CliSocketArr[i] == 0) //表示socket已经关闭嘞
{
continue;
}
if (FD_ISSET(g_CliSocketArr[i], &fdread))
{
// A read event happened on g_CliSocketArr
printf("A read event happened on g_CliSocketArr g_CliSocketArr[i] = %d\n",g_CliSocketArr[i]);
ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Client socket %d closed.,ret = %d\n", ret,g_CliSocketArr[i]);
closesocket(g_CliSocketArr[i]);
FD_CLR(g_CliSocketArr[i],&fdread); //将已经关闭的SOCKET从FD集中删除
g_CliSocketArr[i] = 0;
}
else
{
// We received a message from client
szMessage[ret] = '\0';
/// send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
}
} //if
}//for
}//while
return 0;
}
(1)为什么会出现select模型?
先看一下下面的这句代码:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返 回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过 你跟踪 一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。 看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型的出现就是为了解决上述问题。
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。
(2)Select优势
能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
不用为每一个连接开一个线程。避免线程间过多的切换。
(3)Select不足:
select所用到的FD_SET是有限的,不能同时监听更多。Windows下看的是64,Linux 2.6.15-25-386内核中,该值是1024。
实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。