使用 netty 实现文件上传服务器
代码实现根据官网提供的 example https://github.com/netty/nett…
以及 netty 官网的 api 文档 https://netty.io/4.1/api/inde…
项目地址 https://github.com/1433365571…
1 编写 server 启动类
package server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;
public final class HttpUploadServer {static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));
public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();} else {sslCtx = null;}
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.handler(new LoggingHandler(LogLevel.INFO));
b.childHandler(new HttpUploadServerInitializer(sslCtx));
Channel ch = b.bind(PORT).sync().channel();
System.err.println("Open your web browser and navigate to" +
(SSL ? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();}
}
}
2 绑定 handler
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package server;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.ssl.SslContext;
public class HttpUploadServerInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
public HttpUploadServerInitializer(SslContext sslCtx) {this.sslCtx = sslCtx;}
@Override
public void initChannel(SocketChannel ch) {ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpResponseEncoder());
// Remove the following line if you don't want automatic content compression.
pipeline.addLast(new HttpContentCompressor());
// pipeline.addLast("http-aggregator",
// new HttpObjectAggregator(65536));// 目的是将多个消息转换为单一的 request 或者 response 对象
pipeline.addLast(new HttpUploadServerHandler());
}
}
3 上传处理的 handler
package server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.*;
import io.netty.util.CharsetUtil;
import java.io.File;
import java.io.IOException;
import java.net.URI;
public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private HttpRequest request;
private static final String uploadUrl = "/up";
private static final String fromFileUrl = "/post_multipart";
private static final HttpDataFactory factory =
new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk if size exceed
private HttpPostRequestDecoder decoder;
static {
DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
// on exit (in normal
// exit)
DiskFileUpload.baseDirectory = null; // system temp directory
DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
// exit (in normal exit)
DiskAttribute.baseDirectory = null; // system temp directory
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {if (decoder != null) {decoder.cleanFiles();
}
}
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {if (msg instanceof HttpRequest) {this.request = (HttpRequest) msg;
URI uri = new URI(request.uri());
System.out.println(uri);
urlRoute(ctx, uri.getPath());
}
if (decoder != null) {if (msg instanceof HttpContent) {
// 接收一个新的请求体
decoder.offer((HttpContent) msg);
// 将内存中的数据序列化本地
readHttpDataChunkByChunk();}
if (msg instanceof LastHttpContent) {System.out.println("LastHttpContent");
reset();
writeResponse(ctx, "<h1> 上传成功 </h1>");
}
}
}
// url 路由
private void urlRoute(ChannelHandlerContext ctx, String uri) {StringBuilder urlResponse = new StringBuilder();
// 访问文件上传页面
if (uri.startsWith(uploadUrl)) {urlResponse.append(getUploadResponse());
} else if (uri.startsWith(fromFileUrl)) {decoder = new HttpPostRequestDecoder(factory, request);
return;
} else {urlResponse.append(getHomeResponse());
}
writeResponse(ctx, urlResponse.toString());
}
private void writeResponse(ChannelHandlerContext ctx, String context) {ByteBuf buf = Unpooled.copiedBuffer(context, CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
ctx.channel().writeAndFlush(response);
}
private String getHomeResponse() {return "<h1> welcome home </h1>";}
private String getUploadResponse() {
return "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
"<meta charset=\"UTF-8\">\n" +
"<title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
"\n" +
"<form action=\"http://127.0.0.1:8080/post_multipart\"enctype=\"multipart/form-data\"method=\"POST\">\n" +
"\n" +
"\n" +
"<input type=\"file\"name=" +
"" +"" +
"\"YOU_KEY\">\n" +
"\n" +
"<input type=\"submit\"name=\"send\">\n" +
"\n" +
"</form>\n" +
"\n" +
"</body>\n" +
"</html>";
}
private void readHttpDataChunkByChunk() throws IOException {while (decoder.hasNext()) {InterfaceHttpData data = decoder.next();
if (data != null) {if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) {FileUpload fileUpload = (FileUpload) data;
if (fileUpload.isCompleted()) {fileUpload.isInMemory();// tells if the file is in Memory
// or on File
fileUpload.renameTo(new File(PathUtil.getFileBasePath() + fileUpload.getFilename())); // enable to move into another
// File dest
decoder.removeHttpDataFromClean(fileUpload); //remove
}
}
}
}
}
private void reset() {
request = null;
// destroy the decoder to release all resources
decoder.destroy();
decoder = null;
}
}
4 环境文件
package server;
import java.io.File;
public class PathUtil {private static final ClassLoader classLoader = PathUtil.class.getClassLoader();
public static String getFileBasePath() {String os = System.getProperty("os.name");
String basePath;
if (os.toLowerCase().startsWith("win")) {basePath = "D:/warehouse/";} else {basePath = "/root/upload_source";}
basePath = basePath.replace("/", File.separator);
return basePath;
}
public static String getSourcePath(String name) {return classLoader.getResource(name).getPath();}
}
5 pom 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nettyHttpUploadServer</groupId>
<artifactId>http.upload</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.10.Final</version>
</dependency>
</dependencies>
<!-- <build>-->
<!-- <plugins>-->
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-jar-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <archive>-->
<!-- <manifest>-->
<!-- <addClasspath>true</addClasspath>-->
<!-- <mainClass>server.HttpUploadServer</mainClass>-->
<!-- </manifest>-->
<!-- </archive>-->
<!-- </configuration>-->
<!-- </plugin>-->
<!-- </plugins>-->
<!-- </build>-->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<archive>
<manifest>
<mainClass>server.HttpUploadServer</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>server.HttpUploadServer</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.jolira</groupId>
<artifactId>onejar-maven-plugin</artifactId>
<version>1.4.4</version>
<executions>
<execution>
<goals>
<goal>one-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
6 代码说明
- ChannelHandlerContext 控制数据处理管道 ChannelPipeline 执行流程
- HttpObject 消息处理的接口
- HttpRequest 封装 http 请求头请求协议等
- DefaultFullHttpResponse 构造 http 响应
- httpcontext netty 会将请求体分组处理,获得完整的请求体可以请将 HttpObjectAggregator 放在 ChannelPipeline 中的
- HttpObjectDecoder 之后 此文章解释较详细。
- LastHttpContent 标识 Http 请求结束
- HttpDataFactory 上传文件的处理方式
- HttpPostRequestDecoder post 请求体的解析类
7 访问 http://127.0.0.1:8080/up 上传文件