ChatApplicationを作ってみる
ハッカー初心者の自分に必要なのは開発経験!
ということでまずとりあえずチャットアプリを作ってみる。
Pusherのドキュメントをとりあえず真似て、そのあとに色々いじってみることになりそう。
間違いやアドバイスなどフィードバックがあればコメントしてくれるとありがたいです!!
使用するお供たち
-
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ファイル内で次のコードをかく。
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ファンクションの手前にコードを付け加える。
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
のポインタはなに?
- とりあえず中身を見てみる
クライアントのボディーを読み込む。ユーザーの入力したname
をemail
読み込む。
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
フォルダを作り、その中にcss
とjs
フォルダをつくる。
- HTML
public
フォルダのルートに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> </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> </p>
<div class="chat" style="margin-bottom:150px">
<h5 id="room-title"></h5>
<p> </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
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
(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が完成!
まとめ
とりあえず完成。疑問点が多いのでそれを解決しないといけないのとここからカスタムしていく予定。
フロント部分は説明はしょったのでまた付け加えるかも。