LINUX.ORG.RU

Не могу победить TIME_WAIT, как закрывать сокеты?


0

0

По определённым причинам, необходимо реализовать свой хттп-сервер, не требовательный к ресурсам (т.е. запуск нового thread на новый коннект - вполне ничо так решение). Всё отлично, кроме одного: мат. ожидание нагрузки - 2 запроса в секунду. Но при этом, закрытые сокеты остаются висеть в памяти в состоянии TIME_WAIT, и где-то через 5-10 минут лимит заканчивается, и на очередном вызове (предположительно) accept основной поток завершается, и вёб-сервер накрывается медным тазом, вместе со всеми своими обязанностями. Спрашивается, как решить эту неприятную ситуацию, если вот апач вполне с ней справляется? Хотя, этот кажется процесс новый запускает каждый раз... но всё равно, форки мне не помогли. В интернете была информацию по этой проблеме, но решения найти не смог. Привожу код сервера. Для исключения возможности проблем с синхронизацией, привожу полностью однопоточный код (который легко возвращается в многопоточный). Открывается на 4747 порту.

#include<stdio.h>  
#include<string.h>  
#include<stdlib.h>  
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<sys/socket.h>  
#include<arpa/inet.h>  
#include<netdb.h>  
#include<signal.h>  
#include<fcntl.h>  
#include <pthread.h>


#define CONNMAX 1000  
#define BYTES 1024  
#define MAX_PATH 260


char ROOT[MAX_PATH];  
int listenfd;//, clients[CONNMAX];  



void start_listen(char *port)  
{  
	struct addrinfo hints, *res, *p;  

	// getaddrinfo for host  
	memset (&hints, 0, sizeof(hints));  
	hints.ai_family = AF_INET;  
	hints.ai_socktype = SOCK_STREAM;  
	hints.ai_flags = AI_PASSIVE;  
	if (getaddrinfo( NULL, port, &hints, &res) != 0)  
	{  
		perror ("getaddrinfo() error");  
		exit(1);  
	}  
	// socket and bind  
	for (p = res; p!=NULL; p=p->ai_next)  
	{  
		listenfd = socket (p->ai_family, p->ai_socktype, 0);  
		if (listenfd == -1) continue;  
		if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break;  
	}  
	if (p==NULL)  
	{  
		perror ("socket() or bind()");  
		exit(1);  
	}  

	freeaddrinfo(res);  

	// listen for incoming connections  
	if ( listen (listenfd, 1000000) != 0 )  
	{  
		perror("listen() error");  
		exit(1);  
	}  
}  

int init_server()  
{   
	char c;      

	//Default Values PATH = ~/ and PORT=4747  
	char PORT[6];  
	strcpy(PORT,"4747");  
	strcpy(ROOT, getenv("PWD"));  
	strcat(ROOT, "/web");


	printf("%sWeb panel runned at port %s%s%s\n", "\033[92m", "\033[36m", PORT, "\033[0m");

	//	printf("Server started at port no. %s%s%s with root directory as %s%s%s\n","\033[92m",PORT,"\033[0m","\033[92m",ROOT,"\033[0m");  
	// Setting all elements to -1: signifies there is no client connected  
	int i;  

	start_listen(PORT);  

}  


//client connection  
void * respond(void * sock_v)  
{  
	int sock = (int) sock_v;
	char mesg[99999], *reqline[3], data_to_send[BYTES], path[99999];  
	int rcvd, fd, bytes_read;  

	memset( (void*)mesg, (int)'\0', 99999 );  

	rcvd=recv(sock, mesg, 99999, 0);  

	if (rcvd<0)    // receive error  
		fprintf(stderr,("recv() error\n"));  
	else if (rcvd==0)    // receive socket closed  
		fprintf(stderr,"Client disconnected upexpectedly.\n");  
	else    // message received  
	{  
		//printf("%s", mesg);  
		reqline[0] = strtok (mesg, " \t\n");  
		if ( strncmp(reqline[0], "GET\0", 4)==0 )  
		{  
			reqline[1] = strtok (NULL, " \t");  
			reqline[2] = strtok (NULL, " \t\n");  
			if ( strncmp( reqline[2], "HTTP/1.0", 8)!=0 && strncmp( reqline[2], "HTTP/1.1", 8)!=0 )  
			{  
				write(sock, "HTTP/1.0 400 Bad Request\n", 25);  
			}  
			else  
			{  
				if ( strncmp(reqline[1], "/\0", 2)==0 )  
					reqline[1] = (char*)"/status.html";        //Because if no file is specified, index.html will be opened by default (like it happens in APACHE...  

				strcpy(path, ROOT);  
				strcpy(&path[strlen(ROOT)], reqline[1]);  
				//printf("file: %s\n", path);  

				static int resp_num = 0;

				char buff[100];
				sprintf(buff, "Responsed %d times", ++resp_num);

				send(sock, "HTTP/1.0 200 OK\n\n", 17, 0);  
				send (sock, buff, strlen(buff), 0);  

			}  
		}  
	}  

	//Closing SOCKET  
	
	linger l = { 1, 1 };
	setsockopt(sock, SOL_SOCKET, SO_LINGER, &l, sizeof(linger));
	close(sock); 
	
} 


