WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議。與傳統的 HTTP 通信不同,WebSocket 允許伺服器和客戶端在建立連接後,能夠雙向傳遞數據,且不必每次都發起新的請求。這使得 WebSocket 成為實時應用比如:聊天應用、遊戲、即時通知...等,一個理想的解決方案。
本次教學會使用 Go 語言建立 Websocket 伺服器及使用第三方套件在 Flutter 中使用 websocket。
範例程式
先使用 go mod
初始化 go 語言專案
go mod init my-websocket
我們的教學習慣盡可能以官方的套件來實作,因此我們選用 golang 補充套件 golang.org/x/net/websocket
package main
import (
"fmt"
"net/http"
"golang.org/x/net/websocket"
)
const port = 8080
func main() {
// 設定 WebSocket 路由
http.Handle("/ws", websocket.Handler(handleWebSocket))
// 啟動伺服器
fmt.Printf("WebSocket 伺服器運行於: %d\n", port)
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
if err != nil {
fmt.Println("伺服器啟動失敗:", err)
}
}
func handleWebSocket(ws *websocket.Conn) {
var message string
for {
// 讀取客戶端發送的消息
err := websocket.Message.Receive(ws, &message)
if err != nil {
fmt.Println("讀取消息錯誤:", err)
break
}
fmt.Println("收到消息:", message)
m := []rune(message)
for i := 0; i < len(m)/2; i++ {
m[i], m[len(m)-1-i] = m[len(m)-1-i], m[i]
}
// 回傳消息給客戶端
err = websocket.Message.Send(ws, string(m))
if err != nil {
fmt.Println("發送消息錯誤:", err)
break
}
}
}
當程式執行時,我們可以透過 ws://localhost:8080/ws
做互動。我們將收到訊息前後反轉並回傳給 client!值得一提的是,當連線建立後,即使 client 端不發送封包,server 端仍然可以主動傳訊息!
# 執行 go 語言程式
go run main.go
# 或者編譯 go 語言程式
go build main.go
./main
前幾天我們有介紹了 FutureBuilder
Day-11 在 Flutter 中使用 FutureBuilder 進行狀態管理,FutureBuilder 可以處理異步函式,使其在未取得值時顯示一個畫面,取得值時顯示另一個畫面,而當錯誤發生時,又可以顯示另一個畫面。
在 Flutter 中,除了使用 FutureBuilder
來處理一次性的異步操作,還有另一個更靈活的工具,適合處理連續異步資料流,那就是 StreamBuilder
。StreamBuilder
專門用來監聽 Stream
的變化,並根據不同的狀態來更新 UI。這對於需要持續接收數據的應用場景特別實用,比如 Websocket 連接、實時數據更新、傳感器數據等。
Stream
可以理解為一個持續傳送資料的通道,不斷地傳遞資料給訂閱者,而這些資料可能是同步或異步的。和 Future
不同,Stream
不只會返回一個結果,而是不斷發送多個事件。
StreamBuilder
的使用方法與 FutureBuilder
相似,StreamBuilder
通常會處理五個狀態:
StreamBuilder<int>(
stream: counterStream(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('錯誤 ${snapshot.error}');
} else {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Text('目前尚未連線到任何異步計算');
case ConnectionState.waiting:
return const Text('已連線,等待互動');
case ConnectionState.active:
return Text('資料傳輸中:${snapshot.data}');
case ConnectionState.done:
return const Text('資料傳輸完成');
}
}
},
)
那麼,該如何製作 Stream()
這個物件呢?我們可以使用 asynchronous generator (異步生成器)。該生成器允許使用者使用 yield
發送值到 Stream
中。
Stream<int> getStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // 每秒發出一個整數
}
}
其實原本是想要直接從頭來的,但礙於功力不足,因此,這裡會直接介紹如何使用第三方套件 web_socket_channel 來實作。官方文件:web_socket_channel | Dart package
首先,一如往常,我們先安裝該套件
flutter pub add web_socket_channel
接著我們看一下文檔,有一個 dart 中的範例程式:
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
main() async {
final wsUrl = Uri.parse('ws://example.com');
final channel = WebSocketChannel.connect(wsUrl);
await channel.ready;
channel.stream.listen((message) {
channel.sink.add('received!');
channel.sink.close(status.goingAway);
});
}
我們可以稍微改良後,在 flutter
中做使用
class _WebSocketExampleState extends State<WebSocketExample> {
// 使用 WebSocketChannel 建立連接
// 網址的部分根據我們的伺服器做調整
final WebSocketChannel channel = WebSocketChannel.connect(
Uri.parse('ws://localhost:8080/ws'),
);
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
channel.sink.close();
_controller.dispose();
super.dispose();
}
void _sendMessage() {
if (_controller.text.isNotEmpty) {
channel.sink.add(_controller.text);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('WebSocket Example'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
TextField(
controller: _controller,
decoration: const InputDecoration(labelText: 'Send a message'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _sendMessage,
child: const Text('Send'),
),
const SizedBox(height: 20),
StreamBuilder(
stream: channel.stream, // 監聽 WebSocket 的數據流
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('錯誤 ${snapshot.error}');
} else {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Text('目前尚未連線到任何異步計算');
case ConnectionState.waiting:
return const Text('已連線,等待互動');
case ConnectionState.active:
return Text('資料傳輸中:${snapshot.data}');
case ConnectionState.done:
return const Text('資料傳輸完成');
}
}
},
),
],
),
),
);
}
}
後記:有點想試試從 dart:io
和 dart:html
分別實作 websocket,可能後續再補充吧!