flutter WebSocket 套接字

创业
siman 2020-3-30 10:02:54 5859 1 来自河北
WebSocket允许Mobile App与服务器之间的实时通信。您将通过一个基本的实时多人游戏实例来学习这些原理。
难度:中级
前言
除非您正在构建不需要与服务器交换信息的应用程序,否则必须在移动应用程序和服务器之间进行通信。
HTTP客户端服务器
通常,移动应用程序与服务器之间的通信是通过HTTP协议实现的,藏宝库 28xin.com其中客户端(=移动应用程序)通过Internet向服务器发送HTTP请求。服务器处理完请求后,它将答案返回给客户端并关闭连接。

这是一种单向通信,其中通信必须始终由客户端发起,并且包含一次交换(发送->接收->关闭)。如果客户端没有要求,服务器就不可能将任何东西发送给客户端。
在大多数情况下,这种通信方式就足够了,甚至推荐使用。但是,如果您需要非常频繁地轮询服务器,移动大量数据,根据服务器端可能发生的事件做出反应,则这种通信方式可能会成为瓶颈。
Web套接字
其他一些类型的应用程序,例如聊天,实时游戏,拍卖 ……可能需要:
  • 具有比单个请求/响应方案更长的一段时间,客户端和服务器之间保持开放状态的通信通道
  • 具有双向数据传输,其中服务器可以在没有客户端请求/轮询的情况下将数据发送到客户端
  • 支持数据流
WebSockets允许客户端打开并保持与服务器的连接。

Web套接字是客户端和服务器之间通过网络进行的TCP套接字连接,它允许全双工通信,换句话说:数据可以在两个方向上同时传输。一个TCP套接字是一个端点实例,通过IP地址和端口来定义。
有关WebSockets技术方面的详细文档,请参考RFC6455。
Flutter中的WebSockets
所述web_socket_channel包提供包装为WebSocket连接。
要安装此软件包,请将以下行添加到您的pubspec.yaml文件中:
dependencies:
  web_socket_channel: "^1.0.8"
