java netty之一个write跟踪(数据发送)

本文详细解析了Netty Server初始化、handler部分代码实现及数据发送过程,包括ByteBuf的使用、pipeline的write方法、context的write方法、outboundhandler的flush方法以及NioSocketChannel的unsafe对象的flush方法,全程跟踪数据发送流程。

在深入分析ServerBootstrap之前,先来跟踪一个write的过程吧,也就是数据发送的过程。。。。

先来看看这个例子的代码吧,server部分的初始化:

public class NettyServer {
	public void run() throws Exception {
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup);   //前者用来处理accept事件,后者用于处理已经建立的连接的io
			b.channel(NioServerSocketChannel.class);   //用它来建立新accept的连接,用于构造serversocketchannel的工厂类
			b.childHandler(new ChannelInitializer<SocketChannel>(){      //为accept的pipeline预添加的inboundhandler
				@Override     //当新连接accept的时候,这个方法会调用
				protected void initChannel(SocketChannel ch) throws Exception {
					// TODO Auto-generated method stub
					ch.pipeline().addLast(new MyChannelHandler());
				}
				
			});
			ChannelFuture f = b.bind(80).sync();    //在所有的网卡上监听这个端口
			f.channel().closeFuture().sync();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
	
	public static void main(String args[]) throws Exception {
		new NettyServer().run();
	}
}
接下来是handler部分的代码:
public class MyChannelHandler extends ChannelInboundByteHandlerAdapter {

	@Override
	protected void inboundBufferUpdated(ChannelHandlerContext ctx, ByteBuf in)
			throws Exception {
		// TODO Auto-generated method stub
		//while (in.isReadable()) {
			//System.out.print((char)in.readByte());
		//}
		ByteBuf b = ctx.alloc().buffer();
		b.writeBytes("aaa".getBytes());
		ctx.pipeline().write(b);
		ctx.pipeline().flush().addListener(new ChannelFutureListener(){

			@Override
			public void operationComplete(ChannelFuture future)
					throws Exception {
				// TODO Auto-generated method stub
				future.channel().close().sync();
			}
		});	
	}
}
这个要实现的功能还是很简单的,当建立了连接并收到数据之后,向客户端发送aaa。。。好了,接下来我们来跟踪一下整个过程。。。。

代码首先创建了一个ByteBuf,然后再将aaa写入到buffer里面,然后再发送buf,那是因为NioSocketChannel是基于byte的,因此它的unsafe对象也是基于byte的。。。。

先看看pipeline的write方法:(defaultchannelpipeline)

    //从尾handler的context的write方法
    public ChannelFuture write(Object message) {
        return tail.write(message);
    }
这里调用的是tailhander的context的write方法,而tailhandler是一个inboundhandler,因此会向前寻找到第一个oubounhander来处理,在前面的文章也有提到过对于读取的数据,pipeline上面的处理流程是从head到tail,而对于发送出去的数据,则是从tail到head。

好了,接下来看context的write方法:(defaultchannelhandlercontext)

    //调用write方法,并将创建的promise返回
    public ChannelFuture write(Object message) {
        return write(message, newPromise());
    }
