基于netty的websocket开发小结
WebSocket是html5规范新引入的功能,用于解决浏览器与后台服务器双向通讯的问题,使用WebSocket技术,后台可以随时向前端推送消息,以保证前后台状态统一,在传统的无状态HTTP协议中,这是“无法做到”的。
WebSocket提出之前,为了解决后台推送消息到前台的需求,提出了一些解决方案,这些方案使用已有的技术(如ajax,iframe,flashplayer,java applet ...),通过一些变通的处理来实现。
webSocket是html5新引入的技术,允许后台随时向前端发送文本或者二进制消息,WebSocket是一种全新的协议,不属于http无状态协议,协议名为"ws",这意味着一个websocket连接地址会是这样的写法:
ws://127.0.0.1:8080/websocket。ws不是http,所以传统的web服务器不一定支持,需要服务器与浏览器同时支持, WebSocket才能正常运行,目前的支持还不普遍,需要特别的web服务器和现代的浏览器。
浏览器对WebSocket的支持
Google Chrome浏览器最先支持WebSocket,随后是Safari,Firefox,此外最新版本的Opera和IE(Opera11,IE10)也支持WebSocket。
客户端WebSocket的主要方法
1 构造函数
- var websocket = new WebSocket("ws://127.0.0.1:8080/websocket");
- var websocket = new WebSocket("ws://127.0.0.1:8080/websocket");
2 事件open/message/close/error
WebSocket#onopen, onmessage, onclose, onerror
连接打开时,回调onopen方法,接收到后台消息时会触发到onmessage事件,后台关闭时调用onclose,出现连接异常时可在onerror中捕获
发送消息 - WebSocket#send(data)
关闭连接 - WebSocket#close(optional code, optional reason)
下面是一个简单的html5连接websocket服务器测试页
- <!DOCTYPE html>
- <html>
- <head>
- <script type="text/javascript" charset="utf-8" >
- windowwindow.WebSocket = window.WebSocket || window.MozWebSocket;
- if (!window.WebSocket){
- alert("WebSocket not supported by this browser");
- return;
- };
- var websocket = new WebSocket("ws://127.0.0.1:8080/websocket");
- websocket.onmessage = function(evt){
- var data = evt.data;
- alert("received message: " + data);
- }
- function send() {
- var name = document.querySelector("input[namename=name]").value;
- alert("websocket send message:"+name);
- websocket.send(name);
- }
- </script>
- </head>
- <body>
- <label for="name">What’s your name:</label>
- <input type="text" id="name" name="name" />
- <button onclick="send()">Send</button>
- <div id="message" style="color:red"></div>
- </body>
- </html>
- <!DOCTYPE html>
- <html>
- <head>
- <script type="text/javascript" charset="utf-8" >
- windowwindow.WebSocket = window.WebSocket || window.MozWebSocket;
- if (!window.WebSocket){
- alert("WebSocket not supported by this browser");
- return;
- };
- var websocket = new WebSocket("ws://127.0.0.1:8080/websocket");
- websocket.onmessage = function(evt){
- var data = evt.data;
- alert("received message: " + data);
- }
- function send() {
- var name = document.querySelector("input[namename=name]").value;
- alert("websocket send message:"+name);
- websocket.send(name);
- }
- </script>
- </head>
- <body>
- <label for="name">What’s your name:</label>
- <input type="text" id="name" name="name" />
- <button onclick="send()">Send</button>
- <div id="message" style="color:red"></div>
- </body>
- </html>
服务器对WebSocket的支持
WebSocket不同于http协议,传统的web服务器通常不支持WebSocket.而websocket服务器端的开发大概有借助 Socket.IO开发、NodeJS和Socket.IO联合开发以及借助netty框架开发等等。这里只对netty框架下的websocket开发 进行一个简单的说明!
websocket的协议比较简单, 客户端和普通的浏览器一样通过80或者443端口和服务器进行请求握手,服务器根据http header识别是否一个websocket请求,如果是,则将请求升级为一个websocket连接,握手成功后就进入双向长连接的数据传输阶段. websocket的数据传输是基于帧的方式: 0x00 表示数据开始, 0xff表示数据结束,数据以utf-8编码.
简单的说,第一次请求客户端发送的是http请求,请求头中包含websocket相关的信息,服务器端对请求进行验证:
- private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
- // Allow only GET methods.
- if (req.getMethod() != GET) {
- sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
- return;
- }
- // Send the demo page and favicon.ico
- if (req.getUri().equals("/")) {
- HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK);
- ChannelBuffer content = WebSocketServerIndexPage.getContent(getWebSocketLocation(req));
- res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
- setContentLength(res, content.readableBytes());
- res.setContent(content);
- sendHttpResponse(ctx, req, res);
- return;
- } else if (req.getUri().equals("/favicon.ico")) {
- HttpResponse res = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND);
- sendHttpResponse(ctx, req, res);
- return;
- }
- // Handshake
- WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
- this.getWebSocketLocation(req), null, false);
- this.handshaker = wsFactory.newHandshaker(req);
- if (this.handshaker == null) {
- wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel());
- } else {
- this.handshaker.handshake(ctx.getChannel(), req);
- System.out.println(WebSocketServer.recipients.size());
- WebSocketServer.recipients.add(ctx.getChannel());
- System.out.println(WebSocketServer.recipients.size());
- System.out.println(ctx.getChannel().getId());
- }
- }
- private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
- // Allow only GET methods.
- if (req.getMethod() != GET) {
- sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
- return;
- }
- // Send the demo page and favicon.ico
- if (req.getUri().equals("/")) {
- HttpResponse res = new DefaultHttpResponse(HTTP_1_1, OK);
- ChannelBuffer content = WebSocketServerIndexPage.getContent(getWebSocketLocation(req));
- res.setHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
- setContentLength(res, content.readableBytes());
- res.setContent(content);
- sendHttpResponse(ctx, req, res);
- return;
- } else if (req.getUri().equals("/favicon.ico")) {
- HttpResponse res = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND);
- sendHttpResponse(ctx, req, res);
- return;
- }
- // Handshake
- WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
- this.getWebSocketLocation(req), null, false);
- this.handshaker = wsFactory.newHandshaker(req);
- if (this.handshaker == null) {
- wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel());
- } else {
- this.handshaker.handshake(ctx.getChannel(), req);
- System.out.println(WebSocketServer.recipients.size());
- WebSocketServer.recipients.add(ctx.getChannel());
- System.out.println(WebSocketServer.recipients.size());
- System.out.println(ctx.getChannel().getId());
- }
- }
验证成功后,将请求升级为一个websocket连接,之后的通信就进入双向长连接的数据传输阶段。
- private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
- // Check for closing frame
- if (frame instanceof CloseWebSocketFrame) {
- this.handshaker.close(ctx.getChannel(), (CloseWebSocketFrame) frame);
- return;
- } else if (frame instanceof PingWebSocketFrame) {
- ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData()));
- return;
- } else if (!(frame instanceof TextWebSocketFrame)) {
- throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass()
- .getName()));
- }
- // Send the uppercase string back.
- String request = ((TextWebSocketFrame) frame).getText();
- logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), request));
- WebSocketServer.recipients.write(new TextWebSocketFrame(request.toUpperCase()));
- ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase()));
- }
- private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
- // Check for closing frame
- if (frame instanceof CloseWebSocketFrame) {
- this.handshaker.close(ctx.getChannel(), (CloseWebSocketFrame) frame);
- return;
- } else if (frame instanceof PingWebSocketFrame) {
- ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData()));
- return;
- } else if (!(frame instanceof TextWebSocketFrame)) {
- throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass()
- .getName()));
- }
- // Send the uppercase string back.
- String request = ((TextWebSocketFrame) frame).getText();
- logger.debug(String.format("Channel %s received %s", ctx.getChannel().getId(), request));
- WebSocketServer.recipients.write(new TextWebSocketFrame(request.toUpperCase()));
- ctx.getChannel().write(new TextWebSocketFrame(request.toUpperCase()));
- }
一定要注意在messageReceived中要对HttpRequest和WebSocketFrame分别进行处理。
- public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
- Object msg = e.getMessage();
- if (msg instanceof HttpRequest) {
- handleHttpRequest(ctx, (HttpRequest) msg);
- } else if (msg instanceof WebSocketFrame) {
- handleWebSocketFrame(ctx, (WebSocketFrame) msg);
- }
- }
- public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
- Object msg = e.getMessage();
- if (msg instanceof HttpRequest) {
- handleHttpRequest(ctx, (HttpRequest) msg);
- } else if (msg instanceof WebSocketFrame) {
- handleWebSocketFrame(ctx, (WebSocketFrame) msg);
- }
- }
服务入口
- package com.etao.mobile.websocket;
- import java.net.InetSocketAddress;
- import java.util.concurrent.Executors;
- import org.jboss.netty.bootstrap.ServerBootstrap;
- import org.jboss.netty.channel.group.ChannelGroup;
- import org.jboss.netty.channel.group.DefaultChannelGroup;
- import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
- /**
- * A HTTP server which serves Web Socket requests at:
- *
- * http://localhost:8080/websocket
- *
- * Open your browser at http://localhost:8080/, then the demo page will be
- * loaded and a Web Socket connection will be made automatically.
- *
- * This server illustrates support for the different web socket specification
- * versions and will work with:
- *
- * <ul>
- * <li>Safari 5+ (draft-ietf-hybi-thewebsocketprotocol-00)
- * <li>Chrome 6-13 (draft-ietf-hybi-thewebsocketprotocol-00)
- * <li>Chrome 14+ (draft-ietf-hybi-thewebsocketprotocol-10)
- * <li>Chrome 16+ (RFC 6455 aka draft-ietf-hybi-thewebsocketprotocol-17)
- * <li>Firefox 7+ (draft-ietf-hybi-thewebsocketprotocol-10)
- * </ul>
- */
- public class WebSocketServer {
- private final int port;
- public static ChannelGroup recipients = new DefaultChannelGroup();
- public WebSocketServer(int port) {
- this.port = port;
- }
- public void run() {
- // Configure the server.
- ServerBootstrap bootstrap = new ServerBootstrap(
- new NioServerSocketChannelFactory(Executors
- .newCachedThreadPool(), Executors.newCachedThreadPool()));
- // Set up the event pipeline factory.
- bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory());
- // Bind and start to accept incoming connections.
- bootstrap.bind(new InetSocketAddress(port));
- System.out.println("Web socket server started at port " + port + '.');
- System.out
- .println("Open your browser and navigate to http://localhost:"
- + port + '/');
- }
- public static void main(String[] args) {
- int port;
- if (args.length > 0) {
- port = Integer.parseInt(args[0]);
- } else {
- port = 8080;
- }
- new WebSocketServer(port).run();
- }
- }
- package com.etao.mobile.websocket;
- import java.net.InetSocketAddress;
- import java.util.concurrent.Executors;
- import org.jboss.netty.bootstrap.ServerBootstrap;
- import org.jboss.netty.channel.group.ChannelGroup;
- import org.jboss.netty.channel.group.DefaultChannelGroup;
- import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
- /**
- * A HTTP server which serves Web Socket requests at:
- *
- * http://localhost:8080/websocket
- *
- * Open your browser at http://localhost:8080/, then the demo page will be
- * loaded and a Web Socket connection will be made automatically.
- *
- * This server illustrates support for the different web socket specification
- * versions and will work with:
- *
- * <ul>
- * <li>Safari 5+ (draft-ietf-hybi-thewebsocketprotocol-00)
- * <li>Chrome 6-13 (draft-ietf-hybi-thewebsocketprotocol-00)
- * <li>Chrome 14+ (draft-ietf-hybi-thewebsocketprotocol-10)
- * <li>Chrome 16+ (RFC 6455 aka draft-ietf-hybi-thewebsocketprotocol-17)
- * <li>Firefox 7+ (draft-ietf-hybi-thewebsocketprotocol-10)
- * </ul>
- */
- public class WebSocketServer {
- private final int port;
- public static ChannelGroup recipients = new DefaultChannelGroup();
- public WebSocketServer(int port) {
- this.port = port;
- }
- public void run() {
- // Configure the server.
- ServerBootstrap bootstrap = new ServerBootstrap(
- new NioServerSocketChannelFactory(Executors
- .newCachedThreadPool(), Executors.newCachedThreadPool()));
- // Set up the event pipeline factory.
- bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory());
- // Bind and start to accept incoming connections.
- bootstrap.bind(new InetSocketAddress(port));
- System.out.println("Web socket server started at port " + port + '.');
- System.out
- .println("Open your browser and navigate to http://localhost:"
- + port + '/');
- }
- public static void main(String[] args) {
- int port;
- if (args.length > 0) {
- port = Integer.parseInt(args[0]);
- } else {
- port = 8080;
- }
- new WebSocketServer(port).run();
- }
- }
一定要注意的是netty3.0一下的版本是不支持websocket的!我这里用的是3.3.0.Final版本。
可能存在的隐患:不同草案的websocket 握手标准有所不同,因此netty在这方面的检测是否可以满足要求?
我这里也参考了 基于Websocket草案10协议的升级及基于Netty的握手实现,在此对冯立彬的博客表示感谢!