多人在线聊天服务器

欢迎诸位来阅读在下的博文~

在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共勉共勉!

江山如画,客心如若,欢迎到访,一展风采

文章目录

一、背景

当今即时通信软件繁多,其中有名的就有QQ、微信、MSN Messenger、NET Messenger Service、ICQ等等。其主要用途是传递文字信息与传输文件。使用socket建立通信渠道,多线程实现多台计算机同时进行信息的传递。通过简单的注册登录后,即可在网络中成功进行即时聊天。

即时通信(Instant Message,IM),这是一种可以让使用者在网络上建立某种私人聊天室的实时通信服务。大部分的即时通信服务都提供了状态信息的特征:显示联络人名单、联络人是否在线和能否与联络人交谈。

通常IM服务会在使用者清单(类似电话薄)上的某个人连上IM时,发出信息通知使用者,使用者可据此与此人通过网络开始进行实时的IM文字通信。除了文字外,在频宽充足的前提下,大部分IM服务事实上也提供视频通信能力。实时传讯与电子邮件最大的不同在于不用等候,不需要每隔两分钟就按一次"传送与接收",只要两个人都在线,就像多媒体电话一样传送文字、文件、声音、图像给对方。

二、系统平台的选择

1.应用系统平台模式的选择

所谓平台模式或计算结构是指应用系统的体系结构,简单来说就是系统的层次、模块结构。常见的四种模式如下:

(1)主机------终端模式

(2)单机模式

(3)客户机/服务器模式(C/S模式)

(4)浏览器/n层服务器模式(B/nS模式)

考虑到的场景应用是要在公司或某单位内部建立起服务器,还要再每台计算机里安装相关的通信系统(客户端),所以我们选择研究的系统模式为C/S模式。

2.C/S模式介绍

C/S(Client/Server)模式,即客户端/服务器模式,是一种网络计算模型,它将任务和服务分为两个主要部分:客户端(Client)和服务器端(Server)。在这种模型中,客户端负责发送请求,服务器端则负责处理这些请求并返回响应。以下是C/S模式的基本介绍:

客户端(Client)

  1. 角色:客户端是请求服务的发起者,通常是一个用户界面,允许用户请求服务器提供的数据或服务。

  2. 功能

    • 发送请求:客户端向服务器发送请求,请求可以包括数据查询、文件传输、计算任务等。
    • 接收响应:客户端接收服务器处理请求后返回的响应,并将结果显示给用户。
  3. 特点

    • 可以是桌面应用程序、移动应用程序或Web浏览器。
    • 通常具有用户友好的界面。
    • 可能需要安装特定的客户端软件。

服务器端(Server)

  1. 角色:服务器端是提供服务的实体,负责处理客户端的请求,执行任务,并返回结果。

  2. 功能

    • 处理请求:服务器接收客户端的请求,根据请求类型执行相应的操作。
    • 数据存储:服务器通常负责存储和管理数据。
    • 响应客户端:服务器将处理结果返回给客户端。
  3. 特点

    • 通常运行在强大的硬件上,以支持多个客户端的请求。
    • 可以是Web服务器、数据库服务器、文件服务器等。
    • 需要持续运行,保证服务的可用性。

C/S模式的优点

  1. 分布式计算:将计算任务分布在客户端和服务器端,可以充分利用网络资源。
  2. 易于维护:服务器端集中管理数据和服务,便于维护和更新。
  3. 扩展性:可以轻松地扩展服务器端以支持更多的客户端。
  4. 安全性:可以在服务器端实施安全策略,保护数据安全。

C/S模式的缺点

  1. 依赖服务器:客户端的功能依赖于服务器端的可用性。
  2. 网络通信:C/S模式依赖于网络通信,网络延迟可能会影响用户体验。
  3. 成本:需要投资于服务器硬件和软件,以及维护成本。

C/S模式广泛应用于各种网络应用中,如Web服务、电子邮件、数据库应用等。随着技术的发展,C/S模式也在不断演变,例如在云计算和移动计算领域,出现了新的变体和优化。

3.数据库系统的选择

现在可以使用的数据库有很多种,包括但不限于:MySQL, DB2, Informix, Qracle和SQL Server。基于满足需求。价格和技术三方面的考虑,本系统在分析开发过程中采用MySQL作为数据库系统。

三、系统需求分析

1.即时消息的一般需求

