Netty实现Http高性能服务器


浅谈HTTP Method

要经过netty实现HTTP服务器(或者客户端),首先你要了解HTTP协议。html

HTTP在客户端 - 服务器计算模型中用做请求 - 响应协议。java

例如,web浏览器能够是客户端,而且在托管网站的计算机上运行的应用程序能够是服务器。 客户端向服务器提交HTTP请求消息。git

服务器提供诸如HTML文件和其余内容之类的资源,或表明客户端执行其余功能,向客户端返回响应消息。 响应包含有关请求的完成状态信息,而且还能够在其消息正文中包含所请求的内容。github

什么是HTTP方法?

有写过网页表单的人必定对GET与POST不陌生,但你了解什么是GETPOST吗!?现今的网页设计工具至关的发达,甚至不须要接触HTML语法就能完成一个规模不小的网站,渐渐地不少人都忘记了HTTP底层的实做原理,形成在发生错误的状况下没法正确进行侦错。web

早期在撰写HTML 表单语法时,都会写到如下的写法,然而大部分的软件工程师都会采用POST 进行表单传送。json

<form action="" method="POST/GET">
</form>
复制代码

然而在咱们的网页程序中要获取表单的变数只须要调用系统已经封装好的方法便可,像是PHP使用$_REQUEST、JAVA使用getParameter()、ASP使用Request.Form()这些方法等等。 由上述的方法看来,彷佛用POST或GET好像不是很重要。许多Web工程师对于表单method用法的记忆为"POST能够传送比较多的资料"、"表单传送档案的时候要使用POST"、"POST比GET安全"等等奇怪的概念。数组

其实使用POST 或GET 实际上是有差异的,咱们先说明一下HTTP Method,在HTTP 1.1 的版本中定义了八种Method (方法),以下所示:浏览器

  • OPTIONS安全

  • GETbash

  • HEAD

  • POST

  • PUT

  • DELETE

  • TRACE

  • CONNECT

天阿!这些方法看起来真是陌生。而咱们使用的表单只用了其中两个方法(GET/POST),其余的方法确实不多用到,可是在RESTful 的设计架构中就会使用到更多的Method 来简化设计。

GET与POST方法

先举个例子,若是HTTP 表明如今咱们现实生活中寄信的机制。

:speaker:那么信封的撰写格式就是HTTP。咱们姑且将信封外的内容称为http-header,信封内的书信称为message-body,那么HTTP Method 就是你要告诉邮差的寄信规则。

假设GET 表示信封内不得装信件的寄送方式,如同是明信片同样,你能够把要传递的资讯写在信封(http-header)上,写满为止,价格比较便宜。然而POST 就是信封内有装信件的寄送方式(信封有内容物),不但信封能够写东西,信封内(message-body) 还能够置入你想要寄送的资料或档案,价格较贵。

使用GET 的时候咱们直接将要传送的资料以Query String(一种Key/Vaule的编码方式)加在咱们要寄送的地址(URL)后面,而后交给邮差传送。

使用POST 的时候则是将寄送地址(URL)写在信封上,另外将要传送的资料写在另外一张信纸后,将信纸放到信封里面,交给邮差传送。

GET方法

接着我来介绍一下实际的运做状况:

咱们先来看看GET 怎么传送资料的,当咱们送出一个GET 表单时,以下范例:

<form method="get" action="">
<input type="text" name="id" />
<input type="submit" />
</form>
复制代码

当表单Submit 以后浏览器的网址就变成"xxx.toright.com/?id=010101",浏览器会自动将表单内容转为Query String 加在URL 进行连线。

这时后来看一下HTTP Request 封包的内容:

