C++实现Linux并发服务器

前期准备

首先对linux提供的socket进行了一些简单的封装,使之更加符合面向对象的习惯:

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

#define SER_PORT (6789)

class Socket{
public:
    sockaddr_in addr;//地址
    int fd;//文件描述符

    //返回字符串 :"ip:端口号"
    std::string getAddr(){
        char* ip = inet_ntoa(this->addr.sin_addr);
        std::string port = ":" + std::to_string(ntohs(this->addr.sin_port));
        return ip + port;
    }
    ~ Socket(){
        close(this->fd);
    }
};

class ListenSocket:public Socket{//用于监听客户端连接的Socket
public:
    //创建、绑定、监听
    ListenSocket(uint32_t ip, int port, int listenNumber) : Socket(){
        //初始化文件描述符
        this->fd = socket(AF_INET, SOCK_STREAM, 0);
        //初始化地址
        this->addr.sin_family = AF_INET;
        this->addr.sin_addr.s_addr = htonl(ip);
        this->addr.sin_port = htons(port);
        //绑定
        bind(this->fd,(sockaddr*)&this->addr, sizeof(this->addr));
        //开始监听
        listen(this->fd,listenNumber);
        std::cout<<"正在监听"<<std::endl;
        //设置端口复用
        int opt = 1;
        setsockopt(this->fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }
};

class ConnectSocket:public Socket{//用于与每个客户端通信的socket
public:
    //构造函数:需要指定由哪个监听套接字 accept,构造时会进入停滞状态等待客户端连接
    //关于explicit的知识:https://blog.csdn.net/K346K346/article/details/82779248
    explicit ConnectSocket(ListenSocket* &pServer) : Socket(){
        socklen_t addr_len = sizeof(this->addr);
        this->fd = accept(pServer->fd, (sockaddr *)&this->addr, &addr_len);
    }
    //接收函数,返回客户端发来的消息, 没有消息时会停滞, 客户端退出时返回空字符串
    std::string receive(){
        while(true){
            char recvBuf[1024];
            int r = read(this->fd, recvBuf, sizeof(recvBuf));
            if(r == 0){
                return "";
            }
            return recvBuf;
        }
    }
    //发送函数
    void send(const std::string &message){
        write(this->fd, message.c_str(), message.length());
    }
};

能够使用socket进行通信后 ,可以采用以下五种方式处理客户端的并发访问:

  • 多进程
  • 多线程
  • 使用select
  • 使用poll
  • 使用epoll

一、多进程并发服务器

思路:主进程创建listen套接字,每当监听到一个客户端连接时, 创建一个子进程来处理与它的通信。

实现:

#include <iostream>
#include "mysocket.h"


int main() {

    auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, 128);

    while(1){       
        auto * pClient = new ConnectSocket(pServer);//接受客户端连接
        std::cout<<pClient->getAddr()<<std::endl;//输出其地址

        pid_t pid = fork();//创建子进程

        if(pid == 0){//子进程
            close(pServer->fd); //关闭监听socket
            while(1){//处理客户端发来的消息
                std::string recvMessage = pClient->receive();
                if(recvMessage  == ""){
                    std::cout << "连接中断" << std::endl;
                    break;
                }
                std::cout << recvMessage << std::endl;
                //向客户端发送一个简单的http回复报文
                std::string sendMessage = "HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>";
                pClient->send(sendMessage);
            }

            std::cout << "子进程返回" << std::endl;
            close(pClient->fd);
            return 0;
        }
        else if(pid > 0){//父进程
            continue;
        }
        else{
            std::cout << "创建进程出错" << std::endl;
        }
    }
}

多进程并发服务器的缺点:多个进程的创建和切换会耗费大量的系统资源。

二、多线程并发服务器

思路:和多进程服务器的思路基本一致, 只是改为采用线程来处理每个connectSocket, 减少了创建和切换的代价。

实现:

#include <iostream>
#include <pthread.h>
#include "mysocket.h"

void* task(void *arg){
    auto * pClient = (ConnectSocket *) arg;
    std::cout<<"客户端"<<pClient->getAddr()<<std::endl;

    //将子线程分离,这样一来,该线程运行结束后会自动释放所有资源。 防止产生僵尸进程
    pthread_detach(pthread_self());

    while (1){
        std::string recvMessage = pClient->receive();
        if(recvMessage.empty()){
            std::cout << "连接中断" << std::endl;
            break;
        }
        std::cout << recvMessage << std::endl;

        std::string sendMessage = "HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>";
        pClient->send(sendMessage);
    }

    close(pClient->fd);
    delete(pClient);

    return nullptr;
}

int main(){
    auto* pServer = new ListenSocket(INADDR_ANY, SER_PORT, 128);

    while(1){
        auto * pClient = new ConnectSocket(pServer);
        pthread_t tid;
        pthread_create(&tid, NULL, task, (void*) pClient);
    }
}

