(高级篇 Netty多协议开发和应用)第十章-Http协议开发应用
HTTPC超文本传输协议〉协议是建立在TCP传输协议之上的应用层协议,它的发展是万维网协会和Internet工作小组IET'F合作的结果。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过多年的使用和发展,得到了不断地完善和扩展。
由于HTTP协议是目前Web开发的主流协议,基于HTTP的应用非常广泛,因此,掌握HTTP的开发非常重要,本章将重点介绍如何基于Netty的HTTP协议技进行HTTP服务端和客户端开发。由于Netty的HTTP协议钱是基于Netty的NIO通信框架开发的,因此,Netty的HTTP协议也是异步非阻塞的。
本章主要内容包括:
HTTP协议介绍
NettyHTTP服务端入门开发
HTTP+XML应用开发
HTTP附件处理
10.1 HTTP协议介绍
HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。
HTTP协议的主要特点如下。
1.支持Client/Server模式;
2.简单一一客户向服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体:
3.灵活一 HTTP 允许传输任意类型的数据对象,传输的内容类型由HTTP 消息头中的Content-Type加以标记;
4.无状态一HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要之前的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快,负载较轻。
10.1.1 HTTP协议的URL
HTTPURL (URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息〉的格式如下。
http://host[":"port][abs_path]
其中,http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址:port指定一个端口号,为空则使用默认端口80;abs_pa出指定请求资源的URI,如果URL中没有给出abs_path,那么当它作为请求URI时,必须以/的形式给出,通常这点工作浏览器会自动帮我们完成。
10.1.2 HTTP请求消息CHttpRequest)
HTTP请求自三部分组成,具体如下。
Netty HTTP服务端入门开发
从本节开始我们学习如何使用Netty的HTTP协议栈开发HTTP服务端和客户端应用程序。由于Netty天生是异步事件驱动的架构,因此基于NIOTCP协议找开发的HTTP协议校也是异步非阻塞的。
Netty的HTTP协议核无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用,相比于传统的Tomcat、Jetty等Web容器,它更加轻量和小巧,灵活性和定制性也更好。
10.2.1 HTTP服务端例程场景描述
我们以文件服务器为例学习Netty的HTTP服务端入门开发,例程场景如下:文件服务器使用HTTP协议对外提供服务,当客户端通过浏览器访问文件服务器时,对访问路径进行检查,检查失败时返回HTTP403错误,该页无法访问:如果校验通过,以链接的方式打开当前文件目录,每个目录或者文件都是个超链接,可以递归访问。
如果是目录,可以继续递归访问它下面的子目录或者文件,如果是文件且可读,则可以在浏览器端直接打开,或者通过【目标另存为】下载该文件。
介绍完了样例程序的开发场景,下面我们一起看看如何开发一个基于Netty的HTTP
程序。
10.2..2 HTTP服务端开发
首先看下HTTP文件服务器的启动类是如何实现的。
10-1 HTTP文件服务器启动类HttpFileServer
1 package lqy8_httpFileService_170; 2 3 import io.netty.bootstrap.ServerBootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.EventLoopGroup; 7 import io.netty.channel.nio.NioEventLoopGroup; 8 import io.netty.channel.socket.SocketChannel; 9 import io.netty.channel.socket.nio.NioServerSocketChannel; 10 import io.netty.handler.codec.http.HttpObjectAggregator; 11 import io.netty.handler.codec.http.HttpRequestDecoder; 12 import io.netty.handler.codec.http.HttpResponseEncoder; 13 import io.netty.handler.stream.ChunkedWriteHandler; 14 15 /** 16 * @author lilinfeng 17 * @date 2014年2月14日 18 * @version 1.0 19 */ 20 public class HttpFileServer { 21 //private static final String DEFAULT_URL = "/src/com/phei/netty/"; 22 private static final String DEFAULT_URL = "/src/"; 23 public void run(final int port, final String url) throws Exception { 24 EventLoopGroup bossGroup = new NioEventLoopGroup(); 25 EventLoopGroup workerGroup = new NioEventLoopGroup(); 26 try { 27 ServerBootstrap b = new ServerBootstrap(); 28 b.group(bossGroup, workerGroup) 29 .channel(NioServerSocketChannel.class) 30 .childHandler(new ChannelInitializer<SocketChannel>() { 31 @Override 32 protected void initChannel(SocketChannel ch) 33 throws Exception { 34 ch.pipeline().addLast("http-decoder", 35 new HttpRequestDecoder()); 36 ch.pipeline().addLast("http-aggregator", 37 new HttpObjectAggregator(65536)); 38 ch.pipeline().addLast("http-encoder", 39 new HttpResponseEncoder()); 40 ch.pipeline().addLast("http-chunked", 41 new ChunkedWriteHandler()); 42 ch.pipeline().addLast("fileServerHandler", 43 new HttpFileServerHandler(url)); 44 } 45 }); 46 // ChannelFuture future = b.bind("192.168.1.102", port).sync(); 47 // System.out.println("HTTP文件目录服务器启动,网址是 : " + "http://192.168.1.102:" 48 // + port + url); 49 ChannelFuture future = b.bind("127.0.0.1", port).sync(); 50 System.out.println("HTTP文件目录服务器启动,网址是 : " + "http://127.0.0.1:" 51 + port + url); 52 future.channel().closeFuture().sync(); 53 } finally { 54 bossGroup.shutdownGracefully(); 55 workerGroup.shutdownGracefully(); 56 } 57 } 58 59 public static void main(String[] args) throws Exception { 60 int port = 8080; 61 if (args.length > 0) { 62 try { 63 port = Integer.parseInt(args[0]); 64 } catch (NumberFormatException e) { 65 e.printStackTrace(); 66 } 67 } 68 String url = DEFAULT_URL; 69 if (args.length > 1) 70 url = args[1]; 71 new HttpFileServer().run(port, url); 72 } 73 }
首先我们看main函数,它有两个参数:第一个是端口,第二个是HTTP服务端的URL路径。如果启动的时候没有配置,则使用默认值,默认端口是8080,默认的URL路径是“/src/com/pbei/netty/”
重点关注第34~43行,首先向ChannelPipeline中添加HTTP请求消息解码器,随后,又添加了HttpObjectAggregator 解码器,它的作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,原因是HTTP解码器在每个HTTP 消息中会生成多个消息对象。
(1) HttpRequestIHttpResponse;
(2) HttpContent;
(3) LastHttpContento
第38~39行新增HTTP响应编码器,对HTTP响应消息进行编码;第4041行新增Chunkedhandler,它的主要作用是支持异步发送大的码流〈例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误。
最后添加I-IttpFileServerHandIer,用于文件服务器的业务逻辑处理。下面我们具体看看它是如何实现的。
10-2 HTTP文件服务器 HttpFileServerHandler
1 package lqy8_httpFileService_170; 2 3 import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive; 4 import static io.netty.handler.codec.http.HttpHeaders.setContentLength; 5 import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; 6 import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; 7 import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION; 8 import static io.netty.handler.codec.http.HttpMethod.GET; 9 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; 10 import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; 11 import static io.netty.handler.codec.http.HttpResponseStatus.FOUND; 12 import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; 13 import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; 14 import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; 15 import static io.netty.handler.codec.http.HttpResponseStatus.OK; 16 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; 17 import io.netty.buffer.ByteBuf; 18 import io.netty.buffer.Unpooled; 19 import io.netty.channel.ChannelFuture; 20 import io.netty.channel.ChannelFutureListener; 21 import io.netty.channel.ChannelHandlerContext; 22 import io.netty.channel.ChannelProgressiveFuture; 23 import io.netty.channel.ChannelProgressiveFutureListener; 24 import io.netty.channel.SimpleChannelInboundHandler; 25 import io.netty.handler.codec.http.DefaultFullHttpResponse; 26 import io.netty.handler.codec.http.DefaultHttpResponse; 27 import io.netty.handler.codec.http.FullHttpRequest; 28 import io.netty.handler.codec.http.FullHttpResponse; 29 import io.netty.handler.codec.http.HttpHeaders; 30 import io.netty.handler.codec.http.HttpResponse; 31 import io.netty.handler.codec.http.HttpResponseStatus; 32 import io.netty.handler.codec.http.LastHttpContent; 33 import io.netty.handler.stream.ChunkedFile; 34 import io.netty.util.CharsetUtil; 35 36 import java.io.File; 37 import java.io.FileNotFoundException; 38 import java.io.RandomAccessFile; 39 import java.io.UnsupportedEncodingException; 40 import java.net.URLDecoder; 41 import java.util.regex.Pattern; 42 43 import javax.activation.MimetypesFileTypeMap; 44 45 /** 46 * @author lilinfeng 47 * @date 2014年2月14日 48 * @version 1.0 49 */ 50 public class HttpFileServerHandler extends 51 SimpleChannelInboundHandler<FullHttpRequest> { 52 private final String url; 53 public HttpFileServerHandler(String url) { 54 this.url = url; 55 } 56 @Override 57 public void messageReceived(ChannelHandlerContext ctx,FullHttpRequest request) throws Exception { 58 if (!request.getDecoderResult().isSuccess()) { 59 sendError(ctx, BAD_REQUEST); 60 return; 61 } 62 if (request.getMethod() != GET) { 63 sendError(ctx, METHOD_NOT_ALLOWED); 64 return; 65 } 66 final String uri = request.getUri(); 67 final String path = sanitizeUri(uri);//分析url 68 if (path == null) { 69 sendError(ctx, FORBIDDEN); 70 return; 71 } 72 File file = new File(path); 73 if (file.isHidden() || !file.exists()) { 74 sendError(ctx, NOT_FOUND); 75 return; 76 } 77 if (file.isDirectory()) { 78 if (uri.endsWith("/")) { 79 sendListing(ctx, file); 80 } else { 81 sendRedirect(ctx, uri + '/'); 82 } 83 return; 84 } 85 if (!file.isFile()) { 86 sendError(ctx, FORBIDDEN); 87 return; 88 } 89 RandomAccessFile randomAccessFile = null; 90 try { 91 randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件 92 } catch (FileNotFoundException fnfe) { 93 sendError(ctx, NOT_FOUND); 94 return; 95 } 96 long fileLength = randomAccessFile.length(); 97 HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); 98 setContentLength(response, fileLength); 99 setContentTypeHeader(response, file); 100 if (isKeepAlive(request)) { 101 response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE); 102 } 103 ctx.write(response); 104 ChannelFuture sendFileFuture; 105 sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,fileLength, 8192), ctx.newProgressivePromise()); 106 sendFileFuture.addListener(new ChannelProgressiveFutureListener() { 107 @Override 108 public void operationProgressed(ChannelProgressiveFuture future, 109 long progress, long total) { 110 if (total < 0) { // total unknown 111 System.err.println("Transfer progress: " + progress); 112 } else { 113 System.err.println("Transfer progress: " + progress + " / " 114 + total); 115 } 116 } 117 118 @Override 119 public void operationComplete(ChannelProgressiveFuture future) 120 throws Exception { 121 System.out.println("Transfer complete."); 122 } 123 }); 124 ChannelFuture lastContentFuture = ctx 125 .writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 126 if (!isKeepAlive(request)) { 127 lastContentFuture.addListener(ChannelFutureListener.CLOSE); 128 } 129 } 130 131 @Override 132 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 133 throws Exception { 134 cause.printStackTrace(); 135 if (ctx.channel().isActive()) { 136 sendError(ctx, INTERNAL_SERVER_ERROR); 137 } 138 } 139 private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*"); 140 141 142 143 144 private String sanitizeUri(String uri) { 145 try { 146 uri = URLDecoder.decode(uri, "UTF-8"); 147 } catch (UnsupportedEncodingException e) { 148 try { 149 uri = URLDecoder.decode(uri, "ISO-8859-1"); 150 } catch (UnsupportedEncodingException e1) { 151 throw new Error(); 152 } 153 } 154 if (!uri.startsWith(url)) { 155 return null; 156 } 157 if (!uri.startsWith("/")) { 158 return null; 159 } 160 uri = uri.replace('/', File.separatorChar); 161 if (uri.contains(File.separator + '.') 162 || uri.contains('.' + File.separator) || uri.startsWith(".") 163 || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) { 164 return null; 165 } 166 return System.getProperty("user.dir") + File.separator + uri; 167 } 168 private static final Pattern ALLOWED_FILE_NAME = Pattern 169 .compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*"); 170 171 private static void sendListing(ChannelHandlerContext ctx, File dir) { 172 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK); 173 response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); 174 StringBuilder buf = new StringBuilder(); 175 String dirPath = dir.getPath(); 176 buf.append("<!DOCTYPE html>\r\n"); 177 buf.append("<html><head><title>"); 178 buf.append(dirPath); 179 buf.append(" 目录:"); 180 buf.append("</title></head><body>\r\n"); 181 buf.append("<h3>"); 182 buf.append(dirPath).append(" 目录:"); 183 buf.append("</h3>\r\n"); 184 buf.append("<ul>"); 185 buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n"); 186 for (File f : dir.listFiles()) { 187 if (f.isHidden() || !f.canRead()) { 188 continue; 189 } 190 String name = f.getName(); 191 if (!ALLOWED_FILE_NAME.matcher(name).matches()) { 192 continue; 193 } 194 buf.append("<li>链接:<a href=\""); 195 buf.append(name); 196 buf.append("\">"); 197 buf.append(name); 198 buf.append("</a></li>\r\n");} 199 buf.append("</ul></body></html>\r\n"); 200 ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); 201 response.content().writeBytes(buffer); 202 buffer.release(); 203 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 204 } 205 206 private static void sendRedirect(ChannelHandlerContext ctx, String newUri) { 207 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND); 208 response.headers().set(LOCATION, newUri); 209 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 210 } 211 212 private static void sendError(ChannelHandlerContext ctx, 213 HttpResponseStatus status) { 214 FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, 215 status, Unpooled.copiedBuffer("Failure: " + status.toString() 216 + "\r\n", CharsetUtil.UTF_8)); 217 response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); 218 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 219 } 220 221 private static void setContentTypeHeader(HttpResponse response, File file) { 222 MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); 223 response.headers().set(CONTENT_TYPE, 224 mimeTypesMap.getContentType(file.getPath())); 225 } 226 }
首先从消息接入方法看起,
第58~61行首先对HTTP请求消息的解码结果进行判断,如果解码失败,直接构造HTTP400错误返回。
第62~65行对请求行中的方法进行判断,如果不是从浏览器或者表单设置为GET发起的请求(例如POST),则构造HTTP 405错误返回。
第67行对请求URL进行包装,然后对sanitizeUri方法展开分析。
跳到第145行,首先使用JDK的java.net.URLDecoder对URL进行解码,使用UTF-8字符集,解码成功之后对URI进行合法性判断,如果URI与允许访问的URI一致或者是其子目录(文件〉,则校验通过,否则返回空。
第159行将硬编码的文件路径分隔符替换为本地操作系统的文件路径分隔符。第161164对新的URI做二次合法性校验,如果校验失败则直接返回空。最后对文件进行拼接,使用当前运行程序所在的工程目录+URI构造绝对路径返回。
第68~71行,如果构造的URI不合法,贝IJ返回HTTP403错误。
第72行使用新组装的URI路径构造File对象。
第73~76行,如果文件不存在或者是系统隐藏文件,则构造HTTP404异常返回。如果文件是目录,则发送目录的链接给客户端浏览器。下面我们重点分析返回文件链接响应给客户端的代码。
第172行首先创建成功的HTTP 响应消息,随后设置消息头的类型为“text/html:charset=UTF-8”。
第174行用于构造响应消息体,由于需要将响应结果显示在浏览器上,所以采用了HTML 的格式。由于大家对HTML的语法已经非常熟悉,这里不再详细介绍。我们挑重点的代码进行分析,
第185行打印了一个.的链接。
第186~199行用于展示根目录下的所有文件和文件夹,同时使用超链接来标识。
第201行分配对应消息的缓冲对象
第202 行将缓冲区中的响应消息存放到HTTP 应答消息中,然后释放缓冲区,最后调用writeAndFlush将响应消息发送到缓冲区并刷新到Socket.Channel中。
如果用户在浏览器上点击超链接直接打开或者下载文件,
代码会执行第85行,对超链接的文件进行合法性判断,如果不是合法文件,则返回HTTP403错误。校验通过后,
第85~95 行使用随机文件读写类以只读的方式打开文件,如果文件打开失败,则返回HTTP 404错误。
第96行获取文件的长度,构造成功的HTTP应答消息,然后在消息头中设置contentlength和contenttype,判断是否是Keep-Alive,如果是,则在应答消息头中设置Connection为Keep-Alive。
第103行发送响应消息。第105106行通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中。最后为sendFileFuture增加GenericFutureListener,如果发送完成,打印“Transfercomplete.”。如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHt叩Content的EMPTY_LAST_CONTENT发送到缓冲区中,标识所有的消息体已经发送完成,同时调用flush方法将之前在发送缓冲区的消息刷新到SocketChannel中发送给对方。
如果是非Keep田Alive的,最后一包消息发送完成之后,服务端要主动关闭连接。服务端的代码已经介绍完毕F 下面让我们看看运行结果。
10.2.3 Netty HTTP 文件服务器例程运行结果
启动 HTTP 文件服务器 ,通过浏览器 进行访问,运行结果如下
打开浏览器
点击
对比服务器上的源文件,内容完全一致,说明HTTP文件服务器文件下载功能正常。至此,作为入门级的NettyHTTP协议校的应用一 HTTP文件服务器己经开发完毕,
相信通过本节的例程学习,大家已经初步掌握了基于Netty的HTTP服务端应用开发。
下一节,我们将学习目前最流行的HTTP+XML开发。HTTP+XML应用非常广泛,一旦我们掌握了如何在Netty中实现通用的HTTP+XML协议栈,后续相关的应用层开发和维护将会变得非常简单。
10.3 NettyHTTP+XML协议钱开发
由于HTTP协议的通用性,很多异构系统间的通信交互采用HTTP协议,通过HTTP协议承载业务数据进行消息交互,例如非常流行的HTTP+XML或者RESTful+JSON。
在Java领域,最常用的HTTP协议钱就是基于Servlet规范的Tomcat等Web容器,由于谷歌等业界大佬的强力推荐,Jetty等轻量级的Web容器也得到了广泛的应用。但是,很多基于HTTP的应用都是后台应用,HTTP仅仅是承载数据交换的一个通道,是一个载体而不是Web容器,在这种场景下,一般不需要类似Tomcat这样的重量型Web容器。
在网络安全日益严峻的今天,重量级的Web容器由于功能繁杂,会存在很多安全漏洞,典型的如Tomcat。如果你的客户是安全敏感型的,这意味着你需要为Web容器做很多安全加固工作去修补这些漏洞,然而你并没有使用到这些功能,这会带来开发和维护成本的增加。在这种场景下,一个更加轻量级的HTTP协议校是个更好的选择。
本章节将介绍如何利用Netty提供的基础HTTP协议找功能,扩展开发HTTP+XML协议栈。
略
10.4 总结
本章节重点介绍了HTTP协议以及如何使用Netty的HTTP协议校开发基于HTTP的应用程序,最后通过HTTP+XML协议梭的开发向读者展示了如何基于Netty提供的HTTP协议校做二次定制开发。
本章节的HTTP+XML协议枝在实际项目中非常有用,如果读者打算以它为基础进行商业应用,需要补齐一些产品化的能力,例如配置能力、容错能力、更丰富的APio