额,接着再来看这里调用的write方法吧:
    @Override
    public ChannelFuture write(final Object message, final ChannelPromise promise) {
        if (message instanceof FileRegion) {
        	//如果是fileregion的话,
            return sendFile((FileRegion) message, promise);
        }

        if (message == null) {
            throw new NullPointerException("message");
        }
        validateFuture(promise);

        DefaultChannelHandlerContext ctx = prev;
        EventExecutor executor;
        final boolean msgBuf;
//向上寻找到第一个有buf的outboundhandler
        if (message instanceof ByteBuf) {
        	//如果是bytebuf的类型,那么向上找到第一个有outbytebuffer的outboundhandler
            for (;;) {
                if (ctx.hasOutboundByteBuffer()) {
                	
                    msgBuf = false;   //表示不是msg,是byte
                    executor = ctx.executor();
                    break;
                }
//message的类型,基于byte的handler和基于message的handler都行,因为最终到了head都会处理,成为基于byte的
                if (ctx.hasOutboundMessageBuffer()) {
                	
                    msgBuf = true;  //
                    executor = ctx.executor();
                    break;
                }

                ctx = ctx.prev;
            }
        } else {
            msgBuf = true;
            for (;;) {
                if (ctx.hasOutboundMessageBuffer()) {
                    executor = ctx.executor();
                    break;
                }

                ctx = ctx.prev;
            }
        }
//调用刚刚找到的那个有outbuf的handler的context的write0方法
        if (executor.inEventLoop()) {
            ctx.write0(message, promise, msgBuf);
            return promise;
        }

        final DefaultChannelHandlerContext ctx0 = ctx;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                ctx0.write0(message, promise, msgBuf);
            }
        });

        return promise;
    }
这部分代码就能很明显的看出是从tail向head方向寻找的吧。。。由于我们前期并没有定义outboundhandler,所以这里找到的将是pipeline上面默认的headhandler,好了,接下来来看write0方法吧:
//将数据写入到outbuf中,并调用flush方法
    private void write0(Object message, ChannelPromise promise, boolean msgBuf) {
        Channel channel = channel();
        //如果当前的channel并没有
        if (!channel.isRegistered() && !channel.isActive()) {
            promise.setFailure(new ClosedChannelException());
            return;
        }

        if (isOutboundFreed()) {
            promise.setFailure(new ChannelPipelineException(
                    "Unable to write as outbound buffer of next handler was freed already"));
            return;
        }
        if (msgBuf) {	    
        	//加入message
            outboundMessageBuffer().add(message);
        } else {
            ByteBuf buf = (ByteBuf) message;
            try {
            	//将要发送的数据写进outhandler
                outboundByteBuffer().writeBytes(buf, buf.readerIndex(), buf.readableBytes());
            } finally {
                buf.release();
            }
        }
        //flush操作,将缓冲区的数据发送出去
        invokeFlush0(promise);
    }
将数据写入到outbuf中去,然后最后调用invokeFlush0,好了这里来看看这个invokeFlush0f方法吧:
    //将buf里面的数据flush出去
    private void invokeFlush0(ChannelPromise promise) {
        if (isOutboundFreed()) {
            promise.setFailure(new ChannelPipelineException(
                    "Unable to flush as outbound buffer of next handler was freed already"));
            return;
        }

        Channel channel = channel();   //获取channel
        if (!channel.isActive() && !channel.isRegistered()) {
            promise.setFailure(new ClosedChannelException());
            return;
        }
//提取当前的context的handler,因为是outboundhandler,所以也是operationhandler
        ChannelOperationHandler handler = (ChannelOperationHandler) handler();
        if (handler instanceof ChannelOutboundHandler) {
        	//将bridge部分的数据flush,留在以后看
            flushOutboundBridge();
        }

        try {
        	//真实的调用poutboundhandler或者说operationhandler的flush方法,用于将数据发送出去
        	//其实是headhandler的话,其实就是调用其unsafe对象的flush方法,说白了也就是所属channel的unsafe对象的flush方法
            handler.flush(this, promise);
        } catch (Throwable t) {
            notifyHandlerException(t);
        } finally {
            if (handler instanceof ChannelOutboundByteHandler && !isOutboundFreed()) {
                try {
                    ((ChannelOutboundByteHandler) handler).discardOutboundReadBytes(this);
                } catch (Throwable t) {
                    notifyHandlerException(t);
                }
            }
            freeHandlerBuffersAfterRemoval();
        }
    }
代码还是很简单吧,说白了就是调用当前context的handler的flush方法,我们这里由于并没有定义自己的outboundhandler,所以这地调用的pipeline默认的headhandler的flush方法来将buf中的数据真正的发送出去。。

