socket基础函数(2)

时间:2023-03-09 18:31:28
socket基础函数(2)
http://www.cnblogs.com/RascallySnake/archive/2013/07/11/3185071.html
一、select 

winsock中 #include <winsock.h>

原型

int   select( 
int   nfds,
fd_set*   readfds,
fd_set*   writefds,
fd_set*   exceptfds,
const struct timeval*   timeout
);

nfds:本参数忽略,仅起到兼容作用。
    readfds:(可选)指针,指向一组等待可读性检查的套接口。
    writefds:(可选)指针,指向一组等待可写性检查的套接口。
    exceptfds:(可选)指针,指向一组等待错误检查的套接口。
    timeout:select()最多等待时间,对阻塞操作则为NULL。

注释:
    本函数用于确定一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。

用fd_set结构来表示一组等待检查的套接口。 在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。

有一组宏可用于对fd_set的操作,这些宏与Berkeley Unix软件中的兼容,但内部的表达是完全不同的。

参数:

readfds:标识等待可读性检查的套接口。如果该套接口正处于监听listen()状态,则若有连接请求到达,该套接口便被标识为可读,这样一个accept()调用保证可以无阻塞完成。

对其他套接口而言,可读性意味着有排队数据供读取。

或者对于SOCK_STREAM类型套接口来说,相对于该套接口的虚套接口已关闭,于是recv()或recvfrom()操作均能无阻塞完成。

如果虚电路被“优雅地”中止,则recv()不读取数据立即返回;如果虚电路被强制复位,则recv()将以WSAECONNRESET错误立即返回。如果SO_OOBINLINE选项被设置,则将检查带外数据是否存在(参见setsockopt())。
    writefds:标识等待可写性检查的套接口。如果一个套接口正在connect()连接(非阻塞),可写性意味着连接顺利建立。如果套接口并未处于connect()调用中,可写性意味着send()和sendto()调用将无阻塞完成。〔但并未指出这个保证在多长时间内有效,特别是在多线程环境中〕。
    exceptfds:标识等待带外数据存在性或意味错误条件检查的套接口。请注意如果设置了SO_OOBINLINE选项为假FALSE,则只能用这种方法来检查带外数据的存在与否。对于SO_STREAM类型套接口,远端造成的连接中止和KEEPALIVE错误都将被作为意味出错。如果套接口正在进行连接connect()(非阻塞方式),则连接试图的失败将会表现在exceptfds参数中。
    如果对readfds、writefds或exceptfds中任一个组类不感兴趣,可将它置为空NULL。
    在winsock.h头文件*定义了四个宏来操作描述字集。FD_SETSIZE变量用于确定一个集合中最多有多少描述字(FD_SETSIZE缺省值为64,可在包含winsock.h前用#define FD_SETSIZE来改变该值)。对于内部表示,fd_set被表示成一个套接口的队列,最后一个有效元素的后续元素为INVAL_SOCKET。宏为:
    FD_CLR(s,*set):从集合set中删除描述字s。
    FD_ISSET(s,*set):若s为集合中一员,非零;否则为零。
    FD_SET(s,*set):向集合添加描述字s。
    FD_ZERO(*set):将set初始化为空集NULL。
    timeout: 控制select()完成的时间。若timeout参数为空指针,则select()将一直阻塞到有一个描述字满足条件。否则的话,timeout指向一个timeval结构,其中指定了select()调用在返回前等待多长时间。如果timeval为{0,0},则select()立即返回,这可用于探询所选套接口的状态。如果处于这种状态,则select()调用可认为是非阻塞的,且一切适用于非阻塞调用的假设都适用于它。举例来说,阻塞钩子函数不应被调用,且WINDOWS套接口实现不应yield。

设置了timeout的值之后呢,select在没有文件描述符监视可用的情况下,会等待这个timeout的时间,时间到了select返回0

如果timeout超时之前有文件描述符可用,则返回可用的数量,这时候的timeout则会依然计数,因此如果想要每次都超时一定的时间那么在slelect返回>0的值之后要重新装填timeout的值一次。以保证超时时间没有变化。

如果tv_sec和tv_usec都是0,那么就是超时时间为0,那么select就会立刻返回了。

如果timeout这里是个NULL,那么超时就未被启用,会一直阻塞在监视文件描述符的地方。

struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

&timeout=NULL是传进一个空指针,表示永远等待 
timeout=0是传进一个timeout,其值为0,系统检查所有的fdset,然后立即返回 
你传进一个0然后不断的查询,就和传进一个NULL效果一样了 
不过就是巨耗cpu

返回值:
    select()调用返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;如果超时则返回0;否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

错误代码:
    WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
    WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
    WSAEINVAL:超时时间值非法。
    WSAEINTR:通过一个WSACancelBlockingCall()来取消一个(阻塞的)调用。
    WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
    WSAENOTSOCK:描述字集合中包含有非套接口的元素。

select()函数主要是建立在fd_set类型的基础上的。fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个宏来操作:

fd_set set;

FD_ZERO(&set);

FD_SET(fd, &set);

FD_CLR(fd, &set);

FD_ISSET(fd, &set);

过去,一个fd_set通常只能包含<32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd;现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能表示<1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏:

fd_set set;

FD_ZERO(&set);

FD_SET(0, &set);

FD_CLR(4, &set);

FD_ISSET(5, &set);

―――――――――――――――――――――――――――――――――――――――

注意fd的最大值必须<FD_SETSIZE。

―――――――――――――――――――――――――――――――――――――――

select函数的接口比较简单:

int select(int nfds, fd_set *readset, fd_set *writeset,

fd_set* exceptset, struct tim *timeout);

功能:

测试指定的fd可读?可写?有异常条件待处理?

参数:

nfds: 需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd

值更大,一般设为三组fd_set中所含的最大fd值加1(如在readset,writeset,exceptset中所含最大

的fd为5,则nfds=6,因为fd是从0开始的)。设这个值是为提高效率,使函数不必检查fd_set的所

有1024位。

readset:  用来检查可读性的一组文件描述字。

writeset: 用来检查可写性的一组文件描述字。

exceptset:用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)

