入门中我们基本的思路是为每个实际的连接开辟一个子线程用于数据通讯,我们知道,由于计算机的处理能力毕竟还是有限的,因此在并发多连接的情况下,无限制的开辟线程将会对系统造成很大的负担(呵呵,后果还是蛮严重的。。。),因此,聪明的家伙们想到用各种I/O模型来解决数据传输中遇到的种种问题,我们先来看看大家最先会研究的一种模型吧(疑?印象中我是从消息模型开始的……不管了)
这就是select模型,最初听说这个,就想到了c/c++里的关键字switch & case... 嘿嘿,后来发现确实还蛮像的:)。废话不多说,咱们先进一段代码:(本文转自网络,稍加整理,仅供大家学习交流用)
#include
<
winsock.h
>
#include
<
stdio.h
>
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
#define
PORT 5150
#define
MSGSIZE 1024
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
#pragma
comment(lib, "ws2_32.lib")
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
int
g_iTotalConn
=
0
;
SOCKET g_CliSocketArr[FD_SETSIZE];
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
DWORD WINAPI WorkerThread(LPVOID lpParameter);
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
int
main()
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
int iaddrSize = sizeof(SOCKADDR_IN);
DWORD dwThreadId;
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// 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));
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// Listen
listen(sListen, 3);
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
while (TRUE)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d ", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// Add socket to g_CliSocketArr
g_CliSocketArr[g_iTotalConn++] = sClient;
}
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
DWORD WINAPI WorkerThread(LPVOID lpParam)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
int i;
fd_set fdread;
int ret;
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
struct timeval tv = ...{1, 0};
char szMessage[MSGSIZE];
while (TRUE)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
FD_ZERO(&fdread);
for (i = 0; i < g_iTotalConn; i++)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
FD_SET(g_CliSocketArr[i], &fdread);
}
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
// We only care read event
ret = select(0, &fdread, NULL, NULL, &tv);
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
if (ret == 0)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
// Time expired
continue;
}
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
for (i = 0; i < g_iTotalConn; i++)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
if (FD_ISSET(g_CliSocketArr[i], &fdread))
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
// A read event happened on g_CliSocketArr[i]
ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
// Client socket closed
printf("Client socket %d closed. ", g_CliSocketArr[i]);
closesocket(g_CliSocketArr[i]);
if (i < g_iTotalConn - 1)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
}
}
else
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
// We received a message from client
szMessage[ret] = '/0';
send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
}
}
}
}
return 0;
}
没时间自己从头写代码,在网上找了找现成的,基本上这个代码和入门代码还是很类似的,也是有个子线程的概念在里头,不过相信细心的你一定发现了,这个主函数只创建了一个子线程,还居然是在连接建立之前就创建了,这个,就是select模型和之前入门模型的第一个区别了。对了,这个就是工作者线程,专家们一般都会建议在windows进行程序开发,把一些后台的工作交给这样一些线程来处理,主线程只负责一些简单的操作和一些UI操作。
我们再来从头分析一下:创建监听套接字,绑定,监听这些socket里一定会有的不多说了;然后是创建一个工作者线程;最后是一个循环接受新的连接请求,并将通讯socket保存到一个数组末尾。这里有一点需要注意的,就是我没有重新定义FD_SETSIZE宏,所以服务器最多支持的并发连接数为64。而且,这里决不能无条件的accept,服务器应该根据当前的连接数来决定是否接受来自某个客户端的连接。一种比较好的实现方案就是采用WSAAccept函数,而且让WSAAccept回调自己实现的Condition Function。如下所示:
工作者线程里面使用到了Winsock提供的一些宏用来操作套接字队列fd_set,这里稍微介绍一下:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
工作者线程里面是一个死循环,一次循环完成的动作是:
1.将当前所有的客户端套接字加入到读集fdread中;
2.调用select函数;
其原型是
int
select(
int
nfds , fd_set FAR
*
readfds , fd_set FAR
*
writefds , fd_set FAR
*
exceptfds ,
const
struct
timeval FAR
*
timeout ) ;
该函数来检查你要调用的socket套接字是否已经有了需要处理的数据。
select包含三个socket队列,分别代表:readfds ,检查可读性; writefds,检查可写性; exceptfds,例外数据。
timeout是select函数的返回时间。例如,我们想要检查一个套接字是否有数据需要接收,我们可以把套接接字句柄加入可读性检查队列中,然后调用select,如果,该套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉,所以我们只要检查该套接字句柄是否还存在于可读性队列中,就可以知道到底有没有数据需要接收了。
3.查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)
基本上就是这些,实际的编程中要注意的小细节还是很多(比如在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%)。