Это архив сайта coldflame.by.ru, он не обновлялся с 2007 года. Мой современный сайт тут: http://leonid.shevtsov.me.
Домой! Обо мне Специально для РИ-06-1 Разнообразное... барахло, короче :) Программы и прочее Статьи и переводы Блог SmartDaemon
Предыдущая ОглавлениеСледующая

6. Слегка продвинутые приемы

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

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


6.1. Блокирование

Блокирование. Ты слышал о нем - но что же это такое? По-простому, "блокировать" значит "заснуть". Ты, наверное, заметил, что когда ты запускаешь listener, он ждет, пока ему не пришлют пакет. На самом деле, при вызове recvfrom() не было входящих данных, и поэтому она "блокировала" (т.е. "заснула") до тех пор, пока не пришли данные.

Многие функции блокируют: accept(), все разновидности recv(). Они блокируют потому, что им разрешено. Когда ты открываешь дескриптор сокета с помощью socket(), ОС устанавливает его тип в "блокирующий". Если ты хочешь сделать его "неблокирующим", нужно вызвать fcntl():

#include <unistd.h>
#include <fcntl.h>
.
.
.
sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
. 

Ты можешь опрашивать неблокирующий сокет для получния информации. Если ты читаешь из неблокирующего сокета, когда нет входящих данных, он не может блокировать, и поэтому возвращает -1, при errno будет установлена в EWOULDBLOCK.

Вообще-то такой способ опроса плох. Если твоя программа только и делает, что спрашивает у сокета, есть ли данные, она будет съедать немеряно процессорного времени. Более элегантное решение описано в следующем разделе о select().


6.2. select() - Синхронное мультиплексирование ввода/вывода

Эта функция немного непонятная, но зато очень полезная. Например, если сервер должен одновременно принимать новые соединения и получать данные из уже существующих.

Не вопрос, скажешь ты, вызываем accept() и пару раз recv(). Не так быстро! Как же ты будешь принимать данные, если вызов accept() заблокировал и ждет соединения? Использовать неблокирующие сокеты? Так не пойдет. Мы не хотим съедать все процессорное время в системе. Что дальше?

select() дает тебе возможность следить за несколькими сокетами в одно и то же время. Она будет сообщать, какие сокеты готовы к приему, какие - к передаче, и какие сокеты вызвали исключения, если тебе и это интересно.

Синтаксис select():

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int numfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout); 

Итак, select следит за "наборами" дескрипторов: readfds, writefds, и exceptfds. Если ты хочешь проверять ввод из стандартного ввода и сокета sockfd, просто добавь дескрипторы 0 и sockfd в набор readfds. Параметр numfds должен быть установлен в наибольший номер дескриптора, плюс один (sockfd+1)

После вызова select() в readfds будет указано, какие дескрипторы из выбранных готовы к чтению. Для получения самих дескрипторов можно использовать макрос FD_ISSET(), читай дальше.

Наборы дескрипторов имеют тип fd_set. С ним можно работать через следующие макросы:

  • FD_ZERO(fd_set *set) - очищает набор
  • FD_SET(int fd, fd_set *set) - добавляет fd в набор
  • FD_CLR(int fd, fd_set *set) - удаляет fd из набора
  • FD_ISSET(int fd, fd_set *set) - проверяет, находится ли fd в наборе

Наконец, что это за struct timeval? Иногда нужно, чтобы ожидание прерывалось по истеканию какого-то времени. Может, ты хочешь выводить на экран "Работаю..." каждые 96 секунд, даже если никакое событие не произошло. Эта структура позволяет указать время ожидания. Если оно истекает, select() завершается, даже если не наблуюдалось никакой активности.

Поля struct timeval:

struct timeval {
    int tv_sec;     // секунды
    int tv_usec;    // микросекунды
}; 

Просто укажи в tv_sec количество секунд, а в tv_usec количество микросекунд. Да ,микросекунд, а не миллисекунд. В секунде 1,000 миллисекунд, а миллисекунде 1,000 микросекунд. То есть, в секунде 1,000,000 микросекунд. Почему "usec"? Буква "u" похожа на греческую букву μ (мю), которой обозначают "микро". После вызова, timeout может содержать оставшееся время. Это зависит от варианта Unix.

Класс! У есть нас таймер с разрешением в микросекунду! Что ж, не рассчитывай на него. Разрешение юниксового таймера около 100 миллисекунд, и этого таймера тоже.

Другие интересные вещи: если установить struct timeval в 0, то select() вернется сразу после опроса всех дескрипторов в списке. Если передать NULL вместо timeout'а, то функция будет ждать бесконечно. И, наконец, если тебе не нужен один из наборов, можно вместо него передать NULL.

