返回介绍

第 1 节 C 语言利用 epoll 实现高并发聊天室

发布于 2025-03-07 00:37:22 字数 20344 浏览 0 评论 0 收藏 0

一、实验介绍

1. 环境登录

无需密码自动登录,系统用户名 shiyanlou

2. 环境介绍

本实验环境采用带桌面的 Ubuntu Linux 环境,实验中会用到桌面上的程序:

  1. LX 终端(LXTerminal): Linux 命令行终端,打开后会进入 Bash 环境,可以使用 Linux 命令
  2. GVim:非常好用的编辑器,最简单的用法可以参考课程 Vim 编辑器

3. 环境使用

使用 GVim 编辑器输入实验所需的代码及文件,使用 LX 终端(LXTerminal)运行所需命令进行操作。

实验报告可以在个人主页中查看,其中含有每次实验的截图及笔记,以及每次实验的有效学习时间(指的是在实验桌面内操作的时间,如果没有操作,系统会记录为发呆时间)。这些都是您学习的真实性证明。

二、项目介绍

本项目是实现一个简单的聊天室,聊天室分为服务端和客户端。本项目将很多复杂的功能都去掉了,线程池、多线程编程、超时重传、确认收包等等都不会涉及。总共 300 多行代码,让大家真正了解 C/S 模型,以及 epoll 的使用。为了方便查看,代码已经改的很小白,绝对比 nginx 源码好理解(当然大家有兴趣的话,还是要拜读下 nginx 源码,绝对大有收获)。希望本项目能为大家以后的工作或者学习提供一点帮助! 介绍如下:

1. 服务端

a. 支持多个用户接入,实现聊天室的基本功能
b. 使用 epoll 机制实现并发,增加效率

2. 客户端

a. 支持用户输入聊天消息
b. 显示其他用户输入的信息
c. 使用 fork 创建两个进程
子进程有两个功能:
  1. 等待用户输入聊天信息
  2. 将聊天信息写到管道(pipe),并发送给父进程
父进程有两个功能
  1. 使用 epoll 机制接受服务端发来的信息,并显示给用户,使用户看到其他用户的聊天信息
  2. 将子进程发给的聊天信息从管道(pipe)中读取,并发送给服务端

3. 代码说明

一共有 3 个文件,即: server.cpp, client.cpp, utility.h

a. server.cpp 是服务端程序
b. client.cpp 是客户端程序
c. utility.h 是一个头文件,包含服务端程序和客户端程序都会用到的一些头文件、变量声明、函数、宏等。

最终效果如下: 图片描述信息 如图所示,有两个用户在聊天,限于屏幕大小,只开启了两个客户端,大家可以开启更多客户端。 大家看了截图,是不是马上就想动手完成它,下面我们就来开始。

4. 其他

在本项目中,当介绍模型或者一些技术的时候,会解释源码的有关部分,从而来方便大家按照模块学习。完整源码会在项目实战中贴出。

三、项目实战

1. C/S 模型

首先介绍下模型。服务端和客户端采用经典的 C/S 模型,并且使用 TCP 连接,模型如下: 图片描述信息 解释如下: 图片描述信息

####1.1 TCP 服务端通信的常规步骤

(1) 使用 socket() 创建 TCP 套接字(socket)
(2) 将创建的套接字绑定到一个本地地址和端口上(Bind)
(3) 将套接字设为监听模式,准备接收客户端请求(listen)
(4) 等待客户请求到来: 当请求到来后,接受连接请求,返回一个对应于此次连接的新的套接字(accept)
(5) 用 accept 返回的套接字和客户端进行通信(使用 write()/send() 或 send()/recv() )
(6) 返回,等待另一个客户请求
(7) 关闭套接字
//server.cpp 代码(通信模块):
  //服务端地址 ip 地址 + 端口号
  struct sockaddr_in serverAddr;
  serverAddr.sin_family = PF_INET;
  serverAddr.sin_port = htons(SERVER_PORT);
  serverAddr.sin_addr.s_addr = inet_addr(SERVER_HOST);

  //服务端创建监听 socket
  int listener = socket(PF_INET, SOCK_STREAM, 0);
  if(listener < 0) { perror("listener"); exit(-1);}
  printf("listen socket created \n");

  //将服务端地址与监听 socket 绑定
  if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
    perror("bind error");
    exit(-1);
  }
  //开始监听
  int ret = listen(listener, 5);
  if(ret < 0) { perror("listen error"); exit(-1);}
  printf("Start to listen: %s\n", SERVER_HOST);