好了,那么接下来我们来看看headhandler的flush方法,由于NioSocketChannel是基于byte的,所以我们也看基于byte的headhandler的flush方法吧:

        @Override
        public void flush(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
            int discardedMessages = 0;
            MessageBuf<Object> in = msgSink;
            for (;;) {
            	//这里也是存在一定的可能性将要发送的数据写到了msgbuf中去
                Object m = in.poll();
                if (m == null) {
                    break;
                }
                if (m instanceof ByteBuf) {
                    ByteBuf src = (ByteBuf) m;
                    //将数据提取出来,并写到bytebuf中去
                    byteSink.writeBytes(src, src.readerIndex(), src.readableBytes());
                } else {
                    logger.debug(
                            "Discarded outbound message {} that reached at the head of the pipeline. " +
                                    "Please check your pipeline configuration.", m);
                    discardedMessages ++;
                }

                BufUtil.release(m);
            }

            if (discardedMessages != 0) {
                logger.warn(
                        "Discarded {} outbound message(s) that reached at the head of the pipeline. " +
                        "Please check your pipeline configuration.", discardedMessages);
            }
       //其实最终还是调用usafe的flush来写数据
            unsafe.flush(promise);
        }
    }
嗯,这个还是比较简单的吧,说白了还是调用的channel的unsafe对象的flush方法来发送数据的,好了感觉这篇文章是给自己挖了一个坑啊。。我擦,还要去看unsafe对象的flush方法,好吧,那接下来来看NioSocketChannel的unsafe对象的flush方法吧,其定义在AbstractChannel里的AbstractUnsafe中
        @Override
        //具体的将缓冲区的数据flush
        public void flush(final ChannelPromise promise) {
            if (eventLoop().inEventLoop()) {
            	//如果已经有需要flush的任务了,那么将flush的任务挂起就好了
                FlushTask task = flushTaskInProgress;
                if (task != null) {
                    // loop over the tasks to find the last one
                    for (;;) {
                        FlushTask t = task.next;
                        if (t == null) {
                            break;
                        }
                        task = t.next;
                    }
                    task.next = new FlushTask(null, promise);

                    return;
                }
                //here
                //调用这个方法
                flushNotifierAndFlush(promise);
            } else {
                eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        flush(promise);
                    }
                });
            }
        }
代码还是很简单,好了接下来继续来看flushNotifierAndFlush方法的定义吧:
        private void flushNotifierAndFlush(ChannelPromise promise) {
            flushNotifier(promise);
            //调用flush0方法来将数据发送数据
            flush0();
        }
好吧,直接来看flush0方法吧:
        private void flush0() {
            if (!inFlushNow) { // Avoid re-entrance
                try {
                    // Flush immediately only when there's no pending flush.
                    // If there's a pending flush operation, event loop will call flushNow() later,
                    // and thus there's no need to call it now.
                    if (!isFlushPending()) {
                        //调用flushnow方法将数据flush出去
                    	flushNow();
                    }
                } catch (Throwable t) {
                    flushFutureNotifier.notifyFlushFutures(t);
                    if (t instanceof IOException) {
                        close(voidFuture());
                    }
                }
            } else {
            	//执行flush的任务队列
                if (!flushNowPending) {
                    flushNowPending = true;
                    eventLoop().execute(flushLaterTask);
                }
            }
        }
