场景

需求: 由于公司是做在线教育的,客户的定制化需求,要求同一个账号同时只能观看一个视频。(BS)

分析: 刚开始想过监听浏览器的close()事件,打开视频向redis 中存一个status,关闭浏览器修改这个status。但是不能处理极端情况如: 强制杀死进程、断电等(不考虑缓存播放视频的情况)

方案: 想到socket,自然想到netty对socket 的支持非常好。

为什么选择netty?

1. API使用简单,开发门槛低
2. 功能强大,预置了多种编解码器功能
3. 几行代码,就能解决粘包\拆包问题
4. 成熟、稳定 Netty修复了所有JDK NIO BUG

代码示例: 基于spring 框架整合的Netty

<!--netty的依赖集合,都整合在一个依赖里面了-->
<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.6.Final</version>
</dependency>

netty server 启动类

@Service
public class MyWebSocketServer {

	private static final Logger logger = LoggerFactory.getLogger(MyWebSocketServer.class);

	@Autowired
	private PropertiesUtil propertiesUtil;

	// 在对象加载完依赖注入后执行
	@PostConstruct
	public void init() {
	
		// 创建socket服务
		new Thread(new Runnable() {
			@Override
			public void run() {
				logger.info("正在启动websocket服务器....");
				NioEventLoopGroup boss = new NioEventLoopGroup();
				NioEventLoopGroup work = new NioEventLoopGroup();
				try {
					ServerBootstrap bootstrap = new ServerBootstrap();
					bootstrap.group(boss, work);
					bootstrap.channel(NioServerSocketChannel.class);
					bootstrap.childHandler(new ChannelInitializer() {
						@Override
						protected void initChannel(Channel ch) {

							// 设置log监听器,并且日志级别为debug,方便观察运行流程
							ch.pipeline().addLast("logging", new LoggingHandler("DEBUG"));
							// 设置websocket解码器,将请求、响应消息解码为HTTP
							ch.pipeline().addLast("http-codec", new HttpServerCodec());
							// HTTP聚合器,使用websocket会用到
							ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
							// 支持浏览器和服务器进行websocket通信
							ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
							// TODO 实现心跳检测需要在服务端或客户端加入下面的IdleStateHandler,指定读写的超时时间,
							// 超时后,触发继承ChannelInboundHandlerAdapter自定义的handler中userEventTriggered方法.
							ch.pipeline().addLast(new IdleStateHandler(20L, 0L, 0L, TimeUnit.SECONDS));
							// 自定义的心跳服务处理
							ch.pipeline().addLast(new HeartBeatServerHandler());
							// 自定义的业务handler
							ch.pipeline().addLast("handler", new MyWebSocketServiceHandler());
						}
					});
					Channel channel = bootstrap.bind(9091).sync().channel();
					logger.info("webSocket服务器启动成功:" + channel);
					channel.closeFuture().sync();
				} catch (InterruptedException e) {
					e.printStackTrace();
					logger.info("运行出错:" + e);
				} finally {
					boss.shutdownGracefully();
					work.shutdownGracefully();
					logger.info("websocket服务器已关闭");
				}
			}
		}).start();
	}

}

1. 使用@PostConstruct这样随着spring项目启动,加载完这个类依赖的bean后,去执行init()方法创建netty服务端。
2. 为什么要创建一个线程?因为sync().channel() 是阻塞的,试想一下如果不这样做,spring 初始化到这段代码后,将阻塞在此处,spring 初始化止于此,和跑一个main() 没区别.
3. 重点在26~42行,initChannel(),其中最后两个 handler是自定义的。分别是处理心跳检测Handler,和业务

心跳检测Handler:
Netty中自带了一个IdleStateHandler 可以用来实现心跳检测。 这里会监听到读/写超时的客户端

public class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {

	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

		if (evt instanceof IdleStateEvent) {
			IdleStateEvent event = (IdleStateEvent) evt;
			String eventType = null;
			switch (event.state()) {
				case READER_IDLE:
					eventType = "读空闲";
					break;
				case WRITER_IDLE:
					eventType = "写空闲";
					break;
				case ALL_IDLE:
					eventType = "读写空闲";
					break;
			}
			System.out.println(ctx.channel().remoteAddress() + "超时事件:" + eventType);
			ctx.channel().close();
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		ctx.close();
	}

}

处理业务的Handler:

public class MyWebSocketServiceHandler extends SimpleChannelInboundHandler<Object> {

	private static final Logger logger = LoggerFactory.getLogger(StudyRecordWebSocketHandler.class);
	
	private WebSocketServerHandshaker handshaker;

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
		if (msg instanceof FullHttpRequest){
			// 第一次建立连接,以http请求形式接入
			logger.info("创建连接");
			handleHttpRequest(ctx, (FullHttpRequest) msg);
		}else if (msg instanceof WebSocketFrame){
			// 处理websocket客户端的消息
			handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
		}
	}


	@Override
	public void channelActive(ChannelHandlerContext ctx) {
		logger.info(" 客户端加入连接:"+ctx.channel());
	}

	//断开连接
	@Override
	@SuppressWarnings("all")
	public void channelInactive(ChannelHandlerContext ctx) {
		logger.info("客户端断开连接:"+ctx.channel());
	}

	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) {
		ctx.flush();
	}

	private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame){

		// 判断是否关闭链路的指令
		if (frame instanceof CloseWebSocketFrame) {
			handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
			return;
		}
		// 判断是否ping消息
		if (frame instanceof PingWebSocketFrame) {
			ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
			return;
		}
		// 仅支持文本消息,不支持二进制消息
		if (!(frame instanceof TextWebSocketFrame)) {
			logger.info("本例程仅支持文本消息,不支持二进制消息");
			throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
		}

		// 接收数据
		String requestMsg = ((TextWebSocketFrame) frame).text();
		// TODO 处理消息 .....
	}


	/**
	 * 唯一的一次http请求,用于创建websocket
	 */
	private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
		// 要求Upgrade为websocket,HTTP解析失败,返回HTTP异常
		if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
			//若不是websocket方式,则创建BAD_REQUEST的req,返回给客户端
			sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
			return;
		}

		// 构造握手响应返回
		WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:9091/websocket", null, false);
		handshaker = wsFactory.newHandshaker(req);
		if (handshaker == null) {
			WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
		} else {
			handshaker.handshake(ctx.channel(), req);
		}
	}

	/**
	 * 拒绝不合法的请求,并返回错误信息
	 */
	private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
		// 返回应答给客户端
		if (res.status().code() != 200) {
			ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
			res.content().writeBytes(buf);
			buf.release();
		}
		ChannelFuture f = ctx.channel().writeAndFlush(res);
		// 如果是非Keep-Alive,关闭连接
		if (!isKeepAlive(req) || res.status().code() != 200) {
			f.addListener(ChannelFutureListener.CLOSE);
		}
	}


	// 异常情况关闭客户端
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
}