今天就NIO实现简单的HTTP交互做一下笔记,进而来加深Tomcat源码印象。

一、关于HTTP

  1、HTTP的两个显著特点,HTTP是一种可靠的超文本传输协议

    第一、实际中,浏览器作为客户端,每次访问,必须明确指定IP、PORT。这是因为,HTTP协议底层传输就是使用的TCP方式。

    第二、HTTP协议作为一种规范,简单理解,首先,它传输的是文本(即字符串,这个是区别于二级制数据的)。其次,他对文本的格式是有要求的。

  2、HTTP约定的报文格式

    对于以下报文格式,我们只需要对拿到的数据,进行readLine,然后做基于换行回车空格判断切割等,就能拿到所有信息。

    

  

二、系统架构

  基于第一节的结论,我们就能启动NIO作为服务端,然后用浏览器来发起客户端接入、发送数据,然后服务端回执。浏览器显示回执。其中,浏览器内核持有一个客户端SocketChannel,并且会自动维护其事件监听。并且会自动按照HTTP协议报文格式来解析服务端返回的报文,并自动渲染。所以,我们只需要关注服务端,这里涉及一下几个步骤:

  <1>、接收浏览器SocketChannel发送的数据。

  <2>、解码:进行请求报文解析。

  <3>、编码:计算响应数据,并将响应数据封装为HTTP协议格式。

  <4>、写入SocketChannel,即发送给浏览器。

  

三、服务初始化

1、服务器实例声明

  我们使用NO作为服务端,所以端口、多路复用器这些必不可少。与此同时,我们需要一个线程池去专门进行业务处理,其中具体的业务处理交给HttpServlet。

 public class SimpleHttpServer {
// 服务端口
private int port;
// 处理器
private HttpServlet servlet;
// 轮询器
private final Selector selector;
// 启停标识
private volatile boolean run = false;
// 需要注册的Channel,避免与轮询器产生死锁
private Set<SocketChannel> allConnections = new HashSet<>();
// 执行业务线程池
private ExecutorService executor = Executors.newFixedThreadPool(5); public SimpleHttpServer(int port, HttpServlet servlet) throws IOException {
this.port = port;
this.servlet = servlet;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
// 一旦初始化就开始监听客户端接入事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
}

2、业务处理HttpServlet的细节

HttpServlet

 public interface HttpServlet {
void doGet(Request request, Response response);
void doPost(Request request, Response response);
}

Request

 public class Request {
Map<String, String> heads;
String url;
String method;
String version;
//请求内容
String body;
Map<String, String> params;
}

Response

 public class Response {
Map<String, String> headers;
// 状态码
int code;
//返回结果
String body;
}

3、编解码相关

编码

 //编码Http 服务
private byte[] encode(Response response) {
StringBuilder builder = new StringBuilder(512);
builder.append("HTTP/1.1 ").append(response.code).append(Code.msg(response.code)).append("\r\n");
if (response.body != null && response.body.length() != 0) {
builder.append("Content-Length: ")
.append(response.body.length()).append("\r\n")
.append("Content-Type: text/html\r\n");
}
if (response.headers != null) {
String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue())
.collect(Collectors.joining("\r\n"));
builder.append(headStr + "\r\n");
}
builder.append("\r\n").append(response.body);
return builder.toString().getBytes();
}

解码

 // 解码Http服务
private Request decode(byte[] bytes) throws IOException {
Request request = new Request();
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
String firstLine = reader.readLine();
System.out.println(firstLine);
String[] split = firstLine.trim().split(" ");
request.method = split[0];
request.url = split[1];
request.version = split[2];
//读取请求头
Map<String, String> heads = new HashMap<>();
while (true) {
String line = reader.readLine();
if (line.trim().equals("")) {
break;
}
String[] split1 = line.split(":");
heads.put(split1[0], split1[1]);
}
request.heads = heads;
request.params = getUrlParams(request.url);
//读取请求体
request.body = reader.readLine();
return request;
}

获取请求参数

 private static Map getUrlParams(String url) {
Map<String, String> map = new HashMap<>();
url = url.replace("?", ";");
if (!url.contains(";")) {
return map;
}
if (url.split(";").length > 0) {
String[] arr = url.split(";")[1].split("&");
for (String s : arr) {
if (s.contains("=")) {
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key, value);
} else {
map.put(s, null);
}
}
return map;
} else {
return map;
}
}

