在工作中,文件服务器是很常用的,我们经常需要把一些公共资源放到服务器上的某个目录下,通过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>
<!-- https://mvnrepository.com/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>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</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);
@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 {
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;
//是否绑定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) {
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>{
@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());
//引入文件处理的handler
p.addLast("fileServerHandler",fleServerHandler);
}
}
4、filePipeline 添加自定义文件处理handler
/**
* @Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用
* 文件处理handler
*/
@Component
@ChannelHandler.Sharable
public class FileServerHandler extends ChannelInboundHandlerAdapter {
//https://blog.csdn.net/cowbin2012/article/details/85290876
private Logger logger = LoggerFactory.getLogger(FileServerHandler.class);
//文件存放路径
@Value("${netty.file.path:}")
String path;
@Override
public 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() {
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
logger.debug("Transfer complete.");
}
@Override
public 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
@Validated
public 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(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
//必须大于1,实际工作线程数量,这个数量最好根据JVM的系统信息进行配置,这里直接动态获取
@DecimalMin("1")
private Integer workThreads = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
}
7、启动类
@SpringBootApplication
public class NettyApplication {
public static void main(String[] args) {
SpringApplication.run(NettyApplication.class,args);
}
}
8、yml文件
netty:
file:
enabled: true
path: c:\
port: 3456
启动程序,浏览器访问:http://127.0.0.1:3456/,看到如下效果,
是不是这个效果很像我们访问本地磁盘上的文件,我们可以进入某个目录下,尝试下载一个本地文件,可以看到就像下载浏览器上的某个文件一样,
看图片也很方便,
本篇的讲解到此结束,最后,感谢观看!