TCP聊天室03 服务器端程序设计

//Server.cpp

/*-----------------------------------------------------------------
   使用 TCP 协议的聊天室例子程序(服务器端)
-----------------------------------------------------------------*/
#include <Windows.h>
#include  <process.h>
#include "resource.h"

#pragma comment(lib,"Ws2_32.lib")

#include "Msg.h"

extern int iMsgSeq;
extern CRITICAL_SECTION csMsgQueue;
TCHAR szApp[] = TEXT("Tcp聊天室服务器");
TCHAR szSysInfo[] = TEXT("系统消息");
TCHAR szLogin[] = TEXT("进入聊天室!");
TCHAR szLogout[] = TEXT("退出了聊天室!");

int iThreadCnt = 0;
HWND hWnd = NULL;//对话框句柄
BOOL bStopFlag = FALSE;//退出标志


int CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
	//invoke	DialogBoxParam,eax,DLG_MAIN,NULL,offset _ProcDlgMain,0
	DialogBoxParamW(hInstance, TEXT("CHATSERVICE"), NULL, DlgProc, 0);
    return 0;
}

//检测链路的最后一次活动时间
//pBuffer ——指向要发送的链路检测的数据包
//pSession——指向上次的会话信息
//返回值:链路畅通(TRUE);链路断开(FALSE)
BOOL LinkCheck(SOCKET hSocket, char* lpszBuff, PSSESSION pSession){
	BOOL bRet = FALSE;
	PSMSGPKG pMsg = (PSMSGPKG)lpszBuff;/*此时的lpszBuff被封装成链路检查数据包*/
	DWORD dwTime = GetTickCount();
	//查看是否需要检测链路
	if((dwTime - pSession->dwLastTime) < 30*1000)		return TRUE;

	//30秒内没有数据通信,则发送链路检测包
	pSession->dwLastTime = GetTickCount();
	pMsg->stMsgHdr.iCmdType = CMD_LINK_CHECK;
	pMsg->stMsgHdr.iPkgLen = sizeof(SMSGHDR);

	 //发送检测链路的数据包(只需发送数据包头部就可以)
	return (send(hSocket, lpszBuff, pMsg->stMsgHdr.iPkgLen, 0) != SOCKET_ERROR);
}
//循环取消息队列中的聊天语句并发送到客户端,直到全部消息发送完毕
//pBuffer ——指向从消息队列中取出的消息的缓冲区,该消息将被发送到客户端
//pSession——指向上次的会话信息
//返回值:TRUE ——正常
//        FALSE——出现错误
BOOL SendMsgFromQueue(SOCKET hSocket, char* lpszBuff, PSSESSION pSession){
	PSMSGPKG pMsg = (PSMSGPKG)lpszBuff;/*此时的lpszBuff被封装成下发数据包*/
	int iMsgId = pSession->iMsgId + 1;//iMsgId为会话最后一次得到的消息,取它的下一条消息
	while(!bStopFlag){
		int iRet = GetMsgFromQueue(iMsgId++, pMsg->stMsg2Client.szSender, pMsg->stMsg2Client.szContent);
		if(iRet == 0)			break;

		pSession->iMsgId = iRet;
		pMsg->stMsg2Client.iContent = (lstrlen(pMsg->stMsg2Client.szContent)+1)*sizeof(TCHAR);
		pMsg->stMsgHdr.iPkgLen = sizeof(SMSGHDR) + OFFSET(SMSG2CLIENT, szContent) + pMsg->stMsg2Client.iContent;
		pMsg->stMsgHdr.iCmdType = CMD_MSG_TO_CLIENT;
		iRet = send(hSocket, (char*)pMsg, pMsg->stMsgHdr.iPkgLen, 0);
		if(iRet == SOCKET_ERROR)				return FALSE;
		pSession->dwLastTime = GetTickCount();//更新最后的会话时间

		 //当多人聊天时,队列里的消息会急剧增加,为了防止发送速度较慢
        //队列里的消息会越积越多,从而导致没有机会退出循环去接收来自本SOCKET的
        //(即本线程所服务的客户端)消息,所以在每次发送数据后,通过WaitData去
        //一下,是否有数据到达,如果有,则退出发送消息过程,优先去处理要接收的数据
		iRet = WaitSocket(hSocket, 0);
		if(iRet == SOCKET_ERROR)			return FALSE;//如果链路断了
		if(iRet > 0)			break;//如果有要接收的数据,则退出,优先去处理
	}
	return TRUE;
}


