Netty实现文件服务器

本文介绍了文件上传下载的常用方法,如FTP、HTTP、HTTPS、SCP、SFTP等,重点展示了如何使用Netty框架构建基于HTTP协议的文件下载服务,并提供了客户端的Netty示例。包括服务器端的代码实现、文件处理逻辑以及客户端的下载操作,最后提及了未来可能的扩展方向。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.文件上传下载的常用方法

文件上传下载是一种非常常见的功能,特别是在web服务网站。

常用的文件上传下载协议有以下几种:

  • FTP(File Transfer Protocol):是一种用于在计算机间传输文件的标准网络协议。它使用客户端-服务器架构,通过对服务器的连接进行认证和授权,允许用户将文件上传到服务器或从服务器下载文件。
  • HTTP(s):超文本传输协议(HTTP)是用于在互联网上发送和接收超文本的协议。HTTP允许通过HTTP请求和响应进行文件的上传和下载。通过HTTPS可以提供更安全的传输。
  • SCP(Secure Copy):SCP是一种安全的文件传输协议,基于SSH(Secure Shell)协议。它可以在本地计算机和远程计算机之间进行文件传输,提供了加密和身份验证的功能。
  • SFTP(SSH File Transfer Protocol):SFTP是一个基于SSH的安全文件传输协议。它使用SSH进行身份验证和加密,可以在本地计算机和远程计算机之间进行安全的文件传输。
  • 除了以上几种应用层上的标准协议,我们甚至可以使用原生的socket构建文件服务器。使用Tcp搭建文件服务,采用的协议为私有协议。

2.服务端代码实现

本文演示如何使用netty搭建基于http协议的文件下载服务。(例子来自于netty官方example,这里做了简化,去掉ssl,去掉浏览器文件缓存)

2.1、构建ServerBootstrap

文件下载基于http协议实现,所以跟创建http服务很类似

public final class HttpStaticFileServer {

    private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private EventLoopGroup workerGroup = new NioEventLoopGroup();

    private HttpFileConfig ftpConfig;

    public HttpStaticFileServer(HttpFileConfig ftpConfig) {
        this.ftpConfig = ftpConfig;
    }

    public void start() throws Exception {
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new HttpServerCodec());
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            ch.pipeline().addLast(new HttpStaticFileServerHandler(ftpConfig));
                        }
                    });

            Channel ch = b.bind(ftpConfig.getPort()).sync().channel();
            System.err.println("http file server listens at " +
                    "http://127.0.0.1:" + ftpConfig.getPort() + '/');

            ch.closeFuture().sync();
        } catch (Exception e) {
            shutdown();
            throw e;
        }
    }

    public void shutdown() {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

}

总共添加3个handler,作用如下

HttpServerCodec:将HTTP请求消息从字节流解码为HttpRequest对象,将HTTP响应消息从HttpResponse对象编码为字节流。它还负责处理HTTP的编解码细节,如HTTP头部的解析和生成、HTTP消息的分块处理等。

HttpObjectAggregator:将多个HTTP请求或响应消息进行聚合,形成一个完整的HttpObject对象。在HTTP请求中,它可以将HTTP请求头部和请求体合并为一个FullHttpRequest对象;在HTTP响应中,它可以将HTTP响应头部和响应体合并为一个FullHttpResponse对象。HttpStaticFileServerHandler:自定义类,用于处理上一个节点解析的FullHttpRequest。

2.2、自定义类处理客户端的http请求

