阻塞与非阻塞:
widows下创建套接字默认都是阻塞型的,阻塞型的好处是处理简单,理解容易,但是处理多个套接字时,就必须创建多个线程,即一个连接socket使用一个线程。而非阻塞模式比如在处理发送和接收数据时,会立即返回,不管是否有有效的数据,这就需要不断测试返回代码,来确定套接字在什么时候可读/可写,也就是确定网络事件何时发生,比如中断默认就是一种事件触发型,比如菜单按钮也是事件触发性,但是比如快递邮寄包裹,他其实使用的是一种任务制(提前规定好的)。windows也提供了众多的非阻塞I/O模型,如select、WSAAsyncSelect、WSAEventSelect、overlapped、completion port,比如select就可以设置时间,按规定时间去查询事件是否被触发,像WSAAsyncSelect就是事件驱动型的,等,这里主要用在socket端开发服务器程序。
select模型目的:主要是避免在套接字调用上阻塞的应用程序有能力管理多个套接字,即是单一线程模式下只能处理一个套接字的问题,这样可以避免线程膨胀。
select模型函数:
int select( _In_ int nfds, _Inout_ fd_set *readfds, _Inout_ fd_set *writefds, _Inout_ fd_set *exceptfds, _In_ const struct timeval *timeout );参数说明:
nfds [in]:忽略,仅是为了兼容Berkeley套接字
readfds [in, out]:用来检查可读的套接字组合
writefds [in, out]:用来检查可写的套接字组合
exceptfds [in, out]:用来检查异常的套接字组合
timeout [in]:等待的时间, 如果为NULL,等待的时间为无穷大
返回值:select返回那些即将要被处理的socket总和,假如时间超时,将会返回SOCKET_ERROR,可以使用WSAGetLastError获得出错的原因
Select处理过程:假设以read为例,在这里windows主要是先将套接字s添加到readfds集合中,然后等待select函数返回,在select函数里面会移除没有未决的I/O操作的套接字句柄,即移除未响应的IO套接字句柄,然后看s是否认仍然还是readfs集合中,在就说明s可读了
应用程序:
CInitSock initsock;
sockaddr_in addr;
USHORT usPort = 6000;
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP );
if (sListen == INVALID_SOCKET )
{
TRACE("create socket error:%d\n", WSAGetLastError());
return -1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(usPort);
addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (::bind(sListen, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR)
{
/*WSAEFAULT 10014:The system detected an invalid pointer address
in attempting to use a pointer argument in a call.*/
TRACE("bind socket error: %d\n", WSAGetLastError());
return -1;
}
::listen(sListen, 5);
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(sListen, &fdSocket);
while (true)
{
fd_set fdRead = fdSocket;
int nRet = select(0, &fdRead, NULL, NULL, NULL);
if (!nRet || nRet == SOCKET_ERROR )
{
TRACE("select error: %d\n", WSAGetLastError());
return -1;
}
for (unsigned int i = 0; i < fdSocket.fd_count; i++)
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead)) //这里选择了fdSocket,是因为下一次循环还要使用fdSocket
{
if (fdSocket.fd_array[i] == sListen)
{
if (fdSocket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET sNewClient = ::accept(sListen, (sockaddr*)&addrRemote, &nAddrLen);
if (sNewClient == INVALID_SOCKET )
{
TRACE("accept new client socket error: %d\n", WSAGetLastError());
break;
}
FD_SET(sNewClient, &fdSocket);
TRACE("new client: %s\n", inet_ntoa(addrRemote.sin_addr));
}
}
else
{
char szText[256];
int nRecv = ::recv(fdSocket.fd_array[i], szText, sizeof(szText), 0);
if (!nRecv || nRecv == SOCKET_ERROR )
{
TRACE("recv data error: %d\n", WSAGetLastError());
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
break;
}
else
{
szText[nRecv] = '\0';
TRACE("recv data: %s", szText);
}
}
}//判断fdsocket里面的socket是否得到处理
}
}
closesocket(sListen);
sListen = INVALID_SOCKET;
return 0;
这里我使用了CInitSock, 因为在使用socket之前要加载
Ws2_32.lib,这里我定义了一个类如下:
#pragma once
#include<winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib,"Ws2_32.lib")
class CInitSock
{
public:
CInitSock(BYTE minVer = 2, BYTE majVer = 2);
~CInitSock(void);
};
CInitSock::CInitSock(BYTE minVer, BYTE majVer)
{
int nResult;
WSADATA wsadata;
WORD wVerReq = MAKEWORD(minVer, majVer);
if (nResult = ::WSAStartup(wVerReq, &wsadata))
{
TRACE("WSAStartup Load DLL Failed: %d!\n", nResult);
}
}
CInitSock::~CInitSock(void)
{
/*In a multithreaded environment, WSACleanup terminates
*Windows Sockets operations for all threads.
*/
::WSACleanup();
}
默认构造函数里面有一个默认加载的版本,这里在析构函数里面将之前加载的dll资源进行释放,基础的socket服务器模型通常要进行socket创建,绑定到本地地址和端口,监听客户端的连接,一旦有客户端连接,默认会放入fdSocket中,然后将此函数加入fd_set可读的套接字集合中,select返回后,未响应的socket会被移除,即将要被处理的socket会保留下来,然后从fdSocket判断,到底是哪些socket发生了可读操作:
注意:可读操作包括有未处理的连接请求,数据可读,连接关闭/重启/中断
首先第一个判断的就是未处理的连接请求,如果有就建立新的连接通道,加入fdSocket;如果是数据可读,就读取数据;连接关闭会在下面进行测试
客户端程序:使用的是以前的一个简易客户端程序,如下
WORD wVersionRequested; //请求的版本
WSADATA wsaData;
int nErr;
//协商版本号
wVersionRequested = MAKEWORD(1,1);
nErr = WSAStartup(wVersionRequested, &wsaData);
if(nErr != 0)
{
return;
}
if( LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) != 1 )
{
WSACleanup();
return;
}
//创建socket端口
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
//绑定端口号
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//收发数据
char recvBuf[100], sendBuf[100];
memset(recvBuf, 0, 100);
memset(sendBuf, 0, 100);
sprintf_s(sendBuf,"hello world");
send(sockClient, sendBuf, strlen(sendBuf)+1, 0);
recv(sockClient, recvBuf, 100, 0);
printf("%s\n", recvBuf);
//关闭socket通信
closesocket(sockClient);
WSACleanup();
Sleep(1000);
测试结果:
这里需要先运行服务器端,然后再开启客户端程序,服务器端会建立新的连接,并读取客户端发过来的数据然后显示出来,客户端是没有数据的,因为这里服务器端并没有发送数据,如下服务器数据:
再次开启一个客户端的效果如下:
这里并没有进行换行,两行数据在一起了,并不影响测试结果,这里还可以再强制关闭客户端后的结果,如下
这里看到error的代码为10054,我们在winerror.h里面找到如下定义:
//
// MessageId: WSAECONNRESET
//
// MessageText:
//
// An existing connection was forcibly closed by the remote host.
//
#define WSAECONNRESET 10054L
从这里也能看出的确是强制关闭,注意服务器端里面的TRACE要给我printf才可以,TRACE默认是在调试下使用的输出语句
Select不足:其实添加到fd_set套接字数量是有限制的,winsock2.h定义的64,自定义也不超过1024,因为值太大,会对服务器的性能有影响,假设有1000个的话,在调用select之前就必须设置这1000个套接字,select返回之后,还必须检查这1000个套接字,所以开销较大。