即时消息的一般需求包括格式需求、可靠性需求和性能需求。

  • 格式需求

    (1)所有实体必须至少使用一种消息格式。

    (2)一般即时消息格式必须定义发信者和即时收件箱的标识。

    (3)一般即时消息格式必须包含一个让接收者可以回消息的地址。

    (4)一般即时消息格式应该包含其它通信方法和联系地址,例如电话号码、邮件地址等。

    (5)一般即时消息格式必须允许对信息有效负载编码和鉴别(非ASCII内容)。

    (6)一般即时消息格式必须反映当前最好的国际化实践。

    (7)一般即时消息格式必须反映当前最好的可用性实践。

    (8)必须存在方法,在扩展一般即时消息格式时,不影响原有的域。

    (9)必须提供扩展和注册即时消息格式的模式的机制。

  • 可靠性需求

    协议必须存在机制,保证及时消息成功投递,或者投递失败时发信者获得足够的信息。

  • 性能需求

    (1)即时消息的传输必须足够迅速。

    (2)即时消息的内容必须足够丰富。

    (3)即时消息的长度尽量足够长。

2.即时消息的协议需求

协议是一系列的步骤,它包括双方或者多方,设计它的目的是要完成一项任务。即时通信协议,参与的双方或者多方是即时通信的实体。协议必须是双方或者多方参与的,一方单独完成的不算协议。在协议操作的过程中,双方必须交换信息,包括控制信息、状态信息等。这些信息的格式必须是协议参与方同意并且遵循的。好的协议要求清楚完整,每一步必须有明确的定义,并且不会引出误解;对每种可能的情况必须规定具体的动作。

3.即时消息的安全需求

A发送即时消息M给B,有以下几种情况和相关需求:

(1)如果无法发送,A必须接到确认。

(2)如果M被投递了,B只能接受一次M。

(3)协议必须为B提供方法检查A是否发送了这条消息。

(4)协议必须允许B使用另一条即时消息来回复信息。

(5)协议不能暴露A的IP地址。

(6)协议必须为A提供方法保证没有其他个体C可以看到M。

(7)协议必须为A提供方法保证没有其他个体C可以篡改M。

(8)协议必须为B提供方法鉴别是否发生篡改。

(9)B必须能够阅读M,B可以阻止A发送消息给他。

(10)协议必须允许A使用现在的数字签名标准对信息进行签名。

4.即时消息的加密和鉴别

(1)协议必须提供方法保证通知和即时消息的置信度,确保信息未被监听或者破坏。

(2)协议必须提供方法保证通知和即时消息的置信度,确保信息未被重排序或者回放。

(3)协议必须提供方法保证通知和即时消息被正确的实体阅读。

(4)协议必须允许客户自己使用方法确保信息不被截获、不被重放和解密。

5.即时消息的注册需求

(1)即时通讯系统拥有多个账户,允许多个用户注册。

(2)一个用户可以注册多个ID。

(3)注册所使用的账号类型为字母ID。

6.即时消息的通信需求

(1)用户可以传输文本消息。

(2)用户可以传输RTF格式消息。

(3)用户可以传输多个文件/文件夹。

(4)用户可以加密/解密信息等。

四、系统总体设计

该系统的命名为MyICQ,现在对该系统采用客户机服务器的模式来进行总体设计,它是一个3层的C/S结构:数据库服务器-->应用程序服务器-->应用程序客户端,其分层机构如图所示:

  • 客户层也叫应用表示层,即我们所说的客户端,这是应用程序的用户接口部分。为即时通信工具设计一个客户层有很多优点,这是因为客户层担负着用户与应用之间的对话功能。它用于检查用户的输入数据,显示应用的输出数据。为了使用户能直接进行操作,客户层需要使用图形用户接口。如果通信用户变更,系统只需要改写显示控制和数据检查程序就可以了,而不会影响其它两层。数据检查的内容限于数据的形势和值的范围,不包括有关业务本身的处理逻辑。
  • 服务层又叫功能层,相当于应用层的本体,它是将具体业务处理逻辑编入程序中。例如,用户需要检查数据,系统设法将有关见所要求的信息一次性的传送给功能层;而用户登录后,聊天登录信息是由功能层处理过的检索结果数据,他也是一次性传送给功能层的。在应用设计中,必须避免在表示层和功能层之间进行多次的数据交换,这就需要尽可能一次性的业务处理,达到优化整体设计的目的。
  • 数据层就是DBMS,本系统使用了MySQL数据库服务器来管理数据。MySQL能迅速执行大量数据的更新和检索,因此,从功能层传送到数据层的"要求"一般都使用SQL语言。