要导入软件包,请将以下2种导入添加到您的.dart文件中:
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;
如何连接到服务器?
要连接到服务器,您至少需要知道:
  • 它的TCP套接字地址(例如192.168.1.25:1234
  • Web套接字处理程序的名称(例如“ / svc / websockets”
  • URL方案(对于纯文本通信,为“ ws//”;对于加密通道,为“ wss//”
IOWebSocketChannel channel = new IOWebSocketChannel.connect("ws://192.168.1.25:1234/svc/websockets");
引擎盖下
这条简单的线向服务器发送常规的HTTP请求,该请求还包含“ Upgrade ”标头,以通知服务器客户端希望建立WebSocket连接。该请求启动“ 握手 ”过程。如果服务器支持WebSocket协议,则它同意升级并通过响应中的“ Upgrade ”标头进行通信。握手完成后,初始HTTP连接将被使用相同基础TCP / IP连接的WebSocket连接取代。
该通道现已开放,随时可以使用。
安全吗?
好吧,如果仅依赖基本协议“ ws://”,则它与普通的“ http://”协议一样安全。
因此,强烈建议使用与HTTPS(TLS / SSL)相同的加密。
要建立这种安全的通信,您需要:
  • 服务器上安装的SSL证书
  • 使用wss//而不是ws//
  • 指向SSL端口
通过对客户端进行身份验证来扩展安全性
如前一篇文章所述,您还可以使用“ 令牌 ” 的概念,并且仅允许经过身份验证的客户端进行连接。
因此,您还可以在连接期间将一些额外的数据传递到标头。
以下示例说明了如何在请求标头中传递额外的数据。
  ///
  /// Open a new secured WebSocket communication
  ///
  IOWebSocketChannel channel;
        
    try {
      channel = new IOWebSocketChannel.connect(
                  "wss://192.168.1.25:443/svc/websockets",
                  headers: {
                    'X-DEVICE-ID': deviceIdentity,
                    'X-TOKEN': token,
                    'X-APP-ID': applicationId,
                  });
          ...
    } catch(e){
      ///
      /// An error occurred
      ///
    }
如何关闭通讯?
客户端可以使用以下命令关闭通信通道:
channel.sink.close();
如何向服务器发送消息?
channel.sink.add('the data I need to send to the server');
如何处理来自服务器的通信?
为了能够接受服务器发出的传入消息,您需要从Stream订阅(侦听)事件。
签名如下:
StreamSubscription<T> channel.stream.listen(
        void onData(T event),
        {
                Function onError,
                void onDone(),
                bool cancelOnError
        }
);
哪里:
  • onData:当从服务器接收到某些数据时调用的方法
  • onError:处理任何错误的方法
  • onDone:用于处理通信关闭的方法(例如,从Server
  • cancelOnError:(默认为false)。如果设置为true,则在第一个错误事件发生时自动关闭StreamSubscription
因此,典型的实现将是:
channel.stream.listen(
        (message){
                // handling of the incoming messages
        },
        onError: function(error, StackTrace stackTrace){
                // error handling
        },
        onDone: function(){
                // communication has been closed
        }
);

让我们实践一下
与您可以在Internet上找到的大多数示例不同藏宝库 28xin.com,我没有重写通常的Chat应用程序来解释该主题。相反,我们将构建实时多人游戏的框架。像井字游戏一样。
该示例包括:
  • NodeJS编写的Websockets服务器
  • 一个移动应用程序游戏,其中:
    • 用户将提供他们的名字以加入游戏;
    • 所有玩家的名单将实时刷新;
    • 一个用户将选择另一位玩家来开始新游戏;
    • 会模拟地通知两名玩家进入Tic-Tac-Toe棋盘游戏;
    • 玩家将可以:
      • 辞职;
      • 播放及其动作将直接在另一个播放器板上可见。
免责声明
本示例仅用于说明主题。我们要编写的游戏框架非常基本,还不完整,并且需要进行大量改进,验证……
高级视图
首先,我们需要从客户端和服务器端来描述游戏。
客户端
客户端(移动应用程序本身)将包含2个屏幕。
  • 屏幕1
    该屏幕将允许用户:
    • 输入玩家名称并加入游戏(=动作:join
    • 查看所有加入游戏的玩家的列表(=动作:players_list
    • 选择一个玩家并开始一个新游戏(=动作:new_game)然后,这两个玩家将自动进入第二个屏幕。
  • 屏幕2
    该屏幕将显示:
    • 对手的名字
    • 辞职按钮。如果用户点击此按钮,玩家将退出游戏(=动作:resign),然后将两个玩家带回到屏幕1
    • 一个井字游戏网格,由9个单元组成。
    • 当玩家单击网格的某个单元格时,该玩家的符号(“ X”“ O”)将显示在两个玩家的移动应用中
服务器端
服务器端只会:
  • 记录所有球员的名单,并为他们提供唯一的ID
  • 当新玩家加入时,将该列表广播给所有玩家
  • 记录新游戏的玩家藏宝库 28xin.com
  • 将一个玩家的动作传达给另一位玩家
通讯协议
为了在播放器和服务器之间进行通信,我们需要定义某种语言。我们称此为“ 协议 ”。
将发送到服务器或从服务器发送到Mobile App的所有消息都将遵循此协议,其中包括:
  • 一种行为
  • 一些数据
为了方便起见,我将使用以下JSON对象(= Map):
{
  "action": "action name",
  "data": "data to be sent"
}
下图显示了该协议:

服务器端:WebSocket服务器
对于这个非常基本的WebSocket服务器,我选择使用“ WebSocket ”包在NodeJS中实现。
先决条件
为了进行这项工作,您需要:
  • 安装NodeJS(版本> 6)(有关安装NodeJS的更多详细信息,请参考NodeJS网站)。
  • 安装websocket软件包,使用npm install websocket –save安装软件包
源代码
源代码是此WebSocket服务器非常基础。首先让我们看一下,解释与代码一起进行。
/**
* Parameters
*/
var webSocketsServerPort = 34263; // Adapt to the listening port number you want to use
/**
* Global variables
*/
// websocket and http servers
var webSocketServer = require('websocket').server;
var http = require('http');
/**
* HTTP server to implement WebSockets
*/
var server = http.createServer(function(request, response) {
  // Not important for us. We're writing WebSocket server,
  // not HTTP server
});
server.listen(webSocketsServerPort, function() {
  console.log((new Date()) + " Server is listening on port "
      + webSocketsServerPort);
});

/**
* WebSocket server
*/
var wsServer = new webSocketServer({
  // WebSocket server is tied to a HTTP server. WebSocket
  // request is just an enhanced HTTP request. For more info
  httpServer: server
});

// This callback function is called every time someone
// tries to connect to the WebSocket server
wsServer.on('request', function(request) {
   
    var connection = request.accept(null, request.origin);
        
  //
  // New Player has connected.  So let's record its socket
  //
    var player = new Player(request.key, connection);

  //
  // Add the player to the list of all players
  //
    Players.push(player);

  //
  // We need to return the unique id of that player to the player itself
  //
    connection.sendUTF(JSON.stringify({action: 'connect', data: player.id}));

  //
  // Listen to any message sent by that player
  //
    connection.on('message', function(data) {

    //
    // Process the requested action
    //
      var message = JSON.parse(data.utf8Data);
      switch(message.action){
        //
        // When the user sends the "join" action, he provides a name.
        // Let's record it and as the player has a name, let's
        // broadcast the list of all the players to everyone
        //
          case 'join':
            player.name = message.data;
            BroadcastPlayersList();
            break;

        //
        // When a player resigns, we need to break the relationship
        // between the 2 players and notify the other player
        // that the first one resigned
        //
          case 'resign':
          console.log('resigned');
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'resigned'}));

            setTimeout(function(){
              Players[player.opponentIndex].opponentIndex = player.opponentIndex = null;
            }, 0);
            break;

        //
        // A player initiates a new game.
        // Let's create a relationship between the 2 players and
        // notify the other player that a new game starts
        //
          case 'new_game':
            player.setOpponent(message.data);
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'new_game', 'data': player.name}));
            break;

        //
        // A player sends a move.  Let's forward the move to the other player
        //
          case 'play':
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'play', 'data': message.data}));
            break;
      }
    });

  // user disconnected
  connection.on('close', function(connection) {
    // We need to remove the corresponding player
    // TODO
  });
});

