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?

More than 5 years have passed since last update.

Chat Applicationを作ってみる With GO and Pusher

Posted at

ChatApplicationを作ってみる

ハッカー初心者の自分に必要なのは開発経験!
ということでまずとりあえずチャットアプリを作ってみる。
Pusherのドキュメントをとりあえず真似て、そのあとに色々いじってみることになりそう。

間違いやアドバイスなどフィードバックがあればコメントしてくれるとありがたいです!!

使用するお供たち

  • GO 今回はむかしにちょっとだけかじったGOを探り探りで使ってみる。GOのいいところといえばGopherくんが可愛い!download.jpeg

  • Pusher WebSocketに対してはPusherを使ってみる

  • HTML/CSS/JS フロントの実装

始める前にーWebSocketとはなんぞ?

ということで少し調べてみる

WebSocketとは

WebSocketはネットワーク用通信規格の一つ。
Ajaxのおいて頻繁に発生するデータのやりとりに対し、XHRはブラウザからのサーバへの要求手段にすぎず、サーバからのプッシュに難があるらしい。

そこで誕生したのがWebSocketで、サーバとクライアントのコネクションを専用のプロトコルを使い双方間通信を実現するらしい。

Pusherとは

WebSocketに対応するAPIは色々あると思います。代表的なのはSocketioなどだと思うのですが、一度使ったことがあるので今回はPusherを。

これらを使いWebSocketを介して双方向機能を統合します。

作る

早速、PusherのDocumentationを覗きます。
PusherのDocumentation

Goのインストールと基礎知識、JavaScriptの基礎知識が必要とのこと。
Goのインストール

PusherのSet up

まずのアカウントを作る必要があるようなので作る。
ここでサインアップ
無事完了したらCreate a new applicationのボタンをおす。そうすることでのちに必要となるアップキーなどが使えるようになる。

もう一つApp SettingでEnable client eventsを有効にしておく必要があるらしい。これでクライアント側からイベントをトリガーしたりできるようになるらしい。

メインのGoファイルをつくる

フォルダー名をgo-chatと名ずけてその中にエントリーポイントとなるchat.goファイルをつくる。

Pusherもインストールしておきます。

$ go get github.com/pusher/pusher-http-go

※windowsを使っている人はエラーにエンカウントする可能性があるらしい。

chat.goファイル内で次のコードをかく。

chat.go
    import (
        "encoding/json"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"

        pusher "github.com/pusher/pusher-http-go"
    )

    var client = pusher.Client{
        AppId:   "PUSHER_APP_ID",
        Key:     "PUSHER_APP_KEY",
        Secret:  "PUSHER_APP_SECRET",
        Cluster: "PUSHER_APP_CLUSTER",
        Secure:  true,
    }

    type user struct {
        Name  string `json:"name" xml:"name" form:"name" query:"name"`
        Email string `json:"email" xml:"email" form:"email" query:"email"`
    }

    func main() {
        http.Handle("/", http.FileServer(http.Dir("./public")))

        http.HandleFunc("/new/user", registerNewUser)
        http.HandleFunc("/pusher/auth", pusherAuth)

        log.Fatal(http.ListenAndServe(":8090", nil))
    }

PUSHER_APP_*の部分は自分のアカウントのダッシュボードにのっているものに変える必要がある。

はじめに色々とインポート。

  • encoding/jsonでjsonデータを変換したり、逆にjsonデータに変換したりする
  • io/ioutilでインプットアウトプットのユーティリティを実装する。ファイルを読んだり書き込んだりするらしい。今回ではユーザーの登録時のインプットを読み込むのに使う。
  • logでログの出力を行う。今回はlog.Fatal()でサーバを終了させてる(?)
  • net/httpでhttp処理を色々するらしい(?)のだがまだ全然掴めない。ルーティングに使用?

Pusherクライアントとuserストラクチャ

新しいPusherクライアントを使ってキーをかきかえ。
userストラクチャを作って色々の構造にバインドするらしい(?)。良く理解できないしjson,xmlと用意する意味がうまく理解できない。

エンドポイントをつくる

/
静的ファイルを返す。見た目担当。
/new/user/
新しいユーザーをつくる。
/pusher/auth
チャンネルにサブスクライブする

chat.goファイルのmainファンクションの手前にコードを付け加える。

