资源描述
基于TCP协议编程网络聊天室
设计内容: 基于TCP协议编程方法, 编写程序模拟网络聊天室运行过程。
设计要求:
1. 采取C/S模式, 基于TCP协议编程方法, 使得各个用户经过服务器转发实现聊天功效。
2. 分为两大模块: 用户端模块和服务器端模块。
3. 用户端模块关键功效:
1)登陆功效: 用户能够注册, 然后选择服务器登入聊天室。
2)显示用户: 将在线用户显示在列表中。
3)接收信息: 能接收其她用户发出信息。
4)发送信息: 能发出用户要发出信息。
4.服务器端模块关键功效:
1)检验登陆信息: 检验登陆信息是否正确, 并向用户端返回登陆信息, 如信息正确。就许可用户登陆。
2)显示在线状态: 将该用户状态发给各在线用户。
3)转发聊天信息: 将消息转发给全部在线用户。
5. 编程语言不限。
一、 需求分析
此程序关键分为两部分: 服务器端和用户端。
服务器端用于提供一个网络端口, 等候用户端发出请求, 登录到此服务端, 然后进行网络通讯和消息转发; 用户端可经过服务器端IP地址发送连接请求, 然后登陆聊天室。在服务器端组员列表栏中会显示在线全部些人名单, 有些人退出聊天室, 组员列表会自动除名。整个程序主体使用了CSocket类方法, 实现了网络通讯聊天。整个程序设计为两个部分: 服务器(SpeakerServer)和用户端 (SpeakerClient) 。
多人聊天关键在于要将每个用户端发送过来消息分发给全部其她用户端, 为了处理这个问题, 在服务器程序中建立一个套接口链表, 用来保留全部与用户端建立了连接服务端口。
设计原理: 服务器经过socket()系统调用创建一个Socket数组后(设定了接收连接用户最大数目), 与指定当地端口绑定bind(), 就能够在端口进行侦听listen()。假如有用户端连接请求, 则在数组中选择一个空socket, 将用户端地址赋给这个socket, 然后登陆成功用户就能够在服务器上聊天了。
用户端程序相对简单, 只要建立一个socket与服务器端连接, 成功后经过这个socket来发送和接收就能够了。
服务器端功效:
1、 初始化socket,创建服务器端。
2、 维护一个链表, 保留全部用户IP地址, 端口信息。
3、 接收用户传送来聊天信息, 然后向链表中所用用户转发。
4、 接收用户传送来连接判定命令, 并向用户发出响应命令。
用户端功效:
用户端界面上两个文本框, 一个用于显示接收聊天信息, 一个用来接收用户输入聊天信息。当按下“发送”按钮时将信息发送给服务器。
一、 概要设计:
服务器 用户端
(设计步骤图)
二、 具体设计:
服务器端:
1、 开启服务器代码:
//服务器开启时, 先创建套接字并绑定端口, 再监听此端口。
void CSpeakerServerDlg::OnBnClickedStart()
{
UINT uPort = GetDlgItemInt(IDC_PORT);
//创建套接字
if ( !m_TCPSocketListen.Create(uPort) )
{
m_TraceRichEdit.TraceString(TEXT("绑定监听端口失败,请确定该端口没有被其它程序占用"),TraceLevel_Warning);
return;
}
//监听套接字
if( !m_TCPSocketListen.Listen() )
{
m_TraceRichEdit.TraceString(TEXT("监听失败"),TraceLevel_Warning);
return;
}
UINT uMaxConnect = GetDlgItemInt(IDC_MAX);
//设置接口
m_TCPSocketListen.SetTCPSocketService(this);
//更新界面
m_TraceRichEdit.TraceString(TEXT("服务器开启成功"),TraceLevel_Normal);
GetDlgItem(IDC_START)->EnableWindow(FALSE);
GetDlgItem(IDC_STOP)->EnableWindow(TRUE);
}
2、 监听端口, 收到连接请求, 接收代码:
//先检验是否在服务器最大连接限制内, 若在, 则获取目前用户IP地址和端口等信息, 插入链表中。
//为何要限制连接人数?因为TCP连接是相当占资源, 若不限制连接人数, 服务器资源不够分配。
void CSpeakerServerDlg::OnAccept()
{
//承载能力
if ( m_TCPSocketItemMap.size() > GetDlgItemInt(IDC_MAX) )
{
m_TraceRichEdit.TraceString(TEXT("服务器承载人数已满, 已过滤其她连接"),TraceLevel_Warning);
return;
}
//绑定套接字
CTCPSocketService *pTCPSocketConnect = new CTCPSocketService;
try
{
SOCKADDR_IN SocketAddr;
int nBufferSize = sizeof(SocketAddr);
//连接
m_TCPSocketListen.Accept(*pTCPSocketConnect,(SOCKADDR *) &SocketAddr, &nBufferSize);
if (pTCPSocketConnect->m_hSocket == INVALID_SOCKET) throw TEXT("无效连接套接字");
//获取用户端IP
pTCPSocketConnect->m_dwClientAddr = SocketAddr.sin_addr.S_un.S_addr;
pTCPSocketConnect->SetTCPSocketService(this);
//绑定数据
bool bActive = true;
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.begin();
for (;iter!= m_TCPSocketItemMap.end();iter++)
{
if ( pTCPSocketConnect->m_hSocket == iter->first )
{
bActive = false;
break;
}
}
//插入用户数据
if ( bActive )
{
tagBindParameter *pBindParameter = new tagBindParameter;
pBindParameter->pTCPSocketService = pTCPSocketConnect;
pBindParameter->dwUserID = 0;
m_TCPSocketItemMap.insert(pair<SOCKET,tagBindParameter*>(pTCPSocketConnect->m_hSocket,pBindParameter));
}
}
catch (...)
{
if (pTCPSocketConnect->m_hSocket != INVALID_SOCKET) pTCPSocketConnect->Close();
}
}
3、 接收并检验数据代码:
void CSpeakerServerDlg::OnReceive(SOCKET hSocket)
{
BYTE cbDataBuffer[SOCKET_TCP_BUFFER];
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.find(hSocket);
if ( iter == m_TCPSocketItemMap.end() ) return;
//接收数据
iter->second->pTCPSocketService->Receive(cbDataBuffer,CountArray(cbDataBuffer) );
//解析数据
TCP_Command * pCommand=(TCP_Command *)cbDataBuffer;
//解释数据
WORD wPacketSize = pCommand->wPacketSize;
WORD wDataSize = wPacketSize-sizeof(TCP_Command);
//数据包效验
if ( wPacketSize > SOCKET_TCP_BUFFER+sizeof TCP_Command )
{
m_TraceRichEdit.TraceString(TEXT("数据包太大, 已拒绝"),TraceLevel_Warning);
return;
}
//子消息处理事件
if( !OnEventTCPSocketRead(hSocket,pCommand->wMainCmdID,pCommand->wSubCmdID,pCommand+1,wDataSize) )
{
BYTE * pClientIP=(BYTE *)&iter->second->pTCPSocketService->m_dwClientAddr;
m_TraceRichEdit.TraceString(TraceLevel_Warning,TEXT("收到伪数据包或未处理数据包,wMainCmdID:%d,wSubCmdID:%d,起源IP:%d.%d.%d.%d"),pCommand->wMainCmdID,pCommand->wSubCmdID,pClientIP[0],pClientIP[1],pClientIP[2],pClientIP[3]);
return;
}
}
4、 群发登录消息和用户发送消息代码:
//服务器收到用户消息以后会将收到消息发送给链表之中除了发送用户之外全部用户。
bool CSpeakerServerDlg::OnEventTCPSocketRead( SOCKET hSocket,WORD wMainCmdID, WORD wSubCmdID, VOID * pData, WORD wDataSize )
{
//获取绑定套接字
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.find(hSocket);
if ( iter == m_TCPSocketItemMap.end() ) return false;
CTCPSocketService *pTCPSocketService = iter->second->pTCPSocketService;
switch ( wMainCmdID )
{
case MDM_GP_LOGON:
{
if ( wSubCmdID == SUB_CS_LOGON )
{
//效验数据
ASSERT( wDataSize == sizeof CMD_CS_LOGON );
if ( wDataSize != sizeof CMD_CS_LOGON ) return false;
//获取数据
CMD_CS_LOGON *pUserLogon = (CMD_CS_LOGON*)pData;
m_TraceRichEdit.TraceString(TraceLevel_Normal,TEXT("%s登陆服务器"),pUserLogon->szUserName);
tagUserData *pUserData = new tagUserData;
//给用户分配一个UserID, UserID通常存放于数据库中, 是一个独一无二数字,
//通常在数据库表中设为主键, 是整个游戏或者软件识别用户唯一依据,这里我们没有包含到数据库, 临时取一个数值替换
//其次, 我们应该经过数据库SQL语句查询或者存放过程等方法, 或在数据库中做密码效验也好,
//或在查询到用户密码在服务器中进行判定也好, 不管什么方法, 此处通常需要进行用户密码效验, 这么才能够判定用户是否能够登陆了
pUserData->dwUserID = GetTickCount();
_sntprintf_s(pUserData->szUserName,CountArray(pUserData->szUserName),pUserLogon->szUserName);
_sntprintf_s(pUserData->szPassWord,CountArray(pUserData->szPassWord),pUserLogon->szPassWord);
//更新绑定数据
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.find(hSocket);
if ( iter != m_TCPSocketItemMap.end() )
iter->second->dwUserID = pUserData->dwUserID;
//群发登陆消息
SendUserItem(NULL,pUserData);
//发送在线用户
CUserItemArray::iterator pUserItemSend = m_pUserManager->GetUserItemArray()->begin();
for (;pUserItemSend!=m_pUserManager->GetUserItemArray()->end();pUserItemSend++ )
SendUserItem(pTCPSocketService,pUserItemSend->second);
//插入数据
m_pUserManager->InsertUserItem(pUserData);
return true;
}
}
break;
case MDM_GP_USER:
{
if ( wSubCmdID == SUB_CS_USERT_CHAT )
{
//获取数据
CMD_CS_CHATMSG *pCHATMSG = (CMD_CS_CHATMSG*)pData;
//这里其实需要做很多效验, 如dwSendUserID有效性, 字符串是否为空等, 这里就不做这些效验了
CMD_SC_CHATMSG _SC_CHATMSG;
ZeroMemory(&_SC_CHATMSG,sizeof _SC_CHATMSG);
//获取时间
GetLocalTime(&_SC_CHATMSG.SystemTime);
_sntprintf_s(_SC_CHATMSG.szSendUserName,CountArray(_SC_CHATMSG.szSendUserName),m_pUserManager->GetUserName(iter->second->dwUserID));
_sntprintf_s(_SC_CHATMSG.szDescribe,CountArray(_SC_CHATMSG.szDescribe),pCHATMSG->szDescribe);
SendDataBatch(MDM_GP_USER,SUB_CS_USERT_CHAT,&_SC_CHATMSG,sizeof _SC_CHATMSG);
return true;
}
}
break;
}
return false;
}
5、 当服务器端有些人退出登录时代码:
//用户端退出时, 服务器端获取用户名并群发退出消息, 再在链表中删除该用户数据, 清理她Socket
void CSpeakerServerDlg::OnClose(SOCKET hSocket)
{
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.find(hSocket);
if ( iter == m_TCPSocketItemMap.end() ) return;
//获取用户
m_TraceRichEdit.TraceString(TraceLevel_Normal,TEXT("%s退出了服务器"),m_pUserManager->GetUserName(iter->second->dwUserID));
//删除用户
CMD_DC_DELETE _DC_DELETE;
ZeroMemory(&_DC_DELETE,sizeof _DC_DELETE);
_sntprintf_s(_DC_DELETE.szUserName,CountArray(_DC_DELETE.szUserName),m_pUserManager->GetUserName(iter->second->dwUserID));
//群发消息
SendDataBatch(MDM_GP_USER,SUB_SC_DELETE,&_DC_DELETE,sizeof _DC_DELETE);
//销毁数据
m_pUserManager->RemoveUserItem(iter->second->dwUserID);
iter->second->pTCPSocketService->Close();
SafeDelete(iter->second->pTCPSocketService);
SafeDelete(iter->second);
m_TCPSocketItemMap.erase(iter);
}
6、 关闭服务器连接代码:
void CSpeakerServerDlg::OnBnClickedStop()
{
//关闭监听套接字
m_TCPSocketListen.Close();
//关闭连接套接字
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.begin();
for (;iter != m_TCPSocketItemMap.end(); ++iter)
{
iter->second->pTCPSocketService->Close();
SafeDelete(iter->second->pTCPSocketService);
SafeDelete(iter->second);
}
//更新界面
m_TraceRichEdit.TraceString(TEXT("服务器关闭成功"),TraceLevel_Normal);
GetDlgItem(IDC_START)->EnableWindow(TRUE);
GetDlgItem(IDC_STOP)->EnableWindow(FALSE);
}
7、 退出服务器代码:
void CSpeakerServerDlg::OnCancel()
{
if ( m_TCPSocketListen.m_hSocket != INVALID_SOCKET )
{
if ( AfxMessageBox(TEXT("确定退出服务器吗? 其它全部用户将失去连接"),MB_YESNO|MB_ICONQUESTION) == IDYES )
{
CTCPSocketItemMap::iterator iter = m_TCPSocketItemMap.begin();
for (;iter != m_TCPSocketItemMap.end(); ++iter)
{
iter->second->pTCPSocketService->Close();
SafeDelete(iter->second->pTCPSocketService);
SafeDelete(iter->second);
}
}
}
__super::OnCancel();
}
用户端:
1、 用户端登录:
//登陆消息
LRESULT CSpeakerClientDlg::OnLogonMessage( WPARAM wParam,LPARAM lParam )
{
tagLogonInfo *pLogonInfo = (tagLogonInfo*)wParam;
//关闭之前socket
m_TCPScoketClient.Close();
//初始化套接字
if ( !m_TCPScoketClient.Create() )
{
SetTraceString(TEXT("套接字创建失败"));
SafeDelete(pLogonInfo);
return FALSE;
}
//创建连接
if( m_TCPScoketClient.Connect(pLogonInfo->szServerAddr,pLogonInfo->nPort) == FALSE )
{
int nErrorCode = m_TCPScoketClient.GetLastError();
if ( nErrorCode !=WSAEWOULDBLOCK )
{
SetTraceString(TEXT("连接服务器失败,错误码: %d"),nErrorCode);
SafeDelete(pLogonInfo);
return FALSE;
}
}
//设置接口
m_TCPScoketClient.SetTCPSocketService(this);
//构建数据
CMD_CS_LOGON UserLogon;
ZeroMemory(&UserLogon,sizeof UserLogon);
_sntprintf_s(UserLogon.szUserName,CountArray(UserLogon.szUserName),pLogonInfo->szUserName);
_sntprintf_s(UserLogon.szPassWord,CountArray(UserLogon.szPassWord),pLogonInfo->szPassWord);
//发送登陆请求
m_TCPScoketClient.SendData(MDM_GP_LOGON,SUB_CS_LOGON,&UserLogon,sizeof UserLogon);
//设置界面
SetTraceString(TEXT("连接服务器成功"));
m_LogonDlg.PostMessage(WM_CLOSE);
//清理数据
SafeDelete(pLogonInfo);
return TRUE;
}
2、 用户端发送数据代码:
void CSpeakerClientDlg::OnBnClickedSend()
{
//设置数据
CMD_CS_CHATMSG _UserChat_Msg;
ZeroMemory(&_UserChat_Msg,sizeof _UserChat_Msg);
GetDlgItemText(IDC_EDITCHAT,_UserChat_Msg.szDescribe,CountArray(_UserChat_Msg.szDescribe));
//效验数据
if ( _UserChat_Msg.szDescribe[0] == TEXT('\0') )
{
SetTraceString(TEXT("聊天内容为空, 请先输入您想说话"));
return;
}
//发送数据
m_TCPScoketClient.SendData(MDM_GP_USER,SUB_CS_USERT_CHAT,&_UserChat_Msg,sizeof _UserChat_Msg);
}
3、 用户端接收数据代码:
//用户端接收数据和服务器段类似, 也需解析、 检验
void CSpeakerClientDlg::OnReceive( int nErrorCode )
{
//接收消息
BYTE cbDataBuffer[SOCKET_TCP_BUFFER];
m_TCPScoketClient.Receive(cbDataBuffer,CountArray(cbDataBuffer) );
//解析数据
TCP_Command * pCommand=(TCP_Command *)cbDataBuffer;
//解释数据
WORD wPacketSize = pCommand->wPacketSize;
WORD wDataSize = wPacketSize-sizeof(TCP_Command);
//数据包效验
if ( wPacketSize > SOCKET_TCP_BUFFER+sizeof TCP_Command )
{
SetTraceString(TEXT("数据包太大, 已拒绝"));
return;
}
//子消息处理事件
if( !OnEventTCPSocketRead(pCommand->wMainCmdID,pCommand->wSubCmdID,pCommand+1,wDataSize) )
{
SetTraceString(TEXT("收到未处理数据包,wMainCmdID:%d,wSubCmdID:%d"),pCommand->wMainCmdID,pCommand->wSubCmdID);
return;
}
}
4、 用户端消息显示代码:
//显示消息类型: 当用户登录时, 将用户数据插入用户列表中。服务器端会有xx登录显示。当用户发消息时, 服务器端就能够转发该消息给用户链表全部其她用户。用户退出时, 同理, 用户端也会接收到XX退出了消息。
bool CSpeakerClientDlg::OnEventTCPSocketRead( WORD wMainCmdID, WORD wSubCmdID, VOID * pData, WORD wDataSize )
{
switch ( wMainCmdID )
{
case MDM_GP_LOGON: //登陆消息
{
if ( wSubCmdID == SUB_SC_USERCOME) //用户进入
{
CMD_SC_USERCOME *pUserCome = (CMD_SC_USERCOME*)pData;
//插入数据
if( m_ListUser.FindString(-1,pUserCome->szUserName) == LB_ERR )
{
//设置自己信息
if ( m_UserData.dwUserID == 0 )
{
_sntprintf_s(m_UserData.szUserName,CountArray(m_UserData.szUserName),pUserCome->szUserName);
m_UserData.dwUserID = m_UserData.dwUserID;
SetWindowText(m_UserData.szUserName);
}
//添加用户列表
m_ListUser.AddString(pUserCome->szUserName);
m_ListUser.SetItemData(m_ListUser.GetCount()-1,pUserCome->dwUserID);
}
return true;
}
break;
}
case MDM_GP_USER: //用户消息
{
if ( wSubCmdID == SUB_CS_USERT_CHAT ) //聊天消息
{
CMD_SC_CHATMSG *pCHATMSG = (CMD_SC_CHATMSG*)pData;
//设置聊天数据
static CString str;
CString StrDescribe;
StrDescribe.Format(TEXT("%s %04d-%02d-%02d %02d:%02d:%02d\r\n"),pCHATMSG->szSendUserName,pCHATMSG->SystemTime.wYear, pCHATMSG->SystemTime.wMonth,
pCHATMSG->SystemTime.wDay, pCHATMSG->SystemTime.wHour, pCHATMSG->SystemTime.wMinute, pCHATMSG->SystemTime.wSecond);
str += StrDescribe;
str += pCHATMSG->szDescribe;
str += TEXT("\r\n");
SetDlgItemText(IDC_CHATRECV,str);
return true;
}
else if ( wSubCmdID == SUB_SC_DELETE ) //用户退出消息
{
CMD_DC_DELETE *pDeleteUser = (CMD_DC_DELETE*)pData;
int nIndex = m_ListUser.FindString(-1,pDeleteUser->szUserName);
if ( nIndex != LB_ERR )
{
m_ListUser.DeleteString(nIndex);
}
static CString str;
CString StrDescribe;
StrDescribe.Format(TEXT("%s退出了."), pDeleteUser->szUserName);
str += StrDescribe;
str += TEXT("\r\n");
SetDlgItemText(IDC_CHATRECV, str);
return true;
}
break;
}
}
return false;
}
5、 退出用户端:
void CSpeakerClientDlg::OnCancel()
{
if (m_TCPScoketClient.m_hSocket != INVALID_SOCKET)
{
if (AfxMessageBox(TEXT("确定退出用户端吗? "), MB_YESNO | MB_ICONQUESTION) == IDYES)
{
//关闭套接字
m_TCPScoketClient.Close();
CDialog::OnCancel();
}
}
}
三、 调试分析:
用户端用户登录:
服务器端:
用户1发hello:
用户2发你好:
用户1和用户2退出时服务器端显示:
四、 课设总结:
这次课程设计制作我关键是参考了网上部分网络编程实例和图书馆相关书籍, 找到类似程序, 跟着书上一步一步做出来, 即使这个程序算不上是我自己写出来, 不过经过这个过程, 还是让我学会了好多东西, 也算能比较熟练地掌握MFC这一个软件了, 这算是一个不小收获。而且在做这个程序过程中, 碰到许很多多问题, 有经过网上搜索能够找到答案。有却不行, 最终还是大家一起讨论出来。
总而言之, 最终这个程序功效达成了之前预想可能, 经过服务器端消息转发, 实现了多用户之间群聊, 完整了一个简单网络聊天软件功效。 不过最终验收时候, 老师还是期望我能做出私聊效果来, 不过因为时间限制, 并未能实现它。
私聊实现思绪: 用户1若想与用户2私聊, 则能够在用户列表选中用户2, 服务器接收到用户1请求以后, 将用户2IP地址和端口号发送给用户1, 这么用户1就能够跟用户2T做TCP连接, 并聊天了。
经过此次试验, 也同时让我对于CSocket网络聊天类使用有了更深入了解, 在相关网络编程方面也有了新认识。即使此程序功效还比较简单, 而且一些方面还没能完善。
现在就软件本身还存在问题罗列以下:
1、 组员列表在非正常退出时, 服务器端得不到立刻而有效地更新
2、 用户登录前必需先知道服务器端IP, 这么操作显然比较麻烦
3、 程序界面做比较单一, 缺乏层次性美感。
展开阅读全文