使用socket.io打造公共聊天室
来源:互联网 发布:ubuntu vi 编辑 编辑:程序博客网 时间:2024/05/15 14:33
进去试试 -> 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.html13 app.use(express.static(__dirname + '/public'));14 15 // Chatroom16 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 executes25 // 接收到客户端发送的new message26 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.pic34 });35 });36 37 // when the client emits 'add user', this listens and executes38 // 有新用户进入时39 socket.on('add user', function (username) {40 if (addedUser) return;41 42 // we store the username in the socket session for this client43 // 将名字保存在socket的session中44 socket.username = username;45 ++numUsers;46 addedUser = true;47 socket.emit('login', {48 numUsers: numUsers49 });50 // echo globally (all clients) that a person has connected51 // 广播发送user joined到客户端52 socket.broadcast.emit('user joined', {53 username: socket.username,54 numUsers: numUsers55 });56 });57 58 // when the client emits 'typing', we broadcast it to others59 // 接收到xxx输入的消息60 socket.on('typing', function (data) {61 // 广播发送typing到客户端62 socket.broadcast.emit('typing', {63 username: socket.username,64 pic: data.pic65 });66 });67 68 // when the client emits 'stop typing', we broadcast it to others69 socket.on('stop typing', function () {70 socket.broadcast.emit('stop typing', {71 username: socket.username72 });73 });74 75 // when the user disconnects.. perform this76 socket.on('disconnect', function () {77 if (addedUser) {78 --numUsers;79 80 // echo globally that this client has left81 socket.broadcast.emit('user left', {82 username: socket.username,83 numUsers: numUsers84 });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 message143 function addChatTyping (data) {144 data.typing = true;145 data.message = '正在输入...';146 addChatMessage(data);147 }148 149 // Removes the visual chat typing message150 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 bottom157 // el - The element to add as a message158 // options.fade - If the element should fade-in (default = true)159 // options.prepend - If the element should prepend160 // all other messages (default = false)161 function addMessageElement (el, options) {162 var $el = el;163 164 // Setup default options165 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 options176 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 markup188 function cleanInput (input) {189 return $('<div/>').text(input).text();190 }191 192 // Updates the typing event193 function updateTyping () {194 if (connected) {195 if (!typing) {196 typing = true;197 socket.emit('typing',{198 pic: yourHeadPic199 });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 user215 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 function222 // hash确定名字颜色223 function getUsernameColor (username) {224 // Compute hash code225 var hash = 7;226 for (var i = 0; i < username.length; i++) {227 hash = username.charCodeAt(i) + (hash << 5) - hash;228 }229 // Calculate color230 var index = Math.abs(hash % COLORS.length);231 return COLORS[index];232 }233 234 // Keyboard events235 $document.on('keydown',function (event) {236 // Auto-focus the current input when a key is typed237 // 按ctrl,alt,meta以外的键可以键入文字字母数字等...238 if (!(event.ctrlKey || event.metaKey || event.altKey)) {239 $currentInput.focus();240 }241 // When the client hits ENTER on their keyboard242 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 events265 266 // Focus input when clicking anywhere on login page267 $loginPage.click(function () {268 $currentInput.focus();269 });270 271 // Focus input when clicking on the message input's border272 $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 events289 290 // 客户端socket接收到Login指令291 // Whenever the server emits 'login', log the login message292 socket.on('login', function (data) {293 connected = true;294 // Display the welcome message295 var message = "welcome to sharlly's chatroom";296 //传给Log函数297 log(message, {298 prepend: true299 });300 addParticipantsMessage(data);301 });302 303 // Whenever the server emits 'new message', update the chat body304 socket.on('new message', function (data) {305 addChatMessage(data);306 });307 308 // Whenever the server emits 'user joined', log it in the chat body309 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 body315 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 message322 socket.on('typing', function (data) {323 addChatTyping(data);324 });325 326 // Whenever the server emits 'stop typing', kill the typing message327 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打造的公共聊天室
最后,欢迎大家无聊的时候来我的聊天室聊天哦!
- 使用socket.io打造公共聊天室
- 使用socket.io打造公共聊天室
- nodejs学习笔记(3)-socket.io打造简易聊天室
- 使用nodejs引用socket.io做聊天室
- 使用socket.io编程的简单聊天室
- Socket.io在线聊天室
- SOCKET IO 网络聊天室
- Socket.io在线聊天室
- nodejs+socket.io聊天室
- nodejs socket.io 聊天室
- Socket.io在线聊天室
- socket.io 网络聊天室
- Socket.io在线聊天室
- 在Express和Socket.IO中使用session,聊天室
- 使用node.js和socket.io实现多人聊天室
- 使用Node.js+Socket.IO搭建WebSocket实时应用(聊天室)
- 使用socket.io做一个简单的WEB聊天室
- Node.js websocket 使用 socket.io库实现实时聊天室
- 318. Maximum Product of Word Lengths
- 简述三种异步上传文件方式
- 前端模块化开发篇之grunt&webpack篇
- 进军es6(1)---初识es6
- 进军es6(2)---解构赋值
- 使用socket.io打造公共聊天室
- 2016阿里前端开发实习生面试经历
- 网站重构那些事儿
- 基础算法-堆排序
- 细说webpack之流程篇
- PAT 1001 A+B Format
- Linux模拟shell的实现
- QuickSort
- Halcon 机器视觉编程初探