写点什么

设计一个跨平台的即时通讯系统 (采用华为云 ECS 服务器作为服务端 )【华为云至简致远】

  • 2022 年 8 月 08 日
  • 本文字数:9471 字

    阅读完需:约 31 分钟

【摘要】 这篇文章介绍了 ECS 服务器从购买、配置,部署、设计代码的整个流程。以即时聊天系统的项目案例设计,整体介绍了 ECS 弹性云服务器的一个运用场景。 选择云服务器 ECS 的优势在于:无需自建机房,无需采购以及配置硬件设施,分钟级交付,快速部署,缩短应用上线周期。使用也很简单,一个远程终端连接上去就行。

  1. 前言即时通信软件的出现使人与人之间的交流相处变得更加便捷。好友即使远隔千里,依旧可以互相通信,使得友谊得以长存;亲人即便因工作相隔很远,依然可以多多联系,以便家人安心,自己舒心;正是即时通信系统使得信息走向便捷化的道路,人与人之间的交流沟通更是方便。反过来也是信息交流的不可或缺,使得即时通信系统更加具备研究价值,互联网对其的重视从不减少,反倒是与日俱增,人们对其的功能构想更是丰富多样,更是推动了互联网大环境的发展。


这篇文章就设计一个简单的即时通讯软件,也就是类似于 QQ 这种聊天软件,通过这个软件设计实现过程来了解 TCP 网络编程知识点、客户端设计思路、公网服务器部署方式等知识;本身软件并不复杂,实现的都是一些基本功能,主要是通过这套软件的设计过程来对应介绍相关的知识点。


软件整体包含了一个客户端、一个服务器。客户端采用 QT 设计,支持跨平台运行,服务器采用 Linux 系统运行,为了方便实现公网聊天,不局限于局域网,服务器采用了华为云的 ECS 弹性服务器,系统选择了 ubuntu18.04 64 位 。 服务器端存储数据的数据库采用华为云的 mySQL 云数据,华为云 MySQL 数据的使用方式在上篇文章里讲过了。客户端的数据库采用 SQLITE,数据都是存储在本地的,存储一些聊天记录等信息。


下面是客户端的界面效果:(1)这是客户端登录界面。客户端可以无限运行,一个客户端就登录一个账号。image.png(2)这个截图清晰些 image.png(3)这是登录之后的窗口 image.png(4)这是两个好友登录之后,聊天的效果。登录需要登录服务器。image.pngimage.png


接下来就从服务器的购买、部署、开始介绍整体软件的设计。


  1. 部署 ECS 云服务器华为云的 ECS 云服务器现在购买也挺便宜的,刚好赶上 618 活动。官网地址: https://www.huaweicloud.com/


鼠标光标放在左上角的产品字样上,会弹出一个列表,介绍可以使用购买的产品。


可以看到,ECS 服务器挺火的,HOT 字样都打上了。image.png


云服务器本身就类似一台远程电脑,配上公网 IP,可以随时远程上去操作。有了这个服务器就很方便了,可以 24 小时不关机,也不用关心耗电问题,不用关心维护问题,只要自己代码没问题,就能一直稳定运行的跑。


如果初次使用服务器,也可以去免费领取一个的使用权,这个还是很方便的。


领取地址: https://www.huaweicloud.com/就在华为云的首页,可以看到免费试用产品。image.png


现在有点晚了,服务器已经被领取完了,每天早上 9 点半会发放。image.png


我之前已经领取过了,所以这里就不管了。


领取也很简单,下面贴一下当初领取时的过程记录。(1)首先点击你想要的配置和类型领取 image.png(2)选择配置。如果有选择,当然选择最高配置。image.png(3)选择操作系统,这里截图上选择的是 ubuntu20.04 64 位,也有 ubuntu18.04 可以选择,还有其他系统,具体看自己的需求。image.png(4)这里设置服务器的 root 登录密码,最好在这里就一次设置好,后面也不用再去修改。image.png(5)接下来就是按照流程点击下一步即可。image.png(6)在订单详情页面会看到资源正在创建,需要等待一段时间就会成功。image.png