后续的 accept 连接以及具体通信,在解释了 epoll 之后才会介绍。

1.2 TCP 客户端通信的常规步骤

(1) 创建套接字(socket)
(2) 使用 connect() 建立到达服务器的连接(connect)
(3) 客户端进行通信(使用 write()/send() 或 send()/recv())
(4) 使用 close() 关闭客户连接
//client.cpp 代码(通信模块):
  //客户要连接的服务端地址( ip 地址 + 端口号)
  struct sockaddr_in serverAddr;
  serverAddr.sin_family = PF_INET;
  serverAddr.sin_port = htons(SERVER_PORT);
  serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);

  // 创建套接字(socket)
  int sock = socket(PF_INET, SOCK_STREAM, 0);
  if(sock < 0) { perror("sock error"); exit(-1); }
  //向服务器发出连接请求(connect)
  if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
    perror("connect error");
    exit(-1);
  }

客户端如何实现管道之间的通信,以及与服务端之间的通信,在后面会详细介绍。 完成这步后,我们需要学习下几个比较重要的概念。

2. 基本技术介绍

2.1 阻塞与非阻塞 socket

通常的,对一个文件描述符指定的文件或设备,有两种工作方式: 阻塞与非阻塞方式。

(1). 阻塞方式是指: 当试图对该文件描述符进行读写时,如果当时没有数据可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。

(2). 非阻塞方式是指: 如果没有数据可读,或者不可写,读写函数马上返回,而不会等待。

(3). 举个例子来说,比如说小明去找一个女神聊天,女神却不在。 如果小明舍不得走,只能在女神大门口死等着,当然小明可以休息。当女 神来了,她会把你唤醒(囧,因为挡着她门了),这就是阻塞方式。如果小明发现女神不在,立即离开,以后每隔十分钟回来看一下(采用轮询方式),不在的话仍然立即离开,这就是非阻塞方式。

(4). 阻塞方式和非阻塞方式唯一的区别: 是否立即返回。本项目采用更高效的做法,所以应该将 socket 设置为非阻塞方式。这样能充分利用服务器资源,效率得到了很大提高。

//utility.h 代码(设置非阻塞函数模块):
//将文件描述符设置为非阻塞方式(利用 fcntl 函数)
int setnonblocking(int sockfd)
{
  fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
  return 0;
}

2.2 epoll

前面介绍了阻塞和非阻塞方式,现在该介绍下 epoll 机制了。epoll 真的是一个特别重要的概念,实验的师兄们去 bat 任何一家面试后台开发,或者系统开发等相关职位都会问 epoll 机制。当服务端的在线人数越来越多,会导致系统资源吃紧,I/O 效率越来越慢,这时候就应该考虑 epoll 了。epoll 是 Linux 内核为处理大批句柄而作改进的 poll,是 Linux 特有的 I/O 函数。其特点如下:

