Урок 46
LAN. ENC28J60. Удалённый доступ
До сих пор мы при помощи нашего модуля ENC28J60 могли соединяться, принимать и передавать данные только с узлов, находящихся в локальной сети. Я думаю, что настал тот момент, когда мы должны попробовать с помощью него «пообщаться» с сетью внешней — с сетью «Интернет». Также внешняя сеть может быть не только интернетом, но и какой-то другой сетью. Самое главное то, что мы будем уже обращаться к узлам, находящимся за пределами нашего маршрутизатора. Я думаю, все знают, каким образом происходит маршрутизация и опрос узлов, находящихся вне сети, ну или может узнать.. Данной информации очень предостаточно на просторах глобальной сети. А вот как соединиться с ними при помощи нашей микросхемы и как это организовать в коде — знают не все, Хотя информация все равно есть, но везде разная. Я хочу дать свой вариант ответа на этот вопрос.
Тем не менее, я немного все равно поведаю о том, как происходит опрос внешней сети. Например, мы назначили нашему модулю адрес IP 192.168.1.193, а подключен он в сеть с маской 255.255.255.0, у маршрутизатора адрес 192.168.1.1. В этом случае, если мы обратимся к устройству в нашей сети, имеющему адрес от 192.168.1.1 до адреса 192.168.1.254, то будет считаться, что мы обратились к локальному сетевому устройству и мы имеем полное право знать его MAC-адрес, узнать который мы можем, послав ARP-запрос. А если устройство будет иметь IP-адрес, отличный от вышеуказанных (например 8.143.111.23), то будет считаться, что мы уже обратились к устройству внешней сети и маршрутизатор будет обязан узнать маршрут к данному устройству и предоставить нам определённый к нему доступ. Только MAC-адрес внешнего устройства маршрутизатор нам сообщать уже не будет обязан, поэтому на канальном уровне мы будем работать уже с MAC-адресом маршрутизатора. Поэтому мы должны организовать наш код так, чтобы этот код сначала выяснил, что устройство находится за пределами нашей сети и уже после этого выяснения вернул нам MAC-адрес маршрутизатора, или как принято называть — шлюза или гейта (Gateway), Кратко вообщем вот так.
Конечная цель нашего сегодняшнего урока — послать ICMP-запрос или PING внешнему узлу и дождаться от него ответа, тем самым подтвердить, что мы можем с этим узлом нормально общаться на разных сетевых уровнях.
Поэтому давайте сразу же от теории перейдём к делу.
Создадим проект с именем ENC28J60_REMOTE, а файлы с исходным кодом все загрузим и подключим из проекта прошлого занятия с именем ENC28J60_UDPC.
Первым делом в файле net.h добавим две макроподстановки для маски подсети и адреса маршрутизатора (шлюза). У Вас они могут отличаться от моих. Думаю, свои сетевые параметры вы знаете, если не знаете, то вы их можете увидеть в свойствах ваших сетевых адаптеров. Только это будет иметь место в случае использования статической адресации, в случае использования динамической адресации (DHCP), можно всё это узнать, введя команду ipconfig в командной строке
Прочитав свои параметры, добавим макросы в вышеуказанное место
#define IP_ADDR ip_join(192,168,1,193)
#define IP_GATE ip_join(192,168,1,1)
#define IP_MASK ip_join(255,255,255,0)
Далее при попытке послать запрос ARP сетевому узлу мы обязаны определить, к какой сети данный узел принадлежит — к локальной или внешней (удалённой). Поэтому перейдём в файл arp.c и в функции arp_request сначала добавим переменную для хранения IP-адреса, по которому мы будем определять MAC-адрес с помощью ARP-запроса
uint8_t arp_request(uint32_t ip_addr)
{
uint8_t i, j;
uint32_t ip;
А затем добавим следующее условие
enc28j60_frame_ptr *frame=(void*) net_buf;
//проверим принадлежность адреса к локальной сети
if( ((ip_addr ^ IP_ADDR) & IP_MASK) == 0 ) ip=ip_addr;
else ip = IP_GATE;
То есть, если условие не подтвердится, то есть адрес будет не локальный, то мы будем просить MAC-адрес у маршрутизатора и все дальнейшие пакеты, предназначенные для узла мы будем отправлять на физический адрес маршрутизатора (или роутера).
Соответственно, в дальнейшем коде в теле функции мы исправим использование переменной на нашу новую
if (arp_rec[j].ipaddr==ip)
...
msg->ipaddr_dst = ip;
Давайте соберём код, прошьём контроллер и проверим наш код, послав в терминальной программе сначала запрос по IP-адресу сначала в локальную сеть, а затем в удалённую. Для удалённой сети будем использовать для примера IP-адрес сайта «www.yandex.ru«, узнать который мы можем, пропинговав его в командной строке
Вот что мы должны увидеть в терминальной программе после посылки ARP-запросов
Судя по отчёту в программе, мы всё сделали правильно и на внешний IP-адрес наш роутер нам вернул свой MAC-адрес.
Хорошо. Часть дела сделана. Теперь наша задача добиться того, чтобы не маршрутизатор, а внешний узел нам сам что-то вернул. Поэтому давайте попробуем его «пропинговать», послав ему ICMP-запрос, ну или несколько. Мы этого пока не делали, мы только отвечали на такие запросы, поэтому нам нужна будет отдельная функция, которую мы добавим в файле net.c после функции ip_read
//--------------------------------------------------
uint8_t icmp_request(uint32_t ip_addr)
{
uint8_t res=0;
return res;
}
//--------------------------------------------------
Также, чтобы контролировать работу данной функции в процессе написания в её тело кода, надо её где-то вызвать. Для этого мы в обработчик от USART в файле usart.c добавим ещё одно условие, с помощью которого мы будем посылать ICMP запросы. Ослеживаться они будут с помощью встретившегося посе строки с ip-адресом символа 'p' от слова «ping»
net_cmd();
}
else if (b=='p')
{
usartprop.is_ip=4;//статус попытки отправить ICMP-пакет
net_cmd();
}
Вернёмся в файл net.c, только уже в функцию net_cmd и по аналогии с посылкой запроса UDP, добавим посылку запроса ICMP
usartprop.is_ip=0;
}
else if(usartprop.is_ip==4)//статус попытки отправить ICMP-пакет
{
ip=ip_extract((char*)usartprop.usart_buf,usartprop.usart_cnt);
usartprop.is_ip=5;//статус отправки ICMP-пакета
usartprop.usart_cnt=0;
arp_request(ip);//узнаем mac-адрес
}
else if(usartprop.is_ip==5)//статус отправки ICMP-пакета
{
icmp_request(ip);
usartprop.is_ip=0;
}
}
Таким же образом, как обычно, мы сначала посылаем ARP-запрос, затем возвращаемся сюда со следующим статусом по факту ARP-ответа либо при обнаружении готовой пары адресов в таблице ARP. Поэтому мы должны также добавить новый статус в местах возврата в функцию net_cmd.
Сначала в функции eth_read, а затем в функции arp_request в файле arp.c
if((usartprop.is_ip==3)||(usartprop.is_ip==5))//статус отправки UDP- или ICMP-пакета
Ну и, возвратившись теперь не с пустыми руками в функцию net_cmd, мы смело посылаем запрос ICMP, вызвав соответствующую функцию, код тела которой мы сейчас и продолжим писать.
Но прежде чем продолжить, сначала добавим глобальную переменную для подсчёта отправленных ICMP-пакетов, так как для такой цифры существует отдельное поле в заголовке
char str1[60]={0};
uint32_t ping_cnt=0;//счетчик отправленных пингов
//--------------------------------------------------
Вернёмся в функцию icmp_request и сначала, как водится, создадим переменную для рассчёта длины пакета, а затем расставим указатели на заголовки пакетов в соответствующие им места
uint8_t res=0;
uint16_t len;
enc28j60_frame_ptr *frame=(void*) net_buf;
ip_pkt_ptr *ip_pkt = (void*)(frame->data);
icmp_pkt_ptr *icmp_pkt = (void*)ip_pkt->data;
Заполним ICMP-заголовок, занеся нужные значения в его поля, не забывая также инкрементировать номера пакетов, а также про строку данных, которая имеет определённый состав, который можно прочитать в утилите анализа сетевого трафика WireShark в составе любого пакета ICMP
icmp_pkt_ptr *icmp_pkt = (void*)ip_pkt->data;
//Заполним заголовок пакета ICMP
icmp_pkt->msg_tp = 8;
icmp_pkt->msg_cd = 0;
icmp_pkt->id = be16toword(1);
icmp_pkt->num = be16toword(ping_cnt);
ping_cnt++;
strcpy((char*)icmp_pkt->data,"abcdefghijklmnopqrstuvwabcdefghi");
icmp_pkt->cs = 0;
len = strlen((char*)icmp_pkt->data) + sizeof(icmp_pkt_ptr);
icmp_pkt->cs=checksum((void*)icmp_pkt,len,0);
Далее по иерархии заполняем заголовок IP
icmp_pkt->cs=checksum((void*)icmp_pkt,len,0);
//Заполним заголовок пакета IP
len+=sizeof(ip_pkt_ptr);
ip_pkt->len=be16toword(len);
ip_pkt->id = 0;
ip_pkt->ts = 0;
ip_pkt->verlen = 0x45;
ip_pkt->fl_frg_of=0;
ip_pkt->ttl=128;
ip_pkt->cs = 0;
ip_pkt->prt=IP_ICMP;
ip_pkt->ipaddr_dst = ip_addr;
ip_pkt->ipaddr_src = IP_ADDR;
ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);
И, по окончанию — заголовок пакета Ethernet, который затем отправляем в сеть
ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);
//Заполним заголовок пакета Ethernet
memcpy(frame->addr_src,macaddr,6);
frame->type=ETH_IP;
enc28j60_packetSend((void*)frame,len + sizeof(enc28j60_frame_ptr));
return res;
}
Думаю, с кодом всё ясно, мы уже не раз такой код разжёвывали. Ну оно и хорошо, теперь мы зато не по наслышке знаем о сетевых пакетах, об их формировании, считывании, составе их заголовков и способах отправки.
Соберём код, прошьём контроллер, и пока попробуем отправить пинг нашему компьютеру, так как если мы его отправим во внешнюю сеть, толку от этого не будет, так как мы не увидим ответа, а послав компьютеру, мы его увидим хотя бы в ути лите WireShark. Пошлём несколько запросов ICMP в терминальной программе
и посмтрим результат в утилите (нажмите на картинку для увеличения изображения)
Мы видим, что все наши пакеты пришли и на все на них компьютер отправил ответ.
Следующая задача — ответ этот увидеть в терминальной программе, ну и, конечно не только этот, а и ответы от удалённых узлов.
Для этого перейдём в функцию icmp_read и отделим два условия друг от друга в теле функции
//Отфильтруем пакет по длине и типу сообщения - эхо-запрос
if (len>=sizeof(icmp_pkt_ptr))
{
if (icmp_pkt->msg_tp==ICMP_REQ)
{
icmp_pkt->msg_tp=ICMP_REPLY;
Добавим теперь в нижнем уровне вложенности условий ещё одно условие, которое будет реагировать на другой тип пакета — на PING-ответ
USART_TX((uint8_t*)str1,strlen(str1));
}
else if (icmp_pkt->msg_tp==ICMP_REPLY)
{
sprintf(str1,"%ld.%ld.%ld.%ld-%ld.%ld.%ld.%ld icmp reply\r\n",
ip_pkt->ipaddr_src & 0x000000FF,(ip_pkt->ipaddr_src>>8) & 0x000000FF,
(ip_pkt->ipaddr_src>>16) & 0x000000FF, ip_pkt->ipaddr_src>>24,
ip_pkt->ipaddr_dst & 0x000000FF,(ip_pkt->ipaddr_dst>>8) & 0x000000FF,
(ip_pkt->ipaddr_dst>>16) & 0x000000FF, ip_pkt->ipaddr_dst>>24);
USART_TX((uint8_t*)str1,strlen(str1));
}
}
return res;
}
Вот теперь мы ответ должны будем увидеть в нашей терминальной программе.
Поэтому соберём наш код, прошьём наш контроллер и пошлём сначала несколько пингов локальному узлу, а затем несколько запросов — глобальному. Несколько потому, что мы должны убедиться, что это всё рабоатет постоянно и независимо от того, посылаем ли мы запрос ARP или извлекаем готовый физический адрес из таблицы. Мы должны будем увидеть вот такие ответы в окне терминальной программы
Как мы можем наблюдать, пакеты к нам поступают как с локальных IP-адресов, так и с глобальных.
Таким образом, в ходе нашего сегодняшнего занятия мы научились общаться по сети теперь не только с устройствами, находящимися в нашей локальной сети, но также и с устройствами и узлами, которые расположены уже удалённо — в сети глобальной, в нашем случае — Интернет.
Предыдущий урок Программирование МК AVR Следующий урок
Приобрести плату Arduino UNO R3 можно здесь.
Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий