在Linux网络编程中,errno是一个非常重要的变量。它记录了最近发生的系统调用错误代码。在编写网络应用程序时,合理处理errno可以帮助我们更好地了解程序出现的问题并进行调试。
通常,在Linux网络编程中发生错误时,errno会被设置为一个非零值。因此,在进行系统调用之后,我们应该始终检查errno的值。我们可以使用perror函数将错误信息打印到标准错误输出中,或者使用strerror函数将错误代码转换为错误信息字符串。
在网络编程中,处理网络连接、连接收发数据等经常会涉及到errno的处理。经过查阅了很多资料,发现没有一个系统的讲解,在不同阶段会遇到哪些errno,以及对这些errno需要如何处理。因此,本文将分为三个部分来讲解。
1. 接受连接(accept)
这一阶段发生在 accept 接收 tcp 连接中。
在accept接收tcp连接的过程中,可能会遇到以下errno:
- EAGAIN或EWOULDBLOCK:表示当前没有连接可以接受,非阻塞模式下可以继续尝试接受连接
- ECONNABORTED:表示连接因为某种原因被终止,可以重新尝试接受连接
- EINTR:表示系统调用被中断,可以重新尝试接受连接
- EINVAL:表示套接字不支持接受连接操作,需要检查套接字是否正确
其中 EINTR、EAGAIN与EWOULDBLOCK,表示可能遇到了系统中断,需要对这些errno忽略,如果是其他错误,则需要执行错误回调或者直接处理错误。
在 libevent 为这些需要忽略的errno定义了宏 EVUTIL_ERR_ACCEPT_RETRIABLE,宏里定义了上面三个需要忽略的信号,在 accept 处理时会判断如果遇到这些信号则进行忽略,下次重试就好。
/* True iff e is an error that means a accept can be retried. */ #define EVUTIL_ERR_ACCEPT_RETRIABLE(e) \ ((e) == EINTR || EVUTIL_ERR_IS_EAGAIN(e) || (e) == ECONNABORTED) // libevent accept 处理代码 static void listener_read_cb(evutil_socket_t fd, short what, void *p) { struct evconnlistener *lev = p; int err; evconnlistener_cb cb; evconnlistener_errorcb errorcb; void *user_data; LOCK(lev); while (1) { struct sockaddr_storage ss; ev_socklen_t socklen = sizeof(ss); evutil_socket_t new_fd = evutil_accept4_(fd, (struct sockaddr*)&ss, &socklen, lev->accept4_flags); if (new_fd < 0) break; if (socklen == 0) { /* This can happen with some older linux kernels in * response to nmap. */ evutil_closesocket(new_fd); continue; } .......... } err = evutil_socket_geterror(fd); if (EVUTIL_ERR_ACCEPT_RETRIABLE(err)) { UNLOCK(lev); return; } if (lev->errorcb != NULL) { ++lev->refcnt; errorcb = lev->errorcb; user_data = lev->user_data; errorcb(lev, user_data); listener_decref_and_unlock(lev); } else { event_sock_warn(fd, "Error from accept() call"); UNLOCK(lev); } }
2. 建立连接(connect )
这一阶段发生在 connect 连接中。
在connect连接的过程中,可能会遇到以下errno:
- EINPROGRESS:表示连接正在进行中,需要等待连接完成
- EALREADY:表示套接字非阻塞模式下连接请求已经发送,但连接还未完成,需要等待连接完成
- EISCONN:表示套接字已经连接,无需再次连接
- EINTR:表示系统调用被中断,可以重新尝试连接
- ENETUNREACH:表示网络不可达,需要检查网络连接是否正常
其中 EINPROGRESS、EALREADY、EINTR 表示连接正在进行中,需要等待连接完成或重新尝试连接。如果是其他错误,则需要执行错误回调或者直接处理错误。
一般情况下,我们需要通过 select、poll、epoll 等 I/O 多路复用函数来等待连接完成,或者使用非阻塞的方式进行连接,等待连接完成后再进行下一步操作。
在 libevent 中,为这些需要忽略的 errno 定义了宏 EVUTIL_ERR_CONNECT_RETRIABLE,宏里定义了上面三个需要忽略的信号,在 connect 处理时会判断如果遇到这些信号则进行忽略,下次重试就好。
/* True iff e is an error that means a connect can be retried. */ #define EVUTIL_ERR_CONNECT_RETRIABLE(e) \\ ((e) == EINTR || (e) == EINPROGRESS || (e) == EALREADY) // libevent connect 处理代码 /* XXX we should use an enum here. */ /* 2 for connection refused, 1 for connected, 0 for not yet, -1 for error. */ int evutil_socket_connect_(evutil_socket_t *fd_ptr, const struct sockaddr *sa, int socklen) { int made_fd = 0; if (*fd_ptr < 0) { if ((*fd_ptr = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) goto err; made_fd = 1; if (evutil_make_socket_nonblocking(*fd_ptr) < 0) { goto err; } } if (connect(*fd_ptr, sa, socklen) < 0) { int e = evutil_socket_geterror(*fd_ptr); // 处理忽略的 errno if (EVUTIL_ERR_CONNECT_RETRIABLE(e)) return 0; if (EVUTIL_ERR_CONNECT_REFUSED(e)) return 2; goto err; } else { return 1; } err: if (made_fd) { evutil_closesocket(*fd_ptr); *fd_ptr = -1; } return -1; }
3. 连接的读写
在 Linux 网络编程中,连接读写阶段可能会遇到以下 errno:
- EINTR:表示系统调用被中断,可以重新尝试读写
- EAGAIN 或 EWOULDBLOCK:表示当前没有数据可读或没有缓冲区可写,需要等待下一次读写事件再尝试读写,非阻塞模式下可以继续尝试读写
- ECONNRESET 或 EPIPE:表示连接被重置或对端关闭了连接,需要重新建立连接
- ENOTCONN:表示连接未建立或已断开,需要重新建立连接
- ETIMEDOUT:表示连接超时,需要重新建立连接
- ECONNREFUSED:表示连接被拒绝,需要重新建立连接
- EINVAL:表示套接字不支持读写操作,需要检查套接字是否正确
其中 EINTR、EAGAIN 或 EWOULDBLOCK 表示可能遇到了系统中断或当前没有数据可读或没有缓冲区可写,需要对这些 errno 忽略,如果是其他错误,则需要执行错误回调或者直接处理错误。
在 libevent 中,为这些需要忽略的 errno 定义了宏 EVUTIL_ERR_RW_RETRIABLE,宏里定义了 EINTR、EAGAIN 或 EWOULDBLOCK 需要忽略的信号,在连接的读写处理时会判断如果遇到这些信号则进行忽略,下次重试就好。
/* True iff e is an error that means a read or write can be retried. */ #define EVUTIL_ERR_RW_RETRIABLE(e) \\ ((e) == EINTR || EVUTIL_ERR_IS_EAGAIN(e)) // 连接读写处理代码例子 static void bufferevent_readcb(evutil_socket_t fd, short event, void *arg) { struct bufferevent *bufev = arg; struct bufferevent_private *bufev_p = BEV_UPCAST(bufev); struct evbuffer *input; int res = 0; short what = BEV_EVENT_READING; ev_ssize_t howmuch = -1, readmax=-1; bufferevent_incref_and_lock_(bufev); if (event == EV_TIMEOUT) { /* Note that we only check for event==EV_TIMEOUT. If * event==EV_TIMEOUT|EV_READ, we can safely ignore the * timeout, since a read has occurred */ what |= BEV_EVENT_TIMEOUT; goto error; } input = bufev->input; /* * If we have a high watermark configured then we don't want to * read more data than would make us reach the watermark. */ if (bufev->wm_read.high != 0) { howmuch = bufev->wm_read.high - evbuffer_get_length(input); /* we somehow lowered the watermark, stop reading */ if (howmuch <= 0) { bufferevent_wm_suspend_read(bufev); goto done; } } readmax = bufferevent_get_read_max_(bufev_p); if (howmuch < 0 || howmuch > readmax) /* The use of -1 for "unlimited" * uglifies this code. XXXX */ howmuch = readmax; if (bufev_p->read_suspended) goto done; evbuffer_unfreeze(input, 0); res = evbuffer_read(input, fd, (int)howmuch); /* XXXX evbuffer_read would do better to take and return ev_ssize_t */ evbuffer_freeze(input, 0); if (res == -1) { int err = evutil_socket_geterror(fd); // 处理需要忽略的errno if (EVUTIL_ERR_RW_RETRIABLE(err)) goto reschedule; if (EVUTIL_ERR_CONNECT_REFUSED(err)) { bufev_p->connection_refused = 1; goto done; } /* error case */ what |= BEV_EVENT_ERROR; } else if (res == 0) { /* eof case */ what |= BEV_EVENT_EOF; } if (res <= 0) goto error; bufferevent_decrement_read_buckets_(bufev_p, res); /* Invoke the user callback - must always be called last */ bufferevent_trigger_nolock_(bufev, EV_READ, 0); goto done; reschedule: goto done; error: bufferevent_disable(bufev, EV_READ); bufferevent_run_eventcb_(bufev, what, 0); done: bufferevent_decref_and_unlock_(bufev); } static void bufferevent_writecb(evutil_socket_t fd, short event, void *arg) { struct bufferevent *bufev = arg; struct bufferevent_private *bufev_p = BEV_UPCAST(bufev); int res = 0; short what = BEV_EVENT_WRITING; int connected = 0; ev_ssize_t atmost = -1; bufferevent_incref_and_lock_(bufev); if (evbuffer_get_length(bufev->output)) { evbuffer_unfreeze(bufev->output, 1); res = evbuffer_write_atmost(bufev->output, fd, atmost); evbuffer_freeze(bufev->output, 1); if (res == -1) { int err = evutil_socket_geterror(fd); // 处理需要忽略的 errno if (EVUTIL_ERR_RW_RETRIABLE(err)) goto reschedule; what |= BEV_EVENT_ERROR; } else if (res == 0) { /* eof case XXXX Actually, a 0 on write doesn't indicate an EOF. An ECONNRESET might be more typical. */ what |= BEV_EVENT_EOF; } if (res <= 0) goto error; bufferevent_decrement_write_buckets_(bufev_p, res); } if (evbuffer_get_length(bufev->output) == 0) { event_del(&bufev->ev_write); } /* * Invoke the user callback if our buffer is drained or below the * low watermark. */ if (res || !connected) { bufferevent_trigger_nolock_(bufev, EV_WRITE, 0); } goto done; reschedule: if (evbuffer_get_length(bufev->output) == 0) { event_del(&bufev->ev_write); } goto done; error: bufferevent_disable(bufev, EV_WRITE); bufferevent_run_eventcb_(bufev, what, 0); done: bufferevent_decref_and_unlock_(bufev); }
4. 总结
本文介绍了在 Linux 网络编程中处理 errno 的方法。在接受连接、建立连接和连接读写阶段可能会遇到多种 errno,如 EINTR、EAGAIN、EWOULDBLOCK、ECONNRESET、EPIPE、ENOTCONN、ETIMEDOUT、ECONNREFUSED、EINVAL 等,需要对一些 errno 进行忽略,对于其他错误则需要执行错误回调或者直接处理错误。在 libevent 中,为这些需要忽略的 errno 定义了宏,如 EVUTIL_ERR_ACCEPT_RETRIABLE、EVUTIL_ERR_CONNECT_RETRIABLE、EVUTIL_ERR_RW_RETRIABLE 等,方便开发者处理这些 errno。