服务器买了之后,可以点击右上角的控制台按钮,去查看自己的服务器了。


在控制台页面,点击资源管理,可以看到自己有哪些实例可以使用,分别在什么区域。image.png


点击进去自己的服务器详情页面。image.png


服务器创建之后已经绑定了公网 IP 地址,这些就不用自己再次设置了。image.png


接下来还需要设置一下安全组规则,开放一下外网访问的端口,如果不设置,不开放端口,外网无法访问服务器里的服务,这也是防火墙安全的控制手段。


image.png


点击添加规则按钮,设置自己需要开放的 IP 地址网段,端口范围就行了。image.png


接下来点击页面上的远程登录按钮,登录一下服务器。image.png


登录方式有多种,先选择一个默认方式,先体验一下,看看自己的服务器。image.png 输入密登录即可。image.png 登录之后进入命令行终端,熟悉的命令行出现了。image.png


到此,一台全新的服务器已经搞定了,接下来就可以编写代码运行了。


  1. 使用 SSH 协议登录服务器在网页上直接登录,操作起来始终不方便,我在 windows 下一般使用 SecureCRT 登录服务器。服务器支持 SSH 协议登录,登录是很方便。只需要知道服务器的公网 IP 地址、系统的用户名、系统密码即可。公网地址就在服务器详情页面可以看到。用户名就是 root,密码就是自己创建服务器时设置的密码。


下面就看一下登录效果:image.png 点击接受并保存。image.png


输入用户名、密码即可登录上去。image.pngimage.png


到此,采用 SSH 协议、利用终端软件成功登录了服务器,接下来就可以正常的开发了。


  1. 即时系统整体设计思路(1) 服务器设计:


登录认证: 服务器从客户端接收账户和密码,创建线程比对数据库中该用户账号、密码,比对成功,则返回用户信息给客户端,确认可进行登录。


客户端连接信息保存: 用户在客户端成功登录后,其所属账号、IP 以及端口号被记录在数据库的连接信息表中以便管理。此时用户状态被置为 1。反之,用户在客户端退出时,其账号、IP 被连接信息表清除,此时用户状态被置为 0.


用户注册: 当新用户使用时,用户需要填写用户名和密码。此时用户注册信息将存入数据库中,以备下次登录所需[10]。


增加好友: 服务触发增加好友的事件请求后,会即时将其账号信息存录数据库中进行管理。


删除好友: 服务器触发删除好友的事件请求后,会即时清除数据库中该用户的账户信息。


查找好友: 服务器一旦触发查找好友的事件请求时,会即时比对数据库中信息表的好友信息,该账号信息存在就可成功找到。


修改好友: 服务器触发好友信息修改的事件请求后,会即时更新该用户的账户信息。


好友通信: 服务器触发触发通信的事件请求时,会即时查询数据库中好友连接信息,再由此信息返回好友的 ip 地址及端口号给用户。


(2) 客户端设计:


用户注册: 用户在通信系统的客户端登录界面上输入账号、密码。接着,用户可单击按钮进行注册操作。此时,注册信息会即时发送给服务器,录入数据库进行管理。注册完,关闭客户端。下次登录时,用户通过客户端与服务器连接,查找账户信息,服务器判定账户信息正确,即表明注册成功。


用户登录: 用户点击“登录”按钮,根据客户端输入的账号信息判定其是否为空。若为空,程序运行不执行下述操作:向服务器发送登录请求。若不为空,程序运行即刻执行下述操作:向服务器发送登录请求。


好友在线状况: 客户端收到来自服务器的好友在线信息,好友联系人列表会发现此用户信息,表明该用户处于在线状态。用户可双击任一在线好友,双方即可进行交流。


一对一聊天: 用户双击联系人列表中任一在线友人。这时,客户端会给服务器发请求。服务器收到请求后,即时给客户端发送有关好友 ip 地址和端口号的连接信息。最终,双方实现信息交互。双方在聊天框中交流,客户端还含有好友聊天记录功能即消息管理。