多进程并发服务器的缺点:虽然强于多进程, 但是在面对成千上万的访问时, 仍然力有不逮。

三、select 实现多路IO转接服务器

思路:将所有socket放入一个监听集合中,然后程序进入阻塞等待状态。 当监听到有socket发生io事件时, 操作系统标记这些socket并将程序唤醒。 然后程序轮询监听集合,找到有标记的socket并处理他们的io事件。

实现:

#include <iostream>
#include <sys/select.h>
#include <set>
#include "mysocket.h"
using namespace std;

int main(){
    auto* pServer = new ListenSocket(INADDR_ANY, SER_PORT, 1024);

    fd_set listeningSet,allSet; //声明监听集合以及全部的文件描述符集合
    FD_ZERO(&allSet);//清空文件描述符集合
    FD_SET(pServer->fd, &allSet);//将Listen套接字加入文件描述符集合
    int maxFd = pServer->fd;//监听的最新对象是Listen套接字

    set<ConnectSocket*> pClients;

    while(true){//逐帧监听
        listeningSet = allSet;//更新监听集合
        int todoNums = select(maxFd+1, &listeningSet, nullptr, nullptr, nullptr);
        if (todoNums < 0){
            cout << "select 错误" << endl;
            return -1;
        }
        if(FD_ISSET(pServer->fd, &listeningSet)){
            //如果listen套接字监听到事件, 说明有新的客户端发生了连接
            auto * pClient = new ConnectSocket(pServer);
            pClients.insert(pClient);
            cout << "客户端" << pClient->getAddr() << endl;

            //将新的客户端加入文件描述符集合
            maxFd = maxFd > pClient->fd ? maxFd : pClient->fd;
            FD_SET(pClient->fd, &allSet);

            --todoNums;//待处理事件减一
            if(!todoNums) continue;//待处理事件为0, 只发生了客户端连接事件, 没有io事件
        }

        //发生了io事件
        for(auto & pClient : pClients){//轮询连接套接字
            if(FD_ISSET(pClient->fd, &listeningSet)){//如果监听到事件
                string recvMessage = pClient->receive();
                if(recvMessage.empty()){//客户端退出
                    cout<<pClient->getAddr()<<"退出"<<endl;
                    close(pClient->fd);
                    delete(pClient);
                    pClients.erase(pClient);
                    FD_CLR(pClient->fd, &allSet);
                    continue;
                }
                cout << recvMessage << endl;
                pClient->send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>");

                --todoNums;//待处理事件减一
                if(!todoNums) break;
            }
        }
    }
}

select的缺点:监听上限受制于操作系统,只能达到1024个, 还是无法面对更高的并发访问

四、poll 实现多路IO转接服务器

思路:与select基本一致, 不同的是可以监听超过1024个套接字

由于监听集合中保存的是套接字的fd而不是我们自己定义的Socket类, 所以在接受和发送消息时最好直接用fd进行定位:

std::string Receive(int fd){
    char recvBuf[1024];
    int r = read(fd, recvBuf, sizeof(recvBuf));
    if(r == 0){
        return "";
    }
    return recvBuf;
}
void Send(std::string message, int fd){
    write(fd, message.c_str(), message.length());
}

实现:

//
// Created by haozl on 2019/11/2.
//

#include <iostream>
#include <poll.h>
#include "mysocket.h"
#define MAX_CLIENT_NUM 128

using namespace std;

class pollMonitor{
public:
    pollfd Set[MAX_CLIENT_NUM]{};
    int maxIndex;
    //构造函数, 初始化监听集合和它的最大有效下标
    pollMonitor(){
        maxIndex = 0;
        for(int i=0; i<MAX_CLIENT_NUM; i++){
            Set[i].fd = -1;
            Set[i].events = 0;
            Set[i].revents = 0;
        }
    }
    //将某个套接字加入监听集合
    bool add(int fd){
        for (int i=0; i<MAX_CLIENT_NUM; i++){
            if(Set[i].fd < 0){
                Set[i].fd = fd;
                Set[i].events = POLLIN;
                maxIndex = i > maxIndex ? i : maxIndex;
                return true;
            }
        }
        return false;
    }
    //将某个套接字从监听集合中删除
    void drop(int index){
        close(this->Set[index].fd);
        this->Set[index].fd = -1;
        this->Set[index].events = 0;
        this->Set[index].revents = 0;
    }
    //阻塞监听, 返回监听到的事件个数
    int wait(int timeout){
        return poll(this->Set, this->maxIndex+1, timeout);
    }
};

