zoukankan      html  css  js  c++  java
  • 徒手用Java来写个Web服务器和框架吧<第一章:NIO篇>

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

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

    大体的需求包括

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

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

    让我们开始吧~

    // 更新 2015年09月30日 关于读事件

    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 }

    这里存在的问题是不知道如何处理过大的请求,或许可以利用传输长度[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)由下面(以优先权的顺序)决定:

    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)当接收了一个
      无效的长度时必须能通知用户。
  • 相关阅读:
    hbase 由于zookeeper问题导致连接失败问题
    Python 判断文件/目录是否存在
    mysql5.7设置默认的字符集
    mysql 提示ssl问题
    Ubuntu 安装MySQL报共享库找不到
    hbase 监控指标项
    大量数据通过Phoenix插入到hbase报错记录(2)
    通过phoenix导入数据到hbase出错记录
    mysql5.7 之 sql_mode=only_full_group_by问题
    Hadoop 在启动或者停止的时候需要输入yes确认问题
  • 原文地址:https://www.cnblogs.com/imyijie/p/4822143.html
Copyright © 2011-2022 走看看