public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private HttpFileConfig httpConfig;

    HttpStaticFileServerHandler(HttpFileConfig config) {
        this.httpConfig = config;
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (!request.decoderResult().isSuccess()) {
            sendError(ctx, BAD_REQUEST);
            return;
        }

        if (request.method() != GET) {
            sendError(ctx, METHOD_NOT_ALLOWED);
            return;
        }

        final String uri = request.uri();
        final String path = sanitizeUri(uri);
        if (path == null) {
            sendError(ctx, FORBIDDEN);
            return;
        }

        File file = new File(path);
        if (file.isHidden() || !file.exists()) {
            sendError(ctx, NOT_FOUND);
            return;
        }

        if (file.isDirectory()) {
            if (uri.endsWith("/")) {
                sendFileListing(ctx, file, uri);
            } else {
                sendRedirect(ctx, uri + '/');
            }
            return;
        }

        if (!file.isFile()) {
            sendError(ctx, FORBIDDEN);
            return;
        }

        RandomAccessFile raf;
        try {
            raf = new RandomAccessFile(file, "r");
        } catch (FileNotFoundException ignore) {
            sendError(ctx, NOT_FOUND);
            return;
        }
        long fileLength = raf.length();

        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
        HttpUtil.setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, String.format("filename='%s'", URLEncoder.encode(file.getName(), "UTF-8")));

        if (HttpUtil.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }

        // Write the initial line and the header.
        ctx.write(response);

        // Write the content.
        ChannelFuture sendFileFuture;
        ChannelFuture lastContentFuture;
        sendFileFuture =
                ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
        lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
                if (total < 0) {
                    System.err.println(future.channel() + " Transfer progress: " + progress);
                } else {
                    System.err.println(future.channel() + " Transfer progress: " + progress + " / " + total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture future) {
                System.err.println(future.channel() + " Transfer complete.");
            }
        });

        if (!HttpUtil.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            sendError(ctx, INTERNAL_SERVER_ERROR);
        }
    }

    private String sanitizeUri(String uri) {
        try {
            uri = URLDecoder.decode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }

        if (uri.isEmpty() || uri.charAt(0) != '/') {
            return null;
        }
        uri = uri.replace('/', File.separatorChar);

        // 安全验证
        if (!httpConfig.getSafeRule().test(uri)) {
            return null;
        }

        // Convert to absolute path.
        return httpConfig.getFileDirectory() + File.separator + uri;
    }

    private void sendFileListing(ChannelHandlerContext ctx, File dir, String dirPath) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");

        StringBuilder buf = new StringBuilder()
                .append("<!DOCTYPE html>\r\n")
                .append("<html><head><meta charset='utf-8' /><title>")
                .append("Listing of: ")
                .append(dirPath)
                .append("</title></head><body>\r\n")

                .append("<h3>Listing of: ")
                .append(dirPath)
                .append("</h3>\r\n")

                .append("<ul>")
                .append("<li><a href=\"../\">..</a></li>\r\n");

        for (File f : dir.listFiles()) {
            if (f.isHidden() || !f.canRead()) {
                continue;
            }

            String name = f.getName();
            if (!httpConfig.getSafeRule().test(name)) {
                continue;
            }

            buf.append("<li><a href=\"")
                    .append(name)
                    .append("\">")
                    .append(name)
                    .append("</a></li>\r\n");
        }

        buf.append("</ul></body></html>\r\n");
        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
        response.headers().set(HttpHeaderNames.LOCATION, newUri);

        // Close the connection as soon as the error message is sent.
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(
                HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.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(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
    }
}

对于客户端的请求,如果是目录则枚举目录文件,封装成html的<a>超链接标签;如果是文件,则提供下载。

其中下载用到了DefaultFileRegion工具。

2.3、DefaultFileRegion解析

DefaultFileRegion可以利用零拷贝(zero-copy)技术,将文件内容直接从文件系统读取,并将数据传输到网络中,而无需经过中间缓冲区的复制。这种方式可以大大提高文件传输的效率,并减少CPU和内存的消耗。非常适用于需要传输大文件或者高并发的文件传输场景。

使用DefaultFileRegion写入文件之后,需要再写入特殊的httpconent表示接受,单例内容LastHttpContent.EMPTY_LAST_CONTENT。

需要注意的是,DefaultFileRegion只适用于文件服务器直接推送给客户端情况下使用。如果文件服务器需要将文件加载到内存,则不适合使用。改用下面的HttpChunkedInput。

2.4、使用HttpChunkedInput传输大文件

使用HttpChunkedInput,可以将需要传输的数据分割成多个HttpContent块,并通过Netty的HTTP编解码器进行传输。这样,发送方可以在需要的时候生成和发送每个块,接收方可以在接收到每个块时进行处理。

HttpChunkedInput可以与Netty的HTTP编解码器无缝集成,使得在进行HTTP分块传输时非常方便。对于需要处理大量数据的应用场景,使用HttpChunkedInput可以提高性能和降低内存消耗,从而更好地处理大规模数据传输。

使用HttpChunkedInput,则不需要手动添加LastHttpContent.EMPTY_LAST_CONTENT。内部代码自行添加

    public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
        if (this.input.isEndOfInput()) {
            if (this.sentLastChunk) {
                return null;
            } else {
                this.sentLastChunk = true;
                return this.lastHttpContent;
            }
        } else {
            ByteBuf buf = (ByteBuf)this.input.readChunk(allocator);
            return buf == null ? null : new DefaultHttpContent(buf);
        }
    }

