C ++ socket network programming - Instant Messaging System

Transfer: Author: CoderWill_Hunting Original: https: //blog.csdn.net/lewis1993_cpapa/article/details/80589717 

A: Project Content

This item includes the use of a C ++ implementation of the server and client and instant messaging private chat room having the chat function.

The purpose is to learn the basic concepts of C ++ Web development, but can also be familiar with C in under Linux ++ program compiled and written in simple MakeFile
II: needs analysis

This room there are two main programs:

1. server: the client can accept new connections, and to send each client of the message, broadcast to the corresponding target client.

2. The client: the ability to connect to the server, the server sends a message, and may receive a message sent from the server.

I.e., the most simple C / S model.
Three: abstract and refinement

Class server needs to support:

1. The support multiple clients access, chat rooms basic functions.

2. Start services, the establishment listening port to wait for client connections.

3. Use the epoll mechanisms to achieve concurrent increase efficiency.

4. When the client connection, send a welcome message, and store the connection record.

5. The client sends a message according to the message type, broadcast to all users (the group chat) or the specified user (whisper).

6. When the client requests to exit, the respective cleaning connection information.

The client class needs to support:

1. Connect server.

2. Support for user input messages sent to the server.

3. accepts and displays a message sent by the server.

4. Exit connection.

It involves two things, a write, a read. So the client needs two processes are supported by the following functions.

Child process:

1. Wait for user input information.

2. The information is written to chat pipe (pipe), and sent to the parent process.

Parent:

1. epoll mechanisms to receive a message sent by the server and displayed to the user, allow users to see other users.

2. The chat message sent by the child process reads from the pipe (pipe), and transmits to the client.
Four: C / S model

TCP服务端通信常规步骤:                                                                                                    

1.socket()创建TCP套接字                                                                              

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

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

4.accept()等用户请求到来时接受,返回一个对应此连接新套接字   

5.用accept()返回的套接字和客户端进行通信,recv()/send() 接受/发送信息。                               

6.返回,等待另一个客户请求。

7.关闭套接字

TCP客户端通信常规步骤:

1.socket()创建TCP套接字。

2.connect()建立到达服务器的连接。

3.与客户端进行通信,recv()/send()接受/发送信息,write()/read() 子进程写入管道,父进程从管道中读取信息然后send给客户端

5. close() 关闭客户连接。
五:相关技术介绍

1.socket 阻塞与非阻塞。

阻塞与非阻塞关注的是程序在等待调用结果时(消息,返回值)的状态。

阻塞调用是指在调用结果返回前,当前线程会被挂起,调用线程只有在得到调用结果之后才会返回。

非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。

eg. 你打电话问书店老板有没有《网络编程》这本书,老板去书架上找,如果是阻塞式调用,你就会把自己一直挂起,守在电话边上,直到得到这本书有或者没有的答案。如果是非阻塞式调用,你可以干别的事情去,隔一段时间来看一下老板有没有告诉你结果。

同步异步是对书店老板而言(同步老板不会提醒你找到结果了,异步老板会打电话告诉你),阻塞和非阻塞是对你而言。

更多可以参考博客:socket阻塞与非阻塞

socket()函数创建套接字时,默认的套接字都是阻塞的,非阻塞设置方式代码:

    //将文件描述符设置为非阻塞方式(利用fcntl函数)
    fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);

2. epoll

当服务端的人数越来越多,会导致资源吃紧,I/O效率越来越低,这时就应该考虑epoll,epoll是Linux内核为处理大量句柄而改进的poll,是linux特有的I/O函数。其特点如下:

1)epoll是Linux下多路复用IO接口select/poll的增强版本,其实现和使用方式与select/poll大有不同,epoll通过一组函数来完成有关任务,而不是一个函数。

2)epoll之所以高效,是因为epoll将用户关心的文件描述符放到内核里的一个事件列表中,而不是像select/poll每次调用都需要重复传入文件描述符集或事件集(大量拷贝开销),比如一个事件发生,epoll无需遍历整个被监听的描述符集,而只需要遍历哪些被内核IO事件异步唤醒而加入就绪队列的描述符集合即可。

3)epoll有两种工作方式,LT(Level triggered) 水平触发 、ET(Edge triggered)边沿触发。LT是select/poll的工作方式,比较低效,而ET是epoll具有的高速工作方式。更多epoll之ET LT

