сетевое программирование в Unix. Часть 3. UDP
Светлое небо, зеленые ели
Тихий капель стук по ступенькам крыльца...
(экспрессивно, с надрывом)
А в полях пожелтевшего белого снега
Озверевший конвой доедает з/к
Они его ручки — ам, они его ручки — ам
(шепотом)
Вот так совсем человечка физически уничтожили.
(король русского шансона Михаил Брюк, из КВН )
Вот мы и подошли к финальной части букваря, к протоколу UDP и методам работы с ним.
Начнем традиционно, с клиента, и закончим, как обычно, сервером.
Напоминаю, что UDP — это модель передачи данных без образования соединения и без проверки корректности передачи. Посему если информация пропала — никто не виноват. Но если канал передачи надежный — почему бы и не использовать UDP?
Данные передаются так называемыми датаграммами (datagram) без сохранения порядка при переносе к получателю.
UDP-клиент
К сожалению стандартных и широко распространенных программок типа telnet не обнаружено. Поэтому в этой роли выступает накарябаный на колене за вечер корявый шедевр. Встречайте:
[udp_client.cpp]
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <arpa/inet.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <stdio.h>
8 int main() {
9 struct sockaddr_in server, client={AF_INET,INADDR_ANY,INADDR_ANY};
10 memset(&server,0,sizeof(server));
11 server.sin_family=AF_INET;
12 server.sin_port=htons(1212);
13 server.sin_addr.s_addr=inet_addr("127.0.0.1");
14 int sock;
15 sock=socket(PF_INET,SOCK_DGRAM,0);
16 bind(sock,(sockaddr *)&client,sizeof(client));
17 char buf[81];
18 memset(buf,0,81);
19 strcpy(buf,"request");
20 sendto(sock,&buf,strlen(buf),0,(sockaddr *)&server,sizeof(server));
21 memset(buf,0,81);
22 recvfrom(sock,buf,80,0,NULL,0);
23 puts(buf);
24 return 0;
25 }
Эта мини-программа посылает слово "request" на порт 1212 по адресу 127.0.0.1 (localhost) и читает, что же ответил сервер.
с 1 по 14 строки — стандартная сокетная обвязка, общая для TCP и UDP. У нас клиент, поэтому в 12 и 13 задаем порт и адрес назначения.
В 15 обратите особое внимание на константу SOCK_DGRAM — мы задаем тип сокета как датаграмный.
16 — уже специфична. Мы явно выполняем привязку адреса и порта к созданному сокету. 9 строка явно указывает, что программисту фиолетово, какой порт занять и через какой из сетевых интерфейсов отсылать данные. Ядро поймет это указание правильно и выдаст порт случайным образом.
20 и 22 строки — следующие специфичные для UDP элементы. Системные вызовы sendto(2) и recvfrom(2) предназначены для отправки и получения сообщений в/из сокета. Их можно использовать даже если сокет находится в несоединенном состоянии. Кстати, несмотря на природу SOCK_DGRAM, здесь можно использовать connect(2) и заменить sendto(2) на write(2) и recvfrom(2) на read(2). Будет сделана виртуальная привязка сокета к пункту назначения, но соединение не будет устанавливаться, т.к. сокет остается по прежнему SOCK_DGRAM.
UDP-сервер
Опять же daytime сервер в DGRAM-реинкарнации. Ждет датаграмму, при приходе оной извлекает адрес отправителя и посылает в ответ текущее время сервера.
[udp_server.cpp]
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <arpa/inet.h>
5 #include <string.h>
6 #include <time.h>
7 #include <unistd.h>
9 #include <stdio.h>
10 char * daytime() {
11 time_t now;
12 now=time(NULL);
13 return ctime(&now);
14 }
15 int main() {
16 struct sockaddr_in addr;
17 memset(&addr,0,sizeof(addr));
18 addr.sin_family=AF_INET;
19 addr.sin_port=htons(1212);
20 addr.sin_addr.s_addr=INADDR_ANY;
21 int sock, c_sock;
22 sock=socket(PF_INET,SOCK_DGRAM,0);
23 bind(sock,(struct sockaddr *)&addr,sizeof(addr));
24 for (;;) {
25 struct sockaddr from;
26 unsigned int len=sizeof(from);
27 char buf[81];
28 memset(buf,0,81);
29 recvfrom(sock,&buf,80,0,&from,&len);
30 printf("udp incoming:%s",buf);
31 memset(buf,0,81);
32 strncpy(buf,daytime(),80);
33 sendto(sock,buf,strlen(buf),0,&from,len);
34 puts("answer udp");
35 }
36 return 0;
37 }
Все меньше и меньше остается незнакомых элементов в программах. Будь я графоманом и/или любителем гонораров — расписывал бы каждую строку :).
В этом примере мы заострим внимание всего на двух строках — 29 и 33. При вызове recvfrom(2) системным вызовом заполняются параметры 5 и 6. Адрес отправителя и размер структуры для его хранения. Так сервер узнает о существовании клиента и о наличии у него желания пообщаться. И в 33 строке идет подача данных в ответ.
объедки анализа
Наблюдательные дети наверное заметили, что клиент и сервер у UDP практически идентичны по набору используемых вызовов. Отличие всего в двух маленьких детальках, а дьявол всегда прячется в деталях.
Первая деталька: присваивание адреса и порта сокету: сервер явно указывает порт, клиент — саботирует. Ведь чтобы обратиться к серверу — за ним должен быть зафиксирован порт.
Вторая деталька: порядок вызовов recvfrom(2) и sendto(2). Клиент сначала отсылает, потом принимает, сервер, наоборот, принимает а затем отсылает. Клиент-сервер as is: клиент начинает, сервер реагирует на действия.
TCP и UDP — послесловие
Не исключено, что после разбора примеров из 2 и 3 части станут гораздо яснее тезисы, высказанные в 1 части цикла статей.
Теперь мы переходим к дальнейшему сокетному беспределу. В части 4 цикла будут рассмотрены модели организации серверов, с учетом множества клиентов. Многозадачность, синхронизация, неблокируемый ввод/вывод, мультиплексирование и т.д. Буквы изучили — теперь начинаем писать по слогам:).
mend0za.
Сетевые решения. Статья была опубликована в номере 02 за 2004 год в рубрике программирование