最近的计算机网络课上老师开始讲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. });

  在客户端,也必须有接收发送消息的脚本

  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. // Log a message
  83. function log (message, options) {
  84. var $el = $('<li>').addClass('log').text(message);
  85. addMessageElement($el, options);
  86. }
  87.  
  88. // Adds the visual chat message to the message list
  89. function addChatMessage (data, options) {
  90. // Don't fade the message in if there is an 'X was typing'
  91. var $typingMessages = getTypingMessages(data);
  92. options = options || {};
  93. if ($typingMessages.length !== 0) {
  94. options.fade = false;
  95. $typingMessages.remove();
  96. }
  97. // 选中的头像
  98. if(data.owner) {
  99. //自己的话在右边
  100. var $img = $('<span class="myHeadPicRight"><img src='+data.pic+'.png></span>');
  101.  
  102. var $usernameDiv = $('<span class="yourUsername"/>')
  103. .text(data.username)
  104. .css('color', getUsernameColor(data.username));
  105. var $messageBodyDiv = $('<span class="messageBody">')
  106. .css('float', 'right')
  107. .css('padding-right', '15px')
  108. .text(data.message);
  109.  
  110. var $rightDiv = $('<p style="float:right; width:90%">')
  111. .append($usernameDiv, $messageBodyDiv);
  112. var typingClass = data.typing ? 'typing' : '';
  113. var $messageDiv = $('<li class="message clearfix"/>')
  114. .data('username', data.username)
  115. .addClass(typingClass)
  116. .append($img, $rightDiv);
  117.  
  118. addMessageElement($messageDiv, options);
  119. }else{
  120. var $img = $('<span class="myHeadPic"><img src='+data.pic+'.png></span>');
  121.  
  122. var $usernameDiv = $('<span class="username"/>')
  123. .text(data.username)
  124. .css('color', getUsernameColor(data.username));
  125. var $messageBodyDiv = $('<span class="messageBody">')
  126. .text(data.message);
  127.  
  128. var $rightDiv = $('<p style="float:left; width:90%">')
  129. .append($usernameDiv, $messageBodyDiv);
  130. var typingClass = data.typing ? 'typing' : '';
  131. var $messageDiv = $('<li class="message clearfix"/>')
  132. .data('username', data.username)
  133. .addClass(typingClass)
  134. .append($img, $rightDiv);
  135.  
  136. addMessageElement($messageDiv, options);
  137. }
  138. }
  139.  
  140. // Adds the visual chat typing message
  141. function addChatTyping (data) {
  142. data.typing = true;
  143. data.message = '正在输入...';
  144. addChatMessage(data);
  145. }
  146.  
  147. // Removes the visual chat typing message
  148. function removeChatTyping (data) {
  149. getTypingMessages(data).fadeOut(function () {
  150. $(this).remove();
  151. });
  152. }
  153.  
  154. // Adds a message element to the messages and scrolls to the bottom
  155. // el - The element to add as a message
  156. // options.fade - If the element should fade-in (default = true)
  157. // options.prepend - If the element should prepend
  158. // all other messages (default = false)
  159. function addMessageElement (el, options) {
  160. var $el = el;
  161.  
  162. // Setup default options
  163. if (!options) {
  164. options = {};
  165. }
  166. if (typeof options.fade === 'undefined') {
  167. options.fade = true;
  168. }
  169. if (typeof options.prepend === 'undefined') {
  170. options.prepend = false;
  171. }
  172.  
  173. // Apply options
  174. if (options.fade) {
  175. $el.hide().fadeIn(FADE_TIME);
  176. }
  177. if (options.prepend) {
  178. $messages.prepend($el);
  179. } else {
  180. $messages.append($el);
  181. }
  182. $messages[0].scrollTop = $messages[0].scrollHeight;
  183. }
  184.  
  185. // Prevents input from having injected markup
  186. function cleanInput (input) {
  187. return $('<div/>').text(input).text();
  188. }
  189.  
  190. // Updates the typing event
  191. function updateTyping () {
  192. if (connected) {
  193. if (!typing) {
  194. typing = true;
  195. socket.emit('typing',{
  196. pic: yourHeadPic
  197. });
  198. }
  199. lastTypingTime = (new Date()).getTime();
  200.  
  201. setTimeout(function () {
  202. var typingTimer = (new Date()).getTime();
  203. var timeDiff = typingTimer - lastTypingTime;
  204. if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
  205. socket.emit('stop typing');
  206. typing = false;
  207. }
  208. }, TYPING_TIMER_LENGTH);
  209. }
  210. }
  211.  
  212. // Gets the 'X is typing' messages of a user
  213. function getTypingMessages (data) {
  214. return $('.typing.message').filter(function (i) {
  215. return $(this).data('username') === data.username;
  216. });
  217. }
  218.  
  219. // Gets the color of a username through our hash function
  220. // hash确定名字颜色
  221. function getUsernameColor (username) {
  222. // Compute hash code
  223. var hash = 7;
  224. for (var i = 0; i < username.length; i++) {
  225. hash = username.charCodeAt(i) + (hash << 5) - hash;
  226. }
  227. // Calculate color
  228. var index = Math.abs(hash % COLORS.length);
  229. return COLORS[index];
  230. }
  231.  
  232. // Keyboard events
  233. $document.on('keydown',function (event) {
  234. // Auto-focus the current input when a key is typed
  235. // 按ctrl,alt,meta以外的键可以键入文字字母数字等...
  236. if (!(event.ctrlKey || event.metaKey || event.altKey)) {
  237. $currentInput.focus();
  238. }
  239. // When the client hits ENTER on their keyboard
  240. if (event.which === 13 ) {
  241. // username已存在,已经登录
  242. if (username) {
  243. sendMessage();
  244. socket.emit('stop typing');
  245. typing = false;
  246. } else if(!yourHeadPic) {
  247. // 没有选择头像
  248. alert('请选择头像!');
  249. return false;
  250. } else {
  251. // 首次登录
  252. setUsername();
  253. }
  254. }
  255. });
  256.  
  257. // 输入框一旦change就发送消息
  258. $inputMessage.on('input', function() {
  259. updateTyping();
  260. });
  261.  
  262. // Click events
  263.  
  264. // Focus input when clicking anywhere on login page
  265. $loginPage.click(function () {
  266. $currentInput.focus();
  267. });
  268.  
  269. // Focus input when clicking on the message input's border
  270. $inputMessage.click(function () {
  271. $inputMessage.focus();
  272. });
  273.  
  274. // 选择头像
  275. $headPic.on('click', function() {
  276. var which = parseInt($(this).attr('class').slice(3))-1;
  277. $('.chosePic li').each(function(i, item) {
  278. $(item).children().remove();
  279. yourHeadPic = undefined;
  280. });
  281. $('.chosePic li:eq(' + which + ')').append($('<span></span>'));
  282. yourHeadPic = which + 1;
  283. });
  284.  
  285. // Socket events
  286.  
  287. // 客户端socket接收到Login指令
  288. // Whenever the server emits 'login', log the login message
  289. socket.on('login', function (data) {
  290. connected = true;
  291. // Display the welcome message
  292. var message = "welcome to sharlly's chatroom";
  293. //传给Log函数
  294. log(message, {
  295. prepend: true
  296. });
  297. addParticipantsMessage(data);
  298. });
  299.  
  300. // Whenever the server emits 'new message', update the chat body
  301. socket.on('new message', function (data) {
  302. addChatMessage(data);
  303. });
  304.  
  305. // Whenever the server emits 'user joined', log it in the chat body
  306. socket.on('user joined', function (data) {
  307. log(data.username + ' joined');
  308. addParticipantsMessage(data);
  309. });
  310.  
  311. // Whenever the server emits 'user left', log it in the chat body
  312. socket.on('user left', function (data) {
  313. log(data.username + ' left');
  314. addParticipantsMessage(data);
  315. removeChatTyping(data);
  316. });
  317.  
  318. // Whenever the server emits 'typing', show the typing message
  319. socket.on('typing', function (data) {
  320. addChatTyping(data);
  321. });
  322.  
  323. // Whenever the server emits 'stop typing', kill the typing message
  324. socket.on('stop typing', function (data) {
  325. removeChatTyping(data);
  326. });
  327. });

  了解socket运行只需关注socket.on,socket.broadcast.emit这几个函数。socket.on提供了接收消息的方法,接收到后,其第二个参数就是回调函数,而socket.broadcast.emit是广播发送,向每个用户发送一个对象或一个字符串。到这里你可能会觉得socket.io非常简单,当然这只是它的一些功能,更多用法大家可以自行学习。

  刚刚提供的这个例子改编于socket.io的官方实例,博主在写的时候对前端界面增加了头像选择,以及第一人称第三人称文字的排版布局改动,所以在main.js中可以代码有些繁杂(所以只用关注有socket.的地方),完整代码请到我的github上下载: socket.io打造的公共聊天室

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

  

  

  

