zl程序教程

您现在的位置是:首页 >  后端

当前栏目

基于C++实现(WinForm) LAN 的即时通信软件【100010228】

C++Winform 实现 基于 即时 LAN
2023-09-11 14:17:50 时间

基于 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