900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Netty权威指南——HTTP协议开发应用(HTTP文件服务器)

Netty权威指南——HTTP协议开发应用(HTTP文件服务器)

时间:2022-06-19 19:20:48

相关推荐

Netty权威指南——HTTP协议开发应用(HTTP文件服务器)

目录

一、简介二、HTTP协议介绍2.1 HTTP协议的URL2.2 HTTP请求消息(HttpRequest)2.3 HTTP的响应消息(HttpResponse)三、Netty HTTP 服务端入门开发3.1 HTTP服务端例程场景描述3.2 HTTP文件服务器启动类3.3 HTTP文件服务器处理类3.4 文件服务器效果

一、简介

HTTP超文本传输协议)协议是建立在TCP传输协议之上的应用层协议,它的发展是万维网协会和Internet工作小组IETF合作的结果。HTTP是一个属于应用层的面向对象的协议。由于其简捷、快速的方式,适用于分布式超媒体信息系统。

由于HTTP协议是目前Web开发的主流协议,基于HTTP的应用非常广泛,因此,掌握HTTP的开发非常重要,本章主要介绍如何基于Netty的HTTP协议栈进行HTTP服务端和客户端开发。由于Netty的HTTP协议栈是基于Netty的NIO通信框架开发的,因此,Netty的HTTP协议也是异步非阻塞的。

使用的netty版本:

<dependency><groupId>ty</groupId><artifactId>netty-all</artifactId><version>5.0.0.Alpha2</version></dependency>

二、HTTP协议介绍

HTTP协议的主要特点如下:

支持Client/Server模式;简单——客户想服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;灵活——HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type加以标记;无状态——HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆功能。缺少状态意味着如果后续处理又需要之前的信息,则它必须重传,这样可能会导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就比较快,负载较轻。

2.1 HTTP协议的URL

HTTP URL(URL是一种特殊类型的URI,包含了用于查找某个资源的足够信息)格式如下:

http://host[":"port][abs_path]

其中http表示要通过HTTP协议来定位网络资源;

host表示合法的Internet主机域名或者IP地址;

port指定一个端口号,为空则使用默认端口80;

abs_path指定请求资源的URI,如果URL中没有给出abs_path,那么当它作为请求URI时,必须以"/"的形式给出,通常这一点工作浏览器会自动帮我们完成。

2.2 HTTP请求消息(HttpRequest)

HTTP请求由三部分组成,具体如下:

HTTP 请求头;HTTP 请求行;HTTP 请求正文;

请求头以一个方法符开头,以空格分开,后面跟着请求的URI和协议的版本,格式为:Method Request-URI HTTP-Version CRLF

其中Method表示请求方法,请求方法有很多种:

GET:获取或查询资源,是幂等的,请求的数据会附在URL后面,以分割URL和传输数据,多个参数用&拼接,传输的数据长度会有限制;POST:提交数据,一般用于更新资源,提交的数据直接放在HTTP消息的包体中,传输的数据长度不会收到限制,POST的安全性要比GET高,PUT:请求服务器存储一个资源;DELETE:删除资源;HEAD:请求获取资源响应消息报头;TRACK:请求服务器回送收到的请求信息,主要用于测试或诊断;CONNECT:保留将来使用;OPTIONS:请求查询服务器性能,或者查询与资源相关的选项和需求;

常用的RESTFUL风格进行增删改查的就是前四种请求方法。

Request-URI是一个统一资源标识符。

HTTP-Version表示请求的HTTP协议的版本。

CRLF表示回车和换行(除了作为结尾的CRLF之外,不允许出现单独的CR或者LF字符)。

HTTP的部分请求消息头列表:

2.3 HTTP的响应消息(HttpResponse)

处理完HTTP客户端的请求之后,HTTP服务器返回响应消息给客户端,HTTP响应也是由三个部分组成,分别是:状态行、消息报文、响应正文。

状态行的格式为:HTTP-Version Status-Code Reason-Phrase CRLF, 其中Status-Code表示服务器返回的响应状态码。状态码由三位数字组成,

第一个数组定义了响应的类别,它有五种可能的取值:

1xx:指示信息,表示请求已经接受,继续处理;2xx:成功,表示请求已经被成功接收、理解、接受;3xx:重定向,要完成请求必须进行更进一步的操作;4xx:客户端错误,请求语法有错误或者请求无法实现;5xx:服务器端错误,服务器未能处理请求;

常见的状态码:

响应报头允许服务器传递不能放在状态行中中的附加响应信息,以及关于服务器的信息和Request—URI所标识的资源进行下一步的访问信息,常用的响应报头如下:

