Socket.io学习笔记
Socket.io是一个Web通信框架框架,同时支持基于浏览器环境的客户端、基于Node.js的服务器端。它实现了实时的、事件驱动的双向通信。使用Socket.io,你可以:
- 推送数据,让客户端展示实时更新的仪表/图表、文本信息
- 推送二进制流,从1.0版本开始,Socket.io支持推送图片、音频、视频
- 实现多用户协作,例如联网游戏、共同编辑文档
Socket.io主要由两个部分组成:
- socket.io模块,集成到Node.js的http模块的服务器
- socket.io-client,在浏览器中运行的客户端
Socket.io支持多种传输机制,例如WebSocket、Adobe Flash Sockets、XHR轮询、JsonP轮询,它们被隔离在统一的接口之下,这意味着任何浏览器都可以作为客户端。
标准的WebSocket服务器并不能和Socket.io客户端进行直接通信,需要注意这一点。
我们以一个简单的聊天室应用作为入门教程,你会发现利用Socket.io编写这类应用有多简单。特别是前后端都是基于Socket.io实现的时候,连API接口都一样,太方便了。
首先,为工程添加依赖:
1 2 |
npm install --save express npm install --save socket.io |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 创建一个Express应用 var app = require('express')(); app.use(function (req, res, next) { next(); }) // 创建一个HTTP服务器 var http = require('http').Server(app); // 创建一个IO实例,可以看到socket.io和Express可以共享一个HTTP服务器套接字 var io = require('socket.io')(http); // Express代码,首页 app.get('/', function (req, res) { res.sendfile('index.html'); }); // 侦听客户端连接事件 io.on('connection', function (socket) { console.log('a user connected'); // socket变量为代表一个客户端连接的套接字 socket.on('chat message', function (msg) { // 用户输入文本,并点击发送后,该事件由客户端发送 console.log('message: ' + msg); // 服务器把可以把消息发送给所有人: io.emit('chat message', msg); }); socket.on('disconnect', function () { // 用户刷新浏览器时,会触发该事件,因为socket.io-client做了清理工作 console.log('user disconnected'); }); }); // 启动HTTP服务器 http.listen(3000, function () { console.log('listening on *:3000'); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<!DOCTYPE html> <html lang="en"> <head> <title>Socket.IO chat</title> <meta charset="UTF-8"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } </style> <!-- 这个URL由Socket.io服务器负责生成 --> <script src="/socket.io/socket.io.js"></script> <script src="http://code.jquery.com/jquery-1.11.1.js"></script> </head> <body> <ul id="messages"></ul> <form action=""> <input id="m" autocomplete="off"/> <button>Send</button> </form> <script> // 暴露了全局函数io,调用它后会尝试基于WebSocket协议连接到服务器,并得到一个客户端套接字对象 // 此对象就像Node.js中的套接字一样,是一个EventEmitter var socket = io(); $('form').submit(function(){ // Socket.io的主要思想就是,你可以接收、发送任何事件,事件的载荷可以是任何数据,包括编码为JSON的对象或者二进制数据 socket.emit('chat message', $('#m').val()); $('#m').val(''); return false; }); socket.on('chat message', function(msg){ // 该事件由服务器发送,它把消息广播到所有客户端 $('#messages').append($('<li>').text(msg)); }); </script> </body> </html> |
执行命令 node app.js 启动服务器,然后浏览器打开http://127.0.0.1:3000,多打开几个这样的窗口,就可以聊天了。
有几种不同的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 方法一:配合Node.js的HTTP服务 var app = require( 'http' ).createServer( function ( req, res ) { /* */ } ); var io = require('socket.io')(app); app.listen(80); // 方法二:配合Express框架 var app = require('express')(); var server = require('http').Server(app); var io = require('socket.io')(server); server.listen(80); // 方法三:隐式HTTP服务创建 var io = require('socket.io')(80); |
你可以使用名字空间(即端点,对应URL路径)实现单个WebSocket的多路复用(multiplexing),这样多个应用模块可以共享单个TCP连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var io = require( 'socket.io' )( 80 ); var chat = io // 名字空间一 .of( '/chat' ) .on( 'connection', function ( socket ) { // 发送消息的两种方式: socket.emit( 'a message', { that: 'only' , '/chat': 'will get' } ); chat.emit( 'a message', { everyone: 'in' , '/chat': 'will get' } ); } ); var news = io // 名字空间二 .of( '/news' ) .on( 'connection', function ( socket ) { socket.emit( 'item', { news: 'item' } ); } ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<script> // 名字空间一 var chat = io.connect( 'http://localhost/chat' ); // 名字空间二,如果 协议://主机名:端口 与当前网页的相同,可以省略 var news = io.connect( '/news' ); chat.on( 'connect', function () { chat.emit( 'hi!' ); } ); news.on( 'news', function () { news.emit( 'woot' ); } ); </script> |
Socket.io客户端默认连接到此名字空间,对应URL路径 / ,Socket.io服务器默认也在此名字空间上监听。
下面的代码都是在默认名字空间上发送消息:
1 2 |
io.sockets.emit('hi', 'everyone'); // 方式一 io.emit('hi', 'everyone'); // 方式二 |
在默认名字空间上,也可以监听connection事件,来监听新的客户端连接:
1 2 3 |
io.on( 'connection', function ( socket ) { socket.on( 'disconnect', function () { } ); } ); |
在每个名字空间内部,你还可以定义任意数量的房间(频道,channel)。每个Socket都可以自由的 join() 或者 leave() 房间:
1 2 3 4 5 6 7 8 |
// 在连接后立即加入房间 io.on( 'connection', function ( socket ) { socket.join( 'some room' ); } ); // 调用to可以随时加入房间 io.to( 'some room' ).emit( 'some event' ); // 离开房间 io.leave( 'some room' ); |
Socket.io中的每一个Socket由一个随机的、唯一的、不可猜测的ID来标识。Socket会自动加入到以此ID来标识的房间:
1 2 3 4 5 6 |
io.on( 'connection', function ( socket ) { socket.on( 'say to someone', function ( id, msg ) { // 广播到房间 socket.broadcast.to( id ).emit( 'my message', msg ); } ); } ); |
在断开连接时,Socket会自动离开所有加入过的房间。
如果某些客户端没有准备好接收消息,原因可能包括:网络缓慢、客户端基于长轮询协议连接且正处于请求-响应周期中间,则允许消息丢失,可以提高性能。
某些消息即使丢失,也不会对应用造成太大影响。例如频繁更新的温湿度监测值,丢失一两个数值,可能不会影响客户端的曲线图渲染。这种情况下,我们可以声明消息是易失的(volatile):
1 2 3 4 |
io.on('connection', function (socket) { // 发送一个易失的消息 socket.volatile.emit('bieber tweet', tweet); }); |
某些情况下,你可能需要在客户端确认(acknowledgement)接收到消息之后,执行一个回调。这种情况下,你可以为send/emit方法提供函数作为最后一个参数。使用emit时,确认操作由消息接收者手工触发并且可以附带数据:
1 2 3 4 5 6 7 8 9 |
<script> var socket = io(); // 不带参数的调用io()可以进行自动服务发现 socket.on('connect', function () { // 你可以监听具体事件,也可以监听connect // 像服务器发送一个消息,消息确认后,执行回调 socket.emit('ferret', 'tobi', function (data) { console.log(data); // 打印服务器返回的确认消息 }); }); </script> |
1 2 3 4 5 6 7 |
io.on( 'connection', function ( socket ) { // 服务器,接收客户端emit来的消息后,可以手工的进行确认 socket.on( 'ferret', function ( msg, fn ) { // 调用第二个参数即可确认,其入参是返回给客户端的确认消息 fn( 'woot' ); } ); } ); |
所谓广播,是指像所有Socket发送消息,除了广播消息的这个Socket:
1 2 3 4 |
io.on( 'connection', function ( socket ) { // 广播一个消息 socket.broadcast.emit( 'user connected' ); } ); |
如果你仅仅想使用WebSocket语义,只需要调用send方法,然后监听message事件:
1 2 3 4 5 6 7 |
io.on( 'connection', function ( socket ) { // 接收到客户端消息时: socket.on( 'message', function ( msg ) { } ); socket.on( 'disconnect', function () { } ); } ); |
1 2 3 4 5 6 7 8 9 10 |
<script> var socket = io( 'http://localhost/' ); socket.on( 'connect', function () { // 发送一个消息 socket.send( 'hi' ); // 接收到服务器消息时: socket.on( 'message', function ( msg ) { } ); } ); </script> |
当利用多个Node.js进程或机器来进行负载均衡时,你需要会话关联性(session affinity)。即,关联到特定Session ID的请求需要发送给最初产生会话的那个Node.js进程。
上述要求的原因是,某些Socket.io传输机制——XHR轮询、JSONP轮询——依赖于在Socket的生命周期内发起的多个HTTP请求。WebSocket传输则不存在这个问题,因为整个会话中它只会使用一个TCP长连接。
使用NginX可以很容易的实现会话关联性。如果使用Node.js的Cluster模块,可以结合sticky session。
某些情况下,你可能需要在跨越多个Node.js进程向特定名字空间、房间发送消息,或者广播消息。你可以使用socket.io-redis、socket.io-emitter之类的模块。
socket.io-redis是一个适配器,可以实现跨越多个进程来路由消息。使用它来结合Redis,你可以在多个进程中运行多个Socket.io实例,这些实例之间可以正常收发消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var io = require( 'socket.io' )( 3000 ); var redis = require( 'socket.io-redis' ); /** * 选项: * key 发布/订阅事件的前缀,默认socket.io * host Redis所在主机 * port Redis监听端口 * subEvent 可选,需要订阅的Redis客户端事件名,默认messageBuffer * * pubClient 可选,用于发布事件的Redis客户端 * subClient 可选,用于订阅事件的Redis客户端 * 如果提供上面两者之一,必须使用node_redis或者相同API的客户端 * * requestsTimeout 可选,适配器等待响应的超时时间,默认1000ms * withChannelMultiplexing 可选,是否启用频道复用,默认true * */ io.adapter( redis( { host: 'localhost', port: 6379 } ) ); |
在v1.0之前,Socket.io直接把调试信息打印在console,当前版本则基于debug模块打印调试日志。
要查看服务器端日志,以 DEBUG=* node app.js 启动应用。要查看浏览器日志,使用: localStorage.debug = '*';
Socket.io本身不支持对所有事件进行统一的处理,但是你可以通过中间件socketio-wildcard满足这一需求:
1 |
npm install --save socketio-wildcard |
服务器代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var io = require( 'socket.io' )(); var middleware = require( 'socketio-wildcard' )(); // 启用中间件 io.use( middleware ); io.on( 'connection', function ( socket ) { // 监听通配符事件 socket.on( '*', function ( packet ) { packet.data === [ 'foo', 'bar', 'baz' ] } ); } ); io.listen( 3000 ); |
客户端代码示例:
1 2 3 4 5 6 7 |
var io = require( 'socket.io-client' ); var socket = io( 'http://localhost' ); var patch = require( 'socketio-wildcard' )( io.Manager ); patch( socket ); // 监听通配符事件 socket.on( '*', function () { } ); |
该函数由 require('socket.io') 暴露:
调用/属性/方法/事件 | 说明 | ||
( ) | 创建一个服务器,可以使用new:
|
||
( opts ) | 创建一个服务器,opts为选项: serveClient 调用serveClient()方法 path 调用path()方法 |
||
( http, opts ) | 创建一个服务器,http为Node.js的HTTP服务器 | ||
( port, opts ) | 创建一个服务器,在指定端口自动创建Node.js的HTTP服务器 | ||
serveClient( Bool ):Server | 如果为true,则关联的HTTP服务器负责给客户端服务静态文件,默认true | ||
path( String ):Server | 在什么路径下为客户端服务静态文件,默认 /socket.io | ||
adapter( Adapter ):Server |
设置适配器,适配器用于实现消息存储,参数为Adapter实例。默认使用的适配器是基于内存的 socket.io-adapter 如果不指定入参,则返回当前使用的适配器 |
||
origins( String ):Server | 设置允许的客户端 Orgin头,如果不指定入参,则返回当前值 | ||
sockets:Namespace | 返回默认 / 名字空间对象 | ||
attach( http, opts ):Server attach( port, opts ):Server |
附着到一个engine.io实例,返回得到的Server对象 该函数的别名是listen |
||
of( String ):Namespace | 根据路径,初始化或者取得一个名字空间对象 | ||
emit() | 发送一个消息到所有客户端 | ||
use() | 注册中间件 |
该类型代表连接到特定Scope的客户端Socket的池,以路径标识。
属性/方法/事件 | 说明 | ||
⚡ connection ⚡ connect |
当一个客户端连接到此名字空间时触发,回调函数的入参时客户端Socket对象 | ||
name:String | 返回标识符 | ||
connected:Object | 连接到此名字空间的Sockets对象的Hash,键为Socket的ID | ||
use( Function ):Namespace | 注册一个中间件,所谓中间件就是一个函数,它会针对每一个接入的Socket调用:
|
与客户端通信的基础对象。Socket属于特定的名字空间,在底层使用Client对象进行通信。
属性/方法/事件 | 说明 |
rooms:Array | 套接字加入的房间列表 |
client:Client | 底层的Client对象 |
conn:Socket | 底层Client对象使用的连接对象 |
request:Request | 产生此Socket的HTTP请求对象,用于访问Cookie、User-Agent等 |
id:String | Socket会话的ID,来自底层Client对象 |
emit(name[, …]):Socket | 发布一个名为name的事件,后么可以跟着事件的内容。任何数据结构都被支持,包括Buffer。注意不能发布函数,因为其不可串行化 |
join(name[, fn]):Socket |
加入到一个房间中,fn(err)为可选的回调。Socket会自动加入到以其ID命名的房间 加入房间的机制由Adapter实现 |
leave(name[, fn]):Socket |
从房间中移除一个Socket,fn(err)为可选的回调 当断开连接时,Socket自动离开所有房间 |
to(room):Socket | 让后续的事件发布仅仅在room范围内广播 |
in(room):Socket |
socket.io-client-java提供了全特性支持的Java的Socket.io客户端。
1 2 3 4 5 6 7 |
<dependencies> <dependency> <groupId>io.socket</groupId> <artifactId>socket.io-client</artifactId> <version>0.8.3</version> </dependency> </dependencies> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// 建立一个客户端套接字对象 IO.Options opts = new IO.Options(); opts.forceNew = true; // 当查询参数改变后,是否丢弃既有套接字 opts.reconnection = false; // opts.query = "auth_token=" + authToken; // 指定查询参数 socket = IO.socket( "http://localhost", opts ); // 监听连接事件 socket.on( Socket.EVENT_CONNECT, new Emitter.Listener() { public void call( Object... args ) { socket.emit( "foo", "hi" ); // 发送消息 socket.disconnect(); } } ).on( "event", new Emitter.Listener() { // 监听一般性事件 public void call( Object... args ) { // Java客户端使用org.json来进行JSON对象的编码、解码 JSONObject obj = (JSONObject)args[0]; obj = new JSONObject(); obj.put("hello", "server"); obj.put("binary", new byte[42]); socket.emit("foo", obj); } } ).on( Socket.EVENT_DISCONNECT, new Emitter.Listener() { // 监听断开事件 public void call( Object... args ) { } } ); // 发起连接 socket.connect(); // 在服务器确认收到消息后,执行回调 socket.emit("foo", "woot", new Ack() { @Override public void call(Object... args) {} }); // 在收到服务器消息后,予以确认 socket.on("foo", new Emitter.Listener() { @Override public void call(Object... args) { Ack ack = (Ack) args[args.length - 1]; ack.call(); } }); |
可以使用SIOSocket
参考:Socket.IO with Apache Cordova
如果服务器的开发平台不是Node.js,你就需要第三方的Socket.io实现了。注意:单纯的WebSocket服务器不能和Socket.io客户端正常通信。
这是基于Netty网络框架,在Java开发环境下实现Socket.io服务器的开源项目。
1 2 3 4 5 |
<dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.7</version> </dependency> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// 服务器配置,主机名和端口 Configuration config = new Configuration(); config.setHostname( "localhost" ); config.setPort( 9092 ); // SocketIO服务器对象 final SocketIOServer server = new SocketIOServer( config ); // 监听新的客户端连接 server.addConnectListener(new ConnectListener() { @Override public void onConnect(SocketIOClient client) { // 参数为新的客户端 } }); // 监听客户端断开 server.addDisconnectListener( new DisconnectListener() { @Override public void onDisconnect( SocketIOClient socketIOClient ) { // 参数为断开的客户端 } } ); // 监听一个SocketIO消息事件,把JSON反串行化为ChatObject这个Java类型 server.addEventListener( "chatevent", ChatObject.class, new DataListener<ChatObject>() { @Override public void onData( SocketIOClient client, ChatObject data, AckRequest ackRequest ) { // 广播一个事件到所有客户端 server.getBroadcastOperations().sendEvent( "chatevent", data ); } } ); // 启动服务器 server.start(); Thread.sleep( Integer.MAX_VALUE ); // 停止服务器 server.stop(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
server.addEventListener( "ackevent1", ChatObject.class, new DataListener<ChatObject>() { @Override public void onData( final SocketIOClient client, ChatObject data, final AckRequest ackRequest ) { // 检查客户端是否要求消息确认,可选 if ( ackRequest.isAckRequested() ) { // 发送确认消息给客户端 ackRequest.sendAckData( "client message was delivered to server!", "yeah!" ); } // 发送一个消息给客户端,要求确认 ChatObject ackChatObjectData = new ChatObject( data.getUserName(), "message with ack data" ); client.sendEvent( "ackevent2", new AckCallback<String>( String.class ) { @Override public void onSuccess( String result ) { // 此回调执行时,客户端已经确认,可以检查确认结果 System.out.println( "ack from client: " + client.getSessionId() + " data: " + result ); } }, ackChatObjectData ); // 发送一个消息给客户端,要求确认,但是不关心确认后返回的消息 ChatObject ackChatObjectData1 = new ChatObject( data.getUserName(), "message with void ack" ); client.sendEvent( "ackevent3", new VoidAckCallback() { protected void onSuccess() { System.out.println( "void ack from: " + client.getSessionId() ); } }, ackChatObjectData1 ); } } ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Configuration config = new Configuration(); config.setHostname("localhost"); config.setPort(9092); // 限制WebSocket帧最大长度 config.setMaxFramePayloadLength(1024 * 1024); // 限制HTTP报文最大长度 config.setMaxHttpContentLength(1024 * 1024); final SocketIOServer server = new SocketIOServer(config); // 把二进制消息转换为数组 server.addEventListener("msg", byte[].class, new DataListener<byte[]>() { @Override public void onData(SocketIOClient client, byte[] data, AckRequest ackRequest) { client.sendEvent("msg", data); } }); |
1 2 3 4 5 6 7 8 9 |
final SocketIOServer server = new SocketIOServer( config ); // 创建一个新的名字空间 final SocketIONamespace chat1namespace = server.addNamespace( "/chat1" ); // 在此名字空间上监听 chat1namespace.addEventListener( "message", ChatObject.class, new DataListener<ChatObject>() { @Override public void onData( SocketIOClient client, ChatObject data, AckRequest ackRequest ) { } } ); |
1 2 3 4 5 6 7 8 9 10 |
Configuration config = new Configuration(); config.setHostname( "localhost" ); config.setPort( 10443 ); config.setKeyStorePassword( "test1234" ); InputStream stream = SslChatLauncher.class.getResourceAsStream( "/keystore.jks" ); // 设置密钥存储库 config.setKeyStore( stream ); final SocketIOServer server = new SocketIOServer( config ); |
文章写得很全面,想知道博主花了多久学习整理的?
是通过什么方式来学的这么详细的?
主要是官方文档,边看边练习,记下备忘:D
hh,很佩服认真研究技术,又能记录下来分享的人,而且前后端、乃至多媒体都有实践,厉害!
可以加个微信交个朋友吗,我的:*********
过几天,我删掉这条留言,防止被爬,哈哈哈