Guide to Network Сетевое программирование от Биджа
Скачать 1.34 Mb.
|
(length) B e n j a m i n H e y g u y s w Конечно же, длина хранится в Порядке Байтов Сети. В данном случае она имеет только один байт, так что это не имеет значения, но вообще-то говоря вы захотите хранить ваши целые в пакете в Порядке Байтов Сети) Посылая данные вы должны быть предусмотрительны и использовать команду, подобную выше чтобы знать, что все данные посланы, даже если пришлось использовать множество вызовов send(). Кроме того, принимая эти данные, вы должны сделать ещё кое-что. Вы должны предусматривать, что можете принять часть пакета (как “ 18 42 65 6E 6A ” из а 54 Beej's Guide to Network выше, но это всё, что мы приняли от этого вызова recv().) Нам нужно вызывать recv() снова и снова, пока не примем весь пакет полностью. Но как Нам известно общее количество байт, которое нужно принять для завершения пакета, поскольку это число пришито к пакету спереди. Также мы знаем, что максимальный размер пакета равен 1+8+128, или 137 байт ( мы так его определили. В действительности мы можем сделать пару вещей. Поскольку вызнаете, что каждый пакет начинается с длины, вы можете вызвать recv() и принять только длину пакета. Затем, зная длину пакета, вы можете вызвать её снова, указав точную длину оставшейся части пакета (возможно повторяя вызов, чтобы получить весь пакет. Преимущество этого метода в том, что вы можете иметь только один буфер длиной в один пакета недостаток в том, что вам придётся вызывать recv() по меньшей мере дважды. Другой вариант это указать recv(), что вы хотите принять пакет максимальной длины, подклеить то что пришло к хвосту буфера и, наконец, проверить, завершён ли пакет. Разумеется, вы можете получить кое-что из следующего пакета, так что вам нужно иметь для этого место. Вы должны объявить достаточно большой массив, чтобы вместить два пакета. Это ваш рабочий массив, в котором выбудете перестраивать пакеты по мере их появления. Каждый раз, приняв данные вы добавите их в рабочий буфер и проверите пакет на завершение. То есть, если количество байт в буфере больше или равно указанной в заголовке (+1, потому что длина в заголовке не содержите саму) Если число байт в буфере меньше 1, то, ясно, что пакет не завершён. Для этого случая вам нужно предусмотреть особую обработку, поскольку на то, что первый байт содержит правильную длину полагаться нельзя. Как только пакет завершён, можете делать с ним всё что вам угодно. Используйте и удалите из рабочего буфера. Ну что, в голове ещё шумит Ладно, вот второй из парочки ударов за один вызов recv() вы могли прочесть конец одного пакета и перейти наследующий. Это означает, что в рабочем буфере у вас один завершённый пакет и незавершённая часть следующего Чёрт побери. (Вот поэтому вы и сделали ваш рабочий буфер достаточно большим, чтобы вмещать два пакета - на случай если это произойдёт!) Поскольку вы теперь знаете из заголовка длину первого пакета и следите за количеством байт в буфере вы можете вычислить количество байт, принадлежащих второму (незавершённому) пакету. После обработки первого пакета вы можете удалить его из буфера и сдвинуть часть второго пакета в начало буфера, приготовив всё для следующего recv(). Некоторые из читателей заметят, что перемещение части пакета в начало рабочего буфера занимает время и этого не потребуется если написать программу с использованием закольцованного буфера. К несчастью для остальных из вас, обсуждение закольцованных буферов выходит за рамки этой статьи. Если вам до сих пор интересно, хватайте книгу по структуре данных и начинайте оттуда) Я никогда не говорил, что это легко. Ладно, я говорил, что это легко. И это так просто вам нужна практика и, естественно, я очень скоро приду к вам. Клянусь Эскалибуром! 7.6. Широковещательные пакеты -‐ Hello, world! До сих пор этот документ рассказывало том, как посылать данные от одного хоста другому. А я утверждаю, что, имея соответствующие полномочия, можно посылать данные многим хостам одновременно Используя UDP (только UDP, не TCP) в стандартном IPv4 это делается через механизм, называемый широковещанием. В IPv6 широковещание не поддерживается ивам нужно 55 Beej's Guide to Network прибегнуть к часто превосходящей технике мультивещания, которую, к прискорбию, я в этот раз обсуждать не буду. Но хватит о звёздноглазом будущем - мы застряли в 32- битном настоящем. Подождите Вы не можете просто выскочить и вещать вовсю ивановскую. Прежде чем посылать широковещательный пакет в сеть вам нужно установить сокету опцию SO_BROADCAST. Это как маленькая пластиковая крышечка, которую устанавливают на кнопку запуска баллистической ракеты Вот сколько мощи выдержите в своих руках Тем не менее, если серьёзно, опасность широковещательных пакетов в том, что каждая система, принявшая широковещательный пакет должна очистить шелуху слоёв инкапсуляции, чтобы добраться до порта, которому это предназначено. И затем применить или отбросить это. В любом случае, это много ненужной работы для каждой машины в локальной сети, а они все принимают широковещательные пакеты. Когда игра Doom впервые вышла в свет, было много жалобна её сетевой код. На свете существуют больше одного способа посылки широковещательных пакетов. Так что объединим картошку с мясом как вам указать адрес назначения для широковещательного послания Есть два общепринятых способа ! 1. Послать данные по широковещательному адресу отдельной подсети. Это сетевой номер подсети со всеми установленными битами номера хоста. Например, моя домашняя сеть имеет номер 192.168.1.0, моя сетевая маска 255.255.255.0, значит последний байт адреса это номер хоста (потому что первые три байта, соответственно маске, это номер сети. Так что мой широковещательный адрес 192.168.1.255. Под Unix, команда ifconfig действительно выдаствам все эти данные. (Выражение в двоичной логике - сетевой_номер OR (NOT сетевая_маска), если вам интересно) Вы можете послать этот тип широковещательного пакета ив удалённую сеть тоже, но при этом рискуете, что пакет будет отброшен маршрутизатором сети назначения.(Если он этого не сделает, то какой-нибудь случайный смёрфер может утопить его сеть в широковещательном трафике) ! 2. Послать данные по глобальному широковещательному адресу. Это 255.255.255.255 , aka INADDR_BROADCAST. Многие машины логическим И с вашим сетевым номером автоматически преобразуют его в сетевой широковещательный адреса некоторые нет. Это переменчиво. По иронии судьбы, маршрутизаторы не выпускают этот тип широковещательных пакетов за пределы вашей локальной сети. Что происходит, если вы пытаетесь послать данные по широковещательному адресу без установки опции INADDR_BROADCAST? Давайте запустим старые добрые и и посмотрим, что произойдёт. $ talker 192.168.1.2 foo* sent 3 bytes to 192.168.1.2* $ talker 192.168.1.255 foo* sendto: Permission denied* $ talker 255.255.255.255 foo* sendto: Permission Да, неудачно, и всё потому что мы не установили опцию SO_BROADCAST. Установите её и вызывайте sendto() где захотите В действительности, существует только одна разница между UDP приложениями, которые могут и не могут посылать широковещательные сообщения. Давайте возьмём 56 Beej's Guide to Network старое приложение и добавим участок, который устанавливает опцию SO_BROADCAST. Назовём её broadcaster.c : 37 /* ** broadcaster.c -- дейтаграммный клиент подобный talker.c, но ** этот может вещать */ ! #include #include #include #include #include #include #include #include #include #include ! #define SERVERPORT 4950 // порт для подключения пользователей ! int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // адресная информация подключившегося struct hostent *he; int numbytes; int broadcast = 1; ! //char broadcast = '1'; // если тоне работает, попробуйте так if (argc != 3) { fprintf(stderr,"usage: broadcaster hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // получить информацию хоста perror("gethostbyname"); exit(1); } ! if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } ! // этот вызов позволяет посылать широковещательные пакеты if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof broadcast) == -1) { perror("setsockopt (SO_BROADCAST)"); exit(1); } ! their_addr.sin_family = AF_INET; // порядок байтов хоста their_addr.sin_port = htons(SERVERPORT); // short, порядок байтов сети their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero); if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof their_addr)) == -1) { perror("sendto"); exit(1); 57 http://beej.us/guide/bgnet/examples/broadcaster.c 37 Beej's Guide to Network Programming } ! printf("sent %d bytes to %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); ! close(sockfd); ! return 0; } Какая разница между этой и нормальной ситуацией UDP клиент/сервер? Никакой За исключением того, что клиенту в данном случае разрешено посылать широковещательные сообщения) Так что, вперёд, запускайте старую UDP программу listener водном окне ив другом. Теперь вы можете сделать то, что ранее отказывало. $ broadcaster 192.168.1.2 foo* sent 3 bytes to 192.168.1.2* $ broadcaster 192.168.1.255 foo* sent 3 bytes to 192.168.1.255* $ broadcaster 255.255.255.255 foo* sent 3 bytes to Вы видите, listener отвечает, что он пакеты получает. (Если listener не отвечает, то может быть потому что он подключён к IPv6 адресу. Попробуйте изменить в listener.c AF_UNSPEC на AF_INET, чтобы включить IPv4.) Это нечто возбуждающее. Попробуйте запустить listener на соседней машине в той же сети, чтобы иметь две копии, по одной на каждой машине, и снова запустите broadcaster с вашим широковещательным адресом… Оба а получают пакет, даже если вы вызываете sendto() единожды Круто Если listener получает данные, которые вы посылаете непосредственно ему, и не получает по широковещательному адресу, то может быть на вашей машине установлен брандмауэр, который блокирует эти пакеты. (Да, спасибо вам, Пэт и Баппер, что вы раньше меня поняли почему мой пример не работает. Я сказал, что упомяну вас в этом руководстве, я сделал. Вот так) Опять же, будьте осторожны с широковещательными пакетами. Поскольку все машины в локальной сети будут вынуждены иметь дело с пакетами, независимо оттого, есть для них recvfrom() или нет, это может представлять собой значительную нагрузку для всей вычислительной сети. Определённо, их нужно использовать скупо и подобающим образом. 58 Beej's Guide to Network Programming 8. Общие вопросы Где взять заголовочные файлы Если на вашей системе их нетто возможно, они вам ненужны. Справьтесь в мануале по вашей платформе. Если выработаете на Windows, вам нужен только #include $ Это единственный способ определить, какой сокет ассоциирован с какой программой. :-) Как посмотреть таблицу маршрутизации Задайте команду route (в на большинстве овили команду netstat -r. Как запустить клиент и сервер если у меня только один компьютер Нужна ли сеть для написания сетевых программ К счастью, фактически все машины имеют закольцованное (loopback) сетевое устройство, которое сидит в ядре и претендует название сетевой карты. (Это интерфейс, именуемый как “lo” в таблице маршрутизации) Положим вы вошли в машину под именем “goat”. Запустите клиент водном окне и сервер в другом. Или запустите сервер в фоновом режиме (“server &”) и клиент в том же окне. В итоге с устройством вы можете задавать client goat или client localhost поскольку “localhost”, вероятно определён в вашем файле /etc/hosts) и у вас будет клиент, разговаривающий с сервером без сети Кратко, вносить изменения в программу, чтобы она работала на отдельной, не подключённой к сети машине, ненужно Ура Как мне узнать, что удалённая сторона закрыла соединение Вы узнаете, потому что recv() вернёт Как выполнять утилиту “ping”? Что такое ICMP? Где найти сведения по сырым сокетам и SOCK_RAW ? Ответы на все ваши вопросы по сырым сокетам есть в книгах W. Richard Stevens' UNIX Network Programming. Также посмотрите в подоглавлении в Stevens' UNIX Network Programming source code, доступной онлайн . 38 ! ! ! 59 http://www.unpbook.com/src.html 38 Beej's Guide to Network Как изменить или сократить таймаут вызова connect() ? Вмасто того, чтобы давать тот же ответ, что и W. Richard Stevens, я просто сошлюсь на в UNIX Network Programming source code . Суть в том, что вы создаёте дескрипор сокета вызовом socket(), делаете его не- блокируемым, вызываете connect() и, если всё идёт хорошо, connect() немедленно возвратит -1 и установит в EINPROGRESS. Затем вызваете select() с нужным таймаутом, передавая дескриптор в обоих массивах, чтения и записи. Если таймаут не сработал, значит вызов connect() завершён. В этом месте вам нужно использовать getsockopt() с опцией SO_ERROR чтобы получить возврат из функции connect(), который должен быть нулевым, если не было ошибки. В конце, возможно, вам захочется до начала передачи данных через сокет опять сделать его блокируемым. Заметьте, что в вашу программу была добавлена возможность делать что-либо ещё, пока она подключается. Например, вы можете установить таймаут маленьким, вроде 500 ms, обновлять индикатор на экране каждый таймаут и вызывать снова. Когда таймаут вызовов истечёт, скажем, 20 раз, выбудете знать, что пора отказаться отсоединения. Как я сказал, посмотрите у а исходный код совершенно превосходных примеров. Как писать для Windows? Первым делом удалите Windows и установите Linux или BSD. };-). Нет, действительно, посмотрите раздел по программированию для Windows во введении. Как писать для Solaris/SunOS? Когда я компилирую, то продолжаю получать ошибки компоновщика Ошибки компоновщика происходят потому что на платформе Sun библиотеки сокетов не подключаются автоматически. Посмотрите примеры в разделе по программированию для Solaris/SunOS во введении. Почему select() не ладит с сигналами Сигналы имеют склонность принуждать заблокированные системные вызовы возвращать -1, устанавливая в EINTR. Когда вы устанавливаете обработчик сигналов функцией sigaction(), вы можете установить флаг SA_RESTART, который предполагает перезапуск системного вызова после того как он был прерван. Естественно, это не всегда работает. Моё любимое решение привлекать оператор goto. Как вызнаете, это до бесконечности раздражает ваших профессоров, так что соглашайтесь select_restart: if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) { if (errno == EINTR) { // какой-то сигнал нас прервал, перезапуск goto select_restart; } // обработка настоящих ошибок perror("select"); } 60 http://www.unpbook.com/src.html 39 Beej's Guide to Network Уверен, применять goto в данном случае необходимости нет, вы можете использовать другие структуры. Но, по-моему, goto намного понятнее. Как применять таймаут при вызове recv() ? Используйте select()! Она позволяет вам указывать параметр таймаута для дескриторов сокетов, через которые вы намереваетесь читать. Или можете объединить все действия водной функции, как здесь #include #include #include #include ! int recvtimeout(int s, char *buf, int len, int timeout) { fd_set fds; int n; struct timeval tv; ! // подготовка массива файловых дескрипторов FD_ZERO(&fds); FD_SET(s, &fds); ! // подготовка struct timeval для таймаута tv.tv_sec = timeout; tv.tv_usec = 0; ! // ждём таймаут или данные n = select(s+1, &fds, NULL, NULL, &tv); if (n == 0) return -2; // таймаут! if (n == -1) return -1; // ошибка ! // это данные, так что нормальный recv() return recv(s, buf, len, 0); } // recvtimeout() извлекаем таймаут: n = recvtimeout(s, buf, sizeof buf, 10); // 10 секунд таймаут ! if (n == -1) { // ошибка случилась perror("recvtimeout"); } else if (n == -2) { // таймаут пришёл } else { // в буфере есть данные } Заметьте, что recvtimeout() в случае таймаута возвращает -2. Почему не ноль Потому что recv() возвращает 0, если удалённая сторона закрыла соединение. Так что это значение уже занято и я выбрал -2, как мой индикатор таймаута. ! ! 61 Beej's Guide to Network Как кодировать или сжимать данные перед посылкой через сокет? Простой способ кодирования - использовать SSL (secure sockets layer), но это выходит за рамки этого руководства. (Смотрите OpenSSL project .) Полагая, что вы хотите подключить или внедрить вашу собственную систему сжатия или кодирования данных, самое время подумать о том, что они пройдут через последовательность шагов между обоими концами. Каждый шаг как-то изменяет данные. ! 1. сервер читает данные из файла (или ещё откуда-то) 2. сервер кодирует/сжимает данные (это добавляете вы) 3. сервер посылает кодированные данные (send()) Теперь наоборот ! 1. клиент принимает кодированные данные (recv() ) 2. клиент декодирует/разворачивает данные (это добавляете вы) 3. клиент пишет данные в файл(или ещё куда-то) Если вы собираетесь и сжимать и кодировать, помните, что сначала сжимают. :-) Как только клиент совершит действия, обратные действиям сервера, данные будут в порядке ив итоге неважно, сколько шагов вы добавили. Так что при использовании моего кода вам достаточно найти место между чтением данных и посылкой их в сеть и вклеить некоторый код, выполняющий кодирование. Я постоянно вижу “ |