Winsocket入门教程二:非阻塞式服务器和客户端程序(TCP)
2010年06月06日
上次为大家介绍了阻塞式多线程服务端程序和阻塞式客户端程序的设计方法,但是在上文的最后也提到过,服务器程序会因为建立连接和关闭连接而频繁的创建和关闭线程会产生大量的内存碎片,从而导致服务端程序不能保证长时间的稳定运行。因此我在这里为大家介绍另外一种建立服务器和客户端程序的方法,即建立非阻塞式的服务器和客户端程序。
那什么是非阻塞呢?非阻塞是相对于阻塞而言,阻塞指的是在进行一个操作的时候,如服务器接收客户端的连接(accept),服务器或者客户端读写数据(read、write),如果该操作没有执行完成(成功或者失败都算是执行完成),则程序会一直阻塞在操作执行的地方,直到该操作返回一个明确的结果。而非阻塞式程序则不一样,非阻塞式程序会在产生阻塞操作的地方阻塞一定的时间(该时间可以由程序员自己设置)。如果操作没有完成,在到达所设置的时间之后,无论该操作成功与否,都结束该操作而执行程序下面的操作。
为了执行非阻塞操作,我们在创建了一个套接口后,需要将套接口设置为非阻塞的套接口。为了将套接口设置成为非阻塞套接口,我们需要调用ioctlsocket函数将套接口设置为非阻塞的套接口。ioctlsocket函数的定义如下: int ioctlsocket( SOCKETs, longcmd, u_long FAR *argp) 该函数的作用是控制套接口的I/O模式。
参数s表示要设置的套接口;参数cmd表示要对该套接口设置的命令,为了要将套接口设置成为非阻塞的,我们应该填写FIONBIO;argp表示填写命令的值,如我们要将套接口设置成非阻塞的,我们需要将值设置成为1,如果我们要将套接口设置成为非阻塞状态的话,我们将值设置成为0就是了。
为了进行非阻塞的操作,我们需要在进行操作之前调用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);参数readfds表示要检测的可写套接口的集合(该参数可选,可为设置为NULL);参数exceptfds表示要检测的套接口的错误(该参数可选,可为设置为NULL);参数timeout表示执行该函数时需要等待的时间,如果为NULL则表示阻塞操作,为0则表示立即返回。
下面让我们来看看参数类型fd_set,fd_set表示套接字的集合。在使用select函数时,我们需要将相应的套接字加入到相应的集合中。如果集合中的套接字有信号,select函数的返回值即为集合中有信号的套接字数量。 我们用下面的几个宏来操作fd_set集合。我们可以使用FD_SET(s, *set)将套接字s加入到集合set中;我们可以使用FD_CLR(s, *set)将套接字s移除出集合set;我们可以使用FD_ZERO(*set)将集合set清空;最后,我们可以使用FD_ISSET(s, *set)来判断套接字s是否在集合中有信号。 接下来再让我们来看看select函数的三个集合参数readfds、writefds以及exceptfds。
readfds表示可读套接字的集合,可读套接字在三种情况下有信号出现:一、如果集合中有套接字处于监听状态,并且该套接字上有来自客户端的连接请求;二、如果集合中的套接字收到了send操作发送过来的数据;三、如果集合中的套接字被关闭、重置或者中断。
writefds表示可写套接字的集合,可写套接字在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接成功;二、可以用send操作向集合中的套接字写数据。
exceptfds表示错误套接字的集合,错误套接字在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接失败;二、有带外数据到来。
在我们了解了创建服务器和客户端程序的基础知识后,我们再来看看示例程序,以加深我们对知识的理解。
程序的运行结果如下所示:
下面是服务器程序的代码:
#include #include #include #include #pragma comment(lib, "ws2_32.lib") #define ASSERT assert using std::cin; using std::cout; using std::endl; using std::list; typedef list SocketList; typedef list::iterator SocketListIterator; static const int c_iPort = 10001; bool GraceClose(SOCKET *ps); int main() { int iRet = SOCKET_ERROR; // 初始化Winsocket,所有Winsocket程序必须先使用WSAStartup进行初始化 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; // 初始化地址格式,只能为AF_INET adrServ.sin_port = htons(c_iPort); // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 adrServ.sin_addr.s_addr = INADDR_ANY; // 初始化IP,由于是服务器程序,所以可以将INADDR_ANY赋给该字段,表示任意的IP // 绑定监听套接字到本地 iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in)); ASSERT(SOCKET_ERROR != iRet); // 使用监听套接字进行监听 iRet = listen(skListen, FD_SETSIZE); // SOMAXCONN表示可以连接到该程序的最大连接数 ASSERT(SOCKET_ERROR != iRet); cout 0) { sockaddr_in adrClt; int iLen = sizeof(sockaddr_in); ZeroMemory(&adrClt, iLen); SOCKET s = accept(skListen, (sockaddr*)&adrClt,&iLen); ASSERT(INVALID_SOCKET != s); sl.push_back(s); cout socket is " 0) { for(SocketListIterator iter = sl.begin(); iter != sl.end(); ++iter) { // 如果有数据可读, 则遍历套接字列表中的所有套接字 // 检测出有数据可读的套接字 iRet = FD_ISSET(*iter, &fsRead); if(iRet > 0) { // 读取套接字上的数据 const int c_iBufLen = 512; char szBuf[c_iBufLen + 1] = {'\0'}; int iRead = SOCKET_ERROR; iRead = recv(*iter, szBuf, c_iBufLen, 0); if (0 >= iRead)// 读取出现错误或者对方关闭连接 { iRead == 0 ? cout socket " socket " socket " 发送数据 iRet = select(1, NULL, &fsWrite, NULL, &tv); if (0 socket " 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; }
服务器程序的重点是我们需要将接受自客户端程序的套接字加入到一个链表中,以方便我们的管理。 FD_SET(skListen, &fsListen); iRet = select(1, &fsListen, NULL, NULL, &tv); if(iRet > 0) { sockaddr_in adrClt; int iLen = sizeof(sockaddr_in); ZeroMemory(&adrClt, iLen); SOCKET s = accept(skListen, (sockaddr*)&adrClt,&iLen); ASSERT(INVALID_SOCKET != s); sl.push_back(s); cout socket is " = iRead)// 读取出现错误或者对方关闭连接 { iRead == 0 ? cout socket " socket " #include #include #pragma comment(lib, "ws2_32.lib") #define ASSERT assert using std::cin; using std::cout; using std::endl; 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,所有Winsocket程序必须先使用WSAStartup进行初始化 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; // 初始化地址格式,只能为AF_INET adrServ.sin_port = htons(c_iPort); // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 adrServ.sin_addr.s_addr = inet_addr(c_szIP); // 初始化IP, 由于网络字节顺序和主机字节顺序相反,所以必须使用inet_addr将主机字节顺序转换成网络字节顺序 // 将套接口从阻塞状态设置到非阻塞状态 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 返回值总是为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 返回值大于0 iRet = select(1, NULL, &fsWrite, NULL, &tv); if (0 > 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 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; } 客户端程序比较简单,并且在代码中已经有十分详细的注释了,所以就不在这里详细说明了。
好了,非阻塞式服务器和客户端程序就介绍到这里,下一集中将向大家介绍一下使用Windows消息机制构建socket程序的方法。
2010年06月06日
上次为大家介绍了阻塞式多线程服务端程序和阻塞式客户端程序的设计方法,但是在上文的最后也提到过,服务器程序会因为建立连接和关闭连接而频繁的创建和关闭线程会产生大量的内存碎片,从而导致服务端程序不能保证长时间的稳定运行。因此我在这里为大家介绍另外一种建立服务器和客户端程序的方法,即建立非阻塞式的服务器和客户端程序。
那什么是非阻塞呢?非阻塞是相对于阻塞而言,阻塞指的是在进行一个操作的时候,如服务器接收客户端的连接(accept),服务器或者客户端读写数据(read、write),如果该操作没有执行完成(成功或者失败都算是执行完成),则程序会一直阻塞在操作执行的地方,直到该操作返回一个明确的结果。而非阻塞式程序则不一样,非阻塞式程序会在产生阻塞操作的地方阻塞一定的时间(该时间可以由程序员自己设置)。如果操作没有完成,在到达所设置的时间之后,无论该操作成功与否,都结束该操作而执行程序下面的操作。
为了执行非阻塞操作,我们在创建了一个套接口后,需要将套接口设置为非阻塞的套接口。为了将套接口设置成为非阻塞套接口,我们需要调用ioctlsocket函数将套接口设置为非阻塞的套接口。ioctlsocket函数的定义如下: int ioctlsocket( SOCKETs, longcmd, u_long FAR *argp) 该函数的作用是控制套接口的I/O模式。
参数s表示要设置的套接口;参数cmd表示要对该套接口设置的命令,为了要将套接口设置成为非阻塞的,我们应该填写FIONBIO;argp表示填写命令的值,如我们要将套接口设置成非阻塞的,我们需要将值设置成为1,如果我们要将套接口设置成为非阻塞状态的话,我们将值设置成为0就是了。
为了进行非阻塞的操作,我们需要在进行操作之前调用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);参数readfds表示要检测的可写套接口的集合(该参数可选,可为设置为NULL);参数exceptfds表示要检测的套接口的错误(该参数可选,可为设置为NULL);参数timeout表示执行该函数时需要等待的时间,如果为NULL则表示阻塞操作,为0则表示立即返回。
下面让我们来看看参数类型fd_set,fd_set表示套接字的集合。在使用select函数时,我们需要将相应的套接字加入到相应的集合中。如果集合中的套接字有信号,select函数的返回值即为集合中有信号的套接字数量。 我们用下面的几个宏来操作fd_set集合。我们可以使用FD_SET(s, *set)将套接字s加入到集合set中;我们可以使用FD_CLR(s, *set)将套接字s移除出集合set;我们可以使用FD_ZERO(*set)将集合set清空;最后,我们可以使用FD_ISSET(s, *set)来判断套接字s是否在集合中有信号。 接下来再让我们来看看select函数的三个集合参数readfds、writefds以及exceptfds。
readfds表示可读套接字的集合,可读套接字在三种情况下有信号出现:一、如果集合中有套接字处于监听状态,并且该套接字上有来自客户端的连接请求;二、如果集合中的套接字收到了send操作发送过来的数据;三、如果集合中的套接字被关闭、重置或者中断。
writefds表示可写套接字的集合,可写套接字在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接成功;二、可以用send操作向集合中的套接字写数据。
exceptfds表示错误套接字的集合,错误套接字在两种情况下有信号出现:一、集合中的套接字经过connect操作后,连接失败;二、有带外数据到来。
在我们了解了创建服务器和客户端程序的基础知识后,我们再来看看示例程序,以加深我们对知识的理解。
程序的运行结果如下所示:
下面是服务器程序的代码:
#include #include #include #include #pragma comment(lib, "ws2_32.lib") #define ASSERT assert using std::cin; using std::cout; using std::endl; using std::list; typedef list SocketList; typedef list::iterator SocketListIterator; static const int c_iPort = 10001; bool GraceClose(SOCKET *ps); int main() { int iRet = SOCKET_ERROR; // 初始化Winsocket,所有Winsocket程序必须先使用WSAStartup进行初始化 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; // 初始化地址格式,只能为AF_INET adrServ.sin_port = htons(c_iPort); // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 adrServ.sin_addr.s_addr = INADDR_ANY; // 初始化IP,由于是服务器程序,所以可以将INADDR_ANY赋给该字段,表示任意的IP // 绑定监听套接字到本地 iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in)); ASSERT(SOCKET_ERROR != iRet); // 使用监听套接字进行监听 iRet = listen(skListen, FD_SETSIZE); // SOMAXCONN表示可以连接到该程序的最大连接数 ASSERT(SOCKET_ERROR != iRet); cout 0) { sockaddr_in adrClt; int iLen = sizeof(sockaddr_in); ZeroMemory(&adrClt, iLen); SOCKET s = accept(skListen, (sockaddr*)&adrClt,&iLen); ASSERT(INVALID_SOCKET != s); sl.push_back(s); cout socket is " 0) { for(SocketListIterator iter = sl.begin(); iter != sl.end(); ++iter) { // 如果有数据可读, 则遍历套接字列表中的所有套接字 // 检测出有数据可读的套接字 iRet = FD_ISSET(*iter, &fsRead); if(iRet > 0) { // 读取套接字上的数据 const int c_iBufLen = 512; char szBuf[c_iBufLen + 1] = {'\0'}; int iRead = SOCKET_ERROR; iRead = recv(*iter, szBuf, c_iBufLen, 0); if (0 >= iRead)// 读取出现错误或者对方关闭连接 { iRead == 0 ? cout socket " socket " socket " 发送数据 iRet = select(1, NULL, &fsWrite, NULL, &tv); if (0 socket " 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; }
服务器程序的重点是我们需要将接受自客户端程序的套接字加入到一个链表中,以方便我们的管理。 FD_SET(skListen, &fsListen); iRet = select(1, &fsListen, NULL, NULL, &tv); if(iRet > 0) { sockaddr_in adrClt; int iLen = sizeof(sockaddr_in); ZeroMemory(&adrClt, iLen); SOCKET s = accept(skListen, (sockaddr*)&adrClt,&iLen); ASSERT(INVALID_SOCKET != s); sl.push_back(s); cout socket is " = iRead)// 读取出现错误或者对方关闭连接 { iRead == 0 ? cout socket " socket " #include #include #pragma comment(lib, "ws2_32.lib") #define ASSERT assert using std::cin; using std::cout; using std::endl; 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,所有Winsocket程序必须先使用WSAStartup进行初始化 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; // 初始化地址格式,只能为AF_INET adrServ.sin_port = htons(c_iPort); // 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 adrServ.sin_addr.s_addr = inet_addr(c_szIP); // 初始化IP, 由于网络字节顺序和主机字节顺序相反,所以必须使用inet_addr将主机字节顺序转换成网络字节顺序 // 将套接口从阻塞状态设置到非阻塞状态 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 返回值总是为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 返回值大于0 iRet = select(1, NULL, &fsWrite, NULL, &tv); if (0 > 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 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; } 客户端程序比较简单,并且在代码中已经有十分详细的注释了,所以就不在这里详细说明了。
好了,非阻塞式服务器和客户端程序就介绍到这里,下一集中将向大家介绍一下使用Windows消息机制构建socket程序的方法。