Реализация UDP сервера-клиента в C
Существует два основных протокола транспортного уровня для связи между хостами: TCP и UDP . Создание TCP-сервера / клиента обсуждалось в предыдущем посте .
теория
В UDP клиент не устанавливает соединение с сервером, как в TCP, а просто отправляет дейтаграмму. Точно так же сервер не должен принимать соединение и просто ожидает прибытия дейтаграмм. Датаграммы по прибытии содержат адрес отправителя, который сервер использует для отправки данных нужному клиенту.
Весь процесс можно разбить на следующие этапы:
UDP-сервер:
- Создать UDP-сокет.
- Свяжите сокет с адресом сервера.
- Подождите, пока пакет дейтаграммы не будет получен от клиента.
- Обработайте пакет дейтаграммы и отправьте ответ клиенту.
- Вернитесь к шагу 3.
UDP-клиент:
- Создать UDP-сокет.
- Отправить сообщение на сервер.
- Подождите, пока ответ от сервера не будет получен.
- Обработайте ответ и при необходимости вернитесь к шагу 2.
- Закройте дескриптор сокета и выйдите.
Необходимые функции:
Аргументы:
домен — определяет связь
домен (AF_INET для IPv4 / AF_INET6 для IPv6)
тип — тип сокета, который будет создан
(SOCK_STREAM для TCP / SOCK_DGRAM для UDP)
протокол — протокол, который будет использоваться сокетом.
0 означает использовать протокол по умолчанию для семейства адресов.
Аргументы:
sockfd — дескриптор файла сокета, который будет связан
addr — структура, в которой указан адрес для привязки
addrlen — размер структуры адреса
Аргументы:
sockfd — файловый дескриптор сокета
buf — буфер приложения, содержащий данные для отправки
len — Размер буфера приложения buf
flags — Побитовое ИЛИ флагов для изменения поведения сокета
dest_addr — структура, содержащая адрес назначения
addrlen — размер структуры dest_addr
Аргументы:
sockfd — файловый дескриптор сокета
buf — буфер приложения, в который нужно получать данные
len — Размер буфера приложения buf
flags — Побитовое ИЛИ флагов для изменения поведения сокета
src_addr — возвращается структура, содержащая адрес источника
addrlen — переменная, в которой возвращается размер структуры src_addr
Аргументы:
fd — дескриптор файла
В приведенном ниже коде показан обмен одним приветственным сообщением между сервером и клиентом для демонстрации модели.
// Серверная реализация модели клиент-сервер UDP
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 8080
#define MAXLINE 1024
char *hello = «Hello from server» ;
struct sockaddr_in servaddr, cliaddr;
// Создание дескриптора файла сокета
if ( (sockfd = socket(AF_INET, SOCK_DGRAM, 0))
perror ( «socket creation failed» );
// Заполнение информации о сервере
servaddr.sin_family = AF_INET; // IPv4
// Привязываем сокет с адресом сервера
if ( bind(sockfd, ( const struct sockaddr *)&servaddr,
perror ( «bind failed» );
len = sizeof (cliaddr); // len is value / resuslt
n = recvfrom(sockfd, ( char *)buffer, MAXLINE,
MSG_WAITALL, ( struct sockaddr *) &cliaddr,
printf ( «Client : %s\n» , buffer);
sendto(sockfd, ( const char *)hello, strlen (hello),
MSG_CONFIRM, ( const struct sockaddr *) &cliaddr,
printf ( «Hello message sent.\n» );
// Клиентская реализация модели клиент-сервер UDP
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 8080
#define MAXLINE 1024
char *hello = «Hello from client» ;
struct sockaddr_in servaddr;
// Создание дескриптора файла сокета
if ( (sockfd = socket(AF_INET, SOCK_DGRAM, 0))
perror ( «socket creation failed» );
// Заполнение информации о сервере
sendto(sockfd, ( const char *)hello, strlen (hello),
MSG_CONFIRM, ( const struct sockaddr *) &servaddr,
printf ( «Hello message sent.\n» );
n = recvfrom(sockfd, ( char *)buffer, MAXLINE,
MSG_WAITALL, ( struct sockaddr *) &servaddr,
ОБОРУДОВАНИЕ
ТЕХНОЛОГИИ
РАЗРАБОТКИ
Блог технической поддержки моих разработок
Урок 69. Протокол UDP. Создание UDP-сервера и клиента с помощью библиотеки UIPEthernet.
В уроке научимся передавать данные, используя протокол UDP. Создадим несколько программ, реализующих функции сервера и клиента с UDP протоколом.
Наконец я возвращаюсь к основной на данный момент теме уроков – обмен данными между устройствами в системе Ардуино.
Скорее всего, дальнейшая последовательность уроков на основную тему будет продолжать хаотично прерываться уроками по совершенно другим вопросам. Иного выхода не вижу. Время не течет в обратную сторону, нельзя переписать историю, переставить объекты прошлого. Постараюсь написать подряд несколько уроков об обмене данными, хотя уже накопились очень интересные другие темы.
Протокол UDP.
UDP – это простой протокол передачи данных без предварительной организации соединения абонентов. Протокол ориентирован на передачу датаграмм – отдельных небольших пакетов данных. Датаграммы передаются в одном направлении без проверки готовности приемного устройства и без подтверждения доставки.
Примерно так мы передаем в сотовых сетях короткие сообщения (SMS). Если еще отключить подтверждение о доставке, то совсем похоже получится.
В протоколе TCP:
- происходит соединение;
- затем передаются данные;
- данные проверяются с помощью контрольных кодов;
- если необходимо, данные автоматически повторяются;
- соединение разрывается.
Пользователю остается только передавать данные, все остальное делает библиотека реализации протокола.
В отличие от TCP, протокол UDP не гарантирует ничего. Он не гарантирует, что данные будут доставлены, что придут правильные данные, что пакеты будут доставляться в той же последовательности, как при передаче. Контрольным кодом защищен только заголовок UDP-пакета. Поэтому, единственное, что гарантирует протокол, это то, что данные не попадут по неправильному адресу.
Пользователю необходимо самому побеспокоиться о целостности информации, о повторной передачи данных, о склеивании пакетов и т.п.
Но зато передача UDP-данных происходит значительно быстрее, чем через TCP. Именно поэтому протокол UDP предпочитают многие приложения.
К тому же во многих задачах вполне допустимо искажение или потеря части информации. Например, при просмотре фильма лучше потерять несколько кадров, чем ждать, когда они будут заново переданы.
Мы выполняем задачи на микроконтроллерах невысокой производительности. Поэтому для нас особенно важно, что реализация протокола UDP требует меньше вычислительных ресурсов, чем TCP.
Лично я предпочитаю именно UDP. Все можно сделать оптимально своей задаче. Не надо ждать соединений, минимальная нагрузка на контроллер и т.п.
Я не буду рассказывать о формате данных протокола. Нам это не надо. Пакеты данных при передаче и приеме будет формировать библиотека. Но необходимо твердо понимать, что:
- UDP – протокол без установления предварительных соединений;
- его задача – доставлять отдельные пакеты данных (датаграммы) между IP адресами;
- в сети Ethernet максимальный размер датаграммы 1500 байт;
- протокол не гарантирует, что данные будут доставлены;
- данные могут быть искажены при передаче, средств определения такой ситуации нет;
- протокол не сообщит отправителю, доставлены ли данные и не повторит передачу пакета;
- пакеты данных могут быть доставлены не в той последовательности, как при передаче;
- у UDP протокола высокая скорость передачи данных, он требует для реализации минимальных вычислительных ресурсов.
Библиотека для реализации обмена по UDP-протоколу UIPEthernet.
В предыдущем уроке по теме обмена информацией я рассказывал о библиотеке UIPEthernet и мы использовали ее для реализации TCP протокола. Я даже создал небольшой справочник по функциям UIPEthernet.
Я повторю ссылки на справочную информацию по функциям библиотеки для реализации UDP протокола. Все остальное будет ясно на примерах.
Класс EthernetUDP – поддерживает UDP протокол. | ||
begin() | beginPacket() | stop() |
read() | endPacket() | remoteIP() |
peek() | parsePacket() | remotePort() |
write() | available() | flush() |
Я использую эту версию библиотеки UIPEthernet-2.0.6.
Общая постановка задачи.
Остальная часть урока будет построена аналогично уроку 64. Программы будут выполнять те же функции, только с использованием UDP.
Повторю формализованную задачу.
- У нас есть плата Ардуино с подключенным к ней модулем ENC28J60, т.е. сетевое Ардуино-устройство или локальный контроллер.
- Образована локальная сеть Ethernet из контроллера и компьютера. Попросту говоря, Ардуино-устройство через роутер подключено к компьютеру.
- Задача состоит в том, чтобы от компьютера предать данные Ардуино-устройству и считать данные из Ардуино в компьютер.
Все аппаратные средства, схемы, подключения совершенно такие же, как в уроке 64.
Для отладки и проверки будем использовать мои программы верхнего уровня и программу TCP/IP Builder 1.9
Мы уже пользовались ею в уроке 64.
UDP сервер.
При использовании TCP-протокола мы создавали программные объекты отдельно для сервера и клиента.
EthernetServer server(2000); // создаем сервер, порт 2000
EthernetClient client; // создаем клиента
В UDP-протоколе мы просто передаем данные с одного IP-адреса на другой. Поэтому программно клиент и сервер не выделяются. Часто такие программы называют UDP-приемник и передатчик.
Сервер или клиент определяются по признакам более высокого уровня: кто предоставляет услуги, кто постоянно включен, кто инициирует соединение. А с точки зрения программной реализации обмена клиент и сервер — равнозначные устройства. Вы это прочувствуете ниже.
- Первый вариант UDP-сервера выполняет следующие действия:
- Проверяет, пришел ли новый пакет от клиента.
- Выводит в последовательный порт сообщение Server address: с IP-адресом сервера.
- Передает принятые данные обратно клиенту.
Загрузить скетч можно по ссылке:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
// UDP сервер, возвращает клиенту полученные данные,
// выводит их в последовательный порт
#define SERV_PORT 2000 // порт сервера
char receivingBuffer[100]; // приемный буфер Udp-пакета
EthernetUDP udp; // создаем экземпляр Udp
void setup() <
Ethernet.begin(mac, ip); // инициализируем контроллер
udp.begin(SERV_PORT); // включаем прослушивание порта
Serial.begin(9600);
Serial.print(«Server address:»);
Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
>
void loop() <
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) <
// есть пакет Udp, выводим информацию о пакете
Serial.print(«Received packet from «);
IPAddress ipFrom = udp.remoteIP();
Serial.println(ipFrom);
Serial.print(«Size «);
Serial.print(size);
Serial.print(«, port «);
Serial.println(udp.remotePort());
// чтение Udp-пакета и передача в последовательный порт
udp.read(receivingBuffer, size);
receivingBuffer[size]=0;
Serial.println(«——————-«);
Serial.println(receivingBuffer);
Serial.println();
В бесконечном цикле loop() проверяется, пришел ли новый пакет:
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) <
// есть пакет Udp
Если есть новый пакет, то выводится информация о нем в последовательный порт и данные считываются в буфер.
Затем данные выводятся в последовательный порт и передаются клиенту UDP-пакетом:
// ответ клиенту
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.write(receivingBuffer);
udp.endPacket();
Для проверки надо загрузить скетч и открыть монитор последовательного порта. В окне монитора появится сообщение
Адрес сервера задается в строчке
Роль клиента будет выполнять компьютер.
- Запустите программу TCP/IP Builder.
- В строке Local IP уже должен быть адрес компьютера. Надо добавить порт, для программы урока 2000. Хотя, после того, как я заменил роутер, программа перестала правильно определять адрес сетевой платы компьютера. Раньше был 192.168.1.2, а с новым роутером стал 192.168.1.100. Лучше посмотрите сетевой адрес вашего компьютера. Как это сделать написано в уроке 62.
- Выберите UDP.
- Нажмите кнопку Create Socket (Создать соккет).
- В строке IP укажите IP адрес локального контроллера и порт (те, что в плате Ардуино).
- Нажмите Connect (Подключиться).
- В результате на компьютере появится клиент.
Теперь можно посылать данные из окна Send data на сервер. Полученные от сервера данные будут появляться в окне Receive data.
Заметьте, что кнопка Connect (Соединение) для UDP не активна. В UDP-обмене не бывает соединений.
Я передал сообщение на сервер и получил его в ответ.
Оно же появилось в мониторе последовательного порта с данными об отправителе.
Для работы с UDP-устройствами я написал свою программу. Назвал ее UDP client, хотя она выполняет и роль сервера. Загрузить можно по ссылке:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
Программа позволяет посылать UDP-пакеты по IP-адресу и принимать UDP-датаграммы.
- Поле IP адрес это IP-адрес по которому будет послано сообщение. (Для этой программы адрес сервера.)
- Удаленный порт – порт получателя. (Порт сервера).
- Локальный порт- порт приема сообщений. (Порт клиента.)
- IP-адрес получателя (клиента) формируется автоматически. Это сетевой адрес компьютера. Его можно посмотреть, нажав кнопку Конфигурация сети.
- Большое окно слева для приема сообщений (от сервера).
- Поле ниже для посылаемых сообщений (от клиента серверу).
- Кнопка Послать собственно инициирует посылку данных (на сервер).
- Кнопкой конфигурация сети можно узнать сетевой адрес компьютера (клиента).
Заметьте, что локальный порт можно задавать любой. Программа сервера выведет его в мониторе порта и перешлет клиенту обратно данные именно на этот порт. Удаленный порт может быть только тот, который задан в программе сервера (2000).
Проверка такая же: послать на сервер сообщение и получить ответ.
Если используется роутер с WiFi, то можно проверить работу сервера с мобильным Андроид устройством. Я установил программу UDP terminal, она первая попалась в Google Play Market.
Задал адреса и порты.
Послал сообщение на сервер. Получил ответ.
Сообщение с параметрами отправителя появилось и в мониторе последовательного порта.
Давайте, по аналогии с уроком 64 создадим UDP-сервер, который:
- Под управлением клиента включает и выключает светодиод, подключенный к выводу 2 платы Ардуино;
- В качестве ответа клиенту передает время, прошедшее с начала запуска программы Ардуино (включения платы).
Вот скетч такого сервера:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
// UDP сервер, управляет светодиодом, передает клиенту время
#define SERV_PORT 2000 // порт сервера
char receivingBuffer[100]; // приемный буфер Udp-пакета
EthernetUDP udp; // создаем экземпляр Udp
void setup() <
Ethernet.begin(mac, ip); // инициализируем контроллер
udp.begin(SERV_PORT); // включаем прослушивание порта
pinMode(2, OUTPUT); // вывод светодиода
>
void loop() <
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) <
// есть новый пакет
udp.read(receivingBuffer, size); // чтение Udp-пакета
// управление светодиодом
if(receivingBuffer[0] == ‘0’) digitalWrite(2, LOW); // если пришел 0, гасим светодиод
if(receivingBuffer[0] == ‘1’) digitalWrite(2, HIGH); // если пришла 1, зажигаем светодиод
receivingBuffer[0]=0;
// посылка клиенту в ответ времени
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.println(millis());
udp.endPacket();
>
>
Логика работы программы:
- Если от клиента приходит символ 0 светодиод гасится.
- Если поступает символ 1 светодиод зажигается.
- При приходе от клиента любого символа в ответ передается время работы программы.
Проверим его работу с помощью программы UDP client.
При посылке 1 светодиод загорается, при 0 – гасится.
В качестве примера я написал специализированную программу клиента для управления этим сервером.
Светодиод гасится и зажигается ”птичкой”, время выводится в ”человеческом” виде.
Программу можно загрузить по ссылке:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
DHCP сервер.
В предыдущих программах урока мы задавали IP адрес сервера явно. Применяли статические IP адреса. Все современные сети имеют возможность автоматического распределения адресов. Сервер, использующий этот способ назначения IP адресов, называется DHCP сервером. Об этом написано в уроке 62. Такой сервер мы создавали в уроке 64.
Для получения динамического IP-адреса достаточно при инициализации контроллера указать только один аргумент – mac адрес.
Чтобы первую программу урока перевести на использование динамических IP-адресов необходимо заменить в ней блок setup() на такой:
void setup() <
Serial.begin(9600);
Serial.println(«Getting IP address using DHCP»);
if (Ethernet.begin(mac) == 0) <
Serial.println(«Failed to configure using DHCP»);
while(true) ; // зависаем по ошибке
>
Serial.print(«Server address:»);
Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
udp.begin(SERV_PORT); // включаем прослушивание порта
>
Полностью программу с DHCP UDP-сервером можно загрузить по ссылке:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
Проверка отличается от предыдущей методики только тем, что до запуска сервера мы не знаем какой адрес указать при обращении к нему. Неизвестно какой адрес будет ему присвоен.
Загружаем программу в плату Ардуино. В мониторе последовательного порта появляется сообщение.
Указанный адрес сервера заносим в программу клиента и проверяем, как проходят сообщения.
У меня все работает.
UDP клиент.
Выше я писал, что с программной точки зрения в UDP-протоколе невозможно выделить сервер и клиент. Скорее мы разработаем приемо-передатчик UDP-сообщений. Он будет:
- Данные, которые передаются монитором последовательного порта посылать серверу.
- Выводить в последовательный порт сообщение о передаче с параметрами получателя.
- Данные, поступающие от сервера выводить в монитор последовательного порта с параметрами передатчика.
Попросту говоря, сообщения с монитора последовательного порта он будет передавать серверу, а сообщения с сервера выводить в монитор порта.
Вот ссылка для загрузки скетча:
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
В цикле loop() два программных блока.
- принимает данные с UART в буфер;
- ждет пока встретится символ с кодом 10 (перевод строки);
- выводит параметры приемника в последовательный порт.
// собираем данные из UART в пакет
if( Serial.available() > 0 ) <
transmitBuffer[ii] = Serial.read();
if( transmitBuffer[ii] == 10 ) <
transmitBuffer[ii+1]= 0;
ii=0;
// передача UDP пакета
udp.beginPacket(ipServ, SERV_PORT);
udp.write(transmitBuffer);
udp.endPacket();
// сообщение в последовательный порт
Serial.print(«UDP-packet transmission to «);
Serial.print(udp.remoteIP());
Serial.print(» Port: «);
Serial.println(udp.remotePort());
>
else <
ii++;
if(ii >= 100) ii=0;
>
>
- проверяет есть ли новый пакет UDP-данных;
- выводит в последовательный порт сообщение с параметрами передатчика.
// проверка есть ли новые UDP-пакеты
int size = udp.parsePacket(); // считываем размер принятого пакета
if (size) <
// есть пакет Udp, выводим информацию о пакете
Serial.print(«Received packet from «);
IPAddress ipFrom = udp.remoteIP();
Serial.println(ipFrom);
Serial.print(«Size «);
Serial.print(size);
Serial.print(«, port «);
Serial.println(udp.remotePort());
// чтение Udp-пакета и передача в последовательный порт
udp.read(receivingBuffer, size);
receivingBuffer[size]=0;
Serial.println(«——————-«);
Serial.println(receivingBuffer);
Serial.println();
>
Проверка клиента с помощью разных программ.
Загружаем sketch_69_4 в плату Ардуино.
Запускаем мою программу UDP client.
Набираем сообщение в мониторе последовательного порта, нажимаем Отправить.
Видим в мониторе сообщение о посылке.
В программе UDP client появляется сообщение.
Набираем сообщение в UDP client. Нажимаем кнопку Послать.
Видим, что сообщение появилось в окне монитора.
Обмениваемся нескольким сообщениями.
Проверяем с помощью TCP/IP Builder.
Теперь мобильное Андроид-устройство и программа UDP terminal.
Это все. Не знаю кому как, а мне UDP-протокол для управления небольшим системами кажется намного предпочтительнее TCP.
В следующем уроке будем создавать веб-сервер.