a.
epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本。其实现和使用方式与 select/poll 有很多不同,epoll 通过一组函数来完成有关任务,而不是一个函数。
b.
epoll 之所以高效,是因为 epoll 将用户关心的文件描述符放到内核里的一个事件表中,而不是像 select/poll 每次调用都需要重复传入文件描述符集或事件集。比如当一个事件发生(比如说读事件),epoll 无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入就绪队列的描述符集合就行了。
c.
epoll 有两种工作方式,LT(level triggered):水平触发和 ET(edge-triggered):边沿触发。LT 是 select/poll 使用的触发方式,比较低效;而 ET 是 epoll 的高速工作方式(本项目使用 epoll 的 ET 方式)。
d.
通俗理解就是,比如说有一堆女孩,有的很漂亮,有的很凤姐。现在你想找漂亮的女孩聊天,LT 就是你需要把这一堆女孩全都看一遍,才可以找到其中的漂亮的(就绪事件);而 ET 是你的小弟(内核)将 N 个漂亮的女孩编号告诉你,你直接去看就好,所以 epoll 很高效。另外,还记得小明找女神聊天的例子吗?采用非阻塞方式,小明还需要每隔十分钟回来看一下(select);如果小明有小弟(内核)帮他守在大门口,女神回来了,小弟会主动打电话,告诉小明女神回来了,快来处理吧!这就是 epoll。

epoll 共 3 个函数,如下:

1、int epoll_create(int size)
   创建一个 epoll 句柄,参数 size 用来告诉内核监听的数目,size 为 epoll 所支持的最大句柄数
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
   函数功能: epoll 事件注册函数
  参数 epfd 为 epoll 的句柄,即 epoll_create 返回值
  参数 op 表示动作,用 3 个宏来表示:  
    EPOLL_CTL_ADD(注册新的 fd 到 epfd), 
   EPOLL_CTL_MOD(修改已经注册的 fd 的监听事件),
    EPOLL_CTL_DEL(从 epfd 删除一个 fd);
    其中参数 fd 为需要监听的标示符;
  参数 event 告诉内核需要监听的事件,event 的结构如下:
  struct epoll_event {
    __uint32_t events; //Epoll events
    epoll_data_t data; //User data variable
  };
  其中介绍 events 是宏的集合,本项目主要使用 EPOLLIN(表示对应的文件描述符可以读,即读事件发生),其他宏类型,可以 google 之!
3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,函数返回需要处理的事件数目(该数目是就绪事件的数目,就是前面所说漂亮女孩的个数 N)

因此服务端使用 epoll 的时候,步骤如下:

  1. 调用 epoll_create 函数在 Linux 内核中创建一个事件表;
  2. 然后将文件描述符(监听套接字 listener)添加到所创建的事件表中;
  3. 在主循环中,调用 epoll_wait 等待返回就绪的文件描述符集合;
  4. 分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件(epoll 还有很多其他事件,本项目为简洁明了,不介绍)。

    下面介绍下如何将一个 socket 添加到内核事件表中,如下:

//utility.h(添加 socket 模块):
//将文件描述符 fd 添加到 epollfd 标示的内核事件表中, 并注册 EPOLLIN 和 EPOOLET 事件,EPOLLIN 是数据可读事件;EPOOLET 表明是 ET 工作方式。最后将文件描述符设置非阻塞方式
/**
  * @param epollfd: epoll 句柄
  * @param fd: 文件描述符
  * @param enable_et : enable_et = true, 
   采用 epoll 的 ET 工 作方式;否则采用 LT 工作方式
**/
void addfd( int epollfd, int fd, bool enable_et )
{
  struct epoll_event ev;
  ev.data.fd = fd;
  ev.events = EPOLLIN;
  if( enable_et )
    ev.events = EPOLLIN | EPOLLET;
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
  setnonblocking(fd);
  printf("fd added to epoll!\n\n");
}

服务端详细的事件处理过程,下面会讲。

3. 服务端实现

上面我们介绍了基本的模型和技术,现在该去实现服务端了。首先介绍下 utility.h 中一些变量和函数。

3.1 utility.h

