本文主要介绍在Windows系统下使用select搭建回声服务端的方法。在之前的《网络编程(16)—— IO复用技术之select》一文中我们介绍了在Linux使用Select进行IO复用的方法。本文对其原理不再详述,旨在通过对比使用加强对select的理解和应用。整个Windows版的select服务端的代码如下,稍后我们将对关键代码进行解释。
// SelectServ.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "stdio.h"
#include "winsock2.h"
#pragma comment(lib,"ws2_32.lib")
#define BUF_SIZE 30
#define SOCK_SIZE 20
void ErrorHandler(const char* message);
int _tmain(int argc, _TCHAR* argv[])
{
SOCKET servSock,clntSock;
SOCKADDR_IN servAddr,clntAddr;
int clntAddrSz;
int strLen;
char buf[BUF_SIZE];
fd_set fds,copyRead;
SOCKET socks[SOCK_SIZE];
TIMEVAL tm;
int sockNum = 0;
unsigned long ul=1;
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)==SOCKET_ERROR)
{
ErrorHandler("WSAStartUp Error");
}
servSock=socket(AF_INET,SOCK_STREAM,0);
if(servSock==INVALID_SOCKET)
ErrorHandler("socket error");
//将socket设置成非阻塞模式
//ioctlsocket(servSock,FIONBIO,&ul);
memset(&servAddr,0,sizeof(servAddr));
servAddr.sin_family=AF_INET;
servAddr.sin_addr.s_addr=htonl(INADDR_ANY);
servAddr.sin_port=htons(atoi("8888"));
if(bind(servSock,(const sockaddr*)&servAddr,sizeof(servAddr))==SOCKET_ERROR)
{
ErrorHandler("bind error");
}
if(listen(servSock,SOCK_SIZE)==SOCKET_ERROR)
{
ErrorHandler("listen error");
}
//遍历socks,将所有的元素置于无效的socket
for(int i=0;i<SOCK_SIZE;i++)
socks[i] = INVALID_SOCKET;
sockNum += 1;
socks[0]=servSock;
FD_ZERO(&fds);
FD_SET(servSock,&fds);
tm.tv_sec=100000;
tm.tv_usec=0;
while(1)
{
copyRead=fds;
int selResult = select(sockNum,©Read,0,0,&tm);
printf("select return...\n");
if(selResult==-1)
puts("select error");
else if(selResult==0)
puts("timeout!");
else
{
//先判断是否是有新的客户端连接
if(FD_ISSET(socks[0],©Read))
{
clntAddrSz = sizeof(clntAddr);
clntSock = accept(servSock,(SOCKADDR*)&clntAddr,&clntAddrSz);
//将socket设置成非阻塞模式
ioctlsocket(clntSock,FIONBIO,&ul);
for(int i=0;i<SOCK_SIZE;i++)
{
//遍历socks,在元素为无效的socket处插入客户端的socket
if(socks[i] == INVALID_SOCKET)
{
FD_SET(clntSock,&fds);
socks[i]=clntSock;
sockNum++;
break;
}
}
}
//遍历所有的客户端socket,0的位置为服务端的socket,所以从1开始
for (int i=1;i<SOCK_SIZE;i++)
{
//如果是无效的socket 不必处理
if(socks[i]==INVALID_SOCKET) continue;
if(FD_ISSET(socks[i],©Read))
{
strLen=recv(socks[i],buf,BUF_SIZE,0);
if(strLen <= 0)//客户端断开了连接
{
closesocket(socks[i]);
//从fds删除客户端socket
FD_CLR(socks[i],&fds);
//将对应的位置再次置为无效socket
socks[i] == INVALID_SOCKET;
sockNum--;
}
else if(strLen > 0)
{
send(socks[i],buf,strLen,0);
}
}
}
}
}
closesocket(servSock);
return 0;
}
void ErrorHandler(const char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
第22~26行,声明使用select相关的变量。包括存放socket的集合fd_set和它的副本copyRead;声明超时tm,以及用来进行socket计数的sockNum;ul在后面设置非阻塞socket时将会用到。
第53~55行,遍历数组socks,将每个数组元素都置为无效的socket,为的是后面的循环判断哪个socket有需要接受的数据时,遇到无效的socket直接跳过,可参照第98行代码。
第56行,因为已经创建了一个服务端的socket,所以sockNum要加1。
第58~62行,将服务端的socket代码放到socks的第一个元素的位置,然后利用宏对fds进行清空,并将servSock注册到fds中,并设置超时时间。
第66行,复制fds至copyRead。每次select之后,select监视的socket集合都会发生状态的变化,且不能复原,因此都需要我们在select之前传递一个集合的副本,以防止传递集合时数据被破坏。
第67行,使用select返回接收到数据的socket
第76行,因为我们将servSock放到了socks中的第一个元素,所以先判断是不是socks[0]发生状态变化,也就是说先判断是不是有新的客户端连接。
第81行,将客户端的socket设置成非阻塞模式,这点很重要,否则在101行读取客户端的数据时就会被阻塞,无法同时和多个客户端进行通信。
第82~91行,遍历socks,只在包含无效socket的位置插入socket。同时,每接收到一个新的客户端的连接,sockNum都要加1.
第95~114行,遍历socks,找到接收到数据的客户端的socket,并进行相应的读写。因为socks[0]存放的是servSock,我们之前已经处理过,所以这里的下标i从1开始。
Github位置:
https://github.com/HymanLiuTS/NetDevelopment
克隆本项目:
git clone git@github.com:HymanLiuTS/NetDevelopment.git
获取本文源代码:
git checkout NL51