//通信服务线程,每个客户端登录的连接将产生一个线程
unsigned int WINAPI ServiceProc(void* lpParam)
{
	int iRet = -1;//用于接收调用函数的返回值
	SOCKET hSrvSock = (SOCKET)lpParam;
	//szBuff消息接收发送缓冲区(可被分装成任意类型的消息)让pMsgStruct指向缓冲区
	char szBuff[512]; memset(szBuff, 0, sizeof(char)*512); PSMSGPKG pMsg = (PSMSGPKG)szBuff;
	//为每一个客户端保存一个会话区
	SSESSION stSession; memset(&stSession, 0, sizeof(SSESSION)); stSession.iMsgId = iMsgSeq;
	//连接的客户数量加1,并显示出来
	++iThreadCnt; SetDlgItemInt(hWnd, IDC_COUNT, iThreadCnt, FALSE); 
	 /*********************************************************************
     用户名和密码检测,为了简化程序,现在可以使用任意用户名和密码
    *********************************************************************/
    //接收用户输入的用户名和密码。
    //客户端会发送一个MSGLOGIN数据包,命令代码为CMD_LOGIN,这是服务
    //器接受到客户端的第一个数据包。如果不是,即关闭连接。
	if(!RecvPkg(hSrvSock, szBuff, sizeof(SMSGHDR) + sizeof(SMSGLOGIN))){
		closesocket(hSrvSock); SetDlgItemInt(hWnd, IDC_COUNT, --iThreadCnt, FALSE);
		return FALSE;
	}
	if(pMsg->stMsgHdr.iCmdType != CMD_LOGIN_REQUEST){//判断是否是登录数据包
		closesocket(hSrvSock); SetDlgItemInt(hWnd, IDC_COUNT, --iThreadCnt, FALSE);
		return FALSE;
	}
	StringCchCopy(stSession.szUser, lstrlen(pMsg->stMsgLogin.szUser)+1, pMsg->stMsgLogin.szUser);
	//省略了验证用户名和密码,任何的用户名和密码都是可以通过的
	pMsg->stMsgResp.iResult = 1;//此处为1,说明验证通过
	pMsg->stMsgHdr.iCmdType = CMD_LOGIN_RESPONSE;
	pMsg->stMsgHdr.iPkgLen = sizeof(SMSGHDR) + sizeof(SMSGRESP);
	iRet = send(hSrvSock, szBuff, pMsg->stMsgHdr.iPkgLen, 0);
	if(iRet == SOCKET_ERROR){
		closesocket(hSrvSock); SetDlgItemInt(hWnd, IDC_COUNT, --iThreadCnt, FALSE);
		return FALSE;
	}
	 /*********************************************************************
      广播:xxx 进入了聊天室
    *********************************************************************/
	StringCchCopy((TCHAR*)szBuff, lstrlen(stSession.szUser)+1, stSession.szUser);
	StringCchCat((TCHAR*)szBuff, (lstrlen((TCHAR*)szBuff)+lstrlen(szLogin)+1), szLogin);
	PutMsgIntoQueue(szSysInfo, (TCHAR*)szBuff);
	stSession.dwLastTime = GetTickCount();
	while(!bStopFlag){//循环处理消息
		//将消息队列中的聊天记录发送给客户端
		memset(szBuff, 0, sizeof(char)*512);
		if(!SendMsgFromQueue(hSrvSock, szBuff, &stSession))		break;
		 //注意检测链路放在接收之前,而不是SendMsgQueue之前,为什么?
        //因为检测链路是通过发送数据包来实现的,而在SendMsgQueue本身就可以
        //发送数据包,返回SOCKET_ERROR就说明链路己断。但接收数据不同,如果
        //在接收之前,网络异常中断,这时系统并没设置socket的状态没为断开,会以
        //为对方一直没发数据过来,而处于等待.所以这时调用recv或select并不会返回
        //SOCKET_ERROR,只有通过主动发送数据检测探测,当多次send得不到回应时
        //系统才会将socket置为断开,以后的全部操作才会失败。
		pMsg->stMsgHdr.iCmdType = CMD_LINK_CHECK;
		pMsg->stMsgHdr.iPkgLen = sizeof(SMSGHDR);
		if(LinkCheck(hSrvSock, (char*)pMsg, &stSession) == SOCKET_ERROR || bStopFlag)		break;
		iRet = WaitSocket(hSrvSock, 200*1000);//等待200ms
		if(iRet == SOCKET_ERROR)			break;//如果连接中断,则退出
		if(iRet == 0)										continue;//如果没有接收到数据,则循环

		//注意,这里接收的数据只表明是个完整的数据包。可能是聊天语句的数据包,也可能是
        //是退出命令的数据包(本例没有实现这个,因为客户端退出里,链路会断开,会被LinkCheck检测到)
		memset(szBuff, 0, sizeof(char)*512);
		iRet = RecvPkg(hSrvSock, szBuff, sizeof(szBuff));		if(!iRet)		break;
		stSession.dwLastTime = GetTickCount();
		pMsg = (PSMSGPKG)szBuff;
		if(pMsg->stMsgHdr.iCmdType == CMD_MSG_TO_SERVER){
			PutMsgIntoQueue(stSession.szUser, (TCHAR*)(pMsg->stMsg2Server.szContent));
		}
	}
	 /*********************************************************************
    广播:xxx 退出了聊天室
    *********************************************************************/
	StringCchCopy((TCHAR*)szBuff, lstrlen(stSession.szUser) + 1, stSession.szUser);
	StringCchCat((TCHAR*)szBuff, (lstrlen((TCHAR*)szBuff)+lstrlen(szLogout)+1), szLogout);
	PutMsgIntoQueue(szSysInfo, (TCHAR*)szBuff);
	/*********************************************************************
    关闭socket
    *********************************************************************/
	closesocket(hSrvSock); SetDlgItemInt(hWnd, IDC_COUNT, --iThreadCnt, FALSE);
	return TRUE;
}


