1. 程式人生 > >Windows Socket 非阻塞模式開發

Windows Socket 非阻塞模式開發

                                                                  非阻塞套接字      

     非阻塞模式是指:套接字在執行操作時,呼叫的函式不管操作是否完成都會立即返回的工作模式。

    非阻塞套接字在處理同時建立的多個連線等方面具有明顯的優勢。但是使用過程中有一定的難度。由於函式在操作沒有完成後也仍然會返回,為了實現某些功能必須迴圈呼叫,直到完成功能為止。因此非阻塞模式會使程式效率非常低。

    把套接字設定為非阻塞模式,即告訴系統:在呼叫Windows socket API時,不讓主調執行緒睡眠,而讓函式立即返回。比如在呼叫

recv函式時,即使此時接受緩衝區沒有資料,也不會導致執行緒在recv處等待,recv函式會立即返回。如果沒有呼叫成功函式會返回WSAEROULDBLOCK錯誤程式碼。為了接收到資料必須迴圈呼叫recv,這也是非阻塞與阻塞模式的主要區別。

    預設情況下,使用socket或是WSASocket函式建立的套接字都是阻塞的。在建立套接字之後可以呼叫ioctsocket函式將套接字設定為非阻塞模式。

SOCKET s;

unsigned long ul=1;

int ret;

s=socket(AF_INET,SOCK_STREAM,0);

ret=ioctlsocket(s,FIONBIO,(unsigned long *)&ul);//設定成非阻塞模式。

if(ret==SOCKET_ERROR)//設定失敗。

{

}


    如果Windows socket api函式在返回時,卻沒有完成功能,它將返回WSAEWOULDBLOCK錯誤程式碼。說明請求的操作在呼叫期間內沒有完成。通常情況下應用程式需要重複呼叫此函式直到返回值標識為成功為止。


while(true)

{

   ret=recv(s,buff,num,0);

   if(ret==SOCKET_ERROR)

   {

      err=WSAGetLastError();

      if(err==WSAEWOULDBLOCK)

      {

        continue;

       }

      else if(err==WSAETIMEDOUT)//超時。

      {

       }

      else if(err==WSAENETDOWN)//連線斷開。

       {

       }

      else//其他錯誤。

         break;

   }

   else

break;//接受成功。

}

    在上面的程式碼中,WSAGetLastError函式返回WSAEWOULDBLOCK錯誤碼時,說明此時套接字緩衝區還沒有資料。需要繼續呼叫。除了WSAEWOULDBLOCK錯誤碼之外,還有WSAETIMEDOUTWSAENETDOWN錯誤,這些錯誤說明由於網路原因,與對方已經斷開了連線。

     不同的Windows socket api雖然都返回WSAEWOULDBLOCK但是它們所表示的錯誤原因卻不盡相同:

    對於acceptWSAAcceptWSAEWOULDBLOCK表示沒有收到連線請求。

    recvWSARecvrecvfromWSARecvfrom

,表示接受緩衝區沒有收到資料。

    sendWSASendsendfromWSASendfrom標識傳送緩衝區不可用。

    connecteWSAConnect表示連線未能立即完成。

    以上這些函式,在未完成任務時都會返回WSAEWOULDBLOCK,但是bindWSAStartup函式,卻不會返回該錯誤程式碼。除了呼叫ioctlsocket函式,將套接字設定為非阻塞模式之外,還可以呼叫WSAAsyncSelectWSAEventselect函式。這會在後面介紹。

    由於使用非阻塞套接字,函式會頻繁返回WSAEWOULDBLOCK錯誤。因此任何時候都應該仔細檢查返回程式碼。

    看一個完整的例子:該例分為伺服器程式和客戶端程式。客戶端向伺服器傳送任意一段話,伺服器在接收到時會返回給客戶端當前時間和日期。客戶端和伺服器都採用非阻塞套接字實現。

伺服器程式:

    伺服器程式:由主執行緒、接受客戶端請求執行緒、接收資料執行緒和傳送資料執行緒。

    主執行緒負責顯示介面,建立其他執行緒,同時初始化socket庫、建立socket監聽埠等基本操作。由於不知道客戶端何時會發起連線,因此需要不停的迴圈呼叫accept,當有客戶端連線到來時,接受客戶端的連線請求。為了提高效能,建立一個新的執行緒來執行此操作。此執行緒為接受客戶端請求執行緒。