使用socket.io打造公共聊天室的更多相关文章

  1. Express+Socket.IO 实现简易聊天室

    代码地址如下:http://www.demodashi.com/demo/12477.html 闲暇之余研究了一下 Socket.io,搭建了一个简易版的聊天室,如有不对之处还望指正,先上效果图: 首 ...

  2. AngularJS+Node.js+socket.io 开发在线聊天室

    所有文章搬运自我的个人主页:sheilasun.me 不得不说,上手AngularJS比我想象得难多了,把官网提供的PhoneCat例子看完,又跑到慕课网把大漠穷秋的AngularJS实战系列看了一遍 ...

  3. node+express+socket.io制作一个聊天室功能

    首先是下载包: npm install express npm install socket.io 建立文件: 服务器端代码:server.js var http=require("http ...

  4. 利用socket.io构建一个聊天室

    利用socket.io来构建一个聊天室,输入自己的id和消息,所有的访问用户都可以看到,类似于群聊. socket.io 这里只用来做一个简单的聊天室,官网也有例子,很容易就做出来了.其实主要用的东西 ...

  5. Socket.io文字直播聊天室的简单代码

    直接上代码吧,被注释掉的主要是调试代码,和技术选型的测试代码 var app = require('express')(); var server = require('http').Server(a ...

  6. 利用socket.io+nodejs打造简单聊天室

    代码地址如下:http://www.demodashi.com/demo/11579.html 界面展示: 首先展示demo的结果界面,只是简单消息的发送和接收,包括发送文字和发送图片. ws说明: ...

  7. java基于socket公共聊天室的实现

    项目:一个公共聊天室功能的实现,实现了登录聊天,保存聊天记录等功能. 一.实现代码 1.客户端 ChatClient.java import java.io.BufferedReader; impor ...

  8. Node+Express+MongoDB + Socket.io搭建实时聊天应用

    Node+Express+MongoDB + Socket.io搭建实时聊天应用 前言 本来开始写博客的时候只是想写一下关于MongoDB的使用总结的,后来觉得还不如干脆写一个node项目实战教程实战 ...

  9. Node+Express+MongoDB + Socket.io搭建实时聊天应用实战教程(二)--node解析与环境搭建

    前言 本来开始写博客的时候只是想写一下关于MongoDB的使用总结的,后来觉得还不如干脆写一个node项目实战教程实战.写教程一方面在自己写的过程中需要考虑更多的东西,另一方面希望能对node入门者有 ...

随机推荐

  1. CCPC网赛,HDU_5832 A water problem

    Problem Description           Two planets named Haha and Xixi in the universe and they were created ...

  2. 使用Idea编写javaweb以及maven的综合(一)

    今天总结的第一点是在windows下使用idea编写jsp并且使用tomcat部署:第二点是新建maven项目,之前一直是听说也没有自己实践过,今天就大概说一下. 0x01 IDEA 全称 Intel ...

  3. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

    http://quote.eastmoney.com/center/list.html#28003501_0_2 http://bbs.tianya.cn/post-53726-21098-1.sht ...

  4. Spring Cloud Eureka Server 启停状态监控

    目前发现如下的api: 当时没有找到文档 http://localhost:8761/eureka/apps 参考文章:(此文中api带有v2我自己试验不需要v2) http://blog.csdn. ...

  5. python的一些学习资料(持续更新中)

    Markdown在线编辑器 廖雪峰官方博客[基础入门好资料] python-guide[传说中的巨牛写的] the5fire的技术博客[全职python程序员博客]

  6. Tasklist 命令的使用

    1,根据PID查找进程 tasklist /fi "pid eq 2245" 2,根据名称查找进程 tasklist /fi "imagename eq notepad. ...

  7. AOP和IOC理解

    在百度上看到一篇很有意思的文章,是对AOP的一种解释,如下:(摘自:百度文库的 AOP和IOC最容易理解的说明(Spring知识小计)): IOC,依赖倒置的意思, 所谓依赖,从程序的角度看,就是比如 ...

  8. 提交App Store注意事项1

    1.未遵守苹果iOS APP数据储存指导方针. 如果你的App有离线数据下载功能,尤其需要关注这一点.因为离线数据一般占用存储空间比较大,可以被重新下载和重建,但是用户往往希望系统存储空间紧时也依然能 ...

  9. OC与JS交互前言-b

    一.WebView加载HTML UIWebView提供了三个方法来加载html资源 1. loadHTMLString:baseURL: 把html文件的内容以字符串的形式加载到webView里面,然 ...

  10. Nopcommerce 3.7 增加了Redis 作为缓存啦

    Nopcommerce 3.7  版增加了Redis 缓存,相比之前内存缓存, 感觉使用了Redis 作为缓存,明显加快了Web页面响应速度! 废话少说 拉代码: 1 git clone https: ...