基于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("无效的连接套接字");//获取客户端IPpTCPSocketConnect->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(cbDataBuff er) );//解析数据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],pClientI P[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),pUserL ogon->szUserName);_sntprintf_s(pUserData->szPassWord,CountArray(pUserData->szPassWord),pUserL ogon->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.szSendUserNa me),m_pUserManager->GetUserName(iter->second->dwUserID));_sntprintf_s(_SC_CHATMSG.szDescribe,CountArray(_SC_CHATMSG.szDescribe),pCHA TMSG->szDescribe);SendDataBatch(MDM_GP_USER,SUB_CS_USERT_CHAT,&_SC_CHATMSG,sizeof _SC_CHATMSG);return true;}}break;}return false;}5、当服务器端有人退出登录时的代码://客户端退出时,服务器端获取用户名并群发退出消息,再在链表中删除该用户的数据,清理他的Socketvoid 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_pUse rManager->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;//关闭之前socketm_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),pLogonIn fo->szUserName);_sntprintf_s(UserLogon.szPassWord,CountArray(UserLogon.szPassWord),pLogonIn fo->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_M sg.szDescribe));//效验数据if ( _UserChat_Msg.szDescribe[0] == TEXT('\0') ){SetTraceString(TEXT("聊天内容为空,请先输入您想说的话"));return;}//发送数据m_TCPScoketClient.SendData(MDM_GP_USER,SUB_CS_USERT_CHAT,&_UserChat_Msg,siz eof _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、客户端消息的显示代码://显示的消息类型:当用户登录时,将用户数据插入用户列表中。