由於recvsend函式,在未完成任務時也會返回。因此需要迴圈呼叫它們,直到執行成功為止。同樣為了提高效能,我們也分別建立兩個執行緒用於迴圈呼叫recvsend,接收和傳送資料。注意由於每一個連線都既可以傳送又可以接受資料,因此為每個連線都建立了這兩個執行緒。為了便於管理,我們為將建立一個類,每個連線都是該類的物件。每個連線都包括接收執行緒和傳送執行緒。

    CClient類標頭檔案。Client類管理每個連線。

#pragma once
#include<iostream>
#include"windows.h"
#include"time.h"
class CClient
{
public:
	CClient(void);
	CClient(SOCKET s,sockaddr_in addr);
	~CClient(void);
public:
	bool IsConnected();//判斷連線是否中斷。
	bool DisConnect();//中斷與伺服器的連線。
	bool calc();//計算當前時間,並複製到傳送緩衝區內。
	bool startRunning();//開始執行傳送和接收執行緒。
static DWORD WINAPI sendThread(void*param);//傳送執行緒入口函式。
static DWORD WINAPI recvThread(void*param);//接收執行緒入口函式。
private:
	HANDLE m_hSendThread;//傳送執行緒控制代碼。
	HANDLE m_hRecvThread;//接受執行緒控制代碼。
	HANDLE m_hEvent;//傳送執行緒和接收執行緒同步事件物件。接收客戶端請求後通知傳送執行緒傳送當前時間。
	SOCKET m_socket;//與客戶端連線套接字。
	sockaddr_in m_addr;//客戶端地址。
	bool m_IsConnected;
	char *m_pRecvData;//接收緩衝區。
	char *m_pSendData;//傳送緩衝區。
	bool m_IsSendData;//由於只有接收到客戶端請求後才需要傳送,該變數控制是否傳送資料。
};


CClient類

#include "Client.h"


CClient::CClient(void)
{
}

CClient::CClient( SOCKET s,sockaddr_in addr )
{//初始化各成員變數。
	m_socket=s;
	m_addr=addr;
	m_hRecvThread=NULL;
	m_hSendThread=NULL;
	m_IsConnected=true;
	m_IsSendData=true;
	m_hEvent=CreateEvent(NULL,true,false,NULL);
	m_pRecvData=new char[1024];
	m_pSendData=new char[1024];
	memset(m_pSendData,0,1024);
	memset(m_pRecvData,0,1024);
}


CClient::~CClient(void)
{
	delete []m_pRecvData;
	delete []m_pSendData;
}

bool CClient::IsConnected()
{
	return m_IsConnected;
}


bool CClient::calc()
{
	 time_t t; 
	 struct tm *local;
	 char T[256];
	 memset(T,0,256);
	 t=time(NULL);
	 local=localtime(&t);
	 sprintf(T,"%d/%d/%d %d:%d:%d",local->tm_year+1900,local->tm_mon+1,local->tm_mday,local->tm_hour,local->tm_min,local->tm_sec);
	 strcpy(m_pSendData,T);
	return true;
}

DWORD WINAPI CClient::sendThread( void*param )//傳送執行緒入口函式。
{
	std::cout<<"傳送資料執行緒開始執行!!"<<std::endl;
	CClient *pClient=static_cast<CClient*>(param);//獲得CClient物件指標。以便操縱成員變數。
	WaitForSingleObject(pClient->m_hEvent,INFINITE);//等待接收資料執行緒通知。
	while(pClient->m_IsConnected)
	{
		while(pClient->m_IsSendData)//可以傳送資料。
		{
			std::cout<<"等待接收資料執行緒通知!!"<<std::endl;
			//
			//ResetEvent(pClient->m_hEvent);
			int ret=send(pClient->m_socket,pClient->m_pSendData,1024,0);
			if(ret==SOCKET_ERROR)
			{
				int r=WSAGetLastError();
				if(r==WSAEWOULDBLOCK)
				{
					continue;
				}
				else
				{
					return 0;
				}
			}
			else
			{
				std::cout<<"結果傳送成功!!"<<std::endl;
				pClient->m_IsSendData=false;
				break;
			}

		}
		Sleep(1000);//未收到傳送通知,睡眠1秒。
		
	}
}

