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

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

2.1 处理请求

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

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

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

2.2 响应请求

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

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

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

2.3 效果演示

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

3.总结

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

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

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处

作者:coolblog

本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

基于 Java NIO 实现简单的 HTTP 服务器的更多相关文章

  1. 基于Java Mina框架的部标808服务器设计和开发

    在开发部标GPS平台中,部标808GPS服务器是系统的核心关键,决定了部标平台的稳定性和行那个.Linux服务器是首选,为了跨平台,开发语言选择Java自不待言. 我们为客户开发的部标服务器基于Min ...

  2. 基于Java Mina框架的部标jt808服务器设计和开发

    在开发部标GPS平台中,部标jt808GPS服务器是系统的核心关键,决定了部标平台的稳定性和行那个.Linux服务器是首选,为了跨平台,开发语言选择Java自不待言.需要购买jt808GPS服务器源码 ...

  3. Java NIO: Non-blocking Server 非阻塞网络服务器

    本文翻译自 Jakob Jenkov 的 Java NIO: Non-blocking Server ,原文地址:http://tutorials.jenkov.com/java-nio/non-bl ...

  4. java实现一个简单的Web服务器

    注:本段内容来源于<JAVA 实现 简单的 HTTP服务器> 1. HTTP所有状态码 状态码 状态码英文名称 中文描述 100 Continue 继续.客户端应继续其请求 101 Swi ...

  5. JAX-WS 学习一:基于java的最简单的WebService服务

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

  6. 基于python创建一个简单的HTTP-WEB服务器

    背景 大多数情况下主机资源只有开发和测试相关人员可以登录直接操作,且有些特定情况"答辩.演示.远程"等这些场景下是无法直接登录主机的.web是所有终端用户都可以访问了,解决了人员权 ...

  7. 基于java NIO 的服务端与客户端代码

    在对java NIO  selector 与 Buffer Channel  有一定的了解之后,我们进行编写java nio 实现的 客户端与服务端例子: 服务端: public class NIOC ...

  8. 基于TcpListener实现最简单的http服务器

    最近实现一套简单的网络程序.为了查看程序内部的变量,方便调试.就在想搞一个最最简单的方式.第一个想到写文件,日志.这个不实时,而且打开麻烦,pass .于是想到用网络输出.本来是想写成c/s模式,想着 ...

  9. 基于java mail实现简单的QQ邮箱发送邮件

    刚学习到java邮件相关的知识,先写下这篇博客,方便以后翻阅学习. -----------------------------第一步 开启SMTP服务 在 QQ 邮箱里的 设置->账户里开启 S ...

随机推荐

  1. iframe结构的项目,登录页面嵌套

    参考:http://www.cnblogs.com/qixin622/p/6548076.html 在网页编程时,我们经常需要处理,当session过期时,我们要跳到登陆页面让用户登陆,由于我们可能用 ...

  2. 【机器学习】正则化的线性回归 —— 岭回归与Lasso回归

    注:正则化是用来防止过拟合的方法.在最开始学习机器学习的课程时,只是觉得这个方法就像某种魔法一样非常神奇的改变了模型的参数.但是一直也无法对其基本原理有一个透彻.直观的理解.直到最近再次接触到这个概念 ...

  3. 图之单源Dijkstra算法、带负权值最短路径算法

    1.图类基本组成 存储在邻接表中的基本项 /** * Represents an edge in the graph * */ class Edge implements Comparable< ...

  4. Angular4---认证---使用HttpClient拦截器,解决循环依赖引用的问题

    在angular4 项目中,每次请求服务端需要添加头部信息AccessToken作为认证的凭据.但如果在每次调用服务端就要写代码添加一个头部信息,会变得很麻烦.可以使用angular4的HttpCli ...

  5. 案例:中科院光机所应用大数据可视化工具-LightningChart | 见证高性能图表

    中国科学院上海光学精密机械研究所 中国现代光学和激光科学领域领先研究所 中国科学院上海光学精密机械研究所(简称中科院上海光机所)是我国建立最早.规模最大的激光专业研究所,成立于1964年,现已发展成为 ...

  6. Unix 让进程安全地退出

    终止一个进程有很多方法(暂只说linux环境):前台运行的进程,如果没有提供退出功能,我们通常会Ctrl+C进行终止:后台或守护进程,如果也没有提供退出命令啥的,咱通常会kill掉:此外还有类似关机或 ...

  7. windows系统安装jira

     主题介绍 JIRA是Atlassian公司出品的项目与事务跟踪工具,被广泛应用于缺陷跟踪.客户服务.需求收集.流程审批.任务跟踪.项目跟踪和敏捷管理等工作领域,其配置灵活.功能全面.部署简单.扩展丰 ...

  8. 初始化angularJS之ng-app的自动绑定和手动绑定

    在传统的angularJS应用中,都是通过ng-app把angular应用绑定到某个dom上,这样做会把js代码入侵到html上,angular提供了手动启动的API--angular.bootstr ...

  9. 格式化JSON数据

    function formatJson(json, options) { var reg = null, formatted = '', pad = 0, PADDING = ' '; options ...

  10. 笔记:MyBatis Mapper XML文件详解 - Cache

    缓存(Cache) 从数据库中加载的数据缓存到内存中,是很多应用程序为了提高性能而采取的一贯做法.MyBatis对通过映射的SELECT语句加载的查询结果提供了内建的缓存支持.默认情况下,启用一级缓存 ...