基于nio的HTTP服务器

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

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

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

    3169 引用 • 8208 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • 音乐

    你听到信仰的声音了么?

    60 引用 • 510 回帖 • 1 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    402 引用 • 3520 回帖
  • FlowUs

    FlowUs.息流 个人及团队的新一代生产力工具。

    让复杂的信息管理更轻松、自由、充满创意。

    1 引用
  • jsoup

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

    6 引用 • 1 回帖 • 473 关注
  • Solidity

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

    3 引用 • 18 回帖 • 353 关注
  • 服务

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

    41 引用 • 24 回帖
  • 书籍

    宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”

    76 引用 • 390 回帖
  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    19 引用 • 31 回帖 • 1 关注
  • Kafka

    Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。

    35 引用 • 35 回帖
  • Sym

    Sym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)系统平台。

    下一代的社区系统,为未来而构建

    524 引用 • 4599 回帖 • 701 关注
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 225 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 702 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    262 引用 • 664 回帖
  • 安全

    安全永远都不是一个小问题。

    191 引用 • 813 回帖 • 1 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    122 引用 • 73 回帖
  • 微服务

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

    96 引用 • 155 回帖
  • Hprose

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

    9 引用 • 17 回帖 • 600 关注
  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    10 引用 • 88 回帖
  • Vim

    Vim 是类 UNIX 系统文本编辑器 Vi 的加强版本,加入了更多特性来帮助编辑源代码。Vim 的部分增强功能包括文件比较(vimdiff)、语法高亮、全面的帮助系统、本地脚本(Vimscript)和便于选择的可视化模式。

    28 引用 • 66 回帖 • 7 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 45 关注
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    6 引用 • 15 回帖 • 182 关注
  • Sillot

    Insights(注意当前设置 master 为默认分支)

    汐洛彖夲肜矩阵(Sillot T☳Converbenk Matrix),致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点。其中汐洛绞架(Sillot-Gibbet)基于自思源笔记(siyuan-note),前身是思源笔记汐洛版(更早是思源笔记汐洛分支),是智慧新录乄终端(多端融合,移动端优先)。

    主仓库地址:Hi-Windom/Sillot

    文档地址:sillot.db.sc.cn

    注意事项:

    1. ⚠️ 汐洛仍在早期开发阶段,尚不稳定
    2. ⚠️ 汐洛并非面向普通用户设计,使用前请了解风险
    3. ⚠️ 汐洛绞架基于思源笔记,开发者尽最大努力与思源笔记保持兼容,但无法实现 100% 兼容
    29 引用 • 25 回帖 • 53 关注
  • HHKB

    HHKB 是富士通的 Happy Hacking 系列电容键盘。电容键盘即无接点静电电容式键盘(Capacitive Keyboard)。

    5 引用 • 74 回帖 • 430 关注
  • Tomcat

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

    162 引用 • 529 回帖
  • 正则表达式

    正则表达式(Regular Expression)使用单个字符串来描述、匹配一系列遵循某个句法规则的字符串。

    31 引用 • 94 回帖
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 608 关注
  • WordPress

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

    45 引用 • 113 回帖 • 276 关注