- 浏览: 57814 次
- 性别:
- 来自: 青岛
文章分类
最新评论
-
yening521:
...扯淡。。。
IOCP测试,客户端连接量达到15000没有问题。 -
neworiginou:
写得很好 谢谢~
读new、delete、指向连续空间的指针、数组、空间释放、空间申请[C++][内存管理] 有感 -
jackytan_2008:
多谢,我也试试!
IOCP测试,客户端连接量达到15000没有问题。 -
saybbckey123:
谢谢分享。。。。。。。。。看看
IOCP测试,客户端连接量达到15000没有问题。
WinSock2编程之打造完整的SOCKET池
IOCP编程 2010-02-15 22:46:34 阅读592 评论5 字号:大中小 订阅
在Winodows平台上,网络编程的主要接口就是WinSock,目前大多数的Windows平台上的WinSock平台已经升级到2.0版,简称为WinSock2。在WinSock2中扩展了很多很有用的Windows味很浓的SOCKET专用API,为Windows平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley”套接字模型的API了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到“尤里平台”(我给Unix +Linux平台的简称)下,反过来因为Windows平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到Windows平台下。
如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于Windows的程序,都鼓励使用WinSock2扩展的一些API,更鼓励使用IOCP模型,因为这个模型是目前Windows平台上比较完美的一个高性能IO编程模型,它不但适用于SOCKET编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合Windows线程池,IOCP几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等等),本身具有先天的扩展性和可用性。
今天讨论的重点就是SOCKET池。很多VC程序员也许对SOCKET池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。
在Windows平台上SOCKET实际上被视作一个内核对象的句柄,很多Windows API在支持传统的HANDLE参数的同时也支持SOCKET,比如有名的CreateIoCompletionPort就支持将SOCKET句柄代替HANDLE参数传入并调用。熟悉Windows内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个SOCKET句柄,实际就是在系统内部创建了一个内核对象,对于Windows来说这牵扯到从Ring3层到Ring0层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个SOCKET内核对象的成本还是蛮高的。尤其对于一些面向连接的SOCKET应用,服务端往往要管理n多个代表客户端通信的SOCKET对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁SOCKET句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁SOCKET的性能代价立刻就会体现出来,典型的例如WEB服务器程序,就是一个需要频繁创建和销毁SOCKET句柄的SOCKET应用。这种情况下我们通常都希望对于断开的SOCKET对象,不是简单的“销毁”了之(很多时候“断开”的含义不一定就等价于“销毁”,可以仔细思考一下),更多时候希望能够重用这个SOCKET对象,这样我们甚至可以事先创建一批SOCKET对象组成一个“池”,在需要的时候“重用”其中的SOCKET对象,不需要的时候将SOCKET对象重新丢入池中即可,这样就省去了频繁创建销毁SOCKET对象的性能损失。在原始的“Berkeley”套接字模型中,想做到这点是没有什么办法的。而幸运的是在Windows平台上,尤其是支持WinSock2的平台上,已经提供了一套完整的API接口用于支持SOCKET池。
对于符合以上要求的SOCKET池,首先需要做到的就是对SOCKET句柄的“回收”,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的WinSock函数就是TransmitFile,如果代替closesocket函数像下面这样调用就可以“回收”一个SOCKET句柄,而不是销毁:(注意“回收”这个功能对于TransmitFile函数来说只是个“副业”。)
TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );
注意上面函数的最后一个参数,使用了标志TF_DISCONNECT和TF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示“重用”实际上也就是回收这个SOCKET,经过这个处理的SOCKET句柄,就可以直接再用于connect等操作,但是此时我们会发现,这个回收来的SOCKET似乎没什么用,因为其他套接字函数没法直接利用这个回收来的SOCKET句柄。
这时就要WinSock2的一组专用API上场了。我将它们按传统意义上的服务端和客户端分为两组:
一、 服务端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
二、 客户端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL PASCAL ConnectEx(
__in SOCKET s,
__in const struct sockaddr* name,
__in int namelen,
__in_opt PVOID lpSendBuffer,
__in DWORD dwSendDataLength,
__out LPDWORD lpdwBytesSent,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数“大同小异”,其实仔细观察他们的参数,就已经可以发现一些调用他们的“玄机”了。
首先我们来看AcceptEx函数,与accept函数不同,它需要两个SOCKET句柄作为参数,头一个参数的含义与accept函数的相同,而第二个参数的意思就是accept函数返回的那个代表与客户端通信的SOCKET句柄,在传统的accept内部,实际在返回那个代表客户端的SOCKET时,是在内部调用了一个SOCKET的创建动作,先创建这个SOCKET然后再“accept”让它变成代表客户端连接的SOCKET,而AcceptEx函数就在这里“扩展”(实际上是“阉割”才对)accept函数,省去了内部那个明显的创建SOCKET的动作,而将这个创建动作交给最终的调用者自己来实现。AcceptEx要求调用者创建好那个sAcceptSocket句柄然后传进去,这时我们立刻发现,我们回收的那个SOCKET是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个“回收”来的SOCKET句柄,最终实现服务端的SOCKET重用。
这里需要注意的就是,AcceptEx函数必须工作在非阻塞的IOCP模型下,同时即使AcceptEx函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的“完成通知”才能知道这个事实,这也是AcceptEx函数区别于accept这个阻塞方式函数的最大之处。通常可以利用AcceptEx的非阻塞特性和IOCP模型的优点,一次可以“预先”发出成千上万个AcceptEx调用,“等待”客户端的连接。对于习惯了accept阻塞方式的程序员来说,理解AcceptEx的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个AcceptEx:
//批量创建SOCKET,并调用对应的AcceptEx
for(UINT i = 0; i < 1000; i++)
{//调用1000次
//创建与客户端通讯的SOCKET,注意SOCKET的创建方式
skAccept = ::WSASocket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP,
NULL,
0,
WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == skAccept)
{
throw CGRSException((DWORD)WSAGetLastError());
}
//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用
pAcceptOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skAccept,NULL);
pAddrBuf = pAcceptOL->GetAddrBuf();
//4、发出AcceptEx调用
//注意将AcceptEx函数接收连接数据缓冲的大小设定成了0,这将导致此函数立即返回,虽然与
//不设定成0的方式而言,这导致了一个较低下的效率,但是这样提高了安全性,所以这种效率
//牺牲是必须的
if(!AcceptEx(m_skServer,
skAccept,
pAddrBuf->m_pBuf,
0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击
GRS_ADDRBUF_SIZE,
GRS_ADDRBUF_SIZE,
NULL,
(LPOVERLAPPED)pAcceptOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
if(INVALID_SOCKET != skAccept)
{
::closesocket(skAccept);
skAccept = INVALID_SOCKET;
}
if( NULL != pAcceptOL)
{
GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));
delete pAcceptOL;
pAcceptOL = NULL;
}
}
}
}
以上的例子只是简单的演示了AcceptEx的调用,还没有涉及到真正的“回收重用”这个主题,那么下面的例子就演示了如何重用一个SOCKET句柄:
if(INVALID_SOCKET == skClient)
{
throw CGRSException(_T("SOCKET句柄是无效的!"));
}
OnPreDisconnected(skClient,pUseData,0);
CGRSOverlappedData*pData
= new GRSOverlappedData(GRS_OP_DISCONNECTEX
,this,skClient,pUseData);
//回收而不是关闭后再创建大大提高了服务器的性能
DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0);
......
//在接收到DisconnectEx函数的完成通知之后,我们就可以重用这个SOCKET了
CGRSAddrbuf*pBuf = NULL;
pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skClient,pUseData);
pBuf = pNewOL->GetAddrBuf();
//把这个回收的SOCKET重新丢进连接池
if(!AcceptEx(m_skServer,skClient,pBuf->m_pBuf,
0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击
GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE,
NULL,(LPOVERLAPPED)pNewOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
throw CGRSException((DWORD)iError);
}
}
//注意在这个SOCKET被重新利用后,重新与IOCP绑定一下,该操作会返回一个已设置的错误,这个错误直接被忽略即可
::BindIoCompletionCallback((HANDLE)skClient
,Server_IOCPThread, 0);
至此回收重用SOCKET的工作也就结束了,以上的过程实际理解了IOCP之后就比较好理解了,例子的最后我们使用了BindIoCompletionCallback函数重新将SOCKET丢进了IOCP线程池中,实际还可以再次使用CreateIoCompletionPort函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和SOCKET。
对于客户端来说,可以使用ConnectEx函数来代替connect函数,与AcceptEx函数相同,ConnectEx函数也是以非阻塞的IOCP方式工作的,唯一要注意的就是在WSASocket调用之后,在ConnectEx之前要调用一下bind函数,将SOCKET提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是ConnectEx较之connect函数高效的地方之一。
与AcceptEx函数类似,也可以一次发出成千上万个ConnectEx函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的sockaddr即可。
通过上面的例子和讲解,大家应该对SOCKET池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了IOCP模型,否则还是寸步难行。
在上面的例子中,回收SOCKET句柄主要使用了DisconnectEx函数,而不是之前介绍的TransmitFile函数,为什么呢?因为TransmitFile函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用SOCKET函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx函数这个专用的回收函数,调用之后发现比TransmitFile专业多了,而且不管怎样都不会死锁。
最后需要补充的就是这几个函数的调用方式,不能像传统的SOCKET API那样直接调用它们,而需要使用一种间接的方式来调用,尤其是AcceptEx和DisconnectEx函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:
class CGRSMsSockFun
{
public:
CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)
{
if( INVALID_SOCKET != skTemp )
{
LoadAllFun(skTemp);
}
}
public:
virtual ~CGRSMsSockFun(void)
{
}
protected:
BOOL LoadWSAFun(SOCKET& skTemp,GUID&funGuid,void*&pFun)
{
DWORD dwBytes = 0;
BOOL bRet = TRUE;
pFun = NULL;
BOOL bCreateSocket = FALSE;
try
{
if(INVALID_SOCKET == skTemp)
{
skTemp = ::WSASocket(AF_INET,SOCK_STREAM,
IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
bCreateSocket = (skTemp != INVALID_SOCKET);
}
if(INVALID_SOCKET == skTemp)
{
throw CGRSException((DWORD)WSAGetLastError());
}
if(SOCKET_ERROR == ::WSAIoctl(skTemp,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&funGuid,sizeof(funGuid),
&pFun,sizeof(pFun),&dwBytes,NULL,
NULL))
{
pFun = NULL;
throw CGRSException((DWORD)WSAGetLastError());
}
}
catch(CGRSException& e)
{
if(bCreateSocket)
{
::closesocket(skTemp);
}
}
return NULL != pFun;
}
protected:
LPFN_ACCEPTEX m_pfnAcceptEx;
LPFN_CONNECTEX m_pfnConnectEx;
LPFN_DISCONNECTEX m_pfnDisconnectEx;
LPFN_GETACCEPTEXSOCKADDRS m_pfnGetAcceptExSockaddrs;
LPFN_TRANSMITFILE m_pfnTransmitfile;
LPFN_TRANSMITPACKETS m_pfnTransmitPackets;
LPFN_WSARECVMSG m_pfnWSARecvMsg;
protected:
BOOL LoadAcceptExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_ACCEPTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnAcceptEx);
}
BOOL LoadConnectExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_CONNECTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnConnectEx);
}
BOOL LoadDisconnectExFun(SOCKET&skTemp)
{
GUID GuidDisconnectEx = WSAID_DISCONNECTEX;
return LoadWSAFun(skTemp,GuidDisconnectEx
,(void*&)m_pfnDisconnectEx);
}
BOOL LoadGetAcceptExSockaddrsFun(SOCKET &skTemp)
{
GUID GuidGetAcceptExSockaddrs
= WSAID_GETACCEPTEXSOCKADDRS;
return LoadWSAFun(skTemp,GuidGetAcceptExSockaddrs
,(void*&)m_pfnGetAcceptExSockaddrs);
}
BOOL LoadTransmitFileFun(SOCKET&skTemp)
{
GUID GuidTransmitFile = WSAID_TRANSMITFILE;
return LoadWSAFun(skTemp,GuidTransmitFile
,(void*&)m_pfnTransmitfile);
}
BOOL LoadTransmitPacketsFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnTransmitPackets);
}
BOOL LoadWSARecvMsgFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnWSARecvMsg);
}
public:
BOOL LoadAllFun(SOCKET skTemp)
{//注意这个地方的调用顺序,是根据服务器的需要,并结合了表达式副作用
//而特意安排的调用顺序
return (LoadAcceptExFun(skTemp) &&
LoadGetAcceptExSockaddrsFun(skTemp) &&
LoadTransmitFileFun(skTemp) &&
LoadTransmitPacketsFun(skTemp) &&
LoadDisconnectExFun(skTemp) &&
LoadConnectExFun(skTemp) &&
LoadWSARecvMsgFun(skTemp));
}
public:
GRS_FORCEINLINE BOOL AcceptEx (
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnAcceptEx);
return m_pfnAcceptEx(sListenSocket,
sAcceptSocket,lpOutputBuffer,
dwReceiveDataLength,dwLocalAddressLength,
dwRemoteAddressLength,lpdwBytesReceived,
lpOverlapped);
}
GRS_FORCEINLINE BOOL ConnectEx(
SOCKET s,const struct sockaddr FAR *name,
int namelen,PVOID lpSendBuffer,
DWORD dwSendDataLength,LPDWORD lpdwBytesSent,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnConnectEx);
return m_pfnConnectEx(
s,name,namelen,lpSendBuffer,
dwSendDataLength,lpdwBytesSent,
lpOverlapped
);
}
GRS_FORCEINLINE BOOL DisconnectEx(
SOCKET s,LPOVERLAPPED lpOverlapped,
DWORD dwFlags,DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnDisconnectEx);
return m_pfnDisconnectEx(s,
lpOverlapped,dwFlags,dwReserved);
}
GRS_FORCEINLINE VOID GetAcceptExSockaddrs (
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
sockaddr **LocalSockaddr,
LPINT LocalSockaddrLength,
sockaddr **RemoteSockaddr,
LPINT RemoteSockaddrLength
)
{
GRS_ASSERT(NULL != m_pfnGetAcceptExSockaddrs);
return m_pfnGetAcceptExSockaddrs(
lpOutputBuffer,dwReceiveDataLength,
dwLocalAddressLength,dwRemoteAddressLength,
LocalSockaddr,LocalSockaddrLength,
RemoteSockaddr,RemoteSockaddrLength
);
}
GRS_FORCEINLINE BOOL TransmitFile(
SOCKET hSocket,HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnTransmitfile);
return m_pfnTransmitfile(
hSocket,hFile,nNumberOfBytesToWrite,
nNumberOfBytesPerSend,lpOverlapped,
lpTransmitBuffers,dwReserved
);
}
GRS_FORCEINLINE BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,
DWORD nElementCount,DWORD nSendSize,
LPOVERLAPPED lpOverlapped,DWORD dwFlags
)
{
GRS_ASSERT(NULL != m_pfnTransmitPackets);
return m_pfnTransmitPackets(
hSocket,lpPacketArray,nElementCount,
nSendSize,lpOverlapped,dwFlags
);
}
GRS_FORCEINLINE INT WSARecvMsg(
SOCKET s,LPWSAMSG lpMsg,
LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
)
{
GRS_ASSERT(NULL != m_pfnWSARecvMsg);
return m_pfnWSARecvMsg(
s,lpMsg,lpdwNumberOfBytesRecvd,
lpOverlapped,lpCompletionRoutine
);
}
/*WSAID_ACCEPTEX
WSAID_CONNECTEX
WSAID_DISCONNECTEX
WSAID_GETACCEPTEXSOCKADDRS
WSAID_TRANSMITFILE
WSAID_TRANSMITPACKETS
WSAID_WSARECVMSG
WSAID_WSASENDMSG */
};
这个类的使用非常简单,只需要声明一个类的对象,然后调用其成员AcceptEx、DisconnectEx函数等即可,参数与这些函数的MSDN声明方式完全相同,除了本文中介绍的这些函数外,这个类还包含了很多其他的Winsock2函数,那么都应该按照这个类中演示的这样来动态载入后再行调用,如果无法载入通常说明你的环境中没有Winsock2函数库,或者是你初始化的不是2.0版的Winsock环境。
这个类是本人完整类库的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什么问题,可以直接跟帖说明,我会不定期回答大家的问题,这个类可以免费使用、分发、修改,可以用于任何商业目的,但是对于使用后引起的任何问题,本人概不负责,有问题请跟帖。关于AcceptEx以及其他一些函数,包括本文中没有介绍到得函数,我会在后续的一些专题文章中进行详细深入的介绍,敬请期待。如果你有什么疑问,或者想要了解什么也请跟帖说明,我会在后面的文章中尽量说明。
IOCP编程 2010-02-15 22:46:34 阅读592 评论5 字号:大中小 订阅
在Winodows平台上,网络编程的主要接口就是WinSock,目前大多数的Windows平台上的WinSock平台已经升级到2.0版,简称为WinSock2。在WinSock2中扩展了很多很有用的Windows味很浓的SOCKET专用API,为Windows平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley”套接字模型的API了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到“尤里平台”(我给Unix +Linux平台的简称)下,反过来因为Windows平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到Windows平台下。
如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于Windows的程序,都鼓励使用WinSock2扩展的一些API,更鼓励使用IOCP模型,因为这个模型是目前Windows平台上比较完美的一个高性能IO编程模型,它不但适用于SOCKET编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合Windows线程池,IOCP几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等等),本身具有先天的扩展性和可用性。
今天讨论的重点就是SOCKET池。很多VC程序员也许对SOCKET池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。
在Windows平台上SOCKET实际上被视作一个内核对象的句柄,很多Windows API在支持传统的HANDLE参数的同时也支持SOCKET,比如有名的CreateIoCompletionPort就支持将SOCKET句柄代替HANDLE参数传入并调用。熟悉Windows内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个SOCKET句柄,实际就是在系统内部创建了一个内核对象,对于Windows来说这牵扯到从Ring3层到Ring0层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个SOCKET内核对象的成本还是蛮高的。尤其对于一些面向连接的SOCKET应用,服务端往往要管理n多个代表客户端通信的SOCKET对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁SOCKET句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁SOCKET的性能代价立刻就会体现出来,典型的例如WEB服务器程序,就是一个需要频繁创建和销毁SOCKET句柄的SOCKET应用。这种情况下我们通常都希望对于断开的SOCKET对象,不是简单的“销毁”了之(很多时候“断开”的含义不一定就等价于“销毁”,可以仔细思考一下),更多时候希望能够重用这个SOCKET对象,这样我们甚至可以事先创建一批SOCKET对象组成一个“池”,在需要的时候“重用”其中的SOCKET对象,不需要的时候将SOCKET对象重新丢入池中即可,这样就省去了频繁创建销毁SOCKET对象的性能损失。在原始的“Berkeley”套接字模型中,想做到这点是没有什么办法的。而幸运的是在Windows平台上,尤其是支持WinSock2的平台上,已经提供了一套完整的API接口用于支持SOCKET池。
对于符合以上要求的SOCKET池,首先需要做到的就是对SOCKET句柄的“回收”,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的WinSock函数就是TransmitFile,如果代替closesocket函数像下面这样调用就可以“回收”一个SOCKET句柄,而不是销毁:(注意“回收”这个功能对于TransmitFile函数来说只是个“副业”。)
TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );
注意上面函数的最后一个参数,使用了标志TF_DISCONNECT和TF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示“重用”实际上也就是回收这个SOCKET,经过这个处理的SOCKET句柄,就可以直接再用于connect等操作,但是此时我们会发现,这个回收来的SOCKET似乎没什么用,因为其他套接字函数没法直接利用这个回收来的SOCKET句柄。
这时就要WinSock2的一组专用API上场了。我将它们按传统意义上的服务端和客户端分为两组:
一、 服务端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
二、 客户端:
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
BOOL PASCAL ConnectEx(
__in SOCKET s,
__in const struct sockaddr* name,
__in int namelen,
__in_opt PVOID lpSendBuffer,
__in DWORD dwSendDataLength,
__out LPDWORD lpdwBytesSent,
__in LPOVERLAPPED lpOverlapped
);
BOOL DisconnectEx(
__in SOCKET hSocket,
__in LPOVERLAPPED lpOverlapped,
__in DWORD dwFlags,
__in DWORD reserved
);
注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数“大同小异”,其实仔细观察他们的参数,就已经可以发现一些调用他们的“玄机”了。
首先我们来看AcceptEx函数,与accept函数不同,它需要两个SOCKET句柄作为参数,头一个参数的含义与accept函数的相同,而第二个参数的意思就是accept函数返回的那个代表与客户端通信的SOCKET句柄,在传统的accept内部,实际在返回那个代表客户端的SOCKET时,是在内部调用了一个SOCKET的创建动作,先创建这个SOCKET然后再“accept”让它变成代表客户端连接的SOCKET,而AcceptEx函数就在这里“扩展”(实际上是“阉割”才对)accept函数,省去了内部那个明显的创建SOCKET的动作,而将这个创建动作交给最终的调用者自己来实现。AcceptEx要求调用者创建好那个sAcceptSocket句柄然后传进去,这时我们立刻发现,我们回收的那个SOCKET是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个“回收”来的SOCKET句柄,最终实现服务端的SOCKET重用。
这里需要注意的就是,AcceptEx函数必须工作在非阻塞的IOCP模型下,同时即使AcceptEx函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的“完成通知”才能知道这个事实,这也是AcceptEx函数区别于accept这个阻塞方式函数的最大之处。通常可以利用AcceptEx的非阻塞特性和IOCP模型的优点,一次可以“预先”发出成千上万个AcceptEx调用,“等待”客户端的连接。对于习惯了accept阻塞方式的程序员来说,理解AcceptEx的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个AcceptEx:
//批量创建SOCKET,并调用对应的AcceptEx
for(UINT i = 0; i < 1000; i++)
{//调用1000次
//创建与客户端通讯的SOCKET,注意SOCKET的创建方式
skAccept = ::WSASocket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP,
NULL,
0,
WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == skAccept)
{
throw CGRSException((DWORD)WSAGetLastError());
}
//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用
pAcceptOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skAccept,NULL);
pAddrBuf = pAcceptOL->GetAddrBuf();
//4、发出AcceptEx调用
//注意将AcceptEx函数接收连接数据缓冲的大小设定成了0,这将导致此函数立即返回,虽然与
//不设定成0的方式而言,这导致了一个较低下的效率,但是这样提高了安全性,所以这种效率
//牺牲是必须的
if(!AcceptEx(m_skServer,
skAccept,
pAddrBuf->m_pBuf,
0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击
GRS_ADDRBUF_SIZE,
GRS_ADDRBUF_SIZE,
NULL,
(LPOVERLAPPED)pAcceptOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
if(INVALID_SOCKET != skAccept)
{
::closesocket(skAccept);
skAccept = INVALID_SOCKET;
}
if( NULL != pAcceptOL)
{
GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));
delete pAcceptOL;
pAcceptOL = NULL;
}
}
}
}
以上的例子只是简单的演示了AcceptEx的调用,还没有涉及到真正的“回收重用”这个主题,那么下面的例子就演示了如何重用一个SOCKET句柄:
if(INVALID_SOCKET == skClient)
{
throw CGRSException(_T("SOCKET句柄是无效的!"));
}
OnPreDisconnected(skClient,pUseData,0);
CGRSOverlappedData*pData
= new GRSOverlappedData(GRS_OP_DISCONNECTEX
,this,skClient,pUseData);
//回收而不是关闭后再创建大大提高了服务器的性能
DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0);
......
//在接收到DisconnectEx函数的完成通知之后,我们就可以重用这个SOCKET了
CGRSAddrbuf*pBuf = NULL;
pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT
,this,skClient,pUseData);
pBuf = pNewOL->GetAddrBuf();
//把这个回收的SOCKET重新丢进连接池
if(!AcceptEx(m_skServer,skClient,pBuf->m_pBuf,
0,//将接收缓冲置为0,令AcceptEx直接返回,防止拒绝服务攻击
GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE,
NULL,(LPOVERLAPPED)pNewOL))
{
int iError = WSAGetLastError();
if( ERROR_IO_PENDING != iError
&& WSAECONNRESET != iError )
{
throw CGRSException((DWORD)iError);
}
}
//注意在这个SOCKET被重新利用后,重新与IOCP绑定一下,该操作会返回一个已设置的错误,这个错误直接被忽略即可
::BindIoCompletionCallback((HANDLE)skClient
,Server_IOCPThread, 0);
至此回收重用SOCKET的工作也就结束了,以上的过程实际理解了IOCP之后就比较好理解了,例子的最后我们使用了BindIoCompletionCallback函数重新将SOCKET丢进了IOCP线程池中,实际还可以再次使用CreateIoCompletionPort函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和SOCKET。
对于客户端来说,可以使用ConnectEx函数来代替connect函数,与AcceptEx函数相同,ConnectEx函数也是以非阻塞的IOCP方式工作的,唯一要注意的就是在WSASocket调用之后,在ConnectEx之前要调用一下bind函数,将SOCKET提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是ConnectEx较之connect函数高效的地方之一。
与AcceptEx函数类似,也可以一次发出成千上万个ConnectEx函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的sockaddr即可。
通过上面的例子和讲解,大家应该对SOCKET池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了IOCP模型,否则还是寸步难行。
在上面的例子中,回收SOCKET句柄主要使用了DisconnectEx函数,而不是之前介绍的TransmitFile函数,为什么呢?因为TransmitFile函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用SOCKET函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx函数这个专用的回收函数,调用之后发现比TransmitFile专业多了,而且不管怎样都不会死锁。
最后需要补充的就是这几个函数的调用方式,不能像传统的SOCKET API那样直接调用它们,而需要使用一种间接的方式来调用,尤其是AcceptEx和DisconnectEx函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:
class CGRSMsSockFun
{
public:
CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)
{
if( INVALID_SOCKET != skTemp )
{
LoadAllFun(skTemp);
}
}
public:
virtual ~CGRSMsSockFun(void)
{
}
protected:
BOOL LoadWSAFun(SOCKET& skTemp,GUID&funGuid,void*&pFun)
{
DWORD dwBytes = 0;
BOOL bRet = TRUE;
pFun = NULL;
BOOL bCreateSocket = FALSE;
try
{
if(INVALID_SOCKET == skTemp)
{
skTemp = ::WSASocket(AF_INET,SOCK_STREAM,
IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
bCreateSocket = (skTemp != INVALID_SOCKET);
}
if(INVALID_SOCKET == skTemp)
{
throw CGRSException((DWORD)WSAGetLastError());
}
if(SOCKET_ERROR == ::WSAIoctl(skTemp,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&funGuid,sizeof(funGuid),
&pFun,sizeof(pFun),&dwBytes,NULL,
NULL))
{
pFun = NULL;
throw CGRSException((DWORD)WSAGetLastError());
}
}
catch(CGRSException& e)
{
if(bCreateSocket)
{
::closesocket(skTemp);
}
}
return NULL != pFun;
}
protected:
LPFN_ACCEPTEX m_pfnAcceptEx;
LPFN_CONNECTEX m_pfnConnectEx;
LPFN_DISCONNECTEX m_pfnDisconnectEx;
LPFN_GETACCEPTEXSOCKADDRS m_pfnGetAcceptExSockaddrs;
LPFN_TRANSMITFILE m_pfnTransmitfile;
LPFN_TRANSMITPACKETS m_pfnTransmitPackets;
LPFN_WSARECVMSG m_pfnWSARecvMsg;
protected:
BOOL LoadAcceptExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_ACCEPTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnAcceptEx);
}
BOOL LoadConnectExFun(SOCKET &skTemp)
{
GUID GuidAcceptEx = WSAID_CONNECTEX;
return LoadWSAFun(skTemp,GuidAcceptEx
,(void*&)m_pfnConnectEx);
}
BOOL LoadDisconnectExFun(SOCKET&skTemp)
{
GUID GuidDisconnectEx = WSAID_DISCONNECTEX;
return LoadWSAFun(skTemp,GuidDisconnectEx
,(void*&)m_pfnDisconnectEx);
}
BOOL LoadGetAcceptExSockaddrsFun(SOCKET &skTemp)
{
GUID GuidGetAcceptExSockaddrs
= WSAID_GETACCEPTEXSOCKADDRS;
return LoadWSAFun(skTemp,GuidGetAcceptExSockaddrs
,(void*&)m_pfnGetAcceptExSockaddrs);
}
BOOL LoadTransmitFileFun(SOCKET&skTemp)
{
GUID GuidTransmitFile = WSAID_TRANSMITFILE;
return LoadWSAFun(skTemp,GuidTransmitFile
,(void*&)m_pfnTransmitfile);
}
BOOL LoadTransmitPacketsFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnTransmitPackets);
}
BOOL LoadWSARecvMsgFun(SOCKET&skTemp)
{
GUID GuidTransmitPackets = WSAID_TRANSMITPACKETS;
return LoadWSAFun(skTemp,GuidTransmitPackets
,(void*&)m_pfnWSARecvMsg);
}
public:
BOOL LoadAllFun(SOCKET skTemp)
{//注意这个地方的调用顺序,是根据服务器的需要,并结合了表达式副作用
//而特意安排的调用顺序
return (LoadAcceptExFun(skTemp) &&
LoadGetAcceptExSockaddrsFun(skTemp) &&
LoadTransmitFileFun(skTemp) &&
LoadTransmitPacketsFun(skTemp) &&
LoadDisconnectExFun(skTemp) &&
LoadConnectExFun(skTemp) &&
LoadWSARecvMsgFun(skTemp));
}
public:
GRS_FORCEINLINE BOOL AcceptEx (
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnAcceptEx);
return m_pfnAcceptEx(sListenSocket,
sAcceptSocket,lpOutputBuffer,
dwReceiveDataLength,dwLocalAddressLength,
dwRemoteAddressLength,lpdwBytesReceived,
lpOverlapped);
}
GRS_FORCEINLINE BOOL ConnectEx(
SOCKET s,const struct sockaddr FAR *name,
int namelen,PVOID lpSendBuffer,
DWORD dwSendDataLength,LPDWORD lpdwBytesSent,
LPOVERLAPPED lpOverlapped
)
{
GRS_ASSERT(NULL != m_pfnConnectEx);
return m_pfnConnectEx(
s,name,namelen,lpSendBuffer,
dwSendDataLength,lpdwBytesSent,
lpOverlapped
);
}
GRS_FORCEINLINE BOOL DisconnectEx(
SOCKET s,LPOVERLAPPED lpOverlapped,
DWORD dwFlags,DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnDisconnectEx);
return m_pfnDisconnectEx(s,
lpOverlapped,dwFlags,dwReserved);
}
GRS_FORCEINLINE VOID GetAcceptExSockaddrs (
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
sockaddr **LocalSockaddr,
LPINT LocalSockaddrLength,
sockaddr **RemoteSockaddr,
LPINT RemoteSockaddrLength
)
{
GRS_ASSERT(NULL != m_pfnGetAcceptExSockaddrs);
return m_pfnGetAcceptExSockaddrs(
lpOutputBuffer,dwReceiveDataLength,
dwLocalAddressLength,dwRemoteAddressLength,
LocalSockaddr,LocalSockaddrLength,
RemoteSockaddr,RemoteSockaddrLength
);
}
GRS_FORCEINLINE BOOL TransmitFile(
SOCKET hSocket,HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwReserved
)
{
GRS_ASSERT(NULL != m_pfnTransmitfile);
return m_pfnTransmitfile(
hSocket,hFile,nNumberOfBytesToWrite,
nNumberOfBytesPerSend,lpOverlapped,
lpTransmitBuffers,dwReserved
);
}
GRS_FORCEINLINE BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,
DWORD nElementCount,DWORD nSendSize,
LPOVERLAPPED lpOverlapped,DWORD dwFlags
)
{
GRS_ASSERT(NULL != m_pfnTransmitPackets);
return m_pfnTransmitPackets(
hSocket,lpPacketArray,nElementCount,
nSendSize,lpOverlapped,dwFlags
);
}
GRS_FORCEINLINE INT WSARecvMsg(
SOCKET s,LPWSAMSG lpMsg,
LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
)
{
GRS_ASSERT(NULL != m_pfnWSARecvMsg);
return m_pfnWSARecvMsg(
s,lpMsg,lpdwNumberOfBytesRecvd,
lpOverlapped,lpCompletionRoutine
);
}
/*WSAID_ACCEPTEX
WSAID_CONNECTEX
WSAID_DISCONNECTEX
WSAID_GETACCEPTEXSOCKADDRS
WSAID_TRANSMITFILE
WSAID_TRANSMITPACKETS
WSAID_WSARECVMSG
WSAID_WSASENDMSG */
};
这个类的使用非常简单,只需要声明一个类的对象,然后调用其成员AcceptEx、DisconnectEx函数等即可,参数与这些函数的MSDN声明方式完全相同,除了本文中介绍的这些函数外,这个类还包含了很多其他的Winsock2函数,那么都应该按照这个类中演示的这样来动态载入后再行调用,如果无法载入通常说明你的环境中没有Winsock2函数库,或者是你初始化的不是2.0版的Winsock环境。
这个类是本人完整类库的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什么问题,可以直接跟帖说明,我会不定期回答大家的问题,这个类可以免费使用、分发、修改,可以用于任何商业目的,但是对于使用后引起的任何问题,本人概不负责,有问题请跟帖。关于AcceptEx以及其他一些函数,包括本文中没有介绍到得函数,我会在后续的一些专题文章中进行详细深入的介绍,敬请期待。如果你有什么疑问,或者想要了解什么也请跟帖说明,我会在后面的文章中尽量说明。
发表评论
-
IOCP中在WSASend以及WSARecv的时候出现WSA_IO_PENDING情况的说明
2011-03-15 10:32 1734IOCP中在WSASend以及WSARecv的时候出现WSA_ ... -
关于完成端口(IOCP)的文章
2010-10-03 16:38 2204关于完成端口(IOCP)的文章汇总 - [C/C++] 版权声 ... -
异步IO.APC.IOCP.线程池
2010-09-30 10:01 1853异步IO.APC.IOCP.线程池 异步IO、AP ... -
WaitForSingleObject的用法
2010-09-18 11:24 1189WaitForSingleObject的用法(2006-10- ... -
CONTAINING_RECORD macro之研究
2010-09-18 09:02 1728CONTAINING_RECORD macro之研究 分享 标 ... -
如何优雅地关闭一个socket
2010-09-17 17:06 1911如何优雅地关闭一个socket 收藏 最近在wind ... -
setsockopt 设置socket 详细用法
2010-09-17 15:54 1185setsockopt 设置socket 详细用法 1.clo ... -
Visual C++线程同步技术剖析:临界区,时间,信号量,互斥量
2010-08-13 17:13 1221Visual C++线程同步技术剖 ...
相关推荐
### 打造完整的Socket池 #### 一、引言与背景 随着互联网技术的发展和用户需求的多样化,网络编程已成为软件开发中不可或缺的一部分。在Windows平台上,网络编程的主要接口是WinSock,特别是WinSock2,它提供了...
《用Winsock制作的局域网...通过理解Winsock的工作原理,掌握关键的编程步骤,可以打造出满足实际需求的文件传输工具。在这个过程中,开发者不仅需要深入理解网络编程,还需要具备良好的错误处理和用户体验设计能力。
在MFC中,Socket编程主要是基于Winsock库的封装,提供了类CAsyncSocket和CInternetSession等,简化了网络通信的实现。 1. **CAsyncSocket类**:它是MFC对Winsock API的抽象,用于实现TCP/IP协议的Socket编程。...
Socket编程是网络编程的基础,TCP(Transmission Control Protocol)则是一种面向连接的、可靠的传输协议,常用于文件传输、网页浏览等场景。我们将讲解如何创建Socket,建立TCP连接,以及发送和接收不同类型文件。 ...
在C++中,我们可以使用<winsock2.h>或<ws2tcpip.h>头文件来实现socket通信。在MFC项目中,我们通常会创建一个单独的线程来处理socket的读写操作,以免阻塞主线程,确保用户界面的流畅性。 然后,我们来谈谈AES加密...
2. **Winsock API**:Windows平台上的网络编程主要依赖于Winsock库,书中将详细介绍如何使用Winsock API进行数据传输、连接管理和错误处理等操作。 3. **TCP与UDP编程**:TCP(传输控制协议)和UDP(用户数据报协议...
在编程接口方面,UNIX/Linux系统提供了socket API,这是跨平台的网络编程接口。socket分为流式(SOCK_STREAM,对应TCP)和数据报(SOCK_DGRAM,对应UDP)两种类型。程序员通过创建socket,绑定地址,监听连接,接受...
打开国内外各大知名网站的招聘页面,都可以看到类似于“熟悉TCP/IP协议、掌握socket通讯开发”等字样的要求。本书就是为了满足读者在这方面知识的需求而编写的一本TCP/IP协议与基于TCP/IP编程方面的书籍。 本书有...
在网络编程方面,MFC提供了CSocket类,它是对Winsock API的封装,让开发者能方便地进行客户端和服务器端的交互。 在Chat应用中,我们将主要涉及以下MFC网络编程的关键概念和技术: 1. **CSocket类**:CSocket类是...
2. **系统级编程**:BCB提供对Win32 API的直接访问,允许开发者操作硬件设备、管理系统资源、实现底层进程间通信(IPC)以及编写系统服务。例如,通过CreateProcess函数创建新的进程,使用Mutex或Semaphore进行线程...
《网络编程与Visual C++实践:打造网络协议查看器》 网络编程是计算机科学中的一个重要领域,它涉及如何使计算机通过网络进行通信。在本文中,我们将深入探讨如何利用Microsoft的Visual C++ 6.0开发环境,结合...
在Visual C++中,我们通常通过包含winsock.h或ws2_32.lib来启用这些功能。 要创建BBS系统,首先要建立服务器端,这是整个系统的核心。服务器端需要监听特定端口上的连接请求,并处理来自客户端的数据。使用Visual ...
WinSock API 是 Windows 平台上用于实现 Socket 编程的标准接口,它提供了低级别的网络通信功能,可以用来实现数据传输、远程控制等功能。具体来看: - **WinSock 动态链接库的启动与释放**:在窗体创建和关闭时...
本项目基于Windows平台,使用Winsock API(Windows Socket)进行网络通信,旨在实现客户端与服务端之间的实时信息传输。 一、Windows网络编程基础 Windows网络编程主要依赖于Winsock库,它是对Berkeley套接字接口的...
2. 多线程编程:VC 6.0支持MFC(Microsoft Foundation Classes)库,提供了CWinThread类来实现线程。在FTP程序中,多线程可以用于同时进行多个文件的上传或下载,提高程序的并行处理能力。源码中应有创建、管理和...
在VS2010以上版本的Visual Studio中,可以利用Windows套接字API(Winsock)提供的WSAAsyncSelect或WSAEventSelect函数实现异步Socket编程。这两种方法都可以注册一个事件,当Socket有读写事件发生时,Windows会通知...
Winsock是微软为Windows操作系统精心打造的网络编程接口,支持包括TCP/IP在内的多种网络协议,其设计旨在提供标准化的方法,以便开发者可以编写可在不同平台间无缝迁移的网络应用。 IP多播与Winsock的结合,为...
2. Winsock库:Windows操作系统内置的网络编程接口,用于实现TCP/IP协议,是构建网络应用的基础。 三、主要步骤与实现细节 1. 创建项目与用户界面设计: 使用Visual C++创建一个新的MFC应用程序,设置主窗口布局...
1. **网络编程**:VC提供了Winsock库,它是Windows上的一个网络编程接口,用于实现TCP/IP协议栈。我们需用到的函数包括`socket()`用于创建套接字,`bind()`绑定IP地址和端口号,`listen()`开始监听连接,`accept()`...