Netty 学习笔记——请求处理

Netty Home
Netty GitHub

启动服务

our thread

  1. 创建 selector
  2. 创建 server socket channel
  3. 初始化 server socket channel
  4. 给 server socket channel 从 boss group 中选择一个 NioEventLoop

boss thread

  1. 将 server socket channel 注册到选择的 NioEventLoop 的 selector
  2. 绑定地址启动
  3. 注册接受连接事件(OP_ACCEPT)到 selector 上
Selector selector = sun.nio.ch.SelectorProviderImpl.openSelector()
ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel()
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
javaChannel().bind(localAddress, config.getBacklog());
selectionKey.interestOps(OP_ACCEPT);

知识点

  • Selector 是在 new NioEventLoopGroup()(创建一批 NioEventLoop)时创建。
  • 第一次 Register 并不是监听 OP_ACCEPT,而是 0:
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this)

最终监听 OP_ACCEPT 是通过 bind 完成后的 fireChannelActive() 来触发的。
NioEventLoop 是通过 Register 操作的执行来完成启动的。
类似 ChannelInitializer,一些 Hander 可以设计成一次性的,用完就移除,例如授权。

构建连接

boss thread

  1. NioEventLoop 中的 selector 轮询创建连接事件(OP_ACCEPT):
  2. 创建 socket channel
  3. 初始化 socket channel 并从 worker group 中选择一个 NioEventLoop

worker thread

  1. 将 socket channel 注册到选择的 NioEventLoop 的 selector
  2. 注册读事件(OP_READ)到 selector 上
• SocketChannel socketChannel = serverSocketChannel.accept()
• selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
• selectionKey.interestOps(OP_READ);

知识点

  • 创建连接的初始化和注册是通过 pipeline.fireChannelRead 在 ServerBootstrapAcceptor 中完成的。
  • 第一次 Register 并不是监听 OP_READ ,而是 0 :
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this)
  • 最终监听 OP_READ 是通过“Register”完成后的fireChannelActive (io.netty.channel.AbstractChannel.AbstractUnsafe#register0中)来触发的
  • Worker’s NioEventLoop 是通过 Register 操作执行来启动。
  • 接受连接的读操作,不会尝试读取更多次(16次)

接受数据

  • 多路复用器( Selector )接收到 OP_READ 事件
  • 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read()
  • 分配一个初始 1024 字节的 byte buffer 来接受数据
  • 从 Channel 接受数据到 byte buffer
  • 记录实际接受数据大小,调整下次分配 byte buffer 大小
  • 触发 pipeline.fireChannelRead(byteBuf) 把读取到的数据传播出去
  • 判断接受 byte buffer 是否满载而归:是,尝试继续读取直到没有数据或满 16 次;否,结束本轮读取,等待下次 OP_READ 事件

知识点

  • 读取数据本质:sun.nio.ch.SocketChannelImpl#read(java.nio.ByteBuffer)
  • NioSocketChannel read() 是读数据, NioServerSocketChannel read() 是创建连接
  • pipeline.fireChannelReadComplete(); 一次读事件处理完成
  • pipeline.fireChannelRead(byteBuf); 一次读数据完成,一次读事件处理可能会包含多次读数据操作
  • 为什么最多只尝试读取 16 次?“雨露均沾“
  • AdaptiveRecvByteBufAllocator 对 bytebuf 的猜测:放大果断,缩小谨慎(需要连续 2 次判断)

业务处理

  • 多路复用器( Selector )接收到 OP_READ 事件
  • 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read()
  • 分配一个初始 1024 字节的 byte buffer 来接受数据
  • 从 Channel 接受数据到 byte buffer
  • 记录实际接受数据大小,调整下次分配 byte buffer 大小
  • 触发 pipeline.fireChannelRead(byteBuf) 把读取到的数据传播出去
  • 判断接受 byte buffer 是否满载而归:是,尝试继续读取直到没有数据或满 16 次;否,结束本轮读取,等待下次OP_READ事件

知识点

  • 处理业务本质:数据在 pipeline 中所有的 handler 的 channelRead() 执行过程
    Handler 要实现 io.netty.channel.ChannelInboundHandler#channelRead (ChannelHandlerContext ctx,
    Object msg),且不能加注解 @Skip 才能被执行到。
    中途可退出,不保证执行到 Tail Handler
  • 默认处理线程就是 Channel 绑定的 NioEventLoop 线程,也可以设置其他
pipeline.addLast(new UnorderedThreadPoolEventExecutor(10), serverHandler)

发送数据

Netty 写数据,写不进去时,会停止写,然后注册一个 OP_WRITE 事件,来通知什么时候可以写进去了再写。
Netty 批量写数据时,如果想写的都写进去了,接下来的尝试写更多(调整 maxBytesPerGatheringWrite)。
Netty 只要有数据要写,且能写的出去,则一直尝试,直到写不出去或者满 16 次(writeSpinCount)。
Netty 待写数据太多,超过一定的水位线(writeBufferWaterMark.high()),会将可写的标志位改成
false ,让应用端自己做决定要不要发送数据了。

  • Write - 写数据到 buffer,ChannelOutboundBuffer#addMessage
  • Flush - 发送 buffer 里面的数据,AbstractChannel.AbstractUnsafe#flush
  • 准备数据 - ChannelOutboundBuffer#addFlush
  • 发送- NioSocketChannel#doWrite

