(2)套接字的Select模型
Select模型是套接字中最常见的模型。它的核心是利用select()函数实现套接字的输入输出管理。利用select()函数,应用程序可以判断指定套接字上是否存在数据,如果套接字上存在数据,则调用recv()函数进行接收;还可以通过该函数判断能否向指定套接字上发送数据,即指定套接字是否已经准备好接收数据,如果已经准备好,则调用send()函数发送数据。
在使用Select模型时,需要用到fd_set结构以及FD_ZERO、FD_SET等宏。
fd_set是一个管理多个套接字的结构体。该结构体的定义为
typedef struct fd_set{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
}fd_set;
其中,fd_count
表示管理的套接字数量;
fd_array
是
SOCKET
类型的数组,其元素是管理的套接字,
FD_SETSIZE
的值是
64
,也就是说最多可以管理
64
个套接字。在前文提到的
select()
函数就是对
fd_set
类型的变量进行管理。
FD_ZERO宏:该宏的作用是初始化fd_set结构的对象;
FD_SET宏:该宏的作用是将指定的套接字加入到fd_set结构的对象中;
(2)Select模型的实现
在ThreadFunc_RecvData()函数中,首先定义fd_set结构的对象,用来保存要管理的套接字:
fd_set socket_fs;
接下来通过while()语句,循环接收来自客户端的数据。
int socketindex = pServer->m_client_currentindex;
while (pServer->m_clientconnectflag_array[socketindex])
{
........
}
其中,pServer
是
ThreadFunc_RecvData()
函数的参数,使用方法在“
2.3.3
定义线程函数”中已介绍。通过
pServer
可以在CTCPSocket_Server
类的静态函数中调用该类的普通成员。m_client_currentindex
是套接字在“套接字池”中的索引,该索引值在“
2.3.2
指定回调函数”提到的代码中获取。将
m_client_currentindex
保存在变量
socketindex
中的原因是,在接受客户端连接的线程
ThreadFunc_StartServer
中,如果有新的客户端连入服务端,
m_client_currentindex
值会发生改变。如果不将该值保存在临时变量中,在接收客户端数据时可能会发生错误。
while()的循环语句是数组pServer->m_clientconnectflag_array中的元素。在“2.3.3 定义线程函数”中提到,该数组中包含的是“套接字池”中的套接字是否可用的标志。也就是说,当指定的套接字可用时,while()循环的语句才被执行。当将相应的元素值设置为false时,将结束接收数据的线程。
在while()循环中,首先调用FD_ZERO宏清空套接字集合socket_fs,之后将与客户端通信的套接字添加到该集合中。
FD_ZERO(&socket_fs);
FD_SET(pServer->m_clientsocket_array[socketindex], &socket_fs);
FD_SET()
宏的第一个参数是要加入集合的套接字,第二个参数是套接字集合的指针。之后,利用
select()
函数判断套接字集合中的套接字是否可读、可写。
select()
函数的格式为
int select(
int nfds
, fd_set* readfds
, fd_set* writefds
, fd_set* exceptfds
, const struct timeval* timeout
);
其中,参数nfds 为保留参数,可以将其设置为 0 ; readfds 是具有可读性套接字集合的指针,如果要判断套接字集合中的套接字中是否有数据,则要设置该参数; writefds 是具有可写性套接字集合的指针,如果要判断套接字集合中的套接字是否已经准备好接收数据,则要设置该参数; exceptfds 是检查错误套接字集合的指针,如果要检查套接字集合中的套接字是否发生错误,则要设置该参数; timeout 用于设置调用 select() 函数时的等待时间,其类型是 timeval 结构,该结构主要指定时间,其格式为
typedef struct timeval{
long tv_sec;
long tv_usec;
}timeval;
tv_sec
表示秒,
tv_usec
表示毫秒,在使用时指定两个成员变量中的一个即可。
select()
函数的返回值是套接字集合中发生可读、可写或者异常的套接字数量,如果是
0
则表示已经过了等待时间还没有发生可读、可写或者异常。
if (select(0, &socket_fs, NULL, NULL, &time_selectwait) == 1)
{
value_recvretrun = recv(pServer->m_clientsocket_array[socketindex], buf, LENGTH_RECVDATA, 0);
if (pServer->m_receiveclientdata_proc != NULL)
{
pServer->m_receiveclientdata_proc(buf, LENGTH_RECVDATA, socketindex);
}
}
其中,time_selectwait
是
timeval
结构的对象,其定义为
timeval time_selectwait = { 0, 5000 };
即select()
函数等待
5
秒钟后,如果在套接字集
socket_fs
中没有套接字变为可读,则函数返回
0
,此时通过
while()
语句,继续调用
select()
函数等待套接字变为可读,即
select()
函数返回值是
1
。当套接字变为可读,则说明套接字中有数据等待读取,此时调用
recv()
函数接收数据。
(3)接收数据
服务端通过recv()函数,通过套接字接收来自客户端的数据。该函数的格式是
int recv(SOCKET s, char* buf, int len, int flags);
其中,参数s
表示指定的套接字;
buf
指定了保存数据的缓冲区;
len
表示缓冲区
buf
的长度;
flags
参数会影响
recv()
函数的行为,如果将该参数设置为
0
,则表示没有特殊行为。如果成功接收到了来自客户端的数据,则
recv()
函数的返回值为接收到数据的大小。
在接收到了客户端数据之后,调用“2.3.2 指定回调函数”中提到的回调函数m_receiveclientdata_proc,将接收到的数据交给主窗口来处理。
(4)更新“套接字池”
当客户端退出时,需要对服务端的“套接字池”进行更新。客户端的退出分为两种情况:“优雅”退出和“强行”退出。“优雅”退出指的是客户端调用了closesocket()函数关闭套接字,“强行”退出指的是直接关闭了客户端程序。recv()函数通过不同的返回值来反映这两种退出。当客户端“优雅”退出时,recv()函数的返回值是0;当客户端“强行”退出时,recv()函数的返回值是-1,即SOCKET_ERROR。所以,在“(3)Select模型的实现”中提到的while()循环的执行代码中,还需添加对recv()函数返回值的判断:
if (SOCKET_ERROR == value_recvretrun || 0 == value_recvretrun)
{
closesocket(pServer->m_clientsocket_array[socketindex]);
pServer->m_clientconnectflag_array[socketindex] = false;
}
当客户端退出时,服务端通过closesocket()
函数关闭与客户端通信的套接字,并且把“套接字池”的客户端套机字是否合法的数组进行更新,将该数组的相应元素设置为
false
。此时,
while()
循环会退出。在循环退出之后,对“套接字池”的客户端当前索引值进行更新
pServer->m_client_currentindex--;