// -----------------------------------------------------------
// List of all players
// -----------------------------------------------------------
var Players = [];

function Player(id, connection){
    this.id = id;
    this.connection = connection;
    this.name = "";
    this.opponentIndex = null;
    this.index = Players.length;
}

Player.prototype = {
    getId: function(){
        return {name: this.name, id: this.id};
    },
    setOpponent: function(id){
        var self = this;
        Players.forEach(function(player, index){
            if (player.id == id){
                self.opponentIndex = index;
                Players[index].opponentIndex = self.index;
                return false;
            }
        });
    }
};

// ---------------------------------------------------------
// Routine to broadcast the list of all players to everyone
// ---------------------------------------------------------
function BroadcastPlayersList(){
    var playersList = [];
    Players.forEach(function(player){
        if (player.name !== ''){
            playersList.push(player.getId());
        }
    });

    var message = JSON.stringify({
        'action': 'players_list',
        'data': playersList
    });

    Players.forEach(function(player){
        player.connection.sendUTF(message);
    });
}
客户端:移动应用
现在我们有了服务器,让我们考虑Flutter应用程序。
Websocket助手
让我们从实现WebSocket Helper类开始。
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/io.dart';

///
/// Application-level global variable to access the WebSockets
///
WebSocketsNotifications sockets = new WebSocketsNotifications();

///
/// Put your WebSockets server IP address and port number
///
const String _SERVER_ADDRESS = "ws://192.168.1.45:34263";

class WebSocketsNotifications {
  static final WebSocketsNotifications _sockets = new WebSocketsNotifications._internal();

  factory WebSocketsNotifications(){
    return _sockets;
  }

  WebSocketsNotifications._internal();

  ///
  /// The WebSocket "open" channel
  ///
  IOWebSocketChannel _channel;

  ///
  /// Is the connection established?
  ///
  bool _isOn = false;
  
  ///
  /// Listeners
  /// List of methods to be called when a new message
  /// comes in.
  ///
  ObserverList<Function> _listeners = new ObserverList<Function>();

  /// ----------------------------------------------------------
  /// Initialization the WebSockets connection with the server
  /// ----------------------------------------------------------
  initCommunication() async {
    ///
    /// Just in case, close any previous communication
    ///
    reset();

    ///
    /// Open a new WebSocket communication
    ///
    try {
      _channel = new IOWebSocketChannel.connect(_SERVER_ADDRESS);

      ///
      /// Start listening to new notifications / messages
      ///
      _channel.stream.listen(_onReceptionOfMessageFromServer);
    } catch(e){
      ///
      /// General error handling
      /// TODO
      ///
    }
  }

  /// ----------------------------------------------------------
  /// Closes the WebSocket communication
  /// ----------------------------------------------------------
  reset(){
    if (_channel != null){
      if (_channel.sink != null){
        _channel.sink.close();
        _isOn = false;
      }
    }
  }

