If a C programmer asks "do you want to see something cool?", run away.
--John Van Enk

Monday, August 13, 2007

Внешне указатели, сокеты внутри

Программирование сетевых и многозадачных приложений довольно трудная задача, так как в данной области довольно часто возникают нетривиальные ошибки, которые очень редко проявляются и их очень затруднительно воспроизвести. Типичный пример - потеря соединения. Пусть у нас имеется следующий код:




#define COMMAND_PORT 2021

//Функция обработки принятого соединения
void* ProcessConnection(void*p){
/*делаем что-нибудь полезное*/
int rval=0;
close(*((int*)p));
pthread_exit((void*)rval);
}

//
int main(int argc,char**argv){
//создаем сокет
int sd = socket( PF_INET, SOCK_STREAM, 0 );
if ( sd == -1 ) {
perror( "Error create socket: " );
return EXIT_FAILURE;
}

struct sockaddr_in addr;
bzero( &addr, sizeof( addr ) );
addr.sin_family = AF_INET;
addr.sin_port = htons( COMMAND_PORT );
addr.sin_addr.s_addr = INADDR_ANY;
int addr_size = sizeof( struct sockaddr_in );

//привязываем адрес к сокету
if ( bind( sd, ( struct sockaddr * ) & addr, addr_size ) != 0 ) {
perror( "Error assigning address to socket: " );
return EXIT_FAILURE;
}

//выражаем готовность принимать запросы на соединеие
if ( listen( sd, 5 ) != 0 ) {
perror( "Error create listener queue: " );
return EXIT_FAILURE;
}

//далее извлекаем запросы на соединение и для обработки
//каждого такого соединения создаем поток
while ( 1 ) {
int cld=accept(sd,(struct sockaddr*)&addr,(socklen_t*)&addr_size);
if ( cld ==-1 ) {
perror("Error accepting with client: ");
return EXIT_FAILURE;
} else {

pthread_t tchild;
if(pthread_create(&tchild,NULL,&ProcessConnection,(void*)&cld )!= 0){
perror("Error create thread: " );
close( cld );
return EXIT_FAILURE;
}

}// if-else
}// while
return EXIT_SUCCESS; 
}

На первый взгляд этот код не содержит ошибок и будет работать правильно в большинстве случаев. Но при большой нагрузке на этот сервер часть соединений будет утеряна. Почему? Если обратить внимание на следующую строчку


pthread_create( &tchild, NULL, &ProcessConnection,(void*) &cld );

то можно увидеть, что файловый дескриптор созданный accept(); передается в обрабатывающую функцию не по значению, а по адресу. В действительности неизвестно как операционная система распланирует выполнение потоков и вполне возможно что поток обработки соединения начнет выполнятся тогда, когда будет принят следующий запрос на соединение. А поскольку передается адрес дескриптора, то в данном случае первое принятое соединение будет потеряно (кроме того дескриптор не будет закрыт). Более того возможно возникновение ситуации когда два потока будут обрабатывать одно и то же соединение, что естественно ничего хорошего не принесет. Решение как всегда лежит на поверхности: следует изменить чуть-чуть функцию обработки соединения и передавать не адрес дескриптора а его значение:


//Функция обработки принятого соединения
void* ProcessConnection(void*p){
/*делаем что-нибудь полезное*/
int rval=0;
close((int)p);
pthread_exit((void*)rval);
}
int main(int argc,char**argv){
/*.........*/
//далее извлекаем запросы на соединение и для обработки
//каждого такого соединения создаем поток
while ( 1 ) {
int cld=accept(sd,(struct sockaddr*)&addr,(socklen_t*)&addr_size);
if ( cld ==-1 ) {
perror("Error accepting with client: ");
return EXIT_FAILURE;
} else {

pthread_t tchild;
if(pthread_create(&tchild,NULL,&ProcessConnection,(void*)cld )!= 0) {
perror("Error create thread: " );
close( cld );
return EXIT_FAILURE;
}

}//if-esle
}//while
/*.........*/
}