Dart学习笔记(26):WebSocket套接字

发表于2018-07-04 13:27 阅读(34)

Socket套接字一节里面提到了常用的3种套接字
初显出Dart在服务器端的简单、高效以及功能强大
这里补充一下WebSocket的内容

本文地址:http://www.cndartlang.com/863.html

WebSocket属于HTML5一种新的协议
实现了浏览器和服务器的全双工通信
而在这之前
真正的即时通信只有Flash Socket才能做到

在Dart中,有两个WebSocket
一个在dart:io中,运行在Dart VM虚拟机中
一个在dart:html中,运行在Web App浏览器中

因为一开始的握手要借助HTTP请求来完成
因此,WebSocket的服务端也就是一个Http Server

1、dart:io WebSocket

websocket_server.dart

import 'dart:io';

main() async {
  try {
    var server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4040);
    await for (HttpRequest req in server) {
      if (req.uri.path == '/ws') {
        // 将一个HttpRequest提升为WebSocket连接
        var socket = await WebSocketTransformer.upgrade(req);
        socket.listen((event) {
          print("接收到来自 ${req.connectionInfo.remoteAddress.address}:${req.connectionInfo.remotePort} 的消息:${event}");

          socket.add("数据已接收!");
        });
      }
    }
  } catch (e) {
    print(e);
  }
}

websocket_client.dart

import 'dart:io';

main() async {
  var socket = await WebSocket.connect('ws://127.0.0.1:4040/ws');
  socket.add('Hello, World!');
  socket.listen((event) {
    print("Server: $event");
  });
  socket.close();
}

运行结果:

通过上面的代码可以看出,WebSocket的API并没有特别的地方
功能上也并不是很出彩,dart:io中套接字的接口已经很完善了
如果要使用WebSocket作为客户端运行在Dart VM虚拟机中
除非有特别的需求,否则实在想不出必须这样做的理由

2、dart:html WebSocket聊天室

dart:io中的WebSocket重点在于提供服务器端的功能API
dart:html中的WebSocket重点在于提供浏览器端的套接字接口
WebSocket的优势应该在于浏览器客户端

下面是聊天室的简单实例

pubspec.yaml

name: Note26
dependencies:
  browser: any

bin/ws_server.dart

import 'dart:io';
import 'dart:convert';

main() async {
  //HTTP服务器,绑定IP和端口
  var server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4040);

  //存储客户端名称和套接字
  Map<String, WebSocket> chatClients = new Map<String, WebSocket>();

/////////////////////////////////////////////
  /**
   * 通过遍历Map
   * 向所有在线客户端发送最新的列表
   *
   * 数据使用Json格式
   * {
   *   'code': 命令
   *   'data': 在线列表的数据为List,接收客户端名称和发送消息为Map
   * }
   */
  void sendList() {
    chatClients.values.forEach((socket) {
      socket.add(JSON.encode({
        'code': "list",
        'data': chatClients.keys.toList()
      }));
    });
  }

  /**
   * 遍历Map,发送消息
   * 此时的String data已经经过JSON编码
   */
  void sendSpeak(String data) {
    chatClients.values.forEach((socket) {
      socket.add(data);
    });
  }
/////////////////////////////////////////////

/////////////////////////////////////////////
  /**
   * 处理服务器接收到的请求
   *
   * 主要流程:
   *   1、将HttpRequest请求提升为WebSocket链接
   *   2、监听客户端发送到服务器的数据(连接和发送消息)
   *   3、客户端关闭连接的时候,删除Map中存储的客户端数据,并更新最新列表
   */
  await for (HttpRequest req in server) {
    if (req.uri.path == '/ws') {
      // 将一个HttpRequest提升为WebSocket连接
      var socket = await WebSocketTransformer.upgrade(req);

      //昵称、IP、端口,用于服务器输出
      String name = '';
      String address = req.connectionInfo.remoteAddress.address;
      int port = req.connectionInfo.remotePort;

      //监听客户端发送过来的数据
      socket.listen((event) {
        var message = JSON.decode(event);

        //connect和speak数据都使用了Map
        if(message is! Map) {
          return;
        }

        switch(message['code']) {
          case 'connect':
            name = message['data']['name'];
            //保存套接字,更新在线列表
            chatClients[name] = socket;
            print("客户端连接 昵称:$name IP:$address 端口:$port");
            sendList();
            break;
          case 'speak':
            //发送消息
            print(message['data']);
            sendSpeak(event);
            break;
          default:
            print("接收到Message Data:${event.data}");
        }
      });

      //客户端关闭的时候,删除存储信息,更新在线列表
      socket.done.whenComplete(() {
        chatClients.remove(name);
        print("客户端连接断开连接 昵称:$name IP:$address 端口:$port");
        sendList();
      });
    }
  }
/////////////////////////////////////////////
}

web/index.dart

import 'dart:html';
import 'dart:convert';

/**
* 客户端类
*
* 主要流程:
*   1、查找HTML中各个DOM控件对象
*   2、监听昵称输入框(回车,并且值改变)
*     2.1 清空在线列表、消息输入框、消息显示框
*     2.2 连接WebSocket服务器,监听onOpen、onMessage、onClose事件
*     2.3 服务器发送来的Message有两个命令:list、speak,分别进行处理
*   3、监听消息输入框,向WebSocket服务器发送数据
*/
class Client {
  WebSocket ws;
/////////////////////////////////////////////
  //登陆界面
  DivElement enterScreen = querySelector("#enter_screen");
  //昵称输入框
  InputElement nameElement = querySelector("#name");

  //聊天界面
  DivElement chatScreen = querySelector("#chat_screen");
  //消息输入框
  InputElement inputElement = querySelector("#speak");
  //消息显示框
  DivElement context = querySelector("#context");
  //在线列表
  DivElement chatListElement = querySelector("#nameslist");
/////////////////////////////////////////////

