介绍
应用程序不会持续等待单个网络事件发生,而是以不断轮询的方式不断询问各个socket的网络状态,直到有网络io条件满足的socket产生为止。
创建socket后,在非阻塞io模式下,发生网络时执行过程为,执行系统调用,如果当前io条件不满足,则立即返回。大多数情况下,调用失败代码为,WSAEWOULDBLOCK。这意味着请求的操作在调用期间美元完成。应用程序会等一段时间,再次执行该调用,直到返回成功为止。
相关函数
int ioctlsocket(
SOCKET s;
long cmd;
u_long *argp;
);
s:套接字句柄。
cmd:在套接字s上执行的命令。它的可选值主要有:
- FIONBIO:参数argp指向一个无符号长整型数值。将argp设置为非0值,表示启用套接字的非阻塞模式;将argp设置为0,表示禁用套接字的非阻塞模式。
- FIONREAD:返回一次调用接收函数可以读取的数据量,即决定可以从套接字s中读取的网络输入缓冲区中挂起的数据的数量,参数argp指向一个无符号长整型数值,用于保存调用ioctlsocket()函数的结果。
- SIOCATMARK:用于决定是否所有带外数据都已经被读取。
● argp:指针变量,指明cmd命令的参数。
在ioctlsocket()函数中使用FIONBIO,并将argp参数设置为非0值,可以将已创建的套接字设置为非阻塞模式。
流程图
代码
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT 27015
int __cdecl main(int argc,TCHAR* argv[]) {
WSADATA wsaData;
int iResult;
SOCKET ServerSocket = INVALID_SOCKET;
SOCKET AcceptSocket = INVALID_SOCKET;
char recvbuf[DEFAULT_BUFLEN];
int recvBufLen = DEFAULT_BUFLEN;
sockaddr_in addrClient;
int addrClinetLen = sizeof(sockaddr_in);
//初始化socket
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult!=0)
{
printf_s("wsatartup faild with error code %d\n", iResult);
return 1;
}
//创建监听socket
ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (ServerSocket==INVALID_SOCKET)
{
printf_s("SOCKET FAILED WITH ERROR CODE %d",WSAGetLastError());
WSACleanup();
return 1;
}
//绑定地址和端口
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(DEFAULT_PORT); //监听端口为DEFAULT_PORT
addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
iResult = bind(ServerSocket,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
if (iResult==SOCKET_ERROR)
{
printf_s("bind falied with error code %d\n",iResult);
closesocket(ServerSocket);
WSACleanup();
return 1;
}
//设置socket为非阻塞模式
int iMode = 1;
iResult = ioctlsocket(ServerSocket, FIONBIO, (u_long*)&iMode);
if (iResult==SOCKET_ERROR)
{
printf_s("ioctlsocket failed with error code %d\n",iResult);
closesocket(ServerSocket);
WSACleanup();
return 1;
}
iResult = listen(ServerSocket,SOMAXCONN);
if (iResult==SOCKET_ERROR)
{
printf_s("listen failed with error code %d",iResult);
closesocket(ServerSocket);
WSACleanup();
return -1;
}
printf_s("TCP server starting");
int err;
while (true)
{
AcceptSocket = accept(ServerSocket, (sockaddr FAR*) & addrClient, &addrClinetLen);
if (AcceptSocket == INVALID_SOCKET)
{
err = WSAGetLastError();
if (err==WSAEWOULDBLOCK)
{
Sleep(1000);
continue;
}
else
{
printf_s("accept afiled \n");
closesocket(ServerSocket);
WSACleanup();
return 1;
}
}
while (true)
{
memset(recvbuf, 0, recvBufLen);
iResult = recv(AcceptSocket, recvbuf, recvBufLen, 0);
if (iResult>0)
{
printf_s("recv data %d byte\n",iResult);
int iSendResult = send(AcceptSocket, recvbuf, iResult, 0);
if (iSendResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(AcceptSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iSendResult);
continue;
}
else if (iResult == 0)
{
printf_s("conn closeing ....\n");
printf_s("waiting next conn...\n");
closesocket(AcceptSocket);
break;
}
else
{
err = WSAGetLastError();
if (err==WSAEWOULDBLOCK)
{
Sleep(1000);
printf_s("current io/xx,waitting 1ms\n");
continue;
}
else
{
printf_s("recv faild with error %d\n",err);
closesocket(ServerSocket);
closesocket(AcceptSocket);
WSACleanup();
return 1;
}
}
}
}
closesocket(ServerSocket);
closesocket(AcceptSocket);
WSACleanup();
return 0;
}
阻塞模式
实例主要涉及两个常见io操作,accept()和recv(),数据接收到的时间是不确定的。
在阻塞模式中,这两个函数在网络io不满足的时候会阻塞卡住。
而在非阻塞模型中的函数处理则不同。
非阻塞模式:
改为非阻塞模式后,调用accept后,即使io条件不满足也会返回。
调用WSAGetLlastError 获取错误码,如果错误码为WSAEWOULDBLOCK,则表示无法立即完成socket操作,此时需要再次尝试accept或recv()操作,本实例使用sleep函数休息100ms,然后执行continue,返回到循环开始部分,重新调用函数进行io判断。
轮询方式实现了对多个套接字I/O条件的判断和处理
评价
优点:
- 非阻塞io使用简单的方法使程序避免在io等待的地方阻塞等待,进程不再睡眠等待,在这段时间内可以做其他事。
- 函数轮询的间隙可以对其他socket 进行类似操作。
- 避免了串型io带来的效率低下问题
缺点:
- 由于应用程序需不断尝试接口函数的调用,直到成功完成指定的操作,这对CPU时间是较大的浪费。
- 另外,如果设置了较长的延迟时间,那么最后一次成功的I/O操作对于I/O事件发生而言有滞后时间,因此这种方法并不适合对实时性要求比较高的应用。