Springboot2(25)整合netty實現檔案傳輸
阿新 • • 發佈:2018-12-29
springboot2教程系列
其它netty檔案有部落格Springboot2(24)整合netty實現http服務(類似SpingMvc的contoller層實現)
實現瀏覽本地檔案目錄,實現資料夾目錄的跳轉和檔案的下載
新增依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.1.Final</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons.lang.version}</version> </dependency>
排除tomcat的依賴
Netty Http服務端編寫
handler 處理類
@Component
@Slf4j
@ChannelHandler.Sharable //@Sharable 註解用來說明ChannelHandler是否可以在多個channel直接共享使用
public class FileServerHandler extends ChannelInboundHandlerAdapter {
// private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
//檔案存放路徑
@Value("${netty.file.path:}")
String path;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try{
if (msg instanceof FullHttpRequest) {
FullHttpRequest req = (FullHttpRequest) msg;
if(req.method () != HttpMethod.GET) {
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
String url = req.uri();
File file = new File(path + url);
if(file.exists()){
if(file.isDirectory()){
if(url.endsWith("/")) {
sendListing(ctx, file);
}else{
sendRedirect(ctx, url + "/");
}
return;
}else {
transferFile( file, ctx);
}
}else{
sendError(ctx, HttpResponseStatus.NOT_FOUND);
}
}
}catch(Exception e){
log.error("Exception:{}",e);
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
}
}
/**
* 傳輸檔案
* @param file
* @param ctx
*/
private void transferFile(File file, ChannelHandlerContext ctx){
try{
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
ctx.write(response);
ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
addListener( sendFileFuture);
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}catch (Exception e){
log.error("Exception:{}",e);
}
}
/**
* 監聽傳輸狀態
* @param sendFileFuture
*/
private void addListener( ChannelFuture sendFileFuture){
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
log.debug("Transfer complete.");
}
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
if(total < 0){
log.debug("Transfer progress: " + progress);
}else{
log.debug("Transfer progress: " + progress + "/" + total);
}
}
});
}
/**
* 請求為目錄時,顯示檔案列表
* @param ctx
* @param dir
*/
private static void sendListing(ChannelHandlerContext ctx, File dir){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
String dirPath = dir.getPath();
StringBuilder buf = new StringBuilder();
buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("目錄:");
buf.append("</title></head><body>\r\n");
buf.append("<h3>");
buf.append(dirPath).append(" 目錄:");
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li>連結:<a href=\" ../\")..</a></li>\r\n");
for (File f : dir.listFiles()) {
if(f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
/*if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}*/
buf.append("<li>連結:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.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);
}
/**
* 跳轉連結
* @param ctx
* @param newUri
*/
private static void sendRedirect(ChannelHandlerContext ctx, String newUri){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
response.headers().set(HttpHeaderNames.LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* 失敗響應
* @param ctx
* @param status
*/
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
ChannelPipeline 實現
@Component
@ConditionalOnProperty( //配置檔案屬性是否為true
value = {"netty.file.enabled"},
matchIfMissing = false
)
public class FilePipeline extends ChannelInitializer<SocketChannel> {
@Autowired
FileServerHandler fleServerHandler;
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
p.addLast("http-decoder", new HttpRequestDecoder());
p.addLast("http-aggregator", new HttpObjectAggregator(65536));
p.addLast("http-encoder", new HttpResponseEncoder());
p.addLast("http-chunked", new ChunkedWriteHandler());
p.addLast("fileServerHandler",fleServerHandler);
}
}
服務實現
@Configuration
@EnableConfigurationProperties({NettyFileProperties.class})
@ConditionalOnProperty( //配置檔案屬性是否為true
value = {"netty.file.enabled"},
matchIfMissing = false
)
@Slf4j
public class FileServer {
@Autowired
FilePipeline filePipeline;
@Autowired
NettyFileProperties nettyFileProperties;
@Bean("starFileServer")
public String start() {
Thread thread = new Thread(() -> {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(nettyFileProperties.getBossThreads());
NioEventLoopGroup workerGroup = new NioEventLoopGroup(nettyFileProperties.getWorkThreads());
try {
log.info("start netty [FileServer] server ,port: " + nettyFileProperties.getPort());
ServerBootstrap boot = new ServerBootstrap();
options(boot).group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(filePipeline);
Channel ch = null;
//是否繫結IP
if(StringUtils.isNotEmpty(nettyFileProperties.getBindIp())){
ch = boot.bind(nettyFileProperties.getBindIp(),nettyFileProperties.getPort()).sync().channel();
}else{
ch = boot.bind(nettyFileProperties.getPort()).sync().channel();
}
ch.closeFuture().sync();
} catch (InterruptedException e) {
log.error("啟動NettyServer錯誤", e);
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
});
thread.setName("File_Server");
thread.start();
return "file start";
}
private ServerBootstrap options(ServerBootstrap boot) {
/* boot.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);*/
return boot;
}
}
啟動配置
---application.yml
spring.profiles.active: file
---application-file.yml
netty:
file:
enabled: true
path: d:\
port: 3456
測試
在瀏覽器開啟http://127.0.0.1:3456/