基于nio的HTTP服务器

本贴最后更新于 4047 天前,其中的信息可能已经时移世异

这段时间学习了java的nio。

所以,自己练手,写了一个基于nio的java服务器。

可以说是一个简单的服务器吧。总共就是10多KB而已。

代码已经放到Github上。

目前只实现了基本的访问静态资源的功能。

 

1、基本原理

先上图:

dashuserver原理

其实原理很简单。

a、开启一个selector,然后不断用selector来select。selector.select()。可以获取发生的io时间。然后生成一个AnalyseHandle。扔到线程池。

b、AnalyseHandle会读取浏览器发过来的http请求信息,然后生成一个StaticHandle,扔到线程池。

c、StaticHandle根据http请求信息,读取文件内容,然后按照http协议,把内容返回。

整一个过程是比较简单的,同时每次请求,都会生成一个session对象,里面保存了请求的一些信息,每个handle里,都会有这个对象,这样子就能保证每个handle能获取这次请求的信息。

 

2、代码分析

dashuserver类图

随便画了个类图帮助大家理解。

Server类:

package me.idashu.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**

  • 描述:服务端

  • @author: dashu

  • @since: 13-2-27
    */
    public class Server implements Runnable{

    private int port = 80;
    private Selector selector;

    public Server() {

    }

    @Override
    public void run() {

     try {
         selector = Selector.open();
    
         ServerSocketChannel ssc = ServerSocketChannel.open();
         ssc.configureBlocking(false);
         ssc.socket().bind(new InetSocketAddress(port));
         ssc.register(selector, SelectionKey.OP_ACCEPT);
    
         System.out.println("start server");
    
         while(true){
    
             int n = selector.select();
    
             Set<SelectionKey> set = selector.selectedKeys();
             Iterator<SelectionKey> it = set.iterator();
    
             while (it.hasNext()) {
    
                 SelectionKey key = it.next();
                 it.remove();
    
                 if (key.isAcceptable()) {
                     Session session = new Session(key);
                     System.out.println("access");
                 } else if (key.isReadable()) {
                     key.interestOps(0);
                     Session session = (Session) key.attachment();
                     AnalyseHandle analyseHandle = new AnalyseHandle(session);
                     ThreadPool.getInstance().execute(analyseHandle);
                 }
    
             }
    
         }
    
     } catch (IOException e) {
         e.printStackTrace();
     }
    

    }

    public static void main(String[] args){
    new Thread(new Server()).start();
    }
    }

这是程序的入口,在main函数里面,生成了一个Server类,然后运行里面的run方法。

在run方法里面,生成了一个Selector。然后一直循环调用selector.select()。

这方法可以获取一个key,首先一般都是先这样子if (key.isAcceptable()) {...}

如果这个事件是socket连接事件,就生成一个session。

session里面会根据key,获取一个ServerSocketChannel对象,然后在把对象绑定在selector。而且只关注read方法。

然后继续循环,让发生了else if (key.isReadable()) {...},就是这个key可以读的时候。

把刚才生成的session拿出来,生成一个AnalyseHandle对象,放到线程池。取消key关注的事件,key.interestOps(0)。

Handle抽象类:

package me.idashu.server;

/**

  • 描述:处理事务的类<br>

  • 实现了 Runnable 接口,抽象类,必须继承

  • @author: dashu

  • @since: 13-3-4
    */
    public abstract class Handle implements Runnable{

    protected Session session;

    public Handle(Session session) {
    this.session = session;
    }

}

这个抽象类,就是实现了Runnable接口,然后这个类里面有一个Session对象。

在我们服务器里面,每一个处理事务的线程,都得继承这个类。

ThreadPool线程池:

