Я предполагал, что сервер может читать / записывать из / в сокет, когда TCP-соединение находится в состоянии ESTABLISHED. Но я вижу, что сервер действительно может читать и записывать в сокет, когда TCP-соединение находится в состоянии CLOSE_WAIT. Это происходит, когда клиент закрыл соединение на своей стороне, но сервер еще не обнаружил / не обработал случай окончания потока.
Например. Синхронный, блокирующий однопоточный сервер:
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
int main (void)
{
signal(SIGPIPE, SIG_IGN); // ignore SIGPIPE from last send()
int listen_sock = socket(PF_INET, SOCK_STREAM, 0);
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(8000), .sin_addr.s_addr = htonl(INADDR_ANY)};
bind(listen_sock, (struct sockaddr *) &addr, sizeof(addr));
listen(listen_sock, 10);
while (1) {
int sock = accept(listen_sock, 0, 0);
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0);
sleep(1);
send(sock, buffer, 10, 0);
send(sock, buffer, 10, 0);
sleep(1);
send(sock, buffer, 10, 0);
close(sock);
}
}
Компиляция и запуск:
gcc server_short.c -o server_short && strace ./server_short
Просмотр подключений на одном хосте:
watch -n0.1 'sudo ss -lnt | grep 8000; sudo netstat -tpvn | grep 8000'
Запуск недолговечных клиентов на другом хосте:
seq 20 | xargs -P100 -n1 bash -c 'echo -n "0123456789ABCDEF" | telnet $SERVER_IP 8000'
Затем я вижу соединения в состояниях CLOSE_WAIT и SYN_RECV, SYN_RECV превратится в CLOSE_WAIT, так как соединения в очереди будут обрабатываться сервером:
LISTEN 11 10 0.0.0.0:8000 0.0.0.0:*
tcp 17 0 server_IP:8000 client_IP:33016 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33030 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33020 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33028 CLOSE_WAIT -
tcp 0 0 server_IP:8000 client_IP:33012 CLOSE_WAIT 21282/./server
tcp 0 0 server_IP:8000 client_IP:33046 SYN_RECV -
tcp 0 0 server_IP:8000 client_IP:33040 SYN_RECV -
tcp 0 0 server_IP:8000 client_IP:33044 SYN_RECV -
tcp 0 0 server_IP:8000 client_IP:33036 SYN_RECV -
tcp 17 0 server_IP:8000 client_IP:33018 CLOSE_WAIT -
tcp 0 0 server_IP:8000 client_IP:33048 SYN_RECV -
tcp 17 0 server_IP:8000 client_IP:33034 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33022 CLOSE_WAIT -
tcp 0 0 server_IP:8000 client_IP:33042 SYN_RECV -
tcp 0 0 server_IP:8000 client_IP:33038 SYN_RECV -
tcp 17 0 server_IP:8000 client_IP:33032 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33026 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33014 CLOSE_WAIT -
tcp 17 0 server_IP:8000 client_IP:33024 CLOSE_WAIT -
И затем сервер обрабатывает эти соединения CLOSE_WAIT по одному:
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 10) = 0
accept(3, NULL, NULL) = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4) = 0
accept(3, NULL, NULL) = 4
recvfrom(4, "0123456789ABCDEF", 1024, 0, NULL, NULL) = 16
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
sendto(4, "0123456789", 10, 0, NULL, 0) = 10
nanosleep({tv_sec=1, tv_nsec=0}, 0x7ffdb9a2d0e0) = 0
sendto(4, "0123456789", 10, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=22658, si_uid=1001} ---
close(4) = 0
... AND SO ON
Таким образом, сервер может успешно принять, прочитать запрос, обработать его и отправить ответ, прежде чем обнаруживать конец потока, вызывая чтение или обнаружение сломанного канала, вызывая запись .
Я понимаю, что этот код сервера не идеален, но даже если вы сделаете его асинхронным (с помощью select, epoll, boost :: asio и т. Д.), Вы также примете соединения CLOSE_WAIT, прочитаете входящий запрос и начальную обработку запроса перед обнаружением, что соединение не живо. Хотя серверная сторона знала, что соединение было закрыто клиентом до того, как оно было принято сервером.
Итак, вопросы:
- Соответствует ли это поведение спецификации TCP? Законно ли принимать и читать из соединения CLOSE_WAIT?
- Почему ядро разрешает принимать соединения CLOSE_WAIT? Или почему recv не возвращает ошибку в этом случае? Кто может быть заинтересован в чтении данных из закрытого соединения? Какова цель этого поведения? Кажется, я не вижу, как можно использовать этот случай.
- Как обнаружить этот случай и не начинать обработку запроса, когда на 100% уверены, что никто не получит ответ?