Node.js sample application with Socket.IO
Revision | 51274ace76f331f34e729b043f5ebeaaea4a2ccd (tree) |
---|---|
Time | 2012-10-29 01:06:50 |
Author | hylom <hylom@hylo...> |
Commiter | hylom |
implement all features
@@ -10,6 +10,7 @@ var express = require('express') | ||
10 | 10 | , socketIO = require('socket.io'); |
11 | 11 | |
12 | 12 | var app = express(); |
13 | +var crypto = require('crypto'); | |
13 | 14 | |
14 | 15 | app.configure(function(){ |
15 | 16 | app.set('port', process.env.PORT || 3000); |
@@ -29,10 +30,9 @@ app.configure('development', function(){ | ||
29 | 30 | |
30 | 31 | app.get('/', routes.index); |
31 | 32 | app.post('/room', routes.room); |
32 | -app.get('/echo/:name', routes.echo); | |
33 | 33 | |
34 | 34 | var server = http.createServer(app); |
35 | -var io = socketIO.listen(server); | |
35 | +var io = socketIO.listen(server, {log:false}); | |
36 | 36 | |
37 | 37 | server.listen(app.get('port'), function(){ |
38 | 38 | console.log("Express server listening on port " + app.get('port')); |
@@ -41,32 +41,148 @@ server.listen(app.get('port'), function(){ | ||
41 | 41 | // socket.ioのソケットを管理するオブジェクト |
42 | 42 | var socketsOf = {}; |
43 | 43 | |
44 | +// 指定したroomIdに属するクライアントすべてに対しイベントを送信する | |
45 | +function emitToRoom(roomId, event, data, fn) { | |
46 | + if (socketsOf[roomId] === undefined) { | |
47 | + return; | |
48 | + } | |
49 | + var sockets = socketsOf[roomId]; | |
50 | + Object.keys(sockets).forEach(function (key) { | |
51 | + sockets[key].emit(event, data, fn); | |
52 | + }); | |
53 | +}; | |
54 | + | |
44 | 55 | // socket.ioのコネクション設定 |
45 | 56 | io.sockets.on('connection', function (socket) { |
57 | + | |
46 | 58 | // コネクションが確立されたら'connected'メッセージを送信する |
47 | 59 | socket.emit('connected', {}); |
48 | 60 | |
49 | - // クライアントは'connected'メッセージを受信したら、 | |
50 | - // クライアントの情報とともに'regist request'メッセージを送信する | |
51 | - socket.on('regist request', function (client) { | |
52 | - console.log('regist client'); | |
53 | - if (socketsOf[client.roomId] === undefined) { | |
61 | + // 認証情報を確認する | |
62 | + socket.on('hash password', function (password, fn) { | |
63 | + var hashedPassword = ''; | |
64 | + var shasum = crypto.createHash('sha512'); | |
65 | + | |
66 | + if (password !== '') { | |
67 | + shasum.update('initialhash'); | |
68 | + shasum.update(password); | |
69 | + hashedPassword = shasum.digest('hex'); | |
70 | + } | |
71 | + fn(hashedPassword); | |
72 | + }); | |
73 | + | |
74 | + // 認証情報を確認する | |
75 | + socket.on('check credential', function (client) { | |
76 | + // クライアントはconnectedメッセージを受信したら、minichatオブジェクトを | |
77 | + // 引数にこのメッセージを送信する | |
78 | + | |
79 | + // 認証情報の確認 | |
80 | + if (client.mode == 'create') { | |
81 | + // modeが'create'の場合、すでに対応するroomIdのチャットルームがないか | |
82 | + // チェックする | |
83 | + if (socketsOf[client.roomId] !== undefined) { | |
84 | + socket.emit('room exists', {}); | |
85 | + return; | |
86 | + } | |
54 | 87 | socketsOf[client.roomId] = {}; |
55 | 88 | } |
56 | - socketsOf[client.roomId][client.userName] = socket; | |
89 | + | |
90 | + if (client.mode == 'enter') { | |
91 | + // 対応するroomIdのチャットルームの存在をチェックする | |
92 | + if (socketsOf[client.roomId] === undefined) { | |
93 | + socket.emit('invalid credential', {}); | |
94 | + return; | |
95 | + } | |
96 | + // ユーザー名がかぶっていないかチェックする | |
97 | + if (socketsOf[client.roomId][client.userName] !== undefined) { | |
98 | + socket.emit('userName exists', {}); | |
99 | + return; | |
100 | + } | |
101 | + } | |
102 | + | |
103 | + // ソケットにクライアントの情報をセットする | |
104 | + socket.set('client', client, function () { | |
105 | + socketsOf[client.roomId][client.userName] = socket; | |
106 | + if (client.userName) { | |
107 | + console.log('user ' + client.userName + '@' + client.roomId + ' connected'); | |
108 | + } | |
109 | + }); | |
110 | + | |
111 | + // 認証成功 | |
112 | + socket.emit('credential ok', {}); | |
113 | + | |
114 | + // 既存クライアントにメンバーの変更を通知する | |
115 | + var members = Object.keys(socketsOf[client.roomId]); | |
116 | + emitToRoom(client.roomId, 'update members', members); | |
117 | + | |
118 | + var shasum = crypto.createHash('sha1') | |
119 | + var message = { | |
120 | + from: 'システムメッセージ', | |
121 | + body: client.userName + 'さんが入室しました', | |
122 | + roomId: client.roomId | |
123 | + } | |
124 | + message.date = _formatDate(new Date()); | |
125 | + shasum.update('-' + message.roomId); | |
126 | + message.id = (new Date()).getTime() + '-' + shasum.digest('hex'); | |
127 | + emitToRoom(message.roomId, 'push message', message); | |
128 | + | |
129 | + }); | |
130 | + | |
131 | + // ソケットが切断された場合、ソケット一覧からソケットを削除する | |
132 | + socket.on('disconnect', function () { | |
133 | + socket.get('client', function (err, client) { | |
134 | + if (err || !client) { | |
135 | + return; | |
136 | + } | |
137 | + var sockets = socketsOf[client.roomId]; | |
138 | + if(sockets !== undefined) { | |
139 | + delete sockets[client.userName]; | |
140 | + } | |
141 | + console.log('user ' + client.userName + '@' + client.roomId + ' disconnected'); | |
142 | + var members = Object.keys(sockets); | |
143 | + if (members.length === 0) { | |
144 | + delete socketsOf[client.roomId]; | |
145 | + } else { | |
146 | + // 既存クライアントにメンバーの変更を通知する | |
147 | + emitToRoom(client.roomId, 'update members', members); | |
148 | + var message = { | |
149 | + from: 'システムメッセージ', | |
150 | + body: client.userName + 'さんが退室しました', | |
151 | + roomId: client.roomId | |
152 | + } | |
153 | + var shasum = crypto.createHash('sha1') | |
154 | + message.date = _formatDate(new Date()); | |
155 | + shasum.update('-' + message.roomId); | |
156 | + message.id = (new Date()).getTime() + '-' + shasum.digest('hex'); | |
157 | + emitToRoom(message.roomId, 'push message', message); | |
158 | + | |
159 | + } | |
160 | + }); | |
57 | 161 | }); |
58 | 162 | |
59 | 163 | // クライアントは'say'メッセージとともにチャットメッセージを送信する |
60 | - socket.on('say', function (message) { | |
164 | + socket.on('say', function (message, fn) { | |
61 | 165 | console.log('receive message'); |
62 | - socket.emit('say accept', {}); | |
166 | + | |
167 | + var shasum = crypto.createHash('sha1') | |
63 | 168 | message.date = _formatDate(new Date()); |
64 | - if (socketsOf[message.roomId] !== undefined) { | |
65 | - var sockets = socketsOf[message.roomId]; | |
66 | - Object.keys(sockets).forEach(function (key) { | |
67 | - sockets[key].emit('push message', message); | |
169 | + shasum.update(message.userName + '-' + message.roomId); | |
170 | + message.id = (new Date()).getTime() + '-' + shasum.digest('hex'); | |
171 | + emitToRoom(message.roomId, 'push message', message); | |
172 | + // クライアント側のコールバックを実行する | |
173 | + fn(); | |
174 | + }); | |
175 | + | |
176 | + // クライアントはログが必要な場合'request log'メッセージを送信する | |
177 | + socket.on('request log', function (data) { | |
178 | + socket.get('client', function (err, client) { | |
179 | + if (err || client === undefined) { | |
180 | + return; | |
181 | + } | |
182 | + emitToRoom(client.roomId, 'request log', {}, function (log) { | |
183 | + socket.emit('update log', log); | |
68 | 184 | }); |
69 | - } | |
185 | + }); | |
70 | 186 | }); |
71 | 187 | |
72 | 188 | }); |
@@ -2,6 +2,7 @@ | ||
2 | 2 | |
3 | 3 | (function () { |
4 | 4 | var socket; |
5 | + var messageLogs = {}; | |
5 | 6 | |
6 | 7 | // ページロード時の処理 |
7 | 8 | $(document).ready(function () { |
@@ -11,36 +12,108 @@ | ||
11 | 12 | // メッセージハンドラの定義 |
12 | 13 | // サーバーへの接続完了 |
13 | 14 | socket.on('connected', function(data) { |
14 | - socket.emit('regist request', minichat); | |
15 | + socket.emit('check credential', minichat); | |
15 | 16 | }); |
16 | 17 | |
17 | - // チャットメッセージ受信 | |
18 | - socket.on('push message', function (message) { | |
19 | - var html = '<div class="message">' | |
20 | - + '<p class="postdate pull-right">' + message.date + '</p>' | |
21 | - + '<p class="author">' + message.from + ':</p>' | |
22 | - + '<p class="comment">' + message.body + '</p>' | |
23 | - + '</div>'; | |
24 | - $('#messages').prepend(html); | |
18 | + // 認証成功 | |
19 | + socket.on('credential ok', function(data) { | |
20 | + socket.emit('request log', {}); | |
21 | + }); | |
22 | + | |
23 | + // 認証失敗:ルーム名/パスワードの不一致 | |
24 | + socket.on('invalid credential', function(data) { | |
25 | + authRetry('ルーム名/パスワードが不正です'); | |
26 | + }); | |
27 | + | |
28 | + // 認証失敗:同名のルームがすでに存在 | |
29 | + socket.on('room exists', function(data) { | |
30 | + authRetry('同名のルームがすでに存在します'); | |
25 | 31 | }); |
26 | 32 | |
27 | - // チャットメッセージの送信完了 | |
28 | - socket.on('say accept', function (data) { | |
29 | - $('#message').val(''); | |
33 | + // 認証失敗:ルームに同名のユーザーが存在 | |
34 | + socket.on('userName exists', function(data) { | |
35 | + authRetry('その名前はすでに使われています'); | |
30 | 36 | }); |
31 | 37 | |
32 | - // メッセージの送信ボタンを有効化 | |
38 | + // チャットログの送信 | |
39 | + socket.on('request log', function(data, callback) { | |
40 | + callback(messageLogs); | |
41 | + }); | |
42 | + | |
43 | + // チャットログの更新 | |
44 | + socket.on('update log', function(log) { | |
45 | + Object.keys(log).forEach(function (key) { | |
46 | + messageLogs[key] = log[key]; | |
47 | + }); | |
48 | + updateMessage(); | |
49 | + }); | |
50 | + | |
51 | + // チャットルームへのメンバー追加 | |
52 | + socket.on('update members', function (members) { | |
53 | + $('#members').empty(); | |
54 | + for (var i = 0; i < members.length; i++) { | |
55 | + var html = '<li>' + members[i] + '</li>'; | |
56 | + $('#members').append(html); | |
57 | + } | |
58 | + }); | |
59 | + | |
60 | + // チャットメッセージ受信 | |
61 | + socket.on('push message', function (message) { | |
62 | + messageLogs[message.id] = message; | |
63 | + prependMessage(message); | |
64 | + }); | |
65 | + | |
66 | + // チャットメッセージ送信 | |
33 | 67 | $('#post-message').on('click', function () { |
34 | 68 | var message = { |
35 | 69 | from: minichat.userName, |
36 | 70 | body: $('#message').val(), |
37 | 71 | roomId: minichat.roomId |
38 | 72 | }; |
39 | - socket.emit('say', message); | |
73 | + socket.emit('say', message, function () { | |
74 | + // メッセージの送信に成功したらテキストボックスをクリアする | |
75 | + $('#message').val(''); | |
76 | + }); | |
77 | + }); | |
78 | + | |
79 | + $('#credential-dialog-form').on('submit', function() { | |
80 | + $('#credentialDialog').modal('hide'); | |
81 | + socket.emit('hash password', $('#new-password').val(), function (hashedPassword) { | |
82 | + minichat.roomName = $('#new-room').val(); | |
83 | + minichat.userName = $('#new-name').val(); | |
84 | + minichat.password = hashedPassword; | |
85 | + minichat.roomId = minichat.roomName + minichat.password; | |
86 | + socket.emit('check credential', minichat); | |
87 | + }); | |
88 | + return false; | |
40 | 89 | }); |
41 | 90 | |
42 | 91 | }); // document.ready()ここまで |
43 | 92 | |
93 | + function authRetry(message) { | |
94 | + $('#credential-dialog-header').text(message); | |
95 | + $('#new-room').val(minichat.roomName); | |
96 | + $('#new-name').val(minichat.userName); | |
97 | + $('#credentialDialog').modal('show'); | |
98 | + } | |
99 | + | |
100 | + function prependMessage(message) { | |
101 | + var html = '<div class="message" id="' + message.id + '">' | |
102 | + + '<p class="postdate pull-right">' + message.date + '</p>' | |
103 | + + '<p class="author">' + message.from + ':</p>' | |
104 | + + '<p class="comment">' + message.body + '</p>' | |
105 | + + '</div>'; | |
106 | + $('#messages').prepend(html); | |
107 | + } | |
108 | + | |
109 | + function updateMessage() { | |
110 | + $('#messages').empty(); | |
111 | + var keys = Object.keys(messageLogs); | |
112 | + keys.sort(); | |
113 | + keys.forEach(function (key) { | |
114 | + prependMessage(messageLogs[key]); | |
115 | + }); | |
116 | + } | |
44 | 117 | |
45 | 118 | }).apply(this); |
46 | 119 |
@@ -8,37 +8,22 @@ exports.index = function(req, res){ | ||
8 | 8 | res.render('index', { title: 'minichat' }); |
9 | 9 | }; |
10 | 10 | |
11 | -exports.echo = function(req, res) { | |
12 | - var roomName = req.params.name; | |
13 | - var yourName = 'test'; | |
14 | - var password = 'minichattest'; | |
15 | - var hashedPassword = ''; | |
16 | - var shasum = crypto.createHash('sha512'); | |
17 | - | |
18 | - if (password !== '') { | |
19 | - shasum.update(password); | |
20 | - hashedPassword = shasum.digest('hex'); | |
21 | - } | |
22 | - | |
23 | - var params = { | |
24 | - title: 'チャットルーム:' + roomName, | |
25 | - room: { | |
26 | - name: roomName, | |
27 | - password: hashedPassword | |
28 | - }, | |
29 | - user: {name: yourName} | |
30 | - }; | |
31 | - res.render('room', params); | |
32 | -}; | |
33 | - | |
34 | 11 | exports.room = function(req, res){ |
35 | 12 | var roomName = req.body.roomName || ''; |
36 | 13 | var yourName = req.body.yourName || ''; |
37 | 14 | var password = req.body.password || ''; |
15 | + var mode = req.body.mode; | |
16 | + | |
17 | + if (mode === undefined) { | |
18 | + res.send(500); | |
19 | + return; | |
20 | + }; | |
21 | + | |
38 | 22 | var hashedPassword = ''; |
39 | 23 | var shasum = crypto.createHash('sha512'); |
40 | 24 | |
41 | 25 | if (password !== '') { |
26 | + shasum.update('initialhash'); | |
42 | 27 | shasum.update(password); |
43 | 28 | hashedPassword = shasum.digest('hex'); |
44 | 29 | } |
@@ -49,7 +34,8 @@ exports.room = function(req, res){ | ||
49 | 34 | name: roomName, |
50 | 35 | password: hashedPassword |
51 | 36 | }, |
52 | - user: {name: yourName} | |
37 | + user: {name: yourName}, | |
38 | + mode: mode | |
53 | 39 | }; |
54 | 40 | res.render('room', params); |
55 | 41 | }; |
@@ -24,12 +24,12 @@ block content | ||
24 | 24 | .controls |
25 | 25 | label |
26 | 26 | input#new-password(type='password', name='password', placeholder='パスワードを入力...') |
27 | - button#create(type='submit', class='btn', name='create') チャットルームを作成 | |
27 | + button.btn(type='submit', name='mode', value='create') チャットルームを作成 | |
28 | 28 | |
29 | 29 | .span6 |
30 | 30 | h2 Enter: |
31 | 31 | p 既存のチャットルームに入る |
32 | - form#enter.form-horizontal(action='/enter/', method='post') | |
32 | + form#enter.form-horizontal(action='/room/', method='post') | |
33 | 33 | .control-group |
34 | 34 | label.control-label(for='enter-room') ルーム名 |
35 | 35 | .controls |
@@ -43,7 +43,7 @@ block content | ||
43 | 43 | .controls |
44 | 44 | label |
45 | 45 | input#enter-password(type='password', name='password', placeholder='パスワードを入力...') |
46 | - button(type='submit', name='enter', class='btn') チャットルームへ入る | |
46 | + button.btn(type='submit', name='mode', value='enter') チャットルームへ入る | |
47 | 47 | |
48 | 48 | hr |
49 | 49 | footer |
@@ -6,7 +6,8 @@ block content | ||
6 | 6 | roomName: '#{room.name}', |
7 | 7 | password: '#{room.password}', |
8 | 8 | userName: '#{user.name}', |
9 | - roomId: '#{room.name}#{room.password}' | |
9 | + roomId: '#{room.name}#{room.password}', | |
10 | + mode: '#{mode}' | |
10 | 11 | }; |
11 | 12 | |
12 | 13 | .navbar.navbar-inverse.navbar-fixed-top |
@@ -18,7 +19,7 @@ block content | ||
18 | 19 | a ルーム名:#{room.name} |
19 | 20 | li |
20 | 21 | a(href='/') 退出する |
21 | - .container | |
22 | + #main-contents.container | |
22 | 23 | .row |
23 | 24 | .span4 |
24 | 25 | h3 |
@@ -30,14 +31,35 @@ block content | ||
30 | 31 | #members-box |
31 | 32 | h5 チャットメンバー: |
32 | 33 | ul#members |
33 | - li #{user.name} | |
34 | 34 | .span8 |
35 | 35 | #messages.well |
36 | - .message | |
37 | - p.postdate.pull-right 10/23 12:34 | |
38 | - p.author システムメッセージ: | |
39 | - p.comment チャットルーム「hogehoge」が作成されました。HASH: #{room.password} | |
40 | 36 | |
37 | + // 入室時の確認ダイアログ | |
38 | + // パスワードが不正 | |
39 | + #credentialDialog.modal.hide(data-backdrop='static', keyboard='false') | |
40 | + .modal-header | |
41 | + h3#credential-dialog-header 入室エラー | |
42 | + .modal-body | |
43 | + p#credential-dialog-message ルーム情報や名前を再入力してください: | |
44 | + form#credential-dialog-form.form-horizontal | |
45 | + .control-group | |
46 | + label.control-label(for='new-room-name') ルーム名 | |
47 | + .controls | |
48 | + input#new-room(type='text', name='roomName', placeholder='ルーム名を入力...') | |
49 | + .control-group | |
50 | + label.control-label(for='new-name') 表示するあなたの名前 | |
51 | + .controls | |
52 | + input#new-name(type='text', name='yourName', placeholder='表示名を入力...') | |
53 | + .control-group | |
54 | + label.control-label(for='new-password') 認証用パスワード | |
55 | + .controls | |
56 | + label | |
57 | + input#new-password(type='password', name='password', placeholder='パスワードを入力...') | |
58 | + button.btn.btn-primary(type='submit') 再試行 | |
59 | + | |
60 | + .modal-footer | |
61 | + a(href='/') トップページへ戻る | |
62 | + | |
41 | 63 | |
42 | 64 | hr |
43 | 65 | footer |