基于C语言实现的多人在线聊天系统(客户端和服务端源码)

基于C语言设计的多人在线聊天系统

一、1 任务概述

1.1 编写目的

此项目为西北工业大学网络编程的课程设计选题。目标是在 VS 下,使用 Win32 网络编程和 MFC 框架实现多人在线聊天的服务器端和客户端。用户能够选择网络中的聊天对象,通过 UDP 广播或者通过 TCP 对指定用户进行通信,方便用户的交流。最终,在 windows 操作系统下实现多人在线即时聊天功能。

1.2 项目背景

现在,即时聊天系统是目前 Internet 上最为流行的通讯方式,而各种各样的即时通讯软件也层出不穷;服务提供商也提供了越来越丰富的通讯服务功能。随着互联网的发展,即时通讯的运用将日益广泛。

广域网的聊天系统多重多样,知名的软件主要有 Facebook、腾讯 QQ 等。局域网聊天通信软件也有很多,最著名的应该是飞秋。为了学习和应用 Windows 网络通信编程,我们学习了相关知识,为了应用实践,使用网络通信中的 TCP 和 UDP 编程,实现了多人在线聊天系统。

1.3 参考资料

《Visual_C++_6.0 程序设计从入门到精通》

《VC++ 深入讲解》

《Windows 程序设计(王艳平,第二版)》

《GDI+SDK 参考(翻译版本)》

二、2 概要设计

2.1 整体框架

在这里插入图片描述

如上图所示,服务器与客户端之间采用 TCP/IP 协议通信,客户端与客户端之间通过 UDP 通信。服务器专门负责记录当前在线用户列表,并能进行广播。服务器持续保持对端口的侦听状态,每当有主机上线时,首先连接至服务器,服务器收到连接后,将该主机的进行 UDP 通信的 IP 和端口,发往其他在线主机。这样其他主机便知道该主机已上线,并知道其所在位置,从而可以进行连接和对话。在服务器进行了广播之后,因为各个主机已经知道了其他主机的位置,因此主机之间的对话可以直接进行连接。因此,使用这种模式时,各个主机依然需要保持对端口的侦听。在某台主机离线时,与登录时的模式类似,服务器会收到通知,然后转告给其他的主机。

2.2 需求概述

2.2.1 服务器端功能需求

服务器能够选择端口号并且启动服务器端的 TCP 服务,等待客户端的 Socket 连接。

服务器能够接受多个客户端的请求,并且能够识别不同的客户端。为每个客户端建立唯一的身份表示。

连接成功后,服务器能接收客户端发过来的文本信息并显示。

服务器能够选择用户,并通过界面信息框发送消息。

服务器能够清空消息缓冲区域的内容。

服务器能够选择端口号并启动服务器端的 UDP 服务,并且等待客户端的数据包,接受数据并且显示。

服务器端能够显示本机的地址和状态。

2.2.2 客户端功能需求

某用户刚登录服务器时,服务器需对其发送实时在线用户列表。

用户可以通过指定主机地址和端口号连接指定的服务器。用户在连接过程中可以选择 TCP 或者 UDP 协议进行连接。

某用户登录或退出程序时都需要给服务器发送一个消息以通知其他用户。

用户能够通过界面发送消息。

用户界面能够显示历史消息,包括其他用户传递的消息和用户自身发送的消息。用户能够通过界面清空历史消息缓冲区

状态栏能够显示用户当前连接的状态。

2.3 运行环境

2.3.1 服务器端

硬件:CPU 推荐 Pentium IV 1GHz 以上;

内存:推荐 256MB 以上;

硬盘:数据库空间 2GB 以上。

2.3.2 客户端:

操作系统:Windows7 及以上;

硬件:CPU 推荐 Pentium IV 2GHz 以上;

硬盘:系统安装需 10G 以上。

2.4 开发工具

VC++6.0 或以上版本;

操作系统:Windows7 及以上。

三、3 详细设计

3.1 服务器功能模块

首先服务器初始化 MFC 编程和 Win32 编程的基础环境,将界面窗口绑定到当前的 APP 中,并进行初始化,运行服务器端的界面程序。

服务器端初始化界面控件,从界面获取 TCP 和 UDP 的本地地址和端口号,通过新建 Socket,绑定到本地地址和端口号,运行 Socket 服务。

服务器端采用 MFC 的消息响应机制,当客户端对界面的控件进行操作时,会触发相应的消息,对不同的消息建立不同的逻辑处理功能。

服务器端采用异步消息框架对客户端的连接进行处理。当用户发送进行连接、发送消息、取消连接的时候都会出发相应的消息,然后通过对消息的处理,完成对客户端的相应。

