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

一、简介

HTTP超文本传输协议)协议是建立在TCP传输协议之上的应用层协议,它的发展是万维网协会和Internet工作小组IETF合作的结果。HTTP是一个属于应用层的面向对象的协议。由于其简捷、快速的方式,适用于分布式超媒体信息系统。
由于HTTP协议是目前Web开发的主流协议,基于HTTP的应用非常广泛,因此,掌握HTTP的开发非常重要,本章主要介绍如何基于Netty的HTTP协议栈进行HTTP服务端和客户端开发。由于Netty的HTTP协议栈是基于Netty的NIO通信框架开发的,因此,Netty的HTTP协议也是异步非阻塞的。
使用的netty版本:

		<dependency>
            <groupId>io.netty</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的部分请求消息头列表:

名称(KEY)作用
Accept用于指定客户端接收哪些类型的信息。例如:Accept:image/gif,表名客户端希望接收GIF图像格式的资源
Accept-Charset用于指定客户端接收的字符集
Accept-Encoding指定可接受的内容编码
Accept-Language指定一种自然语言
Authorization主要用于证明客户端是否有权查看某个资源,要求服务器对其进行认证
Host发送请求时,该报头域是必需的,用于指定被请求资源的Internet主机和端口号
User-Agent允许客户端将它的操作系统、浏览器和其他属性告诉服务器
Content-Length请求消息体的长度
Content-Type表示后面的文档属于什么MIME类型。Servlet默认为text/plain ,但通常需要显式的指定为text/html,由于经常要设置Content-Type,因此HTTPServletRequest提供了一个专用的方法setContentType
Connectin连接类型 长连接/短连接

在这里插入图片描述

2.3 HTTP的响应消息(HttpResponse)

处理完HTTP客户端的请求之后,HTTP服务器返回响应消息给客户端,HTTP响应也是由三个部分组成,分别是:状态行、消息报文、响应正文。
状态行的格式为:HTTP-Version Status-Code Reason-Phrase CRLF, 其中 Status-Code 表示服务器返回的响应状态码。状态码由三位数字组成,
第一个数组定义了响应的类别,它有五种可能的取值:

  1. 1xx:指示信息,表示请求已经接受,继续处理;
  2. 2xx:成功,表示请求已经被成功接收、理解、接受;
  3. 3xx:重定向,要完成请求必须进行更进一步的操作;
  4. 4xx:客户端错误,请求语法有错误或者请求无法实现;
  5. 5xx:服务器端错误,服务器未能处理请求;
    常见的状态码:
状态码描述
200OK:客户端请求成功
400Bad Request:客户端请求语法有错误,不能被服务器所理解
401Unauthorized:请求未经授权
403Forbidden : 服务器收到请求,但是拒绝提供服务
404Not Found:请求资源不存在
500Internet Server Error:服务器发生不可预期错误
503Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常。

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

名称作用
Location用于重定向接受者到一个新的位置,Location响应报文域常用于更换域名的时候。
ServerBad Request:包含了服务器用来处理请求的软件信息,与User-Agent请求报文是相对应的。
WWW-Authenticate必须被包含在401(未授权的)响应信息中,客户端收到401消息,并发送Authorization报头域请求服务器对其进行校验,服务端响应报头就该包含该报头域。

三、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 com.lsh.netty.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * @author :LiuShihao
 * @date :Created in 2021/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>() {
                           @Override
                           protected 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 com.lsh.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

/**
 * @author :LiuShihao
 * @date :Created in 2021/6/2 11:55 上午
 * @desc :HttpFile文件服务器处理类
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String url ;
    public HttpFileServerHandler(String url) {
        this.url  = url;
    }
    /**
     * 当服务器接收到消息时,会自动触发 messageReceived方法
     */
    @Override
    protected 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为keepAlive
        if (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() {
            @Override
            public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
                System.out.println("传输完成.");

            }
            @Override
            public 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);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        cause.printStackTrace();
        if(ctx.channel().isActive()){
            sendError(ctx,HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }
    private static final Pattern INSECURE_URI=Pattern.compile(".*[<>&\"].*");

    /**
     *
     *
     * @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 = Pattern.compile("[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协议栈,后续相关的应用层开发和维护将变得非常简单。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liu_Shihao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值