Skip to content

Commit

Permalink
Save parsing twice numeric IPv4 address
Browse files Browse the repository at this point in the history
  • Loading branch information
franz1981 authored and vietj committed Nov 18, 2024
1 parent e48ed03 commit afc4393
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/

package io.vertx.core.net.impl;

import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Fork(2)
public class HostAndPortBenchmark {

@Param("192.168.0.1:8080")
private String host;

@Setup
public void setup() {
}


@Benchmark
public int parseIPv4Address() {
String host = this.host;
return HostAndPortImpl.parseIPv4Address(host, 0, host.length());
}

@Benchmark
public int parseHost() {
String host = this.host;
return HostAndPortImpl.parseHost(host, 0, host.length());
}

@Benchmark
public HostAndPortImpl parseAuthority() {
return HostAndPortImpl.parseAuthority(host, -1);
}

@Benchmark
public boolean isValidAuthority() {
return HostAndPortImpl.isValidAuthority(host);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;
import io.vertx.core.internal.concurrent.InboundMessageQueue;
import io.vertx.core.net.impl.HostAndPortImpl;
import io.vertx.core.spi.metrics.HttpServerMetrics;
import io.vertx.core.spi.tracing.SpanKind;
import io.vertx.core.spi.tracing.TagExtractor;
Expand All @@ -51,6 +52,8 @@
*/
public class Http1xServerRequest extends HttpServerRequestInternal implements io.vertx.core.spi.observability.HttpRequest {

private static final HostAndPort NULL_HOST_AND_PORT = HostAndPort.create("", -1);

private final Http1xServerConnection conn;
final ContextInternal context;

Expand Down Expand Up @@ -216,12 +219,38 @@ public String query() {
}

@Override
public synchronized HostAndPort authority() {
public boolean isValidAuthority() {
HostAndPort authority = this.authority;
if (authority == NULL_HOST_AND_PORT) {
return false;
}
if (authority != null) {
return true;
}
String host = getHeader(HttpHeaderNames.HOST);
if (host == null || !HostAndPortImpl.isValidAuthority(host)) {
this.authority = NULL_HOST_AND_PORT;
return false;
}
return true;
}

@Override
public HostAndPort authority() {
HostAndPort authority = this.authority;
if (authority == NULL_HOST_AND_PORT) {
return null;
}
if (authority == null) {
String host = getHeader(HttpHeaderNames.HOST);
if (host != null) {
authority = HostAndPort.parseAuthority(host, -1);
if (host == null) {
this.authority = NULL_HOST_AND_PORT;
return null;
}
// it's fine to have a benign race here as long as HostAndPort is immutable
// to ensure safe publication
authority = HostAndPort.parseAuthority(host, -1);
this.authority = authority;
}
return authority;
}
Expand All @@ -240,13 +269,15 @@ public Http1xServerResponse response() {

@Override
public MultiMap headers() {
MultiMap headers = this.headers;
if (headers == null) {
HttpHeaders reqHeaders = request.headers();
if (reqHeaders instanceof MultiMap) {
headers = (MultiMap) reqHeaders;
} else {
headers = new HeadersAdaptor(reqHeaders);
}
this.headers = headers;
}
return headers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ public abstract class HttpServerRequestInternal implements HttpServerRequest {
*/
public abstract Object metric();

/**
* This method act as {@link #authority()}{@code != null}, trying to not allocated a new object if the authority is not yet parsed.
*/
public boolean isValidAuthority() {
return authority() != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ public HostAndPort authority() {
return delegate.authority();
}

@Override
public boolean isValidAuthority() {
return delegate.isValidAuthority();
}

@Override
public long bytesRead() {
return delegate.bytesRead();
Expand Down
135 changes: 97 additions & 38 deletions vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
package io.vertx.core.net.impl;

import java.util.Arrays;

import io.vertx.core.net.HostAndPort;

public class HostAndPortImpl implements HostAndPort {

// digits lookup table to speed-up parsing
private static final byte[] DIGITS = new byte[128];

static {
Arrays.fill(DIGITS, (byte) -1);
for (int i = '0';i <= '9';i++) {
DIGITS[i] = (byte) (i - '0');
}
}

public static int parseHost(String val, int from, int to) {
int pos;
if ((pos = parseIPLiteral(val, from, to)) != -1) {
Expand All @@ -30,42 +42,60 @@ public static int parseIPv4Address(String s, int from, int to) {
return -1;
}
}
return from < to && (from + 1 == s.length() || s.charAt(from + 1) != ':') ? -1 : from;
// from is the next position to parse: whatever come before is a valid IPv4 address
if (from == to) {
// we're done
return from;
}
assert from < to;
// we have more characters, let's check if it has enough space for a port
if (from + 1 == s.length()) {
// just a single character left, we don't care what it is
return -1;
}
// we have more characters
if (s.charAt(from) != ':') {
// we need : to start a port
return -1;
}
// we (maybe) have a port - even with a single digit; the ipv4 addr is fineFi
return from;
}

public static int parseDecOctet(String s, int from, int to) {
int val = parseDigit(s, from++, to);
switch (val) {
case 0:
return from;
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
int n = parseDigit(s, from, to);
if (n != -1) {
val = val * 10 + n;
n = parseDigit(s, ++from, to);
if (n != -1) {
from++;
val = val * 10 + n;
}
}
if (val < 256) {
return from;
}
if (val == 0) {
return from;
}
if (val < 0 || val > 9) {
return -1;
}
int n = parseDigit(s, from, to);
if (n != -1) {
val = val * 10 + n;
n = parseDigit(s, ++from, to);
if (n != -1) {
from++;
val = val * 10 + n;
}
}
if (val < 256) {
return from;
}
return -1;
}

private static int parseDigit(String s, int from, int to) {
char c;
return from < to && isDIGIT(c = s.charAt(from)) ? c - '0' : -1;
if (from >= to) {
return -1;
}
char ch = s.charAt(from);
// a very predictable condition
if (ch < 128) {
// negative short values are still positive ints
return DIGITS[ch];
}
return -1;
}

public static int parseIPLiteral(String s, int from, int to) {
Expand Down Expand Up @@ -96,7 +126,7 @@ private static boolean isALPHA(char ch) {
}

private static boolean isDIGIT(char ch) {
return ('0' <= ch && ch <= '9');
return DIGITS[ch] != -1;
}

private static boolean isSubDelims(char ch) {
Expand All @@ -107,6 +137,27 @@ static boolean isHEXDIG(char ch) {
return isDIGIT(ch) || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f');
}

/**
* Validate an authority HTTP header, that is <i>host [':' port]</i> <br>
* This method should behave like {@link #parseAuthority(String, int)},
* but without the overhead of creating an object: when {@code true}
* {@code parseAuthority(s, -1)} should return a non-null value.
*
* @param s the string to parse
* @return {@code true} when the string is a valid authority
* @throws NullPointerException when the string is {@code null}
*/
public static boolean isValidAuthority(String s) {
int pos = parseHost(s, 0, s.length());
if (pos == s.length()) {
return true;
}
if (pos < s.length() && s.charAt(pos) == ':') {
return parsePort(s, pos) != -1;
}
return false;
}

/**
* Parse an authority HTTP header, that is <i>host [':' port]</i>
* @param s the string to parse
Expand All @@ -120,22 +171,30 @@ public static HostAndPortImpl parseAuthority(String s, int schemePort) {
}
if (pos < s.length() && s.charAt(pos) == ':') {
String host = s.substring(0, pos);
int port = 0;
while (++pos < s.length()) {
int digit = parseDigit(s, pos, s.length());
if (digit == -1) {
return null;
}
port = port * 10 + digit;
if (port > 65535) {
return null;
}
int port = parsePort(s, pos);
if (port == -1) {
return null;
}
return new HostAndPortImpl(host, port);
}
return null;
}

private static int parsePort(String s, int pos) {
int port = 0;
while (++pos < s.length()) {
int digit = parseDigit(s, pos, s.length());
if (digit == -1) {
return -1;
}
port = port * 10 + digit;
if (port > 65535) {
return -1;
}
}
return port;
}

private final String host;
private final int port;

Expand Down
3 changes: 3 additions & 0 deletions vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5499,6 +5499,8 @@ public void testEmptyHostPortionOfHostHeader() throws Exception {

private void testEmptyHostPortionOfHostHeader(String hostHeader, int expectedPort) throws Exception {
server.requestHandler(req -> {
assertEquals("", req.authority().host());
assertTrue(((HttpServerRequestInternal) req).isValidAuthority());
assertEquals("", req.authority().host());
assertEquals(expectedPort, req.authority().port());
req.response().end();
Expand All @@ -5516,6 +5518,7 @@ private void testEmptyHostPortionOfHostHeader(String hostHeader, int expectedPor
public void testMissingHostHeader() throws Exception {
server.requestHandler(req -> {
assertEquals(null, req.authority());
assertFalse(((HttpServerRequestInternal) req).isValidAuthority());
testComplete();
});
startServer(testAddress);
Expand Down
Loading

0 comments on commit afc4393

Please sign in to comment.