GET /?id=010101 HTTP/1.1
Host: xxx.toright.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 GTB7.1 ( .NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: UTF-8,*
Keep-Alive: 115
Connection: keep-alive
复制代码

在HTTP GET Method 中是不容许在message-body 中传递资料的,由于是GET 嘛,就是要取资料的意思。

从浏览器的网址列就能够看见咱们表单要传送的资料,如果要传送密码岂不是"一览无遗".......这就是你们常提到安全性问题。

POST方法

再来看看POST 传送资料

<form method="post" action="">
<input type="text" name="id" />
<input type="submit" />
</form>
复制代码

网址列没有变化,那咱们来看一下HTTP Request 封包的内容:

POST / HTTP/1.1
Host: xxx.toright.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 GTB7.1 ( .NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: UTF-8,*
Keep-Alive: 115
Connection: keep-alive
 
Content-Type: application/x-www-form-urlencoded
</code><code>Content-Length: 9
id=020202
复制代码

看出个因此然了吗?原来POST 是将表单资料放在message-body 进行传送,在不偷看封包的状况下彷佛安全一些些.......:yellow_heart: 。此外在传送档案的时候会使用到multi-part 编码,将档案与其余的表单栏位一并放在message-body 中进行传送。这就是GET 与POST 发送表单的差别啰。

Netty HTTP编解码

要经过 Netty 处理 HTTP 请求,须要先进行编解码。

NettyHTTP编解码器

public class HttpHelloWorldServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        /**
         * 或者使用HttpRequestDecoder & HttpResponseEncoder
         */
        p.addLast(new HttpServerCodec());
        /**
         * 在处理POST消息体时须要加上
         */
        p.addLast(new HttpObjectAggregator(1024*1024));
        p.addLast(new HttpServerExpectContinueHandler());
        p.addLast(new HttpHelloWorldServerHandler());
    }
}
复制代码
  • 第 8 行:调用**#new HttpServerCodec()**方法,编解码器支持部分 HTTP 请求解析,好比 HTTP GET请求所传递的参数是包含在 uri 中的,所以经过 HttpRequest 既能解析出请求参数。
    • HttpRequestDecoder 即把 ByteBuf 解码到 HttpRequest 和 HttpContent。
    • HttpResponseEncoder 即把 HttpResponse 或 HttpContent 编码到 ByteBuf。
    • HttpServerCodec 即 HttpRequestDecoder 和 HttpResponseEncoder 的结合。

可是,对于 HTTP POST 请求,参数信息是放在 message body 中的(对应于 netty 来讲就是 HttpMessage),因此以上编解码器并不能彻底解析 HTTP POST请求。

这种状况该怎么办呢?别慌,netty 提供了一个 handler 来处理。

  • 第 12 行:调用**#new HttpObjectAggregator(1024*1024)**方法,即经过它能够把 HttpMessage 和 HttpContent 聚合成一个 FullHttpRequest 或者 FullHttpResponse (取决因而处理请求仍是响应),并且它还能够帮助你在解码时忽略是否为“块”传输方式。

    所以,在解析 HTTP POST 请求时,请务必在 ChannelPipeline 中加上 HttpObjectAggregator。(具体细节请自行查阅代码)

  • 第13行: 这个方法的做用是: http 100-continue用于客户端在发送POST数据给服务器前,征询服务器状况,看服务器是否处理POST的数据,若是不处理,客户端则不上传POST数据,若是处理,则POST上传数据。在现实应用中,经过在POST大数据时,才会使用100-continue协议

HTTP 响应消息的实现

咱们把 Java 对象根据HTTP协议封装成二进制数据包的过程成为编码,而把从二进制数据包中解析出 Java 对象的过程成为解码,在学习如何使用 Netty 进行HTTP协议的编解码以前,咱们先来定义一下客户端与服务端通讯的 Java 对象。

Java 对象

咱们以下定义通讯过程当中的 Java 对象

@Data
public class User {
    private String userName;

    private String method;

    private Date date;
}
复制代码
  1. 以上是通讯过程当中 Java 对象的抽象类,能够看到,咱们定义了一个用户名(默认值为 sanshengshui )以及一个http请求的方法和当前时间日期。
  2. @Data 注解由 lombok 提供,它会自动帮咱们生产 getter/setter 方法,减小大量重复代码,推荐使用

