Я отлаживаю очень простой TCP-сервер на C в Linux. Я остановил выполнение прямо перед строкой, в которой вызывается метод Accept(). К моему удивлению, когда клиент отправляет SYN, tcpdump показывает, что сервер отвечает SYN-ACK (на который сразу же приходит окончательный ответ ACK от клиента).
Команда ss действительно показывает, что приложение уже прослушивает связанный порт.
Я понимаю, что я уже вызвал функцию Listen(), поэтому приложение будет прослушивать связанный порт. Но тогда, по той же семантике, метод Accept() должен быть вызван до того, как сервер сможет принять соединение.
На страницах руководства Listen() написано (курсив мой):
Listen() помечает сокет, на который ссылается sockfd, как пассивный сокет, то есть сокет, который будет использоваться для приема входящих запросов на соединение с помощью Accept(2).
Хотя на страницах руководства Accept() написано:
Он извлекает первый запрос на соединение из очереди ожидающих соединений для прослушивающего сокета.
Из этого можно понять, что Accept() следует вызывать до того, как будет установлено соединение.
Что мне здесь не хватает? Если это стандартное поведение, можно ли мне указать на первоисточник? Или это просто специфика реализации?
Ниже приведен код, который я использую. Если я остановлю его выполнение прямо перед вызовом Listen(), использование netcat покажет, что на отправленный SYN отвечает RST. Но если я сделаю то же самое после выполнения Listen(), tcpdump покажет, что сервер отвечает SYN-ACK.
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
void error(const char* message) {
printf("%s %s\n", message, strerror(errno));
}
int main(int argc, char** argv) {
const int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if ( sockfd == -1 ) {
error("Socket error:");
return 1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( bind(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) == - 1 ) {
error("Bind error:");
return 1;
}
if ( listen(sockfd, 5) == -1 ) {
error("Listen error: ");
return 1;
}
printf("Ready.\n");
struct sockaddr_in cliaddr;
socklen_t cliaddrlen = sizeof(cliaddr);
char response[512];
while (1) {
const int connfd = accept(sockfd, (struct sockaddr*) &cliaddr, &cliaddrlen);
if ( connfd == -1 ) {
printf("Accept error: %s\n", strerror(errno));
return 1;
}
const pid_t pid = fork();
if ( pid == -1 ) {
printf("Fork error: %s\n", strerror(errno));
continue;
}
if ( pid == 0 ) {
close(sockfd);
char buffer[16];
inet_ntop(AF_INET, &cliaddr.sin_addr, buffer, 16);
printf("Connection from %s accepted.\n", buffer);
while ( 1 ) {
int nread = read(connfd, response, 512);
if ( nread == -1 ) {
printf("%s\n", strerror(errno));
}
if (nread == 1 && response[0] == '\n') {
break;
}
write(connfd, response, nread);
//write(STDIN_FILENO, response, nread);
}
printf("Good bye!\n");
close(connfd);
return 0;
}
close(connfd);
wait(NULL);
}
return 0;
}
Целочисленный параметр 'backlog' для Listen() – тот, где вы указываете 5
– определяет, сколько соединений может быть принято таким образом; после достижения этого предела новые SYN будут игнорироваться или отклоняться.
Это позволяет ставить эти соединения в очередь, например. на однопоточном сервере, который, скорее всего, обнаружил бы, когда изначально был разработан API сокетов BSD (который, если я правильно понимаю историю, предшествует многопоточности). Серверу, возможно, придется выполнить fork(), прежде чем он сможет принять() следующего клиента, или он может даже решить обслужить запрос немедленно в том же потоке, если это простой протокол (например, базовый сервер Whois или QotD), прежде чем перейти к следующий клиент.
Если бы сервер вообще не отвечал, у клиента быстро истечет тайм-аут соединения; но как только соединение установлено, тайм-аут может быть намного дольше (например, SMTP определяет тайм-аут в пять минут до приветствия). При этом я не знаю, какие задержки были бы «нормальными» во время создания этого API, поэтому по этому поводу есть некоторые предположения.
Вы не различаете работу пользовательского приложения и поддерживающую работу ядра, что в данном случае также является разделением уровня приложения и транспортного уровня (и ниже). Именно ядро отвечает за TCP-квитирование и, в более общем смысле, за установление соединений на уровне TCP. Вызов приложения listen()
просит его начать делать это для входящих запросов на соединение транспортного уровня. Со стороны пользовательского пространства подключенные сокеты, в которых находится приложение accepts()
, обслуживают уровень приложения, по крайней мере, насколько это необходимо приложению.
Хотя на страницах руководства Accept() написано:
Он извлекает первый запрос на соединение из очереди ожидающих соединений для прослушивающего сокета.
Из этого можно понять, что Accept() следует вызывать перед соединение должно быть установлено.
Нет, не в той степени, в которой вы имеете в виду «до установки TCP-соединения». accept()
обеспечивает обработку запросов на подключение на уровне приложения. Пользовательское пространство обычно не видит ничего ниже этого.
Кроме того, какой цели будет служить отсрочка завершения рукопожатия?