  /// ---------------------------------------------------------
  /// Sends a message to the server
  /// ---------------------------------------------------------
  send(String message){
    if (_channel != null){
      if (_channel.sink != null && _isOn){
        _channel.sink.add(message);
      }
    }
  }

  /// ---------------------------------------------------------
  /// Adds a callback to be invoked in case of incoming
  /// notification
  /// ---------------------------------------------------------
  addListener(Function callback){
    _listeners.add(callback);
  }
  removeListener(Function callback){
    _listeners.remove(callback);
  }

  /// ----------------------------------------------------------
  /// Callback which is invoked each time that we are receiving
  /// a message from the server
  /// ----------------------------------------------------------
  _onReceptionOfMessageFromServer(message){
    _isOn = true;
    _listeners.forEach((Function callback){
      callback(message);
    });
  }
}
此类是处理基于WebSocket的通信的Singleton
我将其实现为Singleton,以允许其在整个应用程序中重用,而不必关心连接。导入此.dart文件的简单事实足以使用套接字(应用程序级全局变量)。
藏宝库 28xin.com游戏通讯助手类
此类负责处理与游戏有关的网络套接字通信。由于将由2个屏幕使用,因此该类也实现为Singleton
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'websockets.dart';

///
/// Again, application-level global variable
///
GameCommunication game = new GameCommunication();

class GameCommunication {
  static final GameCommunication _game = new GameCommunication._internal();

  ///
  /// At first initialization, the player has not yet provided any name
  ///
  String _playerName = "";

  ///
  /// Before the "join" action, the player has no unique ID
  ///
  String _playerID = "";

  factory GameCommunication(){
    return _game;
  }

  GameCommunication._internal(){
    ///
    /// Let's initialize the WebSockets communication
    ///
    sockets.initCommunication();

    ///
    /// and ask to be notified as soon as a message comes in
    ///
    sockets.addListener(_onMessageReceived);
  }

  ///
  /// Getter to return the player's name
  ///
  String get playerName => _playerName;

  /// ----------------------------------------------------------
  /// Common handler for all received messages, from the server
  /// ----------------------------------------------------------
  _onMessageReceived(serverMessage){
    ///
    /// As messages are sent as a String
    /// let's deserialize it to get the corresponding
    /// JSON object
    ///
    Map message = json.decode(serverMessage);

    switch(message["action"]){
      ///
      /// When the communication is established, the server
      /// returns the unique identifier of the player.
      /// Let's record it
      ///
      case 'connect':
        _playerID = message["data"];
        break;

      ///
      /// For any other incoming message, we need to
      /// dispatch it to all the listeners
      ///
      default:
        _listeners.forEach((Function callback){
          callback(message);
        });
        break;
    }
  }

  /// ----------------------------------------------------------
  /// Common method to send requests to the server
  /// ----------------------------------------------------------
  send(String action, String data){
    ///
    /// When a player joins, we need to record the name
    /// he provides
    ///
    if (action == 'join'){
      _playerName = data;
    }

    ///
    /// Send the action to the server
    /// To send the message, we need to serialize the JSON
    ///
    sockets.send(json.encode({
      "action": action,
      "data": data
    }));
  }

  /// ==========================================================
  ///
  /// Listeners to allow the different pages to be notified
  /// when messages come in
  ///
  ObserverList<Function> _listeners = new ObserverList<Function>();

  /// ---------------------------------------------------------
  /// Adds a callback to be invoked in case of incoming
  /// notification
  /// ---------------------------------------------------------
  addListener(Function callback){
    _listeners.add(callback);
  }
  removeListener(Function callback){
    _listeners.remove(callback);
  }
}
为什么选择在WebSockets Helper之上实现游戏通讯助手?
仅仅是因为,与游戏相关的所有逻辑都是集中的。另外,因为例如,如果我们想通过添加一些聊天功能来扩展游戏,我们只需创建一个特定的类,该类也将依赖于相同的WebSockets Helper。

屏幕1:用户加入并启动新游戏的位置
该屏幕负责:
  • 让用户加入游戏,提供一个名称
  • 维护所有球员的实时名单
  • 让用户与其他玩家开始新游戏
import 'package:flutter/material.dart';
import 'game_communication.dart';
import 'game_page.dart';

class StartPage extends StatefulWidget {
  @override
  _StartPageState createState() => _StartPageState();
}

