WebServer06

多路复用版本:

#include<iostream>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<vector>
#include<system_error>
#include<string>
#include<cstring>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<cerrno>
#include<unistd.h>
#include<memory>
​
constexpr int MAX_EVENTS=1024;
constexpr int PORT=8080;
constexpr int BUFFER_SIZE=4096;
constexpr int EPOLL_TIMEOUT=-1; //表示超时
​
//定义客户端结构体
struct ClientData {
    int client_fd;
    char ip[INET_ADDRSTRLEN];//IPv4地址
    uint16_t port;
    std::string read_buffer;//将读取的信息存入read_buffer
    std::string write_buffer;//将要发送的信息存入write_buffer
};
​
//打印客户端信息
void print_client_info(const ClientData* client,const std::string& title) {
    std::cout << "[" << title << "]"
              << "客户端IP:" << client->ip
              << ",客户端端口:" << client->port
              << ",客户端fd:" << client->client_fd
              << std::endl;
}
​
//将文件描述符设置为非阻塞模式
void set_no_block(int fd) {
    int status=fcntl(fd,F_GETFL,0);//获取fd的状态
    if(status == -1) {
        throw std::system_error(errno,std::generic_category(),"获取fd状态失败");
    }
    if(fcntl(fd,F_SETFL,status | O_NONBLOCK) == -1) {
        throw std::system_error(errno,std::generic_category(),"设置fd状态失败");
    }
}
​
//注册/修改事件
void add_or_modify(int epoll_fd,int fd,uint32_t events,ClientData* client) {
    struct epoll_event ev;
    memset(&ev,0,sizeof(ev));
    ev.events = events;
    ev.data.ptr = client;//绑定客户端数据,便于获取
​
    //先尝试修改事件,如果不存在再注册
    if(epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fd,&ev) == -1) {
        if(errno == ENOENT) {
            //ENOENT表示事件不存在,注册
            if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,fd,&ev) == -1) {
                throw std::system_error(errno,std::generic_category(),"注册事件失败");
            }
        } else {
            throw std::system_error(errno,std::generic_category(),"修改事件失败");
        }
    }
}
​
//删除事件
void remove(int epoll_fd,int fd) {
    if(fcntl(epoll_fd,EPOLL_CTL_DEL,fd,nullptr) == -1) {
        throw std::system_error(errno,std::generic_category(),"删除事件失败");
    }
    close(fd);
}
​
void handle_new_connection(int server_fd,int epoll_fd) {
    //server_fd的已完成连接队列有新连接
    struct sockaddr_in client_addr;
    socklen_t client_len=sizeof(client_addr);
    int client_fd=accept4(server_fd,(sockaddr*)(&client_addr),&client_len,SOCK_NONBLOCK);
    if(client_fd == -1) {
        std::cerr << "accept新连接失败" << std::strerror(errno) << std::endl;
        return;
    }
​
    auto client = std::make_unique<ClientData>();
    client->client_fd = client_fd;
    client->port=ntohs(client_addr.sin_port); 
    if(inet_ntop(AF_INET,&client_addr.sin_addr,client->ip,sizeof(client->ip)) == nullptr) {
        std::cerr << "IP转换失败" << std::strerror(errno) << std::endl;
        close(client_fd);
        return;
    }
​
    print_client_info(client.get(),"获得新客户端连接");
    add_or_modify(epoll_fd,client_fd,EPOLLIN | EPOLLET,client.release());
}
​
//处理客户端读事件
void handle_read_event(ClientData* client,int epoll_fd) {
    char raw_buf[BUFFER_SIZE];
    ssize_t read_bytes;
​
    //ET模式必须一次性读完
    while(true) {
        memset(raw_buf,0,sizeof(raw_buf));
        read_bytes=read(client->client_fd,raw_buf,BUFFER_SIZE-1);//非阻塞read
        if(read_bytes > 0) {
            //读取成功
            client->read_buffer.assign(raw_buf,read_bytes);
            std::cout << "收到客户端" << "[IP:" << client->ip << ",端口:" << client->port 
                      << "]的信息" << client->read_buffer << std::endl;
            //回声
            client->write_buffer=client->read_buffer;
            //注册写事件
            add_or_modify(epoll_fd,client->client_fd,EPOLLIN | EPOLLOUT | EPOLLET,client);
        } else if(read_bytes == 0) {
            //正常关闭
            print_client_info(client,"客户端正常关闭连接");
            remove(epoll_fd,client->client_fd);
            close(client->client_fd);//这一步是否必要?
            delete client;
            return;
        } else {
            //读取失败
            if(errno == EAGAIN || errno == EWOULDBLOCK) {
                //非阻塞read情况下数据已经读完
                break;//读完就退出循环
            } else {
                //客户端出现了异常断开
                std::cerr << "客户端连接异常断开" << std::strerror(errno) << std::endl;
                remove(epoll_fd,client->client_fd);
                close(client->client_fd);
                delete client;
                return;
            }
        }
    }
}
​
//处理客户端写事件
void handle_write_event(ClientData* client,int epoll_fd) {
    const char* data=client->write_buffer.data();
    size_t already_written=0;
    size_t data_len=client->write_buffer.size();
    ssize_t write_bytes=0;
​
    //循环发送,因为ET必须一次性写完数据
    while(already_written < data_len) {
        write_bytes=write(client->client_fd,data+already_written,data_len-already_written);
        if(write_bytes > 0) {
            already_written+=write_bytes;
        } else if(write_bytes == 0) {
            //写入0字节,无意义,退出循环
            break;
        } else {
            //写入失败
            if(errno == EAGAIN || errno == EWOULDBLOCK) {
                //数据暂时写不完,等到下次触发事件再写
                break;
            } else {
                std::cerr << "写入数据失败" << std::strerror(errno) << std::endl;
                remove(epoll_fd,client->client_fd);
                close(client->client_fd);
                delete client;
                return;
            }
        }
    }
​
    //数据全部写完,清空两个缓冲区,取消写事件
    if(already_written == data_len) {
        std::cout << "向客户端[IP:" << client->ip << ",端口:" << client->port << "]"
                  << "发送回声消息成功" << std::endl;
        client->read_buffer.clear();
        client->write_buffer.clear();
        //取消写事件,保留读事件
        add_or_modify(epoll_fd,client->client_fd,EPOLLIN | EPOLLET,client);
    }
}
​
//初始化服务器
int init_server() {
    //1.创建socket
    int server_fd=socket(AF_INET,SOCK_STREAM,0);
    if(server_fd < 0) {
        throw std::system_error(errno,std::generic_category(),"创建监听socket失败");
    }
​
    //2.绑定地址
    struct sockaddr_in server_addr{};
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr.s_addr=INADDR_ANY;//监听所有网卡
    server_addr.sin_port=htons(PORT);
    if(bind(server_fd,(sockaddr*)(&server_addr),sizeof(server_addr)) < 0) {
        throw std::system_error(errno,std::generic_category(),"绑定地址与端口失败");
    }
​
    //3.监听
    if(listen(server_fd,128) == -1) {
        throw std::system_error(errno,std::generic_category(),"监听失败");
    }
​
    //4.设置为非阻塞
    set_no_block(server_fd);
​
    std::cout << "服务器初始化成功,监听端口" << PORT << std::endl;
    return server_fd;
}
​
int main() {
    try {
        //1.初始化服务器
        int server_fd=init_server();
        
        //2.创建epoll实例
        int epoll_fd=epoll_create1(0);
        if(epoll_fd < 0) {
            throw std::system_error(errno,std::generic_category(),"创建epoll实例失败");
        }
​
        //3.注册server_fd
        auto server_data=std::make_unique<ClientData>();
        server_data->client_fd=server_fd;
        add_or_modify(epoll_fd,server_fd,EPOLLIN | EPOLLET,server_data.release());
​
        //4.循环等待epoll事件
        struct epoll_event events[MAX_EVENTS];//存储就绪事件
        while(true) {
            int ready_events_cnt=epoll_wait(epoll_fd,events,MAX_EVENTS,EPOLL_TIMEOUT);
            if(ready_events_cnt == -1) {
                if(errno == EINTR) {
                    //信号被中断,继续循环
                    continue;
                } else {
                    throw std::system_error(errno,std::generic_category(),"epoll_wait失败");
                }
            }
​
            //遍历所有就绪事件
            for(size_t i=0;i<ready_events_cnt;i++) {
                ClientData* data=static_cast<ClientData*>(events[i].data.ptr);
                int fd=data->client_fd;
​
                //1.如果是监听socket的事件
                if(fd == server_fd) {
                    //说明监听socket关注的事件(是否有新连接)有了结果
                    handle_new_connection(server_fd,epoll_fd);
                } else {
                    if(events[i].events && EPOLLIN) {
                        //如果关注读事件
                        handle_read_event(data,epoll_fd);
                    } else if(events[i].events && EPOLLOUT) {
                        //如果关注写事件
                        handle_write_event(data,epoll_fd);
                    }
                }
            }
        }
        
        close(server_fd);
        close(epoll_fd);
    } catch(const std::exception& e) {
        std::cerr << "服务器异常退出" << e.what() <<std::endl;
        return 1;
    }
    return 0;
}

