Netty(四)——HTTP服务端之例 - Go语言中文社区

Netty(四)——HTTP服务端之例


        HTTP(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。我们用的tomcat、jetty、jobss等各种服务器,其实就是一个服务端容器,方便我们直接部署我们应用,让用户通过使用客户端(WEB浏览器、手机H5、app等)借住HTTP协议,来完成信息的提交和获取。这篇文章简单总结一下HTTP协议,然后用Netty来实现一个HTTP服务端实例。

 

        一,HTTP协议,是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统,是基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。下边我们分几个方面来看下。

       1,网络层次模型:HTTP协议是在网络层次模型中的应用层,这里来看下OSI七层模型详解https://blog.csdn.net/yaopeng_2005/article/details/7064869 ),TCP、IP详解学习笔记https://www.cnblogs.com/fengzanfeng/articles/1339347.html )通过这两篇文章来了解一下各层协议。

        2,HTTP协议的主要特点:a,支持Client/Server模式;b,简单,客户端向服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;c,灵活,HTTP允许传输任意类型的数据对象,传输内容类型有HTTP消息头中的Content-Type加以标识;d,无状态,表示HTTP协议对于事物处理没有记忆能力。

       3,HTTP协议的URL格式:http://host[":"port][abs_path]。

       4,HTTP请求消息(HttpRequest)组成:a,HTTP请求行,格式为Method(请求方法)  Request-URL(统一资源标识符)  HTTP-Version(协议版本)  CRLF(回车和换行)。其中Method请求方法有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS等;   b,HTTP消息头 ;    c,HTTP请求正文。我们看下这个格式图:

       5,HTTP响应消息(HttpResponse):也是又三个部分组成:状态行、消息报头、响应正文。其中状态行格式为:HTTP-Version Status-Code Reason-Phrase CRLF。这里说下状态码:其中1**:指示信息,表示请求已接受,继续处理;2**:成功,表示请求已被成功接收、理解、处理;3**:重定向,要完成请求必须进行更进一步的操作;4**:客户端错误,请求有语法错误或者无法实现;5**:服务端错误,服务器在处理请求过程中发生了错误。

       好,这里再推荐两篇文章,写的比较详细,看参考一起学习:简单教程http://www.runoob.com/http/http-tutorial.html)图解传说中的HTTP协议https://blog.csdn.net/agzhchren/article/details/79173491)

 

       二,简单小结了Http协议后,我们来看下如何通过Netty来实现HTTP服务端的开发,这样我们就可以直接通过WEB浏览器或其它客户端,直接调用我们的程序了。由于Netty天生是异步事件驱动的架构,因此基于NIO TCP协议栈开发的HTTP协议栈也是异步非阻塞的。下边来看个读取服务器用户目录下的清单的例子。里边写了相关注释:

/**
 * @author liujiahan
 * @Title: HttpFileServer
 * @Copyright: Copyright (c) 2018
 * @Description: 基于Netty开发的HTTP Server端
 * @Created on 2018/10/27
 * @ModifiedBy:
 */
public class HttpFileServer {


    private static final String DEFAULT_URL = "/src/main/java/com/ljh/netty/";

    /**
     * 入口方法,Netty编写服务端的基本流程
     * @param port 端口号
     * @param url 访问地址
     */
    public void run(final int port, final String url) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    //1,http请求消息解码器
                    ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                    //2,HttpObjectAggregator将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
                    ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                    //3,http响应编码器
                    ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                    //4,ChunkedWriteHandler 支持异步发送大的码流
                    ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                    //5,业务处理
                    ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
                }
            });
            ChannelFuture future = b.bind("localhost", port).sync();
            System.out.println("http文件目录服务器启动,网址是:" + "http://localhost:" + port + url);
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    public static void main(String[] args) {
        int port = 8091;
        if (args != null && args.length > 0) {
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {

            }
        }

        String url = DEFAULT_URL;
        if (args.length > 1) {
            url = args[1];
        }
        new HttpFileServer().run(port, url);

    }

}




/**
 * @author liujiahan
 * @Title: HttpFileServerHandler
 * @Copyright: Copyright (c) 2018
 * @Description: 具体的业务处理流程
 * @Created on 2018/10/27
 * @ModifiedBy:
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private String url;

    public HttpFileServerHandler(String url) {
        this.url = url;
    }

    /**
     * 1,接受到消息后的处理过程
     * @param ctx
     * @param request
     * @throws Exception
     */
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        //验证解码
        if (!request.decoderResult().isSuccess()) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        //验证请求方法
        if (request.method() != HttpMethod.GET) {
            sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
            return;
        }
        //uri地址合法的验证
        final String uri = request.uri();
        final String path = sanitizeUri(uri);
        if (path == null) {
            sendError(ctx, HttpResponseStatus.FORBIDDEN);
            return;
        }

        //file地址的验证
        File file = new File(path);
        if (file.isHidden() || !file.exists()) {
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        //如果是个文件夹的处理内容,sendlist下文件夹下的内容
        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 fnfd) {
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        //如果直接打开或者下载文件,获取文件的长度,构造HTTP消息
        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        HttpHeaderUtil.setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        //将应答header设置为keep-alive
        if (HttpHeaderUtil.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }

        ctx.write(response);
        ChannelFuture sendFileFuture = null;
        //将文件写到发送缓存区
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        //添加ChannelProgressiveFutureListener 监听,operationProgressed、operationComplete两个方法
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                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");
            }
        });

        //如果使用chunked编码,最后需要发送一个编码结束的空消息体,表示结束
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if (!HttpHeaderUtil.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }

    }


    /**
     * 2,异常处理
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /*************************************下边为封装的工具方法**************************************/


    /**
     * 不安全URI的正则限制
     */
    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&"].*");

    /**
     * 审核Uri
     *
     * @param uri
     * @return
     */
    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\.]*");

    /**
     * 发送相应的信息
     *
     * @param ctx
     * @param dir
     */
    private static void sendListing(ChannelHandlerContext ctx, File dir) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");

        String dirPath = dir.getPath();
        //拼装响应体的信息
        StringBuilder builder = new StringBuilder();

        builder.append("<!DOCTYPE html>rn");
        builder.append("<html><head><title>");
        builder.append(dirPath);
        builder.append("目录:");
        builder.append("</title></head><body>rn");

        builder.append("<h3>");
        builder.append(dirPath).append(" 目录:");
        builder.append("</h3>rn");
        builder.append("<ul>");
        builder.append("<li>链接:<a href=" ../")..</a></li>rn");
        for (File f : dir.listFiles()) {
            if (f.isHidden() || !f.canRead()) {
                continue;
            }
            String name = f.getName();
            if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                continue;
            }

            builder.append("<li>链接:<a href="");
            builder.append(name);
            builder.append("">");
            builder.append(name);
            builder.append("</a></li>rn");
        }
        builder.append("</ul></body></html>rn");

        ByteBuf buffer = Unpooled.copiedBuffer(builder, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);

    }

    /**
     * 重定向
     *
     * @param ctx
     * @param newUri
     */
    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);
    }

    /**
     * 返回错误信息
     * @param ctx
     * @param status
     */
    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure:" + status.toString() + "rn", CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);

    }

    /**
     * 设置ContentType
     * @param response
     * @param file
     */
    private static void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath()));
    }

}

        好,一个简单的Netty开发的HTTP协议的服务端就这样了。只有不断的使用的,不断的学习,不断的理解,不断的反复,很多东西才会有恍然大悟的感觉,继续学习。。。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/liujiahan629629/article/details/83479965
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-18 23:32:45
  • 阅读 ( 1161 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