基于netty搭建大文件分片传输服务
- 简介
- 一、依赖导入
- 二、代码
- 2.1代码结构
- 2.2 部分代码
- 三、测试
简介
在应用中经常会使用到服务间的大文件传输功能,本文简单介绍和编写基于netty的客户端和服务端之间的大文件分片传输功能,项目源码:https://github.com/itwwj/netty-learn.git中的netty-day07-uploading项目。
一、依赖导入
<properties>
<protostuff.version>1.0.10</protostuff.version>
<objenesis.version>2.4</objenesis.version>
<netty_version>4.1.24.Final</netty_version>
</properties>
<dependencies>
<!-- Netty4.1 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty_version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<!-- Protostuff -->
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis.version}</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
二、代码
2.1代码结构
MyClientChannelInitializer
MyClientHandler
NettyClient
codec
CustomDecoder
CustomEncoder
model
Constants
FileBurstData
FileBurstInstruct
FileDescInfo
FileTransferProtocol
server
MyServerChannelInitializer
MyServerHandler
NettyServer
utils
CacheUtil
FileUtil
MsgUtil
SerializationUtil
FileUploadingApplication
2.2 部分代码
netty启动类:
/**
* netty服务端启动类
*
* @author jie
*/
@Data
@Slf4j
@Component
public class NettyServer {
private EventLoopGroup boosGroup = new NioEventLoopGroup();
private EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
@Autowired
private ChannelInitializer initializer;
public ChannelFuture init(int port) {
ChannelFuture f=null;
try {
//用于启动NIO服务端的辅助启动类,目的是降低服务端的开发复杂度
ServerBootstrap b = new ServerBootstrap();
b.group(boosGroup, workerGroup)
//对应JDK NIO类库中的ServerSocketChannel
.channel(NioServerSocketChannel.class)
//配置NioServerSocketChannel的TCP参数
.option(ChannelOption.SO_BACKLOG, 1024)
//绑定I/O的事件处理类
.childHandler(initializer);
f = b.bind(port).sync();
channel = f.channel();
} catch (InterruptedException e) {
log.error(e.getMessage());
}
return f;
}
/**
* 关闭
*/
public void close() {
if (null == channel) {
return;
}
channel.close();
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
出入栈事件类:
/**
*
* MyChannelInitializer的主要目的是为程序员提供了一个简单的工具,用于在某个Channel注册到EventLoop后,对这个Channel执行一些初始
* 化操作。ChannelInitializer虽然会在一开始会被注册到Channel相关的pipeline里,但是在初始化完成之后,ChannelInitializer会将自己
* 从pipeline中移除,不会影响后续的操作。
* @author jie
*/
@Component
public class MyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
/**
* 这个方法在Channel被注册到EventLoop的时候会被调用
* @param socketChannel
* @throws Exception
*/
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
System.out.println("=========有客户端连接服务器=========");
System.out.println("ip:"+socketChannel.localAddress().getHostString()+" port:"+socketChannel.localAddress().getPort());
//对象传输处理
socketChannel.pipeline().addLast(new CustomDecoder(FileTransferProtocol.class));
socketChannel.pipeline().addLast(new CustomEncoder(FileTransferProtocol.class));
socketChannel.pipeline().addLast(new MyServerHandler());
}
}
事件触发类:
/**
* 操作类
*
* @author jie
*/
@Slf4j
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 文件存放地址
*/
private String path = "E:\\code\\study\\netty-learn\\netty-day07-uploading\\testFile";
/**
* 通道有消息触发
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//数据格式验证
if (!(msg instanceof FileTransferProtocol)) {
return;
}
FileTransferProtocol fileTransferProtocol = (FileTransferProtocol) msg;
//0传输文件'请求'、1文件传输'指令'、2文件传输'数据'
switch (fileTransferProtocol.getTransferType()) {
case 0:
FileDescInfo fileDescInfo = (FileDescInfo) fileTransferProtocol.getTransferObj();
//断点续传信息,实际应用中需要将断点续传信息保存到数据库中
FileBurstInstruct fileBurstInstructOld = CacheUtil.burstDataMap.get(fileDescInfo.getFileName());
if (null != fileBurstInstructOld) {
if (fileBurstInstructOld.getStatus() == Constants.FileStatus.COMPLETE) {
CacheUtil.burstDataMap.remove(fileDescInfo.getFileName());
}
//传输完成删除断点信息
log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收客户端传输文件请求[断点续传]。" + JSON.toJSONString(fileBurstInstructOld));
ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstructOld));
return;
}
//发送信息
FileTransferProtocol sendFileTransferProtocol = MsgUtil.buildTransferInstruct(Constants.FileStatus.BEGIN, fileDescInfo.getFileUrl(), 0);
ctx.writeAndFlush(sendFileTransferProtocol);
log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收客户端传输文件请求。" + JSON.toJSONString(fileDescInfo));
break;
case 2:
FileBurstData fileBurstData = (FileBurstData) fileTransferProtocol.getTransferObj();
FileBurstInstruct fileBurstInstruct = FileUtil.writeFile(path, fileBurstData);
//保存断点续传信息
CacheUtil.burstDataMap.put(fileBurstData.getFileName(), fileBurstInstruct);
ctx.writeAndFlush(MsgUtil.buildTransferInstruct(fileBurstInstruct));
log.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收客户端传输文件数据。" + JSON.toJSONString(fileBurstData));
//传输完成删除断点信息
if (fileBurstInstruct.getStatus() == Constants.FileStatus.COMPLETE) {
CacheUtil.burstDataMap.remove(fileBurstData.getFileName());
}
break;
default:
break;
}
}
/**
* 当连接发生异常时触发
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//在发生异常时主动关掉连接
ctx.close();
log.error("发现异常:\r\n" + cause.getMessage());
}
}
编码器:
/**
* 自定义编码器
* @author jie
*/
public class CustomEncoder extends MessageToByteEncoder {
private Class<?> genericClass;
@Override
protected void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) {
if (genericClass.isInstance(in)) {
byte[] data = SerializationUtil.serialize(in);
out.writeInt(data.length);
out.writeBytes(data);
}
}
public CustomEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
}
解码器:
/**
* 自定义解码器
* @author jie
*/
public class CustomDecoder extends ByteToMessageDecoder {
private Class<?> genericClass;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
in.markReaderIndex();
int dataLength = in.readInt();
if (in.readableBytes() < dataLength) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
in.readBytes(data);
out.add(SerializationUtil.deserialize(data, genericClass));
}
public CustomDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
}
三、测试
启动文件服务端
启动test目录下的测试类:FileUploading
成功将大文件从客户端传输至服务端。