QT/QML VÀ SOCKETIO: CÓ KHẢ THI NẾU TỰ CODE ?

QT/QML và SocketIO: Có khả thi nếu không dùng thư viện ngoài?

Author: Nguyen Kim Quoc

 Picture1

Có lẽ đối với người đã làm quen với lập trình ứng dụng web thì thư viện SocketIO đã không còn quá xa lạ. Nó là một trong những thư viện mà nhiều người sẽ nghĩ đến ngay khi nói về một ứng dụng cần tính realtime.

Còn đối với QT thì đây là một framework mã nguồn mở dùng để thiết kế giao diện dành cho các nền tảng máy tính cá nhân, máy tính nhúng và các nền tảng di động.

Vì vậy khi mà phải dùng đến QT để tạo ứng dụng cho mình thì sẽ có nhiều người sẽ muốn đưa SocketIO vào để hiện thực tính realtime của ứng dụng. Tuy nhiên, đối với người chưa có nhiều kinh nghiệm về việc đưa một thư viện SocketIO C++ của bên thứ 3 vào chương trình QT của mình thì đây đúng là một ác mộng.

Nhưnggg, cơn ác mộng đó sẽ chấm dứt ngay thôi, vì hôm nay mình sẽ chia sẻ cho các bạn một cách để chạy một SocketIO client ngay trong ứng dụng QT của các bạn mà không cần dùng thư viện nào từ bên thứ 3.

#Note: mình sử dụng QML trong QT Quick nhé :))) Dùng kèm với Javascript cho dễ thao tác dữ liệu.

  1. Hiểu Socket.IO
    1. Sơ lược về SocketIO

SocketIO hoạt động như một kênh 2 chiều (bidirectional channel) kết nối giữa client và server, kênh này cho phép truyền dữ liệu theo cả 2 chiều client to server và server to client.

Dữ liệu truyền nhận trong SocketIO có cấu tạo cơ bản gồm 2 phần đó là Event và Message

Picture2

Figure 1 Ví dụ về truyền nhận socketIO với event là "data", message là "hello"

  1. Sâu hơn về cấu trúc dữ liệu của gói tin SocketIO

Ta sẽ dựa theo thư viện SocketIO của NodeJS để tìm hiểu về cấu trúc message của gói tin socketio/socket.io: Realtime application framework (Node.JS server) (github.com) .

Đọc một chút ở phần readme, ta thấy

Picture3

Đi vào socketio/socket.io-protocol: Socket.IO Protocol specification (github.com)

Vì nó khá dài nên mình sẽ nêu ra một vài điểm cần chú ý:

  • Mặc định SocketIO server sẽ tạo ra kết nối long – polling nên phải ta upgrade lên Websocket bằng cách dùng url kết nối: ws://host:port/socket.io/?EIO=3&transport=websocket
  • Sau đó server sẽ trả về một message 0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}
    • Tiền tố “0” dung để định danh đây là packet type “open”của Engine.IO.
    • Các thành phần cần chú ý trong chuỗi Json:
      • Sid là id của client được server định danh.
      • pingInterval thời gian xác minh duy trì kết nối, ở đây 25000 ms sẽ gửi ping một lần.
      • pingTimout thời gian quá hạn để xác minh kết nối, sau khi nhận ping 5000 ms mà không pong lại server thì sẽ disconnect.
    • Định dạng gói tin là 42[“event”, message]
      • Ở đây có 2 tiền tố: 4 là tiền tố của Engine.IO có nghĩa đây là message, 2 là tiền tố của Socket.IO có nghĩa đây là một event.
      • Thành phần thứ nhất trong ngoặc vuông là tên của event.
      • Thành phần thứ 2 trong ngoặc vuông là message, có thể là string “hello”, chuỗi Json như {“name”: “A”, “age” : 18} hoặc là 1 buffer array.
    • Ping là “2”, Pong là “3”: client gửi ping “2”đên server mỗi pingInterval và sau pingTimout nếu không nhận lại được gói pong “3” thì sẽ ngắt kết nối.

OK!!! Giờ mình sẽ đi hiện thực nó trên thằng QT /QML.

  1. Hiện thực tương tác Socket.IO trên QT với QtWebsockets
    1. Tạo một server Socket.IO

Để tiện cho việc kiểm chứng liệu Socket.IO client của chúng ta hoạt động tốt không thì ta sẽ tạo một server để kiểm chứng.

