Есть ли способ обнаружить, что TCP-сокет был закрыт удаленным одноранговым узлом, не читая его?
Во-первых, немного фона для объяснения мотивации: я работаю над очень простым TCP-зеркальным зеркалом TCP на основе select(), который позволяет двум брандмауэрам-клиентам косвенно общаться друг с другом. Оба клиента подключаются к этому серверу, и как только оба клиента подключены, любые TCP-байты, отправленные на сервер клиентом A, перенаправляются клиенту B и наоборот.
Это более-менее работает с одним небольшим успехом: если клиент A подключается к серверу и начинает отправлять данные до того, как клиент B подключился, серверу негде разместить данные. Я не хочу его буферизировать в ОЗУ, так как это может привести к использованию большого количества ОЗУ; и я не хочу просто отбрасывать данные, так как это может понадобиться клиенту B. Поэтому я выбираю третий вариант, который не должен выбирать() - для чтения в клиентском сокете, пока клиент B также не подключится. Таким образом, клиент A блокируется, пока все не будет готово.
Это более или менее работает, но побочный эффект не для выбора-для-чтения-готовности на клиентском соке состоит в том, что если клиент A решает закрыть свое TCP-соединение с сервером, сервер не получает уведомления о этот факт - по крайней мере, только до тех пор, пока не появится клиент B, и сервер окончательно выберет для чтения в клиентском сокете, прочитает все ожидающие данные и затем получит уведомление с закрытым сокетом (то есть recv() возвращает 0).
Я бы предпочел, чтобы у сервера был некоторый способ узнать (своевременно), когда клиент А закрыл свое TCP-соединение. Есть ли способ узнать это? Опрос был бы приемлемым в этом случае (например, я мог бы выбрать() просыпаться один раз в минуту и вызывать IsSocketStillConnected (носок) во всех сокетах, если такая функция существует).
Ответы
Ответ 1
Похоже, что ответ на мой вопрос "нет", если только вы не желаете и не можете изменить свой стек TCP, чтобы получить доступ к необходимой частной информации о состоянии сокета ".
Поскольку я не могу этого сделать, моим решением было перепроектировать прокси-сервер, чтобы всегда считывать данные со всех клиентов и удалять любые данные, поступающие с клиента, чей партнер еще не подключен. Это неоптимально, так как это означает, что потоки TCP, проходящие через прокси-сервер, больше не обладают потокоподобным свойством надежной доставки в порядке доставки, которое ожидают программы, использующие TCP-протокол, но этого будет достаточно для моей цели.
Ответ 2
Если вы хотите проверить, действительно ли сокет фактически закрыт вместо данных, вы можете добавить флаг 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
).
После прочтения вашего зеркального прокси-приложения немного больше, поскольку это приложение обхода брандмауэра, кажется, что вам действительно нужен небольшой протокол управления, чтобы вы могли убедиться, что этим сверстникам действительно разрешено разговаривать друг с другом. Если у вас есть протокол управления, у вас есть много решений, доступных вам, но тот, который я бы защищал, состоял бы в том, чтобы одно из соединений описывало себя как сервер, а другое соединение описывало себя как клиент. Затем вы можете reset установить соединение с клиентом, если нет присутствия сервера для его соединения. Вы можете позволить серверам дождаться соединения с клиентом до некоторого таймаута. Сервер не должен запускать какие-либо данные, и если он работает без подключенного клиента, вы можете reset подключиться к серверу. Это устраняет проблему буферизации данных для мертвого соединения.
Ответ 3
Я не вижу проблемы, как вы ее видите. Пусть говорят, что A подключается к серверу, отправляет некоторые данные и закрывается, он не нуждается в каком-либо сообщении. Сервер не будет считывать свои данные до тех пор, пока B не подключится, как только сервер прочитает сокет A и отправит данные на B. Первое чтение вернет отправленные данные A, а второе возвращает либо 0, либо -1 в том случае, когда сокет закрыто, закрыть сервер B. Предположим, что A отправляет большой фрагмент данных, метод A send() блокируется до тех пор, пока сервер не начнет считывать и не будет потреблять буфер.
Я бы использовал функцию с 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
}
Ответ 4
Для меня решение состояло в опросе статуса сокета.
В 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;
}
}
Ответ 5
Если ваш прокси-сервер должен быть прокси-сервером общего назначения для любого протокола, тогда вы должны обращаться также с теми клиентами, которые отправляют данные и немедленно вызывают close
после отправки (только в одной передаче данных).
Итак, если клиент A отправляет данные и закрывает соединение до того, как соединение будет открыто на B, не беспокойтесь, просто переведите данные в B в обычном режиме (при открытии соединения с B).
Нет необходимости реализовывать специальную обработку для этого сценария.
Ваш прокси обнаруживает закрытое соединение, когда:
-
read
возвращает ноль после открытия соединения с B и считываются все ожидающие данные из A. или
- ваши программы пытаются отправить данные (от B) до A.
Ответ 6
Вы можете проверить, все еще подключен сокет, пытаясь записать в дескриптор файла для каждого сокета. Тогда, если возвращаемое значение записи равно -1 или если errno = EPIPE, вы знаете, что сокет был закрыт. Например,
int isSockStillConnected(int *fileDescriptors, int numFDs){
int i,n;
for (i=0;i<numFDs;i++){
n = write(fileDescriptors+i,"heartbeat",9);
if (n < 0) return -1;
if (errno == EPIPE) return -1;
}
//made it here, must be okay
return 0;
}