基于java nio实现http服务器
2022-04-28 本文已影响0人
sunpy
介绍
java已经学了很长时间了,从大学看张孝祥的java web到现在各种微服务,真的感觉java要学的东西好多,技术也是变化很快,越学越觉得自己不知道的东西太多。最近心血来潮想写一个http服务器,但是第一次写难免有很多bug,有各种异常,自己也不断解决问题,甚至有些技术平时都看过,但是用起来也错误百出,自己对之前一些技术还是理解的不到位,只是一些表面api,通过这次实践也加深的了认识。并且自己压测的时候,500个线程5s执行还没问题。但是增加到3000个线程5s执行的时候,会出现ConnectionRefusedException异常的情况。
服务器功能
- 解析http协议(GET\POST报文)
- 注解处理http请求
- 未完待续(后期有时间再实现日志处理、异常处理、开启线程池处理,心跳等)
包功能
- core包:nio实现http协议的核心包。
- codec包:http协议的编解码。
- HttpEncoder将http协议中请求流解码成SunpyRequest对象
- HttpDecoder将SunpyResponse对象编码成http响应流
- etc包:功能扩展包。
- Worker封装扩展的response响应头。
- annotation包:实现自定义注解功能。
- impl包:注解主要实现功能包。
- file包:文件工具操作包、服务器文件配置解析器
- constant包:常量定义包
- model包:模型传输包。
使用规则
- 配置文件resources/nio-http.properties
- 配置本机ip:server_ip
- 配置本机端口:server_port
- 用户使用此框架必须在com.sunpy.niohttp.user目录下。
如果不想使用该目录,在nio-http.properties中配置user_package=com.sunpy.niohttp.users
实现技术
- java sdk:file、nio、classloader、annotation、reflect、properties
- cglib
- 校验validation
- 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);
}