局域网聊天程序设计

更新时间:2023-10-01 04:25:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

青岛理工大学 C++面向对象课程设计报告

院(系): 计算机工程学院 专业: 软件工程

学生姓名: 管巨伟

班级 软件132 学号: 201307227 题目: 局域网聊天程序设计 ____

起迄日期: _ 2015.6.29~2015.7.10

设计地点: 计算机学院机房

指 导 教 师: 李传斌 杨鑫

完成日期: 2015 年7月 10 日

任务书

一、课程设计目的与要求

1.课程设计目的

面向对象程序设计作为一门软件设计的课程,具有极强的实践性,必须使学生具备灵活应用理论知识的能力及面向对象程序设计技能。所以在《C++面向对象程序设计》课程学习完成后,安排课程设计教学环节。

通过课程设计,使学生实际掌握面向对象的程序设计方法,了解C++面向对象的设计方法与技巧,有效地、深刻地理解课程内容,体会理论、方法和设计原则;培养学生分析实际问题和解决问题的能力,使学生具备使用面向对象程序设计开发工具设计实际系统的能力。

2.课程设计要求

结构化程序设计使用的是功能抽象,面向对象程序设计不仅能进行功能抽象,而且能进行数据抽象。“对象”实际上是功能抽象和数据抽象的统一。C++语言的“对象”是“类”的实例,程序设计的基础是设计类,所以类的有关概念都是重点,尤其要抓住抽象、封装、继承和多态性等要素。

面向对象程序设计的核心是类层次的设计。而具体类的设计的重点是如何选择数据成员和成员函数。根据数据成员和成员函数的特点,结合具体问题设计合适的类。成员函数设计中的难点是选择函数类型及其参数传递方式。

开发系统离不开设计平台,学生应在掌握面向对象程序设计基础上,熟悉并能熟练使用面向对象程序设计开发平台,结合相关理论知识,进行相应系统开发。

二、课程设计内容

课程设计题目及要求

局域网聊天程序设计:服务端设置,通过对客户端多个线程的监听,显示客户端的上线、下线,与客户端进行信息交流。开启服务器是通过新建socket,绑定端口号,监听线程,等待客户端连入。创建动态数组,完成客户端的连入,存储客户端信息。结束线程,完成对资源释放。

1

课程设计报告内容 一、需求分析

1.选做此课题或项的目的

开发此聊天程序旨在供个人聊天交流使用,进行多人并发聊天,交流思想见解,让使用者充分体验网络即时工具的方便快捷。同时了解现今正在使用的交流软件的基础功能,了解网络编程的基础思想,了解线程的使用,多个任务同时进行的原理,拓展自己的视野,提升自己的编码能力。也想通过该课题了解MFC的编码过程,学习收获到客户端、服务端都包含的交互式界面的实现及应用。

2.程序所实现的功能

客户端:1、获取输入的IP地址,2、获取通信的端口号,3、获取从服务器发来的信息,4、给服务器发送用户自定义的数据。

服务端:1、获取客户端IP地址,2、获取通信的端口号,3、记录客户端连入的台数,4、向所有连入客户端发送消息,5、接收客户端发送过来的信息

其他:进行网络的设置、关闭运行的程序、发送消息、系统托盘、系统托盘下对程序的显示和退出操作。

二、内容设计

1.根据所选题目,给出模块图

主聊天界面 ChatRoomDlg

更多功能消息函数 OnBnClickedOther() 网络设置 消息函数 OnBnClickedNetset() 关闭程序窗口消息函数OnBnClickedCancel() 系统托盘消息函数OnMenuTrayinco() 客户端打开消息函数OnBnClickedStartClient() 服务器打开 消 息 函数 OnBnClickedStartServer() 服务端设置OnBnClickedRadioServer() 客户端设置OnBnClickedRadioClient() 发送消息消息函数 SendClientsMsg() 接收消息 CreatThread() 图1 模块图 2

2.画出主程序及其主要模块的流程图

开始 Y 网络设置 N Y 更多功能 服务端或客户端设置 N 系统托盘 发送消息 关闭窗口 结束 图2主程序流程图 3

Y N 客户端设置 开始 网络设置

N 开启客户端 输入IP地址和 端口号 绑定端口号 开启服务端端 Y 服务端开启

发送或接受消息 关闭线程、关闭窗口 结束 图3 网络设置及消息发送流程图 4