三、Netty HTTP 服务端入门开发

简单介绍完HTTP协议,现在开始学习如何使用 Netty 的 HTTP协议栈开发HTTP服务端和客户端应用程序。由于Netty天生是异步事件驱动的结构,因此基于NIO TCP 协议栈开发的 HTTP 协议栈也是异步非阻塞的。

Netty 的 HTTP 协议栈无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用,相比于Tomcat、Jetty、等Web容器,它更加轻量和小巧。灵活性和定制性也更好。

3.1 HTTP服务端例程场景描述

我们使用文件服务器为例学习Netty的HTTP服务端入门开发,例程场景如下:

文件服务器使用HTTP协议对外提供服务,当客户端通过浏览器访问文件服务器时,对访问路径进行检查,检查失败时返回HTTP403错误,该网页无法访问;如果校验通过,以链接的方式打开当前文件目录,每个目录或者文件都是超链接,可以递归访问。

如果是目录,可以继续递归访问它下面的子目录或者文件,如果是文件且可读,则可以在浏览器端直接打开,或者通过「目标另存为」下载该文件。

3.2 HTTP文件服务器启动类

package ty.http;import ty.bootstrap.ServerBootstrap;import ty.channel.ChannelFuture;import ty.channel.ChannelInitializer;import ty.channel.nio.NioEventLoopGroup;import ty.channel.socket.SocketChannel;import ty.channel.socket.nio.NioServerSocketChannel;import ty.handler.codec.http.HttpObjectAggregator;import ty.handler.codec.http.HttpRequestDecoder;import ty.handler.codec.http.HttpResponseEncoder;import ty.handler.logging.LogLevel;import ty.handler.logging.LoggingHandler;import ty.handler.stream.ChunkedWriteHandler;/*** @author :LiuShihao* @date :Created in /6/2 9:10 上午* @desc :HTTP 服务端开发*/public class HttpFileServer {private String ipAddress = "127.0.0.1";private static final String DEFAULT_URL = "/src/com/exp/netty/";public void run(final int port,final String url) throws Exception{NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {//想ChannelPipeline中添加HTTP请求消息解码器ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());//添加HttpObjectAggregator解码器,作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,// 原因是HTTP解码器在每个HTTP消息中会生成多个消息对象。ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));//增加HTTP响应编码器,对HTTP响应消息进行编码ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());//添加Chunked handler 作用是支持异步发送大的码流(例如文件传输),但不占用过多的内存,防止发生Java内存溢出错误ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());//最后添加HTTPFileServerHandler 用于文件服务器的业务逻辑处理ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));}});ChannelFuture future = b.bind(ipAddress, port).sync();System.out.println("HTTP 文 件 目 录 服 务 器 启 动 ,网 址 是 :http://"+ipAddress+":"+port+url);future.channel().closeFuture().sync();}catch (Exception e){System.out.println(e.getMessage());}finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) throws Exception {int port = 8080;String url = DEFAULT_URL;new HttpFileServer().run(port,url);}}

3.3 HTTP文件服务器处理类

package ty.http;import ty.buffer.ByteBuf;import ty.buffer.Unpooled;import ty.channel.*;import ty.handler.codec.http.*;import ty.handler.stream.ChunkedFile;import ty.util.CharsetUtil;import javax.activation.MimetypesFileTypeMap;import java.io.File;import java.io.FileNotFoundException;import java.io.RandomAccessFile;import java.io.UnsupportedEncodingException;import .URLDecoder;import java.util.regex.Pattern;/*** @author :LiuShihao* @date :Created in /6/2 11:55 上午* @desc :HttpFile文件服务器处理类*/public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {private final String url ;public HttpFileServerHandler(String url) {this.url = url;}/*** 当服务器接收到消息时,会自动触发 messageReceived方法*/@Overrideprotected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {// 5.0.0 request.getDecoderRequest() 已经被弃用//对HTTP请求消息的解码结果进行判断,如果解码失败则返回400错误if (!request.decoderResult().isSuccess()) {System.out.println("解码失败返回400");sendError(ctx, HttpResponseStatus.BAD_REQUEST);return;}//对请求方法进行判断,如果不是从浏览器或者表单设置为GET发起的请求(例如POST),则返回405错误if (request.method() != HttpMethod.GET) {sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);System.out.println("请求方式不是GET,返回405");return;}//如果URI不合法 返回403错误final String uri = request.uri();System.out.println("request.uri : " + uri);final String path = sanitizeUri(uri);System.out.println("path : " + path);if (path == null) {sendError(ctx, HttpResponseStatus.FORBIDDEN);System.out.println("URI不合法 返回403错误");return;}//如果文件不存在或者是系统隐藏文件 则返回404错误File file = new File(path);if (file.isHidden() || !file.exists()) {sendError(ctx, HttpResponseStatus.NOT_FOUND);System.out.println("文件不存在或者是系统隐藏文件 则返回404错误");return;}if (file.isDirectory()) {if (uri.endsWith("/")) {//返回sendListing(ctx, file);} else {//重定向sendRedirect(ctx, uri + "/");}return;}//判断文件合法性if (!file.isFile()) {sendError(ctx, HttpResponseStatus.FORBIDDEN);return;}RandomAccessFile randomAccessFile = null;try {//以只读方式打开文件randomAccessFile = new RandomAccessFile(file, "r");} catch (FileNotFoundException e) {sendError(ctx, HttpResponseStatus.NOT_FOUND);return;}//获取文件的长度构造成功的HTTP应答消息long fileLength = randomAccessFile.length();DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);HttpHeaderUtil.setContentLength(response, fileLength);setContentTypeHeader(response, file);//判断是否是keepAlive,如果是就在响应头中设置CONNECTION为keepAliveif (HttpHeaderUtil.isKeepAlive(request)) {response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);}ctx.write(response);ChannelFuture sendFileFuture;//通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());//为sendFileFuture添加监听器,如果发送完成打印发送完成的日志sendFileFuture.addListener(new ChannelProgressiveFutureListener() {@Overridepublic void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {System.out.println("传输完成.");}@Overridepublic void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {if (total < 0) {System.err.println("传输进度 : " + progress);} else {System.err.println("传输进度: " + progress + "/" + total);}}});//如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中,//来标示所有的消息体已经发送完成,同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);//如果是非keepAlive的,最后一包消息发送完成后,服务端要主动断开连接if (!HttpHeaderUtil.isKeepAlive(request)) {lastContentFuture.addListener(ChannelFutureListener.CLOSE);}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){cause.printStackTrace();if(ctx.channel().isActive()){sendError(ctx,HttpResponseStatus.INTERNAL_SERVER_ERROR);}}private static final Pattern INSECURE_URI=pile(".*[<>&\"].*");/***** @param uri* @return*/private String sanitizeUri(String uri){//对URL进行解码 解码成功后对URI进行合法性判断 如果URI与允许访问的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();}}//解码成功后对uri进行合法性判断,避免访问无权限的目录if(!uri.startsWith("/src/com/exp/netty")){System.out.println("uri不是以/src/com/exp/netty开头的,uri不合法!");return null;}if(!uri.startsWith("/")){System.out.println("uri不是以/开头的");return null;}//将硬编码的文件路径分隔符替换为本地操作系统的文件路径分割符uri = uri.replace('/',File.separatorChar);//对新的URI做二次合法性校验,如果校验失败则直接返回空if (uri.contains(File.separator+'.')|| uri.contains('.'+File.separator) || uri.startsWith(".")|| uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()){System.out.println("新的URI做二次合法性校验失败则直接返回空");return null;}//最后对文件进行拼接,使用当前运行程序所在的工作目录 + URI 构造绝对路径返回System.out.println("最后对文件进行拼接:"+System.getProperty("user.dir") + File.separator + uri);return System.getProperty("user.dir") + File.separator + uri;}private static final Pattern ALLOWED_FILE_NAME = pile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");/*** 发送目录的链接到客户端浏览器* @param ctx* @param dir*/private static void sendListing(ChannelHandlerContext ctx,File dir){//创建成功的http响应消息FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);//设置消息头的类型是html文件,不要设置为text/plain,客户端会当做文本解析response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");//构造返回的html页面内容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");System.out.println("buf :" + buf);//分配消息缓冲对象ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);//将缓冲区的内容写入响应对象,并释放缓冲区response.content().writeBytes(buffer);buffer.release();//将响应消息发送到缓冲区并刷新到SocketChannel中ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);}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);}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);}private static void setContentTypeHeader(HttpResponse response,File file){MimetypesFileTypeMap mimetypesTypeMap=new MimetypesFileTypeMap();response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimetypesTypeMap.getContentType(file.getPath()));}}

3.4 文件服务器效果

右键下载:

至此,作为入门级的Netty HTTP 协议栈的应用——HTTP文件服务器已经开发完毕,下一章我们学习目前最流行的 HTTP+XML 开发。HTTP+XML应用非常广泛,一旦我们掌握了如何在Netty中实现通用的 HTTP + XML协议栈,后续相关的应用层开发和维护将变得非常简单。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。