五、即时通信系统的实施原理

IM的工作方式如下:用户登录IM通信服务器,获取一个自建立的历史交流对象列表,然后自身标志位在线状态,当好友列表中的某人在任何时候登陆上线并试图通过计算机联系用户时,IM系统会发一个消息提醒该用户,然后用户能与此人建立一个聊天会话通道进行各种消息交流。

IM的基本技术原理

(1)用户A输入自己的用户名和密码登录IM服务器,服务器通过读取用户数据库来检验用户身份。如果检验通过,登录用户A的IP地址、IM客户端软件的版本号及使用的TCP/UDP端口号,然后返回用户A登录成功的标志,此时用户A在IM系统中为在线状态。

(2)根据用户A存储在IM服务器上的好友列表,服务器将用户A在线等相关信息发送给同时在线的IM好友的PC,这些信息包括在线状态、IP地址、IM客户端使用的TCP端口号等,IM好友的客户端收到此信息后将在客户端软件的界面上显示。

(3)IM服务器把用户A存储在服务器上的好友列表及相关信息回送到其客户端,这些信息包括在线状态、IP地址、IM客户端使用的TCP端口号信息,用户A的IM客户端收到后将显示这些好友列表及其在线状态。

IM通信方式

  1. 在线直接通信
  2. 在线代理通信
  3. 离线代理通信
  4. 扩展方式通信

六、功能模块的划分

模块划分

服务器端多线程

  • 服务器
    服务器端需要和多个客户端同时进行通信,简单来说这就是服务器段端的多线程。如果服务器发现一个新的客户端并与之连线,则马上新建一个线程与该客户端进行通信。用多线程的好处在于可以同时处理多个通信连接,不会出现由于数据排队等待而发生延迟或者丢失等问题,可以很好地利用系统的性能。
    服务器为每一个连接着的客户建立一个线程,为了同时相应多个客户端,需要设计一个主线程来启动服务器端。主线程与进程结构类似,他在获得新连接时生成一个线程来处理这个连接。线程调度的速度很快、占用资源少,可共享进程空间中的数据,因此服务器的响应速度较快,且I/O吞吐量较大。
  • 客户端
    客户端能够完成信息的接受和发送操作,这与服务器的多线程概念不同,他可以采用循环等待的方法来实现客户端。利用循环等待的方式,客户端首先接受用户输入的内容并将其发送到服务器,然后接受来自服务器的信息,将其返回给客户端的用户。

七、数据库设计

MySQL是一个真正的多用户、多线程SQL数据库服务器。它是以客户机/服务器结构实现的,由一个服务器守护程序mysqld以及很多不同的客户程序和库组成。它能够快捷、有效和安全地处理大量的数据。相对于Qracle等数据库来说,MySQL的使用非常简单。

数据库脚本设计

cpp 复制代码
DROP DATABASE IF EXISTS chatdb;
create database chatdb default character set utf8 collate utf8_bin;
 
flush privileges;

use chatdb;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;


SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for qqnum
-- ----------------------------
DROP TABLE IF EXISTS `qqnum`;
CREATE TABLE `qqnum` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

验证

八、服务器端设计

客户端和服务器是TCP连接并交互的。服务器端的主要功能如下:

(1)接受客户端用户的注册,然后把注册信息保存到数据库表中。

(2)接受客户端用户的登录,用户登录成功后,就可以在聊天室里面聊天了。

该聊天服务器采用select通信模型,目前没有使用到线程池,如果以后并发需求大了,很容易可以扩展到"线程池+select模型"的方式。服务器端收到客户端连接后,就开始等待客户端的要求,具体要求是通过客户端发来的命令来实现的,具体命令如下:

cpp 复制代码
#define CL_CMD_REG 'r'  //客户端请求注册命令
#define CL_CMD_LOGIN 'l'	//客户端请求登录命令
#define CL_CMD_CHAT 'c'		//客户端请求聊天命令

这几个命令号服务器和客户端必须一致。命令号是包含在通信协议中的,通信协议是服务器端和客户端相互理解对方要求的手段。这里的协议设计得比较简单,但也可以满足交互需求。

