Есть ли способ обнаружить, что TCP-сокет был закрыт удаленным одноранговым узлом, не читая его?

Во-первых, немного фона для объяснения мотивации: я работаю над очень простым TCP-зеркальным зеркальным фильтром select (), который позволяет двум брандмауэр-клиентам косвенно говорить друг с другом. Оба клиента подключаются к этому серверу, и как только оба клиента подключены, любые TCP-байты, отправленные на сервер клиентом A, перенаправляются клиенту B и наоборот.

Это более или менее работает с одним небольшим успехом: если клиент A подключается к серверу и начинает отправлять данные до того, как клиент B подключился, серверу негде разместить данные. Я не хочу его буферизировать в ОЗУ, так как это может привести к использованию большого количества ОЗУ; и я не хочу просто отбрасывать данные, так как это может понадобиться клиенту B. Поэтому я перехожу к третьему варианту, который не должен выбирать () – для чтения готово к сокету клиента A, пока клиент B также не подключится. Таким образом, клиент A блокируется, пока все не будет готово к работе.

Это более или менее работает, но побочный эффект от выбора-для-чтения-готовности на соке клиента А заключается в том, что если клиент А решает закрыть свое TCP-соединение с сервером, сервер не получает уведомления об этом факте – – по крайней мере, до тех пор, пока не появится клиент B, и сервер, наконец, выберет для чтения для сокета клиента A, прочитает все ожидающие данные и затем получит уведомление с закрытым сокетом (то есть recv () возвращает 0).