/* 限于篇幅,这里先介绍下 utility.h 的主要构成。其中的头文件和一些函数实现没有显示,完整源码位于 3.2 节 */
  //服务端存储所有在线用户 socket, 便于广播信息
  list<int> clients_list;
  // 服务器 ip 地址,为测试使用本地机地址,可以更改为其他服务端地址
  #define SERVER_IP "127.0.0.1"
  // 服务器端口号
  #define SERVER_PORT 8888
  //int epoll_create(int size) 中的 size,为 epoll 支持的最大句柄数
  #define EPOLL_SIZE 5000

  // 缓冲区大小 65535
  #define BUF_SIZE 0xFFFF
  //一些宏
  #define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"
  #define SERVER_MESSAGE "ClientID %d say >> %s"
  #define EXIT "EXIT"
  #define CAUTION "There is only one int the char room!"
  /* 一些函数 */
  //设置非阻塞
  int setnonblocking(int sockfd);
  //将文件描述符 fd 添加到 epollfd 标示的内核事件表
  void addfd( int epollfd, int fd, bool enable_et );
  //服务端发送广播信息,使所有用户都能收到消息
  int sendBroadcastmessage(int clientfd);

3.1 utility.h 完整源码

#ifndef UTILITY_H_INCLUDED
#define UTILITY_H_INCLUDED

#include <iostream>
#include <list>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

using namespace std;

// clients_list save all the clients's socket
list<int> clients_list;

/**********************   macro defintion **************************/
// server ip
#define SERVER_IP "127.0.0.1"

// server port
#define SERVER_PORT 8888

//epoll size
#define EPOLL_SIZE 5000

//message buffer size
#define BUF_SIZE 0xFFFF

#define SERVER_WELCOME "Welcome you join  to the chat room! Your chat ID is: Client #%d"

#define SERVER_MESSAGE "ClientID %d say >> %s"

// exit
#define EXIT "EXIT"

#define CAUTION "There is only one int the char room!"

/**********************   some function **************************/
/**
  * @param sockfd: socket descriptor
  * @return 0
**/
int setnonblocking(int sockfd)
{
  fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
  return 0;
}

/**
  * @param epollfd: epoll handle
  * @param fd: socket descriptor
  * @param enable_et : enable_et = true, epoll use ET; otherwise LT
**/
void addfd( int epollfd, int fd, bool enable_et )
{
  struct epoll_event ev;
  ev.data.fd = fd;
  ev.events = EPOLLIN;
  if( enable_et )
    ev.events = EPOLLIN | EPOLLET;
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
  setnonblocking(fd);
  printf("fd added to epoll!\n\n");
}

/**
  * @param clientfd: socket descriptor
  * @return : len
**/
int sendBroadcastmessage(int clientfd)
{
  // buf[BUF_SIZE] receive new chat message
  // message[BUF_SIZE] save format message
  char buf[BUF_SIZE], message[BUF_SIZE];
  bzero(buf, BUF_SIZE);
  bzero(message, BUF_SIZE);

  // receive message
  printf("read from client(clientID = %d)\n", clientfd);
  int len = recv(clientfd, buf, BUF_SIZE, 0);

  if(len == 0)  // len = 0 means the client closed connection
  {
    close(clientfd);
    clients_list.remove(clientfd); //server remove the client
    printf("ClientID = %d closed.\n now there are %d client in the char room\n", clientfd, (int)clients_list.size());

  }
  else  //broadcast message 
  {
    if(clients_list.size() == 1) { // this means There is only one int the char room
      send(clientfd, CAUTION, strlen(CAUTION), 0);
      return len;
    }
    // format message to broadcast
    sprintf(message, SERVER_MESSAGE, clientfd, buf);

    list<int>::iterator it;
    for(it = clients_list.begin(); it != clients_list.end(); ++it) {
       if(*it != clientfd){
        if( send(*it, message, BUF_SIZE, 0) < 0 ) { perror("error"); exit(-1);}
       }
    }
  }
  return len;
}
#endif // UTILITY_H_INCLUDED

3.3 服务端完整源码

在上面的基础上。服务端的代码就很容易写出了

#include "utility.h"