Этот пример ждет стандартного ввода 2.5 секунд:

/*
** select.c -- пример select()
*/

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define STDIN 0  // дескриптор стандартного ввода

int main(void)
{
    struct timeval tv;
    fd_set readfds;

    tv.tv_sec = 2;
    tv.tv_usec = 500000;

    FD_ZERO(&readfds);
    FD_SET(STDIN, &readfds);

    // нам не нужны наборы writefds и exceptfds:
    select(STDIN+1, &readfds, NULL, NULL, &tv);

    if (FD_ISSET(STDIN, &readfds))
        printf("Была нажата клавиша!\n");
    else
        printf("Время истекло.\n");

    return 0;
} 

Если твой терминал использует построчный ввод, то событие произойдет после нажатия ENTER, а не первой клавиши.

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

Некоторые версии Unix сохраняют в struct timeval время, оставшееся до истечения ожидания. А другие нет. Не стоит надеяться на это, если заботишься о портабельности. (Используй gettimeofday() если надо узнать прошедшее время. Это неудобно, но другого способа нет.)

А что происходит если сокет, из которого ты хочешь читать, закрывает соединение? Тогда select() отмечает его как "готовый к чтению", а когда ты вызываешь recv(), она возвращает 0. Так ты узнаешь, что клиент закрыл соединение.

Если нужно узнать, есть ли новое соединение с сокетом, который слушает с помощью listen(), добавь его дескриптор в набор readfds .

Это был краткий обзор возможностей всемогущей функции select().

Но, по многочисленным просьбам, я написал более серьезный пример.

Эта программа - простой многопользовательский чат-сервер. Открой ее, а потом соединяйся telnet'ом ("telnet hostname 9034") - несколько раз. Когда ты напишешь что-нибудь в одном из окон telnet текст появится во всех остальных.

