在 Winsocket 一:单线程阻塞server&client程序(tcp) 和 Winsocket 二:多线程阻塞服务器程序(tcp)介绍了阻塞tcp程序,阻塞式tcp程序服务器程序会因为建立连接和关闭连接而频繁的创建和关闭线程会产生大量的内存碎片,从而导致服务端程序不能保证长时间的稳定运行,本文简单介绍非阻塞式tcp程序的编写。
一、非阻塞
阻塞是指在进行一个操作的时候,如服务器接收客户端的连接(accept),客户端程序执行connect操作,服务器或者客户端读写数据(read、write),如果该操作没有执行完成(成功或者失败都算是执行完成),则程序会一直阻塞在操作执行的地方,直到该操作返回一个明确的结果。非阻塞式程序会在产生阻塞操作的地方阻塞一定的时间(该时间可以由程序员自己设置)。如果操作没有完成,在到达所设置的时间之后,无论该操作成功与否,都结束该操作而执行程序后面的操作。
二、非阻塞相关的系统函数
1、ioctlsocket()
为了执行非阻塞操作,需要在创建了一个套接口后,调用ioctlsocket函数将套接口设置为非阻塞的套接口:int ioctlsocket( SOCKET s, long cmd, u_long FAR *argp ),其作用是控制套接口的I/O模式。
s 要设置的套接口;
cmd 要对该套接口设置的命令,为了要将套接口设置成为非阻塞的,我们应该填写FIONBIO;
argp 设置为1表示非阻塞,0表示阻塞。
2、select()
为了进行非阻塞的操作,我们需要在进行非阻塞操作之前调用select函数:int select(int nfds, fd_set FAR *readfds, fd_set FAR *writefds, fd_set FAR *exceptfds, const struct timeval FAR *timeout ),其作用是设定一个或多个套接口的状态,并进行必要的等待,以便执行异步I/0(非阻塞)操作。
nfds 可忽略,作用仅仅是为了与伯克利套接口相兼容;
readfds 要检测的可读套接字集合(可设置为NULL);在三种情况下有信号出现:一、如果集合中有套接字处于监听状态,并且该套接字上有来自客户端的连接请求;二、如果集合中的套接字收到了send操作发送过来的数据;三、如果集合中的套接字被关闭、重置或者中断。
writefds 要检测的可写套接字集合(可设置为NULL);在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接成功;二、可以用send操作向集合中的套接字写数据。
exceptfds要检测的错误套接字集合(可设置为NULL);在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接失败;二、有带外数据到来。
timeout 执行该函数时需要等待的时间,如果为NULL则表示阻塞操作,为0则表示立即返回。
其中fd_set表示套接字集合。在使用select函数时,我们需要将相应的套接字加入到相应的集合中。如果集合中的套接字有信号,select函数的返回值即为集合中有信号的套接字数量。
3、与fd_set相关的宏
FD_ZERO(*set)将集合set清空;
FD_SET(s, *set)将套接字s加入到集合set;
FD_CLR(s, *set)将套接字s移除出集合set;
FD_ISSET(s, *set)判断套接字s是否在集合中有信号。
三、非阻塞TCP程序实例
1、服务器程序
// server_2.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <cassert>
#include <list>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define ASSERT assert
using namespace std;
static const int c_iPort = 10001;
bool GraceClose(SOCKET *ps);
int _tmain(int argc, _TCHAR* argv[])
{
int iRet = SOCKET_ERROR;
// 初始化Winsocket
WSADATA data;
ZeroMemory(&data, sizeof(WSADATA));
iRet = WSAStartup(MAKEWORD(2, 0), &data);
ASSERT(SOCKET_ERROR != iRet);
// 建立服务端程序的监听套接字
SOCKET skListen = INVALID_SOCKET;
skListen = socket(AF_INET, SOCK_STREAM, 0);
ASSERT(INVALID_SOCKET != skListen);
// 初始化监听套接字地址信息
sockaddr_in adrServ;
ZeroMemory(&adrServ, sizeof(sockaddr_in));
adrServ.sin_family = AF_INET;
adrServ.sin_port = htons(c_iPort);
adrServ.sin_addr.s_addr = INADDR_ANY;
// 绑定监听套接字到本地
iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in));
ASSERT(SOCKET_ERROR != iRet);
// 使用监听套接字进行监听
iRet = listen(skListen, FD_SETSIZE);
ASSERT(SOCKET_ERROR != iRet);
// 将套接口从阻塞状态设置到费阻塞状态
unsigned long ulMode = 1;
iRet = ioctlsocket(skListen, FIONBIO, &ulMode);
ASSERT(SOCKET_ERROR != iRet);
fd_set fsListen; //fd_set为套字集合
FD_ZERO(&fsListen); //FD_ZERO(*set)将集合set清空
fd_set fsRead;
FD_ZERO(&fsRead);
timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
list<SOCKET> sl;
int i = 2;
for(;;)
{
// 接收来自客户端的连接, 并将新建的套接字加入套接字列表中
FD_SET(skListen, &fsListen);//FD_SET(s, *set)将套接字s加入到集合set中
iRet = select(1, &fsListen, NULL, NULL, &tv);
if(iRet > 0)
{
sockaddr_in adrClit;
int iLen = sizeof(sockaddr_in);
ZeroMemory(&adrClit, iLen);
SOCKET skAccept = accept(skListen, (sockaddr*)&adrClit,&iLen);
ASSERT(INVALID_SOCKET != skAccept);
sl.push_back(skAccept);
cout << "New connection " << skAccept << ", c_ip: "
<< inet_ntoa(adrClit.sin_addr) << ", c_port: " << ntohs(adrClit.sin_port) << endl;
}
// 将套接字列表中的套接字加入到可读套接字集合中,以便在可以检测集合中的套接字是否有数据可读
FD_ZERO(&fsRead);
for(list<SOCKET>::iterator iter = sl.begin(); iter != sl.end(); ++iter)
{
FD_SET(*iter, &fsRead);
}
// 检测集合中的套接字是否有数据可读
iRet = select(sl.size(), &fsRead, NULL, NULL, &tv);
if(iRet > 0)
{
for(list<SOCKET>::iterator iter = sl.begin(); iter != sl.end(); ++iter)
{
// 如果有数据可读, 则遍历套接字列表中的所有套接字,检测出有数据可读的套接字
iRet = FD_ISSET(*iter, &fsRead);// FD_ISSET(s, *set)判断套接字s是否在集合中有信号
if(iRet > 0)
{
// 读取套接字上的数据
const int c_iBufLen = 512;
char recvBuf[c_iBufLen + 1] = {'\0'};
int iRecv = SOCKET_ERROR;
iRecv = recv(*iter, recvBuf, c_iBufLen, 0);
if (iRecv <= 0 )// 读取出现错误或者对方关闭连接
{
iRecv == 0 ? cout << "Connection shutdown at socket " << *iter << endl ://优雅关闭
cout << "Connection recv error at socket " << *iter << endl;// 粗暴关闭
iRet = GraceClose(&(*iter));
ASSERT(iRet);
}
else
{
recvBuf[iRecv] = '\0';
cout << "Server recved message from socket " << *iter << ": " << recvBuf << endl;
// 创建可写集合
FD_SET fsWrite;
FD_ZERO(&fsWrite);
FD_SET(*iter, &fsWrite);
// 如果可可写套接字, 则向客户端发送数据
iRet = select(1, NULL, &fsWrite, NULL, &tv);
if (iRet <= 0)
{
int iWrite = SOCKET_ERROR;
iWrite = send(*iter, recvBuf, iRecv, 0);
if (SOCKET_ERROR == iWrite)
{
cout << "Send message error at socket " << *iter << endl;
iRet = GraceClose(&(*iter));
ASSERT(iRet);
}
}
}
}
} //for
sl.remove(INVALID_SOCKET); // 删除无效的套接字, 套接字在关闭后被设置为无效
} //if
} //for (;;)
// 将套接字设置回阻塞状态
ulMode = 0;
iRet = ioctlsocket(skListen, FIONBIO, &ulMode);
ASSERT(SOCKET_ERROR != iRet);
// 关闭监听套接字
iRet = GraceClose(&skListen);
ASSERT(iRet);
// 清理Winsocket资源
iRet = WSACleanup();
ASSERT(SOCKET_ERROR != iRet);
system("pause");
return 0;
}
bool GraceClose(SOCKET *ps)
{
const int c_iBufLen = 512;
char szBuf[c_iBufLen + 1] = {'\0'};
// 关闭该套接字的连接
int iRet = shutdown(*ps, SD_SEND);
while(recv(*ps, szBuf, c_iBufLen, 0) > 0);
if (SOCKET_ERROR == iRet)
{
return false;
}
// 清理该套接字的资源
iRet = closesocket(*ps);
if (SOCKET_ERROR == iRet)
{
return false;
}
*ps = INVALID_SOCKET;
return true;
}
代码解释:
if(iRet > 0)
{
sockaddr_in adrClit;
int iLen = sizeof(sockaddr_in);
ZeroMemory(&adrClit, iLen);
SOCKET skAccept = accept(skListen, (sockaddr*)&adrClit,&iLen);
ASSERT(INVALID_SOCKET != skAccept);
sl.push_back(skAccept);
cout << "New connection " << skAccept << ", c_ip: "
<< inet_ntoa(adrClit.sin_addr) << ", c_port: " << ntohs(adrClit.sin_port) << endl;
}
将接受自客户端程序的套接字加入到list中,以方便管理。
if (iRecv <= 0 )// 读取出现错误或者对方关闭连接
{
iRecv == 0 ? cout << "Connection shutdown at socket " << *iter << endl ://优雅关闭
cout << "Connection recv error at socket " << *iter << endl;// 粗暴关闭
iRet = GraceClose(&(*iter));
ASSERT(iRet);
}
在套接字被客户端关闭或者异常中断后,需要将list中的套接字设置为无效套接字,并在操作完所有的套接字一次后,将无效的套接字从集合中移除。
2、客户端程序
// client.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <cassert>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
#define ASSERT assert
static const char c_szIP[] = "127.0.0.1";
static const int c_iPort = 10001;
bool GraceClose(SOCKET *ps);
int main()
{
int iRet = SOCKET_ERROR;
// 初始化Winsocket
WSADATA data;
ZeroMemory(&data, sizeof(WSADATA));
iRet = WSAStartup(MAKEWORD(2, 0), &data);
ASSERT(SOCKET_ERROR != iRet);
// 建立连接套接字
SOCKET skClient = INVALID_SOCKET;
skClient = socket(AF_INET, SOCK_STREAM, 0);
ASSERT(INVALID_SOCKET != skClient);
// 初始化连接套接字地址信息
sockaddr_in adrServ;
ZeroMemory(&adrServ, sizeof(sockaddr_in));
adrServ.sin_family = AF_INET;
adrServ.sin_port = htons(c_iPort);
adrServ.sin_addr.s_addr = inet_addr(c_szIP);
// 将套接口从阻塞状态设置到非阻塞状态
unsigned long ulEnable = 1;
iRet = ioctlsocket(skClient, FIONBIO, &ulEnable);
ASSERT(SOCKET_ERROR != iRet);
fd_set fsWrite;
TIMEVAL tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
cout << "Client began to connect to the server..." << endl;
for (;;)
{
// 使用非阻塞方式连接服务器,请注意connect操作的返回值总是为SOCKET_ERROR
iRet = connect(skClient, (sockaddr*)&adrServ, sizeof(sockaddr_in));
int iErrorNo = SOCKET_ERROR;
int iLen = sizeof(int);
// 如果getsockopt返回值不为0,则说明有错误出现
if (SOCKET_ERROR == iRet && 0 != getsockopt(skClient, SOL_SOCKET, SO_ERROR, (char*)&iErrorNo, &iLen))
{
cout << "An error happened on connecting to server. The error no is " << iErrorNo
<< ". The program will exit now." << endl;
exit(-1);
}
FD_ZERO(&fsWrite);
FD_SET(skClient, &fsWrite);
// 如果集合fsWrite中的套接字有信号,则说明连接成功,此时iRet的返回值大于0
iRet = select(1, NULL, &fsWrite, NULL, &tv);
if (iRet > 0)
{
cout << "Successed connect to the server..." << endl;
break;
}
cout << "retrying" << endl;
} //for
for(;;)
{
const int c_iBufLen =512;
char szBuf[c_iBufLen + 1] = {'\0'};
cout << "What you will say:";
cin >> szBuf;
if(0 == strcmp("exit", szBuf))
{
break;
}
FD_ZERO(&fsWrite);
FD_SET(skClient, &fsWrite);
// 如果集合fsWrite中的套接字有信号, 则可以用send操作发数据
iRet = select(1, NULL, &fsWrite, NULL, &tv);
if (0 < iRet)
{
iRet = send(skClient, szBuf, strlen(szBuf), 0);
if(SOCKET_ERROR == iRet)
{
cout << "send error." << endl;
break;
}
fd_set fsRead;
FD_ZERO(&fsRead);
FD_SET(skClient, &fsRead);
// 如果集合fsRead中的套接字有信号, 则可以用recv操作读数据
iRet = select(1, &fsRead, NULL, NULL, &tv);
if (0 < iRet)
{
iRet = recv(skClient, szBuf, c_iBufLen, 0);
if(0 == iRet)
{
cout << "connection shutdown." << endl;
break;
}
else if(SOCKET_ERROR == iRet)
{
cout << "recv error." << endl;
break;
}
szBuf[iRet] = '\0';
cout << szBuf << endl;
}
} //if
} //for
// 将套接字设置回阻塞状态
ulEnable = 0;
iRet = ioctlsocket(skClient, FIONBIO, &ulEnable);
ASSERT(SOCKET_ERROR != iRet);
// 关闭监听套接字
iRet = GraceClose(&skClient);
ASSERT(iRet);
// 清理Winsocket资源
iRet = WSACleanup();
ASSERT(SOCKET_ERROR != iRet);
system("pause");
return 0;
}
bool GraceClose(SOCKET *ps)
{
const int c_iBufLen = 512;
char szBuf[c_iBufLen + 1] = {'\0'};
// 关闭该套接字的连接
int iRet = shutdown(*ps, SD_SEND);
while(recv(*ps, szBuf, c_iBufLen, 0) > 0);
if (SOCKET_ERROR == iRet)
{
return false;
}
// 清理该套接字的资源
iRet = closesocket(*ps);
if (SOCKET_ERROR == iRet)
{
return false;
}
*ps = INVALID_SOCKET;
return true;
}
————————————————
版权声明:本文为CSDN博主「u013071074」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013071074/article/details/26354781
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)