Flutter・DartでWebSocket通信を簡単に実装できるweb_socket

Flutterアプリでリアルタイム通信を実装したい場合、WebSocketを利用するケースがあります。

例えば、

  • チャットアプリ
  • オンラインゲーム
  • リアルタイム通知
  • 株価や為替情報の配信
  • ライブコメント
  • AIチャット
  • IoTデバイスとの通信

など、サーバーとクライアントが継続的にデータをやり取りするアプリではWebSocketが便利です。

DartやFlutterでWebSocket通信を実装する場合、これまで広く利用されてきたパッケージの一つに web_socket_channel があります。

今回紹介する web_socket は、Dartチームが提供するWebSocket通信ライブラリです。

複数のプラットフォームに対応しながら、WebSocket通信のインターフェースをシンプルかつ一貫した形で提供することを目的としています。

この記事では、web_socket の基本的な使い方から、テキスト通信、バイナリ通信、接続終了、エラーハンドリング、web_socket_channel との違いまで解説します。


web_socketとは?

web_socket は、DartやFlutterアプリからWebSocket通信を行うためのパッケージです。

WebSocketは、一度サーバーとの接続を確立すると、その接続を維持したまま双方向にデータを送受信できる通信方式です。

通常のHTTP通信では、

クライアント
    ↓ HTTP Request
サーバー
    ↓ HTTP Response
クライアント

というリクエスト・レスポンス型の通信になります。

一方、WebSocketでは、

クライアント ←────────→ サーバー
             常時接続

という形で通信できます。

そのため、サーバー側からリアルタイムにデータを送信できるのが特徴です。

web_socket は、このWebSocket通信をDartのAPIから扱いやすくすることを目的としています。


web_socketの特徴

主な特徴は以下の通りです。

機能対応
WebSocket通信
テキスト送信
バイナリ送信
イベントストリーム
接続終了イベント
Web対応
Android
iOS
macOS
Windows
Linux
BrowserWebSocket
CupertinoWebSocket
IOWebSocket
OkHttpWebSocket△ 実験的

現在の web_socket には複数の実装が用意されており、各実装で一貫したWebSocket APIを利用できるように設計されています。


インストール

pubspec.yaml に追加します。

dependencies:
  web_socket: ^1.0.1

または、以下のコマンドでも追加できます。

flutter pub add web_socket

Dartパッケージとして利用する場合も同様に、

dart pub add web_socket

で追加できます。

コードから利用する場合は、

import 'package:web_socket/web_socket.dart';

をインポートします。


WebSocketへ接続する

基本的な接続方法は非常にシンプルです。

import 'package:web_socket/web_socket.dart';

void main() async {
  final socket = await WebSocket.connect(
    Uri.parse(
      'wss://example.com/socket',
    ),
  );
}

WebSocket.connect() にWebSocketサーバーのURLを渡します。

WebSocketでは、

ws://

または、

wss://

というURLスキームを利用します。

一般的には暗号化された通信を行うため、実際のサービスでは wss:// を利用するケースが多くなります。

接続処理は Future を返すため、await を利用して接続完了を待つことができます。


WebSocketのイベントを受信する

WebSocketから送られてくるデータは、events から受信できます。

socket.events.listen((event) {
  print(event);
});

events はストリームとして提供されているため、DartのStream APIを利用してリアルタイムにイベントを受信できます。

例えば、

サーバー
    ↓
WebSocket
    ↓
events
    ↓
listen()
    ↓
Flutterアプリ

という流れになります。


テキストデータを受信する

テキストデータを受信した場合は、TextDataReceived を利用できます。

socket.events.listen((event) {
  switch (event) {
    case TextDataReceived(text: final text):
      print('Received: $text');

    case BinaryDataReceived(data: final data):
      print('Received binary data');

    case CloseReceived(
        code: final code,
        reason: final reason,
      ):
      print(
        'Connection closed: '
        '$code [$reason]',
      );
  }
});

Dartのパターンマッチングを利用してイベントの種類ごとに処理を分けられます。

例えばチャットアプリなら、

サーバー
    ↓
"Hello"
    ↓
TextDataReceived
    ↓
メッセージとして表示

という処理ができます。


テキストデータを送信する

テキストを送信する場合は、sendText() を利用します。

socket.sendText(
  'Hello Dart WebSockets!',
);

例えばチャットメッセージを送信するなら、

void sendMessage(
  WebSocket socket,
  String message,
) {
  socket.sendText(message);
}

のように実装できます。

サーバー側でJSONを受け取る設計なら、jsonEncode() と組み合わせることもできます。

import 'dart:convert';

socket.sendText(
  jsonEncode({
    'type': 'message',
    'text': 'Hello',
  }),
);

このようにすると、WebSocket上でJSON形式のデータをやり取りできます。