Я бы предпочел, чтобы у сервера был некоторый способ узнать (своевременно), когда клиент A закрыл свое TCP-соединение. Есть ли способ узнать это? Опрос был бы приемлемым в этом случае (например, я мог бы выбрать () просыпаться один раз в минуту и ​​вызывать IsSocketStillConnected (носок) во всех сокетах, если такая функция существует).

    Если вы хотите проверить, действительно ли сокет фактически закрыт вместо данных, вы можете добавить флаг MSG_PEEK в recv() чтобы увидеть, были ли данные получены, или если вы получили 0 или ошибку.

     /* handle readable on A */ if (B_is_not_connected) { char c; ssize_t x = recv(A_sock, &c, 1, MSG_PEEK); if (x > 0) { /* ...have data, leave it in socket buffer until B connects */ } else if (x == 0) { /* ...handle FIN from A */ } else { /* ...handle errors */ } } 

    Даже если A закрывается после отправки некоторых данных, ваш прокси-сервер, вероятно, хочет перенести эти данные в B сначала перед пересылкой FIN в B, поэтому нет смысла знать, что A отправил FIN на соединение раньше, чем после того, как прочитал все данные он отправил.

    Соединение TCP не считается закрытым до тех пор, пока обе стороны не отправят FIN. Однако, если A принудительно выключит свою конечную точку, вы не узнаете об этом до тех пор, пока не попытаетесь отправить данные на него и не получите EPIPE (при условии, что вы подавили SIGPIPE ).


    Прочитав ваше зеркальное прокси-приложение немного больше, поскольку это приложение обхода брандмауэра, кажется, что вам действительно нужен небольшой протокол управления, чтобы вы могли убедиться, что этим сверстникам действительно разрешено разговаривать друг с другом. Если у вас есть протокол управления, у вас есть много решений, доступных вам, но тот, который я бы защищал, состоял бы в том, чтобы одно из соединений описывало себя как сервер, а другое соединение описывало себя как клиент. Затем вы можете сбросить соединение с клиентом, если нет присутствия сервера для его соединения. Вы можете позволить серверам дождаться соединения с клиентом до некоторого таймаута. Сервер не должен запускать какие-либо данные, и если это происходит без подключенного клиента, вы можете сбросить соединение с сервером. Это устраняет проблему буферизации данных для мертвого соединения.

    По-моему, ответ на мой вопрос: «нет, если вы не желаете и не можете изменить свой стек TCP, чтобы получить доступ к необходимой частной информации о состоянии сокета».

    Поскольку я не могу этого сделать, моим решением было перепроектировать прокси-сервер, чтобы всегда считывать данные со всех клиентов и удалять любые данные, поступающие от клиента, чей партнер еще не подключен. Это неоптимально, так как это означает, что streamи TCP, проходящие через прокси-сервер, больше не имеют streamового свойства надежной доставки в порядке, которое ожидают программы, использующие TCP-протокол, но этого будет достаточно для моей цели.

    Я не вижу проблемы, как вы ее видите. Предположим, что A подключается к серверу, отправляет некоторые данные и закрывается, он не нуждается в каком-либо сообщении. Сервер не будет считывать свои данные до тех пор, пока B не подключится, как только сервер прочитает сокет A и отправит данные на B. Первое чтение вернет отправленные данные A, а второе возвращает либо 0, либо -1 в том случае, когда сокет закрывается, сервер закрывается B. Предположим, что A отправляет большой кусок данных, метод send () A будет блокироваться до тех пор, пока сервер не начнет считывать и не будет потреблять буфер.

    Я бы использовал функцию с select, которая возвращает 0, 1, 2, 11, 22 или -1, где;

    • 0 = Нет данных в любом сокете (время ожидания)
    • 1 = A имеет данные для чтения
    • 2 = B имеет данные для чтения
    • 11 = Сокет имеет ошибку (отключен)
    • 22 = гнездо B имеет ошибку (отключен)
    • -1: одно / оба гнезда / недействительны

       int WhichSocket(int sd1, int sd2, int seconds, int microsecs) { fd_set sfds, efds; struct timeval timeout={0, 0}; int bigger; int ret; FD_ZERO(&sfds); FD_SET(sd1, &sfds); FD_SET(sd2, &sfds); FD_SET(sd1, &efds); FD_SET(sd2, &efds); timeout.tv_sec=seconds; timeout.tv_usec=microsecs; if (sd1 > sd2) bigger=sd1; else bigger=sd2; // bigger is necessary to be Berkeley compatible, Microsoft ignore this param. ret = select(bigger+1, &sfds, NULL, &efds, &timeout); if (ret > 0) { if (FD_ISSET(sd1, &sfds)) return(1); // sd1 has data if (FD_ISSET(sd2, &sfds)) return(2); // sd2 has data if (FD_ISSET(sd1, &efds)) return(11); // sd1 has an error if (FD_ISSET(sd2, &efds)) return(22); // sd2 has an error } else if (ret < 0) return -1; // one of the socket is not valid return(0); // timeout } 

    Для меня решение состояло в опросе статуса сокета.

    В Windows 10 следующий код, казалось, работал (но эквивалентные реализации, похоже, существуют для других систем):

     WSAPOLLFD polledSocket; polledSocket.fd = socketItf; polledSocket.events = POLLRDNORM | POLLWRNORM; if (WSAPoll(&polledSocket, 1, 0) > 0) { if (polledSocket.revents &= (POLLERR | POLLHUP)) { // socket closed return FALSE; } } 

    Если ваш прокси-сервер должен быть прокси-сервером общего назначения для любого протокола, вам также следует обращаться с теми клиентами, которые отправляют данные и сразу же звонят после отправки (только в одной передаче данных).

    Поэтому, если клиент A отправляет данные и закрывает соединение до того, как соединение будет открыто в B, не волнуйтесь, просто переместите данные в B в обычном режиме (при подключении к B).

    Для этого сценария нет необходимости реализовывать специальную обработку.

    Ваш прокси обнаружит закрытое соединение, если:

    • read возвращает ноль после открытия соединения с B и считывания всех ожидающих данных из A. или же
    • ваши программы пытаются отправить данные (от B) до A.

    Вы можете проверить, все еще подключен сокет, пытаясь записать в дескриптор файла для каждого сокета. Тогда, если возвращаемое значение записи равно -1 или errno = EPIPE, вы знаете, что сокет был закрыт.
    например

     int isSockStillConnected(int *fileDescriptors, int numFDs){ int i,n; for (i=0;i