Каркас TCP-сервера
Начнем с каркаса TCP-сервера. Затем можно приступить к созданию библиотеки, поместив в нее фрагменты кода из каркаса. В листинге 2.2 показана функция main.
Листинг 2.2. Функция main из каркаса tcpserver.skel
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <stdarg.h>
5 #include <string.h>
6 #include <errno.h>
7 #include <netdb.h>
8 #include <fcntl.h>
9 #include <sys/time.h>
10 #include <sys/socket.h>
11 #include <netinet/in.h>
12 #include <arpa/inet.h>
13 #include "skel.h"
14 char *program_name;
15 int main( int argc, char **argv )
17 struct sockaddr_in local;
18 struct sockaddr_in peer;
19 char *hname;
20 char *sname;
21 int peerlen;
22 SOCKET s1;
23 SOCKET s;
24 const int on = 1;
25 INIT ();
26 if ( argc == 2 )
27 {
28 hname = NULL;
29 sname = argv[ 1 ];
30 }
31 else
32 {
33 hname = argv[ 1 ];
34 sname = argv[ 2 ];
35 }
36 set_address( hname, sname, &local, "tcp" );
37 s = socket( AF_INET, SOCK_STREAM, 0 );
38 if ( !isvalidsock( s ) )
39 error ( 1, errno, "ошибка вызова socket" );
40 if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &on,
41 sizeof( on ) ) )
42 error( 1, errno, "ошибка вызова setsockopt" );
43 if ( bind( s, ( struct sockaddr * ) klocal,
44 sizeof( local ) ) )
45 error( 1, errno, "ошибка вызова bind" );
46 if ( listen ( s, NLISTEN ) )
47 error( 1, errno, "ошибка вызова listen" );
48 do
49 {
50 peerlen = sizeof( peer );
51 s1 = accept( s, ( struct sockaddr * )&peer, &peerlen );
52 if ( !isvalidsock( s1 ) )
53 error( 1, errno, "ошибка вызова accept" );
54 server( s1, &peer );
55 CLOSE( s1 );
56 } while ( 1 );
57 EXIT( 0 );
58 }
Включаемые файлы и глобальные переменные
1- 14 Включаем заголовочные файлы, содержащие объявления используемых стандартных функций.
25 Макрос INIT выполняет стандартную инициализацию, в частности, установку глобальной переменной program_name для функции error и вызов функции WSAStartup при работе на платформе Windows.
Функция main
26-35 Предполагается, что при вызове сервера ему будут переданы адрес и номер порта или только номер порта. Если адрес не указан, то привязываем к сокету псевдоадрес INADDR_ANY, разрешающий прием соединений по любому сетевому интерфейсу. В настоящем приложении в командной строке могут, конечно, быть и другие аргументы, обрабатывать их надо именно в этом месте.
36 Функция set_address записывает в поля переменной local типа sockaddr_in указанные адрес и номер порта. Функция set_address показана в листинге 2.3.
37-45 Получаем сокет, устанавливаем в нем опцию SO_REUSEADDR (совет 23) и привязываем к нему хранящиеся в переменной local адрес и номер порта.
46-47 Вызываем listen, чтобы сообщить ядру о готовности принимать соединения от клиентов.
48-56Принимаем соединения и для каждого из них вызываем функцию server. Она может самостоятельно обслужить соединение или создать Для этого новый процесс. В любом случае после возврата из функции server соединение закрывается. Странная, на первый взгляд конструкция do-while позволяет легко изменить код сервера так, чтоб завершался после обслуживания первого соединения. Для этого достаточно вместо
while ( 1 );
написать
while ( 0 );
Далее обратимся к функции set__address. Она будет использована во всех каркасах. Это естественная кандидатура на помещение в библиотеку стандартных функций.
Листинг 2.3. Функция set_address
tcpserver.skel
1 static void set_address(char *hname, char *sname,
2 struct sockaddr_in *sap, char *protocol)
3 {
4 struct servant *sp;
5 struct hostent *hp;
6 char *endptr;
7 short port;
8 bzero (sap, sizeof(*sap));
9 sap->sin_family = AF_INET;
10 if (hname != NULL)
11 {
12 if (!inet_aton (hname, &sap->sin_addr))
13 {
14 hp = gethostbyname(hname);
15 if ( hp == NULL )
16 error( 1, 0, "неизвестный хост: %s\n", hname );
17 sap->sin_addr = *( struct in_addr * )hp->h_addr;
18 }
19 }
20 else
21 sap->sin_addr.s_addr = htonl( INADDR_ANY );
22 port = strtol( sname, &endptr, 0 );
23 if ( *endptr == '\0' )
24 sap->sin_port = htons( port );
25 else
26 {
27 sp = getservbyname( sname, protocol );
28 if ( sp == NULL )
29 error( 1, 0, "неизвестный сервис: %s\n", sname );
30 sap->sin_port = sp->s_port;
31 }
32 }
set_address
8- 9 Обнулив структуру sockaddr_in, записываем в поле адресного семейства AF_INET.
10-19 Если hname не NULL, то предполагаем, что это числовой адрес в стандартной десятичной нотации. Преобразовываем его с помощью функции inet_aton, если inet_aton возвращает код ошибки, - пытаемся преобразовать hname в адрес с помощью gethostbyname. Если и это не получается, то печатаем диагностическое сообщение и завершаем программу.
20-21 Если вызывающая программа не указала ни имени, ни адреса хоста, устанавливаем адрес INADDR_ANY.
22-24 Преобразовываем sname в целое число. Если это удалось, то записываем номер порта в сетевом порядке (совет 28).
27-30 В противном случае предполагаем, что это символическое название ервиса и вызываем getservbyname для получения соответствующего номера порта. Если сервис неизвестен, печатаем диагностическое сообщение и завершаем программу. Заметьте, что getservbyname уже возвращает номер порта в сетевом порядке.
Поскольку иногда приходится вызывать функцию set_address напрямую, лесь приводится ее прототип:
#include "etcp.h"
void set_address(char *host, char *port,
struct sockaddr_in *sap, char *protocol);
Последняя функция - error - показана в листинге 2.4. Это стандартная диагностическая процедура.
#include "etcp.h"
void error(int status, int err, char *format,...);
Если status не равно 0, то error завершает программу после печати диагностического сообщения; в противном случае она возвращает управление. Если err не равно 0, то считается, что это значение системной переменной errno. При этом в конце сообщения дописывается соответствующая этому значению строка и числовое значение кода ошибки.
Далее в примерах постоянно используется функция error, поэтому добавим в библиотеку.
Листинг2.4. Функция error
tcpserver.skel
1 void error( int status, int err, char *fmt, ... )
2 {
3 va_list ap;
4 va_start ( ар, fmt );
5 fprintf (stderr, "%s: ", program_name );
6 vfprintf( stderr, fmt, ap ) ;
7 va_end( ap ) ;
8 if ( err )
9 fprintf( stderr, ": %s (%d)\n", strerror( err ), err);
10 if ( status )
11 EXIT( status );
12 }
В каркас включена также заглушка для функции server:
static void server(SOCKET s, struct sockaddr_in *peerp)
{
}
Каркас можно превратить в простое приложение, добавив код внутрь этой заглушки. Например, если скопировать файл tcpserver.skel в и заменить заглушку кодом
static void server(SOCKET s, struct sockaddr_in *peerp)
{
send( s, "hello, world\n", 13, 0);
}
то получим сетевую версию известной программы на языке С. Если откомпилировать и запустить эту программу, а затем подсоединиться к ней с помощью программы telnet, то получится вполне ожидаемый результат:
bsd: $ hello 9000
[1] 1163
bsd: $ telnet localhost 9000
Trying 127 .0.0.1...
Connected to localhost
Escape character '^]'.
hello, world
Connection closed by foreign host.
Поскольку каркас tcpserver. skel описывает типичную для TCP-сервера ситуацию, поместим большую часть кода main в библиотечную функцию tcp_serv показанную в листинге 2.5. Ее прототип выглядит следующим образом:
#include "etcp.h"
SOCKET tcp_server( char *host, char *port );
Возвращаемое значение: сокет в режиме прослушивания (в случае ошибки завершает программу).
Параметр host указывает на строку, которая содержит либо имя, либо IP – адрес хоста, а параметр port - на строку с символическим именем сервиса или номером порта, записанным в виде ASCII-строки.
Далее будем пользоваться функцией tcp_server, если не возникнет необхомо модифицировать каркас кода.
Листинг 2.5. Функция tcp_server
1 SОСКЕТ tcp_server( char *hname, char *sname )
2 {
3 struct sockaddr_in local;
4 SOCKET s;
5 const int on = 1;
6 set_address( hname, sname, &local, "tcp" );
7 s = socket( AF_INET, SOCK_STREAM, 0 );
8 if ( !isvalidsock( s ) )
9 error( 1, errno, "ошибка вызова socket" );
10 if ( setsockopt ( s, SOL_SOCKET, SO_REUSEADDR,
11 ( char * )&on, sizeoff on ) ) )
12 error( 1, errno, "ошибка вызова setsockopt" );
13 if ( bind( s, ( struct sockaddr * } &local,
14 sizeof( local ) ) )
15 error( 1, errno, "ошибка вызова bind" );
16 if ( listen( s, NLISTEN ) )
17 error( 1, errno, "ошибка вызова listen" );
18 return s;
19 }