基于nio的HTTP服务器

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

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

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

    3201 引用 • 8216 回帖 • 4 关注

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • GitBook

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

    3 引用 • 8 回帖
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 403 关注
  • OnlyOffice
    4 引用 • 16 关注
  • ZooKeeper

    ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 HBase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

    59 引用 • 29 回帖 • 9 关注
  • 招聘

    哪里都缺人,哪里都不缺人。

    188 引用 • 1057 回帖 • 2 关注
  • Kafka

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

    36 引用 • 35 回帖
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 29 关注
  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    500 引用 • 1395 回帖 • 243 关注
  • FlowUs

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

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

    1 引用 • 7 关注
  • 安装

    你若安好,便是晴天。

    132 引用 • 1184 回帖 • 1 关注
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖 • 3 关注
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 105 关注
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖 • 2 关注
  • 叶归
    12 引用 • 56 回帖 • 20 关注
  • 又拍云

    又拍云是国内领先的 CDN 服务提供商,国家工信部认证通过的“可信云”,乌云众测平台认证的“安全云”,为移动时代的创业者提供新一代的 CDN 加速服务。

    20 引用 • 37 回帖 • 579 关注
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 648 关注
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    56 引用 • 85 回帖
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    42 引用 • 130 回帖 • 252 关注
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 505 关注
  • Hibernate

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

    39 引用 • 103 回帖 • 730 关注
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖 • 1 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    228 引用 • 476 回帖
  • Sillot

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

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

    主仓库地址:Hi-Windom/Sillot

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

    注意事项:

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

    Oracle(甲骨文)公司,全称甲骨文股份有限公司(甲骨文软件系统有限公司),是全球最大的企业级软件公司,总部位于美国加利福尼亚州的红木滩。1989 年正式进入中国市场。2013 年,甲骨文已超越 IBM,成为继 Microsoft 后全球第二大软件公司。

    107 引用 • 127 回帖 • 342 关注
  • danl
    173 关注