Mình sẽ dùng NodeJs để viết server:

Package.json

{

  "dependencies": {

    "express": "^4.17.1",

    "socket.io": "^4.0.0"

  }

}

Index.js

const app = require("express")();

const httpServer = require("http").createServer(app);

const options = {allowEIO3: true};

var fs = require('fs');

const io = require("socket.io")(httpServer, options);

// Webserver

httpServer.listen(8000);

// Websocket

io.on('connection', function (socket) {

    console.log(socket.id + "connect");

    socket.onAny((data, value)=>{

        console.log(data + ":" + value);

    })

    io.emit("server", "Hello from server");

    socket.on('test', (data) => {

        console.log(data);

    })

    socket.on("disconnect", ()=>{

        console.log(socket.id + "disconnect")

    })

});

  1. QtWebsockets

Đây là thư viện để sử dụng Websocket trong  QT /QML, mình sẽ dùng nó để tạo kết nối tương tác với SocketIO server.

Mọi thứ về QtWebsockets đều có tại đây: WebSocket QML Type | Qt WebSockets 5.15.5

Trong đó, ta sẽ quan tâm đến những thuộc tính và phương thức sau:

  • active: dùng để điều khiển kết nối, true là kết nối, false là ngắt.
  • url: đây là url của server socket mà ta muốn kết nối đến.
  • errorString: dùng cho việc log error trong quá trình hoạt động.
  • status:
    • WebSocket.Connecting
    • WebSocket.Open
    • WebSocket.Closing
    • WebSocket.Closed
    • WebSocket.Error
  • statusChanged: tạo ra 1 trigger khi thuộc tính status thay đổi.
  • textMessageReceived: tạo ra một trigger khi có message đến.
  • sendTextMessage: một phương thức dùng để gửi message đi.
    1. Tiến hành code

Ở đây mình sử dụng phiên bản QT 5.15.2 với Qt Creator 4.14.2

Đầu tiên add websockets vào file .pro

1

QT += websockets

Main.qml

1

import QtWebSockets 1.1

Như vậy là xong phần import thư viện.

Tiếp theo mình sẽ tạo một đối tượng Websockets trong .qml

Main.qml

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

import QtQuick 2.12

import QtQuick.Window 2.12

import QtWebSockets 1.1

import QtQuick.Controls 2.12

Window {

    id: window

    width: 640

    height: 480

    visible: true

    title: qsTr("Hello World")

    WebSocket {

        id: socket

        url: "ws://localhost:8000/socket.io/?EIO=3&transport=websocket"

        active: true

        onTextMessageReceived: {

            console.log(message);

        }

        onStatusChanged: {

            if (socket.status === WebSocket.Error) {

                socket.active = false

                console.log("Error: " + socket.errorString)

            } else if (socket.status === WebSocket.Open) {

                console.log('connect')

                socket.active = true

            } else if (socket.status === WebSocket.Closed) {

                console.log('disconnect')

            }

        }

    }

}

Bấm Ctrl R hoặc nút Run để chạy chương trình QT (Đảm bảo server NodeJs đã được khởi chạy)

Picture4

Nếu như log của QT hiển thị như trên tức là bạn đã kết nối thành công đến server Socket.IO NodeJs

