最近的计算机网络课上老师开始讲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
要打造一个聊天室应用,首先确定聊天中服务器需要接收的几个事件响应,分为如下几点:
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 });