class _StartPageState extends State<StartPage> {
  static final TextEditingController _name = new TextEditingController();
  String playerName;
  List<dynamic> playersList = <dynamic>[];

  @override
  void initState() {
    super.initState();
    ///
    /// Ask to be notified when messages related to the game
    /// are sent by the server
    ///
    game.addListener(_onGameDataReceived);
  }

  @override
  void dispose() {
    game.removeListener(_onGameDataReceived);
    super.dispose();
  }

  /// -------------------------------------------------------------------
  /// This routine handles all messages that are sent by the server.
  /// In this page, only the following 2 actions have to be processed
  ///  - players_list
  ///  - new_game
  /// -------------------------------------------------------------------
  _onGameDataReceived(message) {
    switch (message["action"]) {
      ///
      /// Each time a new player joins, we need to
      ///   * record the new list of players
      ///   * rebuild the list of all the players
      ///
      case "players_list":
        playersList = message["data"];
        
        // force rebuild
        setState(() {});
        break;

      ///
      /// When a game is launched by another player,
      /// we accept the new game and automatically redirect
      /// to the game board.
      /// As we are not the new game initiator, we will be
      /// using the "O"
      ///
      case 'new_game':
        Navigator.push(context, new MaterialPageRoute(
          builder: (BuildContext context)
                      => new GamePage(
                            opponentName: message["data"], // Name of the opponent
                            character: 'O',
                        ),
        ));
        break;
    }
  }

  /// -----------------------------------------------------------
  /// If the user has not yet joined, let the user enter
  /// his/her name and join the list of players
  /// -----------------------------------------------------------
  Widget _buildJoin() {
    if (game.playerName != "") {
      return new Container();
    }
    return new Container(
      padding: const EdgeInsets.all(16.0),
      child: new Column(
        children: <Widget>[
          new TextField(
            controller: _name,
            keyboardType: TextInputType.text,
            decoration: new InputDecoration(
              hintText: 'Enter your name',
              contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
              border: new OutlineInputBorder(
                borderRadius: new BorderRadius.circular(32.0),
              ),
              icon: const Icon(Icons.person),
            ),
          ),
          new Padding(
            padding: const EdgeInsets.all(8.0),
            child: new RaisedButton(
              onPressed: _onGameJoin,
              child: new Text('Join...'),
            ),
          ),
        ],
      ),
    );
  }

  /// ------------------------------------------------------
  /// The user wants to join, so let's send his/her name
  /// As the user has a name, we may now show the other players
  /// ------------------------------------------------------
  _onGameJoin() {
    game.send('join', _name.text);
        
    /// Force a rebuild
    setState(() {});
  }

  /// ------------------------------------------------------
  /// Builds the list of players
  /// ------------------------------------------------------
  Widget _playersList() {
    ///
    /// If the user has not yet joined, do not display
    /// the list of players
    ///
    if (game.playerName == "") {
      return new Container();
    }

    ///
    /// Display the list of players.
    /// For each of them, put a Button that could be used
    /// to launch a new game
    ///
    List<Widget> children = playersList.map((playerInfo) {
        return new ListTile(
          title: new Text(playerInfo["name"]),
          trailing: new RaisedButton(
            onPressed: (){
              _onPlayGame(playerInfo["name"], playerInfo["id"]);
            },
            child: new Text('Play'),
          ),
        );
      }).toList();

    return new Column(
      children: children,
    );
  }

  /// --------------------------------------------------------------
  /// We launch a new Game, we need to:
  ///    * send the action "new_game", together with the ID
  ///      of the opponent we choosed
  ///    * redirect to the game board
  ///      As we are the game initiator, we will play with the "X"
  /// --------------------------------------------------------------
  _onPlayGame(String opponentName, String opponentId){
    // We need to send the opponentId to initiate a new game
    game.send('new_game', opponentId);
        
    Navigator.push(context, new MaterialPageRoute(
      builder: (BuildContext context)
                  => new GamePage(
                      opponentName: opponentName,
                      character: 'X',
                    ),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return new SafeArea(
      bottom: false,
      top: false,
      child: Scaffold(
        appBar: new AppBar(
          title: new Text('TicTacToe'),
        ),
        body: SingleChildScrollView(
          child: new Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              _buildJoin(),
              new Text('List of players:'),
              _playersList(),
            ],
          ),
        ),
      ),
    );
  }
}

