select
我们在TCP的服务端里边,接收一个客户端的时候,我们调用accept函数,这个函数会返回一个客户端的socket,我们在主线程里边不停的接收客户的连接,每当有客户连接时,我们就会在开一个线程,用于对客户的服务。因此,如果有N个的客户进行连接的话,那么线程数量就会有N+1个(N个服务线程+主线程),若N比较大,则线程就会非常多,以至于将整个电脑都给拖垮掉。而我们的select模型呢,就是为了解决这个问题而设计的。
Windows操作系统提供了五种I/O模型,每一种模型适用于一种特定的应用场景。我们应该对自己的应用需求非常明确,综合考虑到程序的扩展性和可移植性等因素,作出自己的选择。
# Select
选择(select)模型是Winsock中最常见的 I/O模型。核心便是利用 select 函数,实现对 I/O的管理!利用 select 函数来判断某Socket上是否有数据可读,或者能否向一个套接字写入数据,防止程序在Socket处于阻塞模式中时,在一次 I/O 调用(如send或recv、accept等)过程中,被迫进入"锁定"状态;同时防止在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。
网上有一种比喻,虽然我很讨厌抄袭,但是确实能够生动的描述select这种模型
老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信~~~~~ 在这种情况下,"下楼检查信箱"然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。 select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收/发送.......
select 的函数原型如下:
//c++
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,const struct timeval* timeout);
2
//Delphi
function select(nfds: Integer; readfds, writefds, exceptfds: PFdSet; timeout: PTimeVal): Integer; stdcall;
2
3
# 参数详解
参数名 | 作用 |
---|---|
nfds | 本参数忽略,仅起到兼容作用 |
readfds | (可选)指针,指向一组等待可读性检查的套接口 |
writefds | (可选)指针,指向一组等待可写性检查的套接口 |
exceptfds | (可选)指针,指向一组等待错误检查的套接口 |
timeout | select()最多等待时间,对阻塞操作则为NULL |
其中最重要的就是FD_SET结构体指针,通过查看源码,它的声明如下
fd_set = record
fd_count: u_int; // how many are SET?
fd_array: array [0..FD_SETSIZE - 1] of TSocket; // an array of SOCKETs
end;
{$EXTERNALSYM fd_set}
TFdSet = fd_set;
PFdSet = ^fd_set;
2
3
4
5
6
7
8
fdset 代表着一系列特定套接字的集合。其中, readfds 集合包括符合下述任何一个条件的套接字:
- 有数据可以读入。
- 连接已经关闭、重设或中止。
- 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
writefds 集合包括符合下述任何一个条件的套接字:
- 有数据可以发出。
- 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
举个例子,假设我们想测试一个套接字是否"可读",必须将自己的套接字增添到readfds集合中,然后调用 select 函数并等待其完成。select 完成之后,再次判断自己的套接字是否仍为 readfds 集合的一部分。若答案是肯定的,则表明该套接字“可读”,可立即着手从它上面读取数据。在三个参数中(readfds、writefds 和 exceptfds),任何两个都可以是空值( NULL);但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;否则, select 函数便没有任何东西可以等待。最后一个参数 timeout 对应的是一个指针,它指向一个timeval 结构,用于决定select 最多等待 I/O操作完成多久的时间。如 timeout 是一个空指针,那么 select 调用会无限期地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。对 timeval 结构的定义如下:
- tv_sec 字段以秒为单位指定等待时间
- tv_usec 字段则以毫秒为单位指定等待时间
若将超时值设置为(0 , 0),表明 select 会立即返回,出于对性能方面的考虑,应避免这样的设置。
# 返回值
select 成功完成后,会在 fdset 结构中,返回刚好有未完成的 I/O操作的所有套接字句柄的总量。若超过 timeval 设定的时间,便会返回0。若 select 调用失败,都会返回 SOCKET_ERROR,应该调用 WSAGetLastError 获取错误码!
用 select 对套接字进行监视之前,必须将套接字句柄分配给一个fdset的结构集合,之后再来调用 select,便可知道一个套接字上是否正在发生上述的 I/O 活动。Winsock 提供了下列宏操作,可用来针对 I/O活动,对 fdset 进行处理与检查:
- FD_CLR(s, *set):从set中删除套接字s。
- FD_ISSET(s, *set):检查s是否set集合的一名成员;如答案是肯定的是,则返回TRUE。
- FD_SET(s, *set):将套接字s加入集合set。
- FD_ZERO( * set):将set初始化成空集合。
例如,假定我们想知道是否可从一个套接字中安全地读取数据,同时不会陷于无休止的“锁定”状态,便可使用 FDSET 宏,将自己的套接字分配给 fdread 集合,再来调用 select。要想检测自己的套接字是否仍属 fdread 集合的一部分,可使用 FD_ISSET 宏。采用下述步骤,便可完成用 select 操作一个或多个套接字句柄的全过程:
- 使用FDZERO宏,初始化一个fdset对象;
- 使用FDSET宏,将套接字句柄加入到fdset集合中;
- 调用 select 函数,等待其返回……select 完成后,会返回在所有 fdset 集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
- 根据 select的返回值和 FDISSET宏,对 fdset 集合进行检查。
- 知道了每个集合中“待决”的 I/O操作之后,对 I/O进行处理,然后返回步骤,继续进行 select 处理。select 函数返回后,会修改 fdset 结构,删除那些不存在待决 I/O 操作的套接字句柄。这正是我们在上述的步骤 ( 4 ) 中,为何要使用 FDISSET 宏来判断一个特定的套接字是否仍在集合中的原因
# 核心代码
TTHread.CreateAnonymousThread(
procedure
begin
while true do
begin
// 初始化一个套接字集合fdsocket,添加监听套接字句柄到这个集合
FD_ZERO(SoecktSet);
_FD_SET(Server, SoecktSet);
// 超时时间
var
TimeVal: TTimeVal;
TimeVal.tv_sec := 1; // 单位秒
TimeVal.tv_usec := 0;
// 至少有一个等待accept的connection
if select(0, @SoecktSet, nil, nil, @TimeVal) > 0 then
begin
// 判断当前socket是否仍然在集合中
if FD_ISSET(Server, SoecktSet) then
begin
// fd_count<=64,也就是说select只能同时管理64个连接
for var I := 0 to SoecktSet.fd_count - 1 do
begin
var
AddrSize := SizeOf(ServerRecord);
var
ClientSocket := accept(Server, @ServerRecord, @AddrSize);
if ClientSocket <> INVALID_SOCKET then
begin
// 创建一个新的线程,在新的线程中再不停的Select
Form1.Memo1.Lines.Add(SoecktSet.fd_count.ToString);
var
CustomWinSocket := TCustomWinSocket.Create(ClientSocket);
Form1.Memo1.Lines.Add('客户端IP:' + CustomWinSocket.RemoteAddress);
end;
end;
end;
end;
end;
end).Start;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46