int main(){
    //实例化listen套接字和监视器, 将listen套接字加入监听
    auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, MAX_CLIENT_NUM);
    auto * monitor = new pollMonitor();
    monitor->add(pServer->fd);

    while(true){//逐帧监听
        int todoNum = monitor->wait(-1);
        if(todoNum < 0){
            cout<<"poll 错误"<<endl;
            return -1;
        }
        if(todoNum > 0){//如果监听到事件发生
            if(monitor->Set[0].revents == POLLIN){
                //如果是从listen套接字监听到事件的, 说明有客户端连接
                auto * pClient = new ConnectSocket(pServer);
                cout <<pClient->getAddr()<<endl;
                monitor->add(pClient->fd);//将新的connect套接字加入监听

                --todoNum;//未完成事件减一
                if (!todoNum) continue;//如果事件全部处理完成, 处理下一帧
            }
            //遍历监听集合中剩下的元素
            for(int i=1; i < monitor->maxIndex+1; i++){
                if(monitor->Set[i].fd < 0) continue;//如果不是有效的套接字 跳过
                if(monitor->Set[i].revents == POLLIN){
                    //有读入事件发生
                    string recvMessage = Receive(monitor->Set[i].fd);
                    if(recvMessage.empty()){
                        monitor->drop(i);//将该connect套接字移出监听
                        cout << "连接中断" << endl;
                        continue;
                    }
                    cout<<recvMessage<<endl;
                    Send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>", monitor->Set[i].fd);

                    --todoNum;//未完成事件减一
                    if (!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
                }
            }
        }
    }
}

poll和select的缺点:监听到事件后,无法直接定位到发生事件的socket,需要程序自己对监听集合进行轮询,在并发量过多的情况下仍然效率低下

五、epoll 实现多路IO转接服务器

思路:监听到事件后,直接定位到发生事件的socket,无需轮询

实现:

//
// Created by haozl on 2019/11/3.
//
#include <iostream>
#include <sys/epoll.h>
#include "mysocket.h"

#define MAX_CLIENT_NUM 128

using namespace std;

class ePollMonitor{
public:
    int epfd;
    epoll_event Set[MAX_CLIENT_NUM]{};
    //构造函数, 创建epoll句柄, 并初始化监听集合
    epollMonitor(){
        this->epfd = epoll_create(MAX_CLIENT_NUM);
        for(auto & item : this->Set){
            item.data.fd = -1;
        }
    }
    //将某个套接字加入监听集合
    void add(int fd){
        epoll_event tmp{};
        tmp.events = EPOLLIN;
        tmp.data.fd = fd;
        epoll_ctl(this->epfd, EPOLL_CTL_ADD, fd, &tmp);
    }
    //将某个套接字从监听集合中删除
    void drop(int index){
        close(this->Set[index].data.fd);
        epoll_ctl(this->epfd, EPOLL_CTL_DEL, this->Set[index].data.fd, nullptr);
    }
    //阻塞监听, 返回监听到的事件个数
    int wait(int timeout){
        return epoll_wait(this->epfd, this->Set, MAX_CLIENT_NUM, timeout);
    }
};

int main(){
    //实例化listen套接字和监视器, 将listen套接字加入监听
    auto * pServer = new ListenSocket(INADDR_ANY, SER_PORT, MAX_CLIENT_NUM);
    auto * monitor = new epollMonitor();
    monitor->add(pServer->fd);

    while(true){//逐帧监听
        int todoNum = monitor->wait(-1);
        if(todoNum < 0){
            cout<<"epoll 错误"<<endl;
            return -1;
        }
        if(todoNum > 0){//如果监听到事件发生
            for(int i=0; i<MAX_CLIENT_NUM; i++){//遍历发生事件的套接字
                if(monitor->Set[i].events != EPOLLIN)//如果不是读入事件 跳过
                    continue;

                if(monitor->Set[i].data.fd == pServer->fd){
                    //如果从listen套接字监听到事件, 说明有客户端连接
                    auto * pClient = new ConnectSocket(pServer);
                    cout <<pClient->getAddr()<<endl;
                    monitor->add(pClient->fd);//将新的connect套接字加入监听

                    todoNum--;//未完成事件减一
                    if(!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
                }
                else{//如果从其他套接字监听到事件, 说明有客户端发来请求
                    string recvMessage = Receive(monitor->Set[i].data.fd);
                    if(recvMessage.empty()){
                        monitor->drop(i);//将该connect套接字移出监听
                        cout << "连接中断" << endl;
                        continue;
                    }
                    cout<<recvMessage<<endl;
                    Send("HTTP/1.1 200 OK\r\n\r\n<h1>Response OK</h1>", monitor->Set[i].data.fd);

                    todoNum--;//未完成事件减一
                    if(!todoNum) break;//如果事件全部处理完成, 跳出处理下一帧
                }
            }
        }
    }
}

参考文章:
https://mp.weixin.qq.com/s/WO2GuaUCtvUFWIupgpWcbg
https://blog.csdn.net/weixin_40204595/article/details/83211064
https://blog.csdn.net/weixin_40204595/article/details/83212900

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值