DWORD WINAPI CClient::recvThread( void*param )//接收資料執行緒入口函式。
{
	std::cout<<"接收資料執行緒開始執行!!"<<std::endl;
	CClient *pClient=static_cast<CClient*>(param);
	while(pClient->m_IsConnected)
	{
		memset(pClient->m_pRecvData,0,1024);
		int ret=recv(pClient->m_socket,pClient->m_pRecvData,1024,0);
		if(ret==SOCKET_ERROR)
		{
			int r=WSAGetLastError();
			if(r==WSAEWOULDBLOCK)
			{
				//std::cout<<"沒有收到來自客戶端的資料!!"<<std::endl;
				Sleep(20);
				continue;
			}
			else if(r==WSAENETDOWN)
			{
				std::cout<<"接收資料執行緒出現錯誤,連線中斷!"<<std::endl;
				break;
			}
			else
			{
				std::cout<<"接收資料執行緒出現錯誤!"<<std::endl;
				break;
			}
		}
		else
		{
			std::cout<<"恭喜,收到來自客戶端的資料:"<<pClient->m_pRecvData<<std::endl;
			pClient->calc();
			std::cout<<"通知傳送執行緒傳送結果!!"<<std::endl;
			SetEvent(pClient->m_hEvent);
			pClient->m_IsSendData=true;
		}
	}
	return 0;
}
bool CClient::startRunning()//開始為連線建立傳送和接收執行緒。
{
	m_hRecvThread=CreateThread(NULL,0,recvThread,(void*)this,0,NULL);//由於static成員函式,無法訪問類成員。因此傳入this指標。
	if(m_hRecvThread==NULL)
	{
		return false;
	}
	m_hSendThread=CreateThread(NULL,0,sendThread,(void*)this,0,NULL);
	if(m_hSendThread==NULL)
	{
		return false;
	}
	return true;

}

bool CClient::DisConnect()
{
	m_IsConnected=false;//接收和傳送執行緒退出。資源釋放交由資源釋放執行緒。
	return true;
}

伺服器主檔案:

#include"windows.h"
#include<iostream>
#include<list>
#include"Client.h"
#pragma comment(lib,"wsock32.lib")

HANDLE hAcceptHandle;
HANDLE hCleanHandle;
HANDLE hEvent;
SOCKET servSocket;
CRITICAL_SECTION cs;
bool IsServerRunning;
std::list<CClient*> clientlist;//該連結串列中儲存與伺服器建立的各連線。清理執行緒將執行對此連結串列進行刪除操作。

bool InitMemember();//初始化成員變數。
bool InitSocket();//初始化套接字,設定為非阻塞模式。繫結並監聽。
bool StartService();//開始執行接收客戶端請求執行緒。
bool StopService();//終止伺服器執行。
DWORD WINAPI CleanThread(void*param);//資源清理執行緒。
DWORD WINAPI AcceptThread(void*param);//接受客戶端請求執行緒。


int main(int argc,char**argv)
{
	InitMemember();
	InitSocket();
	do
	{
		char c;
		std::cout<<"請選擇操作:"<<std::endl;

		std::cin>>c;
		if(c=='e')
		{
			std::cout<<"即將退出伺服器程式。"<<std::endl;
			StopService();
		}
		else if(c=='y')
		{
			if(IsServerRunning)
			{
				std::cout<<"伺服器已經開啟,請不要重複開啟!!"<<std::endl;

			}
			else
			{
				StartService();
			}


		}
		else if(c=='n')
		{
			if(!IsServerRunning)
			{
				std::cout<<"伺服器未開啟,無法關閉!!"<<std::endl;

			}
			else
			{
				StopService();

			}
		}
		else 
		{

		}
		getchar();
	}while(IsServerRunning);

	Sleep(3000);
	WaitForSingleObject(CleanThread,INFINITE);


	return 0;
}

bool InitMemember()
{
	std::cout<<"初始化變數。"<<std::endl;

	IsServerRunning=false;
	hEvent=NULL;
	hCleanHandle=NULL;
	hAcceptHandle=NULL;
	InitializeCriticalSection(&cs);
	servSocket=INVALID_SOCKET;
	return 0;
}