从main函数中我们可以捋清整体思路:

1.初始化服务器(创建socket、绑定地址与端口、开始监听)

2.创建epoll实例并注册server_fd,表示我们关注server_fd(我们关心它是否accept成功)

3.创建events数组,用来存放已就绪的事件

4.调用epoll_wait,等待内核将已就绪的事件拷贝到events数组中

5.遍历events数组:如果已就绪事件的fd恰为server_fd,意味着有新连接;如果不是,就看该事件关心的是读事件还是写事件,分别进行处理即可

一开始我们只注册了server_fd,刚开始events中也只有server_fd(如果有新连接),之后才会调用handle_new_connection,注册新的文件描述符。

几个点:

1.​fcntl

fcntl(file control)是Linux系统调用,专门用于修改或查询文件描述符的属性

fcntl(fd,F_GETFL,0);

cmd=F_GETFL,全称Get File Status Flags,作用是读取当前fd的状态标志。第三个参数为0,因为F_GETFL不需要额外参数。返回值是一个整数,每个比特位代表一个状态标志,例如O_NONBLOCK是非阻塞标志

fcntl(fd,F_SETFL,flags | O_NONBLOCK)

显然,这是在设置fd的状态,第三个参数表示新增O_NONBLOCK标志。之所以要用按位或而不是直接传入O_NONBLOCK,是因为这样可能会覆盖原有的标志,导致fd丢失原有属性。这也是为什么我们要先获取fd的状态再设置为非阻塞