// 设置socket为窗口通知消息类型,异步接受消息
::WSAAsyncSelect(m_socket, m_hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
// 进入监听模式
::listen(m_socket, 5);

当用户进行连接时,捕获连接请求,将用户的 socket 添加到用户列表当中。显示用户的相关信息。

case FD_ACCEPT:		// 监听中的套接字检测到有连接进入
{
    if(m_nClient < MAX_SOCKET)
    {
//定义接受者
        sockaddr_in sockAddr;
        memset(&sockAddr, 0, sizeof(sockAddr));
        int nSockAddrLen = sizeof(sockAddr);
// 接受连接请求,新的套节字client是新连接的套节字,包括客户端的地址和端口号
        SOCKET client = ::accept(s,(SOCKADDR*)&sockAddr,&nSockAddrLen);
// 设置新的套节字为窗口通知消息类型
        int i = ::WSAAsyncSelect(client,
                                 m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
        SocketAddr sa= {};
        sa.sock=client;
        sa.sockAddr=sockAddr;
        AddClient(sa);
    }
    else
    {
        MessageBox("连接客户太多!");
    }
  • 当用户发送消息时,捕获用户消息,将用户的消息显示到界面当中。
case FD_READ:		// 套接字接受到对方发送过来的数据包
{
// 取得对方的IP地址和端口号(使用getpeername函数)
// Peer对方的地址信息
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));
    int nSockAddrLen = sizeof(sockAddr);
//通过peer的方式得到对方的ip地址和端口号信息
    ::getpeername(s, (SOCKADDR*)&sockAddr, &nSockAddrLen);
//得到端口号,并将端口号转化为字符串
    u_short port = sockAddr.sin_port;
    char cs_port[10];
    sprintf_s(cs_port,10,"%d",int(port));
// 转化为主机字节顺序
    int nPeerPort = ::ntohs(sockAddr.sin_port);
// 转化为字符串IP
    CString sPeerIP = ::inet_ntoa(sockAddr.sin_addr);
// 取得对方的主机名称
// 取得网络字节顺序的IP值
    DWORD dwIP = ::inet_addr(sPeerIP);
// 获取主机名称,注意其中第一个参数的转化
    hostent* pHost = ::gethostbyaddr((LPSTR)&dwIP, 4, AF_INET);
    char szHostName[256];
    strncpy_s(szHostName,256, pHost->h_name, 256);
// 接受真正的网络数据
    char szText[1024] = { 0 };
    ::recv(s, szText, 1024, 0);
// 显示给用户
    CString strItem = CString(szHostName) + "["+ sPeerIP+ "/"+CString(cs_port)+ "]: " + CString(szText);
    AddStringToList(strItem,TRUE);
//m_listInfo.InsertString(-1,strItem);
}
  • 当用户切断连接时,捕获用户断开连接的请求。
case FD_CLOSE:		// 检测到套接字对应的连接被关闭。
{   //定义接受者
// 设置新的套节字为窗口通知消息类型
    SocketAddr sa = {};
    sa.sock=s;
    RemoveClient(sa);
    ::closesocket(s);
}

3.2 客户端功能模块

  • 运行客户端,初始客户端的应用,并启动客户端的推图形界面。
CDialog::OnInitDialog();
// 设置图标
SetIcon(theApp.LoadIcon(IDI_MAIN), FALSE);
// 创建状态栏,设置它的属性
m_bar.Create(WS_CHILD|WS_VISIBLE|SBS_SIZEGRIP, CRect(0, 0, 0, 0), this, 101);
m_bar.SetBkColor(RGB(0xa6, 0xca, 0xf0));		// 背景色
int arWidth[] = { 200, -1 };
m_bar.SetParts(2, arWidth);				// 分栏
m_bar.SetText(" TCP and UDP Client by yinkanglong and zhangmeng ", 1, 0);	// 第一个栏的文本
m_bar.SetText(" 空闲", 0, 0);				// 第二个栏的文本
// 初始化发送按钮和发送编辑框的状态
GetDlgItem(IDC_SEND)->EnableWindow(FALSE);
GetDlgItem(IDC_TEXT)->EnableWindow(TRUE);
// 初始化连接套节字
m_socket = INVALID_SOCKET;
m_UDPsocket = INVALID_SOCKET;
return TRUE;
  • 用户选择指定的服务器地址和端口号,进行 UDP 和 TCP 的连接。并能够显示连接状态。
case FD_CONNECT:	// 套节字正确的连接到服务器
{
// 设置用户界面
    GetDlgItem(IDC_CONNECT)->SetWindowText("断开连接");
    GetDlgItem(IDC_ADDR)->EnableWindow(FALSE);
    GetDlgItem(IDC_PORT)->EnableWindow(FALSE);
    GetDlgItem(IDC_TEXT)->EnableWindow(TRUE);
    GetDlgItem(IDC_SEND)->EnableWindow(TRUE);
    m_bar.SetText(" 已经连接到服务器", 0, 0);
}
  • 用户能够接受其他人发送的消息,通过异步消息处理框架对消息进行处理。
case FD_READ:		// 套接字接受到对方发送过来的数据包
{
// 从服务器接受数据
    char szText[1024] = { 0 };
    ::recv(s, szText, 1024, 0);
// 显示给用户
    AddStringToList(CString(szText) + "\r\n");
}
break;
  • 客户端能够启动新的线程,进行 UDP 的连接。
if(m_UDPsocket == INVALID_SOCKET)  // 开启服务
{
// 取得目标地址
    CString sAddr;
    GetDlgItem(IDC_ADDR)->GetWindowText(sAddr);
    if(sAddr.IsEmpty())
    {
        MessageBox("请输入服务器地址!");
        return;
    }
// 取得UDP端口号
    CString sPort;
    GetDlgItem(IDC_EDIT_UDP_PORT)->GetWindowText(sPort);
    int nPort = atoi(sPort);
    if(nPort < 1 || nPort > 65535)
    {
        MessageBox("端口号错误!");
        return;
    }
    remoteUDPAddress.sin_family=AF_INET;
    remoteUDPAddress.sin_port = htons(nPort);
    remoteUDPAddress.sin_addr.S_un.S_addr = inet_addr(sAddr);
// 创建监听套节字
    m_UDPsocket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
    if (m_UDPsocket == SOCKET_ERROR)
    {
        MessageBox("启动服务出错!");
        return;
    }
//绑定随机地址信息
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = 0;
    sin.sin_addr.s_addr = INADDR_ANY;
// 绑定端口
    if(::bind(m_UDPsocket,(sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR)
    {
        MessageBox("端口号绑定失败");
        return;
    }
//开启多线程,用来接收数据包
//MessageBox("UDP线程启动前");
    h_udpThread = ::CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)udpRec,this,0,NULL);
//udpRec();
//MessageBox("UDP线程启动后");
//if(!CreateAndListen(nPort))
//{
//	MessageBox("启动服务出错!");
//	return;
//}
// 设置相关子窗口控件状态
    GetDlgItem(IDC_BUTTON_UDP_START)->SetWindowText("停止UDP客户端");
//m_bar.SetText(" 正在监听……", 0, 0);
//GetDlgItem(IDC_EDIT_UDP)->EnableWindow(FALSE);
}
else				// 停止服务
{
    MessageBox("UDP服务终止");
// 关闭所有UDP连接
    CloseUDPSocket();
// 设置相关子窗口控件状态
    GetDlgItem(IDC_BUTTON_UDP_START)->SetWindowText("开启UDP客户端");
//m_bar.SetText(" 空闲", 0, 0);
//GetDlgItem(IDC_EDIT_UDP)->EnableWindow(TRUE);
}
  • 用户能够将记录当前连接的其他用户。

3.3 服务器与客户端通信(TCP)

tcp 过程

首先初始化 CWinAPP 函数的一些东西,包括初始化唯一的窗口变量

点击开始 OnStart 函数触发,取得端口号,设置了控件状态

OnStart 调用 CreateAndListen 函数,创建了服务器 Socket 服务,并采用异步消息监听模式

OnSocket 负责接收 TCP 异步消息进行处理。接受 Socket 请求,关闭 Socket 请求,读取 Socket 数据流

其中由辅助函数 AddClient RemoveClient UpdateClientList 实现连接列表的控制。

由辅助函数 CloseAllSocket 进行关闭连接,AddStringToList 用来显示数据

发送部分首先由 OnBnClickedButtonSelect 函数对发送对象列表进行控制

由 OnBnClickedButtonSend 函数实现内容发送到发送对象列表

3.4 客户端与客户端之间通信(UDP)

3.4.1 UDP 服务器端过程

点击开始 OnBnClickedButtonUdp 函数触发,取得端口号,设置了控件状态。

OnBnClickedButtonUdp 使用多线程调用了 udpRec 函数,实现了多线程启动 udp 接受服务。

udpRec 函数负责接受同步的 UDP 消息,并对消息进行显示和处理。

其中由辅助函数 AddUDPClient RemoveUDPClient 对 UDP 用户列表实现控制

由副主函数 CloseUDPSocket 进行关闭连接

#发送部分首先 OnBnClickedButtonUdpSedn 对 UDP 用户列表遍历发送数据包

3.4.2 UDP 客户端过程:

点击开始 OnBnClickedButtonUdp 函数触发,取得端口号,设置了控件状态。

OnBnClickedButtonUdp 使用多线程调用了 udpRec 函数,实现了多线程启动 udp 接受服务。

udpRec 函数负责接受同步的 UDP 消息,并对消息进行显示和处理。

由副主函数 CloseUDPSocket 进行关闭连接

3.5 界面设计

猜你喜欢

转载自blog.csdn.net/newlw/article/details/125167964