HearyHTTPd - 写一个自己的HTTP服务器
自己研究用Java写一个HTTP服务器。一看JHTTPd,EasyHTTPd这些名字就用过了,那就叫HearyHTTPd吧,简称hhttpd。构想是做一个基于Reactor的多线程高并发HTTP服务器。
HearyHTTPd - 写一个自己的HTTP服务器
先写下开发日记,记录每天的进展和心得。
1 开源地址
GitHub:HearyShen / HearyHTTPd
2 开发日记
Day 1, 2020.6.15, Mon
先从最简单的例子看起,廖雪峰写的的Java教程 Web基础 中的最基本的HTTP代码跑起来看看效果。这个例子的原理是:用ServerSocket监听端口,主线程阻塞直到接受(accept)到请求,接收到请求后,启动一个线程来处理这个Socket对象,读取输入流以解析请求,写入输出流以进行响应。
我进行了改进,实现了MainReactor和SubReactor的模式。
- MainReactor负责监听和接受请求,只负责将Socket对象加入(put)到BlockingQueue中;
- SubReactor负责处理请求,做出响应,负责从BlockingQueue中取出(take)请求的Socket对象,读取输入流进行解析,写入输出流进行响应。
设计一个Launcher来初始化数据结构,启动MainReactor和SubReactor线程。
第一天只实现了一个MainReactor线程和一个SubReactor线程,处理能力也只是响应"Hello, World."。初步把基本的脚手架搭起来,跑通了。
Day 2, 2020.6.16, Tue
第二天想到我写的Hexo博客就是一个静态网站,完全可以用自己写的HTTP服务器来部署呀。
于是自己写了一个GET请求处理功能,能够根据请求的路径来读取WebRoot路径下的本地文件,响应给浏览器。
不过初步的效果只能返回HTML文件,也就是说文本型的CSS、Javascript文件,二进制型的字体文件、图像文件还无法正常返回。需要进一步处理Content-Type,并实现二进制文件的响应功能。
不巧的是今天遇到一些别的琐事,下午和晚上都被占用了,没来得及做下去。
Day 3, 2020.6.17, Wed
今天对模块分工进行了重新构思和命名,分为:
- Reactor包
- 包含MainReactor和SubReactor,是Reactor线程的实现;
- Processor包
- 包含GetProcessor等,提供对HTTP的GET方法进行整体处理的功能;
- Resolver包
- 包含RequestLineResolver、HeaderResolver,提供对报文请求行、请求头部的内容解析的功能;
- Responser包
- 包含TextResponser、BinaryResponser和NotFoundResponser。Processor对请求解析根据Content-Type进行分流,交给对应的Responser进行处理和响应。
今天整理好了常用的Content-Type,实现了BinaryResponser来处理二进制对象的请求,实现了多线程的SubReactor,并且改进了RequestLineResolver来提升对带参请求的兼容性。
接下来打算研究下NIO,提高IO效率。
Day 4, 2020.6.18, Thu
今天研究了下NIO,并且写了一份基于NIO的请求-响应双向Socket通信的验证代码。
笔记记录在:基于NIO的请求-响应双向Socket通信网络编程
接下来要基于NIO的ServerSocketChannel
/
SocketChannel
以及Selector
的IO复用技术对hhttpd进行改进。
初步构思基于NIO的hhttpd应该是一个MainReactor通过IO复用处理请求,将接收到的请求交给一个SubReactor,SubReactor通过IO复用处理可读请求,将其交给线程池进行处理(read/decode/compute/encode/response)。
简单例子是,比如说:client请求index.html,mainReactor select到这个acceptable的serverSocketChannel,就accept它建立一个socketChannel,register到subReactor的selector上,client的请求数据发到server后,subReactor就能select到readable&writable的socketChannel,于是将这个socketChannel交给threadPool去处理,由worker thread去read/decode/compute/encode/write。
Day 5, 2020.6.19, Fri
今天把项目进行了改写,全面基于Java的NIO。
层次架构可分为:
MainReactor (single thread)
SubReactor (single thread)
- HttpWorker (Runnable in thread pool)
- GetProcesser
- TextResponser
- BinaryResponser
- NotFoundResponser
- etc.
- PostProcesser
- etc.
- GetProcesser
- HttpWorker (Runnable in thread pool)
一些注意点:
注册新SocketChannel后唤醒SubReactor:MainReactor将accept的SocketChannel注册到SubReactor上后,SubReactor线程阻塞在select()
操作处,需要通过selector.wakeUp()
来唤醒阻塞,让SubReactor重新select得到新resigter上去的socketChannel;
处理SocketChannel时要取消其在SubReactor的Selector上的注册以免重复:SubReactor在select得到Readable的SocketChannel后,应该调用selectionKey.cancel()
来取消注册该socketChannel,否则该通道在worker
thread处理完毕将其关闭之前,会一直保持Readable状态,以至于被SubReactor反复重复地select出来,并启动很多woker
thread去处理同一个socketChannel,不仅严重耗费资源,而且第一个执行完毕的线程关闭该socketChannel后,后续的其他线程就无法再读取该socketChannel,由此还会出现IOException异常。
读写NIO的ByteBuffer时要注意offset和length:检查好文档中的offset指的是谁的offset,读写的length必须Math.min(buffer.remaining(), bytesLen)
,即必须是内容长度和buffer长度两者中的最小值。
但是,我发现改写后,在Chrome浏览器的开发者工具中进行了验证,发现网络流耗时反而更长了。用NIO之前,基于Socket和输入输出流的每个资源文件的耗时大约50ms,可是使用NIO后,反而耗时高达300ms。这个反常现象需要进一步排查。
Day 6, 2020.6.20, Sat
使用NIO后反而变慢了一个数量级,这个问题让人费解。
我怀疑是否是Selector机制太慢了?因此尝试了不含Selector的阻塞式ServerSocketChannel,但是没有改善效果。
我还尝试了修改唤醒SubReactor的时机,调整线程池的类型和参数、排查通信过程中建立的每一个连接……,但可惜的时都无济于事。
就在我一筹莫展的时候,我发现我是习惯性用 http://localhost:8080
访问的,我觉得会不会和localhost的解析有关,因此尝试使用
http://127.0.0.1:8080
访问,发现就一切正常了。也就是说,当hhttpd代码里设置为监听的host为127.0.0.1
时,在Chrome中用
http://localhost:8080 会有较大initial connection
lantency从而变得较慢,而用 http://127.0.0.1:8080 访问则很快。
事实上,当我做了更多的测试后,我发现和浏览器实现也有关。例如:不管监听或访问如何设置,Firefox浏览器总是很快。
详细排查结果记录在了:Chromium内核浏览器访问localhost时的初始连接(initial connection)高延迟问题。
Day 7, 2020.6.21, Sun
周日,给自己放个假,懒散的一天。
Day 8, 2020.6.22, Mon
准备和参加了招银网络科技的笔试。
Day 9, 2020.6.23, Tue
先实现了HTTP HEAD方法的处理响应。
然后实现了HTTP GET/POST方法的CGI响应功能。
- 原理是:通过Java的runtime执行本地程序,利用环境变量传入基于CGI协议的参数,执行本地程序(CGI程序)产生的Process实例的标准输入输出可以传递信息,取得CGI程序的输出结果,把这个结果作为响应报文即可。
- 具体地,我写了一个很基本Python的本地CGI程序,输出一个包含HTML页面的报文,显示CGI程序得到的环境变量。
- 实际上,Java的Runtime可以以执行命令的方式调用各种形式的程序,CGI程序不仅限于Python。
相关笔记记录在:在Java中执行Python等本地程序(命令)
另外,还改写了HTTP request line的解析方式,改为通过Java的URI类库进行处理,不再自行解析处理。