使用Netty实现简单的http服务器

记得不久之前我用socket实现了一个简单的静态资源服务器。刚好最近在研究Netty,正好重构一遍,看看Netty在传输上的优势(希望能像Golang一样简单方便)。

参考了Netty的官方示例:https://github.com/netty/netty/tree/4.1/example

 

一、HelloWorld

(1)官方例子

官方示例的HelloWorld例子,也是官方推荐的项目结构。

HttpHelloWorldServer.java

这是Netty的服务入口,基本上都是写成这个样子(官方示例考虑到了ssl,我们平时可能考虑不到,需要学习一下)。

别的没什么好说的,重要的是childHandler()中注册的Handler。

HttpHelloWorldServerInitializer.java

在这里有几个新的Handler。如果用到ssl链接,就需要加入一个Handler,优先处理ssl的Context,再交给HttpServerCodec进行解析。

我看别人写的Netty,都是用的HttpResponseEncoder和HttpRequestDecoder解析请求。官方例子使用了HttpServerCodec,效果应该是一样的。

解析完http请求后,交给我们自己写的HttpHelloWorldServerHandler进行处理。

HttpHelloWorldServerHandler.java

在HttpHelloWorldServerHandler中,主要处理大量的http头信息,构建response,发送给客户端。

启动HttpHelloWorldServer之后,你可以访问http://127.0.0.1:8080/,理论上会HelloWorld的text,但是事实上页面会一直处于读取状态,等待服务器发送信息,看不到HelloWorld的效果。为什么会这样呢?

经过定位,我们可以把问题缩小到这一块:

这里的关键词是KEEP_ALIVE。我们先了解一下KeepAlive的含义。

(2)http中KeepAlive的含义

(tcp中也有KeepAlive,含义是不一样的,不要搞混)。

参考:http://www.cnblogs.com/yjf512/archive/2013/07/23/3207850.html

1.为什么要使用KeepAlive?

原因就是需要加快客户端和服务端的访问请求速度。

KeepAlive就是在浏览器和服务端之间保持长连接,这个连接是可以复用的。当客户端发送一次请求,收到相应以后,第二次就不需要再重新建立连接(慢启动的过程),就可以直接使用这次的连接来发送请求了。在HTTP1.0及各种加强版中,是默认关闭KeepAlive的,而在HTTP1.1中是默认打开的。

2.KeepAlive是不是设置越长越好?

并不是这样的。KeepAlive在增加访问效率的同时,也会增加服务器的压力。对于静态文件是会提高其访问性能,但是对于一些动态请求,如果在一次和下一次的请求过程中占用了服务器的资源,则会导致意想不到的结果。

那么问题来了,是KeepAlive导致浏览器处于持续读取的状态吗?

(3)官方例子中存在的问题

我们看看这一块的逻辑:

在浏览器访问Netty服务端时,会在Header中带上KeepAlive参数,此时服务端中的boolean keepAlive为true,走else块的逻辑,服务端不主动关闭链接,而是继续等待浏览器发送请求。

个人认为,问题出没有关闭链接:

在write传输完后,服务端没有主动关闭链接,浏览器就认为:“服务端可能还没有传输完哦,我等所有信息都收到了再渲染页面吧”,于是一直处于读取状态,不会去渲染页面。

要解决这个问题,只需要注册ChannelFutureListener.CLOSE监听器即可。如果服务端主动关闭链接,浏览器就认为信息已经传输完了,就会把页面渲染出来。

这里我有一个疑问:如果浏览器发送了KeepAlive的header,即浏览器希望和服务端建立长链接,服务端就不能主动关闭链接。但是,如果服务端不主动关闭链接,浏览器就处于持续读取状态,无法渲染页面。这里的KeepAlive意义何在?

后来我看了这一篇文章,想明白了这个问题:

http://www.cnblogs.com/cswuyg/p/3653263.html

KeepAlive的目的是实现链接的重用。如果请求次数很少(我们只请求一次),就没有重用链接的必要,保持链接的成本反而会提高。对此,我们必须设置超时机制,如果链接在一段时间内不再活跃,就关闭链接。

在Netty中,我们可以加入ReadTimeoutHandler:

HttpHelloWorldServerInitializer.java

ReadTimeoutHandler(1)设置了数据读取的超时机制。如果链接在1s之内没有继续读取数据,链接就会关闭,浏览器就会正确渲染页面。

那么问题又来了:“如果链接在1s之内没有继续读取数据,链接就会关闭”。如果Handler处理链接花了比较长的时间(比如让Handler睡眠3s)怎么办?链接岂不是在处理完之前就断开了?

于是我加了一段Sleep:

结果浏览器在大约4s后正确返回了结果。

我个人认为,Handler处理的时间没有算入ReadTimeoutHandler的超时机制中。所以说“如果链接在1s之内没有继续读取数据,链接就会关闭”是不严谨的,应该改为:“如果链接在Handler处理完后1s之内没有继续读取数据,链接就会关闭”

二、静态资源服务器

(1)改造官方的例子

Server.java

经典的入口类。

HttpStaticFileServerInitializer.java

HttpObjectAggregator负责把多个HttpMessage组装成一个完整的Http请求或者响应(个人理解,把多个包中的片段包装成一个整体进行解析/发送)。

ChunkedWriteHandler负责处理大文件传输。大文件传输时,需要复杂的状态管理,ChunkedWriteHandler负责实现这个功能。

HttpStaticFileServerHandler.java

HttpStaticFileServerHandler是我们自己实现的Handler。在下面会有代码的解读。

(2)Header中的CONTENT_TYPE

注意看注释部分,官方示例中使用了activation包(需要在pom.xml中加入依赖),通过MimetypesFileTypeMap类识别静态文件的类型:

从而在Header中返回相应的CONTENT_TYPE,从而帮助客户端对content进行解析。

在这个示例中,我希望浏览器展示一张图片,CONTENT_TYPE会被自动设置为image/jpeg。

(3)缓存处理

服务端不希望客户端总是请求同一静态资源。一方面,如果服务端总是对重复请求做出响应,将浪费很多耗费资源。另一方面,客户端需要接收返回的资源,此过程需要等待,渲染的速度会变慢。

所以,服务端设置header,让客户端缓存静态资源,避免每次都从服务端获取,提高双方的效率。

(4)莫名其妙的favicon.ico

服务端总是会收到这样的uri请求:/favicon.ico。一开始我不知道这个请求是从哪里来的,找了半天,后来才发现这是浏览器自发的行为。

favicon.ico是浏览器中的图标,可以让浏览器的收藏夹中除显示相应的标题外,还以图标的方式区别不同的网站。

一般情况下,浏览器会请求网站根目录,请求获取ico图标。我收到的请求为:http://localhost:8080/favicon.ico。如果获取不到,网页的图标处就会变成一个“空白页面”。

后来我尝试访问http://www.xie4ever.com/favicon.ico(我的网站根目录下的favicon.ico),结果只访问到了空白的页面…我也搞不清楚是怎么一回事。

三、总结

使用Netty实现静态资源服务器,给我的感觉是“逻辑很清楚”,只要记住Netty的工作流程,结合官方的例子,就能简单愉快地实现,比纯socket平滑、简单多了。

但是,比起Golang,Netty还是要复杂一些。所以我还是更推荐使用Golang。

发表评论

电子邮件地址不会被公开。 必填项已用*标注