vip.justlive.oxygen.web.server.aio.HttpServerAioHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of oxygen-web Show documentation
Show all versions of oxygen-web Show documentation
web support of light framework
/*
* Copyright (C) 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package vip.justlive.oxygen.web.server.aio;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import vip.justlive.oxygen.core.exception.Exceptions;
import vip.justlive.oxygen.core.util.base.Bytes;
import vip.justlive.oxygen.core.util.base.HttpHeaders;
import vip.justlive.oxygen.core.util.base.Strings;
import vip.justlive.oxygen.core.util.net.aio.AioHandler;
import vip.justlive.oxygen.core.util.net.aio.ChannelContext;
import vip.justlive.oxygen.core.util.net.http.HttpMethod;
import vip.justlive.oxygen.web.Context;
import vip.justlive.oxygen.web.http.Request;
import vip.justlive.oxygen.web.http.Response;
import vip.justlive.oxygen.web.router.RoutingContext;
import vip.justlive.oxygen.web.router.RoutingContextImpl;
/**
* http aio处理
*
* @author wubo
*/
@Slf4j
@RequiredArgsConstructor
public class HttpServerAioHandler implements AioHandler {
private static final int MAX = 2048;
private final String contextPath;
@Override
public ByteBuffer encode(Object data, ChannelContext channelContext) {
Response response = (Response) data;
Bytes bytes = new Bytes();
// 状态行
bytes.write(response.getRequest().getProtocol()).write(Bytes.SPACE)
.write(Integer.toString(response.getStatus())).write(Bytes.CR).write(Bytes.LF);
byte[] body = new byte[0];
if (response.getOut() != null) {
body = response.getOut().toByteArray();
}
String connection = response.getRequest().getHeader(HttpHeaders.CONNECTION);
if (HttpHeaders.CONNECTION_KEEP_ALIVE.equalsIgnoreCase(connection)) {
response.setHeader(HttpHeaders.CONNECTION, HttpHeaders.CONNECTION_KEEP_ALIVE);
}
// content-length
response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(body.length));
// content-type
if (response.getHeader(HttpHeaders.CONTENT_TYPE) == null && response.getContentType() != null) {
String contentType = response.getContentType();
if (!contentType.contains(HttpHeaders.CHARSET)) {
contentType += Strings.SEMICOLON.concat(HttpHeaders.CHARSET).concat(Strings.EQUAL)
.concat(response.getEncoding());
}
response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
}
// 响应头
response.getHeaders().forEach(
(k, v) -> bytes.write(k).write(Bytes.COLON).write(Bytes.SPACE).write(v).write(Bytes.CR)
.write(Bytes.LF));
// cookies
response.getCookies().values()
.forEach(cookie -> bytes.write(cookie.toBytes()).write(Bytes.CR).write(Bytes.LF));
bytes.write(Bytes.CR).write(Bytes.LF);
// 响应体
if (body.length > 0) {
if (log.isDebugEnabled() && body.length > MAX) {
log.debug("http response [{}*mask*]", bytes.toString());
}
bytes.write(body);
if (log.isDebugEnabled() && body.length <= MAX) {
log.debug("http response [{}]", bytes.toString());
}
}
return ByteBuffer.wrap(bytes.toArray());
}
@Override
public Object decode(ByteBuffer buffer, int readableSize, ChannelContext channelContext) {
int index = buffer.position();
RequestBuilder builder = parseStatusText(buffer);
if (builder == null) {
return null;
}
if (Strings.hasText(contextPath) && !builder.requestUri.startsWith(contextPath)) {
throw Exceptions.fail(String
.format("RequestUri [%s] not match contextPath [%s]", builder.requestUri, contextPath));
}
if (parseHeaders(builder, buffer) && parseBody(builder, buffer)) {
Request request = builder.build(channelContext);
if (log.isDebugEnabled()) {
log.debug("Received Http [{}]",
new String(buffer.array(), index, buffer.position(), StandardCharsets.UTF_8));
}
return request;
}
return null;
}
@Override
public void handle(Object data, ChannelContext channelContext) {
Request request = (Request) data;
Response response = new Response(request);
if (request.getMethod() == HttpMethod.UNKNOWN) {
response.write("method is not supported");
channelContext.write(response);
return;
}
request.local();
response.local();
final RoutingContext ctx = new RoutingContextImpl(request, response);
try {
Context.dispatch(ctx);
} catch (Exception e) {
Context.routeError(ctx, e);
} finally {
Request.clear();
Response.clear();
channelContext.write(response);
}
}
private RequestBuilder parseStatusText(ByteBuffer buffer) {
int position = buffer.position();
String method = null;
String requestUrl = null;
byte[] data;
while (buffer.hasRemaining()) {
byte b = buffer.get();
if (b == ' ') {
int curr = buffer.position();
data = new byte[curr - position - 1];
getData(data, buffer, curr, position);
position = curr;
String line = new String(data, StandardCharsets.UTF_8);
if (method == null) {
method = line;
} else if (requestUrl == null) {
requestUrl = line;
} else {
break;
}
} else if (lineFinished(buffer, b)) {
int curr = buffer.position();
data = new byte[curr - position - 2];
getData(data, buffer, curr, position);
return new RequestBuilder(method, requestUrl, new String(data, StandardCharsets.UTF_8));
}
}
return null;
}
private boolean parseHeaders(RequestBuilder builder, ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
return false;
}
int position = buffer.position();
if (headerFinished(buffer)) {
return true;
}
String name = null;
byte[] data;
while (buffer.hasRemaining()) {
byte b = buffer.get();
int curr = buffer.position();
if (name == null && b == Bytes.COLON) {
data = new byte[curr - position - 1];
getData(data, buffer, curr, position);
name = new String(data, StandardCharsets.UTF_8);
position = curr;
} else if (name != null && lineFinished(buffer, b)) {
data = new byte[curr - position - 2];
getData(data, buffer, curr, position);
builder.addHeader(name, new String(data, StandardCharsets.UTF_8).trim());
name = null;
position = curr;
if (headerFinished(buffer)) {
return true;
}
}
}
return false;
}
private boolean parseBody(RequestBuilder builder, ByteBuffer buffer) {
String contentLength = builder.getContentLength();
if (contentLength != null) {
int length = Integer.parseInt(contentLength);
if (buffer.remaining() < length) {
return false;
}
if (length == 0) {
return true;
}
builder.body = new byte[length];
buffer.get(builder.body);
return true;
}
// 找不到contentLength当没有body处理
return true;
}
private boolean headerFinished(ByteBuffer buffer) {
return buffer.get() == Bytes.CR && buffer.hasRemaining() && buffer.get() == Bytes.LF;
}
private boolean lineFinished(ByteBuffer buffer, byte curr) {
return curr == Bytes.LF && buffer.get(buffer.position() - 2) == Bytes.CR;
}
private void getData(byte[] data, ByteBuffer buffer, int curr, int index) {
buffer.position(index);
buffer.get(data);
buffer.position(curr);
}
@RequiredArgsConstructor
private class RequestBuilder {
final String method;
final String requestUri;
final String version;
final Map> headers = new LinkedHashMap<>();
byte[] body;
void addHeader(String key, String value) {
List values = headers.computeIfAbsent(key, k -> new ArrayList<>(1));
if (!values.contains(value)) {
values.add(value);
}
}
String getContentLength() {
for (Map.Entry> entry : headers.entrySet()) {
if (entry.getKey().equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
return entry.getValue().get(0);
}
}
return null;
}
Request build(ChannelContext channelContext) {
Request request = new Request(HttpMethod.find(method), requestUri, version, contextPath,
body);
String[] arr = new String[0];
headers.forEach((k, v) -> request.getHeaders().put(k, v.toArray(arr)));
request.addAttribute(Request.ORIGINAL_REQUEST, channelContext);
return request;
}
}
}