背景
我们通常是用nginx作为代理服务器,把用户的请求转发到实际的application服务器,作为简单的转发是可以这么做的,但是有时候,我们还需要一些业务处理,那么就需要自己手写代理服务器了,以下介绍使用Java怎么写代理服务器,可以用来代理普通http请求、SSE请求、文件下载请求。
准备
pom文件的依赖如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.sse</groupId>
<artifactId>sseproxy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
代理普通HTTP请求
使用Spring自带的RestTemplate来代理普通的http请求
@Autowired
private RestTemplate restTemplate;
/**
* forward common http request
* @param body
* @param method
* @param request
* @return
* @throws URISyntaxException
*/
@GetMapping("/sseproxy2")
public ResponseEntity<String> sseproxy2(@RequestBody(required = false) String body,
HttpMethod method,
HttpServletRequest request) throws URISyntaxException {
String server = "localhost";
int port = 8080;
URI uri = new URI("http", null, server, port, "/sse", request.getQueryString(), null);
MultiValueMap<String, String> headers = null;
HttpEntity<String> entity = new HttpEntity<>(body, headers);
try {
ResponseEntity<String> responseEntity =
restTemplate.exchange(uri, method, entity, String.class);
return responseEntity;
} catch (HttpClientErrorException ex) {
return ResponseEntity
.status(ex.getStatusCode())
.headers(ex.getResponseHeaders())
.body(ex.getResponseBodyAsString());
}
}
代理SSE
使用SseEmitter代理SSE请求
/**
* forward request of type SSE
*
* @return
*/
@GetMapping("/sseproxy")
public SseEmitter sseproxy() {
SseEmitter sseEmitter = new SseEmitter();
WebClient webClient = WebClient.create("http://localhost:8080/");
ParameterizedTypeReference<ServerSentEvent<String>> type = new ParameterizedTypeReference<>() {};
Flux<ServerSentEvent<String>> eventStream = webClient.get()
.uri("/sse")
.retrieve()
.bodyToFlux(type);
eventStream.subscribe(ctx -> {
logger.info("Current time: {}, content [{}]", LocalTime.now(), ctx.data());
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("SSE MVC - " + LocalTime.now().toString())
.name("sse event - mvc");
try {
sseEmitter.send(event);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return sseEmitter;
}
文件下载
package com.example.sse.demo.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ProxyUtil {
private static final Logger logger = LoggerFactory.getLogger(ProxyUtil.class);
/**
* a proxy for download type
*
* @param response
*/
public static void download(HttpServletResponse response,
CloseableHttpClient httpClient,
String url,
Map<String, Object> bodyParams) throws IOException {
logger.info("start download proxy for url:{}, bodyParams:{}", url, bodyParams.toString());
response.setContentType("application/octet-stream");
OutputStream out = null;
BufferedReader in = null;
try {
out = response.getOutputStream();
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-type", "application/json");
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
for (String key : bodyParams.keySet()) {
node.put(key, String.valueOf(bodyParams.get(key)));
}
httpPost.setEntity(new StringEntity(node.toString(), ContentType.APPLICATION_JSON));
in = new BufferedReader(
new InputStreamReader(httpClient.execute(httpPost).getEntity().getContent()));
logger.info("forward data:");
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
out.write(inputLine.getBytes(StandardCharsets.UTF_8));
out.flush();
}
} catch (IOException e) {
logger.error("proxy download encountered error:" + e.getMessage());
throw new RuntimeException(e);
} finally {
out.close();
out.flush();
in.close();
}
logger.info("end download proxy");
}
}
调用端:
@GetMapping("/downloadproxy")
public void downloadproxy(HttpServletResponse response) throws IOException {
Map<String, Object> bodyParams = new HashMap<>();
bodyParams.put("username", "xiaohei");
bodyParams.put("age", 3);
ProxyUtil.download(response,
HttpClients.createDefault(),
"http://127.0.0.1:8080/sse3",
bodyParams
);
}
参考
- SSE 全称Server Sent Event,直译一下就是服务器发送事件
- https://zhuanlan.zhihu.com/p/614824613
- https://stackoverflow.com/questions/14726082/spring-mvc-rest-service-redirect-forward-proxy
- https://copyprogramming.com/howto/how-to-stream-response-body-with-apache-httpclient#what-is-the-difference-between-httpclient-and-httpasyncclient