1. 程式人生 > >C++網路程式設計伺服器select模型(參考)

C++網路程式設計伺服器select模型(參考)

#include<iostream>
#include<vector>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
const int nPort=10000;
const int buf_len=1024;
struct Connection{
  SOCKET hSocket;
  char Buffer[buf_len];
  int nBytes;
  Connection(SOCKET socket):hSocket(socket),nBytes(0)//結構體建構函式,hSocket的初始值為socket,nBytes的初始值為0;
  {}
};  //注意:要加分號
typedef vector<Connection*> ConnectionList;
SOCKET BindListen()
{
	SOCKET HSListen=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//建立套接字
	sockaddr_in sdListen;
	sdListen.sin_family=AF_INET;
	sdListen.sin_port=htons(nPort);
	sdListen.sin_addr.s_addr=htonl(INADDR_ANY);
	if(bind(HSListen,(sockaddr*)&sdListen,sizeof(sockaddr_in))==SOCKET_ERROR)
	{
		cout<<"繫結失敗"<<endl;
		return INVALID_SOCKET;
	}
	else
		return HSListen;
}
//分析conns中的客戶連線,根據分析結果填充各個fd_set
void ResetFDset(fd_set &fdRead,fd_set &fdWrite,fd_set &fdExcept,SOCKET sdListen,const ConnectionList &conns)
{
	//首先清空各個fd_set
	FD_ZERO(&fdRead);
	FD_ZERO(&fdWrite);
	FD_ZERO(&fdExcept);
	//監聽套接字每次都被放入fdRead,也就是說每次呼叫select都會去檢測是否有新的連線請求
	FD_SET(sdListen,&fdRead);
	FD_SET(sdListen,&fdExcept);
	ConnectionList::const_iterator it=conns.begin();//獲取vector中首個元素的地址,vector中的元素是連續儲存的,類似陣列
	//不同於鏈
	for(;it!=conns.end();++it)
	{
		Connection *pConn=*it;//取出地址為it的內容,其內容為 Connection *型別
		//客戶連線的緩衝區還有空間,可以繼續接收資料,就需要把其對應的套接字控制代碼放入fdRead中
		if(pConn->nBytes<buf_len)
		{
			FD_SET(pConn->hSocket,&fdRead);
		}
		//客戶連線的緩衝區還有資料需要傳送,就需要把其對應的套接字控制代碼放入到fdWrite中
		if(pConn->nBytes>0)
		{
			FD_SET(pConn->hSocket,&fdWrite);
		}
		//每個連線的套接字控制代碼都需要放到fd_Except中,以便select 能夠檢測其I/O錯誤
		FD_SET(pConn->hSocket,&fdExcept);
	}
}
//檢查是否有新的客戶連線
int CheckAccept(const fd_set &fdRead,const fd_set &fdExcept,SOCKET sdListen,ConnectionList &conns)
{
	int lastErr=0;
	//首先檢查是否發生錯誤
	if(FD_ISSET(sdListen,&fdExcept))
	{
		int errlen=sizeof(lastErr);
		getsockopt(sdListen,SOL_SOCKET,SO_ERROR,(char *)&lastErr,&errlen);
		cout<<"I/O error"<<lastErr<<endl;
		return SOCKET_ERROR;
	}
	//檢查fdRead中是否包含了監聽套接字,如果是,則證明有新的連線請求,可以用accept
	if(FD_ISSET(sdListen,&fdRead))
	{
		//由於fd_set有大小限制(最多為FD_SETSIZE),所以當已有客戶連線到達這個限制時,不再接受連線請求
		if(conns.size()>=FD_SETSIZE-1)
		{
			return 0;
		}
		//呼叫accept來接受連線請求
		sockaddr_in saRemote;
		int nSize=sizeof(sockaddr_in);
		SOCKET sd=accept(sdListen,(sockaddr *)&saRemote,&nSize);
		lastErr=WSAGetLastError();
		//為了程式的健壯性,檢查WSAEWOULDBLOCK錯誤
		if(sd==INVALID_SOCKET&&lastErr!=WSAEWOULDBLOCK)
		{
			cout<<"接收錯誤"<<lastErr<<endl;
			return SOCKET_ERROR;
		}
		if(sd!=INVALID_SOCKET)
		{
			//獲取了新的客戶連線套接字控制代碼,需要把它設為非阻塞模式,以便對其使用select函式
			u_long nNoBlock=1;
			if(ioctlsocket(sd,FIONBIO,&nNoBlock)==SOCKET_ERROR)
			{
				cout<<"ioctlsocket error"<<WSAGetLastError()<<endl;
				return SOCKET_ERROR;
			}
			//為新的客戶連線建立一個Connection物件,並且加入到conns中去
			conns.push_back(new Connection(sd));
		}
	}
	return 0;
}
//被動安全關閉連線
//該函式被呼叫的原因是recv返回0,代表對方關閉了連線(即對方停止了資料傳送)
//如果本地還有資料沒有傳送出去,則需要繼續呼叫send來把剩餘的資料傳送出去,之後
//再呼叫shutdown來停止傳送資料
bool PassiveShutdown(SOCKET sd,const char *buff,int len)
{
	if(buff!=NULL && len>0)
	{
		//使用阻塞I/O模型把剩餘資料傳送出去
		u_long nNoBlock=0;
		if(ioctlsocket(sd,FIONBIO,&nNoBlock)==SOCKET_ERROR)
		{
			cout<<"ioctlsocket error"<<WSAGetLastError()<<endl;
			return false;
		}
		int nSent=0;
		//把剩餘資料傳送出去
		while(nSent<len)
		{
			int nTemp=send(sd,&buff[nSent],len=nSent,0);
			if(nTemp>0)
			{
				nSent+=nTemp;
			}
			else if(nTemp==SOCKET_ERROR)
			{cout<<"傳送失敗"<<WSAGetLastError()<<endl;
			return false;
			}
			else
			{
				cout<<"對方關閉連線"<<endl;
				break;
			}
		}
	}
	if(shutdown(sd,SD_SEND)==SOCKET_ERROR)
	{
		cout<<"關閉失敗"<<WSAGetLastError()<<endl;
		return false;
	}
	return true;
}
//select成功返回並標明recv就緒,呼叫recv接收資料
bool TryRead(Connection *pConn)
{
	//pConn->buffer+pConn->nBytes表示緩衝區Buffer可用空間的開始位置
	//buf_len-pConn->nBytes表示緩衝區Buffer可用空間的大小
	int nRet=recv(pConn->hSocket ,pConn->Buffer+pConn->nBytes,buf_len-pConn->nBytes,0);
	if(nRet>0)
	{
		pConn->nBytes+=nRet;
		return true;
	}
	//對方關閉了連線,呼叫被動安全關閉連線PassiveShutdown函式
	else if(nRet==0)
	{
		cout<<"對方關閉連線"<<endl;
		PassiveShutdown(pConn->hSocket,pConn->Buffer,pConn->nBytes);
		return false;
	}
	//發生了錯誤。為了程式的健壯性,檢查WSAEWOULDBLOCK錯誤
	else
	{
		int lastErr=WSAGetLastError();
		if(lastErr==WSAEWOULDBLOCK)
		{
			return true;
		}
		cout<<"接收失敗"<<lastErr<<endl;
		return false;
	}
}
//select成功返回並表明send就緒,呼叫send傳送資料
bool TryWrite(Connection *pConn)
{
	int nSent=send(pConn->hSocket,pConn->Buffer,pConn->nBytes,0);
	if(nSent>0)
	{
		pConn->nBytes-=nSent;
		//Buffer中還有資料尚未傳送出去
		if(pConn->nBytes>0)
		{
			//把尚未傳送的資料從Buffer的尾部移動到Buffer頭部
			memmove(pConn->Buffer,pConn->Buffer+nSent,pConn->nBytes);
		}
		return true;
	}
	//對方關閉了連線,呼叫了被動安全關閉連線PassiveShutdown函式
	else if(nSent==0)
	{
		cout<<"對方關閉連線"<<endl;
	    PassiveShutdown(pConn->hSocket,pConn->Buffer,pConn->nBytes);
	    return false;
	}
	//發生了錯誤,為了程式的健壯性,檢查WSAEWOULDBLOCK錯誤
	else
	{
		int lastErr=WSAGetLastError();
		if(lastErr==WSAEWOULDBLOCK)
		{
			return true;
		}
		cout<<"傳送失敗"<<lastErr<<endl;
		return false;
	}
}
//檢查當前所有客戶連線,看看它們是否可讀(recv),可寫(send),還是I/O錯誤
void CheckConn(const fd_set &fdRead,const fd_set &fdWrite,const fd_set &fdExcept,ConnectionList &conns)
{
	ConnectionList::iterator it=conns.begin();
	while(it!=conns.end())
	{
		Connection *pConn=*it;
		bool bOk=true;
		//檢查當前連線是否發生I/O錯誤
		if(FD_ISSET(pConn->hSocket,&fdExcept))
		{
			bOk=false;
			int lastErr;
			int errlen=sizeof(lastErr);
			getsockopt(pConn->hSocket,SOL_SOCKET,SO_ERROR,(char*)&lastErr,&errlen);
			cout<<"I/O error"<<lastErr<<endl;
		}
		else
		{
			//檢查當前連線是否可讀
			if(FD_ISSET(pConn->hSocket,&fdRead))
			{
				bOk=TryRead(pConn);
			}
		}
		//發生了錯誤,關閉套接字並把其對應的Connection物件從conns中移除
		if(bOk==false)
		{
			closesocket(pConn->hSocket);
			delete pConn;
			it=conns.erase(it);
		}
		else
		{
			++it;
		}
	}
}
//select就緒通告I/O模型伺服器的主體函式
void DoWork()
{
	//獲取監聽套接字
	SOCKET sdListen=BindListen();
	if(sdListen==INVALID_SOCKET)
	{}
	//定義conns,用於儲存當前所有客戶連線
	ConnectionList conns;
	//把監聽套接字設為非阻塞模式
	u_long nNoBlock=1;
	if(ioctlsocket(sdListen,FIONBIO,&nNoBlock)==SOCKET_ERROR)
	{
		cout<<"ioctlsocket 錯誤"<<WSAGetLastError()<<endl;
	}
	fd_set fdRead,fdWrite,fdExcept;
	while(true)
	{
		//分析conns中所有客戶連線並把它們放到合適的fd_set中
		ResetFDset(fdRead,fdWrite,fdExcept,sdListen,conns);
		//呼叫select,等待就緒的套接字I/O操作
		int nRet=select(0,&fdRead,&fdWrite,&fdExcept,NULL);
		if(nRet<=0)
		{
			cout<<"select error"<<WSAGetLastError()<<endl;
			break;
		}
		//檢查是否有新的客戶連線請求
		nRet=CheckAccept(fdRead,fdExcept,sdListen,conns);
		if(nRet==SOCKET_ERROR)
		{
			break;
		}
		//檢查客戶連線
		CheckConn(fdRead,fdWrite,fdExcept,conns);
	}
	//釋放資源
	ConnectionList::iterator it=conns.begin();
	for(;it!=conns.end();++it)
	{
		closesocket((*it)->hSocket);
		delete *it;
	}
	if(sdListen!=INVALID_SOCKET)
		closesocket(sdListen);
}
int main()
{
	WSAData wsaData;
	int nCode;
	if((nCode=WSAStartup(MAKEWORD(2,2),&wsaData))!=0)
	{
		cout<<"初始化失敗"<<nCode<<endl;
		return -1;
	}
	DoWork();
	return 0;
}