package me.idashu.server;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**

  • 描述:线程池<br>

  • 实现单例
    
  • @author: dashu

  • @since: 13-3-1
    */
    public class ThreadPool extends ThreadPoolExecutor{

    private static ThreadPool threadPool;

    private ThreadPool(){
    super(5,10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    }

    public synchronized static ThreadPool getInstance() {
    if (threadPool == null) {
    threadPool = new ThreadPool();
    }
    return threadPool;
    }

}

其实就是一个实现了单例的线程池。

Session类:

package me.idashu.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;

/**

  • 描述:会话
  • @author: dashu
  • @since: 13-2-27
    */

public class Session {

/**
 * 请求Key
 */
private SelectionKey key;
/**
 * 通道
 */
private SocketChannel channel;
/**
 * 应答类型
 */
private String mime;
/**
 * 请求路径
 */
private String requestPath;
/**
 * 请求方法
 */
private String method;
/**
 * 请求头信息
 */
private Map&lt;String, String&gt; heads = new HashMap&lt;String, String&gt;();

public Session(SelectionKey key) throws IOException {
    channel = ((ServerSocketChannel) key.channel()).accept();
    channel.configureBlocking(false);

    key = channel.register(key.selector(), SelectionKey.OP_READ, this);
    key.selector().wakeup();

    this.key = key;
}

/**
 * 放入头信息
 * @param key
 * @param value
 */
public void head(String key, String value){
    heads.put(key, value);
}

/**
 * 取出头信息
 * @param key
 * @return
 */
public String head(String key){
    return heads.get(key);
}

/**
 * 关闭通道
 */
public void close(){
    if (channel != null &amp;&amp; channel.isOpen())
        try {
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
}

public SelectionKey getKey() {
    return key;
}

public void setKey(SelectionKey key) {
    this.key = key;
}

public SocketChannel getChannel() {
    return channel;
}

public void setChannel(SocketChannel channel) {
    this.channel = channel;
}

public String getMime() {
    if (mime == null) {
        int index = requestPath.indexOf(&quot;.&quot;);
        String m = requestPath.substring(index+1);
        mime = Mime.get(m);
    }
    return mime;
}

public void setMime(String mime) {
    this.mime = mime;
}

public String getRequestPath() {
    return requestPath;
}

public void setRequestPath(String requestPath) {
    this.requestPath = requestPath;
}

public Map&lt;String, String&gt; getHeads() {
    return heads;
}

public void setHeads(Map&lt;String, String&gt; heads) {
    this.heads = heads;
}

public String getMethod() {
    return method;
}

public void setMethod(String method) {
    this.method = method;
}

}

每一次请求,都会生成一个session对象。然后session对象会传到handle里面。

handle里面就是我们处理事务的线程Runnable。

这样子每一次有关于请求的数据都被保存到session里面,或者从session里面读取。

AnalyseHandle类:

package me.idashu.server;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

/**

  • 描述:处理请求<br>

  • 判断请求类型
    
  • @author: dashu

  • @since: 13-3-4
    */
    public class AnalyseHandle extends Handle {

    public AnalyseHandle(Session session) {
    super(session);
    }

    @Override
    public void run() {
    SocketChannel sc = (SocketChannel) session.getKey().channel();
    session.setChannel(sc);
    // 请求信息
    StringBuffer headSB = new StringBuffer();
    ByteBuffer bb = ByteBuffer.allocate(1024);
    int count = 0;

     try {
         // 读取请求头信息
         while (true) {
             bb.clear();
             count = sc.read(bb);
             if (count == 0) {
                 break;
             }
             if (count &lt; 0) {
                 // 连接已经中断
                 session.close();
                 return;
             }
             bb.flip();
             byte[] data = new byte[count];
             bb.get(data, 0, count);
             headSB.append(new String(data));
         }
    

// System.out.println("head:\n"+headSB.toString().trim());
// 解析请求
StringTokenizer st = new StringTokenizer(headSB.toString(), Sysconst.CRLF);

        String line = st.nextToken();

        // 请求方法路径等
        StringTokenizer http = new StringTokenizer(line, &quot; &quot;);
        session.setMethod(http.nextToken()); // 方法

        String get = http.nextToken();
        int index = get.indexOf('?');
        if (index &gt; 0) {
            session.setRequestPath(get.substring(0, index)); // 路径
        } else {
            session.setRequestPath(get);
        }

        line = st.nextToken();

        while (line != null &amp;&amp; !line.equals(&quot;&quot;)) {
            int colon = line.indexOf(&quot;:&quot;);
            if (colon &gt; -1) {
                String key = line.substring(0, colon).toLowerCase();
                String value = line.substring(colon + 1).trim();
                session.head(key, value);
            }
            if (st.hasMoreElements()) {
                line = st.nextToken();
            } else {
                break;
            }
        }

        StaticHandle sh = new StaticHandle(session);
        ThreadPool.getInstance().execute(sh);

    } catch (IOException e) {
        e.printStackTrace();
    }
}

}

刚才上面的时候已经提到过,这个类继承了Handle这个类,这个类在Server里面被new出来,放到线程池里。

其实这个类很简单,就是从session获取key,然后从key里获取channel。

得到channel,就可以读取浏览器传过来的http请求信息。

再进行适当的解析,保存会session里面。

在AnalyseHandle的后面,因为我们只处理静态文件,所以就直接生成了一个StaticHandle类,扔线程池里。

StaticHandle类:

package me.idashu.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

/**

  • 描述:

  • @author: dashu

  • @since: 13-3-4
    */
    public class StaticHandle extends Handle {

    /**

    • 静态路径
      */
      private String staticPath;

    public StaticHandle(Session session) {
    super(session);
    }

    @Override
    public void run() {

     // 获取channel
     SocketChannel channel = session.getChannel();
     if (!channel.isOpen())
         return;
    
     File file = new File(Sysconst.ROOTPATH, session.getRequestPath());
     FileChannel from = null;
     try {
         from = new FileInputStream(file).getChannel();
         ByteBuffer bb = ByteBuffer.allocate(1024);
         bb.clear();
    
         // 返回头信息
         StringBuffer sb = new StringBuffer();
         sb.append(&quot;HTTP/1.1 200 OK&quot;).append(Sysconst.CRLF);
         sb.append(&quot;Content-Type: &quot;).append(session.getMime()).append(Sysconst.CRLF);
         sb.append(&quot;Content-Length: &quot;).append(file.length()).append(Sysconst.CRLF);
         sb.append(Sysconst.CRLF);
         System.out.println(sb.toString());
         bb.put(sb.toString().getBytes());
         bb.flip();
         channel.write(bb);
    
         bb = from.map(FileChannel.MapMode.READ_ONLY, 0, from.size());
         channel.write(bb);
     } catch (FileNotFoundException e) {
         // 404
         ByteBuffer bb = ByteBuffer.allocate(1024);
         bb.clear();
         StringBuffer sb = new StringBuffer();
         sb.append(&quot;HTTP/1.1 404 Not Found&quot;).append(Sysconst.CRLF);
         sb.append(&quot;Content-Type: &quot;).append(&quot;text/html&quot;).append(Sysconst.CRLF);
         sb.append(Sysconst.CRLF);
         sb.append(&quot;404&quot;);
         bb.put(sb.toString().getBytes());
         bb.flip();
         try {
             channel.write(bb);
         } catch (IOException e1) {
             e1.printStackTrace();
         }
         sb.append(Sysconst.CRLF);
     } catch (IOException e) {
         e.printStackTrace();
     } finally {
         session.close();
     }
    

    }

    public String getStaticPath() {
    return staticPath;
    }

    public void setStaticPath(String staticPath) {
    this.staticPath = staticPath;
    }
    }

StaticHandle类其实就是一个处理静态文件,返回信息的类而已。

StaticHandle根据AnalyzeHandle类读取的http请求数据,找到对应的静态文件。

如果文件存在,就写回去。不存在,就返回404。

返回了信息之后,调用session.close()方法,关掉通道,整一个http请求结束。

 

其实这里还有几个其他的类的,Mime就是处理http返回类型的,Sysconst就是保存系统常量的。但是在这里就不贴代码了,有需要自己去Github上面去取吧。dashuserver

  • Web
    115 引用 • 431 回帖 • 8 关注
  • NIO
    15 引用 • 26 回帖 • 1 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8207 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
DASHU
大叔已经成为一个老油条了~~

推荐标签 标签

  • etcd

    etcd 是一个分布式、高可用的 key-value 数据存储,专门用于在分布式系统中保存关键数据。

    5 引用 • 26 回帖 • 495 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    173 引用 • 990 回帖
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖 • 2 关注
  • GitBook

    GitBook 使您的团队可以轻松编写和维护高质量的文档。 分享知识,提高团队的工作效率,让用户满意。

    3 引用 • 8 回帖
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 238 关注
  • 996
    13 引用 • 200 回帖 • 2 关注
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    124 引用 • 580 回帖
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖 • 2 关注
  • Hadoop

    Hadoop 是由 Apache 基金会所开发的一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。

    82 引用 • 122 回帖 • 620 关注
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 497 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 462 关注
  • 反馈

    Communication channel for makers and users.

    123 引用 • 906 回帖 • 193 关注
  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖 • 1 关注
  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖
  • 尊园地产

    昆明尊园房地产经纪有限公司,即:Kunming Zunyuan Property Agency Company Limited(简称“尊园地产”)于 2007 年 6 月开始筹备,2007 年 8 月 18 日正式成立,注册资本 200 万元,公司性质为股份经纪有限公司,主营业务为:代租、代售、代办产权过户、办理银行按揭、担保、抵押、评估等。

    1 引用 • 22 回帖 • 689 关注
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖 • 1 关注
  • Python

    Python 是一种面向对象、直译式电脑编程语言,具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法简捷和清晰,尽量使用无异义的英语单词,与其它大多数程序设计语言使用大括号不一样,它使用缩进来定义语句块。

    536 引用 • 672 回帖
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 349 关注
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    333 引用 • 323 回帖 • 65 关注
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 640 关注
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 1 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 4 关注
  • Hprose

    Hprose 是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。你无需专门学习,只需看上几眼,就能用它轻松构建分布式应用系统。

    9 引用 • 17 回帖 • 595 关注
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 74 关注
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    45 引用 • 113 回帖 • 310 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 394 关注
  • GAE

    Google App Engine(GAE)是 Google 管理的数据中心中用于 WEB 应用程序的开发和托管的平台。2008 年 4 月 发布第一个测试版本。目前支持 Python、Java 和 Go 开发部署。全球已有数十万的开发者在其上开发了众多的应用。

    14 引用 • 42 回帖 • 688 关注