void * http_server_loop (void *)
{
	init_server();

	socklen_t addrlen; 
	int slot=0;  

	// ACCEPT connections  
	while (1)  
	{  
		sockaddr_in clientaddr;  
		addrlen = sizeof(clientaddr);  
		
		int sock = accept (listenfd, (sockaddr *) &clientaddr, &addrlen);  

		if (sock<0)  
			fprintf(stderr, "accept() error num: %d\n", (void*)sock);  
		else  
		{  
/*			if ( fork()==0 )  
			{  
				respond(slot);  
				exit(0);  
			}  */

			//pthread_t thread;
			//pthread_create(&thread, NULL, respond, (void*) sock);

			respond((void *)sock);
		}  

	}  

	return 0;  
}


int start_http_server ()
{
	pthread_t thread;

	int err_code;
	if (err_code = pthread_create(&thread, NULL, http_server_loop, NULL))
	{
		fprintf(stderr, "pthread_create fails with: %d\n", err_code);
		return 2;
	}

	return(0);
}
 


//////////////////////////////////////////////////////////////////////////
// For testing
//////////////////////////////////////////////////////////////////////////


int main () 
{
//	start_http_server();
	http_server_loop(0);

	usleep(0x7FFFFFFF);
}

Ответ на: комментарий от Reset

Так мне попробовать отправлять заголовки http 1.1 ? :) Или может отправлять такой заголовок, чтобы браузер сам рвал подключение, посылая FIN? Тогда в общем-то и проблема решится (исключая злоумышленника).

Suspended ()
Ответ на: комментарий от Suspended

TIME_WAIT будет на той стороне, которая инициирует закрытие соединения. Если работаешь по HTTP 1.1, то последовательные запросы будут проходить без открытия нового соединения. Только учти, что в этом случае надо передавать корректный Content-Length, иначе повиснем на чтении.

Reset ★★★★★ ()
Ответ на: комментарий от Reset

>очевидно, что надо использовать http 1.1 и не закрывать сокет

А что, Connection: keep-alive уже отменили? Я, кстати, не понял топикстартера: что именно у него заканчивается? Порты или файловые дескрипторы?

linuxfan ()
Ответ на: комментарий от Suspended

У ott'а в блоге есть http сервер на boost::asio, в HTTP 1.1 переделывается он тривиально. Еще можешь взять evhttp из libevent, но там есть небольшая засада — в callback'е надо вернуть весь буфер с данными целиком, если данные готовятся постепенно, то придется либо делать пайпы (что некроссплатформенно) и несколько потоков (поток подготовки данных и поток libevent'а) либо патчить сам evhttp (это тривиально, код там простой и хорошо документирован).

Reset ★★★★★ ()

>закрытые сокеты остаются висеть в памяти в состоянии TIME_WAIT, и где-то через 5-10 минут лимит заканчивается, и на очередном вызове (предположительно) accept основной поток завершается

Скорее всего, основной поток у тебя завершается, потому что надо игнорировать SIGPIPE, который ты получаешь, когда пытаешься писать в сокет, закрытый с другой стороны. Умолчальное действие для SIGPIPE — завершение приложения. Никаких break/return/exit у тебя нет, так что не надо быть телепатом, чтобы понять истинную причину.

Настоятельно рекомендую почитать Стивенса, у него хорошо описано, что такое TIME_WAIT и почему это ну никак не может отразиться на сервере с двумя запросами в секунду. Также полезная пища для ума «UNIX sockets FAQ».

Ну и на закуску: http://www.developerweb.net/forum/showthread.php?t=2941

linuxfan ()
Ответ на: комментарий от linuxfan

Вообще да, 2 запроса в секунду это 1200 сокетов за 10 минут, а предел у нас 65535, поэтому TIME_WAIT никак не повлияет.

Reset ★★★★★ ()
Ответ на: комментарий от Reset

>Какое keep-alive в http 1.0 ?

А вот такой. Ты думаешь, как в HTTP 1.0 жили? Попробуй, например,

$ telnet ya.ru 80

Trying 93.158.134.8...


Connected to ya.ru.


Escape character is '^]'.


GET http://ya.ru HTTP/1.0


Connection: keep-alive

linuxfan ()
Ответ на: комментарий от linuxfan

Давай-те я более подробно опишу симптономатику. Тестировал у себя на Убунте, и кроме меня второй программист тоже на убунте. У него в каких-то случаях на каждый прогон сервера до падения, девиация количества запросов была кажется в 2 запроса. Т.е. делаем 122-126 запросов - сервер падает. И так довольно стабильно, вне особой зависимости от паузы между последовательными запросами. Так вот, у нас у обоих к моменту, когда сервер по прикидкам собирается упасть, в netstat'e висело [b]сотни[/b] коннектов в состоянии ожидания. Именно здесь я сделал вывод, что превышается число доступных дескрипторов (сокетов) и программа просто грохается.

ПО поводу keep-alive: запросы будут приходить из скрипта посредством AJAX (внутри локальной сети, 2 раза в секунду, т.е. ответ очень быстрый). Сможет ли браузер поддерживать коннект в случае асимметричных запросов?

Ссылки почитаю завтра. Спасибо всем, кто пытается помочь!

P.S. Мне бы хотелось максимально упростить решение, а именно заставить браузер закрыть подключения, чтобы уже получить вожделенный FIN. P.Р.S. А может FIN самому себе слать, а? ;) самозакрытие, хех :)