客户端发送给服务器端的协议:

命令号 , 参数(字符串,长度不定)

比如:"r,Tom" 表示客户端要求注册,用户名是Tom。

服务器端发送给客户端的协议:

命令号 , 返回结果(字符串,长度不定)

并发聊天服务器的详细设计

以下是服务器的核心代码:

cpp 复制代码
    while(1){
        rset = allset;

        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if(nready < 0)
            puts("select error");
            
        // 一、listenfd是否在rset集合中,处理acception事件
        if(FD_ISSET(listenfd, &rset)){
            cliaddr_len = sizeof(cliaddr);
            // accept返回通信套接字,当前非阻塞,因为select已经发生读写事件
            connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);

            printf("received from %s at PORT %d\n", 
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), 
                    ntohs(cliaddr.sin_port));

            for(i = 0; i < FD_SETSIZE; i++){
                if(client[i] < 0){
                    client[i] = connfd; //保存accept返回的通信套接字connfd存到client[]里
                    break;
                }
                
            }

            if(i == FD_SETSIZE){
                fputs("too many clients\n", stderr);
                exit(1);
            }

            FD_SET(connfd, &allset); //将connfd加入allset集合

            //更新最大文件描述符数
            if(connfd > maxfd)
                maxfd = connfd;

            if(i > maxi)
                maxi = i; //更新client[]最大下标值

            if(--nready == 0){
                continue;
            }
        }

        // 二、处理其他客户端的读写事件
        for(i = 0; i<=maxi; i++){
            // 清空buf
            bzero(buf, MAXLINE);
            // 检查clients 哪个有数据就绪
            if((sockfd = client[i]) < 0)
                continue;

            // 检查sockfd是否在rset集合中
            if(FD_ISSET(sockfd, &rset)){
                // 接收客户端数据,不用阻塞立即读取,因为select已经发生读写事件
                if((n = read(sockfd, buf, MAXLINE)) == 0){
                    close(sockfd); //关闭客户端套接字
                    FD_CLR(sockfd, &allset); 
                    client[i] = -1; 
                }
                else{
                    // 处理客户端数据
                    char code = buf[0];
                    switch (code)
                    {
                    // 注册命令处理
                    case CL_CMD_REG:
                        if(1 != countChar(buf, ',')){
                            puts("invalid protocol");
                            break;
                        }

                        GetName(buf, szName);

                        //判断名字是否重复
                        if(IsExist(szName)){
                            sprintf(repBuf,"r,exist");
                        }
                        else{
                            insert(szName);
                            showTable();
                            sprintf(repBuf, "r,ok");
                            printf("reg ok,%s\n",szName);
                        }

                        write(sockfd, repBuf, strlen(repBuf));
                        break;
                    
                    case CL_CMD_LOG:
                        if(1 != countChar(buf, ',')){
                            puts("invalid protocol");
                            break;
                        }
                        GetName(buf, szName);

                        //判断是否注册过
                        if(IsExist(szName)){
                            sprintf(repBuf,"l,ok");
                            printf("login ok,%s\n",szName);
                        }
                        else sprintf(repBuf,"l,notexist");

                        write(sockfd, repBuf, strlen(repBuf));
                        break;

                    case CL_CMD_CHAT:
                        puts("send all");

                        // 群发
                        for(i=0;i<=maxi;i++)
                            if(client[i]!=-1)
                                write(client[i], buf+2, n); //去掉命令头,写回客户端
                        break;

                    }
                }
                if(--nready == 0)
                    break;
            }
        }
    }

    close(listenfd);
    return 0;
}

九、客户端详细设计

在客户端设计上,考虑到平台的适应性和便捷性,可以使用Visual Studio 或者 Qt开发。综合利弊,本人打算使用Qt来实现一个友好简洁的人机界面。我们整套系统的通信是在Linux和Windows之间进行的,这也是常见的应用场景。在企业一线开发中,客户端几乎没有运行在Linux下的。

聊天客户端主要功能:

(1)提供注册界面,供用户输入注册信息,然后把注册信息以TCP方式发送给服务器进行注册登记(其实在服务器端就是写入数据库)。

(2)注册成功后,提供登录界面,让用户输入登录信息进行登录。登录时主要输入用户名,并以TCP方式发送给服务器端,服务器端检查用户名是否存在后,将反馈结果发送给客户端。

