Skip to content

Commit 21498ca

Browse files
franz1981vietj
authored andcommitted
Save parsing twice numeric IPv4 address
1 parent e48ed03 commit 21498ca

File tree

7 files changed

+220
-41
lines changed

7 files changed

+220
-41
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.core.net.impl;
13+
14+
import java.util.concurrent.TimeUnit;
15+
16+
import org.openjdk.jmh.annotations.Benchmark;
17+
import org.openjdk.jmh.annotations.BenchmarkMode;
18+
import org.openjdk.jmh.annotations.Fork;
19+
import org.openjdk.jmh.annotations.Measurement;
20+
import org.openjdk.jmh.annotations.Mode;
21+
import org.openjdk.jmh.annotations.OutputTimeUnit;
22+
import org.openjdk.jmh.annotations.Param;
23+
import org.openjdk.jmh.annotations.Scope;
24+
import org.openjdk.jmh.annotations.Setup;
25+
import org.openjdk.jmh.annotations.State;
26+
import org.openjdk.jmh.annotations.Warmup;
27+
28+
@State(Scope.Thread)
29+
@BenchmarkMode(Mode.AverageTime)
30+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
31+
@Warmup(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
32+
@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
33+
@Fork(2)
34+
public class HostAndPortBenchmark {
35+
36+
@Param("192.168.0.1:8080")
37+
private String host;
38+
39+
@Setup
40+
public void setup() {
41+
}
42+
43+
44+
@Benchmark
45+
public int parseIPv4Address() {
46+
String host = this.host;
47+
return HostAndPortImpl.parseIPv4Address(host, 0, host.length());
48+
}
49+
50+
@Benchmark
51+
public int parseHost() {
52+
String host = this.host;
53+
return HostAndPortImpl.parseHost(host, 0, host.length());
54+
}
55+
56+
@Benchmark
57+
public HostAndPortImpl parseAuthority() {
58+
return HostAndPortImpl.parseAuthority(host, -1);
59+
}
60+
61+
@Benchmark
62+
public boolean isValidAuthority() {
63+
return HostAndPortImpl.isValidAuthority(host);
64+
}
65+
}

vertx-core/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.vertx.core.net.NetSocket;
3434
import io.vertx.core.net.SocketAddress;
3535
import io.vertx.core.internal.concurrent.InboundMessageQueue;
36+
import io.vertx.core.net.impl.HostAndPortImpl;
3637
import io.vertx.core.spi.metrics.HttpServerMetrics;
3738
import io.vertx.core.spi.tracing.SpanKind;
3839
import io.vertx.core.spi.tracing.TagExtractor;
@@ -51,6 +52,8 @@
5152
*/
5253
public class Http1xServerRequest extends HttpServerRequestInternal implements io.vertx.core.spi.observability.HttpRequest {
5354

55+
private static final HostAndPort NULL_HOST_AND_PORT = HostAndPort.create("", -1);
56+
5457
private final Http1xServerConnection conn;
5558
final ContextInternal context;
5659

@@ -216,12 +219,38 @@ public String query() {
216219
}
217220

218221
@Override
219-
public synchronized HostAndPort authority() {
222+
public boolean isValidAuthority() {
223+
HostAndPort authority = this.authority;
224+
if (authority == NULL_HOST_AND_PORT) {
225+
return false;
226+
}
227+
if (authority != null) {
228+
return true;
229+
}
230+
String host = getHeader(HttpHeaderNames.HOST);
231+
if (host == null || !HostAndPortImpl.isValidAuthority(host)) {
232+
this.authority = NULL_HOST_AND_PORT;
233+
return false;
234+
}
235+
return true;
236+
}
237+
238+
@Override
239+
public HostAndPort authority() {
240+
HostAndPort authority = this.authority;
241+
if (authority == NULL_HOST_AND_PORT) {
242+
return null;
243+
}
220244
if (authority == null) {
221245
String host = getHeader(HttpHeaderNames.HOST);
222-
if (host != null) {
223-
authority = HostAndPort.parseAuthority(host, -1);
246+
if (host == null) {
247+
this.authority = NULL_HOST_AND_PORT;
248+
return null;
224249
}
250+
// it's fine to have a benign race here as long as HostAndPort is immutable
251+
// to ensure safe publication
252+
authority = HostAndPort.parseAuthority(host, -1);
253+
this.authority = authority;
225254
}
226255
return authority;
227256
}
@@ -240,13 +269,15 @@ public Http1xServerResponse response() {
240269

241270
@Override
242271
public MultiMap headers() {
272+
MultiMap headers = this.headers;
243273
if (headers == null) {
244274
HttpHeaders reqHeaders = request.headers();
245275
if (reqHeaders instanceof MultiMap) {
246276
headers = (MultiMap) reqHeaders;
247277
} else {
248278
headers = new HeadersAdaptor(reqHeaders);
249279
}
280+
this.headers = headers;
250281
}
251282
return headers;
252283
}

vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestInternal.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,10 @@ public abstract class HttpServerRequestInternal implements HttpServerRequest {
3232
*/
3333
public abstract Object metric();
3434

35+
/**
36+
* This method act as {@link #authority()}{@code != null}, trying to not allocated a new object if the authority is not yet parsed.
37+
*/
38+
public boolean isValidAuthority() {
39+
return authority() != null;
40+
}
3541
}

vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ public HostAndPort authority() {
110110
return delegate.authority();
111111
}
112112

113+
@Override
114+
public boolean isValidAuthority() {
115+
return delegate.isValidAuthority();
116+
}
117+
113118
@Override
114119
public long bytesRead() {
115120
return delegate.bytesRead();

vertx-core/src/main/java/io/vertx/core/net/impl/HostAndPortImpl.java

Lines changed: 97 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
package io.vertx.core.net.impl;
22

3+
import java.util.Arrays;
4+
35
import io.vertx.core.net.HostAndPort;
46

57
public class HostAndPortImpl implements HostAndPort {
68

9+
// digits lookup table to speed-up parsing
10+
private static final byte[] DIGITS = new byte[128];
11+
12+
static {
13+
Arrays.fill(DIGITS, (byte) -1);
14+
for (int i = '0';i <= '9';i++) {
15+
DIGITS[i] = (byte) (i - '0');
16+
}
17+
}
18+
719
public static int parseHost(String val, int from, int to) {
820
int pos;
921
if ((pos = parseIPLiteral(val, from, to)) != -1) {
@@ -30,42 +42,60 @@ public static int parseIPv4Address(String s, int from, int to) {
3042
return -1;
3143
}
3244
}
33-
return from < to && (from + 1 == s.length() || s.charAt(from + 1) != ':') ? -1 : from;
45+
// from is the next position to parse: whatever come before is a valid IPv4 address
46+
if (from == to) {
47+
// we're done
48+
return from;
49+
}
50+
assert from < to;
51+
// we have more characters, let's check if it has enough space for a port
52+
if (from + 1 == s.length()) {
53+
// just a single character left, we don't care what it is
54+
return -1;
55+
}
56+
// we have more characters
57+
if (s.charAt(from) != ':') {
58+
// we need : to start a port
59+
return -1;
60+
}
61+
// we (maybe) have a port - even with a single digit; the ipv4 addr is fineFi
62+
return from;
3463
}
3564

3665
public static int parseDecOctet(String s, int from, int to) {
3766
int val = parseDigit(s, from++, to);
38-
switch (val) {
39-
case 0:
40-
return from;
41-
case 1:
42-
case 2:
43-
case 3:
44-
case 4:
45-
case 5:
46-
case 6:
47-
case 7:
48-
case 8:
49-
case 9:
50-
int n = parseDigit(s, from, to);
51-
if (n != -1) {
52-
val = val * 10 + n;
53-
n = parseDigit(s, ++from, to);
54-
if (n != -1) {
55-
from++;
56-
val = val * 10 + n;
57-
}
58-
}
59-
if (val < 256) {
60-
return from;
61-
}
67+
if (val == 0) {
68+
return from;
69+
}
70+
if (val < 0 || val > 9) {
71+
return -1;
72+
}
73+
int n = parseDigit(s, from, to);
74+
if (n != -1) {
75+
val = val * 10 + n;
76+
n = parseDigit(s, ++from, to);
77+
if (n != -1) {
78+
from++;
79+
val = val * 10 + n;
80+
}
81+
}
82+
if (val < 256) {
83+
return from;
6284
}
6385
return -1;
6486
}
6587

6688
private static int parseDigit(String s, int from, int to) {
67-
char c;
68-
return from < to && isDIGIT(c = s.charAt(from)) ? c - '0' : -1;
89+
if (from >= to) {
90+
return -1;
91+
}
92+
char ch = s.charAt(from);
93+
// a very predictable condition
94+
if (ch < 128) {
95+
// negative short values are still positive ints
96+
return DIGITS[ch];
97+
}
98+
return -1;
6999
}
70100

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

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

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

140+
/**
141+
* Validate an authority HTTP header, that is <i>host [':' port]</i> <br>
142+
* This method should behave like {@link #parseAuthority(String, int)},
143+
* but without the overhead of creating an object: when {@code true}
144+
* {@code parseAuthority(s, -1)} should return a non-null value.
145+
*
146+
* @param s the string to parse
147+
* @return {@code true} when the string is a valid authority
148+
* @throws NullPointerException when the string is {@code null}
149+
*/
150+
public static boolean isValidAuthority(String s) {
151+
int pos = parseHost(s, 0, s.length());
152+
if (pos == s.length()) {
153+
return true;
154+
}
155+
if (pos < s.length() && s.charAt(pos) == ':') {
156+
return parsePort(s, pos) != -1;
157+
}
158+
return false;
159+
}
160+
110161
/**
111162
* Parse an authority HTTP header, that is <i>host [':' port]</i>
112163
* @param s the string to parse
@@ -120,22 +171,30 @@ public static HostAndPortImpl parseAuthority(String s, int schemePort) {
120171
}
121172
if (pos < s.length() && s.charAt(pos) == ':') {
122173
String host = s.substring(0, pos);
123-
int port = 0;
124-
while (++pos < s.length()) {
125-
int digit = parseDigit(s, pos, s.length());
126-
if (digit == -1) {
127-
return null;
128-
}
129-
port = port * 10 + digit;
130-
if (port > 65535) {
131-
return null;
132-
}
174+
int port = parsePort(s, pos);
175+
if (port == -1) {
176+
return null;
133177
}
134178
return new HostAndPortImpl(host, port);
135179
}
136180
return null;
137181
}
138182

183+
private static int parsePort(String s, int pos) {
184+
int port = 0;
185+
while (++pos < s.length()) {
186+
int digit = parseDigit(s, pos, s.length());
187+
if (digit == -1) {
188+
return -1;
189+
}
190+
port = port * 10 + digit;
191+
if (port > 65535) {
192+
return -1;
193+
}
194+
}
195+
return port;
196+
}
197+
139198
private final String host;
140199
private final int port;
141200

vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5499,6 +5499,8 @@ public void testEmptyHostPortionOfHostHeader() throws Exception {
54995499

55005500
private void testEmptyHostPortionOfHostHeader(String hostHeader, int expectedPort) throws Exception {
55015501
server.requestHandler(req -> {
5502+
assertEquals("", req.authority().host());
5503+
assertTrue(((HttpServerRequestInternal) req).isValidAuthority());
55025504
assertEquals("", req.authority().host());
55035505
assertEquals(expectedPort, req.authority().port());
55045506
req.response().end();
@@ -5516,6 +5518,7 @@ private void testEmptyHostPortionOfHostHeader(String hostHeader, int expectedPor
55165518
public void testMissingHostHeader() throws Exception {
55175519
server.requestHandler(req -> {
55185520
assertEquals(null, req.authority());
5521+
assertFalse(((HttpServerRequestInternal) req).isValidAuthority());
55195522
testComplete();
55205523
});
55215524
startServer(testAddress);

0 commit comments

Comments
 (0)