使用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打造的公共聊天室

 

最后,欢迎你们无聊的时候来个人聊天室聊天哦!

相关文章
相关标签/搜索