前面设计的心跳服务端所有读写包括业务处理都是在一个线程中同步完成的。因为心跳业务操作没有几乎没有任何阻塞延时操作,以单个线程足够了。如果中间的业务处理是一个非常耗时操作如:查询数据库。 这将导致所有请求都被会阻塞。所以会将耗时会采用异步线程池来完成。接下来一起设计一个Http服务并实现。
一、Http协议处理流程
http协议是一个超文本协议(rpc是二进制的协议),并且是一个半双工协议,只能由客户端主动发,服务端被动响应。其整个处理流程如下图:
流程说明:
- 客户端发起请求,并根据协议封装请求头与请求参数。
- 服务端接受连接
- 读取请求数据包
- 将数据包decode解码成 HttpRequest对象
- 业务处理(异步),并将结果封装成HttpResponse
- 基于协议编码HttpResponse
- 将编码后的数据写入管道
Http协议报文
实现一个协议必须先了解它的协议报文,Http协议作为一款文本协议(),报文相对简单,都是基于换行符、回车符 以及空格与帽号进行分割,具体如下图:
请求报文:
响应报文:
Http服务端整体设计
为了延续大家对Servlet的使用习惯,设计了Serverlt类来处理具体的业务请求,而SimpeHttpServer用于读写数据,并根据Http协议进行编解码,并包装成Request与Response发送给Servlet。
参考文档
服务端的线程模型
轮询选择器、建立连接、读写消息,逻辑实现与心跳服务一至,不同地方在于在IO线程中多了两个操作,解码(decode)和编码(encode)都是在IO线程中完成,而耗时的业务操作则交给多线程进行异步处理。
IO接收连接交给work线程work线程处理完交给IO线程返给前端
解码 decode
当触发读取事件时,表示有请求到达,IO线程读取相关数据包并基于协议解码 请求头、URL、参数等数据并封装成Request对象。然后在线程池中提交一个任务,用于处理业务。
public class SimpleHttpServer {
private final Selector selector;
int port;
private Set<SocketChannel> allConnections = new HashSet<>();
volatile boolean run = false;
HttpServlet servlet;
ExecutorService executor = Executors.newFixedThreadPool(5);
public SimpleHttpServer(int port, HttpServlet servlet) throws IOException {
this.port = port;
this.servlet = servlet;
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
selector = Selector.open();
listenerChannel.bind(new InetSocketAddress(port));
listenerChannel.configureBlocking(false);
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public Thread start() {
run = true;
Thread thread = new Thread(() -> {
try {
while (run) {
dispatch();
}
} catch (IOException e) {
e.printStackTrace();
}
}, "selector-io");
thread.start();
return thread;
}
public void stop(int delay) {
run = false;
}
private void dispatch() throws IOException {
int select = selector.select(2000);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
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();
continue;
}
System.out.println("当前通道:"+channel);
//解码
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();
}
});
}
}
}
// 解码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;
}
//编码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 ("Connection: close\r\n");// 执行完后关闭链接
builder.append("\r\n").append(response.body);
return builder.toString().getBytes();
}
public abstract static class HttpServlet {
abstract void doGet(Request request, Response response);
abstract void doPost(Request request, Response response);
}
public static class Request {
Map<String, String> heads;
String url;
String method;
String version;
String body; //请求内容
Map<String, String> params;
}
public static class Response {
Map<String, String> headers;
int code;
String body; //返回结果
}
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;
}
}
}
从代码中可以看出 整个读取事件会处理4件事情:
- 读取管道中数据
- 解码并封装request
- 构建response 并附着于当前key,这么做的目的是让IO线程,能够在业务线程处理完毕后顺利拿到返回结果
- 提交任务至线程池,在任务中异步调用servlet.service()方法,然后在改写key的监听为OP_WRITE。接下来需调用 wakeup() IO线程,因为很有可能IO线程这会正处于select(long time) 阻塞状态,从而导致写入延时。
业务处理
基于Request获取URL、参数等数据,以处理该请求。然后将结果写回Response。
编码encode
当业务处理完成后,即会手动触发OP_WRITE事件,这时处理步骤如下:
5. 从key的附参加取出Response
6. Response并进行编码,封装状态码、状态消息、以及响应头和消息体
7. 将编码后的数据写到管道
8. 将监听改为OP_READ,继续监听下一个请求