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

  最近的计算机网络课上老师开始讲socket,tcp相关的知识,当时脑袋里就蹦出一个想法,那就是打造一个聊天室。实现方式也挺多的,常见的可以用C++或者Java进行socket编程来构建这么一个聊天室。当然,我毫不犹豫选择了node来写,node有一个名叫socket.io的框架已经很完善的封装了socket相关API,所以无论是学习还是使用都是非常容易上手的,在这里强烈推荐!demo已经做好并放到我的个人网站了,大家可以试试,挺好玩的。

  进去试试 ->   http://www.yinxiangyu.com:9000  (改编了socket.io官方提供的例子)

  源码 ->  https://github.com/yxy19950717/js-practice-demo/tree/master/2016-4/chat 

  在梳理整个demo之前,先来看看聊天室构建所要用到的原理性的东西。

 

  何为socket

  首先要很明确web聊天室客户端是如何与服务器进行通信的。没错,正是socket(套接字)对这样的通信负责。打个比方,如果你正使用你的计算机浏览页面,并且打开了1个telnet和1个ssh会话,那样你就有3个应用进程。当你的计算机中的运输层(tcp,udp)从底层的网络层接收数据时,它需要将接收到的数据定向到三个进程中的一个。而每个进程都有一个或多个套接字,它相当于从网络向进程传递数据和从进程向网络传递数据的门户。

  

  如上图,在接收端,运输层检查报文段中的字段,标识出接收套接字,进而将报文定向该套接字。这样将运输层报文段中的数据交付到正确的套接字的工作称为多路分解。同样在源主机从不同套接字中收集数据块,并为每个数据封装上首部信息(用于分解)从而生成报文段,然后将报文段传递到网络层,这样的工作叫做多路复用

  

  WebSocket与HTTP

  了解完socket套接字的基本原理,可以知道socket始终不是应用层的东西,它是连接应用层与传输层的一个桥梁,那从实现角度上考虑,我们应该如何来编写聊天室这样一个应用呢?

  HTTP是无状态的协议,何为无状态?就是指HTTP服务器并不保存关于客户的任何信息。因为TCP为HTTP提供了可靠数据传输服务,意味着一个客户进程发出的每个HTTP请求报文都能完整地到达服务器。HTTP的无状态的特点源于分层体系结构,它的优点也很明显,不用担心数据丢失。但也会出现这样的现象:服务器向客户发送被请求的文件,而不存储任何关于该客户的状态信息。也就是说当一个客户端接连两次请求同一个文件,服务器并不会因为刚刚为该客户提供了该文件而不再做出反应,而是重新发送,HTTP不记得之前做过什么事了

  当然在传统的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打造的公共聊天室

 

  最后,欢迎大家无聊的时候来我的聊天室聊天哦!

  

 

 

  

  

posted @ 2016-04-10 14:22  sharlly  阅读(2783)  评论(1编辑  收藏  举报