//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; }