  String chatName;

  Client() {
    //监听昵称输入框
    initNameListen();
    //监听消息输入框
    initInputListen();
  }

  void initNameListen() {
    nameElement.onChange.listen((e) {
      //取消Dom事件的默认动作
      e.preventDefault();

      //清空DOM控件
      addNames([]);
      context.children.clear();
      inputElement.value = "";

      //连接服务器
      ws = new WebSocket("ws://127.0.0.1:4040/ws");

      ws.onOpen.listen((e) {
        print("已连接服务器 ${ws.url}");
        //保存昵称
        chatName = nameElement.value;
        //显示聊天界面
        initHTML(true);

        //向服务器发送客户端信息
        var newUser = {'code': 'connect', 'data': {'name': chatName}};
        //消息输入框设置为可用
        inputElement.disabled = false;
        ws.send(JSON.encode(newUser));
      });

      //监听接收到的Message数据
      ws.onMessage.listen(onMessage);

      //关闭事件,显示登陆界面,隐藏聊天界面,清空昵称输入框
      ws.onClose.listen((e) {
        print("服务器连接已断开");
        initHTML(false);
        nameElement.value = '';
      });

      //终止事件的派发
      e.stopPropagation();
    });
  }

  void onMessage(MessageEvent event) {
    Map message = JSON.decode(event.data);

    switch(message['code']) {
      case 'speak':
        addText(message['data']['name'], message['data']['text']);
        break;
      case 'list':
        addNames(message['data']);
        break;
      default:
        print("接收到Message Data:${event.data}");
    }
  }

  void initInputListen() {
    //当输入框数据改变并回车的时候,触发onChange事件
    inputElement.onChange.listen((e) {
      //待发送的消息
      String value = inputElement.value;
      inputElement.value = '';

      print(value);
      var request = {
        'code': 'speak',
        'data': {
          'name': chatName,
          'text': value
        }
      };
      //发送数据
      ws.send(JSON.encode(request));
    });
  }

/////////////////////////////////////////////
  //刷新消息显示框
  void addText(String name, String text) {
    var result = new DivElement();
    result.innerHtml = "$name : $text";
    context.children.add(result);
  }

  //刷新在线列表
  void addNames(List names) {
    print("刷新在线用户列表:$names");

    chatListElement.children.clear();
    for(var name in names) {
      addName(name);
    }
  }

  void addName(String name) {
    var result = new DivElement();
    result.innerHtml = "$name";
    chatListElement.children.add(result);
  }
/////////////////////////////////////////////

  /**
   * isConnected
   *   true:隐藏登陆界面,显示聊天界面
   *   false:显示登陆界面,显示聊天界面
   */
  void initHTML(bool isConnected) {
    if(isConnected) {
      enterScreen.style.display = "none";
      chatScreen.style.display = "block";
      inputElement.focus();
    } else {
      enterScreen.style.display = "block";
      chatScreen.style.display = "none";
      nameElement.focus();
    }
  }
}

void main() {
  var client = new Client();
}

web/index.html

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Note 26</title>

    <script type="application/dart" src="index.dart"></script>
    <script src="packages/browser/dart.js"></script>

    <link rel="stylesheet" href="index.css">
  </head>

  <body>
    <h1>Note 26:WebSocket Chat Example</h1>

    <div id="enter_screen">
      <div class="box">
        <input class="form-control speak"  placeholder="请输入昵称" type="text" value="" id="name" />
      </div>
    </div>

    <div id="chat_screen" style="display: none;">
          <div class="box mheight">
            <h3>在线列表</h3>
            <div id="nameslist"></div>
          </div>

          <div id="context"></div>

          <input class="form-control speak" type="text" value="" id="speak" disabled/>
    </div>

  </body>
</html>

web/index.css

#context-wrapper {
  min-height: 400px;
}

#context {
  min-height: 220px;
  bottom: 0;
}

.box {
  border: 1px solid #d8d8d8;
  border-radius: 3px;
  background-color: #fff;
  border-bottom-width: 2px;
  margin-right: 20px;
  padding: 20px 20px;
}

.mheight {
  min-height: 200px;
  float:left;
}

运行结果:

HTTP的问题主要在于基于请求—响应模式
服务端不能主动推送数据到客户端
为了保证数据实时同步,实际应用中常使用长轮询来工作
但每次通信都需要一个HTTP请求
如果数据交互频繁、服务器就需要处理很多的HTTP请求
而且,HTTP请求头中会发送Cookie等一些不必要的数据
往往造成Header比数据本身还多,大部分还是重复无用的
这使得效率偏低不说,还浪费大量的宽带

而WebSocket使得浏览器中也可以使用套接字
并且很友好的完成长连接的全双工通信
重要的是稳定,长轮询在遇到网络问题后
想要在不刷新页面的情况下恢复通信,很难
而WebSocket提供了onClose等事件,为通信提供了保障
不过WebSocket的最大问题还是浏览器的支持,兼容性是硬伤

如果你项目面向的用户群使用的浏览器大部分为低版本IE
就避免使用WebSocket
另外,WebSocket是长连接
如果客户端的程序没有数据实时同步的需求
也没有必要使用WebSocket
因为长连接会带来一定的服务器内存开销
然后,如果Isolate、Rpc或异步请求能轻松搞定问题的话
就完全没必要兴师动众的折腾WebSocket
(http库的BrowserClient提供有部分异步请求的功能)

最后,Force库是一个不错的Web实时通信框架
基于WebSocket和Polling长轮询实现
感兴趣的可以看看Pub或Github

dart:io这部分内容终于告一段落了,散花!