群组聊天: 用户点击“群组”图标,客户端就会发送事件请求给服务器。服务器触发事件请求后,即时给客户端发送群聊的连接信息,此连接信息与群聊 ip 地址和端口号有关。最终,多人通信的用户通过客户端聊天界面实现信息交互。


  1. 服务器代码为了方便拷贝代码到服务器上去,我这里采用 NFS 服务器的方式拷贝,在服务器上搭建 NFS 服务器,共享一个目录出来,本地 linux 系统挂载,将代码这些文件拷贝过去。


(1)服务器端需要先安装服务器。root@ecs-348470:~# sudo apt-get install nfs-kernel-server(2)创建一个 work 目录方便当做共享目录使用 root@ecs-348470:~# mkdir work(3)编写 NFS 配置文件再编写 NFS 服务的配置文件/etc/exports, 填入配置信息。/home/work *(rw,no_root_squash,sync,no_subtree_check,insecure)


(4)然后启动服务器即可/etc/init.d/nfs-kernel-server start #启动 NFS 服务本地我使用的是 ubuntu18.04 系统,挂载服务器的路径,将群聊代码拷贝上去。


wbyq@wbyq:~ cd mnt/wbyq@wbyq:~/mnt sudo cp /mnt/hgfs/linux-share-dir/socket_app/* ./wbyq@wbyq:~/mnt这是服务器的第一版群聊代码,采用多线程方式处理客户端的连接请求。群聊模式下,服务器上主要是连接客户端之后,将消息转发给其余的客户端。


**代码已经拷贝上去: **image.png


群聊服务器版本代码实现如下:


#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <sys/types.h> /* See NOTES /#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdio.h>#include <poll.h>#include <stdlib.h>#include <signal.h>#include <pthread.h>/创建服务器:


  1. 创建 socket 套接字

  2. 绑定端口号(给进程固定端口号和 IP 地址)--65535 之内。

  3. 设置监听的队列

  4. 等待客户端连接./app 8080*/int server_fd;


