C++ 實現基於TCP的聊天室

料峭春风吹酒醒發表於2024-04-29

需求以及思路

客戶端啟動以後,需要去連線服務端,並在控制檯輸入訊息傳送到服務端,服務端收到該訊息後傳送給所有已連線的客戶端。

所以客戶端需要做的事情只有兩個:

  • 接收使用者輸入並將其傳送到服務端
  • 接收服務端訊息並將其顯示到控制檯

服務端要做的事情也是兩個:

  • 接待新連線上的客戶端,為其分配一個服務者
  • 接收每個客戶端的訊息,並將其傳送給所有客戶端

透過分析以上的需求,很容易得出一個簡單的實現思路,使用多執行緒或者多程序來實現。

  • 客戶端連線到服務端之後,建立一個執行緒來處理服務端的訊息,然後在主執行緒中處理控制檯輸入。
  • 服務端在主執行緒中監聽請求,當有客戶端連線時分配一個執行緒用於服務該客戶端。
  • 並且需要記錄所有已連線的客戶端,以便能夠給所有客戶端傳送訊息。

這種方式很簡單粗暴,每次連線上一個客戶端時服務端就會新建一個執行緒或者程序,這並不是很理智的一種做法。

使用多執行緒實現

客戶端

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include "thread"

int main(){

    std::atomic_bool  stop = false;

    // 服務端地址
    struct sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);

    //建立socket並連線到服務端
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))<0){
        std::cerr<<"connect failed."<<std::endl;
        return -1;
    }

    //連線建立後,建立一個執行緒讀取資料
    std::thread t([&](){
        thread_local char buffer[1024];
        while(!stop){
            memset(buffer,0,sizeof (buffer));
            ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
            if(bytes_len==0){
                std::cout<<"server closed."<<std::endl;
                break;
            }else if(bytes_len<0){
                std::cout<<"recv error. exit!"<<std::endl;
                break;
            }else{
                std::cout<<buffer<<std::endl;
            }
        }
        stop = true;
        exit(0);
    });

    //主執行緒中輸入併傳送
    while(!stop){
        char buffer[1024];
        std::cin>>buffer;
        if(stop||strcmp(buffer,"quit")==0){
            break;
        }
        ssize_t bytes_len = send(fd, buffer, sizeof(buffer),0);
        if(bytes_len<0)
        {
            std::cout<<"send error. exit!"<<std::endl;
            break;
        }
    }

    stop = true;
    t.join();
    close(fd);
    return 0;
}

服務端

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <list>
#include <thread>

std::atomic_bool stop = false;
std::mutex mu;

// 儲存所有客戶端檔案描述符的容器
std::vector<int> clientFdVec;

// 儲存所有服務執行緒的容器
std::vector<std::thread> clientWorkerVec;

/**
 * @brief notify 傳送訊息給某個指定的客戶端或者所有的客戶端
 * @param msg
 * @param receiverFd  -1表示傳送給所有客戶端,若要傳送給指定客戶端,該值需要>1
 * @param exceptFd    receiverFd為-1時生效,即不傳送給某客戶端
 */
void notify(const std::string & msg,int receiverFd=-1,int exceptFd=-1){
    if(receiverFd<0){
        std::lock_guard<std::mutex> lk(mu);
        for(int cfd : clientFdVec){
            if(cfd==exceptFd) {
                continue;
            }
            send(cfd,msg.c_str(),msg.length(),0);
        }
    }else{
        send(receiverFd,msg.c_str(),msg.length(),0);
    }
}

/**
 * @brief 客戶端服務執行緒
 * @param fd
 */
void clientWork(int fd){
    char buffer[1024];
    while(!stop){
        memset(buffer,0,sizeof(buffer));
        ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
        if(bytes_len==0){
            std::cout<<"client "<<fd<<" closed."<<std::endl;
            break;
        } else if(bytes_len<0){
            std::cerr<<"client "<<fd<<" recv error."<<std::endl;
            break;
        }else{
            std::string msg = std::to_string(fd)+":"+buffer;
            notify(msg);
        }
    }
    std::lock_guard<std::mutex> lk(mu);
    auto it = std::remove_if(clientFdVec.begin(), clientFdVec.end(),[&](int element){
        return element == fd;
    });
    clientFdVec.erase(it, clientFdVec.end());
}

int main() {

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(1234);

    // 建立socket
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    // 繫結socket和addr
    if(bind(fd, (struct sockaddr *) &addr, sizeof(addr))<0){
        std::cout << "bind failed!" << std::endl;
        return -1;
    }

    // 監聽指定地址埠
    while(0==listen(fd, 20)){
        //接收連線請求
        sockaddr_in clientAddr{};
        socklen_t clientAddrLen = sizeof(clientAddr);
        int clientFd = accept(fd, (struct sockaddr*)&clientAddr, &clientAddrLen);
        if(clientFd<0){
            std::cerr<<"accept error."<<std::endl;
            break;
        }

        //記錄這個客戶端
        {
            std::lock_guard<std::mutex> lk(mu);
            clientFdVec.emplace_back(clientFd);
        }

        //開啟一個執行緒服務這個客戶端
        clientWorkerVec.emplace_back(clientWork,clientFd);

        //給所有客戶端傳送訊息:welcome xxx!
        notify(std::string("welcome ")+std::to_string(clientFd));
    }

    stop = true;
    for(auto & t : clientWorkerVec){
        t.join();
    }
    close(fd);

    for(int cfd : clientFdVec){
        close(cfd);
    }
    return 0;
}