Java 对象定义完成以后,接下来咱们就须要定义一种规则,如何把一个 Java 对象转换成二进制数据,这个规则叫作 Java 对象的序列化。

序列化

咱们以下定义序列化接口

/**
 * 序列化接口类
 */
public interface Serializer {
    /**
     * java 对象转换成二进制
     */
    byte[] serialize(Object object);

    /**
     * 二进制转换成 java 对象
     */
    <T> T deserialize(Class<T> clazz, byte[] bytes);
}
复制代码

序列化接口有二个方法,serialize() 将 Java 对象转换成字节数组,deserialize() 将字节数组转换成某种类型的 Java 对象,在工程中,咱们使用最简单的 json 序列化方式,使用阿里巴巴的 fastjson 做为序列化框架。

public class JSONSerializer implements Serializer {
    @Override
    public byte[] serialize(Object object) {
        return JSON.toJSONBytes(object);
    }

    @Override
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return JSON.parseObject(bytes,clazz);
    }
}
复制代码

编码

User user = new User();
        user.setUserName("sanshengshui");
        user.setDate(new Date());
        user.setMethod("get");
        JSONSerializer jsonSerializer = new JSONSerializer();
        //将Java对象序列化成为二级制数据包
        byte[] content = jsonSerializer.serialize(user);
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(content));
        response.headers().set(CONTENT_TYPE, "text/plain");
        response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

        boolean keepAlive = HttpUtil.isKeepAlive(request);
        if (!keepAlive) {
            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
           } else {
            response.headers().set(CONNECTION, KEEP_ALIVE);
            ctx.write(response);
           }
复制代码

HTTP GET解析实践

上面提到过,HTTP GET 请求的参数是包含在 uri 中的,可经过如下方式解析出 uri:

HttpRequest request = (HttpRequest) msg;
String uri = request.uri();
复制代码

特别注意的是,用浏览器发起 HTTP 请求时,经常会被 uri = "/favicon.ico" 所干扰,所以最好对其特殊处理:

if(uri.equals(FAVICON_ICO)){
    return;
}
复制代码

接下来就是解析 uri 了。这里须要用到 QueryStringDecoder

Splits an HTTP query string into a path string and key-value parameter pairs.
This decoder is for one time use only.  Create a new instance for each URI:
 
QueryStringDecoder decoder = new QueryStringDecoder("/hello?recipient=world&x=1;y=2");
assert decoder.getPath().equals("/hello");
assert decoder.getParameters().get("recipient").get(0).equals("world");
assert decoder.getParameters().get("x").get(0).equals("1");
assert decoder.getParameters().get("y").get(0).equals("2");
 
This decoder can also decode the content of an HTTP POST request whose
content type is application/x-www-form-urlencoded:
 
QueryStringDecoder decoder = new QueryStringDecoder("recipient=world&x=1;y=2", false);
复制代码

从上面的描述能够看出,QueryStringDecoder 的做用就是把 HTTP uri 分割成 path 和 key-value 参数对,也能够用来解码 Content-Type = "application/x-www-form-urlencoded" 的 HTTP POST。特别注意的是,该 decoder 仅能使用一次。

解析代码以下:

String uri = request.uri();
HttpMethod method = request.method();
if(method.equals(HttpMethod.GET)){
&emsp;&emsp;QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
&emsp;&emsp;Map<String, List<String>> uriAttributes = queryDecoder.parameters();
&emsp;&emsp;//此处仅打印请求参数(你能够根据业务需求自定义处理)
&emsp;&emsp;for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
&emsp;&emsp;&emsp;&emsp;for (String attrVal : attr.getValue()) {
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;System.out.println(attr.getKey() + "=" + attrVal);
&emsp;&emsp;&emsp;&emsp;}
&emsp;&emsp;}
}
复制代码

HTTP POST 解析实践

如以前所说的那样,解析 HTTP POST 请求的 message body,必定要使用 HttpObjectAggregator。可是,是否必定要把 msg 转换成 FullHttpRequest 呢?答案是否认的,且往下看。

