Netty+WebSocket实现简单网页群聊
这两天看了下WebSocket的RFC文档,对WebSocket协议有了基本的认识,顺便写了篇博客做点笔记 WebSocket 协议。 例子说明:每个网页一个websocket连接,点发送消息后,消息会发送给除了自己之外的其它在线的websocket客户端,简单实现群聊
服务端
采用Netty实现,Netty版本是4.1.2.Final.
服务端共有以下4个类:
WebSocketServer实现IHttpService和IWebSocketService,WebSocketServerHandler持有IHttpService和 IWebSocketService的引用,若收到FullHttpRequest则交给IHttpService其处理,若收到WebSocketFrame则交给IWebSocketService去处理。
IHttpService.java
Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package cc.lixiaohui.demo.netty4.websocket; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; /** * @author lixiaohui * @date 2016年9月24日 下午3:58:31 * */ public interface IHttpService {<!-- --> void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request); } |
IWebSocketService.java
Java代码
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | package cc.lixiaohui.demo.netty4.websocket; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.WebSocketFrame; /** * @author lixiaohui * @date 2016年9月24日 下午3:46:07 * */ public interface IWebSocketService {<!-- --> void handleFrame(ChannelHandlerContext ctx, WebSocketFrame frame); } WebSocketServerHandler.java Java代码 package cc.lixiaohui.demo.netty4.websocket; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author lixiaohui * @date 2016年9月24日 下午2:22:33 * */ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {<!-- --> @SuppressWarnings("unused") private static final Logger logger = LoggerFactory.getLogger(WebSocketServerHandler.class); private IWebSocketService websocketService; private IHttpService httpService; public WebSocketServerHandler(IWebSocketService websocketService, IHttpService httpService) {<!-- --> super(); this.websocketService = websocketService; this.httpService = httpService; } /* * @see * io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel * .ChannelHandlerContext, java.lang.Object) */ @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {<!-- --> if (msg instanceof FullHttpRequest) {<!-- --> httpService.handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) {<!-- --> websocketService.handleFrame(ctx, (WebSocketFrame) msg); } } /* * @see io.netty.channel.ChannelInboundHandlerAdapter#channelReadComplete(io.netty.channel.ChannelHandlerContext) */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {<!-- --> ctx.flush(); } } |
WebSocketServer.java
Java代码
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | package cc.lixiaohui.demo.netty4.websocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelId; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.AttributeKey; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author lixiaohui * @date 2016年9月24日 下午2:08:58 * */ public class WebSocketServer implements IWebSocketService, IHttpService {<!-- --> public static void main(String[] args) {<!-- --> new WebSocketServer(9999).start(); } // ----------------------------static fields ----------------------------- private static final Logger logger = LoggerFactory.getLogger(WebSocketServer.class); private static final String HN_HTTP_CODEC = "HN_HTTP_CODEC"; private static final String HN_HTTP_AGGREGATOR = "HN_HTTP_AGGREGATOR"; private static final String HN_HTTP_CHUNK = "HN_HTTP_CHUNK"; private static final String HN_SERVER = "HN_LOGIC"; // handshaker attachment key private static final AttributeKey<WebSocketServerHandshaker> ATTR_HANDSHAKER = AttributeKey.newInstance("ATTR_KEY_CHANNELID"); private static final int MAX_CONTENT_LENGTH = 65536; private static final String WEBSOCKET_UPGRADE = "websocket"; private static final String WEBSOCKET_CONNECTION = "Upgrade"; private static final String WEBSOCKET_URI_ROOT_PATTERN = "ws://%s:%d"; // ------------------------ member fields ----------------------- private String host; // 绑定的地址 private int port; // 绑定的端口 /** * 保存所有WebSocket连接 */ private Map<ChannelId, Channel> channelMap = new ConcurrentHashMap<ChannelId, Channel>(); private final String WEBSOCKET_URI_ROOT; public WebSocketServer(int port) {<!-- --> this("localhost", port); } public WebSocketServer(String host, int port) {<!-- --> this.host = host; this.port = port; WEBSOCKET_URI_ROOT = String.format(WEBSOCKET_URI_ROOT_PATTERN, host, port); } public void start() {<!-- --> EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup); b.channel(NioServerSocketChannel.class); b.childHandler(new ChannelInitializer<Channel>() {<!-- --> @Override protected void initChannel(Channel ch) throws Exception {<!-- --> ChannelPipeline pl = ch.pipeline(); // 保存该Channel的引用 channelMap.put(ch.id(), ch); logger.info("new channel {}", ch); ch.closeFuture().addListener(new ChannelFutureListener() {<!-- --> public void operationComplete(ChannelFuture future) throws Exception {<!-- --> logger.info("channel close {}", future.channel()); // Channel 关闭后不再引用该Channel channelMap.remove(future.channel().id()); } }); pl.addLast(HN_HTTP_CODEC, new HttpServerCodec()); pl.addLast(HN_HTTP_AGGREGATOR, new HttpObjectAggregator(MAX_CONTENT_LENGTH)); pl.addLast(HN_HTTP_CHUNK, new ChunkedWriteHandler()); pl.addLast(HN_SERVER, new WebSocketServerHandler(WebSocketServer.this, WebSocketServer.this)); } }); try {<!-- --> // 绑定端口 ChannelFuture future = b.bind(host, port).addListener(new ChannelFutureListener() {<!-- --> public void operationComplete(ChannelFuture future) throws Exception {<!-- --> if (future.isSuccess()) {<!-- --> logger.info("websocket started."); } } }).sync(); future.channel().closeFuture().addListener(new ChannelFutureListener() {<!-- --> public void operationComplete(ChannelFuture future) throws Exception {<!-- --> logger.info("server channel {} closed.", future.channel()); } }).sync(); } catch (InterruptedException e) {<!-- --> logger.error(e.toString()); } finally {<!-- --> bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } logger.info("websocket server shutdown"); } /* * @see cc.lixiaohui.demo.netty4.websocket.IHttpService#handleHttpRequest(io.netty.handler.codec.http.FullHttpRequest) */ public void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {<!-- --> if (isWebSocketUpgrade(req)) {<!-- --> // 该请求是不是websocket upgrade请求 logger.info("upgrade to websocket protocol"); String subProtocols = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL); WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(WEBSOCKET_URI_ROOT, subProtocols, false); WebSocketServerHandshaker handshaker = factory.newHandshaker(req); if (handshaker == null) {<!-- -->// 请求头不合法, 导致handshaker没创建成功 WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else {<!-- --> // 响应该请求 handshaker.handshake(ctx.channel(), req); // 把handshaker 绑定给Channel, 以便后面关闭连接用 ctx.channel().attr(ATTR_HANDSHAKER).set(handshaker);// attach handshaker to this channel } return; } // TODO 忽略普通http请求 logger.info("ignoring normal http request"); } /* * @see * cc.lixiaohui.demo.netty4.websocket.IWebSocketService#handleFrame(io.netty * .channel.Channel, io.netty.handler.codec.http.websocketx.WebSocketFrame) */ public void handleFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {<!-- --> // text frame if (frame instanceof TextWebSocketFrame) {<!-- --> String text = ((TextWebSocketFrame) frame).text(); TextWebSocketFrame rspFrame = new TextWebSocketFrame(text); logger.info("recieve TextWebSocketFrame from channel {}", ctx.channel()); // 发给其他所有channel for (Channel ch : channelMap.values()) {<!-- --> if (ctx.channel().equals(ch)) {<!-- --> continue; } ch.writeAndFlush(rspFrame); logger.info("write text[{}] to channel {}", text, ch); } return; } // ping frame, 回复pong frame即可 if (frame instanceof PingWebSocketFrame) {<!-- --> logger.info("recieve PingWebSocketFrame from channel {}", ctx.channel()); ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain())); return; } if (frame instanceof PongWebSocketFrame) {<!-- --> logger.info("recieve PongWebSocketFrame from channel {}", ctx.channel()); return; } // close frame, if (frame instanceof CloseWebSocketFrame) {<!-- --> logger.info("recieve CloseWebSocketFrame from channel {}", ctx.channel()); WebSocketServerHandshaker handshaker = ctx.channel().attr(ATTR_HANDSHAKER).get(); if (handshaker == null) {<!-- --> logger.error("channel {} have no HandShaker", ctx.channel()); return; } handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 剩下的是binary frame, 忽略 logger.warn("unhandle binary frame from channel {}", ctx.channel()); } //三者与:1.GET? 2.Upgrade头 包含websocket字符串? 3.Connection头 包含 Upgrade字符串? private boolean isWebSocketUpgrade(FullHttpRequest req) {<!-- --> HttpHeaders headers = req.headers(); return req.method().equals(HttpMethod.GET) && headers.get(HttpHeaderNames.UPGRADE).contains(WEBSOCKET_UPGRADE) && headers.get(HttpHeaderNames.CONNECTION).contains(WEBSOCKET_CONNECTION); } } |
客户端
客户端采用浏览器,代码:
Js代码
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> </head> </head> <script type="text/javascript"> var socket; if(!window.WebSocket){<!-- --> window.WebSocket = window.MozWebSocket; } if(window.WebSocket){<!-- --> socket = new WebSocket("ws://localhost:9999"); socket.onmessage = function(event){<!-- --> appendln("接收:" + event.data); }; socket.onopen = function(event){<!-- --> appendln("WebSocket 连接已建立"); }; socket.onclose = function(event){<!-- --> appendln("WebSocket 连接已关闭"); }; }else{<!-- --> alert("浏览器不支持WebSocket协议"); } function send(message){<!-- --> if(!window.WebSocket){<!-- -->return;} if(socket.readyState == WebSocket.OPEN){<!-- --> socket.send(message); appendln("发送:" + message); }else{<!-- --> alert("WebSocket连接建立失败"); } } function appendln(text) {<!-- --> var ta = document.getElementById('responseText'); ta.value += text + "\r\n"; } function clear() {<!-- --> var ta = document.getElementById('responseText'); ta.value = ""; } </script> <body> <form onSubmit="return false;"> <input type = "text" name="message" value="你好啊"/> <br/><br/> <input type="button" value="发送 WebSocket 请求消息" onClick="send(this.form.message.value)"/> <hr/> <h3>服务端返回的应答消息</h3> <textarea id="responseText" style="width: 800px;height: 300px;"></textarea> </form> </body> </html> |
测试
打开客户端,可以看到能接收到其他客户端发的消息