chat.go
    func registerNewUser(rw http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            panic(err)
        }

        var newUser user

        err = json.Unmarshal(body, &newUser)
        if err != nil {
            panic(err)
        }

        client.Trigger("update", "new-user", newUser)

        json.NewEncoder(rw).Encode(newUser)
    }

    func pusherAuth(res http.ResponseWriter, req *http.Request) {
        params, _ := ioutil.ReadAll(req.Body)
        response, err := client.AuthenticatePrivateChannel(params)
        if err != nil {
            panic(err)
        }

        fmt.Fprintf(res, string(response))
    }
    

ここのファンクションをハンドラーにつかませる(?)。

  • 疑問

(rw http.ResponseWriter, req *http.Request)
がわからない。ただ覚えればいいのかもしれないけど(?)。
http.ResponseWriterは型として捉えればいいのかな?こいつの正体がわからない。
*http.Requestのポインタはなに?

  • とりあえず中身を見てみる

クライアントのボディーを読み込む。ユーザーの入力したnameemail読み込む。

if err != nil { panic(err) }

はエラー処理のいっつもついてくるボイラープレート的なものと思っていいのかな?

  • newUser

newUserをつくって

err = json.Unmarshal(body, &newUser)

でjsonを変換してそのアドレスにuser型としてぶっこむ。

  • イベントをトリガーする

client.Trigger(channel, event, data)

に乗っ取ってupdateチャンネルにnew-userイベントをnewUserを使ってトリガーする。

  • 疑問2

json.NewEncoder(rw).Encode(newUser)

はリクエストで受けとったものをnewUserに変換してそれをjsonデータにしてリスポンスしてるのかな?
Marshalじゃダメなのかな?

  • 次のファンクションを見てみる

このファンクションでユーザーをチャンネルにauthするのかな?

params, _ := ioutil.ReadAll(req.Body)

アンダースコアはエラー処理を省略するため?なぜ?

response, err := client.AuthenticatePrivateChannel(params)

受け取ったparamsを使ってauthしてるのかな?

フロントをつくる

publicフォルダを作り、その中にcssjsフォルダをつくる。

  • HTML

publicフォルダのルートにindex.htmlを作る。

index.html
<!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Chat with friends in realtime</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css">
        <link rel="stylesheet" href="./css/app.css" >
      </head>
      <body>
        <header>
            <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
                <a class="navbar-brand" href="#">Welcome</a>
            </nav>
        </header>
        <div class="container-fluid">
            <div class="row" id="mainrow">
                <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
                    <ul class="nav nav-pills flex-column" id="rooms">
                    </ul>
                </nav>
                <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="registerScreen">
                    <h3 style="text-align: center">Type in your details to chat</h3>
                    <hr/>
                    <div class="chat" style="margin-bottom:150px">
                        <p>&nbsp;</p>
                        <form id="loginScreenForm">
                            <div class="form-group">
                              <input type="text" class="form-control" id="fullname" placeholder="Name" required>
                            </div>
                            <div class="form-group">
                              <input type="email" class="form-control" id="email" placeholder="Email Address" required>
                            </div>
                            <button type="submit" class="btn btn-block btn-primary">Submit</button>
                          </form>
                      </div>
                </main>

                <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" style="display: none" id="main">
                    <h1>Chats</h1>
                    <p>👈 Select a chat to load the messages</p>
                    <p>&nbsp;</p>
                    <div class="chat" style="margin-bottom:150px">
                        <h5 id="room-title"></h5>
                        <p>&nbsp;</p>
                        <div class="response">
                            <form id="replyMessage">
                                <div class="form-group">
                                    <input type="text" placeholder="Enter Message" class="form-control" name="message" />
                                </div>
                            </form>
                        </div>
                        <div class="table-responsive">
                          <table class="table table-striped">
                            <tbody id="chat-msgs">
                            </tbody>
                        </table>
                    </div>
                </main>
            </div>
        </div>

        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
        <script type="text/javascript" src="./js/app.js"></script>
      </body>
    </html>
  • Css
app.css
 body {
        padding-top: 3.5rem;
    }
    h1 {
        padding-bottom: 9px;
        margin-bottom: 20px;
        border-bottom: 1px solid #eee;
    }
    .chat {
        max-width: 80%;
        margin: 0 auto;
    }
    .sidebar {
        position: fixed;
        top: 51px;
        bottom: 0;
        left: 0;
        z-index: 1000;
        padding: 20px 0;
        overflow-x: hidden;
        overflow-y: auto;
        border-right: 1px solid #eee;
    }
    .sidebar .nav {
        margin-bottom: 20px;
    }
    .sidebar .nav-item {
        width: 100%;
    }
    .sidebar .nav-item + .nav-item {
        margin-left: 0;
    }
    .sidebar .nav-link {
        border-radius: 0;
    }
    .placeholders {
        padding-bottom: 3rem;
    }
    .placeholder img {
        padding-top: 1.5rem;
        padding-bottom: 1.5rem;
    }
    tr .sender {
        font-size: 12px;
        font-weight: 600;
    }
    tr .sender span {
        color: #676767;
    }
    .response {
        display: none;
    }
  • JS
