epoll聊天程序

项目介绍

本项目是实现一个简单的聊天室,聊天室分为服务端和客户端。本项目将很多复杂的功能都去掉了,线程池、多线程编程、超时重传、确认收包等等都不会涉及。总共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是一个头文件,包含服务端程序和客户端程序都会用到的一些头文件、变量声明、函数、宏等。

 

 

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

(1)使用socket()创建TCP套接字(socket)

(2)将创建的套接字绑定到一个本地地址和端口上(Bind)

(3)将套接字设为监听模式,准备接收客户端请求(listen)

(4)等待客户请求到来: 当请求到来后,接受连接请求,返回一个对应于此次连接的新的套接字(accept)

(5)用accept返回的套接字和客户端进行通信(使用write()/send()或send()/recv() )

(6)返回,等待另一个客户请求

(7)关闭套接字

[cpp] view plain copy

  1. //server.cpp代码(通信模块):  
  2.     //服务端地址 ip地址 + 端口号  
  3.     struct sockaddr_in serverAddr;  
  4.     serverAddr.sin_family = PF_INET;  
  5.     serverAddr.sin_port = htons(SERVER_PORT);  
  6.     serverAddr.sin_addr.s_addr = inet_addr(SERVER_HOST);  
  7.   
  8.     //服务端创建监听socket  
  9.     int listener = socket(PF_INET, SOCK_STREAM, 0);  
  10.     if(listener < 0) { perror("listener"); exit(-1);}  
  11.     printf("listen socket created \n");  
  12.   
  13.     //将服务端地址与监听socket绑定  
  14.     if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {  
  15.         perror("bind error");  
  16.         exit(-1);  
  17.     }  
  18.     //开始监听  
  19.     int ret = listen(listener, 5);  
  20.     if(ret < 0) { perror("listen error"); exit(-1);}  
  21.     printf("Start to listen: %s\n", SERVER_HOST);  

 

2. 基本技术介绍

2.1 阻塞与非阻塞socket

 

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

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

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

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

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

 

[cpp] view plain copy

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

 

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. 1、int epoll_create(int size)

  2. 创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll所支持的最大句柄数

 
  1. 2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

  2. 函数功能: epoll事件注册函数

  3.   参数epfd为epoll的句柄,即epoll_create返回值

  4.   参数op表示动作,用3个宏来表示:

  5.    EPOLL_CTL_ADD(注册新的fd到epfd),

  6.  EPOLL_CTL_MOD(修改已经注册的fd的监听事件),

  7.    EPOLL_CTL_DEL(从epfd删除一个fd);

  8.    其中参数fd为需要监听的标示符;

  9.   参数event告诉内核需要监听的事件,event的结构如下:

  10. struct epoll_event {

  11. __uint32_t events; //Epoll events

  12. epoll_data_t data; //User data variable

  13. };

  14. 其中介绍events是宏的集合,本项目主要使用EPOLLIN(表示对应的文件描述符可以读,即读事件发生),其他宏类型,可以google之!

 
  1. 3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

  2. 等待事件的产生,函数返回需要处理的事件数目(该数目是就绪事件的数目,就是前面所说漂亮女孩的个数N)

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

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

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

[cpp] view plain copy

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

3. 服务端实现

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

3.1 utility.h

