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,很佩服认真研究技术,又能记录下来分享的人,而且前后端、乃至多媒体都有实践,厉害!
可以加个微信交个朋友吗,我的:*********
过几天,我删掉这条留言,防止被爬,哈哈哈