Epoll 用法(三步曲):

第一步:int epoll_create(int size)系统调用,创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll支持的最大句柄数。

第二步:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)  事件注册函数

参数 epfd为epoll的句柄。参数op 表示动作 三个宏来表示:EPOLL_CTL_ADD注册新fd到epfd 、EPOLL_CTL_MOD 修改已经注册的fd的监听事件、EPOLL_CTL_DEL从epfd句柄中删除fd。参数fd为需要监听的标识符。参数结构体epoll_event告诉内核需要监听的事件。

第三步:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 等待事件的产生,通过调用收集在epoll监控中已经发生的事件。参数struct epoll_event 是事件队列 把就绪的事件放进去。

eg. 服务端使用epoll的时候步骤如下:

1.调用epoll_create()在linux内核中创建一个事件表。

2.然后将文件描述符(监听套接字listener)添加到事件表中

3.在主循环中,调用epoll_wait()等待返回就绪的文件描述符集合。

4.分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件。
六:代码结构

每个文件的作用:

1.Common.h:公共头文件,包括所有需要的宏以及socket网络编程头文件,以及消息结构体(用来表示消息类别等)

2.Client.h Client.cpp :客户端类的实现

3.Server.h Server.cpp : 服务端类的实现

4.ClientMain.cpp ServerMain.cpp 客户端及服务端的主函数。
七:代码实现

Common.h

定义一些共用的宏定义,包括一些共用的网络编程相关头文件。

1)定义一个函数将文件描述符fd添加到epfd表示的内核事件表中供客户端和服务端两个类使用。

2)定义一个信息数据结构,用来表示传送的信息,结构体包括发送方fd, 接收方fd,用来表示消息类别的type,还有文字信息。

函数recv() send() write() read() 参数传递是字符串,所以在传送前/接受后要把结构体转换为字符串/字符串转换为结构体。

  

  #ifndef  CHATROOM_COMMON_H
    #define CHATROOM_COMMON_H
     
    #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>
     
    // 默认服务器端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 SERVER_PRIVATE_MESSAGE "Client %d say to you privately >> %s"
    #define SERVER_PRIVATE_ERROR_MESSAGE "Client %d is not in the chat room yet~"
    // 退出系统
    #define EXIT "EXIT"
     
    // 提醒你是聊天室中唯一的客户
    #define CAUTION "There is only one int the char room!"
     
     
    // 注册新的fd到epollfd中
    // 参数enable_et表示是否启用ET模式,如果为True则启用,否则使用LT模式
    static 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);
        // 设置socket为非阻塞模式
        // 套接字立刻返回,不管I/O是否完成,该函数所在的线程会继续运行
        //eg. 在recv(fd...)时,该函数立刻返回,在返回时,内核数据还没准备好会返回WSAEWOULDBLOCK错误代码
        fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)| O_NONBLOCK);
        printf("fd added to epoll!\n\n");
    }
     
    //定义信息结构,在服务端和客户端之间传送
    struct Msg
    {
        int type;
        int fromID;
        int toID;
        char content[BUF_SIZE];
     
    };
    #endif // CHATROOM_COMMON_H

服务端类 Server.h Server.cpp

服务端需要的接口:

1)init()初始化

2)Start()启动服务

3)Close()关闭服务

4)广播消息给所有客户端函数 SendBroadcastMessage()

服务端的主循环中每次都会检查并处理EPOLL中的就绪事件,而就绪事件列表主要是两种类型:新连接或新消息。服务器会依次从就绪事件列表里提取事件进行处理,如果是新连接则accept()然后addfd(),如果是新消息则SendBroadcastMessage()实现聊天功能。

Server.h

   

#ifndef CHATROOM_SERVER_H
    #define CHATROOM_SERVER_H
     
    #include <string>
     
    #include "Common.h"
     
    using namespace std;
     
    // 服务端类,用来处理客户端请求
    class Server {
     
    public:
        // 无参数构造函数
        Server();
     
        // 初始化服务器端设置
        void Init();
     
        // 关闭服务
        void Close();
     
        // 启动服务端
        void Start();
     
    private:
        // 广播消息给所有客户端
        int SendBroadcastMessage(int clientfd);
     
        // 服务器端serverAddr信息
        struct sockaddr_in serverAddr;
        
        //创建监听的socket
        int listener;
     
        // epoll_create创建后的返回值
        int epfd;
        
        // 客户端列表
        list<int> clients_list;
    };

    //Server.cpp
   