[cpp] view plain copy

  1. /* 限于篇幅,这里先介绍下utility.h的主要构成。其中的头文件和一些函数实现没有显示,完整源码位于3.2节 */  
  2.     //服务端存储所有在线用户socket, 便于广播信息  
  3.     list<int> clients_list;  
  4.     // 服务器ip地址,为测试使用本地机地址,可以更改为其他服务端地址  
  5.     #define SERVER_IP "127.0.0.1"  
  6.     // 服务器端口号  
  7.     #define SERVER_PORT 8888  
  8.     //int epoll_create(int size)中的size,为epoll支持的最大句柄数  
  9.     #define EPOLL_SIZE 5000  
  10.   
  11.     // 缓冲区大小65535  
  12.     #define BUF_SIZE 0xFFFF  
  13.     //一些宏  
  14.     #define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"  
  15.     #define SERVER_MESSAGE "ClientID %d say >> %s"  
  16.     #define EXIT "EXIT"  
  17.     #define CAUTION "There is only one int the char room!"  
  18.     /* 一些函数 */  
  19.     //设置非阻塞  
  20.     int setnonblocking(int sockfd);  
  21.     //将文件描述符fd添加到epollfd标示的内核事件表  
  22.     void addfd( int epollfd, int fd, bool enable_et );  
  23.     //服务端发送广播信息,使所有用户都能收到消息  
  24.     int sendBroadcastmessage(int clientfd);  
  25. 3.1 utility.h完整源码  
  26.   
  27. #ifndef UTILITY_H_INCLUDED  
  28. #define UTILITY_H_INCLUDED  
  29.   
  30. #include <iostream>  
  31. #include <list>  
  32. #include <sys/types.h>  
  33. #include <sys/socket.h>  
  34. #include <netinet/in.h>  
  35. #include <arpa/inet.h>  
  36. #include <sys/epoll.h>  
  37. #include <fcntl.h>  
  38. #include <errno.h>  
  39. #include <unistd.h>  
  40. #include <stdio.h>  
  41. #include <stdlib.h>  
  42. #include <string.h>  
  43.   
  44. using namespace std;  
  45.   
  46. // clients_list save all the clients's socket  
  47. list<int> clients_list;  
  48.   
  49. /**********************   macro defintion **************************/  
  50. // server ip  
  51. #define SERVER_IP "127.0.0.1"  
  52.   
  53. // server port  
  54. #define SERVER_PORT 8888  
  55.   
  56. //epoll size  
  57. #define EPOLL_SIZE 5000  
  58.   
  59. //message buffer size  
  60. #define BUF_SIZE 0xFFFF  
  61.   
  62. #define SERVER_WELCOME "Welcome you join  to the chat room! Your chat ID is: Client #%d"  
  63.   
  64. #define SERVER_MESSAGE "ClientID %d say >> %s"  
  65.   
  66. // exit  
  67. #define EXIT "EXIT"  
  68.   
  69. #define CAUTION "There is only one int the char room!"  
  70.   
  71. /**********************   some function **************************/  
  72. /** 
  73.   * @param sockfd: socket descriptor 
  74.   * @return 0 
  75. **/  
  76. int setnonblocking(int sockfd)  
  77. {  
  78.     fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);  
  79.     return 0;  
  80. }  
  81.   
  82. /** 
  83.   * @param epollfd: epoll handle 
  84.   * @param fd: socket descriptor 
  85.   * @param enable_et : enable_et = true, epoll use ET; otherwise LT 
  86. **/  
  87. void addfd( int epollfd, int fd, bool enable_et )  
  88. {  
  89.     struct epoll_event ev;  
  90.     ev.data.fd = fd;  
  91.     ev.events = EPOLLIN;  
  92.     if( enable_et )  
  93.         ev.events = EPOLLIN | EPOLLET;  
  94.     epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);  
  95.     setnonblocking(fd);  
  96.     printf("fd added to epoll!\n\n");  
  97. }  
  98.   
  99. /** 
  100.   * @param clientfd: socket descriptor 
  101.   * @return : len 
  102. **/  
  103. int sendBroadcastmessage(int clientfd)  
  104. {  
  105.     // buf[BUF_SIZE] receive new chat message  
  106.     // message[BUF_SIZE] save format message  
  107.     char buf[BUF_SIZE], message[BUF_SIZE];  
  108.     bzero(buf, BUF_SIZE);  
  109.     bzero(message, BUF_SIZE);  
  110.   
  111.     // receive message  
  112.     printf("read from client(clientID = %d)\n", clientfd);  
  113.     int len = recv(clientfd, buf, BUF_SIZE, 0);  
  114.   
  115.     if(len == 0)  // len = 0 means the client closed connection  
  116.     {  
  117.         close(clientfd);  
  118.         clients_list.remove(clientfd); //server remove the client  
  119.         printf("ClientID = %d closed.\n now there are %d client in the char room\n", clientfd, (int)clients_list.size());  
  120.   
  121.     }  
  122.     else  //broadcast message   
  123.     {  
  124.         if(clients_list.size() == 1) { // this means There is only one int the char room  
  125.             send(clientfd, CAUTION, strlen(CAUTION), 0);  
  126.             return len;  
  127.         }  
  128.         // format message to broadcast  
  129.         sprintf(message, SERVER_MESSAGE, clientfd, buf);  
  130.   
  131.         list<int>::iterator it;  
  132.         for(it = clients_list.begin(); it != clients_list.end(); ++it) {  
  133.            if(*it != clientfd){  
  134.                 if( send(*it, message, BUF_SIZE, 0) < 0 ) { perror("error"); exit(-1);}  
  135.            }  
  136.         }  
  137.     }  
  138.     return len;  
  139. }  
  140. #endif // UTILITY_H_INCLUDED  