timeout:有三种可能:

1:timeout=NULL(阻塞:直到有一个fd位被置为1函数才返回)

2:timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数

均返回)

3. timeout所指向的结构,时间设为0(非阻塞:函数检查完每个fd后立即返回)

返回值:

返回对应位仍然为1的fd的总数。

Remarks:

三组fd_set均将某些fd位置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。

使用select函数的过程一般是:

先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数

select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。

以下是一个测试单个文件描述字可读性的例子:

int isready(int fd)

{

int rc;

fd_set fds;

struct tim tv;

FD_ZERO(&fds);

FD_SET(fd,&fds);

tv.tv_sec = tv.tv_usec = 0;

rc = select(fd+1, &fds, NULL, NULL, &tv);

if (rc < 0)   //error

return -1;

return FD_ISSET(fd,&fds) ? 1 : 0;

}

下面还有一个复杂一些的应用:

//这段代码将指定测试Socket的描述字的可读可写性,因为Socket使用的也是fd

uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems)

{

fd_set rfds,wfds;

#ifdef _WIN32

TIM tv;

#else

struct tim tv;

#endif

FD_ZERO(&rfds);

FD_ZERO(&wfds);

if (rd)   //TRUE

FD_SET(*s,&rfds);   //添加要测试的描述字

if (wr)     //FALSE

FD_SET(*s,&wfds);

tv.tv_sec=timems/1000;     //second

tv.tv_usec=timems00;     //ms

for (;;) //如果errno==EINTR,反复测试缓冲区的可读性

switch(select((*s)+1,&rfds,&wfds,NULL,

(timems==TIME_INFINITE?NULL:&tv)))  //测试在规定的时间内套接口接收缓冲区中是否有数据可读

{                                              //0--超时,-1--出错

case 0:

return 0;

case (-1):

if (SocketError()==EINTR)

break;

return 0; //有错但不是EINTR

default:

if (FD_ISSET(*s,&rfds)) //如果s是fds中的一员返回非0,否则返回0

return 1;

if (FD_ISSET(*s,&wfds))

return 2;

return 0;

};

}

Q&A:

和select模型紧密结合的四个宏:

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

理解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被清空

基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。对调整fd_set的大小可参考http://www.cppblog.com/CppExplore/archive/2008/03/21/45061.html中的模型2,可以有效突破select可监控的文件描述符上限。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。

另外,如果select调用中设置了等待时间,那么每次调用时都需要重新对这个时间赋值么?就像对fd_set处理一样。
例如:
fd_set readfd;
struct timval tv;
while(1) {
  FD_ZERO(&readfd);
  FD_SET(fd, &readfd);
  tv.tv_sec = 2;
  tv.tv_usec = 0;
  select(maxfd+1, &readfd, NULL, NULL, &tv);
  ......;
}

如上代码,对fd_set需要每次调用都要重新设置,那么对tv来说是否也是一样呢?能不能把对tv的赋值放在while外面?

答案是不行,如果将时间的初始化放在外边,时间初始化为2秒,假设在1秒后发上了事件,则select将会返回并将tv的时间变成上次阻塞的剩余时间,即1秒,然后再进行监视套接字。这是因为linux系统对select()的实现中会修改参数tv为剩余时间。所以对于select函数中的最后一个参数,需要在循环中设置,每次循环要重新设置。如果设在循环外面,当循环执行起来后,每次循环select都会修改tv的值,tv的值越来越小,导致最后会产生select函数这tv时间内收不到有效时间,而返回-1,造成错误。

socket 可读:

1. 接收缓冲区有数据,一定可读
2. 对方正常关闭socket,也是可读
3. 对于侦听socket,有新链接到达也可读