#include <iostream>
     
    #include "Server.h"
     
    using namespace std;
     
    // 服务端类成员函数
     
    // 服务端类构造函数
    Server::Server(){
        
        // 初始化服务器地址和端口
        serverAddr.sin_family = PF_INET;
        serverAddr.sin_port = htons(SERVER_PORT);
        serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
     
        // 初始化socket
        listener = 0;
        
        // epool fd
        epfd = 0;
    }
    // 初始化服务端并启动监听
    void Server::Init() {
        cout << "Init Server..." << endl;
        
         //创建监听socket
        listener = socket(PF_INET, SOCK_STREAM, 0);
        if(listener < 0) { perror("listener"); exit(-1);}
        
        //绑定地址
        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);
        }
     
        cout << "Start to listen: " << SERVER_IP << endl;
     
        //在内核中创建事件表 epfd是一个句柄
        epfd = epoll_create (EPOLL_SIZE);
        
        if(epfd < 0) {
            perror("epfd error");
            exit(-1);
        }
     
        //往事件表里添加监听事件
        addfd(epfd, listener, true);
     
    }
     
    // 关闭服务,清理并关闭文件描述符
    void Server::Close() {
     
        //关闭socket
        close(listener);
        
        //关闭epoll监听
        close(epfd);
    }
     
    // 发送广播消息给所有客户端
    int Server::SendBroadcastMessage(int clientfd)
    {
        // buf[BUF_SIZE] 接收新消息
        // message[BUF_SIZE] 保存格式化的消息
        char recv_buf[BUF_SIZE];
        char send_buf[BUF_SIZE];
        Msg msg;
        bzero(recv_buf, BUF_SIZE);
        // 接收新消息
        cout << "read from client(clientID = " << clientfd << ")" << endl;
        int len = recv(clientfd, recv_buf, BUF_SIZE, 0);
        //清空结构体,把接受到的字符串转换为结构体
        memset(&msg,0,sizeof(msg));
        memcpy(&msg,recv_buf,sizeof(msg));
        //判断接受到的信息是私聊还是群聊
        msg.fromID=clientfd;
        if(msg.content[0]=='\\'&&isdigit(msg.content[1])){
            msg.type=1;
            msg.toID=msg.content[1]-'0';
            memcpy(msg.content,msg.content+2,sizeof(msg.content));
        }
        else
            msg.type=0;
        // 如果客户端关闭了连接
        if(len == 0)
        {
            close(clientfd);
            
            // 在客户端列表中删除该客户端
            clients_list.remove(clientfd);
            cout << "ClientID = " << clientfd
                 << " closed.\n now there are "
                 << clients_list.size()
                 << " client in the char room"
                 << endl;
     
        }
        // 发送广播消息给所有客户端
        else
        {
            // 判断是否聊天室还有其他客户端
            if(clients_list.size() == 1){
                // 发送提示消息
                memcpy(&msg.content,CAUTION,sizeof(msg.content));
                bzero(send_buf, BUF_SIZE);
                memcpy(send_buf,&msg,sizeof(msg));
                send(clientfd, send_buf, sizeof(send_buf), 0);
                return len;
            }
            //存放格式化后的信息
            char format_message[BUF_SIZE];
            //群聊
            if(msg.type==0){
                // 格式化发送的消息内容 #define SERVER_MESSAGE "ClientID %d say >> %s"
                sprintf(format_message, SERVER_MESSAGE, clientfd, msg.content);
                memcpy(msg.content,format_message,BUF_SIZE);
                // 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
                list<int>::iterator it;
                for(it = clients_list.begin(); it != clients_list.end(); ++it) {
                   if(*it != clientfd){
                        //把发送的结构体转换为字符串
                        bzero(send_buf, BUF_SIZE);
                        memcpy(send_buf,&msg,sizeof(msg));
                        if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
                            return -1;
                        }
                   }
                }
            }
            //私聊
            if(msg.type==1){
                bool private_offline=true;
                sprintf(format_message, SERVER_PRIVATE_MESSAGE, clientfd, msg.content);
                memcpy(msg.content,format_message,BUF_SIZE);
                // 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
                list<int>::iterator it;
                for(it = clients_list.begin(); it != clients_list.end(); ++it) {
                   if(*it == msg.toID){
                        private_offline=false;
                        //把发送的结构体转换为字符串
                        bzero(send_buf, BUF_SIZE);
                        memcpy(send_buf,&msg,sizeof(msg));
                        if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
                            return -1;
                        }
                   }
                }
                //如果私聊对象不在线
                if(private_offline){
                    sprintf(format_message,SERVER_PRIVATE_ERROR_MESSAGE,msg.toID);
                    memcpy(msg.content,format_message,BUF_SIZE);
                    bzero(send_buf,BUF_SIZE);
                    memcpy(send_buf,&msg,sizeof(msg));
                    if(send(msg.fromID,send_buf,sizeof(send_buf),0)<0)
                        return -1;
                }
            }
        }
        return len;
    }
     
    // 启动服务端
    void Server::Start() {
     
        // epoll 事件队列
        static struct epoll_event events[EPOLL_SIZE];
     
        // 初始化服务端
        Init();
     
        //主循环
        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;
            }
     
            cout << "epoll_events_count =\n" << epoll_events_count << endl;
     
            //处理这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 );
     
                    cout << "client connection from: "
                         << inet_ntoa(client_address.sin_addr) << ":"
                         << ntohs(client_address.sin_port) << ", clientfd = "
                         << clientfd << endl;
     
                    addfd(epfd, clientfd, true);
     
                    // 服务端用list保存用户连接
                    clients_list.push_back(clientfd);
                    cout << "Add new clientfd = " << clientfd << " to epoll" << endl;
                    cout << "Now there are " << clients_list.size() << " clients int the chat room" << endl;
     
                    // 服务端发送欢迎信息  
                    cout << "welcome message" << endl;                
                    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");
                        Close();
                        exit(-1);
                    }
                }
                //处理用户发来的消息,并广播,使其他用户收到信息
                else {   
                    int ret = SendBroadcastMessage(sockfd);
                    if(ret < 0) {
                        perror("error");
                        Close();
                        exit(-1);
                    }
                }
            }
        }
     
        // 关闭服务
        Close();
    }

