基于java nio实现http服务器

2022-04-28  本文已影响0人  sunpy

介绍

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;
    }
}

遇见问题

serverSocketChannel.configureBlocking(false);

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

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

    // 该通道注册为读键
    clientChannel.register(selector, SelectionKey.OP_READ);
}
上一篇下一篇

猜你喜欢

热点阅读