netty5/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderUtil.java
Jakob Buchgraber 6fd0a0c55f Faster and more memory efficient headers for HTTP, HTTP/2, STOMP and SPYD. Fixes #3600
Motivation:

We noticed that the headers implementation in Netty for HTTP/2 uses quite a lot of memory
and that also at least the performance of randomly accessing a header is quite poor. The main
concern however was memory usage, as profiling has shown that a DefaultHttp2Headers
not only use a lot of memory it also wastes a lot due to the underlying hashmaps having
to be resized potentially several times as new headers are being inserted.

This is tracked as issue #3600.

Modifications:
We redesigned the DefaultHeaders to simply take a Map object in its constructor and
reimplemented the class using only the Map primitives. That way the implementation
is very concise and hopefully easy to understand and it allows each concrete headers
implementation to provide its own map or to even use a different headers implementation
for processing requests and writing responses i.e. incoming headers need to provide
fast random access while outgoing headers need fast insertion and fast iteration. The
new implementation can support this with hardly any code changes. It also comes
with the advantage that if the Netty project decides to add a third party collections library
as a dependency, one can simply plug in one of those very fast and memory efficient map
implementations and get faster and smaller headers for free.

For now, we are using the JDK's TreeMap for HTTP and HTTP/2 default headers.

Result:

- Significantly fewer lines of code in the implementation. While the total commit is still
  roughly 400 lines less, the actual implementation is a lot less. I just added some more
  tests and microbenchmarks.

