//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;
}
本文介绍了一个基于TCP协议的聊天室服务器程序实现。该程序能够处理客户端登录、消息广播及链路检测等功能,并采用多线程方式确保每个客户端的通信质量。
450

被折叠的 条评论
为什么被折叠?



