Tiếp tục bài trước trong bài này, chúng ta sẽ tìm hiểu về giao thức UDP. UDP (User Datagram Protocol) là một trong những giao thức cốt lõi của giao thức TCP/IP. Dùng UDP, chương trình trên mạng máy tính có thể gửi những dữ liệu ngắn được gọi là datagram tới máy khác.
Giao thức UDP không cung cấp sự tin cậy và thứ tự truyền nhận mà TCP làm, các gói dữ liệu có thể đến không đúng thứ tự hoặc bị mất mà không có thông báo. Tuy nhiên UDP nhanh và hiệu quả hơn đối với các mục tiêu như kích thước nhỏ và yêu cầu khắt khe về thời gian.
Do bản chất không trạng thái của nó nên nó hữu dụng đối với việc trả lời các truy vấn nhỏ với số lượng lớn người yêu cầu.
Cổng
UDP dùng cổng để cho phép các giao tiếp giữa các ứng dụng diễn ra.
Cổng dùng 16 bit để đánh địa chỉ, vì vậy số của cổng nằm trong khoản 0 đến 65.535. Cổng 0 được để dành và không nên sử dụng.
Cổng từ 1 đến 1023 được gọi là cổng “well-known” và trên các hệ điều hành tựa Unix, việc gắn kết tới một trong những cổng này đòi hỏi quyền root.
Cổng 1024 đến 49.151 là cổng đã đăng ký.
Cổng từ 49.152 đến 65.535 là các cổng tạm, được dùng chủ yếu bởi client khi liên lạc với server.
Cấu trúc chung
Nhận dạng
UDP có mã Protocol (thuộc gói IP) là 0x11
Cấu trúc gói UDP
UDP là giao thức hướng thông điệp nhỏ nhất của tầng giao vận
UDP không đảm bảo cho các tầng phía trên thông điệp đã được gửi đi và người gửi cũng không có trạng thái thông điệp UDP một khi đã được gửi
Không có gì để giải thích, cấu trúc của các gói UDP rất đơn giản
Chúng ta sẽ bắt tay vào viết thư viện udp luôn. Tiếp tục tạo các fule udp.h và udp.c
Sao chép mã khởi tạo cho file .h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifndef UDP_H_ #define UDP_H_ //-------------------------------------------------- //Include cac thu vien can thiet #include <mega328p.h> #include <delay.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <stdio.h> #include <spi.h> #include "uart.h" #include "net.h" //------------------------------------------------- //-------------------------------------------------- #endif /* UDP_H_ */ |
Tiếp tục mã khởi tạo cho file .c
1 2 3 4 5 6 7 8 9 10 11 |
#include "udp.h" extern const uint8_t macaddr[6]; extern const uint8_t ip[4]; extern uint8_t debug_string[60]; //-------------------------------------------------- //end file |
Khởi tạo cấu trúc cho frame ở file .h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
typedef struct { uint8_t MAC_dich[6]; //--------------| uint8_t MAC_nguon[6]; // | => It is Ethernet Frame II uint16_t Ethernet_type; //--------------| uint8_t Header_length; //--------------| => IP uint8_t Services; // | uint16_t TotoLength; // | uint16_t Identification; // | uint16_t Flag; // | uint8_t TimeToLive; // | uint8_t Protocol; // | uint16_t CheckSum; // | uint8_t SourceIP[4]; // | uint8_t DestIP[4]; //--------------| uint16_t UDP_Source_Port;//---------------| //UDP uint16_t UDP_Dest_Port; // | uint16_t UDP_Length; // | uint16_t UDP_Checksum; // | uint8_t data[]; //---------------| }UDP_struct; |
Trong thư viện net.c tại hàm NET_ipRead chúng ta kiểm tra trường Protocol để xem có phải gói UDP, nếu đúng thì ném gói này cho thư viện udp.c xử lí
1 2 3 4 |
else if(IP_Frame[23] == 0x11) //it is UDP packet { UDP_read(IP_Frame,len); } |
(nhớ thêm nguyên mẫu hàm vào file udp.h và add thư viện udp.h vào file net.h)
Bây giờ mình sẽ tạo hàm UDP_read trong thư viện udp.c
1 2 3 4 5 6 7 8 9 |
void UDP_read(uint8_t *UDP_Frame,uint16_t len) { UDP_struct *UDP_Struct_Frame = (UDP_struct *)UDP_Frame; //kiem tra dia chi ip xem co phai no gui cho minh khong if( memcmp(UDP_Struct_Frame->DestIP,ip,4) )return; // dung memcmp de so sanh, neu khac thi thoat UART_putString("Da nhan 1 goi UDP\r\n"); } |
Mình sẽ chạy thử mô phỏng và cho phần mềm Hercules gửi thử các gói UDP đến ENC28J60 xem chúng ta đã nhận được gói UDP nào chưa
Toẹt vời ! Đã bắt được các gói UDP
Ngoài IP , UDP còn sử dụng thêm Port (cổng) để xác định nguồn và đích nhận dữ liệu ! Mình sẽ cho ENC28J60 cho phép nhận tin nhắn từ tất cả các cổng ! Do vậy chúng ta chỉ cần quan tâm Source Port ( với tin gửi đến ENC28J60 ) và Dest Port ( với tin mà ENC28J60 gửi đi)
Chúng ta sẽ đọc trường Length để xác định độ dài của tin nhắn gửi đến và in ra màn hình debug nhé.
Các bạn mở file net.h rồi thêm vào 2 #define này nhé
1 2 |
#define swap16(a) ((((a)>>8)&0xff)|(((a)<<8)&0xff00)) #define swap32(a) ((((a)>>24)&0xff)|(((a)>>8)&0xff00)|(((a)<<8)&0xff0000)|(((a)<<24)&0xff000000)) |
Do các biến kiểu 16 và 32 trong struct có byte thấp đứng trước byte cao nên mình #define thêm macro để đảo ngược nó lại
Tiếp tục với hàm UDP_read, mình sẽ in nội dung của tin nhắn ra
1 |
UART_putString(UDP_Struct_Frame->data); |
Mình sẽ vẽ thêm 4 con LED vào để test thử chức năng điều khiển LED nhé
Mình sẽ xài hàm strstr để check
1 2 3 4 5 6 7 8 9 10 11 |
if(strstr(UDP_Struct_Frame->data,"LED1=ON"))PORTC.0=0; else if(strstr(UDP_Struct_Frame->data,"LED1=OF"))PORTC.0=1; else if(strstr(UDP_Struct_Frame->data,"LED2=ON"))PORTC.1=0; else if(strstr(UDP_Struct_Frame->data,"LED2=OF"))PORTC.1=1; else if(strstr(UDP_Struct_Frame->data,"LED3=ON"))PORTC.2=0; else if(strstr(UDP_Struct_Frame->data,"LED3=OF"))PORTC.2=1; else if(strstr(UDP_Struct_Frame->data,"LED4=ON"))PORTC.3=0; else if(strstr(UDP_Struct_Frame->data,"LED4=OF"))PORTC.3=1; |
Và test gửi tin nhắn điều khiển bằng phần mềm Hercules
Xây dựng hàm gửi tin nhắn UDP
Để gửi gói tin UDP tới 1 địa chỉ IP, chúng ta cần biết MAC tương ứng của IP đó. Do vậy, mình sẽ check IP trong bản ARP_table, nếu có rồi thì lấy xài, nếu chưa có thì phát hành bản tin ARP_request để lấy IP của nó
Mình sẽ khai bảo 1 cổng mặc định để cho ENC28j60 gửi đi là cổng 20798. Các bạn thêm #define cho nó vào file udp.h
1 |
#define ENC28J60_UDP_PORT 20798 |
Trong thư viện arp.c mình sẽ viết thêm API để lấy MAC trong bảng
1 2 3 4 5 |
void ARP_table_get_MAC(uint8_t *ip_check,uint8_t * MAC_dest) { int i=ARP_table_checkIP(ip_check) - 1; memcpy(MAC_dest,ARP_table[i].mac,6); } |
Đừng quên thêm nguyên mẫu hàm cho nó vào file arp.h . Chúng ta sẽ bắt tay vào viết hàm UDP_send
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
uint8_t UDP_send(uint8_t *IP_dest,uint16_t port_dest,uint8_t *data,uint32_t timeout) { uint32_t count=0; uint16_t length_mess,frame_length; UDP_struct UDP_Struct_Frame; length_mess = strlen(data); while(1) { if(ARP_table_checkIP(IP_dest) != -1)break; // da nhan dc MAC ARP_send_request(IP_dest); delay_ms(50); count+=50; if(count >= timeout) { UART_putString("Chua gui goi tin udp\r\n"); return 0; //gui that bai } } //make Ethernet II ARP_table_get_MAC(IP_dest,UDP_Struct_Frame.MAC_dich); //mac cua thang nhan memcpy(UDP_Struct_Frame.MAC_nguon,macaddr,6); //mac cua enc28j60 UDP_Struct_Frame.Ethernet_type = 0x0008; // Type = 0x800 = IP //make IP packet UDP_Struct_Frame.Header_length = 0x45; UDP_Struct_Frame.Services=0x00; UDP_Struct_Frame.TotoLength=swap16(length_mess+8+20); UDP_Struct_Frame.Identification=0x2111; UDP_Struct_Frame.Flag=0x0000; UDP_Struct_Frame.TimeToLive=0x80; UDP_Struct_Frame.Protocol=0x11; //UDP UDP_Struct_Frame.CheckSum=0x0000; memcpy(UDP_Struct_Frame.SourceIP,ip,4); //ip cua enc28j60 memcpy(UDP_Struct_Frame.DestIP,IP_dest,4); //ip cua thang nhan //tinh checksum goi ip UDP_Struct_Frame.CheckSum=NET_ipchecksum((uint8_t *)&UDP_Struct_Frame.Header_length); //make UDP packet UDP_Struct_Frame.UDP_Source_Port = swap16(ENC28J60_UDP_PORT); UDP_Struct_Frame.UDP_Dest_Port = swap16(port_dest); UDP_Struct_Frame.UDP_Length = swap16(length_mess + 8); UDP_Struct_Frame.UDP_Checksum=0x0000; strcpy((char*)UDP_Struct_Frame.data,data); //copy data to struct //tinh checksum cho udp UDP_Struct_Frame.UDP_Checksum = UDP_checksum(&UDP_Struct_Frame); frame_length = swap16(UDP_Struct_Frame.UDP_Length) + 34; NET_SendFrame((uint8_t *)&UDP_Struct_Frame,frame_length); //gui goi tin udp return 1; //da gui } |
Giải thích: Để gửi các gói tin UDP đi, mình sẽ phát hành một boardcast (ARP request) tới địa chỉ IP nhận, chờ nó phản hồi để lấy MAC, nếu không thấy phản hồi (hết time oụt) thì không gửi. Sau khi có phản hồi MAC. Chúng ta make gói tin và gửi đi
Trong hàm gửi đi, mình có viết thêm 2 hàm tính checksum cho gói IP và gói UDP. Giao thức UDP không quan trọng checksum, nếu không cần checksum, các bạn có thể để checksum mặc định = 0x00, nhưng vì đang học nên chúng ta cứ viết hàm tính cho nó nhé
Tính checksum
Trước tiên là hàm checksum cho gói IP, các bạn quay trở lại file net.c để tạo thêm hàm checksum cho gói IP. Nội dung các hàm checksum nó ý chang nhau, chẳng qua địa chỉ bắt đầu và độ dài của mỗi loại checksum khác nhau 1 tí thôi, nên mình không giải thích lại hàm checksum nữa !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
uint16_t NET_ipchecksum(uint8_t *IP_packet_start) { uint32_t checksum=0; uint16_t length=20; while(length) //cong het cac byte16 lai { checksum += (uint16_t) (((uint32_t)*IP_packet_start<<8)|*(IP_packet_start+1)); IP_packet_start+=2; length-=2; } while (checksum>>16) checksum=(uint16_t)checksum+(checksum>>16); //nghich dao bit checksum=~checksum; //hoan vi byte thap byte cao return swap16(checksum); } |
Nhớ khai báo nguyên mẫu hàm vào file net.h nhé nhé
Tiếp tục viết thêm hàm checksum cho gói UDP ở file udp.c
Checksum của gói UDP gồm: IP Protocol (0x11) + IP source + IP dest + toàn bộ UDP packet + UDP packet length
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
uint16_t UDP_checksum(UDP_struct *UDP_Struct_Frame) { uint32_t checksum; uint8_t *ptr; uint16_t length; UDP_Struct_Frame->UDP_Checksum=0; //reset check sum length = swap16(UDP_Struct_Frame->UDP_Length) + 8; ptr = (uint8_t *)&UDP_Struct_Frame->SourceIP; checksum=0x11 + length-8; while(length>1) //cong het cac byte16 lai { checksum += (uint16_t) (((uint32_t)*ptr<<8)|*(ptr+1)); ptr+=2; length-=2; } if(length) checksum+=((uint32_t)*ptr)<<8; //neu con le 1 byte while (checksum>>16) checksum=(uint16_t)checksum+(checksum>>16); //nghich dao bit checksum=~checksum; //hoan vi byte thap byte cao return swap16(checksum); } |
OK rồi ! Giờ trong hàm nhận udp điều khiển LED mình sẽ phản hồi lại cho máy tính nhé
1 2 3 4 5 6 7 8 9 10 11 |
if(strstr(UDP_Struct_Frame->data,"LED1=ON")){PORTC.0=0;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da bat LED1\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED1=OF")){PORTC.0=1;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da tat LED1\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED2=ON")){PORTC.1=0;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da bat LED2\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED2=OF")){PORTC.1=1;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da tat LED2\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED3=ON")){PORTC.2=0;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da bat LED3\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED3=OF")){PORTC.2=1;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da tat LED3\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED4=ON")){PORTC.3=0;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da bat LED4\r\n",1000);} else if(strstr(UDP_Struct_Frame->data,"LED4=OF")){PORTC.3=1;UDP_send(UDP_Struct_Frame->SourceIP,swap16(UDP_Struct_Frame->UDP_Source_Port),"Da tat LED4\r\n",1000);} |
Các bạn có thể tải source cho bài này tại đây
Như vậy, chúng ta đã hoàn thành các giao thức cơ bản, nếu hiểu hết các bài học tới thời điểm này, các bạn hoàn toàn có thể tự nghiên cứu để viết thêm TCP – giao thức chính, cũng là giao thức chúng ta cần nhất. Thực tế, giao thức TCP khó hơn các giao thức chúng ta đã học rất nhiều. Trong thời gian tới, mình sẽ cố gắng làm thêm về TCP cho các bạn tham khảo !
Lộ trình sắp tới sẽ là TCP Server -> TCP Client -> Socket -> MQTT
Các bạn giúp mình chia sẻ rộng rãi tutorial để mình có động lực viết thêm, và nếu thấy khóa học giúp ích ít nhiều cho các bạn, các bạn có thể donate ít mì tôm cho mình tại thông tin ở cuối website ! Rất cảm ơn sự ủng hộ của các bạn !
Mình có theo dõi khóa học của bạn, mình có làm theo, gửi các gói ICMP, ARP thì đều ok, đến gói UDP và TCP thì mình gửi packet lên mạng LAN không được, đặc biệt là UDP, mình làm giao thức UDP lần đầu tiên thì gửi phản hồi “Da ON LED 1” lên máy tính được nhưng sang hôm sau ko làm gì mà lại không gửi được nữa, mình cũng ko hiểu tại sao, mọi thứ mình đều làm giống bạn, ko biết bạn có gặp trường hợp như mình không, xin hồi đáp ạ
Hi anh, em có theo dõi serie về ENC28J60 của anh ạ, em làm theo anh y hệt mọi thứ, nhưng đến phần UDP thì em nhận được packet từ máy tính gửi xuống nhưng không gửi phản hồi lại cho máy tính được, không biết anh có từng bị giống em chưa ạ và fix thế nào ạ, mong anh trả lời, tks anh
Bạn dùng wireshark bắt gói tin và kiểm tra lỗi đi
Em có dùng WireShark, nhưng WireShark không bắt được gói tin em gửi đi ạ, mọi thứ đều đúng hết anh, em làm theo y hệt anh, lúc gửi gói tin UDP em cũng in ra màn hình giá trị các trường thì cũng đều đúng hết, nhưng không hiểu sao lại ko gửi được
Bạn thử đổi ip của 28j60 trong proteus trùng với ip của máy tính thử xem có bắt được không. Mình nghĩ do mô phỏng nên lúc đầu proteus sẽ mượn phần cứng thật của máy tính để giả lập.
Khi chạy mô phỏng thì ip 28j60 mô phỏng sẽ nhận ip tĩnh mà mình thiết lập trong code.