徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>

因为有个不会存在大量连接的小的Web服务器需求,不至于用上重量级服务器,于是自己动手写一个服务器。

同时也提供了一个简单的Web框架。能够简单的使用了。

大体的需求包括

  1. 能够处理HTTP协议。
  2. 能够提供接口让使用者编写自己的服务。

会省略一些暂时影响察看的代码。还不够完善,供记录问题和解决办法之用,可能会修改许多地方。

让我们开始吧~

Project的地址 : Github


从ServerSocket开始

点这里是这部分的完整代码,可以对照察看

大家都知道HTTP协议使用的是TCP服务。 而要用TCP通信都得从ServerSocket开始。ServerSocket监听指定IP地址指定端口之后,另一端便可以通过连接这个ServerSocket来建立一对一的Socket进行收发数据。

我们先从命令行参数里获得要监听的ip地址和端口号,当然没有的话使用默认的。

 1 public static void main(String[] args) {
 2     ...
 3     InetAddress ip = null;
 4     int port;
 5     if (args.length == 2 && args[1].matches(".+:\\d+")) {
 6         ...
 7             ip = InetAddress.getByName(address[0]);
 8         ...
 9     } else {
10         ...
11             ip = InetAddress.getLocalHost();
12         ...
13         port = 8080;
14         System.out.println("未指定地址和端口,使用默认ip和端口..." + ip.getHostAddress() + ":" + port);
15     }
16
17     Server server = new Server(ip, port);
18     server.start();
19 }

输入是 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的绑定以及其他的初始化工作。

1     ServerSocketChannel serverChannel;
2     registerServices();
3     serverChannel = ServerSocketChannel.open();
4     serverChannel.bind(new InetSocketAddress(this.ip, this.port));
5     serverChannel.configureBlocking(false);
6     selector = Selector.open();
7     serverChannel.register(selector, SelectionKey.OP_ACCEPT);

registerServices() 暂时先忽略,是用来注册用户写的服务的。

由于是NIO,在这里是用的ServerSocketChannel,绑定到ip和端口,设置好非阻塞,注册ACCEPT事件。不设置非阻塞状态是不能使用Selectior的。

然后开始循环监听和处理事件

 1 public void start() {
 2     init();
 3     while (true) {
 4         ...
 5         selector.select();
 6         ...
 7         Set<SelectionKey> readyKeys = selector.selectedKeys();
 8         Iterator<SelectionKey> iterator = readyKeys.iterator();
 9         while (iterator.hasNext()) {
10             SelectionKey key = iterator.next();
11             iterator.remove();
12             if (key.isAcceptable()) {
13                 ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
14                 ...//处理接受事件
15             } else if (key.isReadable()) {
16                 SocketChannel client = (SocketChannel) key.channel();
17                 ...//处理读事件
18             } else if (key.isWritable()) {
19                 SocketChannel client = (SocketChannel) key.channel();
20                 ...//处理写事件
21             }
22             ...
23         }
24     }
25 }
在我看来SelectionKey指的就是一个事件,它关联一个channel并且可以携带一个对象。 slector.select() 会阻塞直到有注册的事件来临。 获取一个SelectionKey之后需要使用iterator.next()将它从selectedKeys中去除,不然下次selector.select()仍然会获取到这个key。


下面来分析每个事件。

Accept事件

Accept事件其实很简单,就是可以来了一个Socket可以建立连接了。 那么就像下面这样,accept创建一个连接后,在SocketChannel监听Read事件,等到有数据可以读的时候就可以进行读取。

1 if (key.isAcceptable()) {
2     ServerSocketChannel serverSocket = (ServerSocketChannel) key.channel();
3     SocketChannel client = serverSocket.accept();
4     client.configureBlocking(false);
5     client.register(selector, SelectionKey.OP_READ);
6 }

Read事件

这个事件就可以接收到HTTP请求了。读取到数据之后提交给Controller进行异步的HTTP请求解析,根据FilePath转发给服务处理类。处理完后会给通道注册WRITE的监听。client.register(selector, SelectionKey.OP_WRITE)

并让key携带Response对象(将在后续章节写出)

1 if (key.isReadable()) {
2     SocketChannel client = (SocketChannel) key.channel();
3     ByteBuffer buffer = ByteBuffer.allocate(4096);
4     client.read(buffer);
5     executor.execute(new Controller(buffer, client, selector));
6 }

这里存在的问题是不知道如何处理过大的请求

Write事件

这个事件将Response写入SocketChannel。

 1 if (key.isWritable()) {
 2     SocketChannel client = (SocketChannel) key.channel();
 3     Response response = (Response) key.attachment();
 4     if (response == null) {
 5         continue;
 6     }
 7     ByteBuffer byteBuffer = response.getByteBuffer();
 8     if (byteBuffer.hasRemaining()) {
 9         client.write(byteBuffer);
10     }
11     if (!byteBuffer.hasRemaining()) {
12         key.cancel();
13         client.close();
14     }
15 }


如果发现什么问题或者有什么建议请指教。谢谢~

附录区:

