使用socket.io打造公共聊天室

  最近的計算機網絡課上老師開始講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

    官網: http://socket.io/docs/

  要打造一個聊天室應用,首先肯定聊天中服務器須要接收的幾個事件響應,分爲以下幾點:

    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 });
View Code

 

  在客戶端,也必須有接收發送消息的腳本

  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 });
View Code

  瞭解socket運行只需關注socket.on,socket.broadcast.emit這幾個函數。socket.on提供了接收消息的方法,接收到後,其第二個參數就是回調函數,而socket.broadcast.emit是廣播發送,向每一個用戶發送一個對象或一個字符串。到這裏你可能會以爲socket.io很是簡單,固然這只是它的一些功能,更多用法你們能夠自行學習。

  剛剛提供的這個例子改編於socket.io的官方實例,博主在寫的時候對前端界面增長了頭像選擇,以及第一人稱第三人稱文字的排版佈局改動,因此在main.js中能夠代碼有些繁雜(因此只用關注有socket.的地方),完整代碼請到個人github上下載: socket.io打造的公共聊天室

 

最後,歡迎你們無聊的時候來個人聊天室聊天哦!

相關文章
相關標籤/搜索