int main(int argc, char *argv[])
{
  //服务器 IP + port
  struct sockaddr_in serverAddr;
  serverAddr.sin_family = PF_INET;
  serverAddr.sin_port = htons(SERVER_PORT);
  serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
  //创建监听 socket
  int listener = socket(PF_INET, SOCK_STREAM, 0);
  if(listener < 0) { perror("listener"); exit(-1);}
  printf("listen socket created \n");
  //绑定地址
  if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
    perror("bind error");
    exit(-1);
  }
  //监听
  int ret = listen(listener, 5);
  if(ret < 0) { perror("listen error"); exit(-1);}
  printf("Start to listen: %s\n", SERVER_IP);
  //在内核中创建事件表
  int epfd = epoll_create(EPOLL_SIZE);
  if(epfd < 0) { perror("epfd error"); exit(-1);}
  printf("epoll created, epollfd = %d\n", epfd);
  static struct epoll_event events[EPOLL_SIZE];
  //往内核事件表里添加事件
  addfd(epfd, listener, true);
  //主循环
  while(1)
  {
    //epoll_events_count 表示就绪事件的数目
    int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
    if(epoll_events_count < 0) {
      perror("epoll failure");
      break;
    }

    printf("epoll_events_count = %d\n", epoll_events_count);
    //处理这 epoll_events_count 个就绪事件
    for(int i = 0; i < epoll_events_count; ++i)
    {
      int sockfd = events[i].data.fd;
      //新用户连接
      if(sockfd == listener)
      {
        struct sockaddr_in client_address;
        socklen_t client_addrLength = sizeof(struct sockaddr_in);
        int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );

        printf("client connection from: %s : % d(IP : port), clientfd = %d \n",
        inet_ntoa(client_address.sin_addr),
        ntohs(client_address.sin_port),
        clientfd);

        addfd(epfd, clientfd, true);

        // 服务端用 list 保存用户连接
        clients_list.push_back(clientfd);
        printf("Add new clientfd = %d to epoll\n", clientfd);
        printf("Now there are %d clients int the chat room\n", (int)clients_list.size());

        // 服务端发送欢迎信息  
        printf("welcome message\n");        
        char message[BUF_SIZE];
        bzero(message, BUF_SIZE);
        sprintf(message, SERVER_WELCOME, clientfd);
        int ret = send(clientfd, message, BUF_SIZE, 0);
        if(ret < 0) { perror("send error"); exit(-1); }
      }
      //处理用户发来的消息,并广播,使其他用户收到信息
      else 
      {   
        int ret = sendBroadcastmessage(sockfd);
        if(ret < 0) { perror("error");exit(-1); }
      }
    }
  }
  close(listener); //关闭 socket
  close(epfd);  //关闭内核
  return 0;
}

下面在实验楼的环境中测试下服务端。在桌面上先建立两个文件,分别为 server.cpp, utility.h,并将上边的完整源码分别拷贝进去,如下图 图片描述信息 图片描述信息 然后执行,打开 Xfce 终端使用如下命令执行,因为源码中使用了 C++的 list 容器,所以不用 gcc 而使用 g++编译,如下图

g++ server.cpp utility.h -o server

图片描述信息 执行成功后,在桌面上生成一个可执行文件 server, 使用如下命令启动服务端:

./server

图片描述信息 如图所示,服务端已经启动了,正在等待客户端的连接,别急,下面来介绍下客户端的实现。

4. 客户端实现

4.1 子进程和父进程的通信

前面已经介绍了子进程和父进程的功能,他们之间用管道进行通信。如下图所示,我们可以更直观的了解子进程和父进程各自的功能。 图片描述信息 通过调用 int pipe(int fd[2]) 函数创建管道,其中 fd[0]用于父进程读, fd[1]用于子进程写。

//client.cpp 代码(管道模块)
   // 创建管道.
  int pipe_fd[2];
  if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }

通过 int pid = fork() 函数,创建子进程,当 pid < 0 错误;当 pid = 0, 说明是子进程;当 pid > 0 说明是父进程。根据 pid 的值,我们可以父子进程,从而实现对应的功能!

4.2 客户端完整源码

根据上述介绍,我们可以写出客户端的源码。如下:

#include "utility.h"