JSONを使ったリアルタイム通信

実際のアプリでは、単純な文字列ではなくJSON形式で通信することが多いでしょう。

例えばサーバーから、

{
  "type": "message",
  "user": "Taro",
  "text": "Hello"
}

というデータが送られてきた場合、

import 'dart:convert';

socket.events.listen((event) {
  switch (event) {
    case TextDataReceived(text: final text):
      final data =
          jsonDecode(text);

      print(data['type']);
      print(data['text']);

    case BinaryDataReceived(data: final data):
      print(data);

    case CloseReceived(
        code: final code,
        reason: final reason,
      ):
      print(
        'Closed: $code [$reason]',
      );
  }
});

のように処理できます。

これにより、WebSocketを使ったAPIのような通信を構築できます。


バイナリデータを受信する

WebSocketではテキストだけでなくバイナリデータも送受信できます。

web_socket では、バイナリデータを BinaryDataReceived として受信できます。

socket.events.listen((event) {
  switch (event) {
    case BinaryDataReceived(data: final data):
      print(
        'Received ${data.length} bytes',
      );

    case TextDataReceived(text: final text):
      print(text);

    case CloseReceived(
        code: final code,
        reason: final reason,
      ):
      print('Closed');
  }
});

例えば、

  • バイナリファイル
  • 音声データ
  • センサーデータ
  • 独自プロトコル

などをWebSocket経由で送受信する用途に利用できます。


接続が終了したことを検知する

WebSocket接続が終了すると、CloseReceived イベントを受信できます。

case CloseReceived(
    code: final code,
    reason: final reason,
  ):
  print(
    'Connection closed: '
    '$code [$reason]',
  );

例えば、

接続終了
    ↓
CloseReceived
    ↓
接続状態をDisconnectedへ変更
    ↓
UIを更新

という処理ができます。

チャットアプリなどでは、ユーザーに「接続が切断されました」と表示する処理にも利用できます。


WebSocketを閉じる

接続を終了する場合は、close() を利用します。

await socket.close();

例えばWidgetを破棄するときに接続を終了する場合は、

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

のような形で管理できます。

実際には close() がFutureを返すため、ライフサイクルに応じて適切にawaitする設計も検討するとよいでしょう。

WebSocketは接続を維持する仕組みなので、不要になった接続を明示的に閉じることが重要です。


WebSocketConnectionClosed

すでに閉じられたWebSocketに対してデータを送信したり、再度closeしたりすると、WebSocketConnectionClosed が発生します。

例えば、

try {
  socket.sendText(
    'Hello',
  );
} on WebSocketConnectionClosed catch (e) {
  print(
    'WebSocket is already closed',
  );
}

のようにエラーを処理できます。

以前のバージョンでは、接続終了後の操作で StateError が発生するケースがありましたが、現在はWebSocket固有の例外として扱えるようになっています。


WebSocketExceptionを処理する

WebSocket接続時には、接続失敗などの例外が発生する可能性があります。

そのため、接続処理では try-catch を利用するのがおすすめです。

try {
  final socket = await WebSocket.connect(
    Uri.parse(
      'wss://example.com/socket',
    ),
  );

  print('Connected');
} on WebSocketException catch (e) {
  print(
    'Connection failed: $e',
  );
}

例えば、

  • サーバーが停止している
  • URLが間違っている
  • ネットワークに接続できない
  • TLS接続に失敗した

といったケースに対応できます。


WebSocketの接続状態を管理する

リアルタイムアプリでは、接続状態をアプリ側で管理することが重要です。

例えば、

Connecting
    ↓
Connected
    ↓
Disconnected

という状態を管理します。

FlutterではState Managementと組み合わせて、

enum ConnectionStatus {
  connecting,
  connected,
  disconnected,
}

のような状態を用意してもよいでしょう。

例えば、

ConnectionStatus status =
    ConnectionStatus.connecting;

final socket = await WebSocket.connect(
  uri,
);

status =
    ConnectionStatus.connected;

接続が切れた場合は、

case CloseReceived():
  status =
      ConnectionStatus.disconnected;

という形でUIを更新できます。


チャットアプリへの利用

web_socket の代表的な用途がチャットアプリです。

例えば、

Flutterアプリ
    │
    │ WebSocket
    ▼
チャットサーバー
    │
    │ WebSocket
    ▼
別のユーザー

という構成を作れます。

メッセージを送信すると、

socket.sendText(
  jsonEncode({
    'type': 'message',
    'text': message,
  }),
);

サーバーから受信すると、

case TextDataReceived(text: final text):
  final data =
      jsonDecode(text);

  // チャット画面を更新

という処理ができます。

HTTPのポーリング方式と比較すると、サーバーから新しいメッセージをリアルタイムにPushできるため、チャットのような用途に適しています。