Suspended ()
Ответ на: комментарий от Reset

>предел у нас 65535

На самом деле, предел у нас можно узнать с помощью ulimit -n (1024 файловых дескриптора по умолчанию), так что при 1020 живых клиентах (если считать, что открыто 4 дескриптора: stdin, stdout, stderr и слушающий сокет) accept 1021-го вернет EMFILE в errno

linuxfan ()
Ответ на: комментарий от Suspended

Ссылки почитаю завтра. Спасибо всем, кто пытается помочь!

Забубень без чтения ссылок от это в начале main:

    {
        struct sigaction sa;
        sa.sa_handler = SIG_IGN;
        sa.sa_flags = 0;
        if (sigemptyset(&sa.sa_mask) == -1 || sigaction(SIGPIPE, &sa, 0) == -1)
            perror_fatal("failed to ignore SIGPIPE; sigaction");
            exit(1);
    }

Ессно не забыть #include <signal.h>

Это стандартные грабли, на которые наступают абсолютно все.

Сможет ли браузер поддерживать коннект в случае асимметричных запросов?

С этим вообще никаких проблем. Только если ты хочешь использовать keep-alive, то обязательно указывай в ответе Content-Length.

А вообще, рекомендую взять libevent (нагугли самостоятельно), там есть встроенный http-сервер (и клиент), которые и 2000 запросов в секунду способны потянуть.

linuxfan ()
Ответ на: комментарий от Reset

>Это не по стандарту.

Это называется стандарт «де-факто». А так, например, по стандарту HTTP/1.1 подерживает пайплайнинг, вот только реализации протокола такие отвратительные, что по умолчанию эта фича в браузерах отключена (поправьте, если я ошибаюсь, и в свежих сборках ff ее включают по умолчанию).

linuxfan ()
Ответ на: комментарий от linuxfan

>perror_fatal

Тьфумлин, perror, конечно же. #include <errno.h> тоже не забудь.

linuxfan ()
Ответ на: комментарий от Boy_from_Jungle

>> if ( listen (listenfd, 1000000) != 0 )

Зачем такой нужен ?!

Дайте мне таблеток от жадности! Да побольше, побольше! :)

const86 ★★★★★ ()

сокет закрывается как обычно, а вот повторный реюз, покопай в направлнеии setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);

Lee_Noox ★★★ ()
Ответ на: комментарий от linuxfan

Супер! этот код с блокированием SIG_IGN помог! Можете ещё раз объяснить, из-за чего приложение получает его, и после каких событий? вызова send?

кстати, если просто зажать Ctrl + R в браузере, а затем выполнить netstat | grep TIME_WAIT | wc будет 789 4734 63120 но главное, что теперь работает :) надеюсь, такого «побочного» эффекта моего сервера никто не заметит...

Suspended ()
Ответ на: комментарий от Suspended

>Можете ещё раз объяснить, из-за чего приложение получает его, и после каких событий?

SIGPIPE — это сигнал, который получает приложение при записи в сокет или пайп, принимающая сторона которого закрыла его. Такое поведение необходимо, чтобы корректно работали всеми любимые юниксовые пайпы (|). Например, если бы не SIGPIPE, то cat somefile.txt | head -n1 пересылало бы весь somefile.txt в пайп, а так head завершается после вывода одной строчки, а вместе с ним завершается и cat по сигналу.

P. S. TIME_WAIT — это нормально.

linuxfan ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.