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

posted on 2018-09-05 10:25  silyvin  阅读(2076)  评论(0编辑  收藏  举报