bool InitSocket()
{
	std::cout<<"初始化套接字。"<<std::endl;

	WSADATA wsadata;
	WSAStartup(MAKEWORD(2,2),&wsadata);

	servSocket=socket(AF_INET,SOCK_STREAM,0);
	if(servSocket==INVALID_SOCKET)
	{
		return false;
	}
	unsigned long ul=1;
	int r=ioctlsocket(servSocket,FIONBIO,&ul);
	if(r==SOCKET_ERROR)
	{
		return false;
	}
	sockaddr_in addr;
	addr.sin_addr.S_un.S_addr=INADDR_ANY;
	addr.sin_family=AF_INET;
	addr.sin_port=htons(5000);
	r=bind(servSocket,(sockaddr*)&addr,sizeof(addr));
	if(r==SOCKET_ERROR)
	{
		return false;
	}
	int ret=listen(servSocket,10);
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	return true;
}

bool StartService()
{
	std::cout<<"開啟伺服器。"<<std::endl;

	IsServerRunning=true;
	
	hAcceptHandle=CreateThread(NULL,0,AcceptThread,NULL,0,NULL);
	if(hAcceptHandle==NULL)
	{
		return false;
	}
	CloseHandle(hCleanHandle);
	CloseHandle(hAcceptHandle);
}

bool StopService()
{
	std::cout<<"關閉伺服器。"<<std::endl;
	hCleanHandle=CreateThread(NULL,0,CleanThread,NULL,0,NULL);
	if(hCleanHandle==NULL)
	{
		std::cout<<"清理執行緒建立失敗!"<<std::endl;
		return false;
	}
	IsServerRunning=false;
	return 0;
}

DWORD WINAPI CleanThread( void*param )
{
	std::cout<<"資源清理執行緒已執行"<<std::endl;
     //中斷所有連線。退出所有接收和傳送執行緒迴圈。各執行緒將會退出。
	for(std::list<CClient*>::iterator iter=clientlist.begin();iter!=clientlist.end();iter++)
	{
		(*iter)->DisConnect();
	}
	Sleep(100);
	for(std::list<CClient*>::iterator iter=clientlist.begin();iter!=clientlist.end();iter++)
	{
		delete *iter;
	}
	clientlist.clear();
	SetEvent(hEvent);
	std::cout<<"資源清理完畢,資源清理執行緒退出!!"<<std::endl;
	return 0;
}

DWORD WINAPI AcceptThread( void*param )
{
	std::cout<<"接受客戶端連線執行緒開始執行。"<<std::endl;

	while(IsServerRunning)
	{
		sockaddr_in addr;
		SOCKET s;
		int len=sizeof(addr);
		s=accept(servSocket,(sockaddr*)&addr,&len);
		if(s==SOCKET_ERROR)
		{
			int r=WSAGetLastError();
			if(r==WSAEWOULDBLOCK)
			{
				//std::cout<<"未收到客戶端的連線請求。"<<std::endl;

				Sleep(1000);
				continue;
			}
			else
			{
				std::cout<<"未知錯誤,接受客戶端連線執行緒退出。"<<std::endl;
				getchar();
				return false;
			}
		}
		else//收到客戶端請求。
		{
			std::cout<<"收到客戶端的連線請求。"<<std::endl;

			CClient*pClient=new CClient(s,addr);
			pClient->startRunning();//該連結接受和傳送執行緒開始執行。
			clientlist.push_back(pClient);
		}
	}
	std::cout<<"接受客戶端連線執行緒退出。"<<std::endl;

	return 0;
}



客戶端:

    客戶端程式:由主執行緒、接收資料執行緒和傳送資料執行緒組成。

主執行緒:負責介面顯示,初始化socket庫、建立套接字,連線伺服器、接收使用者輸入、建立傳送和接收資料執行緒。

傳送執行緒:向伺服器傳送資料。

接收執行緒:接收從伺服器傳送的時間日期。

客戶端主檔案:

#include<iostream>
#include"windows.h"
#pragma  comment (lib,"wsock32.lib")
SOCKET clientSocket;
HANDLE hRecvThread;
HANDLE hSendThread;
bool IsConnected;
char sendBuff[1024];
char recvBuff[1024];
HANDLE hEvent;
HANDLE hSendEvent;
bool InitMember();
bool InitSocket();
bool startConnect();
DWORD WINAPI recvThread(void*param);
DWORD WINAPI sendThread(void*param);


