1.简介

本文是上一篇文章实践篇,在上一篇文章中,我分析了选择器 Selector 的原理。本篇文章,我们来说说 Selector 的应用,如标题所示,这里我基于 Java NIO 实现了一个简单的 HTTP 服务器。在接下来的章节中,我会详细讲解 HTTP 服务器实现的过程。另外,本文所对应的代码已经上传到 GitHub 上了,需要的自取,仓库地址为 toyhttpd。好了,废话不多说,进入正题吧。

2. 实现

本节所介绍的 HTTP 服务器是一个很简单的实现,仅支持 HTTP 协议极少的特性。包括识别文件后缀,并返回相应的 Content-Type。支持200、400、403、404、500等错误码等。由于支持的特性比较少,所以代码逻辑也比较简单,这里罗列一下:

  1. 处理请求,解析请求头
  2. 响应请求,从请求头中获取资源路径, 检测请求的资源路径是否合法
  3. 根据文件后缀匹配 Content-Type
  4. 读取文件数据,并设置 Content-Length,如果文件不存在则返回404
  5. 设置响应头,并将响应头和数据返回给浏览器。

接下来我们按照处理请求和响应请求两步操作,来说说代码实现。先来看看核心的代码结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*** TinyHttpd** @author code4wt* @date 2018-03-26 22:28:44*/
public class TinyHttpd {private static final int DEFAULT_PORT = 8080;private static final int DEFAULT_BUFFER_SIZE = 4096;private static final String INDEX_PAGE = "index.html";private static final String STATIC_RESOURCE_DIR = "static";private static final String META_RESOURCE_DIR_PREFIX = "/meta/";private static final String KEY_VALUE_SEPARATOR = ":";private static final String CRLF = "\r\n";private int port;public TinyHttpd() {this(DEFAULT_PORT);}public TinyHttpd(int port) {this.port = port;}public void start() throws IOException {// 初始化 ServerSocketChannelServerSocketChannel ssc = ServerSocketChannel.open();ssc.socket().bind(new InetSocketAddress("localhost", port));ssc.configureBlocking(false);// 创建 SelectorSelector selector = Selector.open();// 注册事件ssc.register(selector, SelectionKey.OP_ACCEPT);while(true) {int readyNum = selector.select();if (readyNum == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> it = selectedKeys.iterator();while (it.hasNext()) {SelectionKey selectionKey = it.next();it.remove();if (selectionKey.isAcceptable()) {SocketChannel socketChannel = ssc.accept();socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);} else if (selectionKey.isReadable()) {// 处理请求request(selectionKey);selectionKey.interestOps(SelectionKey.OP_WRITE);} else if (selectionKey.isWritable()) {// 响应请求response(selectionKey);}}}}private void request(SelectionKey selectionKey) throws IOException {...}private Headers parseHeader(String headerStr) {...}private void response(SelectionKey selectionKey) throws IOException {...}private void handleOK(SocketChannel channel, String path) throws IOException {...}private void handleNotFound(SocketChannel channel)  {...}private void handleBadRequest(SocketChannel channel) {...}private void handleForbidden(SocketChannel channel) {...}private void handleInternalServerError(SocketChannel channel) {...}private void handleError(SocketChannel channel, int statusCode) throws IOException {...}private ByteBuffer readFile(String path) throws IOException {...}private String getExtension(String path) {...}private void log(String ip, Headers headers, int code) {}
}

上面的代码是 HTTP 服务器的核心类的代码结构。其中 request 负责处理请求,response 负责响应请求。handleOK 方法用于响应正常的请求,handleNotFound 等方法用于响应出错的请求。readFile 方法用于读取资源文件,getExtension 则是获取文件后缀。

2.1 处理请求

处理请求的逻辑比较简单,主要的工作是解析消息头。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private void request(SelectionKey selectionKey) throws IOException {// 从通道中读取请求头数据SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);channel.read(buffer);buffer.flip();byte[] bytes = new byte[buffer.limit()];buffer.get(bytes);String headerStr = new String(bytes);try {// 解析请求头Headers headers = parseHeader(headerStr);// 将请求头对象放入 selectionKey 中selectionKey.attach(Optional.of(headers));} catch (InvalidHeaderException e) {selectionKey.attach(Optional.empty());}
}private Headers parseHeader(String headerStr) {if (Objects.isNull(headerStr) || headerStr.isEmpty()) {throw new InvalidHeaderException();}// 解析请求头第一行int index = headerStr.indexOf(CRLF);if (index == -1) {throw new InvalidHeaderException();}Headers headers = new Headers();String firstLine = headerStr.substring(0, index);String[] parts = firstLine.split(" ");/** 请求头的第一行必须由三部分构成,分别为 METHOD PATH VERSION* 比如:*     GET /index.html HTTP/1.1*/if (parts.length < 3) {throw new InvalidHeaderException();}headers.setMethod(parts[0]);headers.setPath(parts[1]);headers.setVersion(parts[2]);// 解析请求头属于部分parts = headerStr.split(CRLF);for (String part : parts) {index = part.indexOf(KEY_VALUE_SEPARATOR);if (index == -1) {continue;}String key = part.substring(0, index);if (index == -1 || index + 1 >= part.length()) {headers.set(key, "");continue;}String value = part.substring(index + 1);headers.set(key, value);}return headers;
}

简单总结一下上面的代码逻辑,首先是从通道中读取请求头,然后解析读取到的请求头,最后将解析出的 Header 对象放入 selectionKey 中。处理请求的逻辑很简单,不多说了。

2.2 响应请求

看完处理请求的逻辑,接下来再来看看响应请求的逻辑。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
private void response(SelectionKey selectionKey) throws IOException {SocketChannel channel = (SocketChannel) selectionKey.channel();// 从 selectionKey 中取出请求头对象Optional<Headers> op = (Optional<Headers>) selectionKey.attachment();// 处理无效请求,返回 400 错误if (!op.isPresent()) {handleBadRequest(channel);channel.close();return;}String ip = channel.getRemoteAddress().toString().replace("/", "");Headers headers = op.get();// 如果请求 /meta/ 路径下的资源,则认为是非法请求,返回 403 错误if (headers.getPath().startsWith(META_RESOURCE_DIR_PREFIX)) {handleForbidden(channel);channel.close();log(ip, headers, FORBIDDEN.getCode());return;}try {handleOK(channel, headers.getPath());log(ip, headers, OK.getCode());} catch (FileNotFoundException e) {// 文件未发现,返回 404 错误handleNotFound(channel);log(ip, headers, NOT_FOUND.getCode());} catch (Exception e) {// 其他异常,返回 500 错误handleInternalServerError(channel);log(ip, headers, INTERNAL_SERVER_ERROR.getCode());} finally {channel.close();}
}// 处理正常的请求
private void handleOK(SocketChannel channel, String path) throws IOException {ResponseHeaders headers = new ResponseHeaders(OK.getCode());// 读取文件ByteBuffer bodyBuffer = readFile(path);// 设置响应头headers.setContentLength(bodyBuffer.capacity());headers.setContentType(ContentTypeUtils.getContentType(getExtension(path)));ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());// 将响应头和资源数据一同返回channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}// 处理请求资源未发现的错误
private void handleNotFound(SocketChannel channel)  {try {handleError(channel, NOT_FOUND.getCode());} catch (Exception e) {handleInternalServerError(channel);}
}private void handleError(SocketChannel channel, int statusCode) throws IOException {ResponseHeaders headers = new ResponseHeaders(statusCode);// 读取文件ByteBuffer bodyBuffer = readFile(String.format("/%d.html", statusCode));// 设置响应头headers.setContentLength(bodyBuffer.capacity());headers.setContentType(ContentTypeUtils.getContentType("html"));ByteBuffer headerBuffer = ByteBuffer.wrap(headers.toString().getBytes());// 将响应头和资源数据一同返回channel.write(new ByteBuffer[]{headerBuffer, bodyBuffer});
}

上面的代码略长,不过逻辑仍然比较简单。首先,要判断请求头存在,以及资源路径是否合法。如果都合法,再去读取资源文件,如果文件不存在,则返回 404 错误码。如果发生其他异常,则返回 500 错误。如果没有错误发生,则正常返回响应头和资源数据。这里只贴了核心代码,其他代码就不贴了,大家自己去看吧。

2.3 效果演示

分析完代码,接下来看点轻松的吧。下面贴一张代码的运行效果图,如下:

3.总结

本文所贴的代码是我在学习 Selector 过程中写的,核心代码不到 300 行。通过动手写代码,也使得我加深了对 Selector 的了解。在学习 JDK 的过程中,强烈建议大家多动手写代码。通过写代码,并踩一些坑,才能更加熟练运用相关技术。这个是我写 NIO 系列文章的一个感触。

好了,本文到这里结束。谢谢阅读!

  • 本文链接: https://www.tianxiaobo.com/2018/04/04/基于-Java-NIO-实现简单的-HTTP-服务器/

from: http://www.tianxiaobo.com/2018/04/04/%E5%9F%BA%E4%BA%8E-Java-NIO-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84-HTTP-%E6%9C%8D%E5%8A%A1%E5%99%A8/

基于 Java NIO 实现简单的 HTTP 服务器相关推荐

  1. 并发型服务器响应方式,基于Java NIO 开发高性能并发型服务器程序的研究

    基于Java NIO 开发高性能并发型服务器程序的研究 第8卷%第5期 软件导刊 2009年5月SoftwareGuide Vol.8No.5May.2009 基于JavaNIO开发高性能并发型服务器 ...

  2. 基于java nio的memcached客户端——xmemcached

    1.xmemcached是什么? xmemcached是基于java nio实现的memcached客户端API. 实际上是基于我实现的一个简单nio框架 http://code.google.com ...

  3. java源码聊天软件_【原创】基于Java NIO的多人在线聊天工具源码实现(登录,单聊,群聊)...

    近来在学习Java NIO网络开发知识,写了一个基于Java NIO的多人在线聊天工具MyChat练练手.源码公开在Coding上: 编写一个基于Java NIO的多人在线聊天工具,需要以下几方面的知 ...

  4. 基于Java NIO的Socket通信

    基于Java NIO的Socket通信 Java NIO模式的Socket通信,是一种同步非阻塞IO设计模式,它为Reactor模式实现提供了基础. 下面看看,Java实现的一个服务端和客户端通信的例 ...

  5. 手写简单的HttpServer基于Java nio 实现socket异步通信(请求映射注解方式)

    HttpServer服务类 1 package javax.servlet.http.server2; 2 3 import java.io.IOException; 4 import java.ne ...

  6. java nio 客户端_Java网络编程:Netty框架学习(二)---Java NIO,实现简单的服务端客户端消息传输...

    概述 上篇中已经讲到Java中的NIO类库,Java中也称New IO,类库的目标就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO), ...