app.js
(function () {
        var pusher = new Pusher('PUSHER_APP_KEY', {
            authEndpoint: '/pusher/auth',
            cluster: 'PUSHER_APP_CLUSTER',
            encrypted: true
        });

        let chat = {
            name: undefined,
            email: undefined,
            endUserName: undefined,
            currentRoom: undefined,
            currentChannel: undefined,
            subscribedChannels: [],
            subscribedUsers: []
        }

        var publicChannel = pusher.subscribe('update');

        const chatBody = $(document)
        const chatRoomsList = $('#rooms')
        const chatReplyMessage = $('#replyMessage')

        const helpers = {
            clearChatMessages: () => {
                $('#chat-msgs').html('')
            },

            displayChatMessage: (message) => {
                if (message.email === chat.email) {
                    $('#chat-msgs').prepend(
                        `<tr>
                            <td>
                                <div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div>
                                <div class="message">${message.text}</div>
                            </td>
                        </tr>`
                    )
                }
            },

            loadChatRoom: evt => {
                chat.currentRoom = evt.target.dataset.roomId
                chat.currentChannel = evt.target.dataset.channelId
                chat.endUserName =  evt.target.dataset.userName
                if (chat.currentRoom !== undefined) {
                    $('.response').show()
                    $('#room-title').text('Write a message to ' + evt.target.dataset.userName+ '.')
                }

                evt.preventDefault()
                helpers.clearChatMessages()
            },

            replyMessage: evt => {
                evt.preventDefault()

                let createdAt = new Date().toLocaleString()            
                let message = $('#replyMessage input').val().trim()
                let event = 'client-' + chat.currentRoom

                chat.subscribedChannels[chat.currentChannel].trigger(event, {
                    'sender': chat.name,
                    'email': chat.currentRoom,
                    'text': message, 
                    'createdAt': createdAt 
                });

                $('#chat-msgs').prepend(
                    `<tr>
                        <td>
                            <div class="sender">
                                ${chat.name} @ <span class="date">${createdAt}</span>
                            </div>
                            <div class="message">${message}</div>
                        </td>
                    </tr>`
                )

                $('#replyMessage input').val('')
            },

            LogIntoChatSession: function (evt) {
                const name  = $('#fullname').val().trim()
                const email = $('#email').val().trim().toLowerCase()

                chat.name = name;
                chat.email = email;

                chatBody.find('#loginScreenForm input, #loginScreenForm button').attr('disabled', true)

                let validName = (name !== '' && name.length >= 3)
                let validEmail = (email !== '' && email.length >= 5)

                if (validName && validEmail) {
                    axios.post('/new/user', {name, email}).then(res => {
                        chatBody.find('#registerScreen').css("display", "none");
                        chatBody.find('#main').css("display", "block");

                        chat.myChannel = pusher.subscribe('private-' + res.data.email)
                        chat.myChannel.bind('client-' + chat.email, data => {
                            helpers.displayChatMessage(data)
                        })
                    })
                } else {
                    alert('Enter a valid name and email.')
                }

                evt.preventDefault()
            }
        }


        publicChannel.bind('new-user', function(data) {
            if (data.email != chat.email){
                chat.subscribedChannels.push(pusher.subscribe('private-' + data.email));
                chat.subscribedUsers.push(data);

                $('#rooms').html("");

                chat.subscribedUsers.forEach((user, index) => {
                    $('#rooms').append(
                        `<li class="nav-item"><a data-room-id="${user.email}" data-user-name="${user.name}" data-channel-id="${index}" class="nav-link" href="#">${user.name}</a></li>`
                    )
                })
            }
        })

        chatReplyMessage.on('submit', helpers.replyMessage)
        chatRoomsList.on('click', 'li', helpers.loadChatRoom)
        chatBody.find('#loginScreenForm').on('submit', helpers.LogIntoChatSession)
    }());

APP_KEY_*の部分は変えなければいけない。

Run

go run chat.go

でrunするとhttp://127.0.0.1:8090でapplicationが完成!

まとめ

とりあえず完成。疑問点が多いのでそれを解決しないといけないのとここからカスタムしていく予定。

フロント部分は説明はしょったのでまた付け加えるかも。

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?