这里分为两种情况吧,不过一般都是调用flushNowf方法将数据发送出去,不过当数据发送很慢的时候,还是可能将刮起的flush任务执行的,好了接下来来看flushNow方法吧:
        @Override
        public final void flushNow() {
            if (inFlushNow || flushTaskInProgress != null) {
                return;
            }

            inFlushNow = true;
            //here
            //获取headhandler的context,也就是pipeline里面自定义的那个,是outboundhandler,从他那里来获取outbuf,也就是要发送的数据
            ChannelHandlerContext ctx = headContext();
            Throwable cause = null;
            try {
                if (metadata().bufferType() == BufType.BYTE) {
                    ByteBuf out = ctx.outboundByteBuffer();
                    int oldSize = out.readableBytes();  //本身需要发送的数据量
                    try {
                    	//这个方法延后到了子类中显示
                        doFlushByteBuffer(out);
                    } catch (Throwable t) {
                        cause = t;
                    } finally {
                        int delta = oldSize - out.readableBytes();  //这里就相当于是已经发送了的数据量
                        out.discardSomeReadBytes();
                        flushFutureNotifier.increaseWriteCounter(delta);
                    }
                } else {
                    MessageBuf<Object> out = ctx.outboundMessageBuffer();
                    int oldSize = out.size();
                    try {
                        doFlushMessageBuffer(out);
                    } catch (Throwable t) {
                        cause = t;
                    } finally {
                        flushFutureNotifier.increaseWriteCounter(oldSize - out.size());
                    }
                }

                if (cause == null) {
                    flushFutureNotifier.notifyFlushFutures();
                } else {
                    flushFutureNotifier.notifyFlushFutures(cause);
                    if (cause instanceof IOException) {
                        close(voidFuture());
                    }
                }
            } finally {
                inFlushNow = false;
            }
        }
这里比较需要注意的是,是获取整个pipeline的headhandler的context,将它的buf里面的数据发送出去,其实最终调用的还是doFlushByteBuffer方法来发送数据:AbstractNioByteChannel
 //用于向channel中写数据
    protected void doFlushByteBuffer(ByteBuf buf) throws Exception {
        for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
        	//其实是调用doWriteBytes来写数据,这个方法延后到了后来的类中
            int localFlushedAmount = doWriteBytes(buf, i == 0);
            if (localFlushedAmount > 0) {
                break;
            }
            if (!buf.isReadable()) {
                // Reset reader/writerIndex to 0 if the buffer is empty.
                buf.clear();
                break;
            }
        }
    }
好了,还是来看doWriteBytes方法吧,它定义在NioSocketChannel中:
//写数据到channel里面,将buf里面的数据发送出去
    protected int doWriteBytes(ByteBuf buf, boolean lastSpin) throws Exception {
        final int expectedWrittenBytes = buf.readableBytes();  //相当于是需要发送的数据
        final int writtenBytes = buf.readBytes(javaChannel(), expectedWrittenBytes);  //这里就是真正的发送数据

        final SelectionKey key = selectionKey();   //获取selectionkey
        final int interestOps = key.interestOps();   //获取当前channel挂起的事件
        if (writtenBytes >= expectedWrittenBytes) {
        	//如果想要发送的数据都已经发送完了,那么可以更新感兴趣的事件了,将write事件去除
            // Wrote the outbound buffer completely - clear OP_WRITE.
            if ((interestOps & SelectionKey.OP_WRITE) != 0) {
                key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
            }
        } else {
            // Wrote something or nothing.
            // a) If wrote something, the caller will not retry.
            //    - Set OP_WRITE so that the event loop calls flushForcibly() later.
            // b) If wrote nothing:
            //    1) If 'lastSpin' is false, the caller will call this method again real soon.
            //       - Do not update OP_WRITE.
            //    2) If 'lastSpin' is true, the caller will not retry.
            //       - Set OP_WRITE so that the event loop calls flushForcibly() later.
        	//如果没有发送完数据,那么需要挂起写事件
            if (writtenBytes > 0 || lastSpin) {
                if ((interestOps & SelectionKey.OP_WRITE) == 0) {
                    key.interestOps(interestOps | SelectionKey.OP_WRITE);
                }
            }
        }

        return writtenBytes;
    }

好了,上面的代码还是很简单额,基本一看就能看明白,那么这个发送的流程也就走完了。。。

够长的。。感觉写的也不够仔细。。就这样吧。。

接下来可以看serverbootstrap的流程了。。。。


