程序设计报告之简易聊天软件的设计与实现
程序设计报告
( 2012 / 2013 学年 第 二 学期)
题 目: 简易聊天软件的设计与实现
专 业
学 生 姓 名 班 级 学 号 指 导 教 师
指 导 单 位 计算机学院计算机科学与技术系 日 期 2013年3月28日
一、课题名称
简易聊天软件的设计与实现
二、课题内容和要求
1、课题内容:本课程设计主要是设计并实现一个简单的聊天程序, 创建基于多线程的聊天室程序。
2、课题要求:要求能够实现基本的聊天功能,本聊天室允许两台计算机之间进行聊天,但是需要获取进行聊天的两台机之间IP 地址,可以实现一对一的聊天。
三、原理分析
1、该程序实现局域网内的聊天功能,包括服务器端程序和客户端程序两部分。
客户端程序:可连接到服务器,并将消息发送到服务器端和接受服务器端发送到的消息。 服务器端程序:负责发送用户列表和转发客户端发送过来的消息。 本程序涉及到服务器端和客户端,采用同一个套接字。
2、该聊天软件是采用UDP 连接,UDP 是OSI 参考模型中一种无连接的传输层协议,它提供了简单不可靠的信息传送服务。由于UDP 比较简单,UDP 包含很少的字节,所以它在速度方面有很大优势。很多常用的即时通软件,如QQ 程序,都会使用UDP 实现很多基本功能。UDP 是“面向非连接”的网络协议,它与“面向连接”的TCP 协议对应。在发送数据之前,并不与对方建立连接,而是直接把数据报发出去,不保证可靠的传输。UDP 相对TCP 简单,在速度方面有很大优势,因为它的网络开销少,对传输可靠性要求不是很高的情况下,UDP 的使用是网络程序的首选。
3、要实现聊天功能必须获取两台计算机的IP 地。在IP 地址控件栏输入参与聊天对象的计算机的IP 地址。
4、本程序的核心在于将消息的发送的和接收发在了两个不同的线程中,接收放在新创建的副进程中,因为其要一直处于响应状态,也就是需要一个while 循环;发送放在主线程中。这样消息的接收和发送就不存在先后顺序了,且一直处于循环中的接收也不会影响到发送。
5、程序代码中的新线程入口函数中可能没有必要传递两个参数进去,其中SOCKET 参数可以在入口函数内部创建, SOCKET变量也就是声明是TCP 还是UDP ,和发送或接收没有必
然的联系,如果这样的话,就没有必要声明“详细设计”第五步中的结构体了,CreateThread 方法也刚好传递一个参数,即当前窗口的句柄。
四、需求分析
1、在VC++6.0中MFC 中创建新文档,选中基本对话框栏,然后进行对话框的设置,选择不同的控件,分别设置接收数据、发送数据和发送的控件。 2、对需要用的变量进行定义并初始化等。
3、实现不同的功能响应不同的消息处理函数,实现套接字绑定获取IP 地址等功能。 4、理解CWinApp 中的InitInstance 函数的用法及功能。 5、WSACleanup 函数的调用与终止等。 6、各种不同代码的功能与实现原理。
五、概要设计
1、对需要用的变量进行定义或申明。
2、调用相应的MFC 内置函数,对相应的变量进行初始化等操作。 3、程序设计的概要流程图如下:
图1 程序设计流程图
六、详细设计
1、创建一个基于对话框的MFC 程序设计,界面如下:
图2 对话框界面
2、添加套接字库头文件:
函数能准确保证程序终止前调用WSACleanup 的调用,该函数其实也是调用Win32中的WSAStartup ,该函数的调用位置最好在CWinApp 中的InitInstance 中,包含头文件Afxsock.h ,在StdAfx.h 这个头文件中调用MFC 的内置函数AfxSocketInit ,该函数其他也是调用Win32中的WSASAtartup ,该进行包含。StdAfx.h 头文件是一个预编译头文件,在该文件中包含了MFC 程序运行的一些必要的头文件,如afxwin.h 这样的MFC 核心头文件等。一些必要的头文件,如afxwin.h 这样的MFC 核心头文件等。它是一个被程序加载的文件。
3、加载套接字库:
在CWinApp 中的InitInstance 添加如下代码: if(FALSE==AfxSocketInit()) {
AfxMessageBox("套接字库加载失败!"); return FALSE; }
4、创建套接字:
将自己假想成服务器端,进行套接字和地址结构的绑定,等待别人发送消息过来。
在CDialog 中
添加成员变量:SOCKET m_socket 添加自定义函数:
BOOL CChatDlg::InitSocket() {
m_socket=socket(AF_INET,SOCK_DGRAM,0); //UDP连接方式 if(INVALID_SOCKET==m_socket) {
MessageBox("套接字创建失败!"); return FALSE; }
SOCKADDR_IN addrServer; //将自己假想成server addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrServer.sin_family=AF_INET; addrServer.sin_port=htons(1234); int retVal;
retVal=bind(addrSock,(SOCKADDR*)&addrServer,sizeof(SOCKADDR)); if(SOCKET_ERROR==retVal) {
closesocket(addrSock);
MessageBox("套接字绑定失败!"); return FALSE; }
return TRUE; }
5、在CChatDlg 类的外部添加结构体:
struct RECVPARAM {
SOCKET sock; //保存最初创建的套接字 HWND hWnd; //保存对话框的窗口句柄
};
6、在对话框的初始化代码中完成线程的创建: 在CChatDlg::OnInitDialog函数中添加下面的代码:
if(!InitSocket()) //服务器端的创建
return FALSE;
RECVPARAM *pRecvParam=new RECVPARAM; pRecvParam->hWnd=m_hWnd; pRecvParam->sock=m_socket;
说明:
1)接收部分应该一直处于响应状态,如果和发送部分放在同一段代码中,势必会阻塞掉发送功能的实现,所以考虑将接收放在单独的线程中,使它在一个while 循环中,始终处于响应状态
2)因为需要传递两个参数进去,一个是recvfrom 需要用的套接字,另一个是当收到数据后需要将数据显示在窗口中的对应文本框控件上,所以需要传递当前窗口的句柄,但CreateThread 方法只能传递一个参数,即第四个参数,这时候就想到了采用结构体的方式传递。
HANDLE hThread=CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL); CloseHandle(hThread);
7、创建线程入口函数RecvProc :
如果是成员函数的话,那它属于某个具体的对象,那么在调用它的时候势必要让程序创建一个对象,但该对象的构造函数有参数的话,系统就不知所措了,所以可以将函数创建为全局函数,即不属于类,但这失去了类的封装性,最好的方法是将该方法声明为静态方法,它不属于任何一个对象。 在CChatDlg 类的头文件中添加:
static DWORD WINAPI RecvProc(LPVOID lpParameter); 在cpp 文件中添加:
DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter) {
RECVPARAM* pRecvParam=(RECVPARAM*)lpParameter; HWND hWnd=pRecvParam->hWnd; SOCKET sock=pRecvParam->sock; char recvBuf[200]; char resultBuf[200];
SOCKADDR_IN addrFrom; //这个时候是假想成服务器端 int len=sizeof(SOCKADDR_IN); while(TRUE) //处于持续响应状态 {
int retVal=recvfrom(sock,recvBuf,200,0,(SOCKADDR*)&addrFrom,&len); if(SOCKET_ERROR == retVal) {
AfxMessageBox("接收数据出错");
break;
} else {
sprintf(resultBuf,"%ssaid:%s",inet_ntoa(addFrom.sin_ad
dr),recvBuf); //现在已经拿到客户端送过来的消息了,但因为自身是静态函数,所以拿不到当前窗口对象中的控件的句柄,也就不能对其赋值了,唯一办法就是用消息的形式将接收到的值抛出到窗口的消息队列中,等待消息处理
::PostMessage(hWnd,WM_RECVDATA,0,(LPARAM)resultBuf); } } return 0; }
8、自定义消息: 定义自定义消息的宏:
#define WM_RECVDATA WM_USER+1
声明消息响应函数:因为有参数要传递,所以wParam 和lParam 都要写,如果没有 要传递,可以不写
afx_msg void OnRecvData(WPARAM wParam,LPARAM lParam); 消息映射:
ON_MESSAGE(WM_RECVDATA,OnRecvData) 定义消息响应函数:
void CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam) {
CString recvData=(char*)lParam; CString temp;
GetDlgItemText(IDC_EDIT_RECV,temp); temp+="\r\n"; temp+=recvData;
SetDlgItemText(IDC_EDIT_RECV,temp); }
自此,消息的接收和显示部分已经完成了
9、消息的发送:
在发送按钮点击的响应函数中添加: DWORD dword;
CIPAddressCtrl* pIPAddr=(CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1); pIPAddr->GetAddress(dword); //因为对方有具体的IP 地址值,我们假想对方是服务
器端。在发送的时候程序就从服务器的角色转变为客户端了 SOCKADDR_IN addrServer;
addrServer.sin_addr.S_un.S_addr=htonl(dword); addrServer.sin_family=AF_INET; addrServer.sin_port=htons(1234); CString strSend;
GetDlgItemText(IDC_EDIT_SEND,strSend);
sendto(m_socket,strSend,strlen(strSend)+1,0,(SOCKADDR*)&addrServer,sizeof(SO
CKADDR));
SetDlgItemText(IDC_EDIT_SEND,"");
10、消息面版的控制:
至此,整个聊天软件的程序设计已经完成,但当你运行的时候发现两次发送的消息会在同一行出现。如图所示:
此时,需要鼠标选中接收数据中的编辑一栏,更改其属性,选中属性中的样式然后选中多行。
整个聊天软件的是设计与实现已经完成。
七、测试数据及其结果分析
1、测试数据(如下图所示):
-
图3 测试数据结果
2、结果分析
本聊天软件只可以在两台就计算机之间进行聊天,不可以再同一台上进行,并且程序设计时套接字已绑定,必须在获取了计算机的IP 地址后才能进行聊天,必须在相同的条件下互相发送消息(比如同时联网或同是未连接网络)。
八、调试过程中的问题
1、由于未学过MFC 等程序语言设计,在编程过程中遇到的很多代码都不认识,程序编写过程中比较麻烦。
-
2、在最初的情况下,就连最基本的对话框都不会建立,后来在图书馆借阅了各种书籍,解决了对话框的问题。
3、对话框问题解决后,仍是无从下手,在图书馆的借书里看见了一个完整的基于MFC 的聊天软件设计,就想着按照上面一步步学习,但当做了几步后发现根本不可能,因为其中隐含的代码和知识完全不懂,后又在指导老师的帮助下懂了一些基本知识。
4、在以后的编写程序中,又遇到各种问题,在网上查了一些视频资料等最终把问题解决了。最后在了完成后已经能运行了,可是运行了几次后又出现了问题,源代码中有一个if 的选择语句,运行过程中总是选择返回值为false 的那一条语句,在反复斟酌并在指导老师的帮助下最终解决了问题。并实现了在两台计算机上进行聊天的功能。
九、程序设计总结
这次课程设计花了我不少时间,虽然可能存在不少瑕疵,但总的来说我成功的完成了,或许不是太完美,但是我辛辛苦苦动脑筋做的,是我完成的第一个实验设计,我很开心。通过这次设计,我着实学到了点什么。最基本的,我学会了以前不懂的对话框设计,学会了基本的MFC 中界面设计。还学会了每台电脑的IP 地址的获取,并且对UDP 和TCP 两种不同方式有了一些了解,并对MFA 、SOCKET 等知识有的一些认识。学会了简易聊天软件的实现和设计,完成了基本的聊天功能任务。但是在设计过程中遇到了太多麻烦。我不懂的和要学习的只是的确还有好多好多,在以后的学习中一定要好好学习程序语言设计。通过这次实验,我发现原来我们平时经常用的聊天软件原来是这样开发的,这次试验让我对程序设计语言充满好奇,我要学习的还有好多好多。
-