epoll socket编程中的错误处理
epoll_wait返回状态中的错误处理
epoll可能返回的状态
根据文档,epoll_wait将在epoll_event结构体中返回如下事件
- EPOLLIN:可读
- EPOLLOUT:可写
- EPOLLRDHUP:对端关闭连接或对端关闭写半端
- EPOLLPRI:意料之外的事件,包括
- 最常见:带外数据到达
- A pseudoterminal master in packet mode has seen a state change on the slave
- A cgroup.events file has been modified
- EPOLLERR:发生错误
- EPOLLHUP:连接关闭
- 其它错误
muduo网络库的处理方式
核心代码为(经过删改):
if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
{
// 没有数据可以读,且连接已关闭
if (closeCallback_) closeCallback_();
}
// 出现错误
if (revents_ & (POLLERR | POLLNVAL))
{
if (errorCallback_) errorCallback_();
}
// 可读、或者连接已经半关闭。调用读函数,如果读到了EOF,则对端已关闭写半端;调用写函数,则收到SIGPIP信号或者收EPIPE错误,则连接已完全关闭
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
{
if (readCallback_) readCallback_(receiveTime);
}
// 可写,调用可写回调
if (revents_ & POLLOUT)
{
if (writeCallback_) writeCallback_();
}
libevent的处理方式
// 遇到错误或者连接断开,则标记为可读可写,这样,会在read遇到EOF,会在write遇到EPIPE,进行错误处理
if (what & EPOLLERR) {
ev = EV_READ | EV_WRITE;
// 遇到连接已经完全关闭,相同处理
} else if ((what & EPOLLHUP) && !(what & EPOLLRDHUP)) {
ev = EV_READ | EV_WRITE;
} else {
if (what & EPOLLIN)
ev |= EV_READ;
if (what & EPOLLOUT)
ev |= EV_WRITE;
// libevent的解释为:there is now an EV_CLOSED flag that programs can use to detect
// when a socket has closed without having to read all the bytes until receiving an EOF
// 即,libevent选择在read或者write时处理EPOLLHUP,但是如果出现了EPOLLRDHUP,其实不用再读完所有数据就可以直到连接已经半关闭
if (what & EPOLLRDHUP)
ev |= EV_CLOSED;
}
一个实验及建议的处理方式(Ubuntu18.04)
服务器
服务器监听端口,然后用epoll监听客户端的fd,将相应的可用事件打印
#define MAX 80
#define PORT 8080
#define SA struct sockaddr
int main()
{
int sockfd, connfd;
struct sockaddr_in servaddr{}, cli{};
signal(SIGPIPE, SIG_IGN);
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
// Binding newly created socket to given IP and verification
if ((bind(sockfd, (SA*)&servaddr, sizeof(servaddr))) != 0) {
printf("socket bind failed...\n");
exit(0);
}
else
printf("Socket successfully binded..\n");
// Now server is ready to listen and verification
if ((listen(sockfd, 5)) != 0) {
printf("Listen failed...\n");
exit(0);
}
else
printf("Server listening..\n");
socklen_t len = sizeof(cli);
int epoll_fd = epoll_create1(0);
std::thread t([&](){
while(1){
constexpr size_t MAX_EVENTS = 1024;
struct epoll_event events[MAX_EVENTS]{};
auto event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
for (int i=0; i<event_count; i++){
cout << "epoll ret for fd " << events[i].data.fd << " { ";
auto e = events[i].events;
auto fd = events[i].data.fd;
if (e & EPOLLIN) cout << "EPOLLIN, ";
if (e & EPOLLOUT) cout << "EPOLLOUT, ";
if (e & EPOLLRDHUP) cout << "EPOLLRDHUP, ";
if (e & EPOLLPRI) cout << "EPOLLPRI, ";
if (e & EPOLLERR) cout << "EPOLLERR, ";
if (e & EPOLLHUP) cout << "EPOLLHUP, ";
cout << " } ";
char buffer[1024];
auto nread = read(fd, buffer, 1024); // will block. please set it nonblock
cout << "read: " << nread << "\t\t";
if ((e & EPOLLHUP) || (e & EPOLLRDHUP)){
auto nwrite = write(fd, buffer, 1);
cout << "write " << nwrite << "\t\t";
if (nread == 0){ // EOF, half closed or closed
}
if (nwrite == -1){ // closed
close(fd);
}
}
cout << "\n";
}
}
});
while(1) {
connfd = accept(sockfd, (SA *) &cli, &len);
if (connfd < 0) {
printf("server accept failed...\n");
exit(0);
} else
printf("server accept the client...\n");
struct epoll_event event{};
event.events = EPOLLIN | EPOLLRDHUP | EPOLLPRI | EPOLLERR | EPOLLHUP;
event.data.fd = connfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connfd, &event);
}
}
客户端
客户端我们可以做4种尝试,分别是:关闭写半端、关闭读半端、关闭连接或者让程序自行停止,操作系统帮我们关闭连接
#define MAX 80
#define PORT 8080
#define SA struct sockaddr
int main()
{
int sockfd, connfd;
struct sockaddr_in servaddr, cli;
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("socket creation failed...\n");
exit(0);
}
else
printf("Socket successfully created..\n");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(PORT);
// connect the client socket to server socket
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr))
!= 0) {
printf("connection with the server failed...\n");
exit(0);
}
else
printf("connected to the server..\n");
char buf[1024] = {"test"};
write(sockfd, buf, 4);
// shutdown(sockfd, SHUT_WR);
// shutdown(sockfd, SHUT_RD);
// close(sockfd);
sleep(5);
}
实验结果为:
客户端行为 | 服务端的事件 |
---|---|
shutdown(sockfd, SHUT_WR) | Epoll报告EPOLLRDHUP,服务端读到EOF,但是socket仍然可写 |
shutdown(sockfd, SHUT_RD) | Epoll没有报告EPOLLRDHUP或者EPOLLHUP,这也印证了,关闭读半端只是调用方自行丢弃了读到的数据,并未对连接产生影响 |
close(sockfd) | Epoll报告EPOLLRDHUP,服务端读到EOF,socket写返回EPIPE错误 |
程序自行终止 | 和调用close的行为一样 |
Epoll没有报告过EPOLLHUP。但根据其语义,出现EPOLLHUP、EPOLLERR,连接都应该关闭
综合上述结果,可以做这样处理。这里没有处理EPOLLPRI
auto what = event.events;
if ((what & EPOLLRDHUP) || (what & EPOLLHUP) || (what & EPOLLERR)){
auto nread = read(...);
auto readErrno = errno;
auto nwrite = write(...);
auto writeErrno = errno;
// 处理读取的数据
// ...
// 如果读半端被关闭,但是连接可写,则认为是半关闭
if (nread == 0 && !(nwrite == -1 && writeErrno == EPIPE)){
// 半关闭
}
else if (nread == 0 && nwrite == -1 && writeErrno == EPIPE){
// 连接已完全关闭
}
}
else if (what & EPOLLIN){ // 可读
read(...);
}
else if (what & EPOLLOUT){ // 可写
write(...);
}
参考资料
-
epoll_wait: https://man7.org/linux/man-pages/man2/epoll_wait.2.html
-
epoll_ctl: https://man7.org/linux/man-pages/man2/epoll_ctl.2.html
-
muduo network library
-
libevent