Ответ 1
Вот что говорит справочная страница Linux о write
и EPIPE
:
EPIPE fd is connected to a pipe or socket whose reading end is closed.
When this happens the writing process will also receive a SIG-
PIPE signal. (Thus, the write return value is seen only if the
program catches, blocks or ignores this signal.)
Когда Linux использует pipe
или socketpair
, он может и будет проверять конец чтения пары, поскольку эти две программы продемонстрировали бы:
void test_socketpair () {
int pair[2];
socketpair(PF_LOCAL, SOCK_STREAM, 0, pair);
close(pair[0]);
if (send(pair[1], "a", 1, MSG_NOSIGNAL) < 0) perror("send");
}
void test_pipe () {
int pair[2];
pipe(pair);
close(pair[0]);
signal(SIGPIPE, SIG_IGN);
if (write(pair[1], "a", 1) < 0) perror("send");
signal(SIGPIPE, SIG_DFL);
}
Linux может это сделать, потому что ядро имеет врожденные знания о другом конце трубы или связанной пары. Однако при использовании connect
состояние сокета поддерживается стеком протоколов. Ваш тест демонстрирует это поведение, но ниже - это программа, которая делает все это в одном потоке, аналогично двум вышеуказанным тестам:
int a_sock = socket(PF_INET, SOCK_STREAM, 0);
const int one = 1;
setsockopt(a_sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in a_sin = {0};
a_sin.sin_port = htons(4321);
a_sin.sin_family = AF_INET;
a_sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(a_sock, (struct sockaddr *)&a_sin, sizeof(a_sin));
listen(a_sock, 1);
int c_sock = socket(PF_INET, SOCK_STREAM, 0);
fcntl(c_sock, F_SETFL, fcntl(c_sock, F_GETFL, 0)|O_NONBLOCK);
connect(c_sock, (struct sockaddr *)&a_sin, sizeof(a_sin));
fcntl(c_sock, F_SETFL, fcntl(c_sock, F_GETFL, 0)&~O_NONBLOCK);
struct sockaddr_in s_sin = {0};
socklen_t s_sinlen = sizeof(s_sin);
int s_sock = accept(a_sock, (struct sockaddr *)&s_sin, &s_sinlen);
struct pollfd c_pfd = { c_sock, POLLOUT, 0 };
if (poll(&c_pfd, 1, -1) != 1) perror("poll");
int erropt = -1;
socklen_t errlen = sizeof(erropt);
getsockopt(c_sock, SOL_SOCKET, SO_ERROR, &erropt, &errlen);
if (erropt != 0) { errno = erropt; perror("connect"); }
puts("P|Recv-Q|Send-Q|Local Address|Foreign Address|State|");
char cmd[256];
snprintf(cmd, sizeof(cmd), "netstat -tn | grep ':%hu ' | sed 's/ */|/g'",
ntohs(s_sin.sin_port));
puts("before close on client"); system(cmd);
close(c_sock);
puts("after close on client"); system(cmd);
if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
puts("after send on server"); system(cmd);
puts("end of test");
sleep(5);
Если вы запустите указанную выше программу, вы получите аналогичный результат:
P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
before close on client
tcp|0|0|127.0.0.1:35790|127.0.0.1:4321|ESTABLISHED|
tcp|0|0|127.0.0.1:4321|127.0.0.1:35790|ESTABLISHED|
after close on client
tcp|0|0|127.0.0.1:35790|127.0.0.1:4321|FIN_WAIT2|
tcp|1|0|127.0.0.1:4321|127.0.0.1:35790|CLOSE_WAIT|
after send on server
end of test
Это показывает, что для перехода сокетов к состояниям CLOSED
потребовалось один write
. Чтобы узнать, почему это произошло, может оказаться полезным дамп транзакции TCP:
16:45:28 127.0.0.1 > 127.0.0.1
.809578 IP .35790 > .4321: S 1062313174:1062313174(0) win 32792 <mss 16396,sackOK,timestamp 3915671437 0,nop,wscale 7>
.809715 IP .4321 > .35790: S 1068622806:1068622806(0) ack 1062313175 win 32768 <mss 16396,sackOK,timestamp 3915671437 3915671437,nop,wscale 7>
.809583 IP .35790 > .4321: . ack 1 win 257 <nop,nop,timestamp 3915671437 3915671437>
.840364 IP .35790 > .4321: F 1:1(0) ack 1 win 257 <nop,nop,timestamp 3915671468 3915671437>
.841170 IP .4321 > .35790: . ack 2 win 256 <nop,nop,timestamp 3915671469 3915671468>
.865792 IP .4321 > .35790: P 1:2(1) ack 2 win 256 <nop,nop,timestamp 3915671493 3915671468>
.865809 IP .35790 > .4321: R 1062313176:1062313176(0) win 0
Первые три строки представляют собой трехстороннее рукопожатие. Четвертая строка - это пакет FIN
, который клиент отправляет на сервер, а пятая строка - ACK
с сервера, подтверждающая квитанцию. Шестая строка - это сервер, который пытается отправить 1 байт данных клиенту с установленным флагом PUSH
. Конечной линией является клиентский пакет RESET
, который заставляет состояние TCP для соединения быть освобожденным, и именно поэтому третья команда netstat
не привела к выходу в вышеприведенном тесте.
Таким образом, сервер не знает, будет ли клиент подключаться reset до тех пор, пока он не попытается отправить на него некоторые данные. Причина для reset заключается в том, что клиент назвал close
, а не что-то еще.
Сервер не может точно знать, какой системный вызов клиент фактически выдал, он может следовать только за состоянием TCP. Например, мы могли бы заменить вызов close
вызовом shutdown
.
//close(c_sock);
shutdown(c_sock, SHUT_WR);
Разница между shutdown
и close
заключается в том, что shutdown
управляет только состоянием соединения, а close
также управляет состоянием дескриптора файла, который представляет сокет. A shutdown
не будет close
сокета.
Выход будет отличаться при изменении shutdown
:
P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
before close on client
tcp|0|0|127.0.0.1:4321|127.0.0.1:56355|ESTABLISHED|
tcp|0|0|127.0.0.1:56355|127.0.0.1:4321|ESTABLISHED|
after close on client
tcp|1|0|127.0.0.1:4321|127.0.0.1:56355|CLOSE_WAIT|
tcp|0|0|127.0.0.1:56355|127.0.0.1:4321|FIN_WAIT2|
after send on server
tcp|1|0|127.0.0.1:4321|127.0.0.1:56355|CLOSE_WAIT|
tcp|1|0|127.0.0.1:56355|127.0.0.1:4321|FIN_WAIT2|
end of test
Дамп TCP также покажет что-то другое:
17:09:18 127.0.0.1 > 127.0.0.1
.722520 IP .56355 > .4321: S 2558095134:2558095134(0) win 32792 <mss 16396,sackOK,timestamp 3917101399 0,nop,wscale 7>
.722594 IP .4321 > .56355: S 2563862019:2563862019(0) ack 2558095135 win 32768 <mss 16396,sackOK,timestamp 3917101399 3917101399,nop,wscale 7>
.722615 IP .56355 > .4321: . ack 1 win 257 <nop,nop,timestamp 3917101399 3917101399>
.748838 IP .56355 > .4321: F 1:1(0) ack 1 win 257 <nop,nop,timestamp 3917101425 3917101399>
.748956 IP .4321 > .56355: . ack 2 win 256 <nop,nop,timestamp 3917101426 3917101425>
.764894 IP .4321 > .56355: P 1:2(1) ack 2 win 256 <nop,nop,timestamp 3917101442 3917101425>
.764903 IP .56355 > .4321: . ack 2 win 257 <nop,nop,timestamp 3917101442 3917101442>
17:09:23
.786921 IP .56355 > .4321: R 2:2(0) ack 2 win 257 <nop,nop,timestamp 3917106464 3917101442>
Обратите внимание, что reset в конце наступает через 5 секунд после последнего пакета ACK
. Этот reset вызван отключением программы без должного закрытия сокетов. Это пакет ACK
от клиента к серверу до reset, который отличается от предыдущего. Это указывает на то, что клиент не использовал close
. В TCP индикация FIN
действительно является признаком того, что больше не нужно отправлять данные. Но поскольку TCP-соединение двунаправлено, сервер, который получает FIN
, предполагает, что клиент все еще может получать данные. В приведенном выше случае клиент фактически принимает данные.
Если клиент использует close
или SHUT_WR
для выпуска FIN
, в любом случае вы можете обнаружить прибытие FIN
путем опроса на сокете сервера для читаемого события. Если после вызова read
результат равен 0
, то вы знаете, что пришло FIN
, и вы можете делать то, что хотите, с этой информацией.
struct pollfd s_pfd = { s_sock, POLLIN|POLLOUT, 0 };
if (poll(&s_pfd, 1, -1) != 1) perror("poll");
if (s_pfd.revents|POLLIN) {
char c;
int r;
while ((r = recv(s_sock, &c, 1, MSG_DONTWAIT)) == 1) {}
if (r == 0) { /*...FIN received...*/ }
else if (errno == EAGAIN) { /*...no more data to read for now...*/ }
else { /*...some other error...*/ perror("recv"); }
}
Теперь тривиально верно, что если сервер выдает SHUT_WR
с shutdown
, прежде чем он попытается выполнить запись, он фактически получит ошибку EPIPE
.
shutdown(s_sock, SHUT_WR);
if (send(s_sock, "a", 1, MSG_NOSIGNAL) < 0) perror("send");
Если вместо этого вы хотите, чтобы клиент указывал на сервер немедленный reset, вы можете заставить это произойти на большинстве стеков TCP, включив опцию linger с задержкой времени 0
до вызова close
.
struct linger lo = { 1, 0 };
setsockopt(c_sock, SOL_SOCKET, SO_LINGER, &lo, sizeof(lo));
close(c_sock);
При вышеуказанном изменении выход программы будет выглядеть следующим образом:
P|Recv-Q|Send-Q|Local Address|Foreign Address|State|
before close on client
tcp|0|0|127.0.0.1:35043|127.0.0.1:4321|ESTABLISHED|
tcp|0|0|127.0.0.1:4321|127.0.0.1:35043|ESTABLISHED|
after close on client
send: Connection reset by peer
after send on server
end of test
В этом случае send
получает немедленную ошибку, но это не EPIPE
, это ECONNRESET
. Дамп TCP также отражает это:
17:44:21 127.0.0.1 > 127.0.0.1
.662163 IP .35043 > .4321: S 498617888:498617888(0) win 32792 <mss 16396,sackOK,timestamp 3919204411 0,nop,wscale 7>
.662176 IP .4321 > .35043: S 497680435:497680435(0) ack 498617889 win 32768 <mss 16396,sackOK,timestamp 3919204411 3919204411,nop,wscale 7>
.662184 IP .35043 > .4321: . ack 1 win 257 <nop,nop,timestamp 3919204411 3919204411>
.691207 IP .35043 > .4321: R 1:1(0) ack 1 win 257 <nop,nop,timestamp 3919204440 3919204411>
Пакет RESET
появляется сразу после завершения трехстороннего установления связи. Однако использование этой опции имеет свои опасности. Если на другом конце есть непрочитанные данные в буфере сокета, когда приходит RESET
, эти данные будут очищены, что приведет к потере данных. Принуждение RESET
для отправки обычно используется в протоколах стиля запроса/ответа. Отправитель запроса может знать, что нет потерянных данных, когда он получает весь ответ на свой запрос. Затем безопасно для отправителя запроса принудительно отправить RESET
в соединение.