07/09/2018, 17:25

Mình đã làm bể cá thông minh như thế nào - Wemos authenticate qua laravel (phần 4.3 - Tìm hiểu GET và POST trong Wemos)

Xin chào các bạn, hôm nay mình xin chia sẻ tiếp phần 3 của chủ đề "Wemos authenticate qua laravel" trong loạt bài "Làm bể cá thông minh". Ở phần trước thì ta đã kết nối được web client với server nodejs và authenticate với server PHP (laravel) bằng Jwt. Như trong sơ đồ dưới đây: Client phải ...

Xin chào các bạn, hôm nay mình xin chia sẻ tiếp phần 3 của chủ đề "Wemos authenticate qua laravel" trong loạt bài "Làm bể cá thông minh".

Ở phần trước thì ta đã kết nối được web client với server nodejs và authenticate với server PHP (laravel) bằng Jwt. Như trong sơ đồ dưới đây:

Client phải đăng nhập vào server laravel trước và sẽ nhận được 1 cái jwt-token. Tiếp đó client sử dụng jwt-token này để đăng nhập vào nodejs server và kết nối với socket.io. Tiếp theo chúng ta sẽ cùng kết nối Wemos với server như cách mà web client mình đã làm từ phần trước.

Vì chúng ta phải viết trên môi trường wemos (esp8266) với ngôn ngữ C/C++ nên không thể có thư viện nào để hỗ trợ đầy đủ việc kết nối request HTTP, socket.io, xác thực người dùng bằng jwt như js được. Do vậy chúng ta phải thiết lập các kết nối đó bằng chính các dòng code của mình. :)

Mình sẽ làm chức năng bật tắt đèn và mạch kết nối sẽ như sau:

  • Gửi request đăng nhập vào server laravel và nhận được jwt-token
  • Kết nối với socket.io
    • Thiết lập bắt tay
    • Thực hiện authenticate sử dụng jwt-token đã nhận được
    • Chuyển đổi giao thức từ HTTP thành Web socket
  • On event thay đổi trạng thái của đèn. Khi có event thì thay đổi trạng thái tương ứng.
  • Emit trạng thái hiện tại của đèn

Trong wemos đã có thư viện ESP8266HTTPClient để gửi các HTTP request. Việc sử dụng nó cũng khá đơn giản. Nhưng để hiểu hơn cách kết nối, cách gửi dữ liệu và chuẩn bị cho việc phân tích chỉnh sửa thêm phần authenticate vào thư viện socket.io của esp8266, hôm nay, chúng ta sẽ đi tìm hiểu cách thức gửi một GET hoặc POST request và cách đọc response trả về nhé.

Như yêu cầu ở trên chúng ta sẽ sử dụng kết nối giao thức HTTP để gửi request đăng nhập và lấy jwt-token từ máy chủ. Để làm được điều này trên wemos chúng ta cần điểm qua lại xem HTTP nó sẽ hoạt động như thế nào? Các request gửi đi là cái gì? Và chúng ta sẽ nhận được cái gì?

Ở đây mình sử dụng HTTP phiên bản 1.1 nên chúng ta chỉ đề cập đến HTTP 1.x thôi nhé. Hiện tại đã có phiên bản HTTP 2.0 các bạn có thể tìm hiểu trên mạng để biết thêm thông tin chi tiết.

HTTP được viết tắt của cụm từ HyperText Transfer Protocol có nghĩa là Giao thức truyền tải siêu văn bản. Dữ liệu truyền và nhận của HTTP là các... văn bản (tất nhiên rồi). Bởi vì các văn bản này có liên kết với các văn bản khác nên được gọi là siêu văn bản. Do nó là giao thức truyền tải siêu văn bản nên request gửi đi và response nhận về là các văn bản.

Cấu trúc request GET như sau:

GET / HTTP/1.1
Host: iot-aquarium.vn
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7

Cấu trúc request POST như sau:

POST / HTTP/1.1
Host: iot-aquarium.vn
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7

{"email":"[email protected]","pass":"12345678"}

Response nhận được sẽ là:

HTTP/1.1 200 OK
Date: Mon, 01 Jan 2018 16:35:48 GMT
Server: Apache/2.4.18 (Ubuntu)
Cache-Control: no-cache, private
Content-Length: 53
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"status":"normal","message":"Server work normally!"}

