- vừa được xem lúc

Websocket - Chỉnh sửa document real-time với Golang

0 0 1

Người đăng: Nguyen Van Tuan

Theo Viblo Asia

I. Giới thiệu

Dạo gần đây, thấy sếp mình đang viết một web app để cung cấp cho khách hàng vẽ, xem, trao đổi và trình bày ý tưởng của mình với người khác. Hệ thống theo tưởng tượng của mình là nó sẽ na ná google docs nhưng chắc chắn là sẽ không xịn bằng =))

Tối cuối tuần, mình rảnh tay thay vì lướt linh tinh thì ngồi implement thử một app simple để giải quyết basic nhất bài toán này. Ở bài viết này, mình sẽ hướng dẫn từ ý tưởng cho tới cách xử lý.

Định nghĩa chút về WebSocket

WebSocket là một giao thức truyền thông hai chiều qua một kết nối TCP duy nhất. Nó cho phép giao tiếp giữa trình duyệt và máy chủ diễn ra trong thời gian thực, mà không cần phải mở lại kết nối mỗi lần gửi dữ liệu. WebSocket rất hữu ích cho các ứng dụng cần cập nhật thông tin liên tục như chat, trò chơi trực tuyến, bảng điều khiển thời gian thực, và nhiều hơn nữa.

image.png

II. Ý tưởng

image.png

Mình vẽ nhanh nên có sai gạch đá nhẹ thôi nhé 😄

Trên hình là ý tưởng với 3 flows. Tạo channel và trả về link join channel, khởi tạo mỗi khi người dùng join và lưu dữ liệu gửi tới những người đang xem trong channel.

Chi tiết từng bước

  • Mỗi khi cần trình bày thì host sẽ tạo 1 đường link có chứa thông tin channel và gửi cho tất cả mọi người cùng join.
  • Người dùng click link và join vào channel → hệ thống sẽ khởi tạo 1 connection ws tương ứng với uuid mà người dùng gửi lên ws server.
  • Mỗi khi host hay bất kì 1 ai thay đổi dữ liệu → gọi 1 api lên web app server → write dữ liệu này vào các connection ws có trong channel.

Như nói ở trên, đây chỉ là ý tưởng cơ bản nhất → để sử dụng thực tế thì phải giải quyết rất nhiều bài toán nghiệp vụ nữa đặc biệt là bài toán về conflict khi nhiều người có trong channel sửa dữ liệu.

III. Implement

1. Back end

Ở BE mình dùng thư viện WebSocket của Go để xử lý. Mình sẽ tạo ra 1 biến để lưu thông tin các channel dưới dạng là 1 map và trong map đó sẽ lại là 1 map lưu lại các connections của channel đó =))

var mapWsConn = make(map[string]map[string]*websocket.Conn)
func main() { http.HandleFunc("/index", LoadPage) http.HandleFunc("/ws", InitWebsocket) http.HandleFunc("/ws/close", CloseWebsocket) http.HandleFunc("/save", SaveData) log.Fatal(http.ListenAndServe(":3000", nil))
}

Server gồm có 4 APIs: LoadPage, InitWebsocket, CloseWebsocket, SaveData

func LoadPage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") path, err := os.Getwd() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } content, err := os.ReadFile(path + "/docs-editor-using-websocket/index.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = fmt.Fprintf(w, "%s", content) if err != nil { return }
}
func InitWebsocket(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") channel := r.URL.Query().Get("channel") uuid := r.URL.Query().Get("uuid") if r.Header.Get("Origin") != "http://"+r.Host { http.Error(w, "the origin is invalid", http.StatusInternalServerError) return } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if len(mapWsConn[channel]) == 0 { mapWsConn[channel] = make(map[string]*websocket.Conn) } mapWsConn[channel][uuid] = conn
}
func CloseWebsocket(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") channel := r.FormValue("channel") uuid := r.FormValue("uuid") if _, ok := mapWsConn[channel]; !ok { http.Error(w, "the channel is not found", http.StatusInternalServerError) return } if _, ok := mapWsConn[channel][uuid]; !ok { http.Error(w, "the uuid is not found", http.StatusInternalServerError) return } err := mapWsConn[channel][uuid].Close() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } delete(mapWsConn[channel], uuid) w.WriteHeader(http.StatusOK) _, err = w.Write([]byte("success")) if err != nil { return }
}
func SaveData(w http.ResponseWriter, r *http.Request) { channel := r.FormValue("channel") uuid := r.FormValue("uuid") data := r.FormValue("data") if _, ok := mapWsConn[channel]; !ok { http.Error(w, "the channel is not found", http.StatusInternalServerError) return } for key, ws := range mapWsConn[channel] { if key != uuid { err := ws.WriteJSON(map[string]interface{}{ "data": data, }) if err != nil { continue } } } w.WriteHeader(http.StatusOK) _, err := w.Write([]byte("success")) if err != nil { return }
}

2. Front end

Về giao diện thì mình clone trên mạng cho nhanh còn phần JavaScript thì mình sẽ tự code 😄 Mình lấy html, css ở link này. Thank bro!

https://codepen.io/fajarnurwahid/pen/NWvxeXj

 let ws; if (window.WebSocket === undefined) { console.log("Your browser does not support WebSockets") } else { ws = initWS(); } function initWS() { // close ws before init new ws closeWs(localStorage.getItem("uuid")) let uuid = self.crypto.randomUUID() localStorage.setItem("uuid", uuid) let socket = new WebSocket("ws://" + window.location.host + "/ws" + window.location.search + "&uuid=" + uuid) socket.onopen = function () { console.log("Socket is open") }; // receive data from server socket.onmessage = function (e) { document.getElementById("content").innerHTML = JSON.parse(e.data).data } // close socket socket.onclose = function () { console.log("Socket closed") closeWs(localStorage.getItem("uuid")) } return socket; }

Khởi tạo connection ws 😄 trước khi tạo connection mình sẽ gửi lên server uuid cũ được lưu trong local storage trước để close connection cũ.

UUID sẽ tự bằng thư viện crypto của JavaScript 😄

Đoạn code này cũng xử lý các event open, close và nhận message từ WebSocket server để hiển thị lên giao diện cho người dùng

 // api close ws async function closeWs(uuid) { const formData = new FormData(); if (!!!uuid) { console.log("uuid is not found") return } formData.append("channel", window.location.search.split("=")[1]); formData.append("uuid", uuid); const requestOptions = { method: "POST", body: formData, redirect: "follow" }; await fetch("http://" + window.location.host + "/ws/close", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); }

Đoạn script để close connection WebSocket cũ

 // onchange text editor to save data
const div = document.getElementById('content');
let timeout;
div.addEventListener('input', function() { clearTimeout(timeout); timeout = setTimeout(async function () { await saveData() }, 500)
}) // api save data
async function saveData() { const formData = new FormData(); formData.append("channel", window.location.search.split("=")[1]); formData.append("uuid", localStorage.getItem("uuid")); formData.append("data", document.getElementById("content").innerHTML); const requestOptions = { method: "POST", body: formData, redirect: "follow" }; await fetch("http://" + window.location.host + "/save", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error));
}

Save lại dữ liệu mỗi khi thay đổi. Ở đây, mình set timeout cứ sau 500ms người dùng dừng bấm thì sẽ save lại dữ liệu lên server và server sẽ gửi dữ liệu này cho các connection ws khác.

IV. Kết quả

Dưới đây là video mình demo nhé 😄

V. Nguồn

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 522

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 432

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 153

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 141

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 110

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 244