- Overall performance is up. The current implementation should be significantly faster
  for insertion and retrieval. However, it is slower when it comes to iteration. There is simply
  no way a TreeMap can have the same iteration performance as a linked list (as used in the
  current headers implementation). That's totally fine though, because when looking at the
  benchmark results @ejona86 pointed out that the performance of the headers is completely
  dominated by insertion, that is insertion is so significantly faster in the new implementation
  that it does make up for several times the iteration speed. You can't iterate what you haven't
  inserted. I am demonstrating that in this spreadsheet [1]. (Actually, iteration performance is
  only down for HTTP, it's significantly improved for HTTP/2).

- Memory is down. The implementation with TreeMap uses on avg ~30% less memory. It also does not
  produce any garbage while being resized. In load tests for GRPC we have seen a memory reduction
  of up to 1.2KB per RPC. I summarized the memory improvements in this spreadsheet [1]. The data
  was generated by [2] using JOL.

- While it was my original intend to only improve the memory usage for HTTP/2, it should be similarly
  improved for HTTP, SPDY and STOMP as they all share a common implementation.

[1] https://docs.google.com/spreadsheets/d/1ck3RQklyzEcCLlyJoqDXPCWRGVUuS-ArZf0etSXLVDQ/edit#gid=0
[2] https://gist.github.com/buchgr/4458a8bdb51dd58c82b4
2015-08-04 17:12:24 -07:00

271 lines
10 KiB
Java

/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public final class HttpHeaderUtil {
/**
* Returns {@code true} if and only if the connection can remain open and
* thus 'kept alive'. This methods respects the value of the
* {@code "Connection"} header first and then the return value of
* {@link HttpVersion#isKeepAliveDefault()}.
*/
public static boolean isKeepAlive(HttpMessage message) {
CharSequence connection = message.headers().get(HttpHeaderNames.CONNECTION);
if (connection != null && HttpHeaderValues.CLOSE.equalsIgnoreCase(connection)) {
return false;
}
if (message.protocolVersion().isKeepAliveDefault()) {
return !HttpHeaderValues.CLOSE.equalsIgnoreCase(connection);
} else {
return HttpHeaderValues.KEEP_ALIVE.equalsIgnoreCase(connection);
}
}
/**
* Sets the value of the {@code "Connection"} header depending on the
* protocol version of the specified message. This getMethod sets or removes
* the {@code "Connection"} header depending on what the default keep alive
* mode of the message's protocol version is, as specified by
* {@link HttpVersion#isKeepAliveDefault()}.
* <ul>
* <li>If the connection is kept alive by default:
* <ul>
* <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
* <li>remove otherwise.</li>
* </ul></li>
* <li>If the connection is closed by default:
* <ul>
* <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
* <li>remove otherwise.</li>
* </ul></li>
* </ul>
*/
public static void setKeepAlive(HttpMessage message, boolean keepAlive) {
HttpHeaders h = message.headers();
if (message.protocolVersion().isKeepAliveDefault()) {
if (keepAlive) {
h.remove(HttpHeaderNames.CONNECTION);
} else {
h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
} else {
if (keepAlive) {
h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
} else {
h.remove(HttpHeaderNames.CONNECTION);
}
}
}
/**
* Returns the length of the content. Please note that this value is
* not retrieved from {@link HttpContent#content()} but from the
* {@code "Content-Length"} header, and thus they are independent from each
* other.
*
* @return the content length
*
* @throws NumberFormatException
* if the message does not have the {@code "Content-Length"} header
* or its value is not a number
*/
public static long getContentLength(HttpMessage message) {
String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
if (value != null) {
return Long.parseLong(value);
}
// We know the content length if it's a Web Socket message even if
// Content-Length header is missing.
long webSocketContentLength = getWebSocketContentLength(message);
if (webSocketContentLength >= 0) {
return webSocketContentLength;
}
// Otherwise we don't.
throw new NumberFormatException("header not found: " + HttpHeaderNames.CONTENT_LENGTH);
}
/**
* Returns the length of the content. Please note that this value is
* not retrieved from {@link HttpContent#content()} but from the
* {@code "Content-Length"} header, and thus they are independent from each
* other.
*
* @return the content length or {@code defaultValue} if this message does
* not have the {@code "Content-Length"} header or its value is not
* a number
*/
public static long getContentLength(HttpMessage message, long defaultValue) {
String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
if (value != null) {
return Long.parseLong(value);
}
// We know the content length if it's a Web Socket message even if
// Content-Length header is missing.
long webSocketContentLength = getWebSocketContentLength(message);
if (webSocketContentLength >= 0) {
return webSocketContentLength;
}
// Otherwise we don't.
return defaultValue;
}
/**
* Returns the content length of the specified web socket message. If the
* specified message is not a web socket message, {@code -1} is returned.
*/
private static int getWebSocketContentLength(HttpMessage message) {
// WebSockset messages have constant content-lengths.
HttpHeaders h = message.headers();
if (message instanceof HttpRequest) {
HttpRequest req = (HttpRequest) message;
if (HttpMethod.GET.equals(req.method()) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2)) {
return 8;
}
} else if (message instanceof HttpResponse) {
HttpResponse res = (HttpResponse) message;
if (res.status().code() == 101 &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_LOCATION)) {
return 16;
}
}
// Not a web socket message
return -1;
}
/**
* Sets the {@code "Content-Length"} header.
*/
public static void setContentLength(HttpMessage message, long length) {
message.headers().set(HttpHeaderNames.CONTENT_LENGTH, length);
}
public static boolean isContentLengthSet(HttpMessage m) {
return m.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
}
/**
* Returns {@code true} if and only if the specified message contains the
* {@code "Expect: 100-continue"} header.
*/
public static boolean is100ContinueExpected(HttpMessage message) {
// Expect: 100-continue is for requests only.
if (!(message instanceof HttpRequest)) {
return false;
}
// It works only on HTTP/1.1 or later.
if (message.protocolVersion().compareTo(HttpVersion.HTTP_1_1) < 0) {
return false;
}
// In most cases, there will be one or zero 'Expect' header.
CharSequence value = message.headers().get(HttpHeaderNames.EXPECT);
if (value == null) {
return false;
}
if (HttpHeaderValues.CONTINUE.equalsIgnoreCase(value)) {
return true;
}
// Multiple 'Expect' headers. Search through them.
return message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
}
/**
* Sets or removes the {@code "Expect: 100-continue"} header to / from the
* specified message. If the specified {@code value} is {@code true},
* the {@code "Expect: 100-continue"} header is set and all other previous
* {@code "Expect"} headers are removed. Otherwise, all {@code "Expect"}
* headers are removed completely.
*/
public static void set100ContinueExpected(HttpMessage message, boolean expected) {
if (expected) {
message.headers().set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
} else {
message.headers().remove(HttpHeaderNames.EXPECT);
}
}
/**
* Checks to see if the transfer encoding in a specified {@link HttpMessage} is chunked
*
* @param message The message to check
* @return True if transfer encoding is chunked, otherwise false
*/
public static boolean isTransferEncodingChunked(HttpMessage message) {
return message.headers().contains(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
}
public static void setTransferEncodingChunked(HttpMessage m, boolean chunked) {
if (chunked) {
m.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
m.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
} else {
// Make a copy to be able to modify values while iterating
List<String> encodings = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
if (encodings.isEmpty()) {
return;
}
List<CharSequence> values = new ArrayList<CharSequence>(encodings);
Iterator<CharSequence> valuesIt = values.iterator();
while (valuesIt.hasNext()) {
CharSequence value = valuesIt.next();
if (HttpHeaderValues.CHUNKED.equalsIgnoreCase(value)) {
valuesIt.remove();
}
}
if (values.isEmpty()) {
m.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
} else {
m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, values);
}
}
}
static void encodeAscii0(CharSequence seq, ByteBuf buf) {
int length = seq.length();
for (int i = 0 ; i < length; i++) {
buf.writeByte(c2b(seq.charAt(i)));
}
}
private static byte c2b(char c) {
if (c > 255) {
return '?';
}
return (byte) c;
}
private HttpHeaderUtil() { }
}