Truyền Dữ Liệu BLE Đáng Tin Cậy: Xử Lý MTU, Throughput & Chunking

Sớm hay muộn, mọi lập trình viên BLE đều gặp phải cùng một vấn đề: bạn cần gửi hơn 20 byte mỗi lần. Có thể là một firmware image, một lô dữ liệu cảm biến, hoặc một gói cấu hình. Bạn gọi lệnh write và… chỉ có 20 byte đầu tiên đến được. Phần còn lại bị âm thầm bỏ qua.
Gốc rễ của vấn đề này là MTU (Maximum Transmission Unit) — số byte tối đa mà một gói tin BLE có thể mang. Hiểu rõ MTU, biết cách thương lượng nó, và xây dựng một lớp chunking đáng tin cậy bên trên là điều thiết yếu cho bất kỳ ứng dụng BLE thực tế nào.
Trong bài viết này, chúng ta sẽ đề cập mọi thứ bạn cần biết: MTU thực sự là gì, cách thương lượng MTU trên iOS và Android, sự khác biệt giữa các loại write, cách xây dựng giao thức chunking, và cách tối đa hóa throughput.
Bắt đầu thôi!
Kiến Thức Nền Tảng
Trước khi đi vào code, hãy xây dựng một mô hình tư duy rõ ràng về cách truyền dữ liệu BLE hoạt động.
Tầng ATT
Việc trao đổi dữ liệu BLE diễn ra qua tầng ATT (Attribute Protocol). Khi bạn đọc hoặc ghi một characteristic, bạn đang gửi một gói tin ATT. Mỗi gói tin ATT có phần overhead cố định 3 byte (1 byte opcode + 2 byte attribute handle), phần còn lại dành cho payload thực tế của bạn.
1 | ┌──────────────────────────────────────────┐ |
Vậy payload hiệu dụng mỗi lần write là:
Payload hiệu dụng = MTU - 3 bytes
Với MTU mặc định là 23 byte, bạn chỉ có 20 byte dữ liệu sử dụng được mỗi lần write. Đó là lý do tại sao 20 byte là con số kỳ diệu bạn thấy ở khắp nơi.
MTU vs Độ Dài Gói Tin vs Throughput
Ba khái niệm này có liên quan nhưng khác nhau:
| Khái niệm | Ý nghĩa | Mặc định |
|---|---|---|
| MTU | Payload ATT tối đa mỗi gói tin | 23 bytes |
| Data Length (DLE) | Payload Link Layer tối đa (Bluetooth 4.2+) | 27 bytes, tối đa 251 |
| Throughput | Lượng dữ liệu thực tế truyền được mỗi giây | Phụ thuộc vào nhiều yếu tố |
MTU được thương lượng ở tầng ứng dụng. Data Length Extension (DLE) được thương lượng ở tầng link layer. Cả hai đều cần được tối ưu hóa để đạt throughput tối đa.
Thương Lượng MTU
MTU mặc định của BLE là 23 byte — được thiết kế vào năm 2010 cho các giá trị cảm biến nhỏ. Các thiết bị BLE 4.2+ hiện đại hỗ trợ MTU lên đến 517 byte (giá trị tối đa theo đặc tả Bluetooth). Để mở khóa giá trị này, central phải yêu cầu MTU lớn hơn một cách rõ ràng.
Trên iOS (CoreBluetooth)
iOS xử lý thương lượng MTU tự động. Khi bạn kết nối với một peripheral, CoreBluetooth thương lượng MTU cao nhất mà cả hai bên hỗ trợ. Bạn không cần gọi bất kỳ phương thức “request MTU” nào — thay vào đó, bạn chỉ cần truy vấn kết quả:
1 | // Sau khi kết nối và discover các characteristic |
Có hai biến thể:
1 | // Cho write-without-response — trả về payload tối đa trực tiếp |
Lưu ý quan trọng: trên iOS, bạn không thể đặt một giá trị MTU cụ thể. Hệ thống tự động thương lượng giá trị tối đa. Từ iOS 16, hầu hết các thiết bị thương lượng 517 byte MTU khi peripheral hỗ trợ.
Trên Android
Android yêu cầu gọi MTU request một cách rõ ràng:
1 | // Sau khi kết nối đến GATT server |
Thời điểm rất quan trọng. Luôn yêu cầu MTU sau khi kết nối được thiết lập nhưng trước khi bạn bắt đầu đọc hoặc ghi characteristic. Một sai lầm phổ biến là yêu cầu MTU quá muộn, sau khi MTU mặc định 23 byte đã được sử dụng cho service discovery.
1 | // Luồng kết nối khuyến nghị trên Android |
So Sánh MTU Giữa iOS và Android
| Khía cạnh | iOS | Android |
|---|---|---|
| Cách yêu cầu | Tự động | requestMtu(517) |
| Mặc định | Tự động thương lượng tối đa | 23 cho đến khi bạn yêu cầu |
| Tối đa hỗ trợ | 517 | 517 |
| Truy vấn kết quả | maximumWriteValueLength(for:) |
Callback onMtuChanged |
| Lỗi thường gặp | Không có — chỉ cần đọc giá trị | Quên gọi requestMtu |
Loại Write: With Response vs Without Response
BLE cung cấp hai chế độ write, và việc chọn đúng loại ảnh hưởng trực tiếp đến độ tin cậy và throughput.
Write With Response (Có Xác Nhận)
1 | Central Peripheral |
- Peripheral xác nhận mỗi lần write.
- Nếu write thất bại, bạn biết ngay lập tức.
- Chậm hơn — phải đợi xác nhận trước khi gửi gói tin tiếp theo.
- Payload tối đa mỗi lần write: min(MTU - 3, 512) bytes.
Write Without Response (Không Xác Nhận)
1 | Central Peripheral |
- Không có xác nhận — gửi rồi quên.
- Nhanh hơn nhiều — có thể xếp hàng nhiều gói tin trong một connection event.
- Có rủi ro mất gói nếu buffer của peripheral bị tràn.
- Payload tối đa mỗi lần write: (MTU - 3) bytes.
Khi Nào Sử Dụng Loại Nào
| Tình huống | Loại Khuyến Nghị |
|---|---|
| Ghi cấu hình | With Response |
| Lệnh quan trọng | With Response |
| Cập nhật firmware (OTA/DFU) | Without Response + ACK ở tầng ứng dụng |
| Streaming dữ liệu cảm biến | Without Response |
| Truyền file lớn | Without Response + giao thức chunking |
Để truyền dữ liệu throughput cao, write-without-response là lựa chọn đúng đắn. Nhưng bạn cần xây dựng lớp đảm bảo tin cậy của riêng mình phía trên — đó là lúc chunking phát huy tác dụng.
Kiểm Soát Luồng: Tránh Tràn Buffer
Khi sử dụng write-without-response, rủi ro lớn nhất là làm tràn buffer nội bộ của BLE stack. Nếu bạn gửi gói tin nhanh hơn tốc độ radio có thể truyền, các gói tin sẽ bị âm thầm loại bỏ.
iOS — canSendWriteWithoutResponse
CoreBluetooth cung cấp cơ chế kiểm soát luồng sẵn có:
1 | func sendNextChunk() { |
Đây là cách đúng đắn để stream dữ liệu trên iOS. Đừng bao giờ sử dụng timer hoặc delay tùy ý — peripheralIsReady(toSendWriteWithoutResponse:) cho bạn biết chính xác khi nào stack sẵn sàng nhận thêm dữ liệu.
Android — Kiểm Soát Luồng
Trên Android, writeCharacteristic trả về false nếu buffer nội bộ đầy. Ngoài ra, bạn phải đợi callback onCharacteristicWrite trước khi gửi gói tin tiếp theo:
1 | private val writeQueue = ArrayDeque<ByteArray>() |
Lỗi thường gặp: gọi write liên tục trong vòng lặp trên Android mà không đợi callback. Điều này gây mất dữ liệu âm thầm và cực kỳ khó debug.
Xây Dựng Giao Thức Chunking
Khi payload vượt quá MTU, bạn cần chia nhỏ thành các chunk, gửi tuần tự, và ghép lại ở phía bên kia. Dưới đây là thiết kế giao thức thực tế.
Định Dạng Gói Tin
1 | ┌─────────┬──────────┬─────────────────────┐ |
Với MTU 517 byte, mỗi chunk mang được tối đa 511 byte payload (517 - 3 overhead ATT - 3 header giao thức).
Triển Khai Trên iOS
1 | struct ChunkHeader { |
Phía Nhận (Peripheral / Firmware)
Peripheral ghép lại dữ liệu dựa trên flags và sequence number:
1 | 1. Nhận chunk |
Tối Đa Hóa Throughput
Throughput BLE phụ thuộc vào nhiều yếu tố phối hợp cùng nhau. Dưới đây là cách tối ưu từng yếu tố.
1. Thương Lượng MTU Tối Đa
1 | MTU 23 → 20 bytes/gói tin → ~2.5 KB/s thông thường |
Luôn yêu cầu 517. Ngay cả khi peripheral hỗ trợ ít hơn, quá trình thương lượng sẽ chọn giá trị chung cao nhất.
2. Bật Data Length Extension (DLE)
DLE tăng kích thước gói tin Link Layer từ 27 lên 251 byte. Điều này có nghĩa là ít gói tin radio hơn cho mỗi payload ATT. Trên iOS, DLE được bật tự động. Trên Android 5.0+, thường tự động sau khi thương lượng MTU, nhưng một số thiết bị yêu cầu:
1 | // Android — một số thiết bị cần yêu cầu DLE rõ ràng |
3. Sử Dụng Write Without Response
Write-with-response bị giới hạn một gói tin mỗi connection event do vòng lặp ACK. Write-without-response có thể đóng gói nhiều gói tin vào một connection event duy nhất.
1 | With Response: 1 gói tin × 514 bytes mỗi event |
4. Yêu Cầu Connection Interval Ngắn Hơn
Connection interval xác định tần suất central và peripheral trao đổi dữ liệu. Interval ngắn hơn nghĩa là nhiều cơ hội gửi gói tin hơn.
1 | Connection interval 30ms → ~33 event/giây |
Trên iOS, bạn không thể đặt connection interval trực tiếp — CoreBluetooth chọn giá trị dựa trên các tham số ưa thích của peripheral. Trên Android:
1 | // Android — yêu cầu kết nối ưu tiên cao (interval ngắn hơn) |
Cảnh báo: interval ngắn hơn làm tăng đáng kể mức tiêu thụ pin. Chỉ sử dụng CONNECTION_PRIORITY_HIGH trong quá trình truyền dữ liệu, sau đó chuyển lại:
1 | // Sau khi truyền xong |
5. Tính Toán Throughput
Throughput tối đa lý thuyết:
1 | Throughput = (MTU - 3) × số_gói_mỗi_event × (1000 / connection_interval_ms) |
Trong thực tế, hãy kỳ vọng 30-80 KB/s trên iOS và 50-100 KB/s trên Android với các tham số được điều chỉnh tốt. Các yếu tố thực tế như nhiễu RF, các kết nối BLE khác, và giới hạn firmware của peripheral sẽ làm giảm giá trị lý thuyết tối đa.
Các Lỗi Thường Gặp
1. Gửi Dữ Liệu Mà Không Thương Lượng MTU
1 | // Android — quên gọi requestMtu() |
Luôn thương lượng MTU trước bất kỳ quá trình truyền dữ liệu nào.
2. Ghi Trong Vòng Lặp Liên Tục
1 | // iOS — SAI |
Hãy sử dụng canSendWriteWithoutResponse và peripheralIsReady(toSendWriteWithoutResponse:) thay vào đó.
3. Bỏ Qua Khả Năng Hỗ Trợ Loại Write
Không phải mọi characteristic đều hỗ trợ cả hai loại write. Luôn kiểm tra:
1 | if characteristic.properties.contains(.writeWithoutResponse) { |
4. Hardcode Giá Trị MTU
1 | // SAI — hardcode |
MTU khác nhau giữa các thiết bị. Luôn truy vấn tại thời điểm chạy.
5. Không Xử Lý Ngắt Kết Nối Trong Quá Trình Truyền
Các lần truyền dài có thể bị gián đoạn do ngắt kết nối. Giao thức chunking của bạn nên hỗ trợ tiếp tục:
1 | class ResumeableTransfer { |
Tổng Hợp Các Thực Tiễn Tốt Nhất
- Luôn thương lượng MTU tối đa — gọi
requestMtu(517)trên Android; trên iOS thì tự động, chỉ cần đọc kết quả. - Sử dụng write-without-response cho dữ liệu lớn — nhanh hơn 4-6 lần so với write-with-response.
- Tôn trọng kiểm soát luồng — sử dụng
canSendWriteWithoutResponsetrên iOS và đợi callback trên Android. Đừng bao giờ lặp một cách mù quáng. - Xây dựng giao thức chunking — bao gồm sequence number và cờ start/end để ghép lại và phục hồi lỗi.
- Truy vấn MTU tại thời điểm chạy — đừng bao giờ hardcode 20 byte. Các thiết bị thương lượng giá trị MTU khác nhau.
- Yêu cầu ưu tiên kết nối cao trong quá trình truyền — trên Android, sử dụng
CONNECTION_PRIORITY_HIGHtrong khi truyền, sau đó chuyển về balanced. - Xử lý ngắt kết nối một cách khéo léo — hỗ trợ truyền có thể tiếp tục cho các payload lớn.
- Kiểm thử trên thiết bị thật — simulator không phản ánh hành vi BLE thực tế. Luôn xác thực throughput và độ tin cậy trên phần cứng vật lý trong môi trường RF có nhiễu.
Tổng Kết
Truyền dữ liệu BLE trông đơn giản trên bề mặt — ghi byte vào một characteristic — nhưng để làm đúng đòi hỏi hiểu toàn bộ stack: thương lượng MTU, loại write, kiểm soát luồng, và chunking. Giới hạn 20 byte mặc định không phải là bức tường cứng, nó chỉ là điểm khởi đầu.
Với thương lượng MTU đúng cách (517 byte), write-without-response, kiểm soát luồng, và giao thức chunking được thiết kế tốt, bạn có thể đạt 30-100 KB/s throughput đáng tin cậy — quá đủ cho cập nhật firmware, file cấu hình, và các lô dữ liệu cảm biến.
Điều quan trọng nhất: đừng bao giờ gửi dữ liệu BLE mà không biết MTU của bạn, và đừng bao giờ stream mà không có kiểm soát luồng. Làm đúng hai điều này, phần còn lại sẽ tự nhiên đi theo.
Chúc cuối tuần vui vẻ!