  7. java nio ssl_java连接MQTT+SSL服务器

    java用ssl加密方式连接mqtt服务器.其它ssl加密的也可以参考,SSLSocketFactory获取部分都是一样的.踩了很多坑,根据生成工具不同(openssl和keytool)以及秘钥文件编 ...

  8. java.io和java.nio性能简单对比

    我从java1.3开始学习java,后来主要用1.4,再后来1.5和1.6中的很多新特性,都停留在"知道"的状态,比如nio,虽然据说可以提升性能,但并没有真正深入使用和测试过,工 ...

  9. jax java_JAX-WS 学习一:基于java的最简单的WebService服务

    JAVA 1.6 之后,自带的JAX-WS API,这使得我们可以很方便的开发一个基于Java的WebService服务. 基于JAVA的WebService 服务 1.创建服务端WebService ...

最新文章

  1. 送书啦!40本经典书籍任你挑!
  2. 并行数据处理与性能详解与ForkJoin框架
  3. HD2 电容屏不灵怎么办?
  4. 生成特定分布随机数的方法:Python seed() 函数numpy scikit-learn随机数据生成
  5. Android:Application
  6. Windows进程同步之事件内核对象(Event)
  7. Message Authentication Code
  8. Latex找不到字体:Package fontspec: The font “simsun“ cannot be found
  9. 地图坐标转换(84坐标、百度坐标、国测局坐标)
  10. tomcat自动化部署(拉取\备份、部署、更新、回滚)
  11. 2021-08-06随记(vertical-align, 顶线、底线、中线、基线,vue原理理解)
  12. 万洲金业:反弹不变,黄金弱势走高
  13. 请去Windows应用商店查看有关xxx的更多信息 解决办法
  14. 使用Spark SQL读取Hive上的数据
  15. Unity 云雾shader
  16. 002.光流传感器(ADNS0380版)使用说明
  17. 人脸表情识别系统(VGG_16和Xception模型)配置GPU加速,Fer2013数据集和CK+数据集,tensorboard
  18. 电脑中的快捷键(常用)
  19. 指针的大小与什么有关
  20. 开源精选:AntdFront —— React 纯 Hook 多标签微前端管理系统解决方案

热门文章

  1. spring源码分析之spring-web web模块分析
  2. dalvik 与art 区别
  3. 签消费贷合同免费领取苹果手机? 捷信、马上等多家消费金融“躺枪”
  4. 直击CVPR2017:商汤科技打造技术盛宴23篇论文刮起中国风 By 机器之心2017年7月25日 10:14 近日,CVPR2017 在夏威夷如火如荼地进行,作为全球计算机视觉顶级会议,CVPR
  5. 中科院罗平演讲全文:自动撰写金融文档如何实现,用 AI 解救“金融民工” | CCF-GAIR 2017
  6. c语言函数之间互相调用符号,如何实现C和C++函数互相调用
  7. MyBatis-14MyBatis动态SQL之【foreach】
  8. Spring-AOP 通过配置文件实现 异常抛出增强
  9. csv java web 导入_DAY5-step9 使用CSV模块和Pandas在Python中读取和写入CSV文件
  10. 河南测绘职业学院招生计算机,河南测绘职业学院代码