Nhưng sớm muộn gì thằng này  Picture5 cũng sẽ đến :((( Do hiện tại ứng dụng chưa có khả năng duy trì kết nối với ping và pong.

Vậy thì phải làm sao???

Nhớ lại bên trên 1 xíu, mình đã từng giới thiệu ping là “2” còn pong là “3”. Ok, h sửa main.qml lại như này để kiểm chứng xem ping và pong hoạt động như thế nào.

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

import QtQuick 2.12

import QtQuick.Window 2.12

import QtWebSockets 1.1

import QtQuick.Controls 2.12

Window {

    id: window

    width: 640

    height: 480

    visible: true

    title: qsTr("Hello World")

    WebSocket {

        id: socket

        url: "ws://localhost:8000/socket.io/?EIO=3&transport=websocket"

        active: true

        onTextMessageReceived: {

            console.log(message);

        }

        onStatusChanged: {

            if (socket.status === WebSocket.Error) {

                socket.active = false

                console.log("Error: " + socket.errorString)

            } else if (socket.status === WebSocket.Open) {

                console.log('connect')

                socket.active = true

            } else if (socket.status === WebSocket.Closed) {

                console.log('disconnect')

            }

        }

    }

    Button{

        text: "button"

        onClicked: {

            socket.sendTextMessage('2');

        }

    }

}

Khi đó sẽ có một nút bấm ở phía trên góc trái của ứng dụng

Picture6

Sau khi nhấn nút nhấn thì ứng dụng sẽ gửi một ping “2” đến server. Và nhìn xem, server sẽ gửi lại một pong “3” tương ứng với mỗi lần ping “2”.

Picture7

Việc ping pong như vậy sẽ giúp cho ứng dụng giữ được kết nối đối với server.

Nhưng ta đâu thể bấm tay hoài được ._. Nên ta sẽ có một đối tượng Timer để giải quyết bài toán ping pong tự động này. Giá trị của interval phải <= giá trị của pingInterval tại message nhận được khi thiết lập kết nối.

1

2

3

4

5

6

7

8

9

10

Timer{

        id: pingInterval

        interval: 24000

        running: false

        repeat: true

        onTriggered: {

            if(socket.status === WebSocket.Open)

                socket.sendTextMessage('2')

        }

    }

Và Timer này chỉ bắt đầu chạy khi đã kết nối và phải dừng khi bị ngắt kết nối, vì vậy code bây giờ sẽ trở thành:

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

import QtQuick 2.12

import QtQuick.Window 2.12

import QtWebSockets 1.1

import QtQuick.Controls 2.12

Window {

    id: window

    width: 640

    height: 480

    visible: true

    title: qsTr("Hello World")

    WebSocket {

        id: socket

        url: "ws://localhost:8000/socket.io/?EIO=3&transport=websocket"

        active: true

        onTextMessageReceived: {

            if(message == "40")

                pingInterval.start();

            console.log(message);

        }

        onStatusChanged: {

            if (socket.status === WebSocket.Error) {

                socket.active = false

                console.log("Error: " + socket.errorString)

            } else if (socket.status === WebSocket.Open) {

                console.log('connect')

                socket.active = true

            } else if (socket.status === WebSocket.Closed) {

                pingInterval.stop();

                console.log('disconnect')

            }

        }

    }

    Timer{

        id: pingInterval

        interval: 24000

        running: false

        repeat: true

        onTriggered: {

            if(socket.status === WebSocket.Open)

                socket.sendTextMessage('2')

        }

    }

}

OK rồi, có vẻ đã giải quyết được vấn đề duy trì kết nối. Ở đây mình sẽ không giải quyết vấn đề pingTimeout vì khi server disconnect đã có tín hiệu onStatusChanged để xử lý.

Giờ đến việc gửi nhận dữ liệu. Như ở trên, ta đã biết được cấu trúc dữ liệu có dạng 42[“event”,  message], vậy giờ mình sẽ xây dựng một function dùng cho việc gửi dữ liệu

1

2

3

4

5

6

7

8

function emit(event, data){

    if (data[0] === "{")

        socket.sendTextMessage("42[\""+ event +"\","+ data +"]");

    else

        socket.sendTextMessage("42[\""+ event +"\",\""+ data +"\"]");

}

Với hàm này ta có thể gửi một chuỗi Json hoặc một chuôi String.

Cũng giống như vậy, việc gửi dữ liệu từ server Socket.IO đến client cũng có định dạng dữ liệu tương tự. Đây là dữ liệu từ server gửi về client thông qua dòng io.emit(“server”, “Hello from server”).

Picture8

Toàn bộ dữ liệu đó được in ra từ event onTextMessageReceived

1

2

3

4

5

onTextMessageReceived: {

            if(message == "40")

                pingInterval.start();

            console.log(message);

        }

Giờ đây chỉ cần các thao tác xử lí chuỗi căn bản là ta đã tách được event và payload từ chuỗi string 42["server","Hello from server"], và sau đó, với event nào ta làm việc gì thì do các bạn quyết định.

  • Kết luận

Vậy là mình đã hướng dẫn xong cho các bạn cách tương tác với SocketIO server thông qua thư viện QtWebsockets trong framework QT, dù là hướng dẫn trên công cụ QT nhưng mọi thao tác trên đây hoàn toàn có thể được viết theo một ngôn ngữ nào khác có hỗ trợ thư viện Websocket. Hi vọng bài Blog này sẽ giúp ích cho các bạn. Cám ơn vì đã đọc, hẹn gặp lại ở những bài sau.