[1] 当消息主体出现在消息中时,一条消息的传输长度(transfer-length)是消息主体(messagebody)
的长度;也就是说在实体主体被应用了传输编码(transfer-coding)后。当消息中出现
消息主体时,消息主体的传输长度(transfer-length)由下面(以优先权的顺序)决定:

  1. 任何不能包含消息主体(message-body)的消息(这种消息如1xx,204和304响应和任
    何HEAD方法请求的响应)总是被头域后的第一个空行(CRLF)终止,不管消息里是否存在
    实体头域(entity-header fields)。
  2. 如果Transfer-Encoding头域(见14.41节)出现,并且它的域值是非”“dentity”传输编码
    值,那么传输长度(transfer-length)被“块”(chunked)传输编码定义,除非消息因为通过
    关闭连接而结束。
  3. 如果出现Content-Length头域(属于实体头域)(见14.13节),那么它的十进制值(以
    字节表示)即代表实体主体长度(entity-length,译注:实体长度其实就是实体主体的长度,
    以后把entity-length翻译成实体主体的长度)又代表传输长度(transfer-length)。Content-
    Length 头域不能包含在消息中,如果实体主体长度(entity-length)和传输长度(transferlength)
    两者不相等(也就是说,出现Transfer-Encodind头域)。如果一个消息即存在传输译
    码(Transfer-Encoding)头域并且也Content-Length头域,后者会被忽略。
  4. 如果消息用到媒体类型“multipart/byteranges”,并且传输长度(transfer-length)另外也没
    有指定,那么这种自我定界的媒体类型定义了传输长度(transfer-length)。这种媒体类型不能
    被利用除非发送者知道接收者能怎样去解析它; HTTP1.1客户端请求里如果出现Range头域
    并且带有多个字节范围(byte-range)指示符,这就意味着客户端能解析multipart/byteranges
    响应。
    一个Range请求头域可能会被一个不能理解multipart/byteranges的HTTP1.0代理(proxy)
    再次转发;在这种情况下,服务器必须能利用这节的1,3或5项里定义的方法去定界此消息。
  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)当接收了一个
    无效的长度时必须能通知用户。
时间: 2024-08-27 10:40:39

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>的相关文章

徒手用Java来写个Web服务器和框架吧&lt;第三章:Service的实现和注册&gt;

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response> 这一章先把Web框架的功能说一些,有个雏形. 先是制作一个Service,并绑定到一个正则地址.用到了注解和反射. 项目地址: Telemarketer Service的定义 Telemarketer的Service是一个服务,请求了跟它关联的地址,那就由它来为你服务. 它对外只需一个方法.并且对这个方法的要求大概只有输入一个Reque

徒手用Java来写个Web服务器和框架吧&lt;第二章:Request和Response&gt;

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 接上一篇,说到接受了请求,接下来就是解析请求构建Request对象,以及创建Response对象返回. 多有纰漏还请指出.省略了很多生产用的服务器需要处理的过程,仅供参考.可能在不断的完整中修改文章内容. 先上图 项目地址: https://github.com/csdbianhua/Telemarketer 首先看看如何解析请求 解析请求 构建Request对象 这部分对应代码在这里,可以对照查看 一个HTTP的GET请求大概如下

用C写一个web服务器(二) I/O多路复用之epoll

.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .container::before,.container::after { content: " "; display: table } .container::after { clear: both } .container::before,.container::after { content:

转:C#写的WEB服务器

转:http://www.cnblogs.com/x369/articles/79245.html 这只是一个简单的用C#写的WEB服务器,只实现了get方式的对html文件的请求,有兴趣的朋友可以在此基础之上继续开发更多功能,小弟学c#不久,如有错漏,望请见凉!! 摘要: WWW的工作基于客户机/服务器计算模型,由Web 浏览器(客户机)和Web服务器(服务器)构成,两者之间采用超文本传送协议(HTTP)进行 通信,HTTP协议的作用原理包括四个步骤:连接,请求,应答.根据上述HTTP协议的作

一起写一个 Web 服务器

导读: 本系列深入浅出的讲述了如何用 Python 从 0 开始,写一个 web 服务器,并让其与业界流行的 web 框架协同工作,最后还进一步完善了开头的 web 服务器 demo,让其可以支持多并发请求的处理,并解决了过程当中遇到的"僵尸进程"等一系列 socket/网络编程 中的常见问题,图文并茂.循序渐进,是篇非常不错的教程,对了解整个 Web 编程理论相当有帮助,推荐一看. 作者:伯乐在线 - 高世界 翻译 1.什么是 Web 服务器,以及怎样工作的? 一起写一个 Web 服

使用node.js 文档里的方法写一个web服务器

刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const http=require('http'); const hostname='127.0.0.1' const port=3000; const server = http.createServer((req,res)=>{ res.statusCode=200; res.setHeader('Cont

《ASP.NET Web API 2框架揭秘》第一章 概述【样章】

<ASP.NET Web API 2框架揭秘>(详情请见<新作<ASP.NET Web API 2框架揭秘>正式出版>)以实例演示的方式介绍了很多与ASP.NET Web API相关的最佳实践,同时还提供了一系列实用性的扩展.本书详细讲解了ASP.NET Web API从接收请求到响应回复的整个流程,包括路由.Http Controller的激活.Action方法的选择与执行.参数的绑定与验证.过滤器的执行和安全等相关的机制.除此之外,本书在很多章节还从设计的角度对AS

用C写一个web服务器(四) CGI协议

* { margin: 0; padding: 0 } body { font: 13.34px helvetica, arial, freesans, clean, sans-serif; color: black; line-height: 1.4em; background-color: #F8F8F8; padding: 0.7em } p { margin: 1em 0; line-height: 1.5em } table { font-size: inherit; font: 10

如何写一个Web服务器

最近两个月的业余时间在写一个私人项目,目的是在Linux下写一个高性能Web服务器,名字叫Zaver.主体框架和基本功能已完成,还有一些高级功能日后会逐渐增加,代码放在了github.Zaver的框架会在代码量尽量少的情况下接近工业水平,而不像一些教科书上的toy server为了教原理而舍弃了很多原本server应该有的东西.在本篇文章中,我将一步步地阐明Zaver的设计方案和开发过程中遇到的困难以及相应的解决方法. 为什么要重复造轮子 几乎每个人每天都要或多或少和Web服务器打交道,比较著名