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

基于 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 的基本用法。而剩下的消息处理工作对我来说就相对容易,我很快地就将它们完成了。

最后,希望在今后的实践中自己能够养成多查阅资料、多翻看编程实例并且多动手的好习惯,也希望自己能够多吸取他人的编程经验并提高自己的编程效率。

参考文献:

[1]曹衍龙,刘海英,VisualC++ 网络通信编程实用案例精选.北京:人民邮电出版社,2006[2]谢希仁,计算机网络(第七版).北京:电子工业出版社,2017.1.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神仙别闹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值