ソケットで匿名のチャットルームを作成する.IOとエクスプレス.js


この記事では、2つのペアで一緒に別の部屋に匿名で、人々を接続するチャットアプリケーションを作成する予定です.チャットアプリケーションを使用するExpress.js サーバー側のコードでは、Socket.io クライアント側はバニラJavaScriptで開発されます.

プロジェクトの設定

  • ディレクトリ名を作成しますchat-app を使ってディレクトリをディレクトリに変更します.
  • $ mkdir chat-app && cd chat-app
    
  • コマンドを実行してノードアプリケーションを初期化します.
  • $ yarn init -y
    
  • コマンドを実行することによって我々のプロジェクトで糸を使ってください.
  • $ yarn add express
    
  • JavaScriptファイルを作成します.app.js , と簡単なノードのHTTPサーバを作成します.
  • 次に、我々は我々のアプリケーションにExpressをインポートし、明示的なアプリケーションを作成し、ポート上の要求を聞くためにサーバーを起動します8001 .
  • // app.js
    const http = require("http")
    const express = require("express")
    
    const app = express()
    
    app.get("/index", (req, res) => {
        res.send("Welcome home")
    })
    
    const server = http.createServer(app)
    
    server.on("error", (err) => {
        console.log("Error opening server")
    })
    
    server.listen(8001, () => {
        console.log("Server working on port 8001")
    })
    
  • これで、コマンドを実行してアプリケーションを起動できます.
  • $ node app.js
    
    訪問できます[http://localhost:8001/index](http://localhost:8001/index) ブラウザでアプリケーションが動作することをテストします
    welcome-home.png

    ソケットの初期化。サーバ側のIO


    サーバー側のソケットを初期化するには、次の手順に従います.

  • ソケットをインストールします.コマンドの実行によってアプリケーションにIO依存性があります.
    $ yarn add socket.io
    

  • インポートソケット.コードをIOソケットに作成し、ソケットにイベントリスナーを追加し、接続が完了したかどうかをリッスンします.
    // app.js
    const http = require("http");
    const { Server } = require("socket.io");
    const express = require("express");
    
    const app = express();
    
    app.get("/index", (req, res) => {
      res.send("Welcome home");
    });
    
    const server = http.createServer(app);
    
    const io = new Server(server);
    
    io.on("connection", (socket) => {
      console.log("connected");
    });
    
    server.on("error", (err) => {
      console.log("Error opening server");
    });
    
    server.listen(8001, () => {
      console.log("Server working on port 3000");
    });
    
  • ソケットの初期化。クライアント側のIO


    我々は、バニラJavaScriptを使用して簡単なUIを作成し、我々はExpressのアプリケーションから静的ファイルとしてWebページを提供します.
    我々のUIを構築するためのファイルを含むパブリックディレクトリを作成し、プロジェクトの構造をこのようにします.
    chat-app/
                |- node_modules/
                |- public/
                            |- index.html
                            |- main.js
                |- app.js
                |- package.json
                |- yarn.lock
    

    We are going to be making use of Tailwind CSS to style the Client UI to reduce the amount of custom CSS we’d be writing.


    index.html , 私たちのチャットウィンドウのテンプレートを作成します.
    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdn.tailwindcss.com"></script>
        <title>Anon Chat App</title>
    </head>
    <body>
        <div class="flex-1 p:2 sm:p-6 justify-between flex flex-col h-screen">
            <div id="messages" class="flex flex-col space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
            </div>
            <div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0">
               <div class="relative flex">
                  <input type="text" placeholder="Write your message!" class="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-12 bg-gray-200 rounded-md py-3">
                  <div class="absolute right-0 items-center inset-y-0 hidden sm:flex">
                     <button type="button" class="inline-flex items-center justify-center rounded-lg px-4 py-3 transition duration-500 ease-in-out text-white bg-blue-500 hover:bg-blue-400 focus:outline-none">
                        <span class="font-bold">Send</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-6 w-6 ml-2 transform rotate-90">
                           <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
                        </svg>
                     </button>
                  </div>
               </div>
            </div>
         </div>
             <script src="/socket.io/socket.io.js"></script>
         <script src="./main.js"></script>
    </body>
    </html>
    
    上記のHTMLファイルでは、2つのJavaScriptファイルを含み、最初のソケットを初期化しました.クライアント側と別のIOmain.js カスタムJavaScriptコードを書くファイル.
    それからmain.js ファイルは、チャットボックスにメッセージを追加できる機能を作成します.機能createMessage は2つの引数を期待する.最初の引数はメッセージ文字列です、そして、2番目の引数はメッセージがユーザーか他の外部ユーザーからあるかどうか決定するブールです.
    // main.js
    const messageBox = document.querySelector("#messages");
    
    function createMessage(text, ownMessage = false) {
      const messageElement = document.createElement("div");
      messageElement.className = "chat-message";
      const subMesssageElement = document.createElement("div");
      subMesssageElement.className =
        "px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
      if (ownMessage) {
        subMesssageElement.className += " float-right bg-blue-800 text-white";
      }
      subMesssageElement.innerText = text;
      messageElement.appendChild(subMesssageElement);
    
      messageBox.appendChild(messageElement);
    }
    
    createMessage("Welcome to vahalla");
    createMessage("Who are you to talk to me", true);
    
    サーバーアプリケーションのコードを変更します.app.js クライアントUIをレンダリングするには、静的ファイルを使用します.
    // app.js
    const http = require("http");
    const { Server } = require("socket.io");
    const express = require("express");
    const path = require("path");
    
    const app = express();
    
    app.use(express.static(path.join(__dirname, "public")));
    
    const server = http.createServer(app);
    
    const io = new Server(server);
    
    io.on("connection", (socket) => {
      console.log("connected");
    });
    
    server.on("error", (err) => {
      console.log("Error opening server");
    });
    
    server.listen(8001, () => {
      console.log("Server working on port 8001");
    });
    

    NOTE: To view the changes made in our application, we have to stop the running server application and re-run it for the new changes to take effect. So we are making use of nodemon to automate this process for us.


    実行してnodemonをインストールします.
    $ npm install -g nodemon
    
    次にNodeMonを使用してノードアプリケーションを実行します.
    $ nodemon ./app.js
    
    オープン[http://localhost:8001](http://localhost:3000) あなたのブラウザでチャットアプリがどのように見えるかを表示します.
    chatapp-basic.png

    Webソケット通信用の異なる部屋の作成


    部屋を作成し、各部屋に接続されたユーザーの数を追跡するにはRoom 我々のためにこのデータを管理するクラス.
    新しいファイルを作成しますroom.js プロジェクトのルートディレクトリにあります.その後、我々はRoom クラスとコンストラクターは、我々の部屋の状態を維持するためのプロパティを初期化します.
    // room.js
    
    // the maximum number of people allowed in each room
    const ROOM_MAX_CAPACITY = 2;
    
    class Room {
      constructor() {
        this.roomsState = [];
      }
    }
    
    module.exports = Room;
    
    The roomsState は、作成した各部屋IDに関する情報とその部屋のユーザ数を保持するオブジェクトの配列です.だから典型的なroomsState このようになります.
    // rooms state
    [
        {
            roomID: "some id",
            users: 1
        },
        {
            roomID: "a different id",
            users: 2
        }
    ]
    
    次に、部屋に参加するメソッドを追加します.どの部屋には、各部屋で許される参加者の最大数より少ないユーザー数を持っているかどうかを確認するために、部屋を通ってループします.リスト内のすべての部屋が占有されている場合、それは新しい部屋を作成し、その部屋のユーザー数を1に初期化します.
    ユニークなIDを生成するには、uuid 我々のアプリケーションで.
    uuidをインストールします.
    $ yarn add uuid
    
    次に、次のように実行してパッケージをアプリケーションにインポートします.
    // room.js
    const { v4: uuidv4 } = require("uuid");
    
    class Room {
      constructor() {
        /**/
      }
    
      joinRoom() {
        return new Promise((resolve) => {
          for (let i = 0; i < this.roomsState.length; i++) {
            if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
              this.roomsState[i].users++;
              return resolve(this.roomsState[i].id);
            }
          }
    
          // else generate a new room id
          const newID = uuidv4();
          this.roomsState.push({
            id: newID,
            users: 1,
          });
          return resolve(newID);
        });
      }
    }
    
    module.exports = Room;
    

    NOTE: Making use of an array to manage the rooms' state is, obviously, not the best way to do so. Imagine having thousands of rooms in your application and you have to loop through each room for each join request. It would execute at O(n). For the purpose of this tutorial, we will stick to this approach.


    我々は別の方法を追加しますRoom クラスleaveRoom() , 特定の部屋のユーザー数を減らすために.
    // room.js
    class Room {
      constructor() {
        /**/
      }
    
      joinRoom() {}
    
      leaveRoom(id) {
        this.roomsState = this.roomsState.filter((room) => {
          if (room.id === id) {
            if (room.users === 1) {
              return false;
            } else {
              room.users--;
            }
          }
          return true;
        });
      }
    }
    
    module.exports = Room;
    
    The leaveRoom() メソッドは部屋のIDを取り、部屋の配列を通してループを行います.
    それが一致する部屋を見つけるならば、部屋のユーザーがその特定の部屋状態を削除するために1つであるかどうかチェックします.
    部屋のユーザが1より大きいならばleaveRoom() 方法は、ちょうどその部屋のユーザーの数を控除します.
    最後にroom.js コードはこれに似ているはずです.
    // room.js
    const { v4: uuidv4 } = require("uuid");
    
    // the maximum number of people allowed in a room
    const ROOM_MAX_CAPACITY = 2;
    
    class Room {
      constructor() {
        this.roomsState = [];
      }
    
      joinRoom() {
        return new Promise((resolve) => {
          for (let i = 0; i < this.roomsState.length; i++) {
            if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
              this.roomsState[i].users++;
              return resolve(this.roomsState[i].id);
            }
          }
    
          const newID = uuidv4();
          this.roomsState.push({
            id: newID,
            users: 1,
          });
          return resolve(newID);
        });
      }
    
      leaveRoom(id) {
        this.roomsState = this.roomsState.filter((room) => {
          if (room.id === id) {
            if (room.users === 1) {
              return false;
            } else {
              room.users--;
            }
          }
          return true;
        });
      }
    }
    
    module.exports = Room;
    

    部屋に出入りする


    私たちのチャットアプリケーション内のユーザーのためのさまざまなチャネルを作成するには、我々のための部屋を作成する予定です.
    ソケット.IOはソケットが結合したり残したりできる任意のチャネルを作ることができます.これは、クライアントのサブセットにイベントをブロードキャストするために使用することができます.
    rooms-dark.png
    (ソース:https://socket.io/docs/v3/rooms/ )
    部屋に入るために、我々はユニークな部屋IDで部屋に加わるでしょう.
    io.on("connection", socket => {
        // join a room
      socket.join("some room id");
    
      socket.to("some room id").emit("some event");
    });
    
    当社のサーバーアプリケーションでは、新しいユーザーが接続に参加するとRoom.joinRoom() 我々のユニークな部屋IDであるユニークなIDを返します.
    // app.js
    io.on("connection", async (socket) => {
      const roomID = await room.joinRoom();
      // join room
      socket.join(roomID);
    
      socket.on("disconnect", () => {
        // leave room
        room.leaveRoom(roomID);
      });
    });
    

    メッセージの送受信


    さて、クライアント側から送られてきたメッセージに対してイベントを放射するためにクライアント側のコードに戻ります.また、メッセージイベントをサーバーから来て聞いて、我々のチャットボックスにそのメッセージを書き込みます.
    // main.js
    socket.on("receive-message", (message) => {
      createMessage(message);
    });
    
    sendButton.addEventListener("click", () => {
      if (textBox.value != "") {
        socket.emit("send-message", textBox.value);
        createMessage(textBox.value, true);
        textBox.value = "";
      }
    });
    

    NOTE: In our chat application, we directly add the message from user to the chatbox without confirming if the message is received by the socket server. This is not usually the case.


    その後、私たちのエクスプレスアプリケーションで.
    // app.js
    io.on("connection", async (socket) => {
      const roomID = await room.joinRoom();
      // join room
      socket.join(roomID);
    
      socket.on("send-message", (message) => {
        socket.to(roomID).emit("receive-message", message);
      });
    
      socket.on("disconnect", () => {
        // leave room
        room.leaveRoom(roomID);
      });
    });
    
    我々の急行アプリケーションコードを作ることは、最終的にこれのように見えます.
    // app.js
    const http = require("http");
    const { Server } = require("socket.io");
    const express = require("express");
    const path = require("path");
    const Room = require("./room");
    
    const app = express();
    
    app.use(express.static(path.join(__dirname, "public")));
    
    const server = http.createServer(app);
    
    const io = new Server(server);
    
    const room = new Room();
    
    io.on("connection", async (socket) => {
      const roomID = await room.joinRoom();
      // join room
      socket.join(roomID);
    
      socket.on("send-message", (message) => {
        socket.to(roomID).emit("receive-message", message);
      });
    
      socket.on("disconnect", () => {
        // leave room
        room.leaveRoom(roomID);
      });
    });
    
    server.on("error", (err) => {
      console.log("Error opening server");
    });
    
    server.listen(8001, () => {
      console.log("Server working on port 8001");
    });
    
    クライアント側のJavaScriptはこのように見えます.
    // main.js
    const messageBox = document.querySelector("#messages");
    const textBox = document.querySelector("input");
    const sendButton = document.querySelector("button");
    
    function createMessage(text, ownMessage = false) {
      const messageElement = document.createElement("div");
      messageElement.className = "chat-message";
      const subMesssageElement = document.createElement("div");
      subMesssageElement.className =
        "px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
      if (ownMessage) {
        subMesssageElement.className += " float-right bg-blue-800 text-white";
      }
      subMesssageElement.innerText = text;
      messageElement.appendChild(subMesssageElement);
    
      messageBox.appendChild(messageElement);
    }
    
    const socket = io();
    
    socket.on("connection", (socket) => {
      console.log(socket.id);
    });
    
    socket.on("receive-message", (message) => {
      createMessage(message);
    });
    
    sendButton.addEventListener("click", () => {
      if (textBox.value != "") {
        socket.emit("send-message", textBox.value);
        createMessage(textBox.value, true);
        textBox.value = "";
      }
    });
    

    チャットアプリケーションのテスト


    テキストを我々のチャットアプリには、我々は2つの部屋が作成されていることを確認する4つのブラウザを開きます.

    結論


    あなたがこれを見るならば、それは我々がこれまで読んで、おそらく我々のマシンで実行しているチャットアプリを持っていることを意味します.
    この記事からコードを見つけることができますGitHub repository .
    より多くの課題を含めるには、これらのチャットアプリケーションに含めることができる機能です
  • ユーザーが部屋を去ったり、参加した場合、ユーザーに通知する
  • Reactor室状態アレイをより効率的なデータ構造に
  • トピック選択に基づいてペアリングを許可します(この場合は、ルームオブジェクトに設定する必要があります).
  • ソケットについての詳細を読む.ioの公式ドキュメントをご覧いただけます.
    この記事を読むのを楽しむなら、あなたは考えることができますbuying me a coffee .