1
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?

WebSocketを使ったチャットアプリを作って、Renderにデプロイする

Posted at

デプロイしたもの

牧歌的、低機能な匿名チャット。

image.png

コード

基本的にはAIに書いてもらう。

Server(Node)
const WebSocket = require('ws');
const { Buffer } = require('buffer');

// WebSocketサーバーをポート8080で作成
const wss = new WebSocket.Server({ port: 8080 });

// メッセージ履歴を保存する配列(最大10件)
const messageHistory = [];

const MAX_HISTORY = 1000;

wss.on('connection', (ws) => {
  console.log('クライアントが接続しました。');

  // 過去の10件のメッセージを接続したクライアントに送信
  messageHistory.forEach((message) => {
    ws.send(message);
  });

  // 接続している全てのクライアントにメッセージを送信する関数
  const broadcast = (data) => {
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  };

  // クライアントからメッセージを受け取ったときの処理
  ws.on('message', (message) => {
    const buffer = Buffer.from(message, 'utf8');
    const text = buffer.toString('utf8');
    console.log('受信したメッセージ: ', text);

    // メッセージをテキスト形式で履歴に追加
    const textMessage = message.toString(); // バッファを文字列に変換
    messageHistory.push(textMessage);

    // メッセージ履歴が10件を超えたら古いものを削除
    if (messageHistory.length > MAX_HISTORY) {
      messageHistory.shift(); // 最古のメッセージを削除
    }

    // 全てのクライアントにメッセージを送信
    broadcast(textMessage);
  });

  // 接続終了時
  ws.on('close', () => {
    console.log('クライアントが切断しました。');
  });

  // 初回接続時にクライアントへメッセージ送信
  ws.send('チャットサーバーに接続されました。');
});

console.log('WebSocketチャットサーバーがポート8080で起動中...');



// renderのsleep対策
const URL_SERVER = `https://ws-example.onrender.com/`;
const URL_CLIENT = `https://ws-example-client.onrender.com/`;
const interval = 840000; // 14 minutes

async function reloadWebsite(url) {
  try {
    const response = await fetch(url);
    console.log(`Reloaded at ${new Date().toISOString()}: Status Code ${response.status}`);
  } catch (error) {
    console.error(`Error reloading at ${new Date().toISOString()}:`, error.message);
  }
}

setInterval(() => reloadWebsite(URL_SERVER), interval);
setInterval(() => reloadWebsite(URL_CLIENT), interval);

Client(html)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocketチャットアプリ</title>
  <style>
    body {
      font-family: 'Arial', sans-serif;
      background-color: #f0f2f5;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
    }

    #chat {
      border: 1px solid #ccc;
      border-radius: 10px;
      width: 100%;
      height: 300px;
      overflow-y: scroll;
      margin-bottom: 20px;
      padding: 10px;
      background-color: #fff;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    }

    input[type="text"] {
      width: 70%;
      padding: 10px 15px;
      border-radius: 25px;
      border: 1px solid #ccc;
      font-size: 16px;
      margin-right: 10px;
      transition: border 0.3s ease;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    input[type="text"]:focus {
      border-color: #4c8bf5;
      outline: none;
    }

    button {
      width: 20%;
      padding: 10px;
      background-color: #4c8bf5;
      border: none;
      border-radius: 25px;
      color: white;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.3s ease, box-shadow 0.3s ease;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    button:hover {
      background-color: #3a6fd3;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    }

    button:active {
      background-color: #355bb3;
    }

    h1 {
      text-align: center;
      margin-bottom: 20px;
      font-family: 'Arial', sans-serif;
      color: #333;
    }

    .container {
      width: 90%;
      max-width: 600px;
      margin: 0 auto;
    }

    #message-container {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>最果てチャット💬(WebSocketお試し)</h1>
    <div id="chat"></div>
    <div id="message-container">
      <input type="text" id="message" placeholder="メッセージを入力..." />
      <button id="send">送信</button>
    </div>
  </div>

  <script>
    const socket = new WebSocket('https://ws-example.onrender.com');
    const chatDiv = document.getElementById('chat');
    const messageInput = document.getElementById('message');
    const sendButton = document.getElementById('send');

    // ランダムな絵文字を取得する関数
    const getRandomEmoji = () => {
      const emojis = ['😊', '🚀', '🌟', '😎', '🎉', '🔥', '💡', '', '🍛', '🌈', '👶', '😸', '🐶', '👽', '🐤', '💪', '👼', '💃', '🦄', '🐣', '🦜', '🐸', '🐬', '🍣', '🦐', '🍼' ];
      return emojis[Math.floor(Math.random() * emojis.length)];
    };
    const ICON = getRandomEmoji();

    socket.addEventListener('open', () => {
      console.log('サーバーに接続されました');
      addMessage('サーバーに接続されました');
    });

    socket.addEventListener('message', (event) => {
      if (typeof event.data === 'string') {
        addMessage('受信: ' + event.data);
      } else {
        const reader = new FileReader();
        reader.onload = function() {
          addMessage('受信: ' + reader.result);
        };
        reader.readAsText(event.data); // Blobをテキストとして読み取る
      }
    });

    socket.addEventListener('close', () => {
      console.log('サーバーから切断されました');
      addMessage('サーバーから切断されました');
    });

    sendButton.addEventListener('click', () => {
      sendMessage();
    });

    messageInput.addEventListener('keypress', (event) => {
      if (event.key === 'Enter') {
        sendMessage();
      }
    });

    function sendMessage() {
      const message = messageInput.value;
      if (message !== '') {
        socket.send(ICON + message);
        messageInput.value = '';
      }
    }

    function addMessage(message) {
      const messageElement = document.createElement('div');
      messageElement.textContent = message;
      chatDiv.appendChild(messageElement);
      chatDiv.scrollTop = chatDiv.scrollHeight;
    }
  </script>
</body>
</html>

デプロイ(Render)

無料で使えるRenderを利用する。

静的サイト、アプリのホスティングから、PostgreSQL、Redisまで無料で使える。
Herokuの後継はここだったのか...

Github連携でリポジトリの内容をそのままデプロイできる。
ユーザ登録からデプロイまで初見でも5分程度で終わり、とても良かった。

Renderの無料枠で、アプリをスリープさせないようにする

無料枠では15分アクセスがないとスリープする。

Render spins down a Free web service that goes 15 minutes without receiving inbound traffic. Render spins the service back up whenever it next receives a request to process.

自分で定期的にアクセスさせて、インスタンスを起こし続けておく。

const URL_SERVER = `https://ws-example.onrender.com/`;
const URL_CLIENT = `https://ws-example-client.onrender.com/`;
const interval = 840000; // 14 minutes

async function reloadWebsite(url) {
  try {
    const response = await fetch(url);
    console.log(`Reloaded at ${new Date().toISOString()}: Status Code ${response.status}`);
  } catch (error) {
    console.error(`Error reloading at ${new Date().toISOString()}:`, error.message);
  }
}

setInterval(() => reloadWebsite(URL_SERVER), interval);
setInterval(() => reloadWebsite(URL_CLIENT), interval);

こちらを参考にさせてもらった。
https://medium.com/@shriharshranjangupta/solution-for-render-com-web-services-spin-down-due-to-inactivity-a5c6061b581b

はじめは、Github Actionsのcronを検討したが、実行が20〜30分遅れることもよくあるらしい。

1
1
0

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
1
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?