用Netty搭建文件上传系统

目录

目标

实战

依赖

基础版(适用于小文件传输)

分块版(适用于大文件传输)


目标

用Netty搭建一个文件上传(视频、音频、文本、表格等)系统,要求

  1. 客户端向服务端发送一个文件(可以是视频、音频、文本、表格等格式的文件),服务端接收文件并保存到指定的目录下。
  2. 服务端保存好文件之后向客户端发送回应:xxx文件收到了。

实战

依赖

        
			org.springframework.boot
			spring-boot-starter-web
		

		
			net.sourceforge.tess4j
			tess4j
			4.5.2
		

		
		
			com.github.jai-imageio
			jai-imageio-core
			1.4.0
		
		
		
			io.netty
			netty-all
			4.1.87.Final
		

		
		
			org.projectlombok
			lombok
			1.18.0
		

		
		
			org.slf4j
			slf4j-api
			1.7.25
		

		
		
			ch.qos.logback
			logback-classic
			1.2.3
		

		
		
			org.junit.jupiter
			junit-jupiter-api
			5.2.0
			test
		

		
		
			commons-lang
			commons-lang
			2.6
		

		
			org.springframework.boot
			spring-boot-starter-web
		

		
			org.springframework.boot
			spring-boot-starter-test
			test
		

		
			net.java.dev.jna
			jna
			4.1.0
		

基础版(适用于小文件传输)

服务端

package com.ctx.file.simple.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FileUploadServer {
    private static final int SERVER_PORT = 8000;

    public static void main(String[] args) throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(SERVER_PORT)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new LengthFieldPrepender(4))
                                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))
                                    .addLast(new FileUploadServerHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.bind().sync();
            log.info("服务器启动,监听端口:{}", SERVER_PORT);
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

}

自定义服务端处理文件的Handler

