Netty-开发http服务器
简介
Netty的HTTP协议栈无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用,相比于传统的Tomcat、Jetty等Web容器,它更加轻量和小巧,灵活性和定制性也更好。
我们以文件服务器为例学习Netty的HTTP服务端开发,例程场景如下:文件服务器使用HTTP协议对外提供服务,当客户端通过浏览器访问文件服务器时,对访问路径进行检查,检查失败时返回HTTP 403错误,该页无法访问;如果校验通过,以链接的方式打开当前文件目录,每个目录或者文件都是个超链接,可以递归访问。如果是目录,可以继续递归访问它下面的子目录或者文件,如果是文件且可读,则可以在浏览器端直接打开,或者通过[目标另存为]下载该文件。
开始Coding~
1、新建maven项目,引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>
2、创建服务端启动代码
public class FileServer {
// 文件访问根路径
private final String URL="/src/main/java/com/fy/protobuf/";
public void bind(int port) {
// 处理连接请求
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 处理IO事件
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(bossGroup, workerGroup)
// 指定socket类型
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
// 向IO处理事件组中添加处理器,实际上这就像是一条处理消息的流水线
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
/*
首先向ChannelPipeline中添加HTTP请求消息解码器,随后又添加了HttpObjectAggregator解码器,
它的作用是将多个消息转换为单一的FulHttpRequest或者FullHttpResponse,原因是HTTP解码器在每
个HTTP消息中会生成多个消息对象。
(1) HttpRequest / HttpResponse;
(2) HttpContent;
(3) LastHttpContent.
*/
socketChannel.pipeline()
.addLast(new HttpRequestDecoder())
.addLast(new HttpObjectAggregator(65536))
//新增HTTP响应编码器,对HTTP响应消息进行编码;
.addLast(new HttpResponseEncoder())
//新增Chunked handler它的主要作用是支持异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误
.addLast(new ChunkedWriteHandler())
//逻辑处理
.addLast(new HttpFileServerHandler(URL));
}
});
// 绑定端口同步等待启动成功
ChannelFuture sync = bootstrap.bind(8080).sync();
// 等待服务监听端口关闭
sync.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
3、创建服务端消息处理代码
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
// 这里继承的是SimpleChannelInboundHandler,指定处理消息类型
private final String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 判断消息解码是否成功
if (!request.getDecoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
// 判断消息类型
if (request.getMethod() != GET) {
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
final String uri = request.getUri();
// 路径的处理与安全过滤
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, NOT_FOUND);
return;
}
// 如果是目录则展示文件列表
if (file.isDirectory()) {
if (uri.endsWith("/")) {
sendListing(ctx, file);
} else {
sendRedirect(ctx, uri + '/');
}
return;
}
if (!file.isFile()) {
sendError(ctx, FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile = null;
try {
// 以只读的方式打开文件
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException fnfe) {
sendError(ctx, NOT_FOUND);
return;
}
long fileLength = randomAccessFile.length();
//http响应
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
//设置消息长度
HttpUtil.setContentLength(response, fileLength);
//设置消息类型
setContentTypeHeader(response, file);
if (HttpUtil.isKeepAlive(request)) {
HttpUtil.setKeepAlive(response, true);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
if (total < 0) {
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
});
// http消息结束标记
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, INTERNAL_SERVER_ERROR);
}
}
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String sanitizeUri(String uri) {
try {
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException e1) {
throw new Error();
}
}
if (!uri.startsWith(url)) {
return null;
}
if (!uri.startsWith("/")) {
return null;
}
uri = uri.replace('/', File.separatorChar);
if (uri.contains(File.separator + '.')
|| uri.contains('.' + File.separator) || uri.startsWith(".")
|| uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
return null;
}
return System.getProperty("user.dir") + File.separator + uri;
}
private static final Pattern ALLOWED_FILE_NAME = Pattern
.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
private static void sendListing(ChannelHandlerContext ctx, File dir) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
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);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx,
HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
status, Unpooled.copiedBuffer("Failure: " + status.toString()
+ "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
}
}
4、启动服务器
public class Server {
public static void main(String[] args) throws Exception {
new FileServer().bind(8080);
}
}
打开浏览器测试
可以看到服务器展示目录下的所有文件了。接下来下载一个文件看看。
文件正常下载。再试试预览txt文件。
预览正常。
GitHub服务端地址:https://github.com/GoodBoy2333/netty-server-maven.git