3.3 服务端完整源码

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

[cpp] view plain copy

  1. #include "utility.h"  
  2.   
  3. int main(int argc, char *argv[])  
  4. {  
  5.     //服务器IP + port  
  6.     struct sockaddr_in serverAddr;  
  7.     serverAddr.sin_family = PF_INET;  
  8.     serverAddr.sin_port = htons(SERVER_PORT);  
  9.     serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);  
  10.     //创建监听socket  
  11.     int listener = socket(PF_INET, SOCK_STREAM, 0);  
  12.     if(listener < 0) { perror("listener"); exit(-1);}  
  13.     printf("listen socket created \n");  
  14.     //绑定地址  
  15.     if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {  
  16.         perror("bind error");  
  17.         exit(-1);  
  18.     }  
  19.     //监听  
  20.     int ret = listen(listener, 5);  
  21.     if(ret < 0) { perror("listen error"); exit(-1);}  
  22.     printf("Start to listen: %s\n", SERVER_IP);  
  23.     //在内核中创建事件表  
  24.     int epfd = epoll_create(EPOLL_SIZE);  
  25.     if(epfd < 0) { perror("epfd error"); exit(-1);}  
  26.     printf("epoll created, epollfd = %d\n", epfd);  
  27.     static struct epoll_event events[EPOLL_SIZE];  
  28.     //往内核事件表里添加事件  
  29.     addfd(epfd, listener, true);  
  30.     //主循环  
  31.     while(1)  
  32.     {  
  33.         //epoll_events_count表示就绪事件的数目  
  34.         int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);  
  35.         if(epoll_events_count < 0) {  
  36.             perror("epoll failure");  
  37.             break;  
  38.         }  
  39.   
  40.         printf("epoll_events_count = %d\n", epoll_events_count);  
  41.         //处理这epoll_events_count个就绪事件  
  42.         for(int i = 0; i < epoll_events_count; ++i)  
  43.         {  
  44.             int sockfd = events[i].data.fd;  
  45.             //新用户连接  
  46.             if(sockfd == listener)  
  47.             {  
  48.                 struct sockaddr_in client_address;  
  49.                 socklen_t client_addrLength = sizeof(struct sockaddr_in);  
  50.                 int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );  
  51.   
  52.                 printf("client connection from: %s : % d(IP : port), clientfd = %d \n",  
  53.                 inet_ntoa(client_address.sin_addr),  
  54.                 ntohs(client_address.sin_port),  
  55.                 clientfd);  
  56.   
  57.                 addfd(epfd, clientfd, true);  
  58.   
  59.                 // 服务端用list保存用户连接  
  60.                 clients_list.push_back(clientfd);  
  61.                 printf("Add new clientfd = %d to epoll\n", clientfd);  
  62.                 printf("Now there are %d clients int the chat room\n", (int)clients_list.size());  
  63.   
  64.                 // 服务端发送欢迎信息    
  65.                 printf("welcome message\n");                  
  66.                 char message[BUF_SIZE];  
  67.                 bzero(message, BUF_SIZE);  
  68.                 sprintf(message, SERVER_WELCOME, clientfd);  
  69.                 int ret = send(clientfd, message, BUF_SIZE, 0);  
  70.                 if(ret < 0) { perror("send error"); exit(-1); }  
  71.             }  
  72.             //处理用户发来的消息,并广播,使其他用户收到信息  
  73.             else   
  74.             {     
  75.                 int ret = sendBroadcastmessage(sockfd);  
  76.                 if(ret < 0) { perror("error");exit(-1); }  
  77.             }  
  78.         }  
  79.     }  
  80.     close(listener); //关闭socket  
  81.     close(epfd);    //关闭内核  
  82.     return 0;  
  83. }  
g++ server.cpp utility.h -o server
./server

4. 客户端实现

4.1 子进程和父进程的通信

前面已经介绍了子进程和父进程的功能,他们之间用管道进行通信。如下图所示,我们可以更直观的了解子进程和父进程各自的功能。 

通过调用int pipe(int fd[2])函数创建管道, 其中fd[0]用于父进程读, fd[1]用于子进程写。[cpp] view plain copy

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

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

4.2 客户端完整源码

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

