美文网首页
基于java nio实现http服务器

基于java nio实现http服务器

作者: sunpy | 来源:发表于2022-04-28 11:46 被阅读0次

    介绍

    java已经学了很长时间了,从大学看张孝祥的java web到现在各种微服务,真的感觉java要学的东西好多,技术也是变化很快,越学越觉得自己不知道的东西太多。最近心血来潮想写一个http服务器,但是第一次写难免有很多bug,有各种异常,自己也不断解决问题,甚至有些技术平时都看过,但是用起来也错误百出,自己对之前一些技术还是理解的不到位,只是一些表面api,通过这次实践也加深的了认识。并且自己压测的时候,500个线程5s执行还没问题。但是增加到3000个线程5s执行的时候,会出现ConnectionRefusedException异常的情况。

    服务器功能

    1. 解析http协议(GET\POST报文)
    2. 注解处理http请求
    3. 未完待续(后期有时间再实现日志处理、异常处理、开启线程池处理,心跳等)

    包功能

    1. core包:nio实现http协议的核心包。
    2. codec包:http协议的编解码。
      • HttpEncoder将http协议中请求流解码成SunpyRequest对象
      • HttpDecoder将SunpyResponse对象编码成http响应流
    3. etc包:功能扩展包。
      • Worker封装扩展的response响应头。
    4. annotation包:实现自定义注解功能。
      • impl包:注解主要实现功能包。
    5. file包:文件工具操作包、服务器文件配置解析器
    6. constant包:常量定义包
    7. model包:模型传输包。

    使用规则

    1. 配置文件resources/nio-http.properties
      • 配置本机ip:server_ip
      • 配置本机端口:server_port
    2. 用户使用此框架必须在com.sunpy.niohttp.user目录下。
      如果不想使用该目录,在nio-http.properties中配置user_package=com.sunpy.niohttp.users

    实现技术

    1. java sdk:file、nio、classloader、annotation、reflect、properties
    2. cglib
    3. 校验validation
    4. json处理fastjson

    部分代码实现

    core核心实现:

    SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    /**
     * 1. 从通道中解析出http请求对象
     */
    // 将数据读到缓冲区中
    clientChannel.read(buffer);
    // 从缓冲区中存数据状态转换到从缓冲区中取数据状态
    buffer.flip();
    // 将缓冲区中数据解码成SunpyRequest对象
    SunpyRequest sunpyRequest = new HttpDecoder().decodeHttp(buffer);
    // 清空缓存区
    buffer.clear();
    
    /**
     * 2. 查找请求的映射注解,委托代理执行业务逻辑方法
     */
    System.out.println(sunpyRequest);
    SunpyResponse sunpyResponse = new SunpyResponse();
    new RequestProxy(sunpyRequest, sunpyResponse).doServiceByProxy();
    System.out.println(sunpyResponse);
    
    /**
     * 4. 将http协议的响应对象使用通道发送给客户端
     */
    // 将SunpyResponse对象编码成byte数组
    byte[] response = new HttpEncoder().encodeHttp(sunpyResponse);
    // 将byte字节数组存入到缓冲区中
    buffer.put(response);
    // 注册写事件
    selectionKey.interestOps(SelectionKey.OP_WRITE);
    // 从缓冲区中存的状态转换为从缓冲区中取的状态
    buffer.flip();
    
    // 只要缓冲区中position到limit之间的数据没写完,就一直往通道写入数据
    while (buffer.hasRemaining()) {
        clientChannel.write(buffer);
    }
    
    buffer.clear();
    clientChannel.close();
    

    编码实现:

    public class HttpEncoder {
    
    
        /**
         * HTTP/1.1 200 OK\r\n
         * Server: Apache-Coyote/1.1\r\n
         * Set-Cookie: SHAREJSESSIONID=8a0e17c5-379f-4fc1-85e0-81eb6056f480; Path=/; HttpOnly\r\n
         * Set-Cookie: rememberMe=deleteMe; Path=/mch; Max-Age=0; Expires=Thu, 17-Jan-2019 08:18:45 GMT\r\n
         * Content-Type: application/json;charset=UTF-8\r\n
         * Content-Length: 16\r\n
         * Date: Fri, 18 Jan 2019 08:18:45 GMT\r\n
         * \r\n
         * HTTP response 1/2
         * [Time since request: 0.052455000 seconds]
         * [Request in frame: 397]
         * [Next request in frame: 402]
         * [Next response in frame: 448]
         * File Data: 16 bytes
         */
        public String encodeHttpToStr(SunpyResponse response) {
            StringBuilder sb = new StringBuilder();
            // 响应行
            sb.append(response.getVersion()+ " ");
            sb.append(response.getCode()+ " ");
            sb.append(response.getStatus()+ "\n");
            // 响应头
            Map<java.lang.String, String> headers = response.getHeaders();
    
            headers.forEach((k, v) -> {
                sb.append(k + ":" + v);
                sb.append("\n");
            });
            // 空行
            sb.append("\n");
            // 响应体
            sb.append(response.getBody());
            return sb.toString();
        }
    
    
        public byte[] encodeHttp(SunpyResponse response) throws Exception {
            return encodeHttpToStr(response).getBytes();
        }
    }
    

    解码实现:

    public class HttpDecoder {
    
        /**
         * POST / HTTP/1.1
         * username: zhangsan
         * cache-control: no-cache
         * Postman-Token: 63dde61d-efbf-4aa7-9b7a-732f1c608603
         * Content-Type: text/plain
         * User-Agent: PostmanRuntime/7.1.1
         * Accept: **
         * Host:127.0.0.1:9999
         * accept-encoding:gzip,deflate
         * content-length:34
         * Connection:keep-alive
         *
         *{"level":5,"age":23,"name":"lisi"}
         */
        public SunpyRequest decodeHttp(ByteBuffer buffer) throws IOException {
            SunpyRequest request =new SunpyRequest();
    
            BufferedReader br =new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buffer.array())));
            // 第一行数据请求行
            String lineData =br.readLine();
            String[] lineArr = lineData.split(" ", 3);
            request.setMethod(lineArr[0]);
            request.setUri(lineArr[1]);
            request.setVersion(lineArr[2]);
            // 第二行数据请求头
            String headerData = br.readLine();
            while (headerData != null && !headerData.equals("")) {
                String[] headerArr = headerData.split(":", 2);
                request.getHeaders().put(headerArr[0], headerArr[1]);
                headerData = br.readLine();
            }
            // 第三行空行不解析,第四行请求体
            StringBuilder sb = new StringBuilder();
            String bodyData = br.readLine();
            while (bodyData != null && !bodyData.equals("")) {
                sb.append(bodyData);
                bodyData = br.readLine();
            }
    
            request.setBody(sb.toString());
            return request;
        }
    }
    

    遇见问题

    • ByteBuffer中的flip、clear等函数的操作使用方式:
      https://www.jianshu.com/p/7731e7e5c59d
    • Http协议中响应报文头中Content-Length字段如果比实际传入的响应报文体body长度小:body内容出现乱码。
    • 反射中invoke方法执行指定的方法,对应传递参数值,必须类型符合(一点都不能差,完全匹配),如果没有值,也要传递默认值,如String或其他对象传null,基本数据类型int传0等。
    • 反射中getMethod方法获取指定的方法,对应的参数类型也必须符合。
    • cglib代理,intercept拦截方法中代理执行方法返回值类型,必须与被代理方法返回值类型保持一致,如果不知道返回类型,可以直接写成Object。切记不要写成不可强制类型转换的东西。
    • java nio中已经在通道注册了SelectionKey.OP_READ读的键,可以从通道读数据了。如果还想要通过该通道写数据就必须要注册写的键(selectionKey.interestOps(SelectionKey.OP_WRITE);),否则根本写不了。
    • 通道的异步处理问题:
      ServerSocketChannel服务器端的通道,配置了异步方式,如果获取客户端通道SocketChannel,发现对方没有需要立即过来的数据,就会默认返回null。如果同步的话,就会阻塞。
      配置异步方式:
    serverSocketChannel.configureBlocking(false);
    

    所以获取到客户端通道SocketChannel之后,那么需要判断其是否为null。否则出现空指针异常。

    SocketChannel clientChannel = acceptSocketChannel.accept();
    /**
     * 前面接收数据请求是非阻塞的,但是接收数据的accept方法是阻塞的。
     * 所以采用线程池来读写通道中的数据
     */
    if (clientChannel != null) {
        // 客户端通道设置为非阻塞
        clientChannel.configureBlocking(false);
    
        // 该通道注册为读键
        clientChannel.register(selector, SelectionKey.OP_READ);
    }
    

    相关文章

      网友评论

          本文标题:基于java nio实现http服务器

          本文链接:https://www.haomeiwen.com/subject/juriertx.html