AIチャットアプリにも利用できる

生成AIアプリでもWebSocketは利用できます。

例えば、

Flutter
    ↓ WebSocket
AI Backend
    ↓
LLM
    ↓
生成結果
    ↓
WebSocket
    ↓
Flutter

という構成です。

AIのレスポンスをリアルタイムに受信して、

こんにちは
こんにちは、今日は
こんにちは、今日はどのような
こんにちは、今日はどのようなご用件でしょうか?

というようにUIへ逐次表示することもできます。

ただし、AI API側がWebSocketではなくSSEやHTTPストリーミングを採用している場合もあるため、バックエンドの通信方式に合わせて選択する必要があります。


リアルタイム通知

WebSocketを使えば、サーバーからアプリへリアルタイムにイベントを送信できます。

例えば、

新しい通知
    ↓
サーバー
    ↓
WebSocket
    ↓
Flutter
    ↓
通知バッジ更新

という処理が可能です。

SNSや業務システムなどで、

  • 新しいメッセージ
  • 新しいコメント
  • ステータス変更
  • タスク更新

などをリアルタイムに反映したい場合に利用できます。


WebSocketサーバーとの通信

web_socket はクライアント側のライブラリなので、別途WebSocketサーバーが必要です。

例えば、

Flutter App
    │
    │ WebSocket
    ▼
WebSocket Server
    │
    ├── Node.js
    ├── Dart
    ├── Go
    ├── Python
    └── Java

という構成になります。

FlutterアプリだけでWebSocket通信が完結するわけではありません。

サーバー側もWebSocketプロトコルに対応している必要があります。


web_socket_channelとの違い

DartやFlutterでWebSocketを扱う場合、web_socket_channel もよく利用されます。

両者の大きな違いは、API設計の考え方です。

web_socket は、WebSocket通信に特化したシンプルで一貫したAPIを提供することを目指しています。

一方、web_socket_channel はChannelベースの抽象化を提供しており、WebSocket以外の通信方式を含めたStream/Channel的な扱い方が特徴です。

web_socket では、

final socket =
    await WebSocket.connect(uri);

として接続し、

socket.events.listen(
  (event) {},
);

でイベントを受信します。

データ送信も、

socket.sendText(
  'Hello',
);

のように明示的です。

WebSocket通信そのものをシンプルに扱いたい場合は、web_socket のAPIは理解しやすいでしょう。


複数の実装を共通APIで利用できる

web_socket の特徴的な部分が、複数のWebSocket実装に対して共通のAPIを提供していることです。

現在は、

BrowserWebSocket
CupertinoWebSocket
IOWebSocket
OkHttpWebSocket

といった実装が用意されています。

これらの実装は同じインターフェースに従って動作するように設計されており、共通のコンフォーマンステストを通過しています。

そのため、プラットフォームごとの挙動差をできるだけ小さくしながらWebSocketを利用できます。


BrowserWebSocket

Web環境では、ブラウザのWebSocket APIを利用する実装があります。

Flutter WebでWebSocket通信を行う場合でも、同じ web_socket APIを利用できるため、プラットフォームごとのコード分岐を減らせる可能性があります。

例えば、

Flutter Mobile
    ↓
IOWebSocket

Flutter Web
    ↓
BrowserWebSocket

というように、環境に応じた実装が利用されます。


CupertinoWebSocket

Apple系プラットフォームでは、CupertinoWebSocket が利用できます。

iOSやmacOSなどAppleプラットフォームを対象にしたFlutterアプリでも、共通のWebSocket APIを利用できます。


IOWebSocket

Dart VMのIO環境では、IOWebSocket が利用されます。

FlutterのAndroidやiOSなど、ネイティブアプリケーションでWebSocket通信を行う際に利用される実装です。


OkHttpWebSocket

OkHttpWebSocket も提供されています。

ただし、現時点では実験的な実装という位置付けです。

そのため、通常のアプリ開発では、利用する環境や用途を確認した上で採用を検討した方がよいでしょう。


テストにも利用できる

WebSocketを利用するアプリでは、通信部分のテストも重要です。

web_socket にはテスト用途で利用できるFake WebSocketを作成する仕組みも用意されています。

これを利用することで、

Flutter UI
    ↓
WebSocket Repository
    ↓
Fake WebSocket

という構成で、実際のサーバーに接続せずWebSocket通信をテストできます。

例えば、

  • メッセージ受信
  • 接続終了
  • エラー発生
  • 再接続

などのケースをテストしやすくなります。

WebSocketを利用したアプリでは、ネットワーク環境に依存するテストを減らせるため、ユニットテストやWidgetテストとの相性も良いでしょう。


自動再接続は自分で実装する

