# 使用WebSockets
Http協議是無狀態的,只能由客戶端主動發起,服務端再被動響應,服務端無法向客戶端主動推送內容,并且一旦服務器響應結束,鏈接就會斷開(見注解部分),所以無法進行實時通信。WebSocket協議正是為解決客戶端與服務端實時通信而產生的技術,現在已經被主流瀏覽器支持,所以對于Web開發者來說應該比較熟悉了,Flutter也提供了專門的包來支持WebSocket協議。
> 注意:Http協議中雖然可以通過keep-alive機制使服務器在響應結束后鏈接會保持一段時間,但最終還是會斷開,keep-alive機制主要是用于避免在同一臺服務器請求多個資源時頻繁創建鏈接,它本質上是支持鏈接復用的技術,而并非用于實時通信,讀者需要知道這兩者的區別。
WebSocket協議本質上是一個基于tcp的協議,它是先通過HTTP協議發起一條特殊的http請求進行握手后,如果服務端支持WebSocket協議,則會進行協議升級。WebSocket會使用http協議握手后創建的tcp鏈接,和http協議不同的是,WebSocket的tcp鏈接是個長鏈接(不會斷開),所以服務端與客戶端就可以通過此TCP連接進行實時通信。有關WebSocket協議細節,讀者可以看RFC文檔,下面我們重點看看Flutter中如何使用WebSocket。
在接下來例子中,我們將連接到由[websocket.org提供的測試服務器](http://www.websocket.org/echo.html)。服務器將簡單地返回我們發送給它的相同消息!
### 步驟
1. 連接到WebSocket服務器。
2. 監聽來自服務器的消息。
3. 將數據發送到服務器。
4. 關閉WebSocket連接。
### 1. 連接到WebSocket服務器
[web\_socket\_channel](https://pub.dartlang.org/packages/web_socket_channel) package 提供了我們需要連接到WebSocket服務器的工具.
該package提供了一個`WebSocketChannel`允許我們既可以監聽來自服務器的消息,又可以將消息發送到服務器的方法。
在Flutter中,我們可以創建一個`WebSocketChannel`連接到一臺服務器:
```
final channel = new IOWebSocketChannel.connect('ws://echo.websocket.org');
```
### 2. 監聽來自服務器的消息
現在我們建立了連接,我們可以監聽來自服務器的消息,在我們發送消息給測試服務器之后,它會返回相同的消息。
我們如何收取消息并顯示它們?在這個例子中,我們將使用一個[`StreamBuilder`](https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html) Widget來監聽新消息, 并用一個Text Widget來顯示它們。
```
new StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
return new Text(snapshot.hasData ? '${snapshot.data}' : '');
},
);
```
#### 工作原理
`WebSocketChannel`提供了一個來自服務器的消息Stream 。
該`Stream`類是`dart:async`包中的一個基礎類。它提供了一種方法來監聽來自數據源的異步事件。與`Future`返回單個異步響應不同,`Stream`類可以隨著時間推移傳遞很多事件。
該[`StreamBuilder`](https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html) Widget將連接到一個Stream, 并在每次收到消息時通知Flutter重新構建界面。
### 3. 將數據發送到服務器
為了將數據發送到服務器,我們會`add`消息給`WebSocketChannel`提供的sink。
```
channel.sink.add('Hello!');
```
#### 工作原理
`WebSocketChannel`提供了一個[`StreamSink`](https://docs.flutter.io/flutter/dart-async/StreamSink-class.html),它將消息發給服務器。
`StreamSink`類提供了給數據源同步或異步添加事件的一般方法。
### 4. 關閉WebSocket連接
在我們使用`WebSocket`后,要關閉連接:
```
channel.sink.close();
```
### 完整的例子
```
import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';
class WebSocketRoute extends StatefulWidget {
@override
_WebSocketRouteState createState() => new _WebSocketRouteState();
}
class _WebSocketRouteState extends State<WebSocketRoute> {
TextEditingController _controller = new TextEditingController();
IOWebSocketChannel channel;
String _text = "";
@override
void initState() {
//創建websocket連接
channel = new IOWebSocketChannel.connect('ws://echo.websocket.org');
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("WebSocket(內容回顯)"),
),
body: new Padding(
padding: const EdgeInsets.all(20.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Form(
child: new TextFormField(
controller: _controller,
decoration: new InputDecoration(labelText: 'Send a message'),
),
),
new StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
//網絡不通會走到這
if (snapshot.hasError) {
_text = "網絡不通...";
} else if (snapshot.hasData) {
_text = "echo: "+snapshot.data;
}
return new Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: new Text(_text),
);
},
)
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _sendMessage,
tooltip: 'Send message',
child: new Icon(Icons.send),
),
);
}
void _sendMessage() {
if (_controller.text.isNotEmpty) {
channel.sink.add(_controller.text);
}
}
@override
void dispose() {
channel.sink.close();
super.dispose();
}
}
```
上面的例子比較簡單,不再贅述。我們現在思考一個問題,假如我們想通過WebSocket傳輸二進制數據應該怎么做(比如要從服務器接收一張圖片)?我們發現`StreamBuilder`和`Stream`都沒有指定接收類型的參數,并且在創建WebSocket鏈接時也沒有相應的配置,貌似沒有什么辦法……其實很簡單,要接收二進制數據仍然使用`StreamBuilder`,因為WebSocket中所有發送的數據使用幀的形式發送,而幀是有固定格式,每一個幀的數據類型都可以通過Opcode字段指定,它可以指定當前幀是文本類型還是二進制類型(還有其它類型),所以客戶端在收到幀時就已經知道了其數據類型,所以flutter完全可以在收到數據后解析出正確的類型,所以就無需開發者去關心,當服務器傳輸的數據是指定為二進制時,`StreamBuilder`的`snapshot.data`的類型就是`List<int>`,是文本時,則為`String`。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示