//监听线程
/*传入的参数是主线程中套接字变量的地址*/
unsigned int WINAPI ListenProc(PVOID pSocket){
	TCHAR szErrBind[] = TEXT("无法绑定到TCP端口1234,请检查是否有其它程序在使用!");
	 //创建socket
	SOCKET hListenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	*((SOCKET*)(pSocket)) = hListenSock;
	 //绑定socket
	SOCKADDR_IN stSa; memset(&stSa, 0, sizeof(SOCKADDR_IN));
	stSa.sin_port = htons(1234); stSa.sin_family = AF_INET; stSa.sin_addr.S_un.S_addr = INADDR_ANY;
	if(bind(hListenSock, (PSOCKADDR)&stSa, sizeof(SOCKADDR_IN))){//返回0表示无错误,是成功的
		MessageBox(hWnd, szErrBind, szApp, MB_OK|MB_ICONSTOP);
		closesocket(hListenSock);
		return FALSE;
	}
	//开始监听
	listen(hListenSock, 5);
	while(true){//等待连接并为每个连接创建一个新的服务线程
		SOCKET hServiceSock = accept(hListenSock, NULL, NULL);
		unsigned uThreadId;
		HANDLE hServiceThread = (HANDLE)_beginthreadex(NULL, 0, &ServiceProc, (LPVOID)(hServiceSock), 0, &uThreadId);
		CloseHandle(hServiceThread);
	}
	closesocket(hListenSock);
	return TRUE;
}


int CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    WSADATA stWSA;
    static SOCKET hListenSocket;
    static HANDLE hListenThread;

    switch (message)
    {
    case WM_INITDIALOG:
        hWnd = hwnd;
        InitializeCriticalSection(&csMsgQueue); //初始化临界区对象;
        WSAStartup(MAKEWORD(2, 0), &stWSA); //动态库的信息返回到WSAdata变量中
        
        //创建监听线程
		unsigned uThreadId;
		hListenThread = (HANDLE)_beginthreadex(NULL, 0, &ListenProc, (LPVOID)(&hListenSocket), 0, &uThreadId);
        CloseHandle(hListenThread); 
        return TRUE;
    case WM_CLOSE:
        closesocket(hListenSocket); //当未有客户端连接时,该socket在线程中创建,且未退出线程。
												//所以要在这里监听socket,此时会将accept返回失败,监听线程退出。
        bStopFlag = TRUE;         //设置退出标志,以便让服务线程中止
        while (iThreadCnt > 0); //等待服务线程关闭
        WSACleanup();
        DeleteCriticalSection(&csMsgQueue);
        EndDialog(hwnd, 0);
        return TRUE;
    }
    return FALSE;
}

猜你喜欢

转载自blog.csdn.net/KnightOnHourse/article/details/80062802