Skip to content

Commit 175c596

Browse files
committed
feat: 基于vertx实现http反向代理的demo
1 parent ced6501 commit 175c596

File tree

3 files changed

+235
-0
lines changed

3 files changed

+235
-0
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
<groupId>io.vertx</groupId>
3333
<artifactId>vertx-core</artifactId>
3434
<version>4.5.10</version>
35+
<scope>provided</scope>
3536
</dependency>
3637
<dependency>
3738
<groupId>io.vertx</groupId>
3839
<artifactId>vertx-web</artifactId>
3940
<version>4.5.10</version>
41+
<scope>provided</scope>
4042
</dependency>
4143
</dependencies>
4244

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package top.meethigher.proxy.http;
2+
3+
import io.vertx.core.AsyncResult;
4+
import io.vertx.core.Handler;
5+
import io.vertx.core.MultiMap;
6+
import io.vertx.core.Vertx;
7+
import io.vertx.core.http.*;
8+
import io.vertx.ext.web.Route;
9+
import io.vertx.ext.web.Router;
10+
import io.vertx.ext.web.RoutingContext;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.util.Arrays;
15+
16+
public class ReverseDemo {
17+
18+
private static final Logger log = LoggerFactory.getLogger(ReverseDemo.class);
19+
20+
/**
21+
* 不应该被复制的逐跳标头
22+
*/
23+
protected final String[] hopByHopHeaders = new String[]{
24+
"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization",
25+
"TE", "Trailers", "Transfer-Encoding", "Upgrade"};
26+
27+
28+
protected boolean isHopByHopHeader(String headerName) {
29+
for (String hopByHopHeader : hopByHopHeaders) {
30+
if (hopByHopHeader.equalsIgnoreCase(headerName)) {
31+
return true;
32+
}
33+
}
34+
return false;
35+
}
36+
37+
/**
38+
* 复制请求头。复制的过程中忽略逐跳标头
39+
*/
40+
protected void copyRequestHeaders(HttpServerRequest realReq, HttpClientRequest proxyReq) {
41+
MultiMap realHeaders = realReq.headers();
42+
MultiMap proxyHeaders = proxyReq.headers();
43+
proxyHeaders.clear();
44+
for (String headerName : realHeaders.names()) {
45+
// 若是逐跳标头,则跳过
46+
if (isHopByHopHeader(headerName)) {
47+
continue;
48+
}
49+
// 针对Host请求头进行忽略
50+
if ("host".equalsIgnoreCase(headerName)) {
51+
continue;
52+
}
53+
proxyHeaders.set(headerName, realHeaders.get(headerName));
54+
}
55+
}
56+
57+
/**
58+
* 复制响应头。复制的过程中忽略逐跳标头
59+
*/
60+
protected void copyResponseHeaders(HttpServerResponse realResp, HttpClientResponse proxyResp) {
61+
MultiMap proxyHeaders = proxyResp.headers();
62+
MultiMap realHeaders = realResp.headers();
63+
realHeaders.clear();
64+
for (String headerName : proxyHeaders.names()) {
65+
// 若是逐跳标头,则跳过
66+
if (isHopByHopHeader(headerName)) {
67+
continue;
68+
}
69+
realHeaders.set(headerName, proxyHeaders.get(headerName));
70+
}
71+
}
72+
73+
/**
74+
* 发起请求Handler
75+
*/
76+
protected Handler<AsyncResult<HttpClientResponse>> sendRequestHandler(HttpServerRequest realReq, HttpServerResponse realResp, String realUrl) {
77+
return ar -> {
78+
if (ar.succeeded()) {
79+
HttpClientResponse proxyResp = ar.result();
80+
// 复制响应头。复制的过程中忽略逐跳标头
81+
copyResponseHeaders(realResp, proxyResp);
82+
if (!realResp.headers().contains("Content-Length")) {
83+
realResp.setChunked(true);
84+
}
85+
proxyResp.pipeTo(realResp);
86+
log.info("{} {} {}", realReq.method().name(), realUrl, realResp.getStatusCode());
87+
} else {
88+
Throwable e = ar.cause();
89+
log.error("{} {} send request error", realReq.method().name(), realUrl, e);
90+
}
91+
};
92+
}
93+
94+
/**
95+
* 建立连接Handler
96+
*/
97+
protected Handler<AsyncResult<HttpClientRequest>> connectHandler(HttpServerRequest realReq, HttpServerResponse realResp, String realUrl) {
98+
return ar -> {
99+
if (ar.succeeded()) {
100+
HttpClientRequest proxyReq = ar.result();
101+
// 复制请求头。复制的过程中忽略逐跳标头
102+
copyRequestHeaders(realReq, proxyReq);
103+
// 若存在请求体,则将请求体复制。使用流式复制,避免占用大量内存
104+
if (proxyReq.headers().contains("Content-Length") || proxyReq.headers().contains("Transfer-Encoding")) {
105+
realReq.pipeTo(proxyReq);
106+
}
107+
// 发送请求
108+
proxyReq.send().onComplete(sendRequestHandler(realReq, realResp, realUrl));
109+
} else {
110+
Throwable e = ar.cause();
111+
log.error("{} {} open connection error", realReq.method().name(), realUrl, e);
112+
}
113+
114+
};
115+
}
116+
117+
public void test(HttpServer httpServer, HttpClient httpClient, Router router) {
118+
String source = "/test/*";
119+
String target = "https://reqres.in";
120+
UrlParser.ParsedUrl targetUrl = UrlParser.parseUrl(target);
121+
router.route(source).handler(routingContextHandler(httpClient, targetUrl));
122+
}
123+
124+
private Handler<RoutingContext> routingContextHandler(HttpClient httpClient, UrlParser.ParsedUrl targetUrl) {
125+
return ctx -> {
126+
HttpServerRequest realReq = ctx.request();
127+
HttpServerResponse realResp = ctx.response();
128+
Route route = ctx.currentRoute();
129+
String path = route.getPath();
130+
String routePathTrim = path.replaceAll("^/+", "");
131+
// vertx的uri()是包含query参数的。而path()才是我们常说的不带有query的uri
132+
String realUrl = UrlParser
133+
.getUrl(new UrlParser.ParsedUrl(
134+
targetUrl.isSsl,
135+
targetUrl.host,
136+
targetUrl.port,
137+
targetUrl.uri + realReq.path().replace(routePathTrim, ""),
138+
realReq.query()
139+
));
140+
141+
// 构建请求参数
142+
RequestOptions requestOptions = new RequestOptions();
143+
requestOptions.setAbsoluteURI(realUrl);
144+
requestOptions.setMethod(realReq.method());
145+
146+
// 请求
147+
httpClient.request(requestOptions).onComplete(connectHandler(realReq, realResp, realUrl));
148+
};
149+
}
150+
151+
public static void main(String[] args) {
152+
ReverseDemo reverseDemo = new ReverseDemo();
153+
154+
Vertx vertx = Vertx.vertx();
155+
156+
HttpServerOptions httpServerOptions = new HttpServerOptions();
157+
httpServerOptions.setHttp2ClearTextEnabled(true).setAlpnVersions(Arrays.asList(HttpVersion.HTTP_2));
158+
159+
HttpServer httpServer = vertx.createHttpServer(httpServerOptions);
160+
HttpClient httpClient = vertx.createHttpClient();
161+
162+
Router router = Router.router(vertx);
163+
reverseDemo.test(httpServer, httpClient, router);
164+
165+
httpServer.requestHandler(router).listen(8080).onFailure(e -> {
166+
log.error("server start failed", e);
167+
});
168+
}
169+
170+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package top.meethigher.proxy.http;
2+
3+
import java.net.URI;
4+
5+
public class UrlParser {
6+
7+
/**
8+
* 拼接两个uri。不要求uri以"/"结尾
9+
*/
10+
public static String joinURI(String uri1, String uri2) {
11+
if (uri1.endsWith("/") && uri2.startsWith("/")) {
12+
// 两边都有 '/'
13+
return uri1 + uri2.substring(1);
14+
} else if (!uri1.endsWith("/") && !uri2.startsWith("/")) {
15+
// 两边都没有 '/'
16+
return uri1 + "/" + uri2;
17+
} else {
18+
// 只有一个有 '/'
19+
return uri1 + uri2;
20+
}
21+
}
22+
23+
public static ParsedUrl parseUrl(String url) {
24+
try {
25+
URI uri = new URI(url);
26+
boolean isSsl = "https".equalsIgnoreCase(uri.getScheme());
27+
String host = uri.getHost();
28+
int port = uri.getPort() != -1 ? uri.getPort() : (isSsl ? 443 : 80);
29+
String path = uri.getRawPath();
30+
String query = uri.getRawQuery();
31+
32+
return new ParsedUrl(isSsl, host, port, path, query);
33+
} catch (Exception e) {
34+
throw new IllegalArgumentException("Invalid URL: " + url, e);
35+
}
36+
}
37+
38+
public static String getUrl(ParsedUrl parsedUrl) {
39+
return parsedUrl.getUrl();
40+
}
41+
42+
public static class ParsedUrl {
43+
public final boolean isSsl;
44+
public final String host;
45+
public final int port;
46+
public final String uri;
47+
public final String query;
48+
49+
public ParsedUrl(boolean isSsl, String host, int port, String uri,
50+
String query) {
51+
this.isSsl = isSsl;
52+
this.host = host;
53+
this.port = port;
54+
this.uri = uri.replaceAll("/+$", "");
55+
this.query = query;
56+
}
57+
58+
59+
public String getUrl() {
60+
return (isSsl ? "https" : "http") + "://" + host + ":" + port + uri + (query == null ? "" : ("?" + query));
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)