Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save parsing twice numeric IPv4 address #5395

Merged
merged 1 commit into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading