+2

Unlocking the Power of WebSockets: A Deep Dive into Real-Time Web Communication

Giới thiệu

WebSocket cung cấp một kênh truyền thông hai chiều (full-duplex) qua một kết nối TCP duy nhất. Điều này có nghĩa là cả máy khách và máy chủ có thể gửi dữ liệu cùng lúc mà không cần bất kỳ yêu cầu nào.

Cách thức hoạt động cơ bản cùa Websocket

  • Client sẽ gửi xuống server với field Upgrade yêu cầu chuyển đổi từ giao thức HTTP sang WebSocket. image.png
  • Server nhận request và response với http code 101 (Switching Protocols) nếu chấp nhận sang WebSocket. image.png

2 quá trình trên giống như handshake ở TCP dùng để tạo 1 connection. Sau khi tạo connection xong thì cả 2 bên client, server sẽ trao đổi dữ liệu bằng các frame được định nghĩa trong RFC 6455 specification image.png

  • Cuối cùng là close kết nối, 1 trong 2 bên client, server sẽ gửi 1 yêu cầu close connection

WebSocket giải quyết nhiều vấn đề đau đầu khi phát triển các ứng dụng web thời gian thực và có nhiều lợi ích so với HTTP truyền thống:

  • Header nhẹ giúp giảm chi phí truyền dữ liệu.
  • Chỉ cần một kết nối TCP cho một client.
  • Máy chủ WebSocket có thể push data tới các client.

Lý thuyết

Keep alive

Các bạn có tự hỏi làm sao 1 connection nó có thể keep alive không, trong khi những kiến thức chúng ta được học khi http connection nhận được reponse nó sẽ đóng connection. Đó chính là keep-alive field được định nghĩa trong bảng nâng cấp http/1.0 rfc2616 specification. Cơ chế hoạt động chính để keep connection đó chính là trong khoảng thời gian nếu không có request nào thì sẽ gửi 1 lệnh request rỗng để xem bên kia có response không, nếu không response thì sẽ đóng connection. Cơ chế chính là vậy 😅

Protocol overview

Phần protocol overview mình đã nói khá chi tiết ở phần giới thiệu rồi để làm rõ hơn bạn có thể đọc thêm ở đây protocol overview

Cài đặt

Thì để giúp các bạn hiểu rõ hơn thì mình sẽ implement 1 cơ chế đơn giản dùng websocket bằng thư viện "github.com/gorilla/websocket" golang

Server

Code bên dưới mình tạo ra một server open websocket connection để transfer file từ client --> server. Đoạn code này mình thấy có nhiều ứng dụng, như trong lúc mình làm về bitcoin testnet nó sẽ sync data giữa các node với nhau. Nên mình code demo này cũng dựa vào ý tưởng đó. Các bạn đọc code + comment bên dưới để hiểu hơn nhé 😅

package main

import (
	"bufio"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/websocket"
)

// upgrader is used to upgrade the HTTP connection to a WebSocket connection
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

var filename = "000000000.fdb"

func handlerWS(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil) // upgrade the HTTP connection to a WebSocket connection
	if err != nil {
		log.Println(err)
		return
	}
	defer conn.Close()

	fmt.Println("Client connected, start sync data from file " + filename)

	for {
		messageType := websocket.TextMessage // messagetype use to specify the type of message when sending data to the client

		file, err := os.Open(filename) // open the file
		if err != nil {
			log.Fatalf("Failed to open file: %s", err)
		}
		defer file.Close()

		// Create a new buffered reader to read the file
		reader := bufio.NewReader(file)
		count := 0

		for {
			data, err := reader.ReadBytes('\n') // read the file line by line
			if err != nil {
				if err.Error() == "EOF" {
					log.Println("End of file, close connection")
					closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Server is closing the connection")
					err = conn.WriteMessage(websocket.CloseMessage, closeMsg) // send a close message to the client
					return
				}
				log.Fatalf("Failed to read file: %s", err)
			}

			err = conn.WriteMessage(messageType, data) // send the data to the client, with messageType as TextMessage
			if err != nil {
				fmt.Println("len: ", len(data))
				log.Println("Error write message:", err)
				return
			}

			fmt.Printf("Successfully sent %d messages\n", count)
			count++
		}
	}
}

func main() {
	http.HandleFunc("/ws", handlerWS) // handle the WebSocket connection, and upgrade the HTTP connection to a WebSocket connection
	fmt.Println("Server started on localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil { // start the server
		log.Fatal(err)
	}
}

Client

Client thì đơn giản hơn nhiệm vụ chính là tạo 1 connection tới server, rồi đọc data từ connection đó rồi viết vào file 😅

package main

import (
	"fmt"
	"os"

	"github.com/gorilla/websocket"
)

var filename = "000000000.fdb"

func main() {
	// This is a client implementation

	conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
	if err != nil {
		fmt.Println("Error create Dial:", err)
		panic(err)
	}
	defer conn.Close()

	fmt.Println("Client connected, start sync data from file " + filename)
	count := 0

	for {
		_, p, err := conn.ReadMessage()
		if err != nil {
			if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
				fmt.Println("Close connection, err: ", err)
				return
			}
			fmt.Println("read message from connection fail, err: ", err)
			return
		}

		file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		if err != nil {
			fmt.Printf("Failed to open file: %s", err)
		}
		defer file.Close()

		_, err = file.Write(p)
		if err != nil {
			fmt.Printf("Failed to write file: %s", err)
		}

		fmt.Printf("Successfully received %d messages\n", count)
		count++
	}

}

Repo: https://github.com/phanquocky/go-fundamental/tree/main/websocket

Tổng kết

Bài viết về Websocket này cũng cơ bản, chủ yếu là những đoạn code POC để giúp các bạn thực hành. Các bạn có thể nghĩ ra nhiều ý tưởng hơn giống như tạo chat app mình cũng có đề cập trong repo github của mình. Đó bài nà chỉ vậy thôi 😅 ref: https://yalantis.com/blog/how-to-build-websockets-in-go/#:~:text=WebSockets allow a browser to,tracking apps%2C and so on.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí