最近的計算機網絡課上老師開始講socket,tcp相關的知識,當時腦殼裏就蹦出一個想法,那就是打造一個聊天室。實現方式也挺多的,常見的能夠用C++或者Java進行socket編程來構建這麼一個聊天室。固然,我堅決果斷選擇了node來寫,node有一個名叫socket.io的框架已經很完善的封裝了socket相關API,因此不管是學習仍是使用都是很是容易上手的,在這裏強烈推薦!demo已經作好並放到個人我的網站了,你們能夠試試,挺好玩的。css
進去試試 -> http://www.yinxiangyu.com:9000 (改編了socket.io官方提供的例子)html
源碼 -> https://github.com/yxy19950717/js-practice-demo/tree/master/2016-4/chat 前端
在梳理整個demo以前,先來看看聊天室構建所要用到的原理性的東西。html5
何爲socketnode
首先要很明確web聊天室客戶端是如何與服務器進行通訊的。沒錯,正是socket(套接字)對這樣的通訊負責。打個比方,若是你正使用你的計算機瀏覽頁面,而且打開了1個telnet和1個ssh會話,那樣你就有3個應用進程。當你的計算機中的運輸層(tcp,udp)從底層的網絡層接收數據時,它須要將接收到的數據定向到三個進程中的一個。而每一個進程都有一個或多個套接字,它至關於從網絡向進程傳遞數據和從進程向網絡傳遞數據的門戶。nginx
如上圖,在接收端,運輸層檢查報文段中的字段,標識出接收套接字,進而將報文定向該套接字。這樣將運輸層報文段中的數據交付到正確的套接字的工做稱爲多路分解。一樣在源主機從不一樣套接字中收集數據塊,併爲每一個數據封裝上首部信息(用於分解)從而生成報文段,而後將報文段傳遞到網絡層,這樣的工做叫作多路複用。git
WebSocket與HTTPgithub
瞭解完socket套接字的基本原理,能夠知道socket始終不是應用層的東西,它是鏈接應用層與傳輸層的一個橋樑,那從實現角度上考慮,咱們應該如何來編寫聊天室這樣一個應用呢?web
HTTP是無狀態的協議,何爲無狀態?就是指HTTP服務器並不保存關於客戶的任何信息。由於TCP爲HTTP提供了可靠數據傳輸服務,意味着一個客戶進程發出的每一個HTTP請求報文都能完整地到達服務器。HTTP的無狀態的特色源於分層體系結構,它的優勢也很明顯,不用擔憂數據丟失。但也會出現這樣的現象:服務器向客戶發送被請求的文件,而不存儲任何關於該客戶的狀態信息。也就是說當一個客戶端接連兩次請求同一個文件,服務器並不會由於剛剛爲該客戶提供了該文件而再也不作出反應,而是從新發送,HTTP不記得以前作過什麼事了!ajax
固然在傳統的HTTP應用中,客戶端和服務器端時而須要在一個至關長的時間內進行通訊,一般會帶上cookie進行認證通訊,而長時間保持一個鏈接,會耗費時間和帶寬,這樣一來,性能會不是很好,而聊天室須要的是實時通訊,因此咱們更須要WebSocket這樣的協議。(部分瀏覽器還不支持WebSocket,在不是很追求實時的狀況下,仍然能夠採用HTTP中ajax的方式進行通訊)。
WebSocket是html5的一個新協議,它的出現主要是爲了解決ajax輪詢和long poll時給服務器帶來的壓力。在HTTP中,經過ajax輪詢和Long poll是不斷監聽服務器是否有新消息,而在WebSocket中,每當服務器有新消息時纔會推送,並且它能與代理服務器(通常來講是nginx或者apache)保持長久鏈接,但與HTTP不一樣的是,它只須要一次請求便可保持鏈接。
而對於socket.io這個框架,它兼容了WebSocket以及HTTP兩種協議的使用,在部分不能使用WebSocket協議的瀏覽器中,採用ajax輪詢方式進行消息交換。
若想對WebSocket作更多瞭解,能夠閱讀此文: WebSocket 是什麼原理?爲何能夠實現持久鏈接?
使用socket.io
socket.io是一個徹底由JavaScript實現、基於Node.js、支持WebSocket的協議用於實時通訊、跨平臺的開源框架,它包括了客戶端的JavaScript和服務器端的Node.js。Socket.IO除了支持WebSocket通信協議外,還支持許多種輪詢(Polling)機制以及其它實時通訊方式,並封裝成了通用的接口,而且在服務端實現了這些實時機制的相應代碼。Socket.IO實現的Polling通訊機制包括Adobe Flash Socket、AJAX長輪詢、AJAX multipart streaming、持久Iframe、JSONP輪詢等。Socket.IO可以根據瀏覽器對通信機制的支持狀況自動地選擇最佳的方式來實現網絡實時應用。
有了這樣一個框架,對於瞭解socket編程的你相信運用起來會很是容易上手了。socket.io的API能夠在如下兩個網站上進行學習
github: https://github.com/socketio/socket.io
要打造一個聊天室應用,首先肯定聊天中服務器須要接收的幾個事件響應,分爲以下幾點:
1.新用戶進來時 ('add user')
2.用戶正在輸入時 ('typing')
3.用戶中止輸入時 ('stop typing')
4.用戶發送消息時 ('new message')
5.用戶離開時 ('disconnect')
其次是客戶端的用戶(們)須要接收到的事件響應:
1.我進來了 ('login')
2.有人進來了 ('user joined')
3.有人正在輸入 ('typing')
4.有人中止了輸入 ('stop typing')
5.有人發送了新消息 ('new message')
6.有人離開了 ('user left')
接下來咱們須要用socket的on和emit接口進行編寫,服務器端代碼以下:
index.js:
1 // Setup basic express server 2 var express = require('express'); 3 var app = express(); 4 var server = require('http').createServer(app); 5 var io = require('socket.io')(server); 6 var port = process.env.PORT || 9000; 7 8 server.listen(port, function () { 9 console.log('Server listening at port %d', port); 10 }); 11 12 //路由,連接到public,訪問時直接訪問到index.html 13 app.use(express.static(__dirname + '/public')); 14 15 // Chatroom 16 17 // 在線人數 18 var numUsers = 0; 19 20 // 鏈接打開 21 io.on('connection', function (socket) { 22 var addedUser = false; 23 24 // when the client emits 'new message', this listens and executes 25 // 接收到客戶端發送的new message 26 socket.on('new message', function (data) { 27 socket.pic = data.pic; 28 // we tell the client to execute 'new message' 29 // 廣播發送new message 到客戶端 30 socket.broadcast.emit('new message', { 31 username: socket.username, 32 message: data.message, 33 pic: socket.pic 34 }); 35 }); 36 37 // when the client emits 'add user', this listens and executes 38 // 有新用戶進入時 39 socket.on('add user', function (username) { 40 if (addedUser) return; 41 42 // we store the username in the socket session for this client 43 // 將名字保存在socket的session中 44 socket.username = username; 45 ++numUsers; 46 addedUser = true; 47 socket.emit('login', { 48 numUsers: numUsers 49 }); 50 // echo globally (all clients) that a person has connected 51 // 廣播發送user joined到客戶端 52 socket.broadcast.emit('user joined', { 53 username: socket.username, 54 numUsers: numUsers 55 }); 56 }); 57 58 // when the client emits 'typing', we broadcast it to others 59 // 接收到xxx輸入的消息 60 socket.on('typing', function (data) { 61 // 廣播發送typing到客戶端 62 socket.broadcast.emit('typing', { 63 username: socket.username, 64 pic: data.pic 65 }); 66 }); 67 68 // when the client emits 'stop typing', we broadcast it to others 69 socket.on('stop typing', function () { 70 socket.broadcast.emit('stop typing', { 71 username: socket.username 72 }); 73 }); 74 75 // when the user disconnects.. perform this 76 socket.on('disconnect', function () { 77 if (addedUser) { 78 --numUsers; 79 80 // echo globally that this client has left 81 socket.broadcast.emit('user left', { 82 username: socket.username, 83 numUsers: numUsers 84 }); 85 } 86 }); 87 });
在客戶端,也必須有接收發送消息的腳本
main.js:
1 $(function() { 2 var FADE_TIME = 150; // ms 3 var TYPING_TIMER_LENGTH = 400; // ms 4 var COLORS = [ 5 '#e21400', '#91580f', '#f8a700', '#f78b00', 6 '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', 7 '#3b88eb', '#3824aa', '#a700ff', '#d300e7' 8 ]; 9 // Initialize variables 10 var $document = $(document); 11 var $usernameInput = $('.usernameInput'); // Input for username 12 var $messages = $('.messages'); // Messages area 13 var $inputMessage = $('.inputMessage'); // Input message input box 14 15 var $loginPage = $('.login.page'); // The login page 16 var $chatPage = $('.chat.page'); // The chatroom page 17 18 // 選頭像 19 20 var $headPic = $('.headPic li'); 21 22 // Prompt for setting a username 23 var username; 24 var connected = false; 25 var typing = false; 26 var lastTypingTime; 27 var yourHeadPic; 28 // 直接聚焦到輸入框 29 var $currentInput = $usernameInput.focus(); 30 31 var socket = io(); 32 33 function addParticipantsMessage (data) { 34 var message = ''; 35 if (data.numUsers === 1) { 36 message += "there's 1 participant"; 37 } else { 38 message += "there are " + data.numUsers + " participants"; 39 } 40 log(message); 41 } 42 43 // Sets the client's username 44 function setUsername () { 45 username = cleanInput($usernameInput.val().trim()); 46 47 // If the username is valid 48 if (username) { 49 $loginPage.fadeOut(); 50 $chatPage.show(); 51 $loginPage.off('click'); 52 $currentInput = $inputMessage.focus(); 53 54 // Tell the server your username 55 socket.emit('add user', username); 56 } 57 } 58 59 // Sends a chat message 60 function sendMessage () { 61 var message = $inputMessage.val(); 62 // Prevent markup from being injected into the message 63 message = cleanInput(message); 64 // if there is a non-empty message and a socket connection 65 // 顯示本身 66 if (message && connected) { 67 $inputMessage.val(''); 68 addChatMessage({ 69 pic: yourHeadPic, 70 username: username, 71 message: message, 72 owner: true 73 }); 74 // tell server to execute 'new message' and send along one parameter 75 socket.emit('new message', { 76 message: message, 77 pic: yourHeadPic 78 }); 79 } 80 } 81 82 83 84 // Log a message 85 function log (message, options) { 86 var $el = $('<li>').addClass('log').text(message); 87 addMessageElement($el, options); 88 } 89 90 // Adds the visual chat message to the message list 91 function addChatMessage (data, options) { 92 // Don't fade the message in if there is an 'X was typing' 93 var $typingMessages = getTypingMessages(data); 94 options = options || {}; 95 if ($typingMessages.length !== 0) { 96 options.fade = false; 97 $typingMessages.remove(); 98 } 99 // 選中的頭像 100 if(data.owner) { 101 //本身的話在右邊 102 var $img = $('<span class="myHeadPicRight"><img src='+data.pic+'.png></span>'); 103 104 var $usernameDiv = $('<span class="yourUsername"/>') 105 .text(data.username) 106 .css('color', getUsernameColor(data.username)); 107 var $messageBodyDiv = $('<span class="messageBody">') 108 .css('float', 'right') 109 .css('padding-right', '15px') 110 .text(data.message); 111 112 var $rightDiv = $('<p style="float:right; width:90%">') 113 .append($usernameDiv, $messageBodyDiv); 114 var typingClass = data.typing ? 'typing' : ''; 115 var $messageDiv = $('<li class="message clearfix"/>') 116 .data('username', data.username) 117 .addClass(typingClass) 118 .append($img, $rightDiv); 119 120 addMessageElement($messageDiv, options); 121 }else{ 122 var $img = $('<span class="myHeadPic"><img src='+data.pic+'.png></span>'); 123 124 var $usernameDiv = $('<span class="username"/>') 125 .text(data.username) 126 .css('color', getUsernameColor(data.username)); 127 var $messageBodyDiv = $('<span class="messageBody">') 128 .text(data.message); 129 130 var $rightDiv = $('<p style="float:left; width:90%">') 131 .append($usernameDiv, $messageBodyDiv); 132 var typingClass = data.typing ? 'typing' : ''; 133 var $messageDiv = $('<li class="message clearfix"/>') 134 .data('username', data.username) 135 .addClass(typingClass) 136 .append($img, $rightDiv); 137 138 addMessageElement($messageDiv, options); 139 } 140 } 141 142 // Adds the visual chat typing message 143 function addChatTyping (data) { 144 data.typing = true; 145 data.message = '正在輸入...'; 146 addChatMessage(data); 147 } 148 149 // Removes the visual chat typing message 150 function removeChatTyping (data) { 151 getTypingMessages(data).fadeOut(function () { 152 $(this).remove(); 153 }); 154 } 155 156 // Adds a message element to the messages and scrolls to the bottom 157 // el - The element to add as a message 158 // options.fade - If the element should fade-in (default = true) 159 // options.prepend - If the element should prepend 160 // all other messages (default = false) 161 function addMessageElement (el, options) { 162 var $el = el; 163 164 // Setup default options 165 if (!options) { 166 options = {}; 167 } 168 if (typeof options.fade === 'undefined') { 169 options.fade = true; 170 } 171 if (typeof options.prepend === 'undefined') { 172 options.prepend = false; 173 } 174 175 // Apply options 176 if (options.fade) { 177 $el.hide().fadeIn(FADE_TIME); 178 } 179 if (options.prepend) { 180 $messages.prepend($el); 181 } else { 182 $messages.append($el); 183 } 184 $messages[0].scrollTop = $messages[0].scrollHeight; 185 } 186 187 // Prevents input from having injected markup 188 function cleanInput (input) { 189 return $('<div/>').text(input).text(); 190 } 191 192 // Updates the typing event 193 function updateTyping () { 194 if (connected) { 195 if (!typing) { 196 typing = true; 197 socket.emit('typing',{ 198 pic: yourHeadPic 199 }); 200 } 201 lastTypingTime = (new Date()).getTime(); 202 203 setTimeout(function () { 204 var typingTimer = (new Date()).getTime(); 205 var timeDiff = typingTimer - lastTypingTime; 206 if (timeDiff >= TYPING_TIMER_LENGTH && typing) { 207 socket.emit('stop typing'); 208 typing = false; 209 } 210 }, TYPING_TIMER_LENGTH); 211 } 212 } 213 214 // Gets the 'X is typing' messages of a user 215 function getTypingMessages (data) { 216 return $('.typing.message').filter(function (i) { 217 return $(this).data('username') === data.username; 218 }); 219 } 220 221 // Gets the color of a username through our hash function 222 // hash肯定名字顏色 223 function getUsernameColor (username) { 224 // Compute hash code 225 var hash = 7; 226 for (var i = 0; i < username.length; i++) { 227 hash = username.charCodeAt(i) + (hash << 5) - hash; 228 } 229 // Calculate color 230 var index = Math.abs(hash % COLORS.length); 231 return COLORS[index]; 232 } 233 234 // Keyboard events 235 $document.on('keydown',function (event) { 236 // Auto-focus the current input when a key is typed 237 // 按ctrl,alt,meta之外的鍵能夠鍵入文字字母數字等... 238 if (!(event.ctrlKey || event.metaKey || event.altKey)) { 239 $currentInput.focus(); 240 } 241 // When the client hits ENTER on their keyboard 242 if (event.which === 13 ) { 243 // username已存在,已經登陸 244 if (username) { 245 sendMessage(); 246 socket.emit('stop typing'); 247 typing = false; 248 } else if(!yourHeadPic) { 249 // 沒有選擇頭像 250 alert('請選擇頭像!'); 251 return false; 252 } else { 253 // 首次登陸 254 setUsername(); 255 } 256 } 257 }); 258 259 // 輸入框一旦change就發送消息 260 $inputMessage.on('input', function() { 261 updateTyping(); 262 }); 263 264 // Click events 265 266 // Focus input when clicking anywhere on login page 267 $loginPage.click(function () { 268 $currentInput.focus(); 269 }); 270 271 // Focus input when clicking on the message input's border 272 $inputMessage.click(function () { 273 $inputMessage.focus(); 274 }); 275 276 277 // 選擇頭像 278 $headPic.on('click', function() { 279 var which = parseInt($(this).attr('class').slice(3))-1; 280 $('.chosePic li').each(function(i, item) { 281 $(item).children().remove(); 282 yourHeadPic = undefined; 283 }); 284 $('.chosePic li:eq(' + which + ')').append($('<span></span>')); 285 yourHeadPic = which + 1; 286 }); 287 288 // Socket events 289 290 // 客戶端socket接收到Login指令 291 // Whenever the server emits 'login', log the login message 292 socket.on('login', function (data) { 293 connected = true; 294 // Display the welcome message 295 var message = "welcome to sharlly's chatroom"; 296 //傳給Log函數 297 log(message, { 298 prepend: true 299 }); 300 addParticipantsMessage(data); 301 }); 302 303 // Whenever the server emits 'new message', update the chat body 304 socket.on('new message', function (data) { 305 addChatMessage(data); 306 }); 307 308 // Whenever the server emits 'user joined', log it in the chat body 309 socket.on('user joined', function (data) { 310 log(data.username + ' joined'); 311 addParticipantsMessage(data); 312 }); 313 314 // Whenever the server emits 'user left', log it in the chat body 315 socket.on('user left', function (data) { 316 log(data.username + ' left'); 317 addParticipantsMessage(data); 318 removeChatTyping(data); 319 }); 320 321 // Whenever the server emits 'typing', show the typing message 322 socket.on('typing', function (data) { 323 addChatTyping(data); 324 }); 325 326 // Whenever the server emits 'stop typing', kill the typing message 327 socket.on('stop typing', function (data) { 328 removeChatTyping(data); 329 }); 330 });
瞭解socket運行只需關注socket.on,socket.broadcast.emit這幾個函數。socket.on提供了接收消息的方法,接收到後,其第二個參數就是回調函數,而socket.broadcast.emit是廣播發送,向每一個用戶發送一個對象或一個字符串。到這裏你可能會以爲socket.io很是簡單,固然這只是它的一些功能,更多用法你們能夠自行學習。
剛剛提供的這個例子改編於socket.io的官方實例,博主在寫的時候對前端界面增長了頭像選擇,以及第一人稱第三人稱文字的排版佈局改動,因此在main.js中能夠代碼有些繁雜(因此只用關注有socket.的地方),完整代碼請到個人github上下載: socket.io打造的公共聊天室
最後,歡迎你們無聊的時候來個人聊天室聊天哦!