基于nio的HTTP服务器

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

这段时间学习了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
    116 引用 • 433 回帖 • 8 关注
  • NIO
    15 引用 • 26 回帖 • 1 关注
  • Java

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

    3169 引用 • 8208 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • Notion

    Notion - The all-in-one workspace for your notes, tasks, wikis, and databases.

    5 引用 • 26 回帖
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 576 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖 • 1 关注
  • 知乎

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

    10 引用 • 66 回帖
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 623 关注
  • 大数据

    大数据(big data)是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合,是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高增长率和多样化的信息资产。

    93 引用 • 113 回帖 • 1 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 461 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖
  • Solo

    Solo 是一款小而美的开源博客系统,专为程序员设计。Solo 有着非常活跃的社区,可将文章作为帖子推送到社区,来自社区的回帖将作为博客评论进行联动(具体细节请浏览 B3log 构思 - 分布式社区网络)。

    这是一种全新的网络社区体验,让热爱记录和分享的你不再感到孤单!

    1429 引用 • 10050 回帖 • 486 关注
  • Caddy

    Caddy 是一款默认自动启用 HTTPS 的 HTTP/2 Web 服务器。

    11 引用 • 54 回帖 • 146 关注
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 143 关注
  • AngularJS

    AngularJS 诞生于 2009 年,由 Misko Hevery 等人创建,后为 Google 所收购。是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入等。2.0 版本后已经改名为 Angular。

    12 引用 • 50 回帖 • 457 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    174 引用 • 814 回帖
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖 • 2 关注
  • 创造

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

    175 引用 • 994 回帖
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖 • 2 关注
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    106 引用 • 152 回帖 • 2 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    76 引用 • 429 回帖 • 5 关注
  • TensorFlow

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

    20 引用 • 19 回帖 • 3 关注
  • 生活

    生活是指人类生存过程中的各项活动的总和,范畴较广,一般指为幸福的意义而存在。生活实际上是对人生的一种诠释。生活包括人类在社会中与自己息息相关的日常活动和心理影射。

    230 引用 • 1454 回帖
  • WebComponents

    Web Components 是 W3C 定义的标准,它给了前端开发者扩展浏览器标签的能力,可以方便地定制可复用组件,更好的进行模块化开发,解放了前端开发者的生产力。

    1 引用 • 1 关注
  • 酷鸟浏览器

    安全 · 稳定 · 快速
    为跨境从业人员提供专业的跨境浏览器

    3 引用 • 59 回帖 • 18 关注
  • Flutter

    Flutter 是谷歌的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。 Flutter 可以与现有的代码一起工作,它正在被越来越多的开发者和组织使用,并且 Flutter 是完全免费、开源的。

    39 引用 • 92 回帖 • 8 关注
  • CodeMirror
    1 引用 • 2 回帖 • 127 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 728 关注
  • SEO

    发布对别人有帮助的原创内容是最好的 SEO 方式。

    35 引用 • 200 回帖 • 25 关注