/*发出的消息结构*/struct send_pack{char name[50];char data[100];char type; //0x1 表示上线,0x2 表示下线 0x0 表示正常聊天数据};


/*保存连接上服务器的客户端信息*/struct client_info{int client_fd;struct client_info *next;};


struct client_info *LIST_HEAD=NULL;pthread_rwlock_t rwlock;


struct client_info *List_CreateHead(struct client_info *head);void List_del(struct client_info *head,int client_fd);void List_add(struct client_info *head,int client_fd);


/信号处理函数/void sighandler_func(int sig){/4. 完毕套接字/close(server_fd);printf("进程正常退出....\n");exit(0);}


/*函数功能:线程处理子函数*/void func_start(void arg){int client_fd=(int)arg;free(arg);//写加锁 pthread_rwlock_wrlock(&rwlock);//添加节点 List_add(LIST_HEAD,client_fd);//解锁 pthread_rwlock_unlock(&rwlock);


int cnt;struct send_pack data_pack;struct client_info *tmp_head;int poll_stat;struct pollfd poll_fd;poll_fd.fd=client_fd;poll_fd.events=POLLIN;while(1){  poll_stat=poll(&poll_fd,1,-1);  if(poll_stat>0)  {    cnt=read(client_fd,&data_pack,sizeof(struct send_pack));    if(cnt==0)    {      //写加锁      pthread_rwlock_wrlock(&rwlock);      //删除节点      List_del(LIST_HEAD,client_fd);      //解锁      pthread_rwlock_unlock(&rwlock);            data_pack.type=2;//下线提醒            pthread_rwlock_rdlock(&rwlock);      tmp_head=LIST_HEAD;      while(tmp_head->next)      {        tmp_head=tmp_head->next;        if(tmp_head->client_fd!=client_fd)        {          write(tmp_head->client_fd,&data_pack,sizeof(struct send_pack));        }      }      //解锁      pthread_rwlock_unlock(&rwlock);      break;    }    //加锁    pthread_rwlock_rdlock(&rwlock);    tmp_head=LIST_HEAD;    while(tmp_head->next)    {      tmp_head=tmp_head->next;      if(tmp_head->client_fd!=client_fd)      {        write(tmp_head->client_fd,&data_pack,sizeof(struct send_pack));      }    }    //解锁    pthread_rwlock_unlock(&rwlock);  }  else if(poll_stat<0)  {    perror("poll函数监听失败.\n");    break;  }}
/*6. 关闭连接*/close(client_fd);pthread_exit(NULL);
复制代码


}


int main(int argc,char **argv){if(argc!=2){printf("./app <创建绑定的服务器端口号>\n");return 0;}/初始化读写锁/pthread_rwlock_init(&rwlock,NULL);


/*绑定需要捕获的信号*/signal(SIGINT,sighandler_func);/*创建链表头*/LIST_HEAD=List_CreateHead(LIST_HEAD);
/*1. 创建socket套接字*/server_fd=socket(AF_INET,SOCK_STREAM,0);if(server_fd<0){ printf("server:创建socket套接字失败.\n"); return 0;}/*2. 绑定端口号*/struct sockaddr_in server_addr;server_addr.sin_family=AF_INET;server_addr.sin_port=htons(atoi(argv[1]));//把 16 位值从主机字节序转换成网络字节序server_addr.sin_addr.s_addr=INADDR_ANY;//表示本地所有IP地址if(bind(server_fd,(const struct sockaddr *)&server_addr,sizeof(struct sockaddr_in))){ printf("server:绑定端口号失败.\n"); return 0;}/*3. 设置监听队列*/listen(server_fd,100);
/*4. 等待客户端连接*/struct sockaddr_in client_addr;socklen_t addrlen;pthread_t thread_id;int *client_fd;while(1){ addrlen=sizeof(struct sockaddr_in); client_fd=malloc(sizeof(int)); *client_fd=accept(server_fd,(struct sockaddr *)&client_addr,&addrlen); if(*client_fd<0) { printf("server:处理客户连接失败.\n"); return 0; } printf("客户端的IP地址:%s\n",inet_ntoa(client_addr.sin_addr)); printf("客户端的端口号:%d\n",ntohs(client_addr.sin_port)); /*创建新的线程*/ if(pthread_create(&thread_id,NULL,func_start,client_fd)) { printf("线程创建失败.\n"); break; } /*设置线程的分离属性*/ pthread_detach(thread_id);}close(server_fd);return 0;
复制代码


}


/*创建链表头*/struct client_info *List_CreateHead(struct client_info head){if(head==NULL){head=malloc(sizeof(struct client_info));}return head;}/添加节点*/void List_add(struct client_info *head,int client_fd){struct client_info *p=head;struct client_info new_node;while(p->next){p=p->next;}new_node=malloc(sizeof(struct client_info));if(new_node==NULL)printf("内存空间申请失败.\n");new_node->client_fd=client_fd;new_node->next=NULL;p->next=new_node;}/删除链表节点*/void List_del(struct client_info *head,int client_fd){struct client_info *p=head;struct client_info *old;while(p->next){old=p;p=p->next;if(p->client_fd==client_fd){old->next=p->next;free(p);break;}}}在服务器上编译运行代码:


root@ecs-348470:~/work# gcc server_app.c -o tcp_server -lpthreadroot@ecs-348470:~/work# lsserver_app.c tcp_serverroot@ecs-348470:~/work# ./tcp_server 8899image.png


运行服务器群聊代码,在本地 ubuntu 系统再运行客户端代码,测试通信效果:image.png


  1. 客户端代码设计在设计过程中,为了测试,代码写了两份,一份命令的行的聊天客户端,一份 QT 设计带界面的完整版本。


6.1 Linux 命令行客户端**下面这是命令行版本的群聊客户端代码: **


#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <netinet/in.h>#include <arpa/inet.h>#include <sys/types.h> /* See NOTES /#include <sys/socket.h>#include <stdio.h>#include <poll.h>#include <signal.h>#include <stdlib.h>#include <string.h>#include <pthread.h>/客户端:


  1. 创建 socket 套接字

  2. 连接指定的服务器*/int client_fd;


/信号处理函数/void sighandler_func(int sig){/4. 完毕套接字/close(client_fd);printf("进程正常退出....\n");exit(0);}


/*发出的消息结构*/struct send_pack{char name[50];char data[100];char type; //0x1 表示上线,0x2 表示下线 0x0 表示正常聊天数据};


void func_start(void arg){/1. 通信/int client_fd=(int)arg;struct send_pack data;int cnt;int poll_stat;struct pollfd poll_fd;poll_fd.fd=client_fd;poll_fd.events=POLLIN; while(1){poll_stat=poll(&poll_fd,1,-1);if(poll_stat>0){cnt=read(client_fd,&data,sizeof(struct send_pack));if(cnt==sizeof(struct send_pack)){if(data.type==0){printf("%s:%s\n",data.name,data.data);}else if(data.type==1){printf("%s 用户上线.\n",data.name);}else if(data.type==2){printf("%s 用户下线.\n",data.name);}}else if(cnt==0){printf("服务器断开连接.\n");break;}}else if(poll_stat<0){perror("poll 函数执行错误.\n");break;}}pthread_exit(NULL);}


int main(int argc,char **argv){if(argc!=4){printf("./app <服务器 IP 地址> <服务器端口号> <用户名>\n");return 0;}


/*绑定需要捕获的信号*/signal(SIGINT,sighandler_func);
/*1. 创建socket套接字*/client_fd=socket(AF_INET,SOCK_STREAM,0);if(client_fd<0){ printf("client:创建socket套接字失败.\n"); return 0;}/*2. 连接服务器*/struct sockaddr_in server_addr;server_addr.sin_family=AF_INET;server_addr.sin_port=htons(atoi(argv[2]));//把 16 位值从主机字节序转换成网络字节序server_addr.sin_addr.s_addr=inet_addr(argv[1]);//服务器IP地址if(connect(client_fd,(const struct sockaddr *)&server_addr,sizeof(struct sockaddr_in))){ printf("client: 连接服务器失败.\n"); return 0;}
/*3. 创建子线程*/pthread_t thread_id;if(pthread_create(&thread_id,NULL,func_start,&client_fd)){ printf("线程创建失败.\n"); return 0;}/*4. 设置线程的分离属性*/pthread_detach(thread_id);
struct send_pack data_pack;strcpy(data_pack.name,argv[3]); //赋值用户名data_pack.type=1;//上线提示/*上线提示*/write(client_fd,&data_pack,sizeof(struct send_pack));
/*进行正常聊天*/data_pack.type=0;//正常聊天while(1){ gets(data_pack.data); write(client_fd,&data_pack,sizeof(struct send_pack));}
/*5. 完毕套接字*/close(client_fd);return 0;
复制代码


}这是聊天过程运行效果:(1) 左边是华为云 ECS 服务器,右边是本地 ubuntu 系统运行客户端 image.png(2)群聊天过程演示 image.pngimage.png


6.2 QT 完整版本客户端这是 QT 设计的带私聊、群聊的客户端版本。界面子文件、资源文件、代码文件都较多,就不全部贴出了。和上面的区别就是加了界面,加了一些功能,核心实现思路差不多。界面效果在文章首页已经截图了。


#include "qconnectobject.h"#include "mainwidget.h"QConnectObject::QConnectObject(QObject *parent) : QObject(parent){


tcpClient = new QTcpSocket(this);connect(tcpClient,SIGNAL(readyRead()),this,SLOT(tcpreadData()));connect(tcpClient,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(ReadError(QAbstractSocket::SocketError)));connectstate = false;
复制代码


}


void QConnectObject::acceptConfig(QString stra, QString strb, QString strc){m_ip = stra;mc_port = strb;


if(connectstate == false){QString ipAdd(stra), portd(strb);if (ipAdd.isEmpty() || portd.isEmpty()){return;}tcpClient->connectToHost(ipAdd,portd.toInt());if (tcpClient->waitForConnected(1000)){qDebug()<<"connectstate";connectstate = true;}}mtimer = new QTimer();connect(mtimer,SIGNAL(timeout()),this,SLOT(mtimeout()));mtimer->start(1000);}


void QConnectObject::acceptLogin(QString stra, QString strb){tcpClient->write(QString("LOGIN:%1:%2").arg(stra).arg(strb).toLocal8Bit());}


void QConnectObject::acceptResiger(QString stra, QString strb){tcpClient->write(QString("RESIGER:%1:%2").arg(stra).arg(strb).toLocal8Bit());}


void QConnectObject::acceptMsg(QString str){tcpClient->write(str.toLocal8Bit());}


void QConnectObject::ReadError(QAbstractSocket::SocketError){qDebug()<<"ReadError";tcpClient->disconnectFromHost();connectstate = false;}void QConnectObject::tcpreadData(){QByteArray datagram = tcpClient->readAll();QString accbuf=QString::fromLocal8Bit(datagram);qDebug()<<accbuf;QStringList strlistA = accbuf.split(":");if(strlistA.size() >= 1){if(strlistA.at(0) == "LOGIN"){//登录 if(strlistA.size()>=2){if(strlistA.at(1) == "true"){emit sendLogin(true);MainWidget::myport = strlistA.at(2);}else if(strlistA.at(1) == "false"){emit sendLogin(false);}}}if(strlistA.at(0) == "RESIGER"){//登录 if(strlistA.size()>=2){if(strlistA.at(1) == "true"){emit sendResiger(true);}else if(strlistA.at(1) == "false"){emit sendResiger(false);}}}if(strlistA.at(0) == "CONNECT_CLIENT"){//连接用户 emit sendClientInfo(accbuf);}if(strlistA.at(0) == "MSG"){//消息 emit sendMSG(accbuf);} if(strlistA.at(0) == "GROUP"){//群 emit sendMSG(accbuf);}}}


void QConnectObject::acceptProgress(double){


}


void QConnectObject::mtimeout(){if(connectstate == false){QString ipAdd(m_ip), portd(mc_port);if (ipAdd.isEmpty() || portd.isEmpty()){qDebug()<<"return"<<connectstate<<m_ip;return;}tcpClient->connectToHost(ipAdd,portd.toInt());if (tcpClient->waitForConnected(1000)){connectstate = true;qDebug()<<"connectstate";}}else{


}
复制代码


}


QString getHostIpAddress(){QString strIpAddress;QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();// 获取第一个本主机的 IPv4 地址 int nListSize = ipAddressesList.size();for (int i = 0; i < nListSize; ++i){if (ipAddressesList.at(i) != QHostAddress::LocalHost &&ipAddressesList.at(i).toIPv4Address()) {strIpAddress = ipAddressesList.at(i).toString();break;}}// 如果没有找到,则以本地 IP 地址为 IPif (strIpAddress.isEmpty())strIpAddress = QHostAddress(QHostAddress::LocalHost).toString();return strIpAddress;}void QConnectObject::sendFileClicked(){fileName = QFileDialog::getOpenFileName();qDebug()<<fileName;m_port = qrand()%30 + 300 + qrand()%50 + qrand()%10;QString filstr = "FileTran:" + getHostIpAddress() + ":" + QString::number(m_port);tcpClient->write(filstr.toLocal8Bit());}


image.png


  1. 总结这篇文章介绍了 ECS 服务器从购买、配置,部署、设计代码的整个流程。以即时聊天系统的项目案例设计,整体介绍了 ECS 弹性云服务器的一个运用场景。


最后的总结说明:选择云服务器 ECS 的一些优势,为什么要选择 ECS 云服务器?【1】无需自建机房,无需采购以及配置硬件设施。【2】分钟级交付,快速部署,缩短应用上线周期。【3】成本透明,按需使用,支持根据业务波动随时扩展和释放资源。如果中小型公司自己搭建服务器,成本相对购买现成的云服务器而言,会高很多很多,而且稳定性都是问题。


【华为云至简致远】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/352809


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

想了解更多的华为云产品相关信息,请联系我们:电话:950808 按 0 转 1


用户头像

还未添加个人签名 2022.07.30 加入

还未添加个人简介

评论

发布
暂无评论
设计一个跨平台的即时通讯系统(采用华为云ECS服务器作为服务端 )【华为云至简致远】_云服务器ECS_IT资讯搬运工_InfoQ写作社区