socket网络编程服务端程序支持多客户端(重叠I/O方式)

  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循环,一直在不停的等待事件的到来。然后另外一个线程不停的监听端口,如果有新的客户端连接请求,就注册新的事件。 程序注释已经非常详细了,可以大概看一下。 

五,大致界面

socket网络编程服务端程序支持多客户端(重叠I/O方式)_第1张图片

六、完整代码在以下地址:
https://download.csdn.net/download/hill_guo/11163659

你可能感兴趣的:(MFC)