网络基础 select 模型
(一)select简介
select模型为五种IO模型中的一种(I/O多路复用模型)。该模型的函数包括select、poll、epoll等函数。这个函数能够允许指示内核等待多个事件中的任意一个发生,并且仅仅在一个或者多个事件发生(或者经过指定的时间后)后才唤醒进程。select模型也是同步I/O模型,都需要在读写事件就绪后(此过程select函数阻塞),用户自己负责将数据从内核拷贝到用户进程空间(通过recv系统调用)。关于I/O模型的同步异步问题,前一篇总结已作描述。(二)select优缺点
select优点:(1) 它能够同时监视多个文件描述符(以下简称fd),当select返回后,数组中就绪的fd中标志位会被修改,用户进程需要判断哪个fd的标志位修改了,从而进行后续的读写操作。
(2) select良好的跨平台性,几乎支持所有的平台。
select缺点:
(1) 单个进程所监视的最大数量的文件描述符为1024个(采用32个整数的32位标识),在linux下可以修改宏定义修改这个限制。
(2) 每调用一次select,都需要遍历一次所有fd。随着fd的增加,调用开销将线性增加。且在调用select的时候,都需要将fd集合从用户态拷贝到内核态,这个过程也是随着fd,调用开销也是线性增长的。
(三)select函数
select函数原型如下:int select( int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR *exceptfds, const struct timeval FAR * timeout );
其中,select包含了三个socket队列,分别为:readfds 检查可读性; writefds 检查可写性;exceptfds 检查异常。 timeout是select函数 返回的时间。
其中,fd_set为一个结构体:
typedef struct fd_set
{
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
fd_set即为描述字集合,它是一个整数数组,每个数的每一位对应一个描述字。如果FD_SETSIZE为32,则数组中第一个整数元素对应描述字0~31,数组中第二个整数元素对应描述字32~63,以此类推。
其中,timeout告诉内核等待一组fd中任一个准备好需要花多少时间,结构timeval指定了秒级和微妙级。
struct timeval{
long tv_sec;
long tv_usec;
}
对于时间的用法,有三种可能(这三种可能同样适用于其他事件等待机制的系统调用函数,pthread_cond_timedwait, WaitForSingleObject 等)
1. 永远等待下去,timeout设为NULL。仅有一个fd准备好I/O时才返回;2. 等待固定事件,指定timeout中的秒数和微妙数。
3. 根本不等待,timeout设为0。检查fd后立即返回,这成为轮询。
举例来说,如果需要监听一个fd是否有数据需要接收,则开始可以将该fd加入readfds中,然后调用select。此时该线程会阻塞在select处,直到有数据到来或者等待超时(由timeout参数决定)。
当select在监听多个fd的读事件时,有数据到来,那如何判断是哪一个fd对应的数据? 当有数据到来时,select会将readfds中没有数据需要接受的fd从可读性检查队列中删除掉,所以只要检测每个fd是否还存在于readfds中,就能知道到底有没有需要接收的数据了。
对于fd_set操作的宏,表示如下:
FD_CLR( s,*set) 从队列set删除描述符s。
FD_ISSET( s, *set) 检查描述符s是否存在与队列set中。
FD_SET( s,*set )把描述符s添加到队列set中。
FD_ZERO( *set ) 把set队列初始化成空队列。
(四)基础服务器模型
一种较为容易的服务器模型就是,一个线程负责监听客户端的连接请求。当收到客户端的连接请求后,再创建另一个工作线程,用于处理服务器与客户端的交互信息。该方法能够适用连接数目不多的情况,一旦连接数量增加,则相应线程的开销也会增加(频繁创建、销毁线程,线程间的频繁切换等)。而且大部分时候,线程都是处于非活动状态的,这也在无端消耗着CPU。 大体流程如下图所示:
#include <winsock.h>
#include <stdio.h>
#define PORT 5150
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib")
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];
DWORD WINAPI WorkerThread(LPVOID lpParameter);
int main()
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
int iaddrSize = sizeof(SOCKADDR_IN);
DWORD dwThreadId;
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
// Create worker thread
HANDLE hHandle = CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
CloseHandle(hHandle);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Add socket to fdTotal
g_CliSocketArr[g_iTotalConn++] = sClient;
}
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++)
{
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++)
{
if (FD_ISSET(g_CliSocketArr[i], &fdread))
{
// A read event happened on pfdTotal->fd_array[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.\n", g_CliSocketArr[i]);
closesocket(g_CliSocketArr[i]);
if (i < g_iTotalConn - 1)
{
g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
}
}
else
{ // We received a message from client
szMessage[ret] = '\0';
send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
}
}
}
}
return 0;
}
当然,不仅仅是上面一种服务器的构建方式,还有其他的方式,比如开启一个守护线程不断监测所有套接字的读事件(不论是服务器的侦听套接字,还是与服务器建立的连接套接字),并分别做不同处理(对于侦听套接字有读数据,表示有新的连接到来;对于连接套接字有读数据,表示连接上有数据交互了)。该方式的特点是线程个数固定(就一个服务器处理线程处理跟所有客户端的信令交互),但是这同样也是缺点,对于连接数很多的情况,单个线程处理所有客户端的请求显然无法满足实时性要求较高的环境。该服务器设计的大体框架如下图所示:
参考阅读:
IO多路复用之select总结: http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
select函数实现原理分析: http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml
winsock IO模型 select模型: http://www.cnblogs.com/azraelly/archive/2012/08/11/2633685.html
连接套接字管理模块主要作用:
1. 管理连接后的客户端(主要对客户端连接套接字进行管理),以便与各个客户端通信;
2. 对TCP消息进行封装,TCP为流式套接字,可以通过该管理模块,使得客户端与服务器处理线程的消息一致(收发的消息内容、消息长度相同)。
参考阅读:
IO多路复用之select总结: http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
select函数实现原理分析: http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml
winsock IO模型 select模型: http://www.cnblogs.com/azraelly/archive/2012/08/11/2633685.html