首先解释下 FullHttpRequest 是什么:

Combinate the HttpRequest and FullHttpMessage, so the request is a complete HTTP request.
复制代码

即 FullHttpRequest 包含了 HttpRequest 和 FullHttpMessage,是一个 HTTP 请求的彻底体。

而把 msg 转换成 FullHttpRequest 的方法很简单:

FullHttpRequest fullRequest = (FullHttpRequest) msg;
复制代码

接下来就是分几种 Content-Type 进行解析了。

private void dealWithContentType() throws Exception{
        String contentType = getContentType();
        //可使用HttpJsonDecoder
        if(contentType.equals("application/json")){
            String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
            JSONObject obj = JSON.parseObject(jsonStr);
            for(Map.Entry<String, Object> item : obj.entrySet()){
                logger.info(item.getKey()+"="+item.getValue().toString());
            }

        }else if(contentType.equals("application/x-www-form-urlencoded")){
            //方式一:使用 QueryStringDecoder
			String jsonStr = fullRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
			QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false);
			Map<String, List<String>> uriAttributes = queryDecoder.parameters();
            for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
                for (String attrVal : attr.getValue()) {
                    logger.info(attr.getKey() + "=" + attrVal);
                }
            }

        }else if(contentType.equals("multipart/form-data")){
            //TODO 用于文件上传
        }else{
            //do nothing...
        }
    }
    private String getContentType(){
        String typeStr = headers.get("Content-Type").toString();
        String[] list = typeStr.split(";");
        return list[0];
    }
复制代码

功能测试

我是利用Postman对netty实现的http服务器进行请求,你们若是觉的能够的话,能够自行下载。

Get 请求

Postman:

Server:

16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
//打印请求url
16:58:59.159 [nioEventLoopGroup-3-1] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: /
复制代码

Post 请求

Postman:

Server:

16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
16:58:59.130 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
16:58:59.159 [nioEventLoopGroup-3-1] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: /
17:03:59.813 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x0f3f5fdd, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xfd00cb1b, L:/0:0:0:0:0:0:0:1:8888 - R:/0:0:0:0:0:0:0:1:45768]
17:03:59.813 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x0f3f5fdd, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
//打印post请求的url
17:03:59.817 [nioEventLoopGroup-3-2] INFO com.sanshengshui.netty.HttpHelloWorldServerHandler - http uri: /ttt
复制代码

Gatling性能,负载测试

若是对Gatling测试工具不太熟悉的话,能够看一下我以前写的文章:

  1. 负载,性能测试工具-Gatling

  2. Gatling简单测试SpringBoot工程

性能测试报告大致以下:

================================================================================
---- Global Information --------------------------------------------------------
> request count                                    1178179 (OK=1178179 KO=0     )
> min response time                                      0 (OK=0      KO=-     )
> max response time                                  12547 (OK=12547  KO=-     )
> mean response time                                     1 (OK=1      KO=-     )
> std deviation                                         32 (OK=32     KO=-     )
> response time 50th percentile                          0 (OK=0      KO=-     )
> response time 75th percentile                          1 (OK=1      KO=-     )
> response time 95th percentile                          2 (OK=2      KO=-     )
> response time 99th percentile                          5 (OK=5      KO=-     )
> mean requests/sec                                10808.982 (OK=10808.982 KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                       1178139 (100%)
> 800 ms < t < 1200 ms                                   0 (  0%)
> t > 1200 ms                                           40 (  0%)
> failed                                                 0 (  0%)
================================================================================
复制代码

其余

关于Netty实现高性能的HTTP服务器详解到这里就结束了。

Netty实现高性能的HTTP服务器 项目工程地址: github.com/sanshengshu…

原创不易,若是感受不错,但愿给个推荐!您的支持是我写做的最大动力!

版权声明:

做者:穆书伟

博客园出处:www.cnblogs.com/sanshengshu…

github出处:github.com/sanshengshu…    

我的博客出处:sanshengshui.github.io/

相关文章
相关标签/搜索