/*
** selectserver.c -- многопользовательский чат-сервер
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9034   // порт для соединений

int main(void)
{
    fd_set master;   // главный список дескрипторов
    fd_set read_fds; // временный список - для select()
    struct sockaddr_in myaddr;     // адрес сервера
    struct sockaddr_in remoteaddr; // адрес клиента
    int fdmax;        // максимальный номер дескриптора
    int listener;     // дескриптор слушающего сокета
    int newfd;        // принятый дескриптор - из accept()
    char buf[256];    // буфер для клиентских сообщений
    int nbytes;
    int yes=1;        // для setsockopt() SO_REUSEADDR, см. ниже
    socklen_t addrlen;
    int i, j;

    FD_ZERO(&master);    // очищаем наборы
    FD_ZERO(&read_fds);

    // get the listener
    if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // отделаться от сообщения "Адрес уже используется"
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
                                                        sizeof(int)) == -1) {
        perror("setsockopt");
        exit(1);
    }

    // bind
    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = INADDR_ANY;
    myaddr.sin_port = htons(PORT);
    memset(&(myaddr.sin_zero), '\0', 8);
    if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
        perror("bind");
        exit(1);
    }

    // слушаем
    if (listen(listener, 10) == -1) {
        perror("listen");
        exit(1);
    }

    // добавляем слушающий сокет в главный набор
    FD_SET(listener, &master);

    // сохраняем главный дескриптор файла как максимальный
    fdmax = listener; // пока

    // главный цикл
    for(;;) {
        read_fds = master; // копируем набор
        if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
            perror("select");
            exit(1);
        }

        // пробегаем по соединениям, ищем входящие данные
        for(i = 0; i <= fdmax; i++) {
            if (FD_ISSET(i, &read_fds)) { // нашли!
                if (i == listener) {
                    // обрабатываем новое соединение
                    addrlen = sizeof(remoteaddr);
                    if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
                                                             &addrlen)) == -1) { 
                        perror("accept");
                    } else {
                        FD_SET(newfd, &master); // добавляем в главный список
                        if (newfd > fdmax) {    // keep track of the maximum
                            fdmax = newfd;
                        }
                        printf("selectserver: новое соединение от %s на "
                            "сокете %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
                    }
                } else {
                    // обрабатываем входящие данные
                    if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
                        // ошибка или соединение закрыто
                        if (nbytes == 0) {
                            // соединение закрыто
                            printf("selectserver: сокет %d отключился\n", i);
                        } else {
                            perror("recv");
                        }
                        close(i); // пока!
                        FD_CLR(i, &master); // удаляем из главного списка
                    } else {
                        // получили данные от клиента
                        for(j = 0; j <= fdmax; j++) {
                            // рассылаем их всем остальным
                            if (FD_ISSET(j, &master)) {
                                // кроме слушающего сокета и отправителя
                                if (j != listener && j != i) {
                                    if (send(j, buf, nbytes, 0) == -1) {
                                        perror("send");
                                    }
                                }
                            }
                        }
                    }
                } // какой УЖАС!
            }
        }
    }
    
    return 0;
} 

Заметь, что в программе есть два набора дескрипторов: master и read_fds. Главный, master, хранит дескрипторы всех подключенных сокетов и слушающего сокета.

Набор master нужен потому, что select() изменяет набор, который ты передаешь. Но мне нужно помнить полный список сокетов, и поэтому я храню его отдельно. А для вызова select() я копирую master в read_fds, в который потом возвращается результат.

Но это значит, что каждое новое соединение нужно добавлять в набор master? Да! А когда соединение закрывается, его нужно удалять из этого набора? Да.

Обрати внимание, что я проверяю, когда сокет listener готов к приему. Это значит, что у него новое соединение, и я вызываю accept() и добавляю его в список master. А когда клиентское соединение готово к приему, а recv() возвращает 0, это значит, что клиент отсоединился и я удаляю его из списка master.

Если же recv() возвращает не ноль, то клиент прислал данные. Тогда я пробегаю по всему списку master и отылаю их всем остальным клиентам.

Это был не такой краткий обзор возможностей всемогущей функции select().


6.3. Пересылка по частям

В разделе о send() я писал, что send() может не послать все байты, которые ты ей передал? То есть, ты хотел послать 512, а она вернула только 412. Куда делись остальные 100 байт?

Они все еще в твоем буфере, ждут отправки. Из-за обстоятельств, над которыми мы не имеем власти, ОС решила не слать все данные одним куском, и теперь мы должны отослать их остаток.

Ты можешь написать функцию вроде следующей:

#include <sys/types.h>
#include <sys/socket.h>

int sendall(int s, char *buf, int *len)
{
    int total = 0;        // сколько байт мы послали
    int bytesleft = *len; // сколько байт еще надо послать
    int n;

    while(total < *len) {
        n = send(s, buf+total, bytesleft, 0);
        if (n == -1) { break; }
        total += n;
        bytesleft -= n;
    }

    *len = total; // сохраняем количество реально посланных байт

    return n==-1?-1:0; // возвращаем -1 при ошибке, 0 при успехе
} 

В этом примере, s - это сокет, buf - буфер с данными, а len - указатель на количество байт в буфере.

Функция возвращает -1 при ошибке (а errno хранит код ошибки - оставшийся от вызова send().) Количество реально отосланных байт возвращается в len. Оно должно быть равно тому количеству, которое нужно было отослать, если не произошла ошибка. sendall() сделает все возможное, чтобы отослать данные, но все равно не сможет ничем помочь при ошибке.

И пример вызова этой функции:

char buf[10] = "Beej!";
int len;

len = strlen(buf);
if (sendall(s, buf, &len) == -1) {
    perror("sendall");
    printf("Мы послали только %d байт из-за ошибки!\n", len);
} 

А что происходит, когда получатель получает только часть пакета? Если пакеты разной длины, как он узнает, где начало и конец пакета? Да, реальный мир предоставляет достаточно проблем даже для самых отважных! Помнишь о инкапсуляции (из раздела о инкапсуляции данных в самом начале руководства)? Читай дальше!


6.4. Применяем инкапсуляцию

А что вообще значит "инкапсулировать данные"? В простейшем случае, это значит прилепить к ним заголовок с какой-либо идентифицирующей информацией, длиной пакета, или и тем, и другим

Как должен выглядеть твой заголовок? Как кусок двоичных данных, который отражает все, что нужно для успешной передачи данных.

Вот это пространное определение.

Хорошо. Допустим, у тебя есть многопользовательский чат на основе SOCK_STREAM. Когда пользователь что-то вводит, тебе нужно передать на сервер две вещи: кто послал сообщение и что он послал.

Пока все нормально. "В чем проблема?", спросишь ты.

Проблема в том, что сообщения могут иметь произвольную длину. Один парень по имени "петя" может сказать "Тест", а другой парень, "Василий", может сказать "Эй, привет, как дела?"

И ты шлешь все эти данные клиентам. Исходящий с сервера поток данных может выглядеть как:

п е т я Т е с т В а с и л и й Э й , п р и в е т , к а к д е л а ?

И так далее. Как клиент узнает, где заканчивается одно сообщение и начинается другое. Конечно, можно сделать все сообщения одной длины, и потом пересылать их с помощью sendall(), описанной выше. Но это же отнимает кучу трафика! Мы не хотим отсылать 1024 байт только дла того, чтобы "петя" сказал "Тест".

Таким образом, мы инкапсулируем эти данные в пакет вместе с маленьким заголовком. И клиенты, и сервер знают, как упаковывать и распаковывать данные. И сейчас мы определим протокол, который описывает взаимодействие клиента и сервера!

В этом случае. предположим имя пользователя имеет фиксированную длину в 8 символов, или дополненное до десяти символов нулями '\0'. А данные пусть имеют переменную длину, но не больше 128 символов. Рассмотрим структуру пакета, которую можно использовать в такой ситуации:

  1. len (1 байт, без знака) - длина всего пакета, включая данные
  2. name (8 байт) - имя пользователя, дополненное нулями, если нужно.
  3. chatdata - (n байт) - сами данные, не больше 128 байт. Длина пакета равна длине данных плюс 8 (длина имени).

Почему я выбрал ограничения в 8 и 128 байт? Я их придумал сам, руководствуясь здравым смыслом. Возможно, 8 байт недостаточно для имени, и ты можешь выделить для него 30 байт или даже больше. Выбор за тобой.

Используя этот протокол, первый пакет может выглядеть как (в шестнадцатеричной и тексте):

    0C   EF E5 F2 FF 00 00 00 00 D2 E5 F1 F2
 (длина) п  е  т  я    (нули)    Т  е  с  т

А второй:

    2B   C2 E0 F1 E8 EB E8 E9 00 DD E9 2C 20 EF F0 E8 E2 E5 F2 2C 20 EA E0 EA 20 E4 E5 EB E0 3F
 (длина) В  а  с  и  л  и  й     Э  й  ,     п  р  и  в  е  т  ,     к  а  к     д  е  л  а  ? 

(Конечно, длина должна хранится в Сетевом порядке байт. В этом конкретном случае длина - всего один байт, так что это не важно, но вообще нужно передавать все числа в твоих пакетах в Сетевом порядке.)

Когда ты отсылаешь такой пакет, нужно использовать команду вроде sendall(), чтобы все данные были отправлены наверняка, даже если для этого нужно несколько раз вызывать send().

Аналогично, когда ты получаешь эти данные, нужно их обработать. Ведь ты можешь получить только часть пакета (например, только "2B C2 E0 F1 E8" от Василия, за один вызов recv()). Нужно вызывать recv() снова и снова, пока не будет получен весь пакет.

Но как? Мы знаем, сколько всего байт в пакете, поскольку длина указана в самом начале. Еще мы знаем, что максимальная длина пакета 1+8+128, то есть 137 байт (при нашем определении пакета.)

Нужно создать буфер, в котором поместятся два пакета. Это будет рабочий буфер, в котором ты будешь собирать пакеты по мере поступления.

Каждый раз, когда ты получаешь данные, ты отправляешь их в рабочий буфер, и проверяешь, собран ли пакет. То есть, проверяешь, что количество байт в буфере больше либо равно длине, указанной в заголовке (+1, так как длина пакета не включает сам байт длины.) Если буфере нет ни одного байта, очевидно, что пакет не готов. Это нужно обрабатывать отдельно, так как первый байт буфера не содержит длину пакета.

Когда пакет собран, можно его обрабатывать как тебе угодно. Обработай пакет и удали его из рабочего буфера.

Но могло случиться, что за один вызов recv() ты получил конец одного и начало другого пакета! Тогда в буфере содержится один целый пакет и еще кусочек. Хе-хе. Вот почему буфера должно хватать на два пакета!

Поскольку мы знаем длину первого пакета и число байт в буфере, можно вычесть первое из второго и узнать, сколько байт принадлежат ко второму, незаконченному пакету. Когда первый пакет обработан, нужно сместить второй в начало буфера, чтобы приготовить его к следующему вызову recv().

(Некоторые читатели заметят, что перемещение пакета в начало буфера занимает время, и без этого можно обойтись, используя циклический буфер. Увы, описание циклического буфера выходит за рамки этого руководства. Если тебе интересно, почитай книгу по структурам данных.)

Я не говорил, что будет просто. Ладно, я говорил, что будет просто. И это действительно несложно: если будешь упражняться, довольно скоро все поймешь. Клянусь Экскалибуром!


6.5. Широковещательные пакеты - Hello, World!

Пока что мы рассматривали передачу данных от одной машины к другой. Но возможно также пересылать данные сразу нескольким машинам!

Используя UDP (UDP, а не TCP) и стандарт IPv4, это делается с помощью механизма, который называется широковещание. Для IPv6 (который здесь не рассматривается... пока), широковещание не поддерживается, и приходится использовать более продвинутую технологию мультикастинга. Но не стоит заглядываться в будущее - пока что мы застряли в 32-битном настоящем.

Но нельзя просто так взять и начать широковещать ни с того ни с сего. Перед тем, как это сделать, нужно установить флаг сокета SO_BROADCAST. Это как предохранитель над кнопкой запуска ракеты! Вот сколько мощи у тебя в руках!

Говоря совершенно серьезно, использование широковещательных пакетов небезопасно. Всякая система, получившая пакет, должна развернуть все уровни инкапсуляции данных, чтобы узнать, на какой порт он послан, и передать его соответствующей программе - или отбросить. В любом случае, она проделывает немало работы, и поскольку через это проходят все машины в локальной сети, большинство, скорее всего, тратит время зря. Эта проблема была в сетевом коде первой версии игры Doom, и вызвала немало нареканий.

Да, я сказал в локальной сети. Как правило, широковещательные пакеты ограничиваются локальной сетью, как их не отсылать.

И вся соль вот в чем: как указать адрес назначения для широковещательного пакета? Есть два способа.

  1. Отослать данные на адрес широковещания. Он равен адресу сети, в котором все биты адреса машины установлены в 1. Например, адрес моей домашней сети 192.168.1.0, маска подсети 255.255.255.0, и адрес машины хранится в последнем байте(соответственно маске подсети). Так что мой адрес широковещания 192.168.1.255. Под Unix, команда ifconfig сообщит эти данные.
    (Если тебе интересно, формула адреса широковещания - адрес_сети ИЛИ (НЕ маска_подсети).)
  2. Отослать данные на "глобальный" адрес широковещания. Он равен 255.255.255.255, или INADDR_BROADCAST. Большинство машин автоматически преобразуют его в "локальный" адрес, логически умножив на адрес сети, но некоторые так не сделают. Всяко бывает.

Что же происходит когда ты отсылаешь данные на адрес широковещания, не установив флаг сокета SO_BROADCAST? Давай запустим пример talker и listener и посмотрим.

$ talker 192.168.1.2 foo
отослал 3 байт на 192.168.1.2
$ talker 192.168.1.255 foo
sendto: Permission denied
$ talker 255.255.255.255 foo
sendto: Permission denied

Да, все не так уж хорошо... а все потому, что мы не установили флаг SO_BROADCAST. Сделав это, можно слать куда угодно... и кого угодно!

Это единственное отличие широковещающего приложения UDP от не-широковещающего. Откроем программу talker и добавим SO_BROADCAST. Назовем новую программу broadcaster.c:

/*
** broadcaster.c -- датаграммный "клиент" как talker.c, который
**                  умеет широковещать
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#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);
    }

    // вот и отличие от talker.c:
    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); // сетевой порядок
    their_addr.sin_addr = *((struct in_addr *)he->h_addr);
    memset(&(their_addr.sin_zero), '\0', 8);  // обнуляем

    if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
        perror("sendto");
        exit(1);
    }

    printf("отослал %d байт на %s\n", numbytes, inet_ntoa(their_addr.sin_addr));

    close(sockfd);

    return 0;
}

Как эта ситуация отличается от "нормальной" ситуации UDP клиент-сервер? Никак! За тем исключением, что клиент может слать широковещательные пакеты. Так что можешь запустить listener в одном окне, broadcaster в другом, и предыдущий пример должен заработать

$ broadcaster 192.168.1.2 foo
отослал 3 байт на 192.168.1.2
$ broadcaster 192.168.1.255 foo
отослал 3 байт на 192.168.1.255
$ broadcaster 255.255.255.255 foo
отослал 3 байт на 255.255.255.255

А listener должен сообщать о получении этих пакетов.

Это уже интересно. Но запусти теперь listener на двух машинах в сети одновременно и снова broadcaster на адрес широковещания... Обе программы listener получили пакет, хотя ты отослал его только раз! Здорово!

Если listener получает пакеты, посланные на его адрес, но не широковещательные пакеты, их блокирует брандмауэр на локальной машине.

Будь осторожен с широковещательными пакетами. Каждая машина в локальной сети должна будет их обрабатывать, и это конкретно загружает всю сеть. Широковещание должно использоваться редко и в соответствующей ситуации.


Предыдущая ОглавлениеСледующая