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?

Sơ đồ bốn lớp mạng: lớp ứng dụng gồm HTTP, MQTT, Modbus TCP, NTP; lớp vận chuyển gồm TCP và UDP; lớp mạng là IP; lớp liên kết là Wi-Fi và Ethernet; kèm bảng cổng 80, 1883, 502, 123 và giải thích địa chỉ IP với cổng
Các giao thức ứng dụng chạy trên TCP hoặc UDP; TCP và UDP chạy trên IP.
  • Đị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?

So sánh TCP và UDP: TCP bắt tay ba bước, mỗi dữ liệu được xác nhận và gói mất được gửi lại; UDP gửi từng gói không xác nhận, gói mất thì mất luôn; kèm bảng so sánh năm tiêu chí
TCP có kết nối và xác nhận; UDP gửi là xong (hình vẽ minh họa).
  • 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

Minh họa TCP là dòng byte: bên gửi gọi hai lần gửi led on và led off, bên nhận có thể đọc được đúng từng lệnh, hai lệnh dính nhau, hoặc một lệnh bị cắt đôi
Hai lần gửi có thể tới nơi dính liền hoặc bị cắt ở chỗ bất kỳ.
  • 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

Hình minh họa Serial Monitor của hai chương trình TCP và UDP trên ESP32: địa chỉ IP và cổng của máy chủ TCP, máy khách kết nối, các lệnh nhận được; và các gói UDP phát đi, nhận về kèm địa chỉ máy gửi
Kết quả mong đợi trên Serial Monitor (hình minh họa; địa chỉ IP và số cổng là ví dụ).
  1. 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.
  2. 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.
  3. 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ượngNguyên nhân thường gặpCách xử lý
Không kết nối được tới cổng 5000Má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ồiChương trình client không gửi ký tự xuống dòngBậ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ắtXử lý theo từng lần đọc thay vì theo ký tự kết thúcGom 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ậtRouter 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ộngRouter 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ổngThử 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 UDPBình thường với UDP, nhất là khi sóng Wi-Fi yếuThêm số thứ tự để phát hiện; dữ liệu quan trọng thì dùng TCP

9. Xem thêm


Tin tức liên quan

Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32
Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32

Giải thích Bluetooth Low Energy: quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, UUID. Hướng dẫn cho ESP32 làm thiết bị BLE có đặc tính đọc, ghi, thông báo và thử bằng điện thoại.

Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32
Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32

Giải thích điều khiển hồng ngoại: sóng mang 38 kHz, mắt thu và giao thức NEC với khung 32 bit, mã lặp. Hướng dẫn tự giải mã phím điều khiển và tự phát lệnh bằng ESP32, không dùng thư viện ngoài.

RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32
RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32

Giải thích sóng RF 433 MHz và điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung và khung tin 24 bit. Hướng dẫn tự viết chương trình phát và thu trên hai mạch ESP32, không dùng thư viện ngoài.

Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32
Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32

Giải thích vòng dòng 4-20 mA: vì sao dùng dòng điện, vì sao bắt đầu từ 4 mA, cách đổi ra giá trị đo và ngưỡng báo lỗi. Hướng dẫn đọc bằng ESP32 qua điện trở shunt 120 Ω, với ADC có sẵn và với ADS1115.

CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32
CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32

Phần 2 về CAN bus với ESP32: đặt bộ lọc nhận theo mã lọc và mặt nạ, chọn tốc độ bit theo chiều dài dây, hiểu ba trạng thái lỗi của nút và viết chương trình theo dõi bộ đếm lỗi, tự hồi phục sau bus-off.

CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32
CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32

Giải thích CAN bus: hai dây CAN_H, CAN_L, mức trội và mức lặn, khung tin có ID 11 bit, phân xử ưu tiên, CRC và ACK. Kèm hai chương trình ESP32 dùng bộ điều khiển TWAI: tự kiểm tra và hai nút gửi nhận.

Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU
Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU

Hướng dẫn dùng ESP32 làm máy khách Modbus TCP đọc thanh ghi của một thiết bị trong mạng, và làm cổng chuyển đổi Modbus TCP sang Modbus RTU qua RS-485: cách đổi khung, mã thiết bị, thời gian chờ, mã lỗi 0x0B.

Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi
Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi

Giải thích Modbus TCP/IP: phần đầu MBAP, cổng 502, điểm khác với Modbus RTU. Kèm chương trình ESP32 làm máy chủ Modbus TCP qua Wi-Fi với 10 thanh ghi giữ và 8 cuộn, tự phân tích khung, không dùng thư viện.


Bình luận
Đã thêm vào giỏ hàng