2.epoll_event

它是epoll事件的载体,它告诉内核要监听哪个fd的什么事件,以及事件触发时返回什么数据。我们可以通过查看epoll_ctl进而查看epoll_event的字段:

man epoll_ctl

可以看到:

       The event argument describes the object linked to the  file  descriptor
       fd.  The struct epoll_event is defined as:
​
           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;
​
           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };
​

add_or_modify函数中,我们先对它进行了初始化,即调用了memset函数,避免结构体中未定义的垃圾值导致监听错误事件

3.add_or_modify函数

先尝试修改,如果发现不存在就再注册。这样比较省事。因为同一个fd可能需要多次更新监听事件,比如读事件触发之后要添加写事件来发送回声数据,如果每次都先判断是否注册就比较麻烦

4.epoll_ctl

它的作用是修改epoll实例中的事件列表,原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfdepoll实例的文件描述符

op:操作类型:EPOLL_CTL_ADD为添加,EPOLL_CTL_MOD为修改,EPOLL_CTL_DEL为删除

fd:要操作的客户端/服务器fd

event:要注册/更新的事件结构体

也就是说,当我们调用epoll_ctl时,相当于告诉内核:我要监听这个fdEPOLLIN(读)+EPOLLET(边缘触发)事件,并且把这个ClientData数据和它绑定。后续调用epoll_wait时,内核就会把就绪的事件填充到events数组中,相当于告诉我们:fd=xxx触发了EPOLLIN事件,这是你之前绑定的ClientData数据

5.accept4accept

accept4accept的加强版,accept4可以在接收新连接时直接设置新客户端fd的属性,避免额外调用fcntl,减少系统调用开销。

两者的函数原型:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
​
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

新增的这个flags参数允许我们在接受新连接的同时设置新fd的属性,无需后续调用fcntl

// accept(两步)
int client_fd = accept(listen_fd, &addr, &addrlen);
fcntl(client_fd, F_SETFL, O_NONBLOCK); //额外调用 fcntl
​
// accept4(一步)
int client_fd = accept4(listen_fd, &addr, &addrlen, SOCK_NONBLOCK);

在高并发场景下,如果用accept,每个连接需要2次系统调用,而accept4只需要1次,相当于把系统调用开销减半。而且accept4是原子操作,可以规避极端条件下的竞态条件。

6.inet_ntoainet_ntop

二者都能将IPv4地址从二进制格式转换为字符串格式

#include <arpa/inet.h>  //必需头文件
​
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

可以看到inet_ntopdst参数是用户提供的缓冲区。这也意味着,inet_ntoa是线程不安全的,因为它使用的是静态缓冲区,多线程调用会覆盖结果。而inet_ntop使用的是用户提供的缓冲区,不会相互覆盖,不过相对复杂一点。

7.release()、reset()、get()、move()

add_or_modify(epoll_fd,client_fd,EPOLLIN | EPOLLET,client.release())