客户端类实现

需要的接口:

1)连接服务端connect()

2)退出连接close()

3)启动客户端Start()

Client.h

   

#ifndef CHATROOM_CLIENT_H
    #define CHATROOM_CLIENT_H
     
    #include <string>
    #include "Common.h"
     
    using namespace std;
     
    // 客户端类,用来连接服务器发送和接收消息
    class Client {
     
    public:
        // 无参数构造函数
        Client();
     
        // 连接服务器
        void Connect();
     
        // 断开连接
        void Close();
     
        // 启动客户端
        void Start();
     
    private:
     
        // 当前连接服务器端创建的socket
        int sock;
     
        // 当前进程ID
        int pid;
        
        // epoll_create创建后的返回值
        int epfd;
     
        // 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
        int pipe_fd[2];
     
        // 表示客户端是否正常工作
        bool isClientwork;
     
        // 聊天信息
        Msg msg;
        //结构体要转换为字符串
        char send_buf[BUF_SIZE];
        char recv_buf[BUF_SIZE];
        //用户连接的服务器 IP + port
        struct sockaddr_in serverAddr;
    };


Client.cpp

   

#include <iostream>
     
    #include "Client.h"
     
    using namespace std;
     
    // 客户端类成员函数
     
    // 客户端类构造函数
    Client::Client(){
        
        // 初始化要连接的服务器地址和端口
        serverAddr.sin_family = PF_INET;
        serverAddr.sin_port = htons(SERVER_PORT);
        serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
        
        // 初始化socket
        sock = 0;
        
        // 初始化进程号
        pid = 0;
        
        // 客户端状态
        isClientwork = true;
        
        // epool fd
        epfd = 0;
    }
     
    // 连接服务器
    void Client::Connect() {
        cout << "Connect Server: " << SERVER_IP << " : " << SERVER_PORT << endl;
        
        // 创建socket
        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]用于子进程写
        if(pipe(pipe_fd) < 0) {
            perror("pipe error");
            exit(-1);
        }
     
        // 创建epoll
        epfd = epoll_create(EPOLL_SIZE);
        
        if(epfd < 0) {
            perror("epfd error");
            exit(-1);
        }
     
        //将sock和管道读端描述符都添加到内核事件表中
        addfd(epfd, sock, true);
        addfd(epfd, pipe_fd[0], true);
     
    }
     
    // 断开连接,清理并关闭文件描述符
    void Client::Close() {
     
        if(pid){
           //关闭父进程的管道和sock
            close(pipe_fd[0]);
            close(sock);
        }else{
            //关闭子进程的管道
            close(pipe_fd[1]);
        }
    }
     
    // 启动客户端
    void Client::Start() {
     
        // epoll 事件队列
        static struct epoll_event events[2];
        
        // 连接服务器
        Connect();
        
        // 创建子进程
        pid = fork();
        
        // 如果创建子进程失败则退出
        if(pid < 0) {
            perror("fork error");
            close(sock);
            exit(-1);
        } else if(pid == 0) {
            // 进入子进程执行流程
            //子进程负责写入管道,因此先关闭读端
            close(pipe_fd[0]);
     
            // 输入exit可以退出聊天室
            cout << "Please input 'exit' to exit the chat room" << endl;
            cout<<"\\ + ClientID to private chat "<<endl;
            // 如果客户端运行正常则不断读取输入发送给服务端
            while(isClientwork){
                //清空结构体
                memset(msg.content,0,sizeof(msg.content));
                fgets(msg.content, BUF_SIZE, stdin);
                // 客户输出exit,退出
                if(strncasecmp(msg.content, EXIT, strlen(EXIT)) == 0){
                    isClientwork = 0;
                }
                // 子进程将信息写入管道
                else {
                    //清空发送缓存
                    memset(send_buf,0,BUF_SIZE);
                    //结构体转换为字符串
                    memcpy(send_buf,&msg,sizeof(msg));
                    if( write(pipe_fd[1], send_buf, sizeof(send_buf)) < 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)
                {
                    memset(recv_buf,0,sizeof(recv_buf));
                    //服务端发来消息
                    if(events[i].data.fd == sock)
                    {
                        //接受服务端广播消息
                        int ret = recv(sock, recv_buf, BUF_SIZE, 0);
                        //清空结构体
                        memset(&msg,0,sizeof(msg));
                        //将发来的消息转换为结构体
                        memcpy(&msg,recv_buf,sizeof(msg));
     
                        // ret= 0 服务端关闭
                        if(ret == 0) {
                            cout << "Server closed connection: " << sock << endl;
                            close(sock);
                            isClientwork = 0;
                        } else {
                            cout << msg.content << endl;
                        }
                    }
                    //子进程写入事件发生,父进程处理并发送服务端
                    else {
                        //父进程从管道中读取数据
                        int ret = read(events[i].data.fd, recv_buf, BUF_SIZE);
                        // ret = 0
                        if(ret == 0)
                            isClientwork = 0;
                        else {
                            // 将从管道中读取的字符串信息发送给服务端
                            send(sock, recv_buf, sizeof(recv_buf), 0);
                        }
                    }
                }//for
            }//while
        }
        
        // 退出进程
        Close();
    }

ClientMain.cpp

   

#include "Client.h"
     
    // 客户端主函数
    // 创建客户端对象后启动客户端
    int main(int argc, char *argv[]) {
        Client client;
        client.Start();
        return 0;
    }

ServerMain.cpp
   

 #include "Server.h"
     
    // 服务端主函数
    // 创建服务端对象后启动服务端
    int main(int argc, char *argv[]) {
        Server server;
        server.Start();
        return 0;
    }

最后是Makefile 文件 对上面的文件进行编译

   

 CC = g++
    CFLAGS = -std=c++11
     
    all: ClientMain.cpp ServerMain.cpp Server.o Client.o
        $(CC) $(CFLAGS) ServerMain.cpp  Server.o -o chatroom_server
        $(CC) $(CFLAGS) ClientMain.cpp Client.o -o chatroom_client
     
    Server.o: Server.cpp Server.h Common.h
        $(CC) $(CFLAGS) -c Server.cpp
     
    Client.o: Client.cpp Client.h Common.h
        $(CC) $(CFLAGS) -c Client.cpp
     
    clean:
        rm -f *.o chatroom_server chatroom_client


---------------------  
 

Guess you like

Origin blog.csdn.net/qq_38446366/article/details/90484545