ServerBootstrap添加ChunkedWriteHandler()配合处理大文件分块问题

  ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new HttpServerCodec());
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new HttpStaticFileServerHandler(ftpConfig));
                        }
                    });

2.5文件服务配置类

配置类复制端口设定,文件目录设定,文件安全检测策略设定等。

public class HttpFileConfig {

    /**
     * 需要提供对外服务的文件目录
     */
    private String fileDirectory;


    /**
     * 对外服务端口,默认为8080
     */
    private int port  = 8080;

    /**
     * url安全规则判定,默认运行访问制定目录下所有文件
     */
    private Predicate<String> safeRule = new Predicate<String>() {
        @Override
        public boolean test(String s) {
            return true;
        }
    };

    //省略setter/getter
    
}

2.6启动入口

public class Main {

    public static void main(String[] args) throws Exception {
        HttpFileConfig cfg = new HttpFileConfig();
        cfg.setFileDirectory("F:\\projects");
        HttpStaticFileServer fileServer = new HttpStaticFileServer(cfg);
        fileServer.start();
    }
}

3.客户端演示

3.1netty客户端

使用netty,也可以实现文件下载功能。

public class HttpDownloadClient {

    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8080;
    private static EventLoopGroup group = new NioEventLoopGroup();


    public static void main(String[] args) throws Exception {

        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new HttpClientCodec());
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new HttpObjectAggregator(65536));
                            ch.pipeline().addLast(new HttpFileDownloadClientHandler());
                        }
                    });


            ChannelFuture future = b.connect(SERVER_HOST, SERVER_PORT).sync();

            // 这里填写具体文件名称,服务器相对路径
            String targetFile = "/";
            // 构建HTTP GET请求
            DefaultHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, targetFile );
            // 设置请求头信息,指定下载文件的名称和保存路径
            requestSetting(request);

            // 发送HTTP GET请求
            future.channel().writeAndFlush(request);
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    private static void requestSetting(DefaultHttpRequest request) {
        request.headers().set(HttpHeaderNames.HOST, SERVER_HOST);
        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
        request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
        request.headers().set(HttpHeaderNames.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
        request.headers().set(HttpHeaderNames.USER_AGENT, "Netty File Download Client");
        request.headers().set(HttpHeaderNames.ACCEPT_LANGUAGE, "en-us,en;q=0.5");
        request.headers().set(HttpHeaderNames.ACCEPT, "*/*");
        request.headers().set(HttpHeaderNames.RANGE, "bytes=0-");
        request.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
        request.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
    }

     static class HttpFileDownloadClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {

        }


        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        }

        private static final Pattern FILENAME_PATTERN = Pattern.compile("filename\\*?=\"?(?:UTF-8'')?([^\";]*)\"?;?");

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse response) throws Exception {
            System.out.println(response);
            String contentType = response.headers().get(HttpHeaderNames.CONTENT_TYPE);
            if (contentType.contains("html")) {
                if (response.getDecoderResult().isSuccess() && response.content().isReadable()) {
                    String html = response.content().toString(io.netty.util.CharsetUtil.UTF_8);
                    System.out.println(html);
                }
            } else if (contentType.contains("application/octet-stream")) {
                Matcher matcher = FILENAME_PATTERN.matcher(response.headers().get(HttpHeaderNames.CONTENT_DISPOSITION));
                String fileName = "f@" + System.currentTimeMillis();
                if (matcher.find()) {
                    fileName = matcher.group(1).replaceAll("'", "").replaceAll("\"", "");
                    System.out.println("下载文件:" + fileName);
                }
                RandomAccessFile file = new RandomAccessFile("download/"+fileName, "rw");
                FileChannel fileChannel = file.getChannel();
                fileChannel.write(response.content().nioBuffer()); // 将数据从ByteBuffer写入到RandomAccessFile
            }
        }
    }
}

通过解析 FullHttpResponse的Header,如果是html文本则显示html内容;如果是字节流则保存到本地目录。

本地测试了以下,一个3.5G的电影,大概需要60秒。

进一步拓展

1.增加用户界面;

2.增加文件断点续传;

3.多线程下载;

3.2浏览器客户端

直接在浏览器显示,当然最方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jforgame

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值