std::unique_ptr是独占式智能指针,即同一时间只能有一个unique_ptr管理同一个对象,所有权不能复制,只能移动。它的核心作用是自动管理内存:当unique_ptr超出作用域,如函数返回、被销毁等,会自动调用delete销毁所管理的对象 。

而在我们的代码中,client管理的ClientData对象需要生命周期独立于unique_ptr,因为客户端连接存活期间ClientData必须存在,所以不能让unique_ptr自动销毁对象。这就需要release()

release()做了两件事:返回unique_ptr管理对象的裸指针、让uniuqe_ptr本身失去对对象的所有权。也就是说在调用release()之后,client会变成空指针,不再管理任何对象。这个裸指针被传递给add_or_modify函数,在函数中绑定到epoll_eventdata.ptr,直到客户端断开后,我们才手动delete该裸指针。

❓如果不用release(),直接传递client,会如何?

答:一方面,unique_ptr不可复制,add_or_modify的参数是ClientData*,是裸指针,无法直接传递unique_ptr;另一方面,若强行传递&(*client)unique_ptr仍然拥有所有权,超出作用域时会自动delete对象,导致后续data.ptr变成野指针,访问崩溃

❓如果使用std::move()会怎样?

答:std::move()转移所有权,对象被新的智能指针管理。而参数需要的是裸指针,且对象还是自动管理的,局部智能指针超出作用域后还是会自动delete对象。

比喻:你用一个盒子装了一瓶红酒,这个红酒就是对象,盒子就是unique_ptr,用来保护这个对象。release()将智能指针转为裸指针,相当于把这个盒子拆掉,整瓶红酒裸露在外面。刚好我们的add_or_modify就是要求拆掉包装,只留红酒。拆掉盒子后,盒子变为空,后面会自动销毁。而std::move是将这个红酒从一个盒子拿出来,再放到另一个盒子里,再传递给add_or_modifyadd_or_modify不收,就算强行让它收也不行。因为还是在用智能指针管理,函数结束之后盒子+红酒都会被销毁。

使用release()避免了智能指针自动销毁导致野指针的问题,后续自己手动delete即可

总结一下:

release():放弃所有权,返回裸指针,不销毁对象

reset():放弃所有权,且销毁对象,unique_ptr变为空

std::move():将unique_ptr的所有权移动给另一个unique_ptr,原unique_ptr变为空,对象仍被管理,不产生裸指针

get():不放弃所有权,返回裸指针。

可见,release()是永久交出对象控制权,让对象脱离智能指针管理。而get()用于临时访问对象,不改变所有权

进行简单的压测:

for i in {1..100}; do (while true; do echo "test"; sleep 0.001; done) | nc 127.0.0.1 8080 > /dev/null 2>&1 & done; sleep 30; pkill -f "nc 127.0.0.1 8080" && echo "压测结束!"

解释一下这个语句:

1.for i in {1..100}; do ... done -> 循环创建100个并发连接

重复执行大括号里的内容100次,相当于你手动打开了100个终端,每个终端都执行一次连接服务器+发数据的操作。

2.(while true; do echo "test"; sleep 0.001; done)

小括号:让里面的命令在后台子进程中运行,即这100个连接互不干扰

echo "test":发送字符串"test"给服务器。即模拟客户端发数据,你的服务器会回声返回

sleep 0.001:每次发完数据,暂停1毫秒,避免发得太快把服务器冲垮

3.| nc 127.0.0.1 8080 -> 把数据通过TCP发给服务器

|(管道符):把前面echo "test"输出的内容传给后面的nc命令

nc 127.0.0.1 8080:建立TCP连接到127.0.0.1:8080,也就是我们的服务器地址。并将前面传来的"test"字符串发给服务器,接收服务器的回声响应

4.> /dev/null 2>&1 -> 屏蔽冗余输出

> /dev/null:让服务器返回的回声数据不显示在终端上

2>&1:把nc命令的错误信息,比如连接断开的提示也一起屏蔽,让终端只显示关键信息

5.&+sleep 30+pkill ...

每个循环末尾的&:让这个连接在后台运行,不阻塞下一个连接的创建,这使得100个连接瞬间同时建立,而不是排队

sleep 30:所有连接创建完后,等待30秒,即压测时长为30秒

pkill -f "nc 127.0.0.1 8080":30秒后,强制关闭所有和服务器连接的nc进程

&& echo "压测结束!" :清理完后,终端提示“压测结束”

在进行压测之前,我们可以在终端输入top并回车查看CPU使用率。根据CPU使用率可以提高并发数量。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值