netty(六)WebSocket实践
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // http 编解码 pipeline.addLast(new HttpServerCodec()); // 大对象出站 // pipeline.addLast(new ChunkedWriteHandler()); // 捕捉出站异常 pipeline.addLast(new OutBoundExceptionHandler()); // 聚合 pipeline.addLast(new HttpObjectAggregator(64 * 1024)); // 初始http pipeline.addLast(new HttpRequestHandler()); // netty 代理 握手,处理 close ping pong pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); // string -> websocketframe pipeline.addLast(new WebSocketFrameEncoder()); // websocketframe -> string pipeline.addLast(new WebSocketFrameDecoder()); // 读空闲9秒激发 ch.pipeline().addLast(new IdleStateHandler(9, 0, 0, TimeUnit.SECONDS)); // 自定义 websocket 处理 pipeline.addLast(new TextWebSocketFrameHandler()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); super.exceptionCaught(ctx, cause); } }
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> { public static AttributeKey<String> key = AttributeKey.valueOf("userName"); @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { String url = request.getUri(); if(-1 != url.indexOf("/ws")) { String temp [] = url.split(";"); String name = URLDecoder.decode(temp[1], "UTF-8"); ctx.channel().attr(key).set(name); // 传递到下一个handler:升级握手 ctx.fireChannelRead(request.retain()); } else { System.out.println("not socket"); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
public class WebSocketFrameDecoder extends MessageToMessageDecoder<WebSocketFrame> { @Override protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception { if(frame instanceof TextWebSocketFrame) { TextWebSocketFrame tframe = (TextWebSocketFrame)frame; // 内存泄漏 // out.add(tframe.retain().text()); // 虽然 frame 会自动释放,且往下传递的是String对象,所以让MessgeToMessageDecoder自动释放掉fram out.add(tframe.text()); } else { // 不必要,但这种情况下release无害 // frame.release(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } }
public class WebSocketFrameEncoder extends MessageToMessageEncoder<String> { @Override protected void encode(ChannelHandlerContext ctx, String msg, List<Object> out) throws Exception { // MessageToMessageEncoder 会自动释放msg,if msg instanceof ReferenceCounted // out.add(new TextWebSocketFrame(msg).retain()); // 往下一个outHandler传递,不用+1年龄 out.add(new TextWebSocketFrame(msg)); } }
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<String> { private static final String SPLIT = ":\t"; private static final ChannelGroup group = new DefaultChannelGroup("ChannelGroups", GlobalEventExecutor.INSTANCE); private static final ConcurrentHashMap<String, Channel> userChannel = new ConcurrentHashMap<String, Channel>(); private static String getOnine() { StringBuilder stringBuilder = new StringBuilder(group.size() * 20); String online = "[在线用户]"; stringBuilder.append(online); for(Channel channel : group) { String name = channel.attr(HttpRequestHandler.key).get(); stringBuilder.append(" " + name); } return stringBuilder.toString(); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) { // 移除性能更加 ctx.pipeline().remove(HttpRequestHandler.class); String userName = ctx.channel().attr(HttpRequestHandler.key).get(); Channel old = userChannel.put(userName, ctx.channel()); if(old != null) { group.remove(old); System.out.println(old.attr(HttpRequestHandler.key).get() + SPLIT + "[切换设备]"); ChannelFuture channelFuture = old.writeAndFlush("您的账户在其它地方登陆"); channelFuture.addListener(ChannelFutureListener.CLOSE); } ctx.writeAndFlush("-=====登录成功=====-"); String up = userName + SPLIT + "[上线]"; System.out.println(up); group.writeAndFlush(up); group.add(ctx.channel()); String online = getOnine(); System.out.println(online); group.writeAndFlush(online); }else if (evt instanceof IdleStateEvent) { // 2*4+1 s内读空闲时,关掉连接,表示客户端不可见了 IdleStateEvent evnet = (IdleStateEvent) evt; if (evnet.state().equals(IdleState.READER_IDLE)) { ctx.close(); } } else { super.userEventTriggered(ctx, evt); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { String userName = ctx.channel().attr(HttpRequestHandler.key).get(); userChannel.remove(userName); String down = userName + SPLIT + "[下线]"; System.out.println(down); group.writeAndFlush(down); group.remove(ctx.channel()); String online = getOnine(); System.out.println(online); group.writeAndFlush(online); super.channelInactive(ctx); } @Override public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { // 这一段也是错的,书上因为是直接write,因此要retain,这里已经新建了String实例,所以retain会造成内存泄漏 // String send = ctx.channel().attr(HttpRequestHandler.key).get() + SPLIT + msg.retain().text(); // System.out.println(send); // group.writeAndFlush(new TextWebSocketFrame(send)); if("HeartBeat".equals(msg)) { // 心跳只给自己发 ctx.writeAndFlush(msg); } else { // 聊天发给所有人 String send = ctx.channel().attr(HttpRequestHandler.key).get() + SPLIT + msg; System.out.println(send); group.writeAndFlush(send); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
public class OutBoundExceptionHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { super.write(ctx, msg, promise); promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { if(!channelFuture.isSuccess()) { channelFuture.cause().printStackTrace(); channelFuture.channel().close(); } } }); } }
public class WebSocketServerJob implements Runnable { static final EventLoopGroup CLIENT_BOSS_LOOP_GROUP = new NioEventLoopGroup(4); static final EventLoopGroup CLIENT_WORKER_LOOP_GROUP = new NioEventLoopGroup(4); public static void main(String[] args) throws Exception { new Thread(new WebSocketServerJob()).start(); } @Override public void run() { startHttpServer(8990); } public static void startHttpServer(int port) { try { ServerBootstrap bs = new ServerBootstrap(); bs.group(CLIENT_BOSS_LOOP_GROUP, CLIENT_WORKER_LOOP_GROUP); bs.channel(NioServerSocketChannel.class); bs.childHandler(new WebSocketServerInitializer()); ChannelFuture future = bs.bind(port).sync(); System.out.println("web socket server start at " + port); future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { CLIENT_BOSS_LOOP_GROUP.shutdownGracefully(); CLIENT_WORKER_LOOP_GROUP.shutdownGracefully(); } } }
前端:http://localhost:8080/wechat-demo/websocket.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>websocket demo</title> <script src="jquery-1.11.3.js"></script> <script type="text/javascript"> var urlRoad = 'ws://192.168.201.227:8990/ws;' ; var ws = null; function connect () { if(ws != null) return; ws = new WebSocket(urlRoad + $("#fname").val()); ws.onopen = WSonOpen; ws.onmessage = WSonMessage; ws.onclose = WSonClose; ws.onerror = WSonError; } function WSonOpen() { alert("登陆成功,连接已经建立。"); }; function WSonMessage(event) { $("#board").val(event.data + "\n" + $("#board").val()); }; function WSonClose() { ws = null; alert("连接关闭。"); }; function WSonError() { alert("WebSocket错误。"); }; function send() { if(ws == null) { alert("未登录"); return; } var text = $("#tosend").val(); if(text.trim() == '') return; $("#tosend").val(''); ws.send(text) } </script> </head> <body> 用户名: <input type="text" id="fname"><input type="submit" onclick="connect()" value="登陆"> <br><textarea onchange="this.scrollTop=this.scrollHeight" style= "overflow-x:auto" id="board" rows=10 cols=80></textarea> <br><input id="tosend" type="text" style="width:400px"></input><input type="submit" onclick="send()" value="发送"> </body> </html>
1 疑问:pipeline.addLast(new WebSocketServerProtocolHandler(""));这一句中,有实际情况表明有时需要 "/ws" 服务器才能握手
2 这篇文章提供另外一种实现结构:https://www.cnblogs.com/tohxyblog/p/7946498.html
2021.4.5 补充,该文章与
/Users/joyce/work/jds/warn/push-center/push_center/src/main/java/com/jince/push/ws/WebSocketHandler.java
好像一致,都是在运行期执行:handshaker.handshake(ctx.channel(), req);
这里,利用了 netty channel的线程安全性与@Sharable 的结论:运行期pipeline可以随意add、remove,因为每个channel都有一个自己的pipeline,自始至终只会被一个线程执行
/* * 功能:读取 h5页面发送过来的信息 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) {// 如果是HTTP请求,进行HTTP操作 LOGGER.debug("当前为Http请求"); handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) {// 如果是Websocket请求,则进行websocket操作 LOGGER.debug("当前为Websocket请求"); handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } private static final String WSURI = "/ws"; private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { // 如果HTTP解码失败,返回HHTP异常 if (req instanceof HttpRequest) { HttpMethod method = req.method(); // 如果是websocket请求就握手升级 if (method.equals(HttpMethod.GET) && WSURI.equalsIgnoreCase(req.uri())) { LOGGER.debug("req instanceof HttpRequest"); WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(wsFactoryUri, null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } } }
public final ChannelFuture handshake(Channel channel, FullHttpRequest req, HttpHeaders responseHeaders, final ChannelPromise promise) { if(logger.isDebugEnabled()) { logger.debug("{} WebSocket version {} server handshake", channel, this.version()); } FullHttpResponse response = this.newHandshakeResponse(req, responseHeaders); ChannelPipeline p = channel.pipeline(); if(p.get(HttpObjectAggregator.class) != null) { p.remove(HttpObjectAggregator.class); } if(p.get(HttpContentCompressor.class) != null) { p.remove(HttpContentCompressor.class); } ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class); final String encoderName; if(ctx == null) { ctx = p.context(HttpServerCodec.class); if(ctx == null) { promise.setFailure(new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline")); return promise; } p.addBefore(ctx.name(), "wsdecoder", this.newWebsocketDecoder()); p.addBefore(ctx.name(), "wsencoder", this.newWebSocketEncoder()); encoderName = ctx.name(); } else { p.replace(ctx.name(), "wsdecoder", this.newWebsocketDecoder()); encoderName = p.context(HttpResponseEncoder.class).name(); p.addBefore(encoderName, "wsencoder", this.newWebSocketEncoder()); }
当然本文的脉络更清晰
3 ChannelGroup的基础实践参见:https://www.cnblogs.com/silyvin/articles/9413329.html
4 package:链接: https://pan.baidu.com/s/1y7X8Jik7ycgaOmz3mYuYUg 密码: 9xy9
5 实践版本4.0.17.Final,在更高版本中,4.1.17.Final,
FullHttpRequest.getUri与WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE已被置为:@Deprecated,虽然能够继续使用,但需要:
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> { public static AttributeKey<String> key = AttributeKey.valueOf("userName"); @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { String url = request.getUri(); if(-1 != url.indexOf("/ws")) { String temp [] = url.split(";"); String name = URLDecoder.decode(temp[1], "UTF-8"); ctx.channel().attr(key).set(name); // 在更高版本中4.1.17.Final,此句是需要的 // request.setUri("/ws"); // 传递到下一个handler:升级握手 ctx.fireChannelRead(request.retain()); } else { System.out.println("not socket"); ctx.close(); } }
6 (2019.12.5)
每个WebSocket连接都始于一个HTTP请求。具体来说,WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
注意,关键的地方是,这里面有个Upgrade首部,用来把当前的HTTP请求升级到WebSocket协议,这是HTTP协议本身的内容,是为了扩展支持其他的通讯协议。如果服务器支持新的协议,则必须返回101:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
7 2021.5.6
浏览器tab关闭,会发送fin包,netty这边进入channelInActive