在工作中,文件服务器是很常用的,我们经常需要把一些公共资源放到服务器上的某个目录下,通过IP加端口就可以实现文件的查看,下载等功能,
常见的方法像tomcat,将文件放到webapps下,启动tomcat后,设置对外访问的端口就可以达到目的,但是这种方式有点局限,就是需要你提前知道文件的全路径,不够直观,另一种做法,也是比较简单、性能高效的方式,就是在服务器安装nginx,然后通过nginx建立资源目录,达到对公共资源进行操作的目的,也是生产中常用的做法,但是nginx的做法和tomcat有一个共同的缺点,就是资源不能直观的对用户展示出来;
这里引出本篇要讲解的另一种方式,就是通过写netty程序作为服务端,然后暴露相关端口和资源目录即可,用户既能看到文件,也能进去进行读取、下载,对于运维人员来说,可以通过开发目录达到对资源目录权限的控制;
代码编写
在编写netty代码之前,个人觉得需要对netty的代码编写结构和思路进行简单的梳理,其实掌握了netty的逻辑,不难发现,netty的服务器程序主要包括三部分,用一张简单的图展示如下,
简单解释一下,需要一个服务端的server,这个server的代码比较固定,两个包装线程的NioEventLoopGroup ,然后加入相关的配置,配置包括 pipline的代码,可以理解为处理流通在服务端和客户端之间的数据流的一个组件,通过这个组件,我们可以对流通在channel中的数据进行具体的业务处理,比如控制编码格式,控制数据流的大小,对数据的规则进行校验、过滤等操作,这是比较粗糙的层面,如果需要在这个过程加入自身业务逻辑更加细致的处理,就需要一个个的handler了,即自定义的handler,这个就是我们熟悉的责任链模式在netty框架中的具体应用,不同的handler关注自身的那部分业务处理即可
有了上述的基本概念,再写代码时就可以按照这个思路去编写代码,更加具体的概念大家可以参考相关的资料进行脑补;
1、添加pom依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.1.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><!-- /artifact/com.rabbitmq/amqp-client --><dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>5.7.0</version></dependency><!--<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><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>ty</groupId><artifactId>netty-all</artifactId><version>4.1.25.Final</version></dependency><!-- /artifact/mons/commons-lang3 --><dependency><groupId>mons</groupId><artifactId>commons-lang3</artifactId><version>3.8</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.0</version></dependency></dependencies>
2、编写文件处理的server
/*** 文件服务器启动配置类*/@Configuration@EnableConfigurationProperties({NettyFileProperties.class})@ConditionalOnProperty(value = {"netty.file.enabled"},matchIfMissing = false)public class FileServer {private Logger logger = LoggerFactory.getLogger(FileServer.class);@AutowiredFilePipeline filePipeline;@AutowiredNettyFileProperties nettyFileProperties;@Bean("starFileServer")public String start() {Thread thread = new Thread(() -> {NioEventLoopGroup bossGroup = new NioEventLoopGroup(nettyFileProperties.getBossThreads());NioEventLoopGroup workerGroup = new NioEventLoopGroup(nettyFileProperties.getWorkThreads());try {logger.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;//是否绑定IPif(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) {logger.error("启动NettyServer错误", e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}});thread.setName("File_Server");thread.start();return "file start";}private ServerBootstrap options(ServerBootstrap boot) {return boot;}}
从这段代码即可以看到,pipline就加入了server代码中,
3、编写filePipeline
@Component@ConditionalOnProperty( value = {"netty.file.enabled"},matchIfMissing = false)public class FilePipeline extends ChannelInitializer<SocketChannel>{@AutowiredFileServerHandler fleServerHandler;@Overrideprotected 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());//引入文件处理的handlerp.addLast("fileServerHandler",fleServerHandler);}}
4、filePipeline 添加自定义文件处理handler
/*** @Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用* 文件处理handler*/@Component@ChannelHandler.Sharablepublic class FileServerHandler extends ChannelInboundHandlerAdapter {///cowbin/article/details/85290876private Logger logger = LoggerFactory.getLogger(FileServerHandler.class);//文件存放路径@Value("${netty.file.path:}")String path;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {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) {}}/*** 传输文件** @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) {logger.error("Exception:{}", e);}}/*** 监听传输状态** @param sendFileFuture*/private void addListener(ChannelFuture sendFileFuture) {sendFileFuture.addListener(new ChannelProgressiveFutureListener() {@Overridepublic void operationComplete(ChannelProgressiveFuture future)throws Exception {logger.debug("Transfer complete.");}@Overridepublic void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {if (total < 0) {logger.debug("Transfer progress: " + progress);} else {logger.debug("Transfer progress: " + progress + "/" + total);}}});}/*** 跳转链接** @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 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 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);}}
5、辅助配置类
@ConfigurationProperties(prefix="netty.file")@Data@Validatedpublic class NettyFileProperties {@NotNull(message = "端口不能为空")@Range(min=1000, max=60000)private Integer port;@NotNull(message = "文件路径不能为空")private String path;@Pattern(regexp="((25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))\\.){3}(25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d)))",message="ip地址格式不正确")private String bindIp;//必须大于1 ,老板线程,即认为是分配工作的线程@DecimalMin("1")private Integer bossThreads = Math.max(1, SystemPropertyUtil.getInt("ty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));//必须大于1,实际工作线程数量,这个数量最好根据JVM的系统信息进行配置,这里直接动态获取@DecimalMin("1")private Integer workThreads = Math.max(1, SystemPropertyUtil.getInt("ty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));}
7、启动类
@SpringBootApplicationpublic class NettyApplication {public static void main(String[] args) {SpringApplication.run(NettyApplication.class,args);}}
8、yml文件
netty:file:enabled: truepath: c:\port: 3456
启动程序,浏览器访问:http://127.0.0.1:3456/,看到如下效果,
是不是这个效果很像我们访问本地磁盘上的文件,我们可以进入某个目录下,尝试下载一个本地文件,可以看到就像下载浏览器上的某个文件一样,
看图片也很方便,
本篇的讲解到此结束,最后,感谢观看!
如果觉得《netty搭建简单的文件服务器》对你有帮助,请点赞、收藏,并留下你的观点哦!