屏幕2:游戏板
第二个屏幕负责:
  • AppBar中显示对手的名字
  • 允许用户辞职,经由辞职按钮
  • 实时显示游戏以及所有动作
  • 允许用户进行移动并将移动发送给对手。
import 'package:flutter/material.dart';
import 'game_communication.dart';

class GamePage extends StatefulWidget {
  GamePage({
    Key key,
    this.opponentName,
    this.character,
  }): super(key: key);

  ///
  /// Name of the opponent
  ///
  final String opponentName;
  
  ///
  /// Character to be used by the player for his/her moves ("X" or "O")
  ///
  final String character;

  @override
  _GamePageState createState() => _GamePageState();
}

class _GamePageState extends State<GamePage> {

  ///
  /// One game in terms of grid cells.
  /// When the user plays, one of this cells is filled with "X" or "O"
  ///
  List<String> grid = <String>["","","","","","","","",""];

  @override
  void initState(){
    super.initState();
    ///
    /// Ask to be notified when a message from the server
    /// comes in.
    ///
    game.addListener(_onAction);
  }

  @override
  void dispose(){
    game.removeListener(_onAction);
    super.dispose();
  }

  /// ---------------------------------------------------------
  /// The opponent took an action
  /// Handler of these actions
  /// ---------------------------------------------------------
  _onAction(message){
    switch(message["action"]){
      ///
      /// The opponent resigned, so let's leave this screen
      ///
      case 'resigned':
          Navigator.of(context).pop();
        break;

      ///
      /// The opponent played a move.
      /// So record it and rebuild the board
      ///
      case 'play':
          var data = (message["data"] as String).split(';');
          grid[int.parse(data[0])] = data[1];

          // Force rebuild
          setState((){});
        break;
    }
  }

  /// ---------------------------------------------------------
  /// This player resigns
  /// We need to send this notification to the other player
  /// Then, leave this screen
  /// ---------------------------------------------------------
  _doResign(){
    game.send('resign', '');
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return new SafeArea(
      top: false,
      bottom: false,
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Game against: ${widget.opponentName}', style: new TextStyle(fontSize: 16.0)),
          actions: <Widget>[
            new RaisedButton(
              onPressed: _doResign,
              child: new Text('Resign'),
            ),
          ]
        ),
        body: _buildBoard(),
      ),
    );
  }

  /// --------------------------------------------------------
  /// Builds the Game Board.
  /// --------------------------------------------------------
  Widget _buildBoard(){
    return new SafeArea(
      top: false,
      bottom: false,
      child: new GridView.builder(
        gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
        ),
        itemCount: 9,
        itemBuilder: (BuildContext context, int index){
          return _gridItem(index);
        },
      ),
    );
  }

  Widget _gridItem(int index){
    Color color = grid[index] == "X" ? Colors.blue : Colors.red;

    return new InkWell(
      onTap: () {
        ///
        /// The user taps a cell.
        /// If the latter is empty, let's put this player's character
        /// and notify the other player.
        /// Repaint the board
        ///
        if (grid[index] == ""){
          grid[index] = widget.character;

          ///
          /// To send a move, we provide the cell index
          /// and the character of this player
          ///
          game.send('play', '$index;${widget.character}');

          /// Force the board repaint
          setState((){});
        }
      },
      child: new GridTile(
        child: new Card(
          child: new FittedBox(
            fit: BoxFit.contain,
            child: new Text(grid[index], style: new TextStyle(fontSize: 50.0, color: color,))
          ),
        ),
      ),
    );
  }
}

主要的
最后,我们有只启动第一个屏幕的主例程。
import 'package:flutter/material.dart';
import 'start_page.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'WebSockets Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new StartPage(),
    );
  }
}

结果
以下视频显示了运行此示例应用程序的2个移动设备。如您所见,交互是实时的。
28xin.com


结论WebSockets易于实现,并且在移动应用需要处理实时和全双工通信时必不可少。
我希望本文通过这个实际示例来揭开WebSockets概念的神秘面纱,该示例再次旨在演示使用WebSockets的通信。
请继续关注新文章和愉快的编码。
您需要登录后才可以回帖 登录 | 立即注册
感谢分享
2022-8-4 21:48:55 回复

Powered by CangBaoKu v1.0 小黑屋 藏宝库It社区 ( 冀ICP备14008649号 )

GMT+8, 2024-5-16 18:24 , Processed in 0.251161 second(s), 35 queries . © 2003-2025 cbk Team.

快速回复 返回顶部 返回列表