今天就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。
1 public class SimpleHttpServer { 2 // 服务端口 3 private int port; 4 // 处理器 5 private HttpServlet servlet; 6 // 轮询器 7 private final Selector selector; 8 // 启停标识 9 private volatile boolean run = false; 10 // 需要注册的Channel,避免与轮询器产生死锁 11 private Set<SocketChannel> allConnections = new HashSet<>(); 12 // 执行业务线程池 13 private ExecutorService executor = Executors.newFixedThreadPool(5); 14 15 public SimpleHttpServer(int port, HttpServlet servlet) throws IOException { 16 this.port = port; 17 this.servlet = servlet; 18 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 19 selector = Selector.open(); 20 serverSocketChannel.bind(new InetSocketAddress(port)); 21 serverSocketChannel.configureBlocking(false); 22 // 一旦初始化就开始监听客户端接入事件 23 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); 24 } 25 }
2、业务处理HttpServlet的细节
HttpServlet
1 public interface HttpServlet { 2 void doGet(Request request, Response response); 3 void doPost(Request request, Response response); 4 }
Request
1 public class Request { 2 Map<String, String> heads; 3 String url; 4 String method; 5 String version; 6 //请求内容 7 String body; 8 Map<String, String> params; 9 }
Response
1 public class Response { 2 Map<String, String> headers; 3 // 状态码 4 int code; 5 //返回结果 6 String body; 7 }
3、编解码相关
编码
1 //编码Http 服务 2 private byte[] encode(Response response) { 3 StringBuilder builder = new StringBuilder(512); 4 builder.append("HTTP/1.1 ").append(response.code).append(Code.msg(response.code)).append("\r\n"); 5 if (response.body != null && response.body.length() != 0) { 6 builder.append("Content-Length: ") 7 .append(response.body.length()).append("\r\n") 8 .append("Content-Type: text/html\r\n"); 9 } 10 if (response.headers != null) { 11 String headStr = response.headers.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue()) 12 .collect(Collectors.joining("\r\n")); 13 builder.append(headStr + "\r\n"); 14 } 15 builder.append("\r\n").append(response.body); 16 return builder.toString().getBytes(); 17 }
解码
1 // 解码Http服务 2 private Request decode(byte[] bytes) throws IOException { 3 Request request = new Request(); 4 BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes))); 5 String firstLine = reader.readLine(); 6 System.out.println(firstLine); 7 String[] split = firstLine.trim().split(" "); 8 request.method = split[0]; 9 request.url = split[1]; 10 request.version = split[2]; 11 //读取请求头 12 Map<String, String> heads = new HashMap<>(); 13 while (true) { 14 String line = reader.readLine(); 15 if (line.trim().equals("")) { 16 break; 17 } 18 String[] split1 = line.split(":"); 19 heads.put(split1[0], split1[1]); 20 } 21 request.heads = heads; 22 request.params = getUrlParams(request.url); 23 //读取请求体 24 request.body = reader.readLine(); 25 return request; 26 }
获取请求参数
1 private static Map getUrlParams(String url) { 2 Map<String, String> map = new HashMap<>(); 3 url = url.replace("?", ";"); 4 if (!url.contains(";")) { 5 return map; 6 } 7 if (url.split(";").length > 0) { 8 String[] arr = url.split(";")[1].split("&"); 9 for (String s : arr) { 10 if (s.contains("=")) { 11 String key = s.split("=")[0]; 12 String value = s.split("=")[1]; 13 map.put(key, value); 14 } else { 15 map.put(s, null); 16 } 17 } 18 return map; 19 } else { 20 return map; 21 } 22 }
四、交互实现
1、服务端启动
对于已经初始化好的ServerSocketChannel,我们下来要做的无非就是while(true)轮询selector。这个套路已经非常固定了。这里我们启动一个线程来轮询:
1 public void start() { 2 this.run = true; 3 new Thread(() -> { 4 try { 5 while (run) { 6 selector.select(2000); 7 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 8 while (iterator.hasNext()) { 9 SelectionKey key = iterator.next(); 10 iterator.remove(); 11 // 监听客户端接入 12 if (key.isAcceptable()) { 13 handleAccept(key); 14 } 15 // 监听客户端发送消息 16 else if (key.isReadable()) { 17 handleRead(key); 18 } 19 } 20 } 21 } catch (IOException e) { 22 e.printStackTrace(); 23 } 24 }, "selector-io").start(); 25 }
2、处理客户端接入
1 // 当有客户端接入的时候,为其注册 可读 事件监听,等待客户端发送数据 2 private void handleAccept(SelectionKey key) throws IOException { 3 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); 4 SocketChannel socketChannel = channel.accept(); 5 socketChannel.configureBlocking(false); 6 socketChannel.register(selector, SelectionKey.OP_READ); 7 }
3、处理客户端发送的消息
1 /** 2 * 接收到客户端发送的数据进行处理 3 * 1、将客户端的请求数据取出来,放到ByteArrayOutputStream。 4 * 2、将数据交给Servlet处理。 5 */ 6 private void handleRead(SelectionKey key) throws IOException { 7 final SocketChannel channel = (SocketChannel) key.channel(); 8 ByteBuffer buffer = ByteBuffer.allocate(1024); 9 final ByteArrayOutputStream out = new ByteArrayOutputStream(); 10 while (channel.read(buffer) > 0) { 11 buffer.flip(); 12 out.write(buffer.array(), 0, buffer.limit()); 13 buffer.clear(); 14 } 15 if (out.size() <= 0) { 16 channel.close(); 17 return; 18 } 19 process(channel, out); 20 }
4、业务处理并发送返回数据
1 private void process(SocketChannel channel, ByteArrayOutputStream out) { 2 executor.submit(() -> { 3 try { 4 Request request = decode(out.toByteArray()); 5 Response response = new Response(); 6 if (request.method.equalsIgnoreCase("GET")) { 7 servlet.doGet(request, response); 8 } else { 9 servlet.doPost(request, response); 10 } 11 channel.write(ByteBuffer.wrap(encode(response))); 12 } catch (Throwable e) { 13 e.printStackTrace(); 14 } 15 }); 16 }
五、单元测试
1 @Test 2 public void simpleHttpTest() throws IOException, InterruptedException { 3 SimpleHttpServer simpleHttpServer = new SimpleHttpServer(8080, new HttpServlet() { 4 @Override 5 public void doGet(Request request, Response response) { 6 System.out.println(request.url); 7 response.body="hello_word:" + System.currentTimeMillis(); 8 response.code=200; 9 response.headers=new HashMap<>(); 10 } 11 @Override 12 public void doPost(Request request, Response response) {} 13 }); 14 simpleHttpServer.start(); 15 new CountDownLatch(1).await(); 16 }
六、小结
以上,使用原生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源码部门铺一下技术前提。GIT地址:https://gitee.com/llzx/nio_practice.git