Главная страница
Навигация по странице:

  • 7.1. Блокировка Блокировка. Вы о ней слыхали - что это за чертовщина В двух словах, блокировка на жаргоне технарей это сон. Вы, наверное, заметили, что когда вы запускаете listener

  • FD_SET(int fd, fd_set *set);

  • 7.3. Обработка незавершённых send() Помните в прошлом разделе по send()

  • 7.4. Сериализация -­‐ Как упаковать данные

  • Guide to Network Сетевое программирование от Биджа


    Скачать 1.34 Mb.
    НазваниеGuide to Network Сетевое программирование от Биджа
    Дата02.05.2019
    Размер1.34 Mb.
    Формат файлаpdf
    Имя файлаbgnet_A4_rus.pdf
    ТипGuide
    #75918
    страница6 из 13
    1   2   3   4   5   6   7   8   9   ...   13

    talker вызывает connect() и задаёт адреса. С этого момента talker может посылать и принимать данные только с адреса, определённого ом. Поэтому вам ненужно использовать sendto() ивы можете просто пользоваться send() и recv().
    37

    Beej's Guide to Network Programming
    7. Немного продвинутая техника Эти приёмы не являются действительно продвинутыми, но они выходят за рамки уже пройденных более общих уровней. Действительно, если вы дошли так далеко, то можете считать себя весьма успешным в основах сетевого программирования Unix! Поздравляю Отныне мы отважно вступаем мир более эзотерических знаний, которые вам захочется узнать о сокетах! Налетайте
    7.1. Блокировка Блокировка. Вы о ней слыхали - что это за чертовщина В двух словах, блокировка на жаргоне технарей это сон. Вы, наверное, заметили, что когда вы запускаете listener, выше, он просто сидит там до появления пакета. Происходит так, что она вызывает
    recvfrom(), а данных нет, и recvfrom() сказано заблокироваться (в данном случае спать) пока не появятся данные. Множество функций блокируются. accept() блокируется. Все recv() функции блокируются. Они могут это делать потому что им разрешено. Когда вы создаёте сокет функцией socket(), ядро устанавливает его на блокировку. Если вы не хотите, чтобы сокет блокировался, вызовите fcntl():
    #include
    #include sockfd = socket(PF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); Установив сокет на не-блокировку, вы можете эффективно его опрашивать. Если вы попытаетесь читать из неблокируемого сокета, а данных нет, он, неблокируемый, вернёт
    -1 и установит errno в EWOULDBLOCK.
    Вообще-то говоря, этот тип опроса это плохая идея. Если вы запустите вашу программу в постоянном цикле опроса данных от сокета, она будет жрать процессорное время, как свинья помои. Более элегантное решение узнать есть ли данные на чтение дано в следующем разделе по select().
    7.2. Мультиплексирование синхронного ввода/вывода
    Эта функция это нечто странное, но очень полезное. Возьмём такую ситуацию вы сервер и хотите слушать входящие подключения и одновременно читать уже имеющиеся. Выскажете- без проблем, один accept() и парочка recv(). Не так быстро, приятель Что если вы заблокированы на вызове accept()? Как вы собираетесь в это же время принимать данные от recv()? Использовать неблокируемые сокеты!” Да никогда Вы жене хотите быть процессорной свиньёй. И что дальше
    select() даёт вам возможность следить за несколькими сокетами одновременно. Она скажет вам какие готовы для чтения, какие для записи, а какие возбудили исключение, если вы действительно хотите это знать. Как было сказано, в настоящее время select(), хоть и очень переносимый, но и один из самых медленных способов мониторинга сокетов. Возможная альтернатива libevent
    25
    38
    http://www.monkey.org/

    provos/libevent/
    25

    Beej's Guide to Network или что-либо подобное, объединяющее системо-зависимые вещи, связанные с получением нотификации сокетов. Без дальнейших хлопот я предлагаю запись select():
    #include
    #include
    #include
    !
    int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); Функция мониторит массивы файловых дескрипторов, в частности readfds,
    writefds и exceptfds. Если вы хотите знать можно ли читать со стандартного ввода и сокета sockfd, просто добавьте 0 ив массив readfds. Параметр numfds должен содержать значение наибольшего файлового дескриптора плюс один. В этом примере он должен быть установлен в sockfd+1, поскольку он заведомо больше стандартного ввода
    (0). Когда select() возвращает управление, readfds будет модифицирован чтобы отражать какой из выбранных файловых дескрипторов готов к чтению. Вы можете проверить их с помощью макроса FD_ISSET(). Перед тем как продолжить, я расскажу, как работать с этими массивами. Каждый массив имеет тип fd_set. С ним работают следующие макросы
    FD_SET(int fd, fd_set *set); Добавляет fd в set.
    FD_CLR(int fd, fd_set *set); Удаляет fd из set.
    FD_ISSET(int fd, fd_set *set); Возвращает true если fd есть в set.
    FD_ZERO(fd_set *set); Очищает set. Наконец, что странного в struct timeval? Хорошо, иногда вы не хотите вечно ждать когда кто-нибудь пришлёт вам какие-нибудь данные. Может вы хотите каждые 96 секунд печатать на терминале “Ещё работаю, хотя ничего не происходит. Эта структура с временем позволяет вам определить период таймаута. Если время истекло и
    select() не нашёл готового файлового дескриптора он возвращает управление ивы можете продолжить работу. struct timeval имеет следующие поля struct timeval { int tv_sec;
    // секунды int tv_usec;
    // микросекунды
    }; Просто установите в tv_sec число секунд ив число микросекунд ожидания. Да, это микросекунды, а не миллисекунды. В миллисекунде 1000 микросекунд, ив секунде 1000 миллисекунд. Таким образом в секунде 1 000 000 микросекунд. Почему она
    “usec”? Предполагается, что “u” выглядит как греческая буква μ (мю, которую мы используем для обозначения “микро”. Кроме того, когда функция select() возвращает управление, timeout может быть изменена чтобы показать сколько времени ещё осталось. Это зависит от вида вашей Unix. Ура У насесть таймер с микросекундным разрешением На это не рассчитывайте. Вероятно выбудете ждать некоторую часть стандартного времени квантования Unix, вне зависимости оттого, насколько малую величину вы записали в вашу struct timeval. Другие интересные вещи Если установите поля вашей struct timeval в 0,
    select() вернётся немедленно, опросив все файловые дескрипторы в ваших массивах. Если вы установите параметр timeout вона будет ждать пока первый файловый
    39

    Beej's Guide to Network дескриптор не станет готовым. Наконец, если вы не хотите ждать определённого массива дескрипторов, при вызове установите его в NULL. Следующий отрывок кода ждёт 2.5 секунды чтобы что-то появилось на стандартном вводе
    /*
    ** select.c -- пример select()
    */
    !
    #include
    #include
    #include
    #include
    !
    #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 ненужны
    } Если у вас терминал со строковой буферизацией, то нажимать нужно клавишу RETURN, иначе не дождётесь. Некоторые из вас могут подумать, что это великолепный способ ожидания данных от дейтаграммных сокетов, ивы правы - это может быть. Некоторые ы могут для этого использовать select(), а некоторые нет. Посмотрите, что скажут ваши man страницы поэтому поводу. Некоторые ы обновляют время в вашей struct timeval для обозначения времени, оставшегося до истечения таймаута, а некоторые нет. Не полагайтесь на это, если хотите писать переносимые программы. (Используйте gettimeofday() если вам надо отследить затраченное время. Это глупость, я знаю, но всё-таки выход) Что происходит когда сокет в массиве чтения закрывает соединение В этом случае
    select() возвращается с этим дескриптором сокета, как готовым для чтения. И когда вы действительно вызываете recv() с ним, recv() возвращает 0. Так вы узнаёте, что клиент закрыл соединение.
    40
    http://beej.us/guide/bgnet/examples/select.c
    26

    Beej's Guide to Network Programming
    Ещё одно интересное замечание о select(), если у вас есть сокет с запущенным
    listen(), вы можете проверить, есть ли новое подключение, установив файловый дескриптор сокета в массив readfds. Вот, друзья мои, быстрый обзор всемогущей функции select(). Но по требованию народа ниже приведён углублённый пример. К сожалению, разница между простым примером выше и приведённым здесь весьма значительна. Но всё равно взгляните и прочтите последующее описание. Эта программа работает как простой многопользовательский сервер чата. Запустите е водном окне и затем отправьте ей по у “telnet hostname 9034” из многих других окон. Когда вы печатаете что-нибудь водном окне, это должно появляться во всех остальных.
    /*
    ** selectserver.c -- убогий многопользовательский сервер чата
    */
    !
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    !
    #define PORT "9034"
    // этот порт мы слушаем
    !
    // получить sockaddr, IPv4 или IPv6: void *get_in_addr(struct sockaddr *sa)
    { if (sa->sa_family == AF_INET) { return &(((struct sockaddr_in*)sa)->sin_addr);
    }
    !
    return &(((struct sockaddr_in6*)sa)->sin6_addr);
    }
    !
    int main(void)
    { fd_set master;
    // главный список файловых дескрипторов fd_set read_fds;
    // временный список файловых дескрипторов для select() int fdmax;
    // максимальный номер файлового дескриптора
    !
    int listener;
    // дескриптор слушаемого сокета int newfd;
    // новопринятый дескриптор сокета struct sockaddr_storage remoteaddr; // адрес клиента socklen_t addrlen;
    !
    char buf[256];
    // буфер для данных клиента int nbytes;
    !
    char remoteIP[INET6_ADDRSTRLEN];
    !
    int yes=1;
    // для setsockopt() SO_REUSEADDR, ниже int i, j, rv;
    !
    41
    http://beej.us/guide/bgnet/examples/selectserver.c
    27

    Beej's Guide to Network Programming
    struct addrinfo hints, *ai, *p;
    !
    FD_ZERO(&master);
    // очистка главного и временного массивов
    FD_ZERO(&read_fds);
    !
    // получить сокет и связать memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) { fprintf(stderr, "selectserver: %s\n", gai_strerror(rv)); exit(1);
    }
    !
    for(p = ai; p != NULL; p = p->ai_next) { listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (listener < 0) { continue;
    }
    !
    // убрать мерзкое сообщение "address already in use" setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) { close(listener); continue;
    }
    !
    break;
    }
    !
    // если мы здесь, значит не связались if (p == NULL) { fprintf(stderr, "selectserver: failed to bind\n"); exit(2);
    }
    !
    freeaddrinfo(ai);
    // с этим закончили
    !
    // слушаем if (listen(listener, 10) == -1) { perror("listen"); exit(3);
    }
    !
    // добавить слушателя в главный массив
    FD_SET(listener, &master);
    !
    // сохранить наибольший файловый дескриптор fdmax = listener;
    // вот он
    !
    // главный цикл for(;;) { read_fds = master;
    // копируем if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(4);
    }
    !
    // ищем готовые для чтения данные в существующих подключениях for(i = 0; i <= fdmax; i++) {
    42

    Beej's Guide to Network Programming
    if (FD_ISSET(i, &read_fds)) {
    // Есть if (i == listener) {
    !
    // обрабатываем новые подключения addrlen = sizeof remoteaddr; newfd = accept(listener,
    (struct sockaddr *)&remoteaddr,
    &addrlen);
    !
    if (newfd == -1) { perror("accept");
    } else {
    FD_SET(newfd, &master);
    // добавить в главный массив if (newfd > fdmax) {
    // отслеживаем максимальный номер fdmax = newfd;
    } printf("selectserver: new connection from %s on "
    "socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET6_ADDRSTRLEN), newfd);
    }
    } else {
    // обработка данных от клиента if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) {
    // ошибка или соединение закрыто клиентом if (nbytes == 0) {
    // соединение закрыто printf("selectserver: socket %d hung up\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");
    }
    }
    }
    }
    }
    } // END обработка данных от клиента
    } // END есть новое входящее подключение
    } // END цикл по файловым дескрипторами выдумаете, что это не закончится
    !
    return 0;
    } Заметьте, что у меня два массива файловых дескрипторов master и read_fds. Первый, master, содержит как уже подключённые, таки прослушиваемые для новых подключений дескрипторы.
    43

    Beej's Guide to Network Я завёл массив master потому что select() реально изменяет переданный массив для отображения готовых к чтению сокетов. Поскольку мне нужно сохранять подключения от одного вызова select() до другого, в последний момент перед вызовом
    select() я копирую master в read_fds и затем вызываю select(). Означает ли это, что получив новое соединение я должен добавить его в массив
    master? Ага И каждый раз, когда соединение закрывается, мне нужно удалить его из массива master. Да, это так Заметьте, я проверяю сокет listener на готовность чтения. Когда он готов, это означает, что есть ожидающее подключение, я принимаю его ом и добавляю в массив master. Точно также, когда клиентское подключение готово к чтению и recv() возвращает 0, я знаю, что клиент закрыл подключение и я должен удалить его из массива
    master. Если recv() клиента всё таки возвращает не-ноль, я знаю, что были приняты какие-то данные. Я получаю их и по списку master раздаю остальным подключённым клиентам. Вот, друзья мои, наипростейший обзор всемогущей функции select(). В довесок, запоздалый бонус есть функция, именуемая poll(), которая ведёт себя почти также как select(), нос отличающейся системой управления массивом файловых дескрипторов. Посмотрите е
    7.3. Обработка незавершённых send() Помните в прошлом разделе по send() я сказал, что send() может не выслать всех указанных байт То есть, вы хотит послать 512 байта она возвращает число 412. Что случилось с оставшимися 100 байтами Они до сих пор в вашем маленьком буфере ждут отсылки. По независящим от вас обстоятельствам ядро решило не посылать их одним куском и теперь, мой друг, вам пора взять их оттуда. Для этого вы тоже можете написать функцию, подобную этой
    #include
    #include
    !
    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. Это будет число, указанное вами для
    44

    Beej's Guide to Network отсылки, если не было ошибки. sendall(), пыхтя и вздыхая, пошлёт данные наилучшим образом, но если случится ошибка, она просто вернётся к вам. Для полноты вот пример вызова функции char buf[10] = "Beej!"; int len;
    !
    len = strlen(buf); if (sendall(s, buf, &len) == -1) { perror("sendall"); printf("We only sent %d bytes because of the error!\n", len);
    } Что происходит когда часть пакета появляется на стороне приёмника? Если пакеты переменной длины, то откуда приёмник узнаёт, где кончается один пакет и начинается другой Да, сценарии реального мира - вещь беспокойная. Может быть вам нужно
    инкапсулировать (помните об этом говорилось вначале) Читайте дальше, там подробности
    7.4. Сериализация -­‐ Как упаковать данные
    Посылать посети текстовые данные достаточно легко, неправда ли, но что происходит если вы хотите послать некие двоичные данные, целочисленные или с плавающей запятой Оказывается, у вас есть несколько вариантов.
    !
    1. Преобразовать числа в текст функцией типа и послать текст.
    Приёмник преобразует текст обратно в цифру функцией типа strtol().
    2. Просто послать необработанные данные, передав указатель в send().
    3. Закодировать данные в переносимую двоичную форму. Приёмник их декодирует. Закрытый просмотр Только сегодня Занавес открывается
    Бидж говорит:”Я предпочитаю Метод Три, выше КОНЕЦ Прежде чем серьёзно начать этот раздел я должен сказать, что где-то существуют библиотеки, которые это делают. Но и упаковать, и оставить переносимыми безошибочным, это весьма серьёзно. Так что сходите на охоту, сделайте домашнее задание прежде чем решиться делать это самому. Для любопытных я включил информацию о том как такие штуки работают) В действительности все эти методы имеют свои достоинства и недостатки, но, как я сказал, в общем-то, я предпочитаю третий метод. Но всё таки сначала поговорим о достоинствах и недостатках двух других. Первый метод, кодирование чисел в текст перед посылкой выгоден тем, что легко можете распечатать и прочесть приходящие по проводам данные. Иногда читаемый протокол превосходен при использовании в не-широковещательно-интенсивной ситуации, как Internet Relay Chat (IRC) . Однако он невыгоден тем, что преобразование медленно и результат почти всегда занимает больше места, чем исходное число Метод два передача необработанных данных. Этот метод существенно легче (но опасней просто берёте указатель на посылаемые данные и вызываете с ним
    1   2   3   4   5   6   7   8   9   ...   13


    написать администратору сайта