我目前正在测试我的网络代码。这涉及通过 IPv4 环回地址 (127.0.0.1) 建立连接。不幸的是,该程序经常(并非总是)在发送数据时给出 EPIPE 错误。
我正在使用伯克利网络套接字和libevent。我通过以下方式制作一个非阻塞套接字:
CBSocketReturn CBNewSocket(uint64_t * socketID,bool IPv6){
*socketID = socket(IPv6 ? PF_INET6 : PF_INET, SOCK_STREAM, 0);
if (*socketID == -1) {
if (errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT) {
return CB_SOCKET_NO_SUPPORT;
}
return CB_SOCKET_BAD;
}
// Stop SIGPIPE annoying us.
if (CB_NOSIGPIPE) {
int i = 1;
setsockopt(*socketID, SOL_SOCKET, SO_NOSIGPIPE, &i, sizeof(i));
}
// Make socket non-blocking
evutil_make_socket_nonblocking((evutil_socket_t)*socketID);
return CB_SOCKET_OK;
}
我通过以下方式制作连接事件:
bool CBSocketDidConnectEvent(uint64_t * eventID,uint64_t loopID,uint64_t socketID,void (*onDidConnect)(void *,void *),void * node){
CBEvent * event = malloc(sizeof(*event));
event->loop = (CBEventLoop *)loopID;
event->onEvent.ptr = onDidConnect;
event->node = node;
event->event = event_new(((CBEventLoop *)loopID)->base, (evutil_socket_t)socketID, EV_TIMEOUT|EV_WRITE, CBDidConnect, event);
if (NOT event->event) {
free(event);
event = 0;
}
*eventID = (uint64_t)event;
return event;
}
void CBDidConnect(evutil_socket_t socketID,short eventNum,void * arg){
CBEvent * event = arg;
if (eventNum & EV_TIMEOUT) {
// Timeout for the connection
event->loop->onTimeOut(event->loop->communicator,event->node,CB_TIMEOUT_CONNECT);
}else{
// Connection successful
event->onEvent.ptr(event->loop->communicator,event->node);
}
}
并通过以下方式添加它:
bool CBSocketAddEvent(uint64_t eventID,uint16_t timeout){
CBEvent * event = (CBEvent *)eventID;
int res;
if (timeout) {
struct timeval time = {timeout,0};
res = event_add(event->event, &time);
}else
res = event_add(event->event, NULL);
return NOT res;
}
要连接:
bool CBSocketConnect(uint64_t socketID,uint8_t * IP,bool IPv6,uint16_t port){
// Create sockaddr_in6 information for a IPv6 address
int res;
if (IPv6) {
struct sockaddr_in6 address;
memset(&address, 0, sizeof(address)); // Clear structure.
address.sin6_family = AF_INET6;
memcpy(&address.sin6_addr, IP, 16); // Move IP address into place.
address.sin6_port = htons(port); // Port number to network order
res = connect((evutil_socket_t)socketID, (struct sockaddr *)&address, sizeof(address));
}else{
struct sockaddr_in address;
memset(&address, 0, sizeof(address)); // Clear structure.
address.sin_family = AF_INET;
memcpy(&address.sin_addr, IP + 12, 4); // Move IP address into place. Last 4 bytes for IPv4.
address.sin_port = htons(port); // Port number to network order
res = connect((evutil_socket_t)socketID, (struct sockaddr *)&address, sizeof(address));
}
if (NOT res || errno == EINPROGRESS)
return true;
return false;
}
连接后,将创建 canSend 事件:
bool CBSocketCanSendEvent(uint64_t * eventID,uint64_t loopID,uint64_t socketID,void (*onCanSend)(void *,void *),void * node){
CBEvent * event = malloc(sizeof(*event));
event->loop = (CBEventLoop *)loopID;
event->onEvent.ptr = onCanSend;
event->node = node;
event->event = event_new(((CBEventLoop *)loopID)->base, (evutil_socket_t)socketID, EV_TIMEOUT|EV_WRITE|EV_PERSIST, CBCanSend, event);
if (NOT event->event) {
free(event);
event = 0;
}
*eventID = (uint64_t)event;
return event;
}
void CBCanSend(evutil_socket_t socketID,short eventNum,void * arg){
CBEvent * event = arg;
if (eventNum & EV_TIMEOUT) {
// Timeout when waiting to write.
event->loop->onTimeOut(event->loop->communicator,event->node,CB_TIMEOUT_SEND);
}else{
// Can send
event->onEvent.ptr(event->loop->communicator,event->node);
}
}
但是发送通常会给出 EPIPE 错误:
int32_t CBSocketSend(uint64_t socketID,uint8_t * data,uint32_t len){
ssize_t res = send((evutil_socket_t)socketID, data, len, CB_SEND_FLAGS);
printf("SENT (%li): ",res);
for (uint32_t x = 0; x < res; x++) {
printf("%c",data[x]);
}
printf("n");
if (res >= 0)
return (int32_t)res;
if (errno == EAGAIN)
return 0; // False event. Wait again.
return CB_SOCKET_FAILURE; // Failure
}
它降落在return CB_SOCKET_FAILURE;
上,errno设置为EPIPE。为什么会这样呢?如果设置了发送标志,则发送标志只是MSG_NOSIGNAL,因为SIGPIPE不断中断程序并出现此错误。我希望 EPIPE 使 CBSocketSend 返回CB_SOCKET_FAILURE而不是中断程序,但没有理由让 EPIPE 发送失败,那么它为什么要这样做呢?
上次我收到错误时,我注意到连接的线程仍在 connect() 调用中。使连接事件由与连接的线程不同的线程处理是否存在危险?
请参阅以下位置的网络代码:
https://github.com/MatthewLM/cbitcoin/blob/master/test/testCBNetworkCommunicator.chttps://github.com/MatthewLM/cbitcoin/tree/master/src/structures/CBObject/CBNetworkCommunicatorhttps://github.com/MatthewLM/cbitcoin/tree/master/dependencies/sockets
谢谢。
编辑:我再次运行它,并在connect()完成后出现错误。
编辑2:似乎在没有另一方接受的情况下给出了连接事件。
我不是 TCP/IP 专家,但我确实注意到本文档说即使设置了"面向流的套接字"MSG_NOSIGNAL EPIPE
仍然可以返回。看起来您正在使用SOCK_STREAM
创建套接字。另一端可能会断开连接。
在CBSocketConnect()
看起来,如果你得到EINPROGRESS
你只是返回true——如果它成功连接,你也会返回。您将无法知道是否需要等待连接完成。据此,您可以select()
或poll()
连接完成。
以上是我应@MatthewMitchell和@user315052的要求从OP发表的评论的转贴。
编辑:我正在添加对这个答案的更详细的描述,以及随后的一些讨论。
所以,首先尝试做connect()
。然后,如果错误结果EINPROGRESS
,请注册从 libevent
唤醒写入事件。进入 EV_WRITE
的回调函数后,检查与 getsockopt()
的连接状态 带套接字选项SO_ERROR
在第 SOL_SOCKET
级。如果返回的选项值为 0
,则连接成功。否则,请将其视为errno
数字。
按照本答案中所示的此建议进行操作后,您发现客户端遇到了错误ECONNREFUSED
。这就解释了为什么您的写入因EPIPE
而失败。调查服务器后,您发现服务器由于错误EADDRINUSE
而无法侦听绑定地址。这可以通过在侦听套接字上设置SO_REUSEADDR
选项来处理。
下面是一个简单的libevent
玩具程序,它合成了EINPROGRESS
,然后等待连接完成,等待EV_WRITE
。基本上,该程序表明,在您的应用程序中,您应该首先尝试执行connect
调用,如果它失败并EINPROGRESS
,您应该等待完成再执行 I/O。
这是libevent
回调函数:
extern "C" void on_connect (int sock, short ev, void *arg) {
assert(ev == EV_WRITE);
std::cout << "got wrieable on: " << sock << 'n';
int optval = -1;
socklen_t optlen = sizeof(optval);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &optval, &optlen);
assert(optval == 0);
std::cout << "succesful asynchronous connect on: " << sock << 'n';
event_loopbreak();
}
以下是玩具应用程序使用的一些辅助函数:
static void init_addr (struct sockaddr_in *addr, short port) {
memset(addr, ' ', sizeof(*addr));
addr->sin_family = AF_INET;
addr->sin_port = htons(port);
addr->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
}
static void setup_accept (int sock) {
const int one = 1;
struct sockaddr_in addr;
init_addr(&addr, 9876);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 1);
}
static int complete_accept (int sock) {
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
return accept(sock, (struct sockaddr *)&addr, &addrlen);
}
static int try_connect (int sock) {
struct sockaddr_in addr;
init_addr(&addr, 9876);
return connect(sock, (struct sockaddr *)&addr, sizeof(addr));
}
main
程序如下:
int main () {
int accept_sock = socket(PF_INET, SOCK_STREAM, 0);
setup_accept(accept_sock);
int sock = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK);
std::cout << "trying first connect on: " << sock << 'n';
int r = try_connect(sock);
assert(r < 0 && errno == EINPROGRESS);
event_init();
struct event ev_connect;
event_set(&ev_connect, sock, EV_WRITE, on_connect, 0);
event_add(&ev_connect, 0);
int new_sock = complete_accept(accept_sock);
event_dispatch();
return 0;
}
从您的进程唤醒以处理连接成功的那一刻起,直到它尝试写入套接字的那一刻,连接的状态仍然可以在操作系统的内核角度发生变化,libevent 无法预见它。
您描述的方案可以由以下阶段组成,因为您要连接的服务器的行为方式与我将要描述的方式相同。给定进程 A(客户端)和进程 B(连接的另一端):
- B 运行,绑定服务器套接字,等待。
- A 运行,
connect()
,等待 - B 唤醒,是否
accept()
- A 唤醒以处理连接的成功。
- B 关闭套接字(由于进程终止或显式
close()
)。 - A 尝试发送,得到
errno == EPIPE
.
这可以在环回上重现。
顺便说一句,SO_NOSIGPIPE
不是便携式插座选项。如果您正在编写可移植的 C 库,最好使用 signal()
和 SIG_IGN
忽略信号。