1.采用重叠I/O方式实现的socket网络编程,异步非阻塞方式,代码效率比阻塞式的socket编程方式高。2.实现了TCP server方式,只用于服务端,可以支持多客户端。3.可以使用在各种场合用于监控网络数据。4.代码封装成库形式,非常方便移植。5.本程序使用到了多线程技术,互斥同步线程技术,同时支持多通道连接技术,非常经典,程序注释完整,思路清晰。6.平台使用的是VC6.0,语言用的是C++,MFC做的界面。使用ws2_32.lib实现的各种功能,并没有使用MFC的socket库。
先致敬博主小猪,相关理论知识可以看他的相关文章(https://blog.csdn.net/PiggyXP/article/details/114908)。贴代码前这里多说两句。重叠I/O方式比阻塞式的socket编程有两个好处,一个是重叠I/O采用异步非阻塞式,代码运行效率高,其二是一个线程可以支持64个客户端连接,不像阻塞socket编程,一个客户端连接就要对应一个线程,如果多客户端连接就要开很多线程,维持一个线程池。
支持多客户端的服务端程序比单连接的服务端程序复杂得多,要考虑很多点,特别是客户端的退出,再接入,需要耐心。现在开始上代码。
一、创建socket套接字,这个一般都一样:
//初始化WSA
WORD sockVersion = MAKEWORD(2,2);
WSADATA wsaData;
if( WSAStartup(sockVersion, &wsaData) != 0 ) //加载套接字版本
{
AfxMessageBox("load tcp server socket error !");
return 0;
}
AfxMessageBox("load tcp server socket successfully!");
//创建套接字
//SOCKET TcpSrvSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
pThread->m_sockarr[0] = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(pThread->m_sockarr[0] == INVALID_SOCKET)
{
AfxMessageBox("create tcp server socket error !");
return 0;
}
pThread->m_Srvsock = pThread->m_sockarr[0]; //传递套接字给主线程,用于关闭套接字。
pThread->m_sockFlag[0] = 1;
AfxMessageBox("create tcp server socket successfully !");
//绑定套接字
sockaddr_in TcpSrvAddr;
memset( &TcpSrvAddr,0,sizeof(TcpSrvAddr) );
TcpSrvAddr.sin_addr.S_un.S_addr = /*htonl(INADDR_ANY);*/inet_addr(pThread->m_LocalIpAddr);
TcpSrvAddr.sin_family = AF_INET;
TcpSrvAddr.sin_port = htons(pThread->m_LocalPort);
if( bind(pThread->m_sockarr[0],(SOCKADDR*)&TcpSrvAddr,sizeof(TcpSrvAddr)) == SOCKET_ERROR )
{
AfxMessageBox("bind tcp server socket error !");
return 0;
}
AfxMessageBox("bind tcp server socket successfully.");
//监听套接字
if( listen(pThread->m_sockarr[0],5) == SOCKET_ERROR )
{
AfxMessageBox("listen tcp server socket error !");
return 0;
}
AfxMessageBox("listen tcp server socket successfully.");
二、创建互斥对象,由于要用到两个线程,必须通过互斥对象来同步两个线程中的共享数据
/*
**创建互斥对象,使其一开始就具有信号状态。
*/
pThread->m_hMutex1 = CreateMutex(NULL,FALSE,NULL);
三、开始监听端口线程
3.1监听端口,建立accept套接字
//开始连接,等待客户端连接得到accept套接字
pThread->m_dwSocketTotal++;
tmpSock = accept(pThread->m_sockarr[0],(SOCKADDR*)&RemoteAddr,&nAddrlen); //blocking the thread until accept successfully.
3.2多客户端连接对应到具体通道
/*
**这里对处理通道数的删减很关键,思路如下:
**设定一个64元素的数组,下标从0 - 63,其中下标0固定对应bind套接字通道,下标1 - 63 分别代表63路accept套接字通道。
**我们知道每一个套接字对应一个客户端,从下标1开始标识套接字,比如连接了5个客户端,那么下标1-5对应的内容置1,
**当3号客户端关闭了,那么下标3对应的内容清零,下次有新的客户端连接,对应到下标3位置。
**m_dwSocketTotal代表建立的套接字数量,m_dwEventTotal代表建立的事件数量,
**SockChannel表示新建立的客户端套接字对应位置。
*/
for( SockChannel = 1; SockChannel < 64; SockChannel++ )
{
if( pThread->m_sockFlag[SockChannel] == 0 )
break;
}
pThread->m_sockarr[SockChannel] = tmpSock;
if(pThread->m_sockarr[SockChannel] == INVALID_SOCKET)
{
AfxMessageBox("accept tcp server socket error !");
return 0;
}
pThread->m_sock = pThread->m_sockarr[SockChannel]; //传递accept套接字给主线程,用于结束线程的时候close套接字。
pThread->m_sockFlag[SockChannel] = 1;
AfxMessageBox("accept tcp server socket successfully.");
3.3互斥对象同步
/*
**创建重叠数据结构,用于某一条链路的提交接收数据和发送数据重叠操作请求
**极其小心2个线程之间的共享数据,要建立互斥对象来同步这些数据避免内存泄露。
**在启动链接的时候创建mutex对象。pThread->m_dwEventTotal是关键互斥对象,极其重要。
**设置阻塞100ms(即每隔1000ms扫描一次服务器端口判断是否有新的连接请求),不能完全阻塞,否则无法监听端口了。
*/
dwWaitResult = WaitForSingleObject(pThread->m_hMutex1,INFINITE);
3.4关联事件,每个客户端关联一个事件
//接收数据初始化
if( pThread->m_EventArray[SockChannel] == 0 )
{ //如果通道号SockChannel位置已经有空事件就创建新事件了。
pThread->m_dwEventTotal++;
pThread->m_EventArray[/*pThread->m_dwEventTotal*/SockChannel] = WSACreateEvent(); //创建一个事件句柄,接收数据。
}
ZeroMemory(&pThread->m_TcpSrvOverlapped[/*pThread->m_dwEventTotal*/SockChannel], sizeof(WSAOVERLAPPED)); //初始化重叠数据结构,接收数据。
pThread->m_TcpSrvOverlapped[/*pThread->m_dwEventTotal*/SockChannel].hEvent = pThread->m_EventArray[/*pThread->m_dwEventTotal*/SockChannel]; //关联事件,接收数据。
memset( pThread->m_RecvBuf[/*pThread->m_dwEventTotal*/SockChannel],0,sizeof(pThread->m_RecvBuf[/*pThread->m_dwEventTotal*/SockChannel]) ); //初始化接收buffer,接收数据。
pThread->m_databuf[/*pThread->m_dwEventTotal*/SockChannel].buf = pThread->m_RecvBuf[/*pThread->m_dwEventTotal*/SockChannel]; //绑定WSABUF数据结构,接收数据。
pThread->m_databuf[/*pThread->m_dwEventTotal*/SockChannel].len = DATA_BUFSIZE;
/*
**发送数据初始化
**发送操作我不打算为其创建事件,只管发送。
*/
#if 0
//pThread->m_EventArray[pThread->m_dwEventTotal] = WSACreateEvent(); //创建一个事件句柄,发送数据。
ZeroMemory(&pThread->m_SendDataOverlapped[/*pThread->m_dwEventTotal*/0], sizeof(WSAOVERLAPPED)); //初始化重叠数据结构,发送数据。
//pThread->m_SendDataOverlapped[pThread->m_dwEventTotal].hEvent = pThread->m_EventArray[pThread->m_dwEventTotal]; //关联事件,发送数据。
memset( pThread->m_SendBuf[/*pThread->m_dwEventTotal*/0],0,sizeof(pThread->m_SendBuf[/*pThread->m_dwEventTotal*/0]) ); //初始化发送buffer,发送数据。
pThread->m_wsasendbuf[/*pThread->m_dwEventTotal*/0].buf = pThread->m_SendBuf[/*pThread->m_dwEventTotal*/0]; //绑定WSABUF数据结构,发送数据。
pThread->m_wsasendbuf[/*pThread->m_dwEventTotal*/0].len = DATA_BUFSIZE;
#endif
3.5申请接收数据重叠操作请求
/*
**为每一个链路申请接收数据重叠操作请求.
*/
int tmpResult = 0;
tmpResult = WSARecv(pThread->m_sockarr[SockChannel],&pThread->m_databuf[SockChannel],
1,&recvedLength,&Flags,&pThread->m_TcpSrvOverlapped[SockChannel],NULL);
if( tmpResult == SOCKET_ERROR )
{
//发生错误,关闭套接字, 结束线程。
if(WSAGetLastError() != WSA_IO_PENDING)
{
closesocket( pThread->m_sockarr[SockChannel] );
WSACloseEvent( pThread->m_EventArray[SockChannel] );
pThread->m_EmptyEvent = WSACreateEvent();
pThread->m_EventArray[SockChannel] = pThread->m_EmptyEvent; //关联一个空事件
pThread->m_sockFlag[SockChannel] = 0;
//pThread->m_dwEventTotal--;
pThread->m_dwSocketTotal--;
ReleaseMutex(pThread->m_hMutex1); //不要忘记这一步,否则线程彻底阻塞。
continue;
}
else
{
//it is normal when returns WSA_IO_PENDING. do nothing except for waiting.
}
}
3.6开启数据处理部分线程
/*
**第一次建立accept套接字调用数据处理线程,只调用一次,启动一个线程即可.
**一个线程最大处理63个链接,还有一个事件用于关闭线程。
**断开所有链接的时候记得手动清零所有CEventSockMulti对象的过程变量,然后结束线程。
*/
if( !pThread->m_bCallThread )
{
if (!(pThread->m_Thread2 = AfxBeginThread(DataProcessThread, pThread)))
return FALSE;
pThread->m_bCallThread = 1;
}
else
{
/*
**超过64条链路输出提示,如果需要,可以在这里再开一个线程,每个线程最多支持64条链路。
*/
if( pThread->m_dwSocketTotal > 63 )
AfxMessageBox("the connect number is larger 64.");
}
四、数据处理部分线程
4.1同步互斥对象和等待事件的发生
while(1)
{
//等待互斥对象信号有效
WaitForSingleObject(pSockObj->m_hMutex1,INFINITE);
/*
**等待事件(重叠操作——关闭线程,接收数据)变为有信号状态,否则阻塞线程。
**只阻塞10ms,目的是为了及时响应后续新增链路accept套接字的信号状态。
**注意:在调用WSAWaitForMultipleEvents前就要确定事件的数量和内容。
** 不能在已经调用了WSAWaitForMultipleEvents阻塞线程后在别的线程再注册事件和增加事件数量。
** 这样WSAWaitForMultipleEvents将不会响应后面注册的事件。
*/
dwIndex = WSAWaitForMultipleEvents(pSockObj->m_dwEventTotal+1,
pSockObj->m_EventArray, FALSE, 10, FALSE);
if( (dwIndex >= 0) && (dwIndex < 64) )
break;
/*
**释放互斥对象让他变为信号状态,供另外线程使用
*/
ReleaseMutex(pSockObj->m_hMutex1);
Sleep( 1 );
}
4.2判断客户端关闭退出
/*
**获取重叠操作(重叠操作——接收)结果,第4个参数为false,不阻塞线程。
**如果dwBytesTransferred为0,表示客户端关闭,则关闭对应的连接套接字。
**具体是这样:当客户端退出的时候会向服务端发送一条消息,但是没有数据。利用这个特点关闭对应套接字。
*/
DWORD dwBytesTransferred;
WSAGetOverlappedResult( pSockObj->m_sockarr[dwIndex], &pSockObj->m_TcpSrvOverlapped[dwIndex],
&dwBytesTransferred, FALSE, &Flags);
if(dwBytesTransferred == 0) //表示对方关闭了套接字,则退出线程。
{
closesocket( pSockObj->m_sockarr[dwIndex] );
WSACloseEvent( pSockObj->m_EventArray[dwIndex] ); // 关闭事件
/*
**关闭套接字和对应的事件后,m_EventArray对应位置的事件需要填上空事件
**否则会导致其他通道的套接字事件无法正常响应。
*/
pSockObj->m_EmptyEvent = WSACreateEvent();
pSockObj->m_EventArray[dwIndex] = pSockObj->m_EmptyEvent; //关联一个空事件
//pSockObj->m_dwEventTotal--;
pSockObj->m_dwSocketTotal--;
pSockObj->m_sockFlag[dwIndex] = 0;
::SendMessage(pSockObj->p_Owner->m_hWnd, WM_COMM_RXCHAR, (WPARAM)dwIndex, 2); //通知主窗口,有客户端关闭。
ReleaseMutex(pSockObj->m_hMutex1);
continue;
}
4.3处理数据
if( 0 == dwIndex ) //关闭线程
{
for(index = 0; index < pSockObj->m_dwEventTotal; index++ )
{
WSACloseEvent(pSockObj->m_EventArray[index]); //关闭事件对象句柄
closesocket(pSockObj->m_sockarr[index+1]); //关闭套接字,accept套接字是从下标1开始。
pSockObj->m_sockFlag[dwIndex] = 0;
}
pSockObj->m_dwEventTotal = 0;
pSockObj->m_dwSocketTotal = 0;
WSACleanup();
AfxEndThread(100); //结束线程
}
else //接收到数据,发送通知给主窗口处理数据。
{
//每接收到一次数据申请一次发送数据重叠操作
//Sleep(2000);//debug模式这里发送的数据客户端接收不到需要延时2S,exe文件没有这个问题。
sprintf(pSockObj->m_SendBuf[0],"got the channel %d message.\n\r",dwIndex);
tmpResult = WSASend(pSockObj->m_sockarr[dwIndex],&pSockObj->m_wsasendbuf[0],1,&sentLength,
0,&pSockObj->m_SendDataOverlapped[0],NULL);
//向父窗口发出消息,处理数据。
//WSAResetEvent(pSockObj->m_EventArray[pSockObj->m_dwEventTotal]); //复位事件对象句柄
::SendMessage(pSockObj->p_Owner->m_hWnd, WM_COMM_RXCHAR, (WPARAM)dwIndex, 0);
//再次申请接收数据重叠操作
tmpResult = WSARecv(pSockObj->m_sockarr[dwIndex],&pSockObj->m_databuf[dwIndex],
1,&recvedLength,&Flags,&pSockObj->m_TcpSrvOverlapped[dwIndex],NULL);
if( tmpResult == SOCKET_ERROR )
{
//发生错误,关闭套接字, 结束线程。
if(WSAGetLastError() != WSA_IO_PENDING)
{
closesocket( pSockObj->m_sockarr[dwIndex] );
WSACloseEvent( pSockObj->m_EventArray[dwIndex] );
pSockObj->m_EmptyEvent = WSACreateEvent();
pSockObj->m_EventArray[dwIndex] = pSockObj->m_EmptyEvent; //关联一个空事件
//pSockObj->m_dwEventTotal--;
pSockObj->m_dwSocketTotal--;
pSockObj->m_sockFlag[dwIndex] = 0;
ReleaseMutex(pSockObj->m_hMutex1);
continue;
}
else
{
//it is normal when returns WSA_IO_PENDING. do nothing except for waiting.
}
}
}
4.4程序结构说明
在上面这些代码的外部还有一个大的while循环,一直在不停的等待事件的到来。然后另外一个线程不停的监听端口,如果有新的客户端连接请求,就注册新的事件。 程序注释已经非常详细了,可以大概看一下。
五,大致界面
六、完整代码在以下地址:
https://download.csdn.net/download/hill_guo/11163659