[cpp] view plain copy

  1. #include "utility.h"  
  2.   
  3. int main(int argc, char *argv[])  
  4. {  
  5.     //用户连接的服务器 IP + port  
  6.     struct sockaddr_in serverAddr;  
  7.     serverAddr.sin_family = PF_INET;  
  8.     serverAddr.sin_port = htons(SERVER_PORT);  
  9.     serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);  
  10.   
  11.     // 创建socket  
  12.     int sock = socket(PF_INET, SOCK_STREAM, 0);  
  13.     if(sock < 0) { perror("sock error"); exit(-1); }  
  14.     // 连接服务端  
  15.     if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {  
  16.         perror("connect error");  
  17.         exit(-1);  
  18.     }  
  19.   
  20.     // 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写  
  21.     int pipe_fd[2];  
  22.     if(pipe(pipe_fd) < 0) { perror("pipe error"); exit(-1); }  
  23.   
  24.     // 创建epoll  
  25.     int epfd = epoll_create(EPOLL_SIZE);  
  26.     if(epfd < 0) { perror("epfd error"); exit(-1); }  
  27.     static struct epoll_event events[2];   
  28.     //将sock和管道读端描述符都添加到内核事件表中  
  29.     addfd(epfd, sock, true);  
  30.     addfd(epfd, pipe_fd[0], true);  
  31.     // 表示客户端是否正常工作  
  32.     bool isClientwork = true;  
  33.   
  34.     // 聊天信息缓冲区  
  35.     char message[BUF_SIZE];  
  36.   
  37.     // Fork  
  38.     int pid = fork();  
  39.     if(pid < 0) { perror("fork error"); exit(-1); }  
  40.     else if(pid == 0)      // 子进程  
  41.     {  
  42.         //子进程负责写入管道,因此先关闭读端  
  43.         close(pipe_fd[0]);   
  44.         printf("Please input 'exit' to exit the chat room\n");  
  45.   
  46.         while(isClientwork){  
  47.             bzero(&message, BUF_SIZE);  
  48.             fgets(message, BUF_SIZE, stdin);  
  49.   
  50.             // 客户输出exit,退出  
  51.             if(strncasecmp(message, EXIT, strlen(EXIT)) == 0){  
  52.                 isClientwork = 0;  
  53.             }  
  54.             // 子进程将信息写入管道  
  55.             else {  
  56.                 if( write(pipe_fd[1], message, strlen(message) - 1 ) < 0 )  
  57.                  { perror("fork error"); exit(-1); }  
  58.             }  
  59.         }  
  60.     }  
  61.     else  //pid > 0 父进程  
  62.     {  
  63.         //父进程负责读管道数据,因此先关闭写端  
  64.         close(pipe_fd[1]);   
  65.   
  66.         // 主循环(epoll_wait)  
  67.         while(isClientwork) {  
  68.             int epoll_events_count = epoll_wait( epfd, events, 2, -1 );  
  69.             //处理就绪事件  
  70.             for(int i = 0; i < epoll_events_count ; ++i)  
  71.             {  
  72.                 bzero(&message, BUF_SIZE);  
  73.   
  74.                 //服务端发来消息  
  75.                 if(events[i].data.fd == sock)  
  76.                 {  
  77.                     //接受服务端消息  
  78.                     int ret = recv(sock, message, BUF_SIZE, 0);  
  79.   
  80.                     // ret= 0 服务端关闭  
  81.                     if(ret == 0) {  
  82.                         printf("Server closed connection: %d\n", sock);  
  83.                         close(sock);  
  84.                         isClientwork = 0;  
  85.                     }  
  86.                     else printf("%s\n", message);  
  87.   
  88.                 }  
  89.                 //子进程写入事件发生,父进程处理并发送服务端  
  90.                 else {   
  91.                     //父进程从管道中读取数据  
  92.                     int ret = read(events[i].data.fd, message, BUF_SIZE);  
  93.   
  94.                     // ret = 0  
  95.                     if(ret == 0) isClientwork = 0;  
  96.                     else{   // 将信息发送给服务端  
  97.                       send(sock, message, BUF_SIZE, 0);  
  98.                     }  
  99.                 }  
  100.             }//for  
  101.         }//while  
  102.     }  
  103.   
  104.     if(pid){  
  105.        //关闭父进程和sock  
  106.         close(pipe_fd[0]);  
  107.         close(sock);  
  108.     }else{  
  109.         //关闭子进程  
  110.         close(pipe_fd[1]);  
  111.     }  
  112.     return 0;  
  113. }  

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

 
  1. cd Desktop

  2. g++ client.cpp utility.h -o client

  3. ./client

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

 
  1. cd Desktop

  2. ./client

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值