引言:最近同时接触了Windows与Linux下的select模型,原以为两者相同,其实并非如此。以下便是对Windows与Linux下的select模型进行的比较分析,以供大家学习,共同进步,不足之处请留言。
Windows下select模型
1.Windows下的 fd_set 原型:
typedef struct fd_set {
u_int fd_count; //记录连接的套接字的个数
SOCKET fd_array[FD_SETSIZE]; // 存储连接的套接字的数组
} fd_set;
功能:保存连接的套接字句柄的结构体
2.Windows下的Select函数原型:
int select(
int nfds, // 可忽略,无意义,填0
fd_set FAR* readfds, // 检查可读性
fd_set FAR* writefds, // 检查可写性
fd_set FAR* exceptfds, // (可选指针)要检查错误的一组套接字
const struct timeval FAR* timeout // 等待时间,使用结构体timeval
);
MSDN:This function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.
功能:轮询检查的套接字信号的到来,将有可读信号的套接字保存在readfds参数中,将有可写信号的套接字保存在writefds参数中。
3.理解 Windows下select实现原理:
select实现的伪代码
DWORD WINAPI SelectModel::ServiceProc(LPARAM lparam)
{
..
fd_set fdRead;
while (true)
{
FD_ZERO(&fdRead);//清空
fdRead = g_fdClientSock;
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 100;
nRet = select(0, &fdRead, 0, 0, &tv);//select
if (nRet == 0)//超时
continue;
else if(nRet != SOCKET_ERROR)
{
for (int i = 0; i < fdRead.fd_count; i++)//for取出有信号的套接字
{
... ...
}
}
}
return 0;
}
假设有5个客户端连接,假设它们socket值分别为101、102、103、104、105(并非是linux下的位)。此时
g_fdClientSock.fd_count=5,
g_fdClientSock.fd_array[0]=101
g_fdClientSock.fd_array[1]=102
g_fdClientSock.fd_array[2]=103
g_fdClientSock.fd_array[3]=104
g_fdClientSock.fd_array[4]=105
(1) fdRead = g_fdClientSock;我们将g_fdClientSock赋值给临时变量fdRead,将fdRead作为select检查后存储有数据信号的套接字的结果集。
(2) nRet = select(0, &fdRead, 0, 0, &tv);此时有套接字102、103在超时时间内(timeval)并发到来,返回值nRet此时为2,fdRead的fd_count为2,它的
fd_array[0]=102
fd_array[1]=103
fd_array[2]=103
fd_array[3]=104
fd_array[4]=105
不妨看出,Windows下的select实现就是轮询一遍我们要检查的fdRead,然后将有信号的套接字按照数组内的排序顺序依次放在fdRead的前面,我们再使用的时候只需要根据nRet(有数据到来的信号数量)与fdRead检测到谁有信号到来。
如果实现原理还有不怎么明了的,这里再啰嗦举例两个:
- 此时101、104有信号到来,返回值nRet=2,fdRead的fd_count为2,fdRead的
fd_array[0]=101
fd_array[1]=104
fd_array[2]=103
fd_array[3]=104
fd_array[4]=105 - 此时103、104、105有信号到来,返回值nRet=3,fdRead的fd_count为3,fdRead的
fd_array[0]=103
fd_array[1]=104
fd_array[2]=105
fd_array[3]=104
fd_array[4]=105
(3)数据的接收,我们只需要使用一次for循环就能取出来
for (int i = 0; i < fdRead.fd_count; i++)
{
... ...
}
4.select模型的局限与优化
1) FD_SETSIZE的值为64,通常认为select的局限为64,既然为通常,那么我们就会有相关的优化方法
2) 普遍认为的优化是使用自定义的fd_set结构体,但是select函数一次只能轮询64个,如果你定义FD_SETSIZE为640的话,可以加一个循环,循环十次分别使用select检测数据信号的到来,此处也能多加线程,但是要注意线程同步的问题。
3) 少见高效率的优化方法,为libevent库中讲到的动态fd_array的方法,能够达到的效果为万级别,它新定义的fd_set结构体为:
struct win_fd_set {
u_int fd_count;
SOCKET fd_array[1];
};
此处fd_array的大小为1,此后使用的是动态申请内存的方法,使得fd_array动态变化:
win_fd_set * Set = (win_fd_set*)malloc(sizeof(win_fd_set) + sizoef(SCOEKT) * n);
//n为1时,fd_array的大小为2,n为100时,fd_array的大小为1001
至于具体的实现方法,大家可以下载libevent看看里面的源码
5.相关宏的实现
#define FD_SET(fd, set) do { \
u_int __i; \
for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
break; \
} \
} \
if (__i == ((fd_set FAR *)(set))->fd_count) { \
if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
((fd_set FAR *)(set))->fd_array[__i] = (fd); \
((fd_set FAR *)(set))->fd_count++; \
} \
} \
} while(0, 0)//
#define FD_CLR(fd, set) do { \
u_int __i; \
for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
((fd_set FAR *)(set))->fd_array[__i] = \
((fd_set FAR *)(set))->fd_array[__i+1]; \
__i++; \
} \
((fd_set FAR *)(set))->fd_count--; \
break; \
} \
} \
} while(0, 0)
#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
FD_CLR(fd, set) //从fd_set中清除一个套接字描述符(句柄)
FD_ZERO(set) //清空fd_set结构体
FD_ISSET(fd, set) //判断某个套接字描述符是否有信号,内部调用的是__WSAFDIsSet,也可以用程序中直接循环fdread的方法,此时就不用调用FD_ISSET实现
FD_SET(fd, set) //将套接字描述符添加到fd_set结构体中
Linux下的select模型
1.Linux下的fd_set原型:
#define __NFDBITS (8 * sizeof(unsigned long)) //每个ulong型可以表示的bit数,8*4=32
#define __FD_SETSIZE 1024 //socket最大取值为1024
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS) //1024个bit,共需要多少个ulong?1024/32=32
typedef struct {
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;
/ /用ulong数组的bit位来存储套接字信息
0000 0000 0000 0000 … 0000 0000 0000 0000
… 16行,16列
…
0000 0000 0000 0000 … 0000 0000 0000 0000
typedef __kernel_fd_set fd_set;
从宏__FD_SETSIZE看出,Linux下的select模型的上限为1024。比Windows的select更强大。当然我们也可以多线程,也可以重定义__FD_SETSIZE来改变容量的大小,但是还是得根据你的服务器配置来。
2.Linux下的Select函数原型:
这里的第一个参数与Windows下的不同,第一参数表示select检查的最大连接数
int select(
int nfds, // 监控的套接字最大值+1
fd_set *readfds, // 集合中任意描述字准备好读,则返回
fd_set *writefds, // 集合中任意描述字准备好写,则返回
fd_set *exceptfds, // 集合中任意描述字有异常等待处理,则返回
struct timeval *timeout); // 超时则返回(NULL 则一直等待,0 则立即返回)
返回值:返回值 =0 超时, 返回值<0 错误,返回值>0正常
3.理解Linux下select的实现原理
引自:http://blog.csdn.net/qdx411324962/article/details/42499535
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
4.Linux下select的局限
(1)每次调用 select 都需要把fd集合从用户态拷贝到内核态,fd很多时开销很大
(2)调用 select 需要内核遍历 fd, fd 很多时开销很大
(3)select 支持文件描述符监视有数量限制,默认 1024
5.相关宏的实现
#define __FD_SET(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] |= (1<<((fd) & 31)))
#define __FD_CLR(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] &= ~(1<<((fd) & 31)))
#define __FD_ISSET(fd, fdsetp) ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] & (1<<((fd) & 31))) != 0)
#define __FD_ZERO(fdsetp) (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp))))
FD_SET:设置对应的bit为1(增加fd 到集合中)
FD_CLR:清除对应的bit位(从集合中清除fd)
FD_ISSET:判断对应的bit是否为1(描述字是否准备好)
FD_ZERO:清空所有的bit位(清空描述符集合)
模型对比解析select的优势
同步阻塞的I/O模型图
指明一点,在阻塞的通信中,recv并不是直接就获取到数据,进行recv之后,开始的是等待有数据到来的信号,此时内核开始接收数据,结束数据完毕后,发一个数据到来的信号出去,recv接收到这个信号后,就去内核拷贝数据到用户空间(程序缓冲区),所以注意了recv做的只是从内核的缓冲区拷贝到程序的缓冲区的事情!
通过了解之后,我们知道recv阻塞的时间是浪费在接收数据+将数据从内核拷贝到用户空间(程序的缓冲区)中的。这里可以复习下recv与send数据的原理
I/O复用模型(select)
select模型究竟相对于同步阻塞模型解决了什么问题?
解决的是“等待数据信号到来的时间”,将等待数据信号到来的时间交给select同时检测多个套接字的多个据到来的消息(通过检测内核中是否有已注册的I/O事件有数据的到来,如果有数据的到来就返回告诉主程序有注册的I/O事件的数据到来),然后recv就直接开始去内核中拷贝数据到程序缓冲区中
select的缺点
1)虽然select能够同时检测多个I/O事件,但是我们注意到select的第五个参数,是轮询检测数据到来的时间,微观来说,在select的这段时间里面是阻塞的,而且它占用的是程序自生的时间片,此时就会浪费cpu资源。
2)前面也有讲到Windows下FD_SETSIZE与Linux下__FD_SETSIZE的限制问题。Linux可以通过sizeof(fd_set)获取select模型的上限
3)select并没有解决数据从内核缓冲区拷贝到程序缓冲区的时间问题,它得到的仅是数据到达内核后的信号,拷贝数据还是得通过recv,而recv占用的也是程序自身的时间片
Windows下select模型的实现
实现流程图
Accept部分代码
BOOL SelectModel::Accept()
{
SOCKADDR_IN clientAddr;
int nLen = sizeof(SOCKADDR_IN);
stClientInfo clientInfo;
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ServiceProc, (LPVOID)this, NULL,NULL);
while (true)
{
if (g_fdClientSock.fd_count >= FD_SETSIZE)
{
Sleep(50);
continue;
}
memset(&clientInfo, 0, sizeof(stClientInfo));
memset(&clientAddr, 0, sizeof(SOCKADDR_IN));
SOCKET clientSock = accept(m_hSocket, (sockaddr*)&clientAddr, &nLen);
if (clientSock == INVALID_SOCKET)
continue;
strcpy(clientInfo.sIP, inet_ntoa(clientAddr.sin_addr));
clientInfo.nPort = htons(clientAddr.sin_port);
g_mapClientInfo[clientSock] = clientInfo;
FD_SET(clientSock, &g_fdClientSock);
}
}
通过一个while循环接受(accept)客户端的连接,将接收到的客户端信息添加到fd_set结构体g_fdClientSock中
ServiceProc线程函数
DWORD WINAPI SelectModel::ServiceProc(LPARAM lparam)
{
SelectModel* pSelect = (SelectModel*)lparam;
fd_set fdRead;
char* recvBuffer = (char*)malloc(sizeof(char) * 1024);
if (recvBuffer == NULL)
return -1;
int nRet = 0;
while (true)
{
FD_ZERO(&fdRead);
fdRead = g_fdClientSock;
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 100;
nRet = select(0, &fdRead, 0, 0, &tv);
if (nRet == 0)//超时
continue;
else if(nRet != SOCKET_ERROR)
{
for (int i = 0; i < fdRead.fd_count; i++)
{
memset(recvBuffer, 0, sizeof(char) * 1024);
nRet = recv(fdRead.fd_array[i], recvBuffer, sizeof(char) * 1024, 0);
if (nRet == SOCKET_ERROR || nRet == 0)
{
pSelect->EraseClientInfo(g_fdClientSock.fd_array[i]);
closesocket(g_fdClientSock.fd_array[i]);
FD_CLR(g_fdClientSock.fd_array[i], &g_fdClientSock);
}
else
{
pSelect->NetFunc(fdRead.fd_array[i], (void*)recvBuffer, strlen(recvBuffer));
}
}
}
}
return 0;
}
通过select检测fdRead中的套接字是否有信号到来,如果有信号到来,通过for循环一次取出相应的套接字,通过recv接受有信号的套接字的数据,接收完数据后,通过消息处理函数NetFunc进行消息的处理。如果客户端断开连接,就关闭套接字(closesocket),然后FD_CLR掉g_fdClientSock中对应的套接字描述符的信息
避免文章过长,linux下源码自行下载查阅,原理和Windows一样:http://pan.baidu.com/s/1pKB9KZh
自己封装的Windows下select模型源码:http://pan.baidu.com/s/1bp74ArP