4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Serverpodでwebsocketを使ってみた

Last updated at Posted at 2023-06-25

成果物

Serverpodでチャットアプリを作り、ブラウザとPCアプリでチャットすることができました。
chat_app_by_flutter_1.gif

はじめに

Serverpodという開発フレームワークをご存知でしょうか?
Dartという共通言語でサーバからフロントまで構築でき、かつ、マルチプラットフォームに開発できるものです。
意外と流行っていない印象がありますが、面白そうなので使ってみました。
今回はマルチプラットフォームな簡易チャットアプリを作りました。

開発環境

OS: Ubuntu 22.04 LTS
Flutter: 3.10.4
Dart: 3.0.3

構築

serverpodのインストール

まだの方は、serverpodをインストールしてください。

exampleアプリの構築

まずは、serverpod createコマンドでアプリを作ります。

$ serverpod create chatapp
$ cd chatapp/chatapp_server
$ docker-compose up --build --detach

これで、バックエンドも含めて、exampleのアプリが立ち上がるのがびっくりです。
今回は分かりやすいように、このexampleのアプリを改修していきます。

websocket用の型の定義

chatapp/chatapp_server/lib/src/protocol内に、以下のchat.yamlを生成します。

chat.yaml
class: Chat
table: chat
fields:
  message: String
  user_id: int

上記ファイル生成後、serverpod generateして、classファイルを生成します。

$ serverpod generate

chatapp/chatapp_server/generated/tables.pgsqlにデータベースのテーブル作成用のSQL文が自動生成されています。serverpodのチュートリアル動画では、ここでデータベースにログインして生成していますが、今回は省略します。

tables.pgsql
--
-- Class Chat as table chat
--

CREATE TABLE "chat" (
  "id" serial,
  "message" text NOT NULL,
  "user_id" integer NOT NULL
);

ALTER TABLE ONLY "chat"
  ADD CONSTRAINT chat_pkey PRIMARY KEY (id);

エンドポイント処理の定義

chatapp/chatapp_server/lib/src/endpoint内に、エンドポイント処理定義用のコードファイル,chat_endpoint.dartを生成します。

chat_endpoint.dart
import 'dart:async';
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart'; //NOTE Chat型を使用するために追加

class ChatEndpoint extends Endpoint {
  final String _hello = "hello";
  @override
  Future<void> streamOpened(StreamingSession session) async {
    //NOTE clientからのwebsocket接続要求時の処理
    print("chat websocket started");
    session.messages.addListener(_hello, (message) {
      //リスナーの追加
      print("send to client");
      sendStreamMessage(session, message);
    });
  }

  @override
  Future<void> handleStreamMessage(//NOTE クライアントからwebsocketを受信したときの処理。
    StreamingSession session,
    SerializableEntity message,
  ) async {
    print("websocket handle");
    if (message is Chat) {//NOTE Chatメッセージが来た時の処理
      print("recieve Chat");
      session.messages.postMessage(
          _hello, Chat(message: message.message, user_id: message.user_id));
    }
  }

  @override
  Future<void> streamClosed(StreamingSession session) async {
    print("stream close");
  }
}

上記ファイル生成後、もう一度serverpod generateして、client用のファイルを生成します。

$ serverpod generate

すると、chatapp/chatapp_clientの中にclient用のコードが自動生成されます。
このコードを、flutterから使用します。

フロントエンド

最後にflutterのコードを修正します。
chatapp/chatapp_flutter/lib/main.dartを以下のように編集します。

main.dart
import 'package:marubatsugame_client/marubatsugame_client.dart';
import 'package:flutter/material.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'dart:math' as math;

var client = Client('http://localhost:8080/')
  ..connectivityMonitor = FlutterConnectivityMonitor();

final int _userId = math.Random().nextInt(1000); //ランダムなユーザIDを生成
Future<void> main() async {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Serverpod Chat Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Serverpod Chat Example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

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

class MyHomePageState extends State<MyHomePage> {
  final List<Chat> _messages = [];

  @override
  void initState() {
    Future(() async {
      await client.openStreamingConnection();
      await for (var message in client.chat.stream) {
        if (message is Chat) {
          _messages.add(message); //NOTE メッセージを追加
        }
        setState(() {});
      }
    });
    super.initState();
  }

  @override
  void dispose() {
    client.closeStreamingConnection(); //NOTE websocketコネクションの開放
    super.dispose();
  }

  final _textEditingController = TextEditingController();

  void _sendMessage() async {
    try {
      //NOTE APIではなく、websocketに変更
      Chat message =
          Chat(message: _textEditingController.text, user_id: _userId);
      await client.chat
          .sendStreamMessage(message);
    } catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 16.0),
              child: TextField(
                controller: _textEditingController,
                decoration: const InputDecoration(
                  hintText: 'Enter chat Message', //NOTE ヒントテキストを変更
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(bottom: 16.0),
              child: ElevatedButton(
                onPressed: _sendMessage,
                child: const Text('Send to Server'),
              ),
            ),
            for (final message in _messages)//NOTE メッセージの表示
              _ChatDisplay(message: message.message, userId: message.user_id)
          ],
        ),
      ),
    );
  }
}

class _ChatDisplay extends StatelessWidget {
  //NOTE chat用のwidgetを生成
  final String? message;
  final int? userId;

  const _ChatDisplay({
    required this.message,
    required this.userId,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 50,
      color: userId == _userId
          ? Colors.grey[300]
          : Colors.green[300], //NOTE 自分と自分以外で、色分け
      child: Center(
        child: Text(message!),
      ),
    );
  }
}

以上で、コードの編集は終わりです。

起 動

バックエンドを起動します。

$ cd chatapp_server
$ dart bin/main.dart

フロントエンド(Flutter)を起動します。
マルチプラットフォームをアピールするため、web版(chrome)とPCアプリ(linux)の2つ、起動します。

$ flutter run -d chrome

別ターミナルで、

$ flutter run -d linux

実際の様子(再掲)
chat_app_by_flutter_1.gif
web版とPCアプリ版で、websocketを用いて、相互通信することができました。

処理の流れ

  1. ブラウザ(chrome)からサーバにwebsocketでメッセージ送信
  2. サーバから各websocket client(chrome, linux)にメッセージをブロードキャスト
  3. 各websocket clientは受け取ったメッセージを描画

おわりに

serverpodを使用し、マルチプラットフォームな簡易チャットアプリを作成しました。
バックエンドからフロントエンドまでのシームレスな開発は癖になります。
今後はデータベースも絡めた簡単な対戦ゲームを作っていこうと思います。

参考資料

Tutorials & Examples
https://docs.serverpod.dev/tutorials
このチュートリアルを見れば体系的に学べます。
特にserverpod generateコマンドで、何をやっているかがだいたい分かります。

Streams and messaging
https://docs.serverpod.dev/concepts/streams
今回の肝である、websocketの公式ガイドです。

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?