目录
目标
实战
依赖
基础版(适用于小文件传输)
分块版(适用于大文件传输)
用Netty搭建一个文件上传(视频、音频、文本、表格等)系统,要求:
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();
}
}