WebeServer实现:学到了哪些东西
前言
这里话就是总结一下之前没讲过的一些东西
系统调用
- accept与accept4
当我们调用accept接收一个新的fd的时候,往往需要在调用fcntl将这个fd变成非阻塞IO,那么有没有一个系统调用可以一次性做完这两件事呢,有的有的就是accept4.
// accept 函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);// accept4 函数原型
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);int connfd = ::accept4(sockfd_, (sockaddr *)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
accept4 可以通过 flags 参数设置以下 socket 选项:
SOCK_NONBLOCK:将新 socket 设置为非阻塞模式。
SOCK_CLOEXEC:将新 socket 设置为在执行 exec 时自动关闭(避免子进程继承)
2. epoll_create与epoll_create1
epoll_create1 通过 flags 参数提供了额外的功能:
* EPOLL_CLOEXEC:设置文件描述符的 close-on-exec 标志,确保在执行 exec() 系统调用时自动关闭 epoll 实例。
* 0:如果 flags 为 0,则 epoll_create1 的行为与 epoll_create 相同
3. eventfd与epoll
eventfd本质上是一个计数器,对它的read/write操作都是原子的,当我们通过write向eventfd写入的时候,此时epoll会监听到此fd可读,至于可写嘛…只要没到最大值就是可写,监听的意义不大。
4. timefd与epoll
timefd在muduo库作为超时机制也使用的不少,当timefd对应的time超时的时候,也会触发epoll的可读事件,可以用来配合回调处理某些超时时间。
5. readv与writev
readv系统调用用于把数据读取到不同的位置,这在有些场景下还是很好用的:(1) 把报文头和报文内容分开,读取到不同的位置;(2)一次性把所有数据都读出来,避免调用多次read
// read 函数原型
ssize_t read(int fd, void *buf, size_t count);// readv 函数原型
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
- mmap与sendFile
sendFile避免了将数据的多次拷贝,直接就可以把数据发送出去
#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- IO多路复用与阻塞/非阻塞IO
先说我个人的看法:无论是水平触发还是边缘触发,都应该使用非阻塞IO,当然边缘触发必须使用非阻塞IO。
为什么呢?对于水平触发而言,epoll可读就意味着我们一定可以读出数据吗,答案是否定的,man手册对这块有解释:就算可读,数据在后期也可能被丢弃了,那如果是阻塞IO的话,这是非常危险的。
对于边缘触发呢,则要求我们一定得把所有数据一次性读出来,因为边缘触发一定是只有在状态发生变化的时候才会报道,那我们怎么一次性读出所有数据呢----当然是调用多次read啦,这也就意味着如果是阻塞IO,那就很难做到了因为读不到就阻塞了,而非阻塞IO就不同,利用非阻塞IO的返回值,就可以这么设计
/*水平触发和边缘触发都可以用这个逻辑*/
/*水平触发为了保证公平性,也可以只读一次*/
bool ET = true; /*表示边缘触发*/
do{ssize_t readLen = read()if(readLen < 0) {if(errno != EAGAIN) {/*处理一下*/}break;}
}while(ET)
- epoll的边缘触发与水平触发
对于水平触发,你可以每次只read一部分,然后如果没读完那么epoll还会触发可读的,但是对于边缘触发,必须要一次性把缓冲区内容都读出来,要不然它是不会再上报的。
至于边缘触发是不是一定比水平触发性能好,我个人还是觉得要视场景分析:
场景1:fd可读,但是读取的缓存区buf长度大于等于需要读取的长度。此时水平触发性能更好,因为我们可以一次性读完所有的数据,对于水平触发的代价就是一次epoll + 一次read即可。但是此时边缘触发呢,则是需要一次epoll + 两次read(第二次read返回负值break)
场景2:fd可读,但是读取的缓存区buf长度小于需要读取的长度。此时边缘触发性能更好。边缘触发一次epoll + 多次read,而水平触发需要多次epoll + 多次read调用。
场景3:公平性,水平触发很明显比边缘触发更公平,假设我们需要处理所有的活跃文件fd,如果有的fd需要读的内容过多,意味着边缘触发需要读取多次,这对其他文件描述符是不公平的。而水平触发嘛…看你怎么设计程序了,可以每个人都读一次保证公平,也可以使用上面伪代码介绍的方式(前提是非阻塞IO)
C++这一块
正好给我这个c with class的人开开眼(>.<),不过我感觉c++的回调虽然很强大,但是用的挺难受的,什么bind move啦,也许是我不适应吧
- 智能指针
unique_ptr与share_ptr与weak_ptr
unique_ptr没啥好讲的,主要是这里的share_ptr与weak_ptr和交叉引用问题,如果交叉引用则这两对象的析构函数将都不能调用。结合项目来说吧,这里的channel里用到了weak_ptr:
假设由于某些意外,需要我们关闭掉Clientfd对应的连接,此时可能尚有可读事件还没有处理,为了防止出问题,我们可以看看Clientfd对应的对象是否存在。
std::weak_ptr<void> tie_; /*观察对象是否存在*/bool tied_;// Channel本身是fd的封装 是绑定到某个连接了的
void Channel::handleEvent(Timestamp receiveTime)
{if(tied_) /*如果绑定了对象 weak_prt的作用就体现出来了 */{std::shared_ptr<void> guard = tie_.lock();if(guard){handleEventWithGuard(receiveTime);}/*如果对象都不存在了,就没必要处理对应事件了,要不还可能会报错,毕竟回调函数里可是要访问对象的资源的*/}else /*监听Socket的fd是不存在绑定的*/{handleEventWithGuard(receiveTime);}}
后期补充一下啦
3. unique_lock与lock_gurad
4. 虚函数
5. move
7. bind函数与函数指针相比,好在哪里
8. 右值引用与forward