徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>
因为有个不会存在大量连接的小的Web服务器需求,不至于用上重量级服务器,于是自己动手写一个服务器。
同时也提供了一个简单的Web框架。能够简单的使用了。
大体的需求包括
- 能够处理HTTP协议。
- 能够提供接口让使用者编写自己的服务。
会省略一些暂时影响察看的代码。还不够完善,供记录问题和解决办法之用,可能会修改许多地方。
让我们开始吧~
// 更新 2015年09月30日 关于读事件
Project的地址 : Github
从ServerSocket开始
大家都知道HTTP协议使用的是TCP服务。 而要用TCP通信都得从ServerSocket开始。ServerSocket监听指定IP地址指定端口之后,另一端便可以通过连接这个ServerSocket来建立一对一的Socket进行收发数据。
我们先从命令行参数里获得要监听的ip地址和端口号,当然没有的话使用默认的。
public static void main(String[] args) {
...
InetAddress ip = null;
int port;
if (args.length == 2 && args[1].matches(".+:\\d+")) {
...
ip = InetAddress.getByName(address[0]);
...
} else {
...
ip = InetAddress.getLocalHost();
...
port = 8080;
System.out.println("未指定地址和端口,使用默认ip和端口..." + ip.getHostAddress() + ":" + port);
} Server server = new Server(ip, port);
server.start();
}
输入是 start 123.45.67.89:8080
或者直接一个 start
InetAddress.getByName(address[0])
通过一个IP地址的字符串构造一个InetAddress对象。
InetAddress.getLocalHost()
获取localhost的InetAddress对象。
接下来看看Server类。
首先,这个服务器要轻量级,不宜创建太多线程。考虑使用NIO来进行IO处理,一个线程处理IO。所以我们需要一个Selector来选择已经就绪的管道,同时用一个线程池来处理任务。(可以用Runtime.getRuntime().availableProcessors()
获取可用的处理器核数。)
Server启动时首先进行ServerSocket的绑定以及其他的初始化工作。
ServerSocketChannel serverChannel;
registerServices();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(this.ip, this.port));
serverChannel.configureBlocking(false);
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
registerServices()
暂时先忽略,是用来注册用户写的服务的。
由于是NIO,在这里是用的ServerSocketChannel,绑定到ip和端口,设置好非阻塞,注册ACCEPT事件。不设置非阻塞状态是不能使用Selectior的。
然后开始循环监听和处理事件
public void start() {
init();
while (true) {
...
selector.select();
...
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
...//处理接受事件
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
...//处理读事件
} else if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
...//处理写事件
}
...
}
}
}
在我看来SelectionKey指的就是一个事件,它关联一个channel并且可以携带一个对象。slector.select()
会阻塞直到有注册的事件来临。 获取一个SelectionKey之后需要使用iterator.next()
将它从selectedKeys中去除,不然下次selector.select()
仍然会获取到这个key。
下面来分析每个事件。
Accept事件
Accept事件其实很简单,就是可以来了一个Socket可以建立连接了。 那么就像下面这样,accept创建一个连接后,在SocketChannel监听Read事件,等到有数据可以读的时候就可以进行读取。
if (key.isAcceptable()) {
ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
SocketChannel client = serverSocket.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
Read事件
这个事件就可以接收到HTTP请求了。读取到数据之后提交给Controller
进行异步的HTTP请求解析,根据FilePath转发给服务处理类。处理完后会给通道注册WRITE的监听。client.register(selector, SelectionKey.OP_WRITE)
。
并让key携带Response
对象(将在后续章节写出)
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(4096);
client.read(buffer);
executor.execute(new Controller(buffer, client, selector));
}
这里存在的问题是不知道如何处理过大的请求,或许可以利用传输长度[1]重复读取再合并?
同时还有另一个问题。在 selector.select() 已经阻塞后,在另一个线程注册了事件,select无法获取,在只有一个连接的测试环境下似乎没办法。
所以仍需定一个超时时间。比如 if (selector.select(500) == 0) { continue; }
------更新 2015年09月30日------
多次实验发现,一次请求可能不是一次读完。所以根据读到的http首部中的Content-Length进行持续读取。
所以决定直接把channel直接给Connector(原为Controller)处理。同时取消对读取事件的兴趣。
SocketChannel client = (SocketChannel) key.channel();
executor.execute(new Connector(client, selector)); key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
另外关于在另一个线程注册事件select已经在阻塞结果无法知道的问题。
可以使用 selector.wakeup(); 进行强制选择。
Write事件
这个事件将Response写入SocketChannel。
SocketChannel client = (SocketChannel) key.channel();
Response response = (Response) key.attachment();
ByteBuffer byteBuffer = response.getByteBuffer();
if (byteBuffer.hasRemaining()) {
client.write(byteBuffer);
}
if (!byteBuffer.hasRemaining()) {
key.cancel();
client.close();
}
如果发现什么问题或者有什么建议请指教。谢谢~
附录区:
[1] 当消息主体出现在消息中时,一条消息的传输长度(transfer-length)是消息主体(messagebody)
的长度;也就是说在实体主体被应用了传输编码(transfer-coding)后。当消息中出现
消息主体时,消息主体的传输长度(transfer-length)由下面(以优先权的顺序)决定:
- 任何不能包含消息主体(message-body)的消息(这种消息如1xx,204和304响应和任
何HEAD方法请求的响应)总是被头域后的第一个空行(CRLF)终止,不管消息里是否存在
实体头域(entity-header fields)。 - 如果Transfer-Encoding头域(见14.41节)出现,并且它的域值是非”“dentity”传输编码
值,那么传输长度(transfer-length)被“块”(chunked)传输编码定义,除非消息因为通过
关闭连接而结束。 - 如果出现Content-Length头域(属于实体头域)(见14.13节),那么它的十进制值(以
字节表示)即代表实体主体长度(entity-length,译注:实体长度其实就是实体主体的长度,
以后把entity-length翻译成实体主体的长度)又代表传输长度(transfer-length)。Content-
Length 头域不能包含在消息中,如果实体主体长度(entity-length)和传输长度(transferlength)
两者不相等(也就是说,出现Transfer-Encodind头域)。如果一个消息即存在传输译
码(Transfer-Encoding)头域并且也Content-Length头域,后者会被忽略。 - 如果消息用到媒体类型“multipart/byteranges”,并且传输长度(transfer-length)另外也没
有指定,那么这种自我定界的媒体类型定义了传输长度(transfer-length)。这种媒体类型不能
被利用除非发送者知道接收者能怎样去解析它; HTTP1.1客户端请求里如果出现Range头域
并且带有多个字节范围(byte-range)指示符,这就意味着客户端能解析multipart/byteranges
响应。
一个Range请求头域可能会被一个不能理解multipart/byteranges的HTTP1.0代理(proxy)
再次转发;在这种情况下,服务器必须能利用这节的1,3或5项里定义的方法去定界此消息。 - 通过服务器关闭连接能确定消息的传输长度。(请求端不能通过关闭连接来指明请求消息体
的结束,因为这样可以让服务器没有机会继续给予响应)。
为了与HTTP/1.0应用程序兼容,包含HTTP/1.1消息主体的请求必须包括一个有效的内容长
度(Content-Length)头域,除非服务器是HTTP/1.1遵循的。如果一个请求包含一个消息主体
并且没有给出内容长度(Content-Length),那么服务器如果不能判断消息长度的话应该以
400响应(错误的请求),或者以411响应(要求长度)如果它坚持想要收到一个有效内容长
度(Content-length)。
所有的能接收实体的HTTP/1.1应用程序必须能接受"chunked"的传输编码(3.6节),因此当
消息的长度不能被提前确定时,可以利用这种机制来处理消息。
消息不能同时都包括内容长度(Content-Length)头域和非identity传输编码。如果消息包括了
一个非identity的传输编码,内容长度(Content-Length)头域必须被忽略.
当内容长度(Content-Length)头域出现在一个具有消息主体(message-body)的消息里,
它的域值必须精确匹配消息主体里字节数量。HTTP/1.1用户代理(user agents)当接收了一个
无效的长度时必须能通知用户。
徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>的更多相关文章
- 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response>
徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 接上一篇,说到接受了请求,接下来就是解析请求构建Request对象,以及创建Response对象返回. 多有纰漏还请指出.省略 ...
- 徒手用Java来写个Web服务器和框架吧<第三章:Service的实现和注册>
徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response> 这一章先把Web框架的功能说 ...
- java写的web服务器
经常用Tomcat,不知道的以为Tomcat很牛,其实Tomcat就是用java写的,Tomcat对jsp的支持做的很好,那么今天我们用java来写一个web服务器 //首先得到一个server, S ...
- 用java写一个web服务器
一.超文本传输协议 Web服务器和浏览器通过HTTP协议在Internet上发送和接收消息.HTTP协议是一种请求-应答式的协议——客户端发送一个请求,服务器返回该请求的应答.HTTP协议使用可靠的T ...
- 用C写一个web服务器(二) I/O多路复用之epoll
.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .conta ...
- 转:C#写的WEB服务器
转:http://www.cnblogs.com/x369/articles/79245.html 这只是一个简单的用C#写的WEB服务器,只实现了get方式的对html文件的请求,有兴趣的朋友可以在 ...
- 各种容器与服务器的区别与联系 Servlet容器 WEB容器 Java EE容器 应用服务器 WEB服务器 Java EE服务器
转自:https://blog.csdn.net/tjiyu/article/details/53148174 各种容器与服务器的区别与联系 Servlet容器 WEB容器 Java EE容器 应用服 ...
- 使用node.js 文档里的方法写一个web服务器
刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const h ...
- Tomcat源码分析 (一)----- 手写一个web服务器
作为后端开发人员,在实际的工作中我们会非常高频地使用到web服务器.而tomcat作为web服务器领域中举足轻重的一个web框架,又是不能不学习和了解的. tomcat其实是一个web框架,那么其内部 ...
随机推荐
- 前言(Core Data 应用开发实践指南)
Core Data 并不是数据库,它其实是一个拥有多种功能的框架.其中,有个功能是把程序与数据库之间的交互过程自动化,不用再编写SQL代码,改用Objective-C对象来实现. Core Data ...
- pace.js原理简介
简介: 不少童鞋可能都使用过pace.js:http://github.hubspot.com/pace/docs/welcome/ 只要在页面上引入pace.js和相关的css,并不需要对业务逻辑做 ...
- js url参数的获取和设置以及删除
//获取url参数的值:name是参数名 function getQueryString(name) { var reg = new RegExp("(^|&)" + na ...
- 开源半成品的Web版工作流模板设计器(基于AngularJS 2和Redux), 还在继续填坑中
先上个图: 很多企业内部的应用都需要有个工作流平台(插件),无奈灵活方便好用且能够自行更改代码定制嵌入的实在不多,只好自己动手慢慢搞. https://github.com/shibamo/99-fl ...
- 正则匹配所有的a标签
<a\b[^>]+\bhref="([^"]*)"[^>]*>([\s\S]*?)</a>分组1和分组2即为href和value解释: ...
- Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be
有些操作只能回到主线程操作 比如: mbprogresshud只能在主线程中使用 而且注意凡是关于布局的代码也只能下载主线程
- eeclipse使用快捷键
eclipse块注释: 方式一:多行“//”注释 在Eclipse中拖动鼠标,选中需要注释的代码 Ctrl+Shift+C会发现所选代码被“//”注释掉.当调试完后,可再次按住Ctrl+Shift+C ...
- Dirty Flag 模式及其应用
之前在开发中就发现"dirty"是一种不错的解决方案:可以用来延缓计算或者避免不必要的计算.后来在想,这应该也算一种设计模式吧,于是搜索"Dirty设计模式", ...
- 初学bootstrap ---栅格系统
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- handlebars使用总结
对自己使用handlebars做一个小总结,以后忘记了,好有地方看一下,不会用的小伙伴也可以借鉴一下,写的不好. 使用 Handlebars的安装是比较简单和方便的;handlebars是一个纯JS库 ...