<think>嗯,用户问的是如何使用Netty的FileRegion实现服务端向客户端发送文件的例子。首先,我需要回忆一下Netty中处理大文件传输的相关组件。记得之前看过文档,FileRegion主要用于零拷贝传输文件,这样可以提高效率,特别是大文件的时候。 用户提到的是FileRegion,所以我应该先确定FileRegion的使用场景。根据引用[1],DefaultFileRegion适用于文件服务器直接推送给客户端的情况,不需要将文件加载到内存。所以如果用户是要直接发送文件而不处理内容,FileRegion是合适的。但如果有需要处理文件内容的话,可能需要用HttpChunkedInput,不过用户的问题明确问的是FileRegion,所以这部分暂时不用深入。 接下来,我需要构建一个示例代码。服务端需要配置ChannelPipeline,添加必要的处理器,比如HttpRequestDecoder和HttpResponseEncoder,因为是HTTP服务。然后在处理请求的时候,检查是否是HTTP GET请求,然后创建DefaultFileRegion实例,写入到Channel中。同时,需要正确设置HTTP响应头,比如Content-Length和Content-Type。 注意,FileRegion需要配合正确的环境,比如使用NIO传输,而且要在支持零拷贝的操作系统上。另外,文件传输完成后要记得释放资源,比如关闭文件通道。根据引用[2],sendfile系统调用是通过transferTo实现的,所以Netty的FileRegion底层应该也是依赖这个,这样可以减少用户态和内核态之间的数据拷贝,提升性能。 可能用户会遇到的问题包括文件路径是否正确,权限问题,以及是否正确处理了HTTP请求。在示例代码中,需要确保文件存在,并且正确捕获异常,比如FileNotFoundException。同时,需要正确设置HTTP响应状态,比如200 OK,以及关闭连接在传输完成后。 另外,引用[4]提到绑定IP和端口,所以在服务器启动时需要绑定到指定的端口,这里在代码中应该体现出来,比如ServerBootstrap绑定到8080端口。 最后,用户可能需要了解FileRegion的适用场景和限制。根据引用[1],如果文件需要加载到内存处理,就不适合用FileRegion,这时候应该用ChunkedInput。所以在相关问题部分,可以提到这些优化方法,以及性能比较,比如零拷贝和传统方式的区别。 综上,整理步骤:配置服务器,处理HTTP请求,创建FileRegion,发送文件,处理异常,关闭连接。然后给出代码示例,并解释关键点,最后补充注意事项和相关问题。</think>以下是通过Netty的FileRegion实现服务端向客户端发送文件的完整示例与说明: ### 一、实现步骤 1. **配置服务器引导类** ```java public class FileServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new HttpRequestDecoder()) .addLast(new HttpResponseEncoder()) .addLast(new FileServerHandler()); } }); b.bind(8080).sync().channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 2. **自定义处理器实现** ```java public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { if (request.method() == HttpMethod.GET) { File file = new File("/path/to/largefile.zip"); RandomAccessFile raf; try { raf = new RandomAccessFile(file, "r"); long fileLength = raf.length(); HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream"); ctx.write(response); FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, fileLength); ctx.write(region, ctx.newProgressivePromise()); ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } catch (FileNotFoundException e) { ctx.writeAndFlush(new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND )); } catch (IOException e) { ctx.close(); } } } } ``` ### 二、关键说明 1. **零拷贝机制**:通过`FileRegion`直接操作文件通道,绕过用户态内存拷贝(底层使用操作系统的sendfile系统调用)[^2] 2. **资源管理**:必须确保文件通道正确关闭,推荐使用try-with-resources或finally块 3. **协议支持**:示例使用HTTP协议传输,实际也可用于其他自定义协议 4. **性能优化**:适合传输大文件,避免内存溢出(OOM),但需要确保目标系统支持零拷贝[^1] ### 三、注意事项 1. **文件锁定**:传输期间文件会被锁定,不可修改 2. **内存映射**:当文件大小超过JVM内存限制时,仍能正常传输 3. **进度监控**:可通过添加`ChannelProgressiveFutureListener`实现进度跟踪 4. **异常处理**:需捕获`IOException`并关闭连接
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值