Các request gửi đi và các response trả về có cấu trúc rất dễ hiểu. Mỗi thuộc tính của phần header sẽ được ở trên từng dòng. Phần header và phần body của request hoặc reponse sẽ cách nhau bởi một dòng trống. Như vậy thì khi gửi request ta chỉ cần gửi đúng cấu trúc như trên là hệ thống có thể hoạt động trơn tru rồi.

Viết hàm gửi GET request

Wemos cung cấp cho chúng ta một class WiFiClient dùng để gửi các dữ liệu vào mạng wifi đã kết nối. Chúng ta có thể sử dụng nó như cổng Serial vậy.
Muốn gửi dữ liệu đi thì chỉ cần gọi hàm print hoặc println trên đối tượng của class WiFiClient là được. Và hàm nhận dữ liệu sẽ là hàm read. Chúng ta sẽ cùng nhau xem ví dụ hàm getRest để hiểu hơn nhé.

WiFiClient internets;

void sendGetREST(String host, int port, String path) {
    char hostname[128];
    // Chuyển String thành array char vì hàm connect chỉ nhận char array.
    // Ở ngoài ta sử dụng String của arduino để dễ dàng thao tác
    host.toCharArray(hostname, 128);

    // Thực hiện tạo kết nối
    if (!internets.connect(hostname, port)) {
        Serial.println(F("Connect failed"));
    }

    /* Tạo request
        GET /{path}  HTTP/1.1
        Host: {host}
        Cookie: {cookie}
        Accept: application/json
        Origin: WemosSocketIOClient
        Connection: keep-alive
    */
    String request = "";
    request += F("GET /");
    request += path;
    request += F(" HTTP/1.1
");
    request += F("Host: ");
    request += host;
    request += F("
");
    request += F("Cookie: ");
    request += cookie;
    request += F("
");
    request += F("Accept: application/json
");
    request += F("Origin: WemosSocketIOClient
");
    request += F("Connection: keep-alive

");

    // Gửi request
    internets.print(request);
}

Việc gửi request đơn giản như vậy đó. Đối với phương thức POST cũng tương tự ta chỉ cần việc print chuỗi request vào internet là xong.

Chú ý:

  • Mỗi dòng của header được viết trên một dòng. Tức là nó sẽ được ngăn cách nhau bởi ký tự xuống dòng. Ở mỗi hệ điều hành thì quy định ký tự xuống dòng của các file văn bản là khác nhau:
    • Trong Unix là ký tự
    • Trong Mac là ký tự
    • Trong Windows là ký tự

Bạn chỉ cần sử dụng cho việc xuống dòng là đủ.

  • internet là một đối tượng của class WiFiClient bạn cần phải khai báo nó trước khi sử dụng.

Đọc dữ liệu trả về

Sau khi gửi request xong thì server sẽ xử lý và trả về kết quả cho mình. Server sẽ print dữ liệu vào chính connect mình vừa tạo ra nên chúng ta không cần phải thực hiện kết nối lại. Và việc trả dữ liệu về có thể sẽ không xảy ra nếu ta gửi request vào một server lỗi. Do vậy cần phải thiết lập time out cho quá trình chờ đợi. Các bạn hãy xem hàm receiveGetREST dưới đây để hiểu rõ hơn nhé.

#define DATA_BUFFER_LEN 1024

WiFiClient internets;
char databuffer[DATA_BUFFER_LEN];
char *dataptr;

// Hàm chờ response
bool waitForInput(void) {
    unsigned long now = millis();

    // Chờ cho đến khi có dữ liệu trong internets hoặc là thời gian chờ > 30s
    while (!internets.available() && ((millis() - now) < 30000UL)) {
        ;
    }
    return internets.available();
}

// Hàm đọc từng dòng của dữ liệu trả về
void readLine() {
    dataptr = databuffer;

    // Khi mà dữ liệu vẫn còn hoặc chưa tràn bộ nhớ thì đọc tiếp
    while (internets.available() != false && (dataptr < &databuffer[DATA_BUFFER_LEN - 2])) {
        char c = internets.read();
        // Nếu gặp ký tự xuống dòng thì không đọc nữa.
        if (c == '
') {
            break;
        } else if (c != '
') {
            // Nếu không phải ký tự xuống dòng thì thêm dữ liệu vào databuffer
            *dataptr++ = c;
        }
    }
    *dataptr = 0;
}

void receiveGetREST() {
    // Chờ response trả về
    if (!waitForInput()) {
        Serial.println(F("[getRest] Time out"));
    }

    while (internets.available()) {
        readLine();
        // In các dòng dữ liệu ra cửa sổ Serial
        Serial.println(databuffer);
    }

    // Sau khi hoàn tất hãy nhớ đóng kết nối
    internets.stop();
    delay(100);
    Serial.println(F("[stopConnect] Connect was stopped"));
}

Chương trình gửi GET request đầy đủ sẽ như sau:

#include <ESP8266WiFi.h>

#define DATA_BUFFER_LEN 1024

const char* ssid = "hoanghoi";
const char* password = "12345678";
String host = "iot-aquarium.vn";
String cookie = "";
int httpPort = 80;
unsigned long prevTime;
long interval = 5000;
String path = "device/session";
WiFiClient internets;
char databuffer[DATA_BUFFER_LEN];
char *dataptr;
// Hàm chờ response
bool waitForInput(void) {
    unsigned long now = millis();

    // Chờ cho đến khi có dữ liệu trong internets hoặc là thời gian chờ > 30s
    while (!internets.available() && ((millis() - now) < 30000UL)) {
        ;
    }
    return internets.available();
}

// Hàm đọc từng dòng của dữ liệu trả về
void readLine() {
    dataptr = databuffer;

    // Khi mà dữ liệu vẫn còn hoặc chưa tràn bộ nhớ thì đọc tiếp
    while (internets.available() != false && (dataptr < &databuffer[DATA_BUFFER_LEN - 2])) {
        char c = internets.read();
        // Nếu gặp ký tự xuống dòng thì không đọc nữa.
        if (c == '
') {
            break;
        } else if (c != '
') {
            // Nếu không phải ký tự xuống dòng thì thêm dữ liệu vào databuffer
            *dataptr++ = c;
        }
    }
    *dataptr = 0;
}

void receiveGetREST() {
    // Chờ response trả về
    if (!waitForInput()) {
        Serial.println(F("[getRest] Time out"));
    }

    while (internets.available()) {
        readLine();
        // In các dòng dữ liệu ra cửa sổ Serial
        Serial.println(databuffer);
    }

    // Sau khi hoàn tất hãy nhớ đóng kết nối
    internets.stop();
    delay(100);
    Serial.println(F("[stopConnect] Connect was stopped"));
}
void sendGetREST(String host, int port, String path) {
    char hostname[128];
    // Chuyển String thành array char vì hàm connect chỉ nhận char array.
    // Ở ngoài ta sử dụng String của arduino để dễ dàng thao tác
    host.toCharArray(hostname, 128);

    // Thực hiện tạo kết nối
    if (!internets.connect(hostname, port)) {
        Serial.println(F("Connect failed"));
    }

    /* Tạo request
        GET /{path}  HTTP/1.1
        Host: {host}
        Cookie: {cookie}
        Accept: application/json
        Origin: WemosSocketIOClient
        Connection: keep-alive
    */
    String request = "";
    request += F("GET /");
    request += path;
    request += F(" HTTP/1.1
");
    request += F("Host: ");
    request += host;
    request += F("
");
    request += F("Cookie: ");
    request += cookie;
    request += F("
");
    request += F("Accept: application/json
");
    request += F("Origin: WemosSocketIOClient
");
    request += F("Connection: keep-alive

");

    // Gửi request
    internets.print(request);
}

void setupNetwork() {
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
}

void setup() {
    Serial.begin(115200);
    setupNetwork();
}

void loop() {
    if(prevTime + interval < millis() || prevTime == 0){
        prevTime = millis();
        sendGetREST(host, httpPort, path);
        receiveGetREST();
    }
}

Các bạn hãy thay đổi ssid và password thành tên và mật khẩu wifi của mạng cần kết nối và host thành domain hoặc Ip cần truy cập và path thành path cần truy cập. Sau đó nạp vào Wemos rồi bật Serial monitor lên xem nó hoạt động thế nào nhé.

Như vậy là chúng ta đã cùng nhau viết một chương trình gửi GET request. Thật đơn giản phải không?!

Các bạn hãy tự mình viết thử chương trình gửi POST request nhé!

Tuy nhiên mỗi request gửi đi server sẽ tạo 1 sesion khác nhau. Chúng ta cần phải cài đặt cookie nhận được ở response trả về từ request thứ nhất và phải gửi kèm nó với mỗi request về sau thì mới có thể giữ được session. Mình sẽ nói rõ hơn vấn đề này ở phần sau.

Cảm ơn các bạn đã đọc bài viết của mình.

Nếu thấy hay thì hãy upvote hoặc share cho bạn bè cùng đọc nhé!

Nếu có bất kì thắc mắc nào thì hãy comment bên dưới.

Chúc các bạn thành công!

0