package com.ctx.file.simple.server;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
@Slf4j
public class FileUploadServerHandler extends SimpleChannelInboundHandler {
    private static final String UPLOAD_DIR = "C:\\Users\\20203\\Desktop\\test2\\";
    /**
     * 接收到新消息时,会调用该方法。
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // 读取文件长度
        long fileLength = msg.readLong();
        // 读取文件名字节数组
        byte[] fileNameBytes = new byte[msg.readInt()];
        msg.readBytes(fileNameBytes);
        // 将文件名字节数组转换为字符串
        String fileName = new String(fileNameBytes, StandardCharsets.UTF_8);
        // 读取文件内容字节数组
        byte[] fileContentBytes = new byte[(int) (fileLength - fileNameBytes.length)];
        msg.readBytes(fileContentBytes);
        // 构造文件保存路径
        String filePath = UPLOAD_DIR + fileName;
        // 创建文件对象
        File file = new File(filePath);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            // 将文件内容字节数组写入文件
            fos.write(fileContentBytes);
        }
        // 打印接收到的文件保存路径
        log.info("接收到文件,保存为:{}", filePath);
        // 向客户端发送文件接收成功的消息
        ctx.writeAndFlush(ctx.alloc().buffer().writeBytes(("文件:" + filePath + " 收到了").getBytes()));
    }

    /**
     * 异常回调方法
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端

package com.ctx.file.simple.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

@Slf4j
public class FileUploadClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8000;

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(SERVER_HOST, SERVER_PORT))
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    //.addLast(new LoggingHandler(LogLevel.DEBUG)) // 添加LoggingHandler
                                    //LengthFieldPrepender是一个Netty编码器,用于在消息前添加指定长度的字段。在这里,它被用于在消息的开头添加一个4字节的长度字段。这是为了告诉接收方消息的长度,以便接收方可以正确解码消息。
                                    .addLast(new LengthFieldPrepender(4))
                                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))
                                    .addLast(new FileUploadClientHandler());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect().sync();
            System.out.println("连接服务器成功");

            // 上传文件
            File file = new File("C:\\Users\\20203\\Desktop\\test\\mda-pdfd35jjw0xfnhtm.mp4");
            try (FileInputStream fis = new FileInputStream(file)) {
                // 读取文件名并转换为字节数组
                byte[] fileNameBytes = file.getName().getBytes(StandardCharsets.UTF_8);
                // 创建字节数组,用于存储文件内容
                byte[] fileContentBytes = new byte[(int) file.length()];
                // 从文件输入流中读取文件内容到字节数组
                fis.read(fileContentBytes);
                // 创建一个ByteBuf,用于存储编码后的消息
                ByteBuf byteBuf = channelFuture.channel().alloc().buffer();
                // 写入消息的总长度,包括文件名长度和文件内容长度
                byteBuf.writeLong(fileNameBytes.length + fileContentBytes.length);
                // 写入文件名长度
                byteBuf.writeInt(fileNameBytes.length);
                // 写入文件名字节数组
                byteBuf.writeBytes(fileNameBytes);
                // 写入文件内容字节数组
                byteBuf.writeBytes(fileContentBytes);
                // 将编码后的消息写入到通道,并刷新缓冲区
                channelFuture.channel().writeAndFlush(byteBuf);
            }
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

自定义客户端接收服务端回应的Handler

package com.ctx.file.simple.client;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
@Slf4j
public class FileUploadClientHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        log.info("{}服务端发来消息:{}",ctx.channel().remoteAddress(),msg.toString(StandardCharsets.UTF_8));
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

分块版(适用于大文件传输)

服务端

package com.ctx.file.part.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FileUploadServer {
    private static final int SERVER_PORT = 8000;

    public static void main(String[] args) throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(SERVER_PORT)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new LengthFieldPrepender(4))
                                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))
                                    .addLast(new FileUploadServerHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.bind().sync();
            log.info("服务器启动,监听端口:{}", SERVER_PORT);
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

自定义服务端处理文件的Handler

package com.ctx.file.part.server;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;

@Slf4j
public class FileUploadServerHandler extends SimpleChannelInboundHandler {
    private static final String UPLOAD_DIR = "C:\\Users\\20203\\Desktop\\test2\\";

    private long fileLength;
    private String fileName;
    private FileOutputStream fos;
    private long receivedLength;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        if (fileLength == 0) {
            if (msg.readableBytes() >= 16) {
                fileLength = msg.readLong();
                long fileNameLength = msg.readLong();
                byte[] fileNameBytes = new byte[(int) fileNameLength];
                msg.readBytes(fileNameBytes);
                fileName = new String(fileNameBytes, StandardCharsets.UTF_8);
                String filePath = UPLOAD_DIR + fileName;
                File file = new File(filePath);
                fos = new FileOutputStream(file);
            } else {
                return;
            }
        }

        if (fileLength > 0) {
            // 获取消息中的内容长度
            int contentLength = msg.readableBytes();
            // 创建字节数组来存储内容
            byte[] contentBytes = new byte[contentLength];
            // 从消息中读取内容并存储到字节数组中
            msg.readBytes(contentBytes);
            // 将内容写入文件输出流
            fos.write(contentBytes);
            // 更新文件长度和已接收长度
            fileLength -= contentLength;
            receivedLength += contentLength;

            log.info("服务端接收到文件内容:{}/{}", receivedLength, fileLength + receivedLength);
            if (fileLength == 0) {
                fos.close();
                log.info("接收到文件,保存为:{}", UPLOAD_DIR + fileName);
                ctx.writeAndFlush(ctx.alloc().buffer().writeBytes(("文件:" + fileName + " 收到了").getBytes()));
            }
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端

package com.ctx.file.part.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

@Slf4j
public class FileUploadClient {
    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 8000;
    private static final int SEND_SIZE = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(SERVER_HOST, SERVER_PORT))
                    .handler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new LengthFieldPrepender(4))  // 添加长度字段编码器
                                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))  // 添加长度字段解码器
                                    .addLast(new FileUploadClientHandler());  // 添加自定义的文件上传客户端处理器
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect().sync();
            log.info("连接服务器成功");

            // 上传文件
            File file = new File("C:\\Users\\20203\\Desktop\\test\\a.txt");
            try (FileInputStream fis = new FileInputStream(file)) {
                String fileName = file.getName();
                long fileLength = file.length();

                // 发送文件名和文件长度
                ByteBuf metadataBuf = channelFuture.channel().alloc().buffer();
                metadataBuf.writeLong(fileLength);  // 写入文件长度
                metadataBuf.writeLong(fileName.length());  // 写入文件名长度
                metadataBuf.writeBytes(fileName.getBytes(StandardCharsets.UTF_8));  // 写入文件名字节数组
                channelFuture.channel().writeAndFlush(metadataBuf);

                // 发送文件内容
                ByteBuf fileBuf;
                long remainingBytes = fileLength;
                byte[] buffer = new byte[SEND_SIZE];
                int bytesRead;
                int totalBytesSent = 0;  // 已发送的总字节数
                while ((bytesRead = fis.read(buffer)) != -1) {
                    // 创建文件缓冲区
                    fileBuf = channelFuture.channel().alloc().buffer(bytesRead);
                    // 将文件内容写入缓冲区
                    fileBuf.writeBytes(buffer, 0, bytesRead);
                    // 将缓冲区内容写入通道并刷新
                    channelFuture.channel().writeAndFlush(fileBuf);
                    // 更新已发送的总字节数
                    totalBytesSent += bytesRead;
                    // 更新剩余字节数
                    remainingBytes -= bytesRead;
                    log.info("已发送字节数: {}/{}", totalBytesSent, fileLength);
                }
                if (remainingBytes == 0) {
                    log.info("文件上传完成");
                } else {
                    log.error("文件上传失败");
                }
            }
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

自定义客户端接收服务端回应的Handler

package com.ctx.file.part.client;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;

@Slf4j
public class FileUploadClientHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        log.info("{} 服务端发来消息:{}", ctx.channel().remoteAddress(), msg.toString(StandardCharsets.UTF_8));
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("发生异常:", cause);
        ctx.close();
    }
}

你可能感兴趣的:(Netty,Netty文件传输系统,netty大文件传输,netty文件按块传输)