基于C++实现(WinForm) LAN 的即时通信软件【100010228】
基于 LAN 的即时通信软件的设计
一、概述
1.1 设计目的:
设计一个基于 LAN 的即时通信软件,实现在局域网下可靠的、稳定的即时通信功能以及其从属的附加功能。
1.2 设计内容:
1.2.1 功能设计:
·实现一对一的单播通信,包括消息发送与接收以及文件的发送与接收;
·实现一对一的单播通信,包括消息发送与接收以及文件的发送与接收;
·附加功能:实现登陆、注册、获取当前在线情况等功能;
1.2.2 界面设计:
·客户端的交互界面设计。
1.2.3 客户端、服务器设计:
·客户端需要完成的功能;
·服务器需要完成的功能;
·客户端、服务器的交互设计;
1.3 设计要求:
结合《计算机网络》课程所学的知识以及查阅相应的资料完成相应的设计内容,且需要保证设计的质量以及程序的可靠性和稳定性。
二、设计任务分析
2.1 功能设计分析:
·实现一对一的单播、多播通信:
主要运用消息转发技术,需要服务器来处理消息的解析和转发;其中消息的解析包括获取消息的发送者、接收者、类别;针对不同的解析结果需要做出不同的响应。
·实现附加功能:实现登陆、注册、获取当前在线情况等功能;将客户端对附加功能的调用当作特殊
的请求消息发送给服务器,服务器解析后做出不同的响应。
2.2 界面设计:
客户端界面需要有较好的交互性,因此需要设计:
·登陆、注册对话框:包括用户名输入框、登陆和注册按钮;
·主界面对话框:包括消息发送编辑框、消息接收显示区、好友在线情况显示区、发送按钮、以及登陆按钮;
2.3 客户端、服务器设计:
2.3.1 客户端设计:
·获取客户所发送的消息内容;
·根据客户要求封装消息并发送消息;
·接收服务器发来的消息;
·解析接收的消息并执行对应响应的功能;
2.3.2 服务器端设计:
·获取客户端发来的消息
·解析消息并执行对应的处理
·将处理结果封装成消息发送给指定客户
三、总体设计
3.1 界面设计结果
3.1.1 登陆、注册对话框
3.1.2 主界面对话框
说明:
编辑框 1:消息接收窗口编辑框 2:消息发送编辑框编辑框 3:消息接收者编辑框编辑框 4:当前在线用户显示框
“发送”按钮:发送编辑框 2 中的内容给编辑框 3 内对应的客户;
“文件”按钮:发送编辑框 2 中对应的文件给编辑框 3 内对应的客户;
“登陆”按钮:打开登陆、注册对话框;
3.2 客户端程序处理流程图:登陆、注册处理:
普通消息发送、文件发送、以及消息接受
3.3 服务器端程序处理流程图
四、程序实现
4.1 消息结构体:
4.1.1 消息结构体
structmes//消息结构体
{
char from[32];//发送者
char to[32];//接收者
char content[MAX_PATH];//消息内容
};
4.1.2 消息模板:
4.1.3 解释:
客户端和服务器会根据消息的三部分内容进行消息解析,不同的解析结果对应不同的消息处理。
4.2 客户端程序实现:
4.2.1 客户端自定义套接字类(继承 MFC 抽象类 CSOCKET)实现:
class CMySocket : public CSocket
{
// Attributes
public:
CLanMessageDlg *dlg;//主对话框指针
// Operations
public:
CMySocket();
virtual ~CMySocket();
void OnReceive(int n);//消息接收响应
// Overrides
public:
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CMySocket)
//}}AFX_VIRTUAL
// Generated message map functions
//{{AFX_MSG(CMySocket)
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_MSG
// Implementation
protected:
};
void CMySocket::OnReceive(int n)//消息响应处理
{
if(dlg->online)//在线才进行响应
{
mes trans;
if(!Receive((void*)&trans,sizeof(trans))){return;}//获取服务器消息
CString txt1=trans.from,txt2="在线用户";
if(txt1==txt2)//如果是"在线客户"消息
{
CString show;
show.Format("当前用户:%s\r\n%s:%s",dlg->me,trans.from,trans.content);
dlg->m_online=show;
dlg->UpdateData(false);//刷新主界面
}
else if(trans.from[0]=='_')//如果是文件消息
{
CStdioFile put;
CString name=trans.from;
name=name.Mid(1);//获取文件名
if(!put.Open(name,CFile::modeCreate|CFile::modeWrite))//将文件内容存储
{ AfxMessageBox("创建文件失败!");return;}
else put.WriteString(trans.content);
put.Close();
}
else {//如果是普通消息
CString show;
show.Format("From %s:%s\r\n",trans.from,trans.content);
dlg->m_show+=show;
dlg->UpdateData(false);//刷新主界面
}
}
}
4.2.2 登陆、注册对话框实现:
class CLanMessageDlg : public CDialog
{
// Construction
public:
CString me;//客户名字
CLanMessageDlg(CWnd* pParent = NULL); // standard constructor
bool online;//是否在线
CSocket *client;//套接字指针
// Dialog Data
//{{AFX_DATA(CLanMessageDlg)
enum { IDD = IDD_LANMESSAGE_DIALOG };
CString m_online;//在线情况
CString m_to;//消息接收者
CString m_message;//要发送的消息
CString m_show;//接收到的消息
CString m_tips;
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CLanMessageDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
//{{AFX_MSG(CLanMessageDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnButtonlogin();
afx_msg void OnButtonsend();
afx_msg void OnButtonfile();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
void CLOGINDLG::OnButtonlogin() //向服务端申请登陆
{
// TODO: Add your control notification handler code here
UpdateData(true);//刷新获取用户名
mes a;
sprintf(a.from,"%s",m_name);
sprintf(a.content,"登陆");//消息封装
//消息发送
if(!dlg->client->Send((void*)&a,sizeof(a))) {MessageBox("错误发送");return;}
dlg->me=m_name;
mes trans;
//获取登陆结果
if(!dlg->client->Receive((void*)&trans,sizeof(trans))){return;}
CString txt1="您已登陆成功!",txt2=trans.content;
if(txt1==txt2) //如果登陆成功
{
dlg->online=true;
GetDlgItem(IDC_BUTTONLOGIN)->EnableWindow(false);//禁用登陆和注册按钮
GetDlgItem(IDC_BUTTONREGIST)->EnableWindow(false);
}
else
{
MessageBox(trans.content);//打印登陆失败原因
}
}
void CLOGINDLG::OnButtonregist()//向服务端申请注册
{
// TODO: Add your control notification handler code here
UpdateData(true);//获取注册用户名
mes a;
sprintf(a.from,"%s",m_name);
sprintf(a.content,"注册");//封装注册消息
//发送消息
if(!dlg->client->Send((void*)&a,sizeof(a))) {MessageBox("错误发送");return;}
mes trans;
//接收注册结果
if(!dlg->client->Receive((void*)&trans,sizeof(trans))){return;}
CString txt1="注册成功!",txt2=trans.content;
if(txt1==txt2) //如果注册成功
{
MessageBox(trans.content);
}
}
4.2.3 主对话框实现:
void CMySocket::OnReceive(int n)//消息响应处理
{
if(dlg->online)//在线才进行响应
{
mes trans;
if(!Receive((void*)&trans,sizeof(trans))){return;}//获取服务器消息
CString txt1=trans.from,txt2="在线用户";
if(txt1==txt2)//如果是"在线客户"消息
{
CString show;
show.Format("当前用户:%s\r\n%s:%s",dlg->me,trans.from,trans.content);
dlg->m_online=show;
dlg->UpdateData(false);//刷新主界面
}
else if(trans.from[0]=='_')//如果是文件消息
{
CStdioFile put;
CString name=trans.from;
name=name.Mid(1);//获取文件名
if(!put.Open(name,CFile::modeCreate|CFile::modeWrite))//将文件内容存储
{ AfxMessageBox("创建文件失败!");return;}
else put.WriteString(trans.content);
put.Close();
}
else {//如果是普通消息
CString show;
show.Format("From %s:%s\r\n",trans.from,trans.content);
dlg->m_show+=show;
dlg->UpdateData(false);//刷新主界面
}
}
}
void CLanMessageDlg::OnButtonlogin() //登陆按钮实现
{
// TODO: Add your control notification handler code here
CLOGINDLG *dlg;
dlg=new CLOGINDLG();
dlg->dlg=this;
dlg->DoModal();//显示登陆、注册对话框
//登陆成功才可发送
if(online) {
GetDlgItem(IDC_BUTTONSEND)->EnableWindow(true);
GetDlgItem(IDC_BUTTONFILE)->EnableWindow(true);
}
}
void CLanMessageDlg::OnButtonsend() //普通消息发送
{
UpdateData(true);
mes send;
sprintf(send.from,"%s",me);
sprintf(send.content,"%s",m_message);
sprintf(send.to,"%s",m_to);//封装普通消息
if(!client->Send((void*)&send,sizeof(send))) {MessageBox("错误发送");return;}
m_show+="To"+m_to+":"+m_message+"\r\n";
m_message="";
UpdateData(false);//刷新屏幕
// TODO: Add your control notification handler code here
}
void CLanMessageDlg::OnButtonfile() //发送文件
{
// TODO: Add your control notification handler code here
UpdateData(true);
CStdioFile get;
if(!get.Open(m_message,CFile::typeText,NULL)) {MessageBox("无效文件!");return;}//如果打开文件成功
CString txt,temp;
while(get.ReadString(temp))
txt+=temp+"\r\n";
MessageBox(txt);
mes send;//通知对方有文件传来
sprintf(send.from,"%s",me);
sprintf(send.content,"发送文件");
sprintf(send.to,"%s",m_to);
if(!client->Send((void*)&send,sizeof(send))) {MessageBox("错误发送");return;}
sprintf(send.from,"_%s",get.GetFileName());//封装文件内容
sprintf(send.content,"%s",txt);
sprintf(send.to,"%s",m_to);
//将文件发送过去
if(!client->Send((void*)&send,sizeof(send))) {MessageBox("错误发送");return;}
get.Close();
}
4.3 服务器程序实现:
# include <iostream>
# include <string>
# include <winsock2.h>
# include <vector>
# include <fstream>
using namespace std;
# pragma comment(lib, "ws2_32.lib")
# define PORT 3000//服务器ip和端口号
# define IP_ADDRESS "127.0.0.1"
struct mes//消息结构体
{
char from[32];//发送者
char to[32];//接收者
char content[MAX_PATH];//内容
};
struct user//用户结构体
{
string name;//用户名
SOCKET socket;//客户宿主套接字
bool online;//是否在线
};
string login="登陆",regist="注册";
static vector<user> client;//客户动态数组
inline void updateonline()//更新在线人数
{
string txt;
for(int i=0;i<client.size();i++)
{
if(client[i].online)
{
txt+="\r\n"+client[i].name;
}
}
mes a;
sprintf(a.content,"%s",txt.c_str());
sprintf(a.from,"在线用户");
for(i=0;i<client.size();i++)//将情况反馈给在线用户
{
if(client[i].online)
{
send(client[i].socket,(char*)&a,sizeof(a),0);
}
}
}
user* handlelogin(mes rec,user *who)//处理登陆
{
bool log_error=true;
for(int i=0;i<client.size();i++)
{
if(rec.from==client[i].name&&client[i].online==false) //注册且不在线,则登陆
{
client[i].socket=who->socket;//获取宿主套接字
client[i].online=true;//设置在线
cout<<rec.from<<"登陆成功"<<endl;
mes a;
sprintf(a.from,"server");
sprintf(a.content,"您已登陆成功!");
send(client[i].socket,(char*)&a, sizeof(a), 0);//反馈给客户端
who->name=client[i].name;
log_error=false;
return &client[i];
}
if(rec.from==client[i].name&&client[i].online)//如果注册且在线,提示已经登陆
{
mes a;
sprintf(a.from,"server");
sprintf(a.content,"该用户已经登陆!");
send(who->socket,(char*)&a, sizeof(a), 0);
return who;
}
}
if(log_error) //未注册则提醒注册
{
mes a;
sprintf(a.from,"server");
sprintf(a.content,"该用户未注册!");
send(who->socket,(char*)&a, sizeof(a), 0);
return who;//如果登陆失败
}
}
void transmit(mes rec,user *who)//消息转发
{
string name=rec.to;
if(name=="群发")//群发消息
{
for(int i=0;i<client.size();i++)
{
if(rec.from!=client[i].name&&client[i].online)send(client[i].socket,(char*)&rec, sizeof(rec), 0);
}
}
else//将接收到的消息转发给指定的客户
{
for(int i=0;i<client.size();i++)
{
if(name==client[i].name&&client[i].online) send(client[i].socket,(char*)&rec, sizeof(rec), 0);
}
}
}
void handleregist(mes rec,user *who)//处理注册
{
user _user;
_user.name=rec.from;
_user.online=false;
client.push_back(_user);//存入动态数组
ofstream out;
out.open("user.txt",ios_base::app);
out<<" "<<_user.name;
out.close();//存进数据文件
mes a;
sprintf(a.from,"server");
sprintf(a.content,"注册成功!");//反馈
send(who->socket,(char*)&a, sizeof(a), 0);
}
DWORD WINAPI ClientThread(LPVOID lpParameter)//交互线程
{
struct sockaddr_in ClientAddr;
int AddrLen = sizeof(ClientAddr);
SOCKET CientSocket =(SOCKET)lpParameter;//获取连接的套接字
int Ret = 0;
user *who=new user;
who->socket=(SOCKET)lpParameter;
who->online=false;
mes rec;
//
while ( true )
{
//recv
Ret = recv(CientSocket, (char*)&rec, sizeof(rec), 0);
getpeername(CientSocket, (struct sockaddr *)&ClientAddr, &AddrLen);
if ( Ret == 0 || Ret == SOCKET_ERROR ) //客户端退出、下线处理
{
cout << "客户" << inet_ntoa(ClientAddr.sin_addr) << " ; " << ClientAddr.sin_port << " quit! " << endl;//客户端退出
cout<<who->name<<"下线"<<endl;
who->online=false;
updateonline();
break;
}
cout << "客户" << inet_ntoa(ClientAddr.sin_addr) << " ; " << ClientAddr.sin_port <<"---say : " <<rec.from<<":"<< rec.content<< endl;//输出服务端收到的信息
string handle=rec.content;//信息处理选项
if(handle==login) //处理登陆
{
who=handlelogin(rec,who);
updateonline();
}
else if(handle==regist) handleregist(rec,who);//处理注册
else transmit(rec,who);//消息转发
}
return 0;
}
void readuser()//读取用户
{
ifstream in;
in.open("user.txt");
if (in)
{
while(!in.eof())
{
user _user;
in>>_user.name;
_user.online=false;
client.push_back(_user);
}
in.close();
}
}
int run()//创建服务器套接字并开始监听
{
WSADATA Ws;
SOCKET ServerSocket,clientsocket;
struct sockaddr_in LocalAddr, ClientAddr;
int Ret = 0;
int AddrLen = 0;
HANDLE hThread = NULL;
//1.初始化windows套接字
if ( WSAStartup(MAKEWORD(2, 2), &Ws) != 0 ){cout<<"Init Windows Socket Failed:"<<GetLastError()<<endl;return -1;}
//2.创建套接字
ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if ( ServerSocket == INVALID_SOCKET ){cout<<"Create Socket Failed:"<<GetLastError()<<endl;return -1;}
LocalAddr.sin_family = AF_INET;
LocalAddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
LocalAddr.sin_port = htons(PORT);
memset(LocalAddr.sin_zero, 0x00, 8);
//3.绑定套接字
Ret = bind(ServerSocket, (struct sockaddr*)&LocalAddr, sizeof(LocalAddr));
if ( Ret != 0 ){cout<<"Bind Socket Failed:"<<GetLastError()<<endl;return -1;}
//4.监听用户,最大用户量为10
Ret = listen(ServerSocket, 10);
if ( Ret != 0 ){cout<<"listen Socket Failed:"<<GetLastError()<<endl;return -1;}
cout<<"服务端已经启动"<<endl;
//开始运转
while(true)
{
//接受连接
AddrLen = sizeof(ClientAddr);
clientsocket = accept(ServerSocket, (struct sockaddr*)&ClientAddr, &AddrLen);
if (clientsocket == INVALID_SOCKET ){cout<<"Accept Failed::"<<GetLastError()<<endl;break;}
//打印客户端连接
cout<<"客户端连接: ip.addr : "<<inet_ntoa(ClientAddr.sin_addr)<<" ; port: "<<ClientAddr.sin_port<<endl;
//将信息交换转接给线程执行
hThread = CreateThread(NULL, 0, ClientThread, (LPVOID)clientsocket, 0 , NULL);
if ( hThread == NULL ){cout<<"Create Thread Failed!"<<endl;break;}
CloseHandle(hThread);
}
closesocket(ServerSocket);
WSACleanup();
}
int main()
{
readuser();//读取用户
run();
return 0;
}
五、运行结果:
5.1 登陆、注册:
5.1.1“张三”登陆
5.1.2“张三”登陆成功:显示用户名及当前在线用户
5.1.3 登陆在线用户:显示用户已经登陆,登录失败
5.1.4 登陆未注册用户:显示用户未注册,提示注册
5.1.5 用户注册:提示注册成功
5.2 消息发送:
5.2.1 单发消息:
5.2.2 群发消息:
5.3 文件发送:
小方向张三发送一个文件,消息盒子显示文件内容
文件被存储到桌面,用记事本打开文件:
六、心得与体会
6.1 遇到的问题及解决方案
6.1.1 服务器如何与多个客户端进行交互?
解决方案:服务器主线程负责监听客户端的连接,每连接一个客户就启动一个线程,把客户端套接字传给线程,线程来处理与客户端的交互。总之,接受了多少个客户的连接就需要建立多少个处理线程。
实现方法:
6.1.2:如何实现客户端消息的非阻塞式接收?
解决方案:客户端的是图形界面程序,故不太适合循环阻塞式消息接收,需要利用 MFC 类库提供的消息响应机制来实现非阻塞式接收。本程序使用了 MFC 类库中 CSocket 类提供的 OnReceive()接口函数来实现非阻塞式接收。
实现方法:
6.2 心得与体会:
本次《计算机网络》课程设计我选择了“基于 Lan 的即时通信软件设计”这个题目。选择这个题目的原因主要有两个:其一是它比较贴近真实的生活应用场景,其二是想借此机会对”Winsock”进行更深层次的了解,以便日后更好的进行网络程序设计。
在刚开始翻看简单的 Winsock 编程案例时,代码的实现让人一头雾水,情形和刚开始接触 MFC 编程的时侯是一模一样的,Window 系统提供的函数接口复杂多样,在没有仔细了解 Winsock 的用法和原理情况下,我花了很长的时间也没有读懂案例的程序代码。为此,我去图书馆借阅了 windows 网络编程相关的书籍。接着一边看案例,一边看书、编代码,最终弄懂了 Winsock 的基本用法。而剩下的消息处理工作对我来说就相对容易,我很快地就将它们完成了。
最后,希望在今后的实践中自己能够养成多查阅资料、多翻看编程实例并且多动手的好习惯,也希望自己能够多吸取他人的编程经验并提高自己的编程效率。
♻️ 资源
大小: 5.26MB
➡️ 资源下载:https://download.csdn.net/download/s1t16/87354165