(3)用户登录成功后,就可以发送聊天信息,所有在线的人都可以看到该聊天信息。

以下是详细的代码:

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 网络配置
    qDebug() << "Connecting to server...";
    tcpSocket = new QTcpSocket(this);
    tcpSocket->connectToHost(host, port);
    qDebug() << "1111";
    if (!tcpSocket->waitForConnected(3000)) {
        qDebug() << "Error: " << tcpSocket->errorString();
    }

    // 连接相关信号和槽
    connect(tcpSocket, &QTcpSocket::connected, this, &MainWindow::onConnected);
    connect(tcpSocket, &QTcpSocket::disconnected, this, &MainWindow::onDisconnected);
    connect(tcpSocket, &QTcpSocket::readyRead, this, &MainWindow::onReadyRead);
    connect(&myform, &Form::sendMessage, this, &MainWindow::onRecvMessage, Qt::UniqueConnection);
    connect(this, &MainWindow::send, &myform, &Form::recv);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::sendMessage(const QString &message)
{
    if (tcpSocket->state() == QAbstractSocket::ConnectedState) {
        tcpSocket->write(message.toUtf8());
    } else {
        qDebug() << "Not connected to server!";
    }
}
// 登录按钮点击事件
void MainWindow::on_pushButton_clicked()
{
    name = ui->lineEdit->text();
    //借助网络发送数据
    QString message = QString("l,%1").arg(name);
    sendMessage(message);
}

// 注册按钮点击事件
void MainWindow::on_pushButton_2_clicked()
{

    connect(&myresform, &Dialog::sendText, this, &MainWindow::onReceiveText, Qt::UniqueConnection);

    myresform.exec();
}

void MainWindow::onReceiveText(const QString &text)
{
    resname.clear();
    resname = text;
    // 借助网络发送数据
    QString message = QString("r,%1").arg(resname);
    sendMessage(message);
}

void MainWindow::onReadyRead()
{
    QByteArray data = tcpSocket->readAll();
    qDebug() << "Received from server:" << QString::fromUtf8(data);

    QString message = QString::fromUtf8(data);

    if(message == "r,exist"){
        QMessageBox::warning(this,"提示","用户名已存在");
    }
    else if(message == "r,ok"){
        QMessageBox::warning(this,"提示","用户名创建成功");
    }
    else if(message == "l,notexist"){
        QMessageBox::warning(this,"提示","用户不存在");
    }
    else if(message == "l,ok"){
        this->hide();
        myform.name = name;
        myform.show();
    }
    // 显示消息
    else{
        emit send(message);
    }
}

void MainWindow::onConnected()
{
    qDebug() << "Connected to server!";
}

void MainWindow::onDisconnected()
{
    qDebug() << "Disconnected from server.";
}

void MainWindow::onRecvMessage(const QString &text)
{
    QString sendMes = QString("c,%1:%2").arg(name).arg(text);
    sendMessage(sendMes);
}

十、结果演示

(1)首先运行起服务器

(2)然后运行客户端

(3)注册,输入用户名,点击确定,提醒注册成功

(4)登录,并且开始聊天

(5)重复以上操作,继续开一个客户端

可以发现,两人通过服务器正常聊天了。

十一、留给读者的话

本次项目,从数据库、服务器,再到客户端,基本包含了整个软件的设计流程(不包含后面的维护和迭代更新)。在下从中受益颇多,同时也考虑着在后续中对服务器进行更新。首先打算把线程池引入,进一步提高服务器的并发性;然后考虑将select更新成epoll,epoll的接口和性能远非select可比;再来是客户端之间允许直接使用udp协议通信,减轻服务器的负担;最后,如果可以的话,在下想完善客户端的界面,最后加上一个好友列表,能够查看自己好友的在线状态。这些都可以在后续慢慢完善,当然,如果诸位有更好的idea,欢迎评论区留言,在下不胜感激~

最后,诸位如果想获取完整源码的话,可以三连+私信留下邮箱~

至此,结束~

望诸位不忘三连支持一下~

相关推荐
九河云1 小时前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock1 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
CoderIsArt1 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
幺零九零零2 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
师太,答应老衲吧3 小时前
SQL实战训练之,力扣:2020. 无流量的帐户数(递归)
数据库·sql·leetcode
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法