注意したいのが、web_socket 自体は自動再接続機能を提供するライブラリではないという点です。

例えば、

接続
  ↓
ネットワーク切断
  ↓
WebSocket切断
  ↓
再接続

という処理が必要な場合は、アプリ側で実装する必要があります。

例えば、

Future<void> reconnect() async {
  while (true) {
    try {
      socket = await WebSocket.connect(uri);
      break;
    } catch (_) {
      await Future.delayed(
        const Duration(seconds: 3),
      );
    }
  }
}

のような再接続処理を実装できます。

ただし、実際のアプリでは単純なループではなく、

  • 最大リトライ回数
  • 指数バックオフ
  • ネットワーク状態
  • アプリのライフサイクル
  • 認証情報の更新

なども考慮する必要があります。


WebSocketとHTTPの使い分け

すべての通信をWebSocketにする必要はありません。

例えば、

ユーザー情報取得
    ↓
HTTP

商品一覧取得
    ↓
HTTP

リアルタイムチャット
    ↓
WebSocket

リアルタイム通知
    ↓
WebSocket

というように使い分けるのが一般的です。

WebSocketは常時接続を維持するため、リアルタイム性が必要ない通信までWebSocketにすると、システムが複雑になる可能性があります。


SSEとの違い

リアルタイム通信ではWebSocketだけでなく、SSE(Server-Sent Events)も選択肢になります。

大きな違いは通信方向です。

WebSocketは、

Client ←→ Server

の双方向通信ができます。

一方SSEは、

Client ← Server

というサーバーからクライアントへの一方向ストリーミングが基本です。

そのため、

チャット
↓
WebSocket

双方向ゲーム
↓
WebSocket

AIレスポンスのストリーミング
↓
SSEでも可能

サーバーからのイベント通知
↓
SSEでも可能

というように、用途に応じて選択するとよいでしょう。


メリット

シンプルなWebSocket API

接続、イベント受信、テキスト送信、バイナリ送信、終了という基本的な操作が分かりやすく整理されています。

Dart公式チームによるパッケージ

Dartチームが提供しているため、Dartエコシステムとの親和性が高い点も魅力です。

複数プラットフォームに対応

Android、iOS、Web、macOS、Windows、Linuxなど幅広い環境をサポートしています。

実装間の一貫性を重視

複数のWebSocket実装に対して共通のAPIを提供し、各実装で一貫した挙動を目指しています。

テキストとバイナリの両方に対応

一般的なJSON通信からバイナリ通信まで対応できます。

Streamベースで扱える

DartのStream APIを利用してイベントを受信できるため、Flutterの状態管理とも組み合わせやすくなっています。


注意点

WebSocketサーバーが必要

web_socket はクライアントライブラリなので、別途WebSocketに対応したサーバーが必要です。

自動再接続は自分で実装する

ネットワーク切断後の再接続やリトライ処理は、基本的にアプリ側で設計する必要があります。

認証機能は別途設計が必要

WebSocket接続時の認証やトークン更新などは、バックエンドと合わせて設計する必要があります。

WebSocketが必ず最適とは限らない

サーバーからの一方向配信だけなら、SSEなどの方がシンプルな場合もあります。

高レベルな機能は提供しない

チャットルーム、再接続、メッセージACK、JSONプロトコル、認証などは提供されません。

必要な機能はアプリケーション側やバックエンド側で実装する必要があります。


まとめ

web_socket は、Dart・FlutterアプリでWebSocket通信を実装するためのシンプルなパッケージです。

Dartチームによって開発されており、複数のプラットフォームで一貫したWebSocket APIを提供することを目的としています。

主な特徴として、

  • シンプルなWebSocket API
  • テキスト通信
  • バイナリ通信
  • Streamによるイベント受信
  • 接続終了イベント
  • Web対応
  • Android / iOS対応
  • macOS / Windows / Linux対応
  • 複数のWebSocket実装
  • テスト用Fake WebSocket

などが挙げられます。

特に、web_socket_channel とは異なるアプローチで、WebSocket通信そのものをシンプルかつ一貫したAPIで扱いたい場合に検討しやすいパッケージです。

チャットアプリやリアルタイム通知、オンラインゲーム、ライブコメント、AIアプリなど、サーバーとの双方向リアルタイム通信が必要なFlutterアプリでは活用できるでしょう。

一方で、自動再接続や認証、メッセージACK、JSONプロトコルなどの高レベルな機能は自分で設計する必要があります。

そのため、WebSocket通信の土台として web_socket を利用し、アプリ固有の通信レイヤーをその上に構築するという使い方が向いています。

Dart・Flutterの標準的なWebSocket通信ライブラリとして、今後のFlutterプロジェクトで選択肢の一つとして覚えておきたいパッケージです。

タイトルとURLをコピーしました