TCP và UDP là gì: địa chỉ IP, cổng, kết nối và cách dùng trên ESP32
Giải thích TCP và UDP cho người làm vi điều khiển: địa chỉ IP, cổng, kết nối, xác nhận, vì sao TCP là dòng byte. Kèm hai chương trình ESP32: máy chủ TCP nhận lệnh và phát, nhận gói UDP trong mạng Wi-Fi.
Khi ESP32 nối Wi-Fi, mọi dữ liệu nó trao đổi với máy tính, điện thoại hay máy chủ đều đi qua một trong hai giao thức: TCP hoặc UDP. Trang web, MQTT và Modbus TCP dùng TCP; đồng bộ giờ qua NTP và việc dò tìm thiết bị trong mạng dùng UDP. Bài viết giải thích địa chỉ IP, cổng, sự khác nhau giữa TCP và UDP, một điểm hay gây lỗi của TCP, rồi viết hai chương trình ESP32: một máy chủ TCP nhận lệnh và một chương trình phát, nhận gói UDP.
Bài này là nền cho hai bài Modbus TCP/IP tiếp theo trong loạt.
1. TCP và UDP nằm ở đâu?
- Địa chỉ IP xác định một máy trong mạng, ví dụ
192.168.1.50. Router cấp địa chỉ này cho ESP32 khi nối Wi-Fi. - Cổng (port) là một số từ 0 đến 65535, xác định một chương trình trên máy đó. Một máy chạy được nhiều dịch vụ cùng lúc, mỗi dịch vụ một cổng.
- Một số cổng đã thành quy ước: 80 cho web, 1883 cho MQTT, 502 cho Modbus TCP, 123 cho NTP. Chương trình tự viết nên dùng cổng lớn hơn 1024; bài dùng 5000 và 5001.
- Máy chủ (server) mở một cổng và chờ. Máy khách (client) chủ động gửi tới địa chỉ IP và cổng của máy chủ.
2. TCP và UDP khác nhau thế nào?
- TCP thiết lập một kết nối trước khi gửi. Mỗi phần dữ liệu được bên nhận xác nhận; phần nào mất thì được gửi lại tự động. Dữ liệu tới nơi đủ và đúng thứ tự, hoặc kết nối báo lỗi.
- UDP không có kết nối. Mỗi lần gửi là một gói độc lập; gói có thể mất, tới trễ hoặc tới sai thứ tự mà bên gửi không biết.
- Đổi lại, UDP nhẹ và nhanh, gửi được cho mọi máy trong mạng cùng lúc (phát rộng), và mỗi gói giữ nguyên ranh giới của nó.
Lệnh điều khiển và dữ liệu không được phép mất thì dùng TCP. Dữ liệu gửi liên tục mà mất một gói cũng không sao, hoặc việc tìm thiết bị khi chưa biết địa chỉ IP, thì dùng UDP.
3. TCP là một dòng byte
- TCP bảo đảm đủ byte và đúng thứ tự, nhưng không giữ ranh giới giữa các lần gửi. Hai lệnh gửi liền nhau có thể được đọc ra trong một lần, hoặc một lệnh bị chia thành hai lần đọc.
- Chương trình nhận phải tự tách thông điệp: dùng một ký tự kết thúc như ký tự xuống dòng, hoặc gửi độ dài ở đầu mỗi thông điệp.
- Đây là lỗi rất hay gặp: chương trình chạy đúng khi thử từng lệnh, rồi sai khi lệnh tới dồn dập.
4. Chuẩn bị
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340); thêm một mạch thứ hai nếu muốn thử chương trình UDP giữa hai mạch.
- Mạng Wi-Fi 2,4 GHz và một máy tính hoặc điện thoại nối cùng mạng.
- Một chương trình TCP client trên máy tính hoặc điện thoại: bất kỳ phần mềm nào cho nhập địa chỉ IP, cổng và gõ chữ để gửi.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
Cách nối Wi-Fi, IP tĩnh và tự kết nối lại có trong bài kết nối Wi-Fi trên ESP32.
5. Chương trình 1: máy chủ TCP nhận lệnh
ESP32 mở cổng 5000 và chờ. Khi có máy khách kết nối, nó gom các ký tự tới thành từng dòng và thực hiện ba lệnh: led on, led off, trang thai.
/*
PYWORLD.VN - TCP trên ESP32: máy chủ TCP nhận từng dòng lệnh qua Wi-Fi và trả lời
ESP32 mở cổng 5000; máy tính hoặc điện thoại trong cùng mạng kết nối tới và gõ "led on", "led off", "trang thai"
TCP là một dòng byte liên tục, nên mỗi lệnh kết thúc bằng ký tự xuống dòng để máy chủ biết đâu là hết lệnh
*/
#include <WiFi.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
#define LED_PIN 2
const uint16_t CONG_TCP = 5000;
const size_t DO_DAI_TOI_DA = 80;
const unsigned long THOI_GIAN_IM_LANG_MS = 60000; // Máy khách im lặng lâu hơn mức này thì đóng kết nối
WiFiServer mayChu(CONG_TCP);
WiFiClient mayKhach;
String dongLenh;
unsigned long lucNhanCuoi = 0;
void xuLyLenh(const String &lenh) {
Serial.printf("Lenh: \"%s\"\n", lenh.c_str());
if (lenh == "led on") {
digitalWrite(LED_PIN, HIGH);
mayKhach.println("OK led dang BAT");
} else if (lenh == "led off") {
digitalWrite(LED_PIN, LOW);
mayKhach.println("OK led dang TAT");
} else if (lenh == "trang thai") {
mayKhach.printf("led %s, da chay %lu giay, RSSI %d dBm\n",
digitalRead(LED_PIN) ? "BAT" : "TAT", millis() / 1000, WiFi.RSSI());
} else {
mayKhach.println("Khong hieu lenh. Dung: led on | led off | trang thai");
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("\nDang ket noi WiFi %s", WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
mayChu.begin();
Serial.printf("\nMay chu TCP dang cho o %s cong %u\n", WiFi.localIP().toString().c_str(), CONG_TCP);
}
void loop() {
// Chưa có máy khách: xem có ai xin kết nối không
if (!mayKhach || !mayKhach.connected()) {
WiFiClient moi = mayChu.accept();
if (moi) {
mayKhach = moi;
dongLenh = "";
lucNhanCuoi = millis();
Serial.printf("May khach %s cong %u da ket noi\n", mayKhach.remoteIP().toString().c_str(), mayKhach.remotePort());
mayKhach.println("Xin chao tu ESP32. Go: led on | led off | trang thai");
}
return;
}
// Đang có máy khách: gom các byte tới thành từng dòng
while (mayKhach.available()) {
char kyTu = mayKhach.read();
lucNhanCuoi = millis();
if (kyTu == '\n') {
dongLenh.trim();
if (dongLenh.length() > 0) {
xuLyLenh(dongLenh);
}
dongLenh = "";
} else if (dongLenh.length() < DO_DAI_TOI_DA) {
dongLenh += kyTu;
}
}
if (millis() - lucNhanCuoi > THOI_GIAN_IM_LANG_MS) {
Serial.println("May khach im lang qua lau: dong ket noi");
mayKhach.stop();
}
}
mayChu.accept()trả về một máy khách mới nếu có ai vừa kết nối. Chương trình phục vụ một máy khách tại một thời điểm.- Máy khách im lặng quá 60 giây thì bị đóng kết nối, để một máy khách bỏ quên không chiếm chỗ mãi.
- Bất kỳ ai trong cùng mạng Wi-Fi cũng kết nối được tới cổng này. Không mở cổng này ra Internet khi chưa có biện pháp xác thực.
6. Chương trình 2: phát và nhận gói UDP
Mỗi 3 giây ESP32 phát một gói tới địa chỉ phát rộng của mạng, cổng 5001. Mọi máy trong mạng đang nghe cổng này đều nhận được. Khi nhận một gói thông báo từ máy khác, ESP32 gửi một gói trả lời về đúng địa chỉ và cổng của máy gửi.
/*
PYWORLD.VN - UDP trên ESP32: phát gói tin tới mọi máy trong mạng và trả lời gói tin nhận được
Mỗi 3 giây ESP32 phát một gói "PYWORLD;<số thứ tự>;<giây đã chạy>" tới địa chỉ phát rộng, cổng 5001
Nạp cho hai mạch cùng mạng Wi-Fi thì chúng thấy nhau; gói UDP không cần kết nối và có thể bị mất
*/
#include <WiFi.h>
#include <WiFiUdp.h>
const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";
const uint16_t CONG_UDP = 5001;
const unsigned long CHU_KY_PHAT_MS = 3000;
WiFiUDP udp;
uint32_t soThuTu = 0;
unsigned long lucPhatCuoi = 0;
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("\nDang ket noi WiFi %s", WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
udp.begin(CONG_UDP);
Serial.printf("\nIP %s, nghe UDP o cong %u, dia chi phat rong %s\n",
WiFi.localIP().toString().c_str(), CONG_UDP, WiFi.broadcastIP().toString().c_str());
}
void loop() {
// Phát định kỳ: không cần biết có ai đang nghe hay không
if (millis() - lucPhatCuoi >= CHU_KY_PHAT_MS) {
lucPhatCuoi = millis();
soThuTu++;
udp.beginPacket(WiFi.broadcastIP(), CONG_UDP);
udp.printf("PYWORLD;%u;%lu", (unsigned)soThuTu, millis() / 1000);
udp.endPacket();
Serial.printf("Da phat goi %u\n", (unsigned)soThuTu);
}
// Nhận: mỗi lần parsePacket() trả về đúng một gói trọn vẹn
int doDai = udp.parsePacket();
if (doDai > 0) {
char noiDung[101];
int soByte = udp.read(noiDung, sizeof(noiDung) - 1);
noiDung[soByte > 0 ? soByte : 0] = '\0';
if (udp.remoteIP() == WiFi.localIP()) {
return; // Bỏ qua gói do chính mình phát
}
Serial.printf("Nhan %d byte tu %s cong %u: %s\n", doDai, udp.remoteIP().toString().c_str(), udp.remotePort(), noiDung);
// Chỉ trả lời gói thông báo, không trả lời câu trả lời, để hai mạch không trả lời nhau mãi
if (strncmp(noiDung, "PYWORLD;", 8) == 0) {
udp.beginPacket(udp.remoteIP(), udp.remotePort());
udp.printf("DA NHAN;%s", WiFi.localIP().toString().c_str());
udp.endPacket();
}
}
}
Cách phát rộng này thường dùng để thiết bị tự giới thiệu: một ứng dụng trên điện thoại nghe cổng 5001 sẽ biết địa chỉ IP của mọi mạch ESP32 trong mạng mà không cần nhập tay.
7. Kết quả mong đợi
- Chương trình 1: sửa tên và mật khẩu Wi-Fi, nạp, mở Serial Monitor ở 115200 baud để xem địa chỉ IP. Dùng chương trình TCP client kết nối tới địa chỉ đó, cổng 5000, rồi gõ
led on: LED trên mạch sáng và có dòng trả lời. - Chương trình 2: nạp cho hai mạch cùng mạng Wi-Fi. Mỗi mạch in gói phát rộng của mạch kia và gói trả lời nhận được.
- Tắt một mạch: mạch còn lại vẫn phát bình thường và không báo lỗi gì. Đó là đặc điểm của UDP.
8. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không kết nối được tới cổng 5000 | Máy tính và ESP32 khác mạng, sai địa chỉ IP, hoặc router bật chế độ cách ly thiết bị | Nối cùng một mạng Wi-Fi; xem lại IP trên Serial Monitor; tắt chế độ cách ly (AP isolation) nếu có |
| Kết nối được nhưng gõ lệnh không có phản hồi | Chương trình client không gửi ký tự xuống dòng | Bật tùy chọn gửi kèm ký tự xuống dòng (LF) trong chương trình client |
| Lệnh bị dính nhau hoặc bị cắt | Xử lý theo từng lần đọc thay vì theo ký tự kết thúc | Gom ký tự cho tới khi gặp ký tự xuống dòng như trong chương trình |
| Địa chỉ IP của ESP32 đổi sau mỗi lần bật | Router cấp địa chỉ động | Đặt IP tĩnh trên ESP32 hoặc gán địa chỉ cố định trong router |
| Không nhận được gói UDP phát rộng | Router chặn phát rộng giữa các thiết bị Wi-Fi, hoặc hai bên dùng khác cổng | Thử gửi thẳng tới địa chỉ IP của mạch kia; kiểm tra số cổng |
| Thỉnh thoảng mất gói UDP | Bình thường với UDP, nhất là khi sóng Wi-Fi yếu | Thêm số thứ tự để phát hiện; dữ liệu quan trọng thì dùng TCP |
9. Xem thêm
- Kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh.
- ESP32 làm web server điều khiển relay: HTTP chạy trên TCP.
- Dùng MQTT với ESP32 và đồng hồ Internet dùng NTP, một ví dụ về UDP.
Xem thêm