使用select解決執行緒建立過多的問題

select基本用法

詳細用法以及原理參考 關於Select Model的兩篇譯文

//#include <sys/select.h>
void FD_CLR(fd, fd_set *fdset);
void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy);
int FD_ISSET(fd, fd_set *fdset);
void FD_SET(fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
int select(int nfds, 
           fd_set *restrict readfds, 
           fd_set *restrict writefds, 
           fd_set *restrict errorfds,
           struct timeval *restrict timeout);

簡單示例

#include <sys/select.h>
#include <cstdio>
#include <unistd.h>


int main() {
    fd_set rd;
    struct timeval tv{};
    int ret;

    FD_ZERO(&rd); //所有位置歸零

    // #include <unistd.h>
    // 0:STDIN_FILENO 標準輸入
    // 1:STDOUT_FILENO 標準輸出
    // 2:STDERR_FILENO 標準錯誤輸出
    FD_SET(0, &rd); //標準輸入檔案描述符加入到rd集合中

    // 設定超時時間位5s
    tv.tv_sec = 3;
    tv.tv_usec = 0;

    // 將rd集合進行select,監聽其可讀事件
    // 程式在這裡阻塞,直到超時或者標準輸入上有資料可讀
    ret = select(1, &rd, nullptr, nullptr, &tv);

    if (ret == 0) // Timeout
    {
        printf("select timeout!\n");
    } else if (ret == -1) // Failure
    {
        printf("fail to select!\n");
    } else // Successful
    {
        printf("data is available!\n");
        char buffer[1024]{0};
        read(0,buffer,sizeof(buffer));
        printf("the data is [%s]!\n",buffer);
    }
    return 0;
}

服務端

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <list>
#include <thread>

std::atomic_bool stop = false;

int main() {

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(1234);

    fd_set readFdSet;
    fd_set tmpReadFdSet;
    FD_ZERO(&readFdSet);

    int srvFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (bind(srvFd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
        std::cout << "bind failed!" << std::endl;
        return -1;
    }
    listen(srvFd, 5);

    FD_SET(srvFd, &readFdSet);
    int ret;
    while (!stop) {
        tmpReadFdSet = readFdSet;
        ret = select(FD_SETSIZE, &tmpReadFdSet, nullptr, nullptr, nullptr);
        if (ret == 0) {
            std::cerr << "time out." << std::endl;
            break;
        } else if (ret < 0) {
            std::cerr << "select error." << std::endl;
            break;
        }
        for (int fd = 0; fd < FD_SETSIZE; fd++) {
            if (!FD_ISSET(fd, &tmpReadFdSet)) {
                continue;
            }
            if (fd == srvFd) //處理server
            {
                struct sockaddr_in client_address{};
                socklen_t client_len = sizeof(client_address);
                int cfd = accept(fd, (struct sockaddr *) &client_address, &client_len);
                FD_SET(cfd, &readFdSet);
                std::cout << "client[" << cfd << "]connected." << std::endl;
            } else //處理訊息
            {
                char buffer[1024];
                memset(buffer, 0, sizeof(buffer));
                ssize_t bytes_len = recv(fd, buffer, sizeof(buffer), 0);

                if (bytes_len == 0) {
                    std::cout << "client " << fd << " closed." << std::endl;
                    FD_CLR(fd, &readFdSet);

                    std::string msg = std::to_string(fd) + " leaved.";
                    for (int i = 1; i < FD_SETSIZE; i++) {
                        if (i != srvFd && FD_ISSET(i, &readFdSet)) {
                            send(i, msg.c_str(), msg.length(), 0);
                        }
                    }//end for

                    break;
                } else if (bytes_len < 0) {
                    std::cerr << "client " << fd << " recv error." << std::endl;
                    break;
                } else {
                    //send message to all
                    std::string msg = std::to_string(fd) + ":" + buffer;
                    for (int i = 1; i < FD_SETSIZE; i++) {
                        if (i != srvFd && FD_ISSET(i, &readFdSet)) {
                            send(i, msg.c_str(), msg.length(), 0);
                        }
                    }//end for
                }

            }
        }
    }
    return 0;
}

客戶端

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include "thread"

int main(){

    std::atomic_bool  stop = false;

    // 服務端地址
    struct sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(1234);

    //建立socket並連線到服務端
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))<0){
        std::cerr<<"connect failed."<<std::endl;
        return -1;
    }

    //連線建立後,建立一個執行緒讀取資料
    std::thread t([&](){
        thread_local char buffer[1024];
        while(!stop){
            memset(buffer,0,sizeof (buffer));
            ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
            if(bytes_len==0){
                std::cout<<"server closed."<<std::endl;
                break;
            }else if(bytes_len<0){
                std::cout<<"recv error. exit!"<<std::endl;
                break;
            }else{
                std::cout<<buffer<<std::endl;
            }
        }
        stop = true;
        exit(0);
    });

    //主執行緒中輸入併傳送
    while(!stop){
        char buffer[1024];
        std::cin>>buffer;
        if(stop||strcmp(buffer,"quit")==0){
            break;
        }
        ssize_t bytes_len = send(fd, buffer, sizeof(buffer),0);
        if(bytes_len<0)
        {
            std::cout<<"send error. exit!"<<std::endl;
            break;
        }
    }

    stop = true;
    t.join();
    close(fd);
    return 0;
}

相關文章