四、交互实现

1、服务端启动

  对于已经初始化好的ServerSocketChannel,我们下来要做的无非就是while(true)轮询selector。这个套路已经非常固定了。这里我们启动一个线程来轮询:

 public void start() {
this.run = true;
new Thread(() -> {
try {
while (run) {
selector.select(2000);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 监听客户端接入
if (key.isAcceptable()) {
handleAccept(key);
}
// 监听客户端发送消息
else if (key.isReadable()) {
handleRead(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}, "selector-io").start();
}

2、处理客户端接入

 // 当有客户端接入的时候,为其注册 可读 事件监听,等待客户端发送数据
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}

3、处理客户端发送的消息

 /**
* 接收到客户端发送的数据进行处理
* 1、将客户端的请求数据取出来,放到ByteArrayOutputStream。
* 2、将数据交给Servlet处理。
*/
private void handleRead(SelectionKey key) throws IOException {
final SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
while (channel.read(buffer) > 0) {
buffer.flip();
out.write(buffer.array(), 0, buffer.limit());
buffer.clear();
}
if (out.size() <= 0) {
channel.close();
return;
}
process(channel, out);
}

4、业务处理并发送返回数据

 private void process(SocketChannel channel, ByteArrayOutputStream out) {
executor.submit(() -> {
try {
Request request = decode(out.toByteArray());
Response response = new Response();
if (request.method.equalsIgnoreCase("GET")) {
servlet.doGet(request, response);
} else {
servlet.doPost(request, response);
}
channel.write(ByteBuffer.wrap(encode(response)));
} catch (Throwable e) {
e.printStackTrace();
}
});
}

五、单元测试

 @Test
public void simpleHttpTest() throws IOException, InterruptedException {
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(8080, new HttpServlet() {
@Override
public void doGet(Request request, Response response) {
System.out.println(request.url);
response.body="hello_word:" + System.currentTimeMillis();
response.code=200;
response.headers=new HashMap<>();
}
@Override
public void doPost(Request request, Response response) {}
});
simpleHttpServer.start();
new CountDownLatch(1).await();
}

六、小结

  以上,使用原生NIO实现了一个简单的HTTP交互样例,虽然,只做了自定义Servlet中做了GET方法的实现。其实原理已经很明了。真正的Tomcat交互内核,其实就是在这个原理的基础上做了工业级软件架构设计。小结一下:

  <1>、浏览器地址栏访问,对于浏览器内核,可以理解触发了两个事件,OP_CONNECT事件、OP_WRITE事件。

  <2>、NIO实现的服务端还是遵循固定套路。当监听到OP_READ事件后,直接处理,然后回写结果。

  <3>、浏览器会在OP_WRITE事件后,自动变更监听为OP_READ事件。等待服务端返回。

  <4>、关于编码、解码、请求参数获取等,均属于HTTP协议的范畴,其实无关NIO。

  <5>、服务端selector轮询、accept接入channel注册。这两个操作之间使用的是用一个同步器,所以存在死锁的风险。Tomcat里边做了很好的处理。这里以后再聊。

  谨以此笔记记录一下原生NIO学习心得,为后续Tomcat源码部门铺一下技术前提。

NIO实践-HTTP交互实现暨简版Tomcat交互内核的更多相关文章

  1. 手写一个Web服务器,极简版Tomcat

    网络传输是通过遵守HTTP协议的数据格式来传输的. HTTP协议是由标准化组织W3C(World Wide Web Consortium,万维网联盟)和IETF(Internet Engineerin ...

  2. Virtex6 PCIe 超简版基础概念学习(二)

    Virtex6 PCIe 超简版基础概念学习(二) 分类:FPGAPCIe (2081)  (0)  举报  收藏 文档版本 开发工具 测试平台 工程名字 日期 作者 备注 V1.0 ise14.7 ...

  3. SpringBoot2+Netty打造通俗简版RPC通信框架

    2019-07-19:完成基本RPC通信! 2019-07-22:优化此框架,实现单一长连接! 2019-07-24:继续优化此框架:1.增加服务提供注解(带版本号),然后利用Spring框架的在启动 ...

  4. java语言实现简单接口工具--粗简版

    2016注定是变化的一年,忙碌.网红.项目融资失败,现在有点时间整整帖子~~ 目标: 提高工作效率与质量,能支持平台全量接口回归测试与迭代测试也要满足单一接口联调测试. 使用人员: 测试,开发 工具包 ...

  5. python练习_购物车(简版)

    python练习_购物车(简版) 需求: 写一个python购物车可以输入用户初始化金额 可以打印商品,且用户输入编号,即可购买商品 购物时计算用户余额,是否可以购买物品 退出结算时打印购物小票 以下 ...

  6. 《程序设计语言——实践之路(英文第三版)》【PDF】下载

    <程序设计语言--实践之路(英文第三版)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382234 内容简介 <程序设计语 ...

  7. 按行切割大文件(linux split 命令简版)

    按行切割大文件(linux split 命令简版) #-*- coding:utf-8 -*- __author__ = 'KnowLifeDeath' ''' Linux上Split命令可以方便对大 ...

  8. Underscore源码阅读极简版入门

    看了网上的一些资料,发现大家都写得太复杂,让新手难以入门.于是写了这个极简版的Underscore源码阅读. 源码: https://github.com/hanzichi/underscore-an ...

  9. 200行代码实现简版react🔥

    200行代码实现简版react

随机推荐

  1. [白话解析] 通过实例来梳理概念 :准确率 (Accuracy)、精准率(Precision)、召回率(Recall)和F值(F-Measure)

    [白话解析] 通过实例来梳理概念 :准确率 (Accuracy).精准率(Precision).召回率(Recall)和F值(F-Measure) 目录 [白话解析] 通过实例来梳理概念 :准确率 ( ...

  2. 如何解决jeecgBoot前端运行项目之后无法获取验证码的问题

    我也是第一次接触这个开源项目,拿到项目之后,安装完环境和依赖,当我启动项目的时候,验证码却刷新不出来. 然后公司后端告诉我需要改两个接口,一个是public目录下的index.html和vue.con ...

  3. 全)Java从单体到微服务打造房产销售平台 2018年慕课网 高清视频+源码

    第1章 课程介绍本章从整体上介绍课程有什么收获,以及课程如何安排,其中包括微服务的两个不同学习阶段--单体开发阶段(基于SpringBoot)和微服务改造阶段(基于SpringCloud),知识点梳理 ...

  4. Docker部署Python项目

    简介 软件开发最大的麻烦事之一就是环境配置,操作系统设置,各种库和组件的安装.只有它们都正确,软件才能运行.如果从一种操作系统里面运行另一种操作系统,通常我们采取的策略就是引入虚拟机,比如在 Wind ...

  5. 学习oracle的SQL语句 练习

    --1.查询emp表,显示薪水大于2000,且工作类别是MANAGER的雇员信息 select * from emp where sal > 2000and job = 'MANAGER'; - ...

  6. ceph bluestore与 filestore 数据存放的区别

    一. filestore 对象所在的PG以文件方式放在xfs文件中 1 查看所有的osd硬盘,跟其他linux其他硬盘一样,被挂载一个目录中. [root@hz-storage1 ~]# df -h ...

  7. webpack4.X + react-router 路由跳转

    webpack4.X  react-router 环境准备工作:windows7.webStorm 2017.1.4.Nodejs 8.7.0.npm 5.4.2 PS:安装的时我们都带上版本,这样即 ...

  8. JavaScript基础原始数据类型的封装对象(013)

    JavaScript提供了5种原始数据类型:number, string, boolean, null, and undefined.对于前面3个,即number, string, 和boolean提 ...

  9. JavaScript基础-自己定义自己的函数(016)

    把一个函数对象赋值给变量后,就可以通过这个变量再次定义函数,甚至可以在一个函数内部再次定义它自己: var scareMe = function () { alert("Boo!" ...

  10. QtableWidget用法流程

    QtableWidget用法流程 ​ 作者:流火 日期:2020/5/10 QTableWidget的基本构造函数 QTableWidget 是QTableview的子类.主要去呗是QTableVie ...