Урок 45
LAN. ENC28J60. UDP Client
В предыдущем уроке мы познакомились с новым для нас протоколом транспортного уровня UDP и написали примитивный эхо-сервер. Сегодня мы поставим обратную задачу — написать клиент. Как мы уже знаем из нашей практики, что клиент писать зачастую сложнее, чем сервер, так как мы обращаемся по сетевому адресу, не зная при этом адреса физического и это накладывает определённые неудобства, так как нам нужно данный адрес сначала узнать, а потом уже слать на него какие-то пакеты. И, мало того, пакет приходится формировать полностью, что в сервере как правило не требуется, достаточно в пришедшем пакете заменить лишь некоторые поля в заголовках. Но клиент зато требуется чаще и нам от этого уйти не получится. А UDP-клиент тоже очень пользуется спросом. Впоследствии мы попробуем запросить из интернета текущее время с помощью такого клиента. Но там уже будет другая проблема. Мы пока работаем только в локальной сети, с глобальной будет немного по-другому. Но тем не менее всё это мы осилим.
А пока давайте создадим проект таким же образом, как и всегда. Проект будет называться ENC28J60_UDPC, а файлы будут использованы из проекта прошлого занятия ENC28J60_UDPS. Изменилась только одна буква, так как у нас теперь клиент. Процедуру подключения всех наших библиотек и создания проекта я уже не буду, так как это мы делать уже умеем прекрасно.
Итак, наш проект создан, библиотеки подключены, main.с имеет вид как и в прошлом проекте. Тогда попробуем его собрать и прошить и далее работаем с кодом.
Зайдём первым делом в usart.h и, во-первых увеличим немного строку для строки ввода из терминальной программы, так как в данную строку мы должны теперь будем писать ещё и порт, а он порой бывает даже пятисимвольный
typedef struct USART_prop{
uint8_t usart_buf[25];
Соответственно, в одноимённом c-файле тоже сделаем корректировку в функции обработки прерывания от USART
//если вдруг случайно превысим длину буфера
if (usartprop.usart_cnt>25)
UDP-пакеты мы будем посылать, как вы поняли, также из терминальной строки, только после IP-адреса мы через двоеточие (:) будем писать номер порта и заканчиваться строка будет уже не буквой a, а буквой u по понятным причинам.
В этой же функции добавим ещё одно условие
else if (b=='a')
{
usartprop.is_ip=1;//статус отправки ARP-запроса
net_cmd();
}
else if (b=='u')
{
usartprop.is_ip=2;//статус попытки отправить UDP-пакет
net_cmd();
}
else
Перейдём теперь в файл net.c и уберём там весь закомментированный код из всех функций, чтобы не мешался, так как он уже вряд ли нам потребуется.
Переходим в функцию net_cmd и пока удалим там весь вывод информации в USART
USART_TX(usartprop.usart_buf,usartprop.usart_cnt);
USART_TX((uint8_t*)"\r\n",2);
ip=ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);
sprintf(str1,"%lu",ip);
USART_TX((uint8_t*)str1,strlen(str1));
USART_TX((uint8_t*)"\r\n",2);
arp_request(ip);
Добавим здесь ещё одно условие
usartprop.usart_cnt=0;
}
else if(usartprop.is_ip==2)//статус попытки отправить UDP-пакет
{
ip=ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);
usartprop.is_ip=3;//статус отправки UDP-пакета
usartprop.usart_cnt=0;
arp_request(ip);//узнаем mac-адрес
}
После обработки строки с IP-адресом мы установим другой статус, также обнулим счётчик байтов USART, ну и отправим также ARP-запрос.
Теперь перейдём в функцию ip_extract и там также немного переработаем код, чтобы он чувствовал также и двоеточие. Добавим ещё переменную с этим символом, а также изменим немного имя другой переменной
int ch1='.';
int ch2=':';
Скорректируем имя также и в коде
ss1=strchr(ip_str,ch1);
После тела цикла for узнаем вхождение нашего символа
len-=offset;
}
ss1=strchr(ip_str,ch2);
strncpy(ss2,ip_str,len);
Далее с помощью условия узнаем, есть ли у нас двоеточие, извлечём в его теле строку с цифрами последнего байта IP-адреса, преобразуем её в число, привяжем к нашему IP-адресу и вернём её, а дальнейший код у нас выполняться уже не будет
ss1=strchr(ip_str,ch2);
if (ss1!=NULL)
{
offset=ss1-ip_str+1;
strncpy(ss2,ip_str,offset);
ss2[offset]=0;
ip |= ((uint32_t)atoi(ss2))<<24;
return ip;
}
strncpy(ss2,ip_str,len);
Давайте проверим работу данного кода. Соберём код, прошьём контроллер, запустим терминальную программу. а также утилиту WireShark, только давайте сегодня отфильтруем вывод уже не по IP, а по MAC. Делается это следующим образом
В терминальной программе попробуем ввести строку следующего содержания
Как мы видим, всё сработало и пакет ARP у нас также отправился и компьютер на него ответил. Также можно проверить, что у нас не перестал работать запрос ARP по-старому, без порта и с буквой a.
Также мы видим, что в утилите WireShark у нас также видны отправленные и принятые пакеты ARP
Теперь нам необходимо из строки извлечь ещё и порт.
Создадим для этого функцию, которая будет этим заниматься. Так как процедуры в теле функции аналогичны предыдущей, то сразу даю её со всем кодом
//--------------------------------------------------
uint16_t port_extract(char* ip_str, uint8_t len)
{
uint16_t port=0;
int ch1=':';
char *ss1;
uint8_t offset = 0;
ss1=strchr(ip_str,ch1);
offset=ss1-ip_str+1;
ip_str+=offset;
port = atoi(ip_str);
return port;
}
//--------------------------------------------------
Но вызывать мы данную функцию пока не будем, так как рано. А рано потому что все равно толку не будет никакого ибо мы пока находимся в обработчике приёма по USART и по определению мы ещё не приняли ответ ARP от узла и поэтому MAC-адрес мы его не знаем, да и мало того, возможно он у нас есть уже в ARP-таблице. Вот такая вот засада образовалась. Нам надо будет оперативно покинуть обработчик и ждать ответа, а потом вовремя в функцию net_cmd вернуться. Ну ничего, мы всё разрулим. Где наша не пропадала?
Ну, во-первых, чтобы вернувшись когда-то в нашу функцию, мы обнаружили там наши данные необнулёнными, так сказать не вернулись к разбитому корыту, для этого у нас есть два пути. Сделать переменную с этими данными глобальной, или приписать static. Пойдём по второму пути, так как глобальных переменных у нас и так достаточно. Пока у нас только ip, поэтому заодно добавим и вторую переменную
void net_cmd(void)
{
static uint32_t ip=0;
static uint16_t port=0;
Также подготовим здесь условие для следующего статуса, чтобы нам было куда вернуться
arp_request(ip);//узнаем mac-адрес
}
else if(usartprop.is_ip==3)//статус отправки UDP-пакета
{
usartprop.is_ip=0;
}
}
Теперь будем думать, откуда именно нам сюда возвращаться, так как перед нами есть условие — в этот момент мы обязательно должны знать mac-адрес.
Но прежде, чем мы этим займёмся, то мы должны обнаружить ещё одну засаду (а я ведь предупреждал, что клиент сложнее, так как лёгкой жизни я и не обещал, ну для того я и тут).
Вторая засада. В функции eth_send мы перед отправкой пакета узлу просто меняем местами MAC-адреса приёмника и источника. Но у нас не всегда адрес источника будет становиться адресом приёмника. Как раз отправка UDP-пакета от клиента именно этот случай. Нам ведь не приходило образцовых пакетов UDP и никакого источника у нас нет. Поэтому мы удалим первую строку из данной функции
void eth_send(enc28j60_frame_ptr *frame, uint16_t len)
{
memcpy(frame->addr_dest,frame->addr_src,6);
Но теперь перед нами встала ответственность за адрес приёмника во всех остальных случаях, которые мы использовали в предыдущих уроках, мы ведь его заранее не указывали, поэтому придётся это сделать сейчас. Ну это будет несложно. Мы строку, которую сейчас удалили, добавим в определённых местах перед вызовом нашей функции отправки пакета Ethernet. Ну или немного её изменим. Что ж, начнём искать эти места и, ясное дело, после каждой вставки строки в код будем проверять былую работоспособность, которую мы сейчас уничтожили. Но всё это конечно в благих целях.
Будем вспоминать всё сначала, как мы создавали наш этот проект и как он у нас развивался, какие мы отправляли сначала пакеты, какие потом.
Сначала мы отвечали на ARP-запрос. Поэтому зайдём в функцию arp_send в файле arp.c и добавим нашу строку перед отправкой пакета
memcpy(frame->addr_dest,frame->addr_src,6);
eth_send(frame,sizeof(arp_msg_ptr));
Ну, а как проверить работоспособность ответа ARP и остальных наших функций, я уж не буду показывать, надеюсь, все это помнят.
Затем мы отвечали на ICMP-запрос. В функции icmp_read также вставим нашу строку в подобающее место, потому что функцию ip_send нам такими вещами засорять никак нельзя, так как в неё нужно уже прийти с нужным адресом, ибо он будет везде формироваться по-разному, а нам ведь не хочется там плодить разные кейсы, так что придём туда уже подготовленными к отправке пакета
memcpy(frame->addr_dest,frame->addr_src,6);
ip_send(frame,len+sizeof(ip_pkt_ptr));
Далее мы посылали ARP-запрос. А происходило это в функции arp_request в файле arp.c. Поэтому перейдём в соответствующий файл и сначала также очистим его от закомментированного кода, а затем уже перейдём в функцию arp_request и … ничего там не будем трогать, так как там мы напрямую вызываем функцию enc28j60_packetSend, как говорится, без посредников.
Ну и также мы на прошлом занятии отправляли UDP-пакет в ответ на пришедший в качестве эхо-ответа. Это происходило в функции udp_reply в файле udp.c. Добавим код в данной функции
memcpy(frame->addr_dest,frame->addr_src,6);
ip_send(frame,len+sizeof(ip_pkt_ptr));
Ух ты! три раза написали одно и то же. Ну и ладно. Зато легко проделать. Надеюсь, все проверили работоспособность, которая теперь, как мы убедились у нас не пострадала.
Ну теперь начнём потихоньку выполнять нашу основную задачу: «Вернуться в функцию net_cmd из нужных мест и отправить оттуда UDP-пакет, вооружившись перед этим значением IP-адреса и номера порта приёмника.
Оказывается, есть такое местечко и прячется оно не очень далеко, а совсем рядом. Это функция eth_read. После вызова функции заполнении ARP-таблицы мы и осуществим возврат в нашу функцию net_cmd, но только при условии, что у нас подобающий статус
else if(res==2)
{
arp_table_fill(frame);
if(usartprop.is_ip==3)//статус отправки UDP-пакета
{
memcpy(frame->addr_dest,frame->addr_src,6);
net_cmd();
}
}
Также мы видим, что вернёмся мы не с пустыми руками, а уже с MAC-адресом.
Но этого мало. Это один случай. Это когда мы таблицу заполняли. А если у нас запись уже была? То тут другая фунция. А функция это arp_request в файле arp.c, в которую мы сейчас перейдём и, прежде чем вызовем нашу любимую net_cmd, мы тут внесём некоторые коррективы, поработав над ошибками. У нас используется сразу в двух циклах одна и та же переменная для счёта циклов, хотя один цикл у нас вложен в другой. Я до сих пор не понимаю, как вообще работал код. Исправим эту ситуацию, добавив сначала ещё одну переменную
uint8_t arp_request(uint32_t ip_addr)
{
uint8_t i,j;
Далее заменим в главном цикле переменную во всех местах (выделено, как обычно, жирным шрифтом)
//проверим, может такой адрес уже есть в таблице ARP, а заодно и удалим оттуда просроченные записи
for (j=0;j<5;j++)
{
//Если записи уже более 12 часов, то удалим её
if((clock_cnt-arp_rec[j].sec>43200))
{
memset(arp_rec+(sizeof(arp_record_ptr)*j),0,sizeof(arp_record_ptr));
}
if (arp_rec[j].ipaddr==ip_addr)
{
//смотрим ARP-таблицу
Перенесём код установки указателя на пакет в начало функции
uint8_t i,j;
enc28j60_frame_ptr *frame=(void*) net_buf;
Подключим в файле arp.c глобальную переменную структуры
uint8_t current_arp_index=0;
extern USART_prop_ptr usartprop;
Ну и теперь в необходимом месте в функции arp_request вызовем нашу net_cmd
USART_TX((uint8_t*)str1,strlen(str1));
}
memcpy(frame->addr_dest,arp_rec[j].mac_addr,6);
if(usartprop.is_ip==3)//статус отправки UDP-пакета
{
net_cmd();
}
return 0;
}
Перейдём в файл udp.с и создадим функцию для отправки UDP-пакета в самом верху после глобальных объявлений
//--------------------------------------------------
uint8_t udp_send(uint32_t ip_addr, uint16_t port)
{
uint8_t res=0;
return res;
}
//--------------------------------------------------
Напишем в заголовочном файле прототип на данную функцию и в файле net.c в функции net_cmd данную функцию вызовем, узнав перед этим номер порта
else if(usartprop.is_ip==3)//статус отправки UDP-пакета
{
port=port_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);
udp_send(ip,port);
usartprop.is_ip=0;
}
Ну и теперь у нас осталась самая главная работа — написать код функции udp_send, чем мы и займёмся, перейдя в неё в файле udp.c
А прежде, чем перейдём, добавим глобальный массив буфера
extern char str1[60];
extern uint8_t net_buf[ENC28J60_MAXFRAME];
//--------------------------------------------------
А вот теперь перейдём
Создадим переменную для длины и получим указатели на все необходимые пакеты
uint8_t res=0;
uint16_t len;
enc28j60_frame_ptr *frame=(void*) net_buf;
ip_pkt_ptr *ip_pkt = (void*)(frame->data);
udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);
В файле udp.h заведём переменную для постоянного локального порта нашего приложения в контроллере
#include "usart.h"
//--------------------------------------------------
#define LOCAL_PORT 333
//--------------------------------------------------
У вас может быть любое своё значение.
Вернёмся в нашу функцию отправки пакета UDP и заполним в заголовке поля портов источника и приёмника, помня о том, что эти значения в полях хранятся в формате big endian
udp_pkt_ptr *udp_pkt = (void*)(ip_pkt->data);
udp_pkt->port_dst = be16toword(port);
udp_pkt->port_src = be16toword(LOCAL_PORT);
Занесём какую-нибудь строку в поле данных и после узнаем длину пакета UDP
udp_pkt->port_src = be16toword(LOCAL_PORT);
strcpy((char*)udp_pkt->data,"UDP Reply:\r\nHello to UDP Client!!!\r\n");
len = strlen((char*)udp_pkt->data) + sizeof(udp_pkt_ptr);
Занесём длину в соответствующее поле заголовка
len = strlen((char*)udp_pkt->data) + sizeof(udp_pkt_ptr);
udp_pkt->len = be16toword(len);
Затем обнулим поле с контрольной суммой и, вызвав необходимую функцию, занесём туда новую контрольную сумму
udp_pkt->len = be16toword(len);
udp_pkt->cs=0;
udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);
С UDP-пакетом вроде всё. Теперь обернём его в заголовок IP, такой порядок, что поделать. Для этого заполним необходимые поля заголовка IP
udp_pkt->cs=checksum((uint8_t*)udp_pkt-8, len+8, 1);
ip_pkt->ipaddr_src = ip_addr;
ip_pkt->prt = IP_UDP;
ip_pkt->id = 0;
ip_pkt->ts = 0;
ip_pkt->verlen = 0x45;
Далее пакет IP заворачиваем в заголовок Ethernet, для чего нам надо заполнить его поля. MAC-адрес источника у нас уже занесен, а адрес приёмника заносится в функции отправки пакета Ethernet. Поэтому нам остаётся лишь заполнить тип пакета
ip_pkt->verlen = 0x45;
frame->type=ETH_IP;
Ну и, понятное дело, не забываем вызвать функцию отправки пакета
frame->type=ETH_IP;
ip_send(frame,len+sizeof(ip_pkt_ptr));
return res;
}
Соберём код, прошьём контроллер, и попробуем отправить пакет из терминальной программы, лучше 2 раза, так как первый раз функция вызывается в отдном месте, а второй раз — в другом
Убедимся, что наши пакеты пришли благополучно
Прекрасно, пакеты все дошли. Убедимся, также что они дошли полностью
Также можно убедиться ещё и в утилите netcat, отправив всё-таки там сначала UDP-пакет нашему контроллеру (я говорю контроллеру, потому что уже не модулю. Мы уже посылаем пакеты приложению, так как используем номер порта и транспортный протокол). Это нужно для того, чтобы узнать сначала порт, на который отправлять пакеты.
Пакет отправился, контроллер ответил. Узнаем в терминальной программе порт, на который мы будем отправлять пакет
И теперь отправим несколько раз пакет именно на этот порт
И пакеты пришли, правда почему-то не все, но это, видимо, какие-то издержки утилиты
Итак, мы сегодня научились посылать UDP-пакеты на определённый узел, используя определённый порт, таким образом создав UDP-клиент, хотя это оказалось и не так легко, как кажется на первый взгляд. Всё это говорит о растущей у нас квалификации, что подтверждает плоды предыдущих занятий, что они не прошли даром. В следующем занятии мы попытаемся уже отправить какие-нибудь пакеты не в локальную сеть, а уже в глобальную. Я думаю, и это у нас обязательно получится!
Предыдущий урок Программирование МК AVR Следующий урок
Приобрести плату Arduino UNO R3 можно здесь.
Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)