Flutter (81): Using WebSockets

Time: Column:Mobile & Frontend views:220

The HTTP protocol is stateless, meaning that clients can only initiate requests, and servers respond passively. The server cannot actively push content to the client, and once the server's response ends, the connection will be closed (see note). This makes real-time communication impossible. The WebSocket protocol was developed to solve the problem of real-time communication between clients and servers, and it is now supported by mainstream browsers. Therefore, web developers should be quite familiar with it. Flutter also provides a dedicated package to support the WebSocket protocol.

Note: Although the HTTP protocol can keep connections alive for a while using the keep-alive mechanism, the connection will eventually close. The keep-alive mechanism is primarily used to avoid frequent connection creation when requesting multiple resources from the same server. It essentially supports connection reuse technology, not real-time communication, and readers need to understand the difference between the two.

The WebSocket protocol is fundamentally a TCP-based protocol. It first initiates a special HTTP request for a handshake. If the server supports the WebSocket protocol, it will upgrade the protocol. WebSocket utilizes the TCP connection created during the HTTP handshake. Unlike HTTP, the WebSocket TCP connection is a long-lived connection (it does not close), allowing real-time communication between the server and client. For more details about the WebSocket protocol, readers can refer to the RFC document. Below, we will focus on how to use WebSocket in Flutter.

In the following example, we will connect to a test server provided by websocket.org, which will simply return the same message we send to it!

Note: Since the test server provided by websocket.org may not always be available, if readers encounter connection issues while running the example, they can write and start a WebSocket service locally for testing. Details on how to write a WebSocket service involve server-side development skills, and readers can find relevant tutorials online; this book will not elaborate on that.

81.1 Communication Steps Using WebSocket communication involves five steps:

  1. Connect to the WebSocket server.

  2. Listen for messages from the server.

  3. Send data to the server.

  4. Close the WebSocket connection.

1. Connecting to the WebSocket Server

The web_socket_channel package provides the tools we need to connect to a WebSocket server. This package offers a WebSocketChannel that allows us to both listen for messages from the server and send messages to it.

In Flutter, we can create a WebSocketChannel to connect to a server:

final channel = IOWebSocketChannel.connect('wss://echo.websocket.events');

Note: wss://echo.websocket.events is the test service address provided by flutter.cn.

2. Listening for Messages from the Server

Now that we have established a connection, we can listen for messages from the server. After we send a message to the test server, it will return the same message.

How do we receive messages and display them? In this example, we will use a StreamBuilder to listen for new messages and display them in a Text widget.

StreamBuilder(
  stream: widget.channel.stream,
  builder: (context, snapshot) {
    return Text(snapshot.hasData ? '${snapshot.data}' : '');
  },
);

WebSocketChannel provides a message Stream from the server. This Stream class is a fundamental class in the dart

package. It offers a way to listen for asynchronous events from a data source. Unlike Future, which returns a single asynchronous response, the Stream class can deliver many events over time. The StreamBuilder component connects to a Stream and notifies Flutter to rebuild the UI every time a message is received.


3. Sending Data to the Server

To send data to the server, we add messages to the sink provided by WebSocketChannel.

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

WebSocketChannel provides a StreamSink that sends messages to the server. The StreamSink class offers a general method for synchronously or asynchronously adding events to a data source.

4. Closing the WebSocket Connection

After we have used the WebSocket, we need to close the connection:

channel.sink.close();

81.2 Example Below is a complete example demonstrating the WebSocket communication process.

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() {
    // Create WebSocket connection
    channel = IOWebSocketChannel.connect('wss://echo.websocket.events');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("WebSocket (Echo)"),
      ),
      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) {
                // Handle network issues
                if (snapshot.hasError) {
                  _text = "Network error...";
                } 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();
  }
}

The example above is relatively straightforward and will not be elaborated on further. Now, let’s consider a question: what if we want to transmit binary data via WebSocket (for instance, receiving an image from the server)? We find that neither StreamBuilder nor Stream specifies a parameter for the received type, and there are no corresponding configurations when creating the WebSocket connection. It seems there’s no way to do it... However, it’s quite simple. To receive binary data, we still use StreamBuilder because all data sent over WebSocket is transmitted in frames, and each frame has a fixed format. The data type of each frame can be specified through the Opcode field, which indicates whether the current frame is of text type or binary type (among others). Therefore, when the client receives a frame, it already knows its data type, allowing Flutter to parse the correct type upon receiving the data. Thus, developers need not worry; when the server transmits data designated as binary, the type of snapshot.data in StreamBuilder will be List<int>, and if it is text, it will be String.