int main(int argc,char**argv)
{
	InitMember();
	InitSocket();
	startConnect();
	char buff[256];
	while(IsConnected)
	{
		memset(buff,0,256);
		std::cout<<"請輸入表示式:";
		std::cin>>sendBuff;
		if(!strcmp(buff,"exit"))
		{
			std::cout<<"即將推出!"<<std::endl;
			IsConnected=false;
			HANDLE hHandleArray[2];
			hHandleArray[0]=hRecvThread;
			hHandleArray[1]=hSendThread;
			SetEvent(hEvent);
			WaitForMultipleObjects(2,hHandleArray,true,INFINITE);
			getchar();
			return 0;
		}
		else
		{
			SetEvent(hEvent);
		}
	}


	return 0;
}
//初始化套接字。
bool InitSocket()
{
	WSAData wsadata;
	WSAStartup(MAKEWORD(2,2),&wsadata);
	clientSocket=socket(AF_INET,SOCK_STREAM,0);
	if(clientSocket==INVALID_SOCKET)
	{
		return false;
	}
	unsigned long ul=1;
	ioctlsocket(clientSocket,FIONBIO,&ul);

	sockaddr_in addr;
	addr.sin_family=AF_INET;
	addr.sin_port=htons(5000);
	addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");
	while(true)
	{
		int ret=connect(clientSocket,(sockaddr*)&addr,sizeof(addr));
		if(ret==SOCKET_ERROR)
		{
			int r=WSAGetLastError();
			if(r==WSAEWOULDBLOCK||r==WSAEINVAL)
			{
				Sleep(20);
				continue;
			}
			else if(r==WSAEISCONN)//套接字原來已經連線!!
			{
				break;
			}
			else
			{
				std::cout<<"發生錯誤"<<std::endl;
				return false;
			}
		}
		if(ret==0)
		{
			break;
		}
	}
	IsConnected=true;
	return true;
}

bool InitMember()
{
	hSendThread=NULL;
	hRecvThread=NULL;
	hEvent=CreateEvent(NULL,true,false,NULL);
	hSendEvent=CreateEvent(NULL,true,false,NULL);
	IsConnected=false;
	memset(recvBuff,0,1024);
	memset(sendBuff,0,1024);
	return true;
}
//建立接收和傳送資料執行緒。
bool startConnect()
{
	hRecvThread=CreateThread(NULL,0,recvThread,NULL,0,NULL);
	if(hRecvThread==NULL)
	{
		return false;
	}
	hSendThread=CreateThread(NULL,0,sendThread,NULL,0,NULL);
	if(hSendThread==NULL)
	{
		return false;
	}
	return 0;
}
//接收資料執行緒入口函式。
DWORD WINAPI recvThread( void*param )
{
	std::cout<<"資料接收執行緒已開始執行!"<<std::endl;
	while(IsConnected)
	{
		WaitForSingleObject(hSendEvent,INFINITE);
		//ResetEvent(hSendEvent);
		int ret=recv(clientSocket,recvBuff,1024,0);
		if(ret==SOCKET_ERROR)
		{
			int r=WSAGetLastError();
			if(r==WSAEWOULDBLOCK)
			{
				//std::cout<<"沒有收到伺服器返回的資料!!"<<std::endl;
				Sleep(10);
				continue;
			}
			else if(r==WSAENETDOWN)
			{
				std::cout<<"資料傳送失敗!"<<std::endl;
				return false;
			}
		}
		else
		{
			std::cout<<"接收成功!"<<std::endl;

			std::cout<<recvBuff<<std::endl;
		}
	}
	return true;
}
//傳送資料執行緒入口函式。
DWORD WINAPI sendThread( void*param )
{
	std::cout<<"資料傳送執行緒已開始執行!!"<<std::endl;

	while(IsConnected)//是否與伺服器連線
	{
		WaitForSingleObject(hEvent,INFINITE);//等待接收資料執行緒通知。
		ResetEvent(hEvent);
		int ret=send(clientSocket,sendBuff,256,0);
		if(ret==SOCKET_ERROR)
		{
			int r=WSAGetLastError();
			if(r==WSAEWOULDBLOCK)
			{
				std::cout<<"資料傳送失敗!"<<std::endl;
				Sleep(20);
				continue;
			}
			else
			{
				std::cout<<"資料傳送失敗!"<<std::endl;
				break;
			}
		}
		else
		{
			std::cout<<"傳送成功!!"<<std::endl;
			SetEvent(hSendEvent);
		}
	}
	return true;
}


執行結果: