Flutter-WebSockets

829 阅读3分钟

WebSocket

Http协议是无状态的,只能由客户端主动发起,服务器再被动响应,服务端无法向客户端主动推送内容,并且一旦服务器响应结束,链接就会断开。所以无法进行实时通讯,WebSocket协议是为了解决客户端与服务端实时通讯而产生的技术,现在已经被主流浏览器支持。

注意:Http协议中虽然可以通过keep-alive机制使服务器在响应结束后链接会保持一段时间,但最终还是会断开,keep-alive机制主要是用于避免在同一台服务器请求多个资源时频繁创建链接,本质上是支持链接复用的技术,而并非用于实时通讯。

websocket协议本质上是基于TCP的协议,它先通过Http协议发起一条特殊的http请求进行握手后,如果服务器支持WenSocket协议,则会进行协议升级。WebSocket会使用Http协议握手后创建的tcp链接。和Http协议不同的是,WebSocket链接是个长链接(不会断开),所以服务器与客户端可以通过此TCP链接进行实时通讯。

WebSocket通讯步骤

使用WebSocket通讯分为五个步骤

  1. 链接到WebSocket服务器
  2. 监听来自服务器的消息
  3. 将数据发送到服务器
  4. 关闭WebSocket链接

链接WebSocket服务器

可以使用Web_sokcet_channel pacgage,里面提供了需要连接WebSocket服务器的工具。该package提供了一个WebSocketChannel允许监听来自服务器的消息。也可以将消息发送到服务器。当然也可以使用系统自带的WebSocket,需要导入

import 'dart:io';

final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');

监听服务器的消息

有两种方式:

  1. 使用一个StreamBuilder来监听消息:
StreamBuilder(
  stream: widget.channel.stream,
  builder: (context, snapshot) {
    return Text(snapshot.hasData ? '${snapshot.data}' : '');
  },
);
  1. 直接监听channel.stream
channel.stream.listen(dataChange, onError: onError,onDone: onDone);

void dataChange(data){
    debugPrint("websocket-data:$data\n");
}

void onError(err){
  debugPrint("websocket-err: $err\n");
}

void onDone(){
  debugPrint("websocket-onDone\n");
}

WebSocketChannel提供了一个来自服务器消息的Steam。该stream类是dart:async包的一个基础类。它提供了一种方法监听来自数据源的异步事件。与Future返回单个响应不同,Stream类可以随着时间推移传递很多事件。StreamBuilder组件将链接到一个Stream,并在每次收到消息时通知Flutter重构界面。

将数据发送到服务器

channel.sink.add('Hello!');

WebSocket提供了一个StreamSink,它将消息发送到服务器。
StreamSink类提供了给数据源同步或异步添加事件的一般方法。

关闭WebSocket连接

channel.sink.close();

实例:

import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';

class WebSocketRoute extends StatefulWidget {
  @override
  _WebSocketRouteState createState() => _WebSocketRouteState();
}

class _WebSocketRouteState extends State<WebSocketRoute> {
  TextEditingController _controller = TextEditingController();
  IOWebSocketChannel channel;
  String _text = "";


  @override
  void initState() {
    //创建websocket连接
    channel = IOWebSocketChannel.connect('ws://echo.websocket.org');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("WebSocket(内容回显)"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: InputDecoration(labelText: 'Send a message'),
              ),
            ),
            StreamBuilder(
              stream: channel.stream,
              builder: (context, snapshot) {
                //网络不通会走到这
                if (snapshot.hasError) {
                  _text = "网络不通...";
                } else if (snapshot.hasData) {
                  _text = "echo: "+snapshot.data;
                }
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 24.0),
                  child: Text(_text),
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: Icon(Icons.send),
      ),
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      channel.sink.add(_controller.text);
    }
  }

  @override
  void dispose() {
    channel.sink.close();
    super.dispose();
  }
}

WebSocket中所有发送的数据使用帧的形式发送,而帧时有固定的格式,每一个帧的数据类型都可以通过Opcode字段指定,它可以指定当前帧是文本类型还是二进制类型(或其他类型),所以客户端在收到帧时就已经知道了其数据类型,所以flutter完全可以在接收到数据后解析出正确的类型,所以无需开发者去关心,当服务器传输指定为二进制时,StreamBuilder的snapshot.data的类型就是List[int],是文本时,则为String。
另外附录网上搜索到的关于wss的安全证书校验(未验证过):

//全局配置受信任的证书
class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
  }
}