pChatRoom->m_ClientArray.GetAt(idx).hThread = tItem.hThread;//保存线程,方 //便之后的其他使用 ResumeThread(tItem.hThread);//恢复线程运行 CString strMsg;

strMsg = _T(\客户端:\进入聊天室!\pChatRoom->ShowMsg(strMsg);

pChatRoom->SendClientsMsg(strMsg, &tItem);//消息的转发 Sleep(100);

} }

__Error_End: closesocket(pChatRoom->m_ListenSock); return TRUE; }

DWORD WINAPI ClientThreadProc(LPVOID lpParameter)//客户端处理线程函数: { CString strMsg;

CClientItem m_ClientItem = *(CClientItem *)lpParameter;//复制传进来的结点避免以前结 //点被修

while( TRUE && !(m_ClientItem.m_pMainWnd->bShutDown)) {//一直监视连接进来的客户端 if ( SOCKET_Select(m_ClientItem.m_Socket, 100, TRUE) ) {//判断缓冲区内是否有信息 TCHAR szBuf[MAX_BUF_SIZE] = {0};//定义一个缓冲区用于存储接收进来的信息 int iRet = recv(m_ClientItem.m_Socket, (char *)szBuf, MAX_BUF_SIZE, 0);//接收信息 if ( iRet > 0 ) {//接收成功 strMsg = szBuf; strMsg = _T(\客户端:\ m_ClientItem.m_pMainWnd->ShowMsg(strMsg); m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg, &m_ClientItem); }else{//客户端关闭 strMsg = _T(\客户端:\离开了聊天室!\ m_ClientItem.m_pMainWnd->ShowMsg(strMsg); m_ClientItem.m_pMainWnd->RemoveClientFromArray(m_ClientItem); m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg, &m_ClientItem); break; } } Sleep(500); } return TRUE; }

停止客户端、服务端:

void CChatRoomDlg::StopClient() { bShutDown = TRUE;

10

DWORD dwRet = WaitForSingleObject(m_hConnectThred, 1000);//等待线程结束的响应 if ( dwRet != WAIT_OBJECT_0 ) { TerminateThread(m_hConnectThred, -1);//强制结束线程 closesocket(m_ConnectSock); } m_hConnectThred = NULL; m_ConnectSock = INVALID_SOCKET; m_bIsServer = -1; bShutDown = FALSE; }

void CChatRoomDlg::StopServer() { UINT nCount = m_ClientArray.GetCount(); HANDLE *m_pHandles = new HANDLE[nCount+1];//额外的服务端线程,数组长度加1 m_pHandles[0] = m_hListenThread; for( int idx = 0; idx < nCount; idx++ ) { m_pHandles[idx+1] = m_ClientArray.GetAt(idx).hThread; } bShutDown = TRUE; DWORD dwRet = WaitForMultipleObjects(nCount+1, m_pHandles, TRUE, 1000); if ( dwRet != WAIT_OBJECT_0 ) { for( INT_PTR i = 0; i < m_ClientArray.GetCount(); i++ ) { TerminateThread(m_ClientArray.GetAt(i).hThread, -1); closesocket(m_ClientArray.GetAt(i).m_Socket); } TerminateThread(m_hListenThread, -1); closesocket(m_ListenSock); } delete [] m_pHandles; //释放数组资源 m_hListenThread = NULL; m_ListenSock = INVALID_SOCKET; m_bIsServer = -1; bShutDown = FALSE; }

系统托盘:

void CChatRoomDlg::OnBnClickedOther() { CPoint pt; CRect mRect; CMenu mMenu, *pMenu = NULL; GetDlgItem(IDC_OTHER)->GetWindowRect(&mRect);//取得更多功能按钮的位置 pt = mRect.BottomRight(); //获取按钮右下角的坐标 pt.y = mRect.top+10; mMenu.LoadMenu(IDR_MENU1); //加载菜单项

11

pMenu = mMenu.GetSubMenu(0);

pMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this);

//将菜单项显示在相应坐标位置

}

BOOL CChatRoomDlg::TrayMyIcon(BOOL bAdd) { BOOL bRet = FALSE; NOTIFYICONDATA tnd; tnd.cbSize = sizeof(NOTIFYICONDATA); tnd.hWnd = GetSafeHwnd(); //获得当前线程 tnd.uID = IDR_MAINFRAME; if ( bAdd == TRUE ) { tnd.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; tnd.uCallbackMessage = WM_TRAYICON_MSG;//消息响应

tnd.hIcon=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDR_MAINFRAME)); _tcscpy_s(tnd.szTip, sizeof(tnd.szTip), _T(\聊天室v1.0\鼠标移入显示信息 ShowWindow(SW_MINIMIZE); ShowWindow(SW_HIDE); bRet = Shell_NotifyIcon(NIM_ADD, &tnd);//显示系统托盘图标 }else{ ShowWindow(SW_SHOWNA); SetForegroundWindow(); bRet = Shell_NotifyIcon(NIM_DELETE, &tnd);//显示程序后,托盘图标消失 } return bRet; }

void CChatRoomDlg::OnMenuTrayinco() { TrayMyIcon(); }

LRESULT CChatRoomDlg::OnTrayCallBackMsg(WPARAM wparam, LPARAM lparam) { switch(lparam)//消息类型 { case WM_RBUTTONUP://右键单击 { CMenu mMenu, *pMenu = NULL; CPoint pt; mMenu.LoadMenu(IDR_MENU2); pMenu = mMenu.GetSubMenu(0); GetCursorPos(&pt);//获得坐标位置 SetForegroundWindow(); pMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this); break;

12

} case WM_LBUTTONDBLCLK://左键双击 ShowWindow(SW_RESTORE); SetForegroundWindow(); TrayMyIcon(FALSE); break; default:break; } return NULL; }

void CChatRoomDlg::OnMenuShow() { ShowWindow(SW_RESTORE); TrayMyIcon(FALSE); }

void CChatRoomDlg::OnMenuExit() { TrayMyIcon(FALSE); OnBnClickedCancel(); }

三、调试分析

1.实际完成的情况说明(完成的功能,支持的数据类型等)。

本程序实现了基础的局域网聊天过程,通过网络的设置可以实现服务端和客户端的信息收发,同时支持一对多的收发信息,多个客户端可以并发和服务端进行数据的收发。为防止操作错误,提供了多个提醒方式。其中包括停止服务器,停止客户端,以及程序处于客户端、服务端、最初状态时的关闭提醒。另外添加了系统托盘功能,在暂时不使用时可先挂起。 实现程序过程中使用了包括库中包含的网络编程所需基本CString、Socket、HANDLE等数据,为对IP地址、端口号的处理,还包含了fd_set、sockaddr_in 等的结构,过程中为对进行判别状态,数据转换,还使用了BOOL、char、TCHAR等的中间转换及判断。

2.程序的性能分析。

本程序封装了多个函数,在使用时可反复调用,无需再重写相应代码。通过设计客户端类,将客户端独立分开,开启相应线程,无需等待上一个客户端停止即可与服务端进行数据收发,实现一对多的通信,提高了通信效率。开设多个线程时,关闭相应就复杂,需要将所有线程关闭,释放资源,增加了代码的复杂度。关闭线程时,本程序设置了标记,通过标记的执行来使程序将对应的线程自动释放,将该线程申请的资源释放完全。但在无法正常关闭的情况下,采用了强制关闭,可能会使申请的相应资源无法彻底释放。运行时并未出现太久等待,消息的响应较为快捷。同时在程序中使用动态数组存储客户端连入,不会为满足较多的连入量而定义较大数组,造成空间的浪费。还设置了多种操作提示,避免用户操作失误带来的不必要结果,安全性较为可观。总的来说,本程序综合考虑了代码的简洁性,类的分类清楚,资源的释放,空间的利用率高、运行效率高效、安全性能等多方面的优化,性能较为可观。

13

3.上机过程中出现的问题及其解决方案。

1、网络设置中,对RadioButton的选定和转换选定进行切换,操作的结果和预想的不一样,在网上查找后,在格式中先将对应一组的RadioButton进行编号,再将这组RadioButton的第一个的group属性设置为true,其他设置为false。

2、连接服务器响应时,accept()函数为阻塞式的,这就在启动服务器时等待客户端连接,无法正常进行接下来的步骤。添加了异步I/O模型,通过select()函数的使用,在缓冲区中查看是否有客户端的连接,可以将阻塞的问题解决。

3、可能会有多个客户端连接服务器,这个过程中每个客户端各自独立运行,在并行运行的问题上,通过对线程的创建,每个运行的程序单独开辟线程,使程序单独运行。

4、对客户端的连入,服务端应该详细知道其信息,上线或下线时在服务端应该添加删除相应客户端,通过使用动态式数组,定义客户端类,每连入一个客户端,即添加入数组中,下线即从数组中删除客户端。

5、程序多个地方会重用一个变量,如窗口指针、socket等的重用,如直接使用过程中可能会对其有所修改,使后面使用时出现非预想的结果。此时定义了对应变量来保存,使变量的使用更为方便。

6、程序关闭时,直接关闭窗口相应的线程要停止,以便对占用的资源释放。强制终止线程仍会有该线程申请的一些资源得不到释放的问题。此时设置了bool变量判定程序是否关闭,关闭时让对应线程自动停止释放资源。

7、系统托盘的设计,菜单栏的添加,运行时菜单栏出现在非预想位置。通过对CPoint的添加,获取更多功能按钮的位置,通过TrackPopupMenu的使用,将菜单显示在了更多按钮的右侧偏下的位置。

4.程序中可以改进的地方说明。

由于对新知识的学习,时间的限制,程序可以添加登录界面,当用户注册登录后进行数据的收发。收发数据时显示的为客户端的IP地址,可以相应改成客户端登录的昵称。还缺少对文件、图片、视频、语音的数据收发。系统托盘时的图标显示为默认图标,此时可以设置成具有程序特点的图标来表示。

5.程序中可以扩充的功能及设计实现构想。

用户登录界面可以直接添加一个窗口,设计好界面后,在按钮、文本编辑框等中添加响应代码,完成对聊天界面的调用即可。在聊天界面中,将用户登录的昵称获取,在聊天显示中,将字符串改成“昵称+所发消息”即可。添加流的操作,统一使用二进制流的操作,以便对图片、视频、语音的操作,通过输入输出流完成数据的收发。添加图片,在函数中将默认图片的名称改成自定义的图片,使系统托盘具有自己程序的特色。

四、用户手册

程序支持Windows7,Windows8.1,WindowsXP系统。

14

Sleep(500); }

__Error_End: closesocket(pChatRoom->m_ConnectSock);//关闭线程 return TRUE; }

服务端的线程函数: #include \

#include \

BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut, BOOL bRead)//去缓冲区中查看是否有socket连接 { fd_set fdset; timeval tv;//超时时间 FD_ZERO(&fdset);//清0 FD_SET(hSocket, &fdset); nTimeOut = nTimeOut > 1000 ? 1000 : nTimeOut; tv.tv_sec = 0;//以秒为单位 tv.tv_usec = nTimeOut;//一毫秒为单位

int iRet = 0; if ( bRead ) { iRet = select(0, &fdset, NULL , NULL, &tv);//监听可读文件 }else{ iRet = select(0, NULL , &fdset, NULL, &tv);//监听可写 }

if(iRet <= 0) { return FALSE; } else if (FD_ISSET(hSocket, &fdset)){//查看缓冲区中的socket是否准备好 return TRUE; } return FALSE; }

DWORD WINAPI ListenThreadFunc(LPVOID pParam) { CChatRoomDlg *pChatRoom = (CChatRoomDlg *)pParam;//创建窗口指针 ASSERT(pChatRoom != NULL);//判断线程是否创建成功 pChatRoom->m_ListenSock = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);//创 //建socket并用一个变量保存 if ( pChatRoom->m_ListenSock == INVALID_SOCKET ) {//如果创建的socket是无效套接字 AfxMessageBox(_T(\新建Socket失败!\ return FALSE; }

int iPort = pChatRoom->GetDlgItemInt(IDC_LISTEN_PORT); if ( iPort <= 0 || iPort > 65535 ) {

30

AfxMessageBox(_T(\请输入合适的端口:1 - 65535\ goto __Error_End; }

sockaddr_in service;//定义结构储存IP,端口号用于bind service.sin_family = AF_INET; service.sin_addr.s_addr = INADDR_ANY; service.sin_port = htons(iPort);

if ( bind(pChatRoom->m_ListenSock, (sockaddr*)&service, sizeof(sockaddr_in)) == SOCKET_ERROR ) {//判断绑定端口是否成功 AfxMessageBox(_T(\绑定端口失败!\ goto __Error_End; }

if( listen(pChatRoom->m_ListenSock, 5) == SOCKET_ERROR ) {//判断监听端口是否成功 AfxMessageBox(_T(\监听失败!\ goto __Error_End; } pChatRoom->ShowMsg(_T(\系统信息:启动服务器成功!\ pChatRoom->m_bIsServer = TRUE;//用于判断后面发送消息的是服务端 pChatRoom->EnableWindow(IDC_START_SERVER, FALSE);//隐藏窗口 pChatRoom->EnableWindow(IDC_STOP_SERVER);

while( TRUE && !(pChatRoom->bShutDown)) {//为了连接多个客户端 if ( SOCKET_Select(pChatRoom->m_ListenSock, 100, TRUE) ) {//查看缓冲区中是否 //有连接的客户端 sockaddr_in clientAddr;//定义变量供accept使用 int iLen = sizeof(sockaddr_in); SOCKET accSock = accept(pChatRoom->m_ListenSock, (struct sockaddr *)&clientAddr , &iLen);//连接缓冲区中客户端 if (accSock == INVALID_SOCKET) { continue; } CClientItem tItem;//定义客户端节点 tItem.m_Socket = accSock; tItem.m_pMainWnd = pChatRoom; tItem.m_strIp = inet_ntoa(clientAddr.sin_addr);//转换IP地址格式 INT_PTR idx = pChatRoom->m_ClientArray.Add(tItem);//将新连接的客户端入队 tItem.hThread=CreateThread(NULL,0,ClientThreadProc,

&(pChatRoom->m_ClientArray.GetAt(idx)), CREATE_SUSPENDED, NULL);//给连入客户端 //创建线程,客户端并发运行 pChatRoom->m_ClientArray.GetAt(idx).hThread = tItem.hThread;//保存线程,方 //便之后的其他使用 ResumeThread(tItem.hThread);//恢复线程运行 CString strMsg;

strMsg = _T(\客户端:\进入聊天室!\

pChatRoom->ShowMsg(strMsg);

31

pChatRoom->SendClientsMsg(strMsg, &tItem);//消息的转发 Sleep(100);

} }

__Error_End: closesocket(pChatRoom->m_ListenSock); return TRUE; }

DWORD WINAPI ClientThreadProc(LPVOID lpParameter)//客户端处理线程函数: { CString strMsg;

CClientItem m_ClientItem = *(CClientItem *)lpParameter;//复制传进来的结点避免以前结 //点被修

while( TRUE && !(m_ClientItem.m_pMainWnd->bShutDown)) {//一直监视连接进来的客户端 if ( SOCKET_Select(m_ClientItem.m_Socket, 100, TRUE) ) {//判断缓冲区内是否有信息 TCHAR szBuf[MAX_BUF_SIZE] = {0};//定义一个缓冲区用于存储接收进来的信息 int iRet = recv(m_ClientItem.m_Socket, (char *)szBuf, MAX_BUF_SIZE, 0);//接收信息 if ( iRet > 0 ) {//接收成功 strMsg = szBuf; strMsg = _T(\客户端:\ m_ClientItem.m_pMainWnd->ShowMsg(strMsg); m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg, &m_ClientItem); }else{//客户端关闭 strMsg = _T(\客户端:\离开了聊天室!\ m_ClientItem.m_pMainWnd->ShowMsg(strMsg); m_ClientItem.m_pMainWnd->RemoveClientFromArray(m_ClientItem); m_ClientItem.m_pMainWnd->SendClientsMsg(strMsg, &m_ClientItem); break; } } Sleep(500); } return TRUE; }

客户端类的设计: #pragma once

class CChatRoomDlg;

#define MAX_BUF_SIZE 1024

#define WM_TRAYICON_MSG (WM_USER+100)

class CClientItem {//客户端节点类用于保存IP,socket,handle和指向窗口的指针 public: CString m_strIp; SOCKET m_Socket; HANDLE hThread;

32

CChatRoomDlg *m_pMainWnd; CClientItem(){ m_pMainWnd = NULL; m_Socket = INVALID_SOCKET; hThread = NULL; } };

DWORD WINAPI ListenThreadFunc(LPVOID pParam); DWORD WINAPI ClientThreadProc(LPVOID lpParameter); DWORD WINAPI ConnectThreadFunc(LPVOID pParam);

BOOL SOCKET_Select(SOCKET hSocket, int nTimeOut = 100, BOOL bRead = FALSE);

33

指导教师评语: 成绩: 指导教师: 年 月 日

34

本文来源:https://www.bwwdw.com/article/572d.html

Top