知识点

  • 写数据写不进去时,会停止写,注册一个 OP_WRITE 事件,来通知什么时候可以写进去了
  • OP_WRITE 不是说有数据可写,而是说可以写进去,所以正常情况,不能注册,否则一直触发
  • 批量写数据时,如果尝试写的都写进去了,接下来会尝试写更多(maxBytesPerGatheringWrite)。
  • 只要有数据要写,且能写,则一直尝试,直到 16 次(writeSpinCount),写 16 次还没有写完,就直接 schedule 一个 task 来继续写,而不是用注册写事件来触发,更简洁有力
  • 待写数据太多,超过一定的水位线(writeBufferWaterMark.high()),会将可写的标志位改成 false ,让应用端自己做决定要不要继续写
  • channelHandlerContext.channel().write() :从 TailContext 开始执行;channelHandlerContext.write() : 从当前的 Context 开始。

断开连接

  • 多路复用器(Selector)接收到 OP_READ 事件 :
  • 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read()
  • 接受数据
  • 判断接受的数据大小是否 < 0 , 如果是,说明是关闭,开始执行关闭
  • 关闭 channel(包含 cancel 多路复用器的 key)。
  • 清理消息:不接受新信息,fail 掉所有 queue 中消息。
  • 触发 fireChannelInactive 和 fireChannelUnregistered

知识点

  • 关闭连接,会触发 OP_READ 方法。读取字节数是 -1 代表关闭
  • 数据读取进行时,强行关闭,触发 IO Exception,进而执行关闭。
  • Channel 的关闭包含了 SelectionKey 的 cancel

关闭服务

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

关闭所有 Group 中的 NioEventLoop
修改 NioEventLoop 的 State 标志位
NioEventLoop 判断 State 执行退出

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用Netty处理在线播放mp4请求,下面是大致的实现思路: 1. 创建一个Netty服务器,监听指定的端口,等待客户端连接。 2. 当有客户端连接时,创建一个ChannelPipeline,添加一系列的ChannelHandler,用于处理请求和响应。 3. 当收到客户端的请求时,解析请求中的URL,获取要播放的mp4文件的路径。 4. 使用Java的NIO API读取mp4文件,将数据写入响应的Channel中,实现流式传输。 5. 客户端收到响应后,使用HTML5的video标签播放mp4文件,实现在线播放。 示例代码如下: ```java public class Mp4StreamingServer { private final int port; public Mp4StreamingServer(int port) { this.port = port; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new Mp4StreamingServerHandler()); } }); ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = Integer.parseInt(args[0]); new Mp4StreamingServer(port).run(); } } ``` ```java public class Mp4StreamingServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final String CONTENT_TYPE = "Content-Type"; private static final String CONTENT_LENGTH = "Content-Length"; private static final String KEEP_ALIVE = "keep-alive"; @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { if (request.method() != HttpMethod.GET) { sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); return; } String uri = request.uri(); String path = sanitizeUri(uri); if (path == null) { sendError(ctx, HttpResponseStatus.FORBIDDEN); return; } File file = new File(path); if (!file.exists() || file.isHidden() || !file.isFile()) { sendError(ctx, HttpResponseStatus.NOT_FOUND); return; } if (!file.canRead()) { sendError(ctx, HttpResponseStatus.FORBIDDEN); return; } RandomAccessFile raf = new RandomAccessFile(file, "r"); long fileLength = raf.length(); HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); HttpUtil.setContentLength(response, fileLength); setContentTypeHeader(response, file); if (HttpUtil.isKeepAlive(request)) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write(response); ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(raf, 0, fileLength, 8192), ctx.newProgressivePromise()); sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception { if (total < 0) { System.err.println("Transfer progress: " + progress); } else { System.err.println("Transfer progress: " + progress + " / " + total); } } @Override public void operationComplete(ChannelProgressiveFuture future) throws Exception { System.err.println("Transfer complete."); } }); ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); if (!HttpUtil.isKeepAlive(request)) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); private String sanitizeUri(String uri) { try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { try { uri = URLDecoder.decode(uri, "ISO-8859-1"); } catch (UnsupportedEncodingException e1) { throw new Error(); } } if (!uri.startsWith("/")) { return null; } uri = uri.replace('/', File.separatorChar); if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) { return null; } return System.getProperty("user.dir") + File.separator + uri; } private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } private static void setContentTypeHeader(HttpResponse response, File file) { MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); } } ``` 在上面的示例代码中,Mp4StreamingServer类是启动Netty服务器的入口,它创建了一个ServerBootstrap实例,配置了NioEventLoopGroup、NioServerSocketChannel、ChannelInitializer和Mp4StreamingServerHandler等组件,并监听指定的端口,等待客户端连接。 Mp4StreamingServerHandler类是处理请求和响应的核心,它继承了SimpleChannelInboundHandler<FullHttpRequest>,重写了channelRead0方法,用于读取客户端的请求,解析请求中的URL,获取要播放的mp4文件的路径,读取mp4文件并将数据写入响应的Channel中,实现流式传输。同时,如果客户端请求中包含keep-alive头,则在响应中添加该头,以保持连接,避免浪费资源。最后,通过调用ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)方法,通知客户端数据传输完成,释放资源。 总之,使用Netty处理在线播放mp4请求,可以提高服务器的并发处理能力和吞吐量,同时实现实时流媒体传输,提升用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值