int main(int argc, char *argv[])
{
  //用户连接的服务器 IP + port
  struct sockaddr_in serverAddr;
  serverAddr.sin_family = PF_INET;
  serverAddr.sin_port = htons(SERVER_PORT);
  serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);

  // 创建 socket
  int sock = socket(PF_INET, SOCK_STREAM, 0);
  if(sock < 0) { perror("sock error"); exit(-1); }
  // 连接服务端
  if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
    perror("connect error");
    exit(-1);
  }

  // 创建管道,其中 fd[0]用于父进程读,fd[1]用于子进程写
  int pipe_fd[2];
  if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }

  // 创建 epoll
  int epfd = epoll_create(EPOLL_SIZE);
  if(epfd < 0) { perror("epfd error"); exit(-1); }
  static struct epoll_event events[2]; 
  //将 sock 和管道读端描述符都添加到内核事件表中
  addfd(epfd, sock, true);
  addfd(epfd, pipe_fd[0], true);
  // 表示客户端是否正常工作
  bool isClientwork = true;

  // 聊天信息缓冲区
  char message[BUF_SIZE];

  // Fork
  int pid = fork();
  if(pid < 0) { perror("fork error"); exit(-1); }
  else if(pid == 0)    // 子进程
  {
    //子进程负责写入管道,因此先关闭读端
    close(pipe_fd[0]); 
    printf("Please input 'exit' to exit the chat room\n");

    while(isClientwork){
      bzero(&message, BUF_SIZE);
      fgets(message, BUF_SIZE, stdin);

      // 客户输出 exit,退出
      if(strncasecmp(message, EXIT, strlen(EXIT)) == 0){
        isClientwork = 0;
      }
      // 子进程将信息写入管道
      else {
        if( write(pipe_fd[1], message, strlen(message) - 1 ) < 0 )
         { perror("fork error"); exit(-1); }
      }
    }
  }
  else  //pid > 0 父进程
  {
    //父进程负责读管道数据,因此先关闭写端
    close(pipe_fd[1]); 

    // 主循环(epoll_wait)
    while(isClientwork) {
      int epoll_events_count = epoll_wait( epfd, events, 2, -1 );
      //处理就绪事件
      for(int i = 0; i < epoll_events_count ; ++i)
      {
        bzero(&message, BUF_SIZE);

        //服务端发来消息
        if(events[i].data.fd == sock)
        {
          //接受服务端消息
          int ret = recv(sock, message, BUF_SIZE, 0);

          // ret= 0 服务端关闭
          if(ret == 0) {
            printf("Server closed connection: %d\n", sock);
            close(sock);
            isClientwork = 0;
          }
          else printf("%s\n", message);

        }
        //子进程写入事件发生,父进程处理并发送服务端
        else { 
          //父进程从管道中读取数据
          int ret = read(events[i].data.fd, message, BUF_SIZE);

          // ret = 0
          if(ret == 0) isClientwork = 0;
          else{   // 将信息发送给服务端
            send(sock, message, BUF_SIZE, 0);
          }
        }
      }//for
    }//while
  }

  if(pid){
     //关闭父进程和 sock
    close(pipe_fd[0]);
    close(sock);
  }else{
    //关闭子进程
    close(pipe_fd[1]);
  }
  return 0;
}

同理,在实验楼的环境中,建立一个 client.cpp 文件,并将上述完整源码拷贝进去,然后启动一个新的 XFce 终端,执行如下命令:

cd Desktop
g++ client.cpp utility.h -o client
./client

图片描述信息 如图所示,通过查看两个终端界面,可以看到有一个用户登陆服务端了。 同理,再点击一下桌面上的 XFce,开启一个终端,运行同样的命令(这里不用运行 g++进行编译了,因为前面已经生成了可执行文件 client):

cd Desktop
./client

现在就有两个用户在聊天室了,可以相互发送消息了。当然你可以开启更多的客户端去聊天。 图片描述信息

图片描述信息 欢迎交流和沟通! 到此我们的聊天室就完成了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文