Nginx反向代理、HAProxy负载均衡(h2c)、Java HTTP Client、Jetty Web Server、NanoHTTPD Web Server
综合/最新
常用配置:
反向代理 -
Nginx:location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
HAProxy:frontend f1
mode http
bind :80
acl a-name hdr(host) -i domain.name
use_backend b1 if a-name
backend b1
mode http
server s1 localhost:8080
代理间交互:启用 PROXY protocol 来传递 原始客户端 IP:Port 信息,比用 X-Forwarded-For 传递更标准化,Nginx 和 HAProxy 均已支持。
Let's Encrypt 证书:
cert.pem 域名证书文件。
chain.pem 中间证书链文件,用于验证证书的完整性。
fullchain.pem 完整证书链文件,即 cert.pem + fullchain.pem,亲测可用于 Nginx、HAProxy 配置。
privkey.pem 私钥文件,切勿泄露。
生成KeyStore文件: openssl pkcs12 -export -out keystore.p12 -inkey privkey.pem -in fullchain.pem -password pass:secret -name optional-alias
Nginx
综合:
Debian 13 默认Nginx版本为1.26;首选多支持一些额外模块的 nginx-full;若基本使用只需安装 nginx-core(即nginx)。
错误页面 return 404; 若要直接关闭连接(防DDOS攻击)可用非标准的 return 444; 浏览器报 net::ERR_EMPTY_RESPONSE,curl 报 Empty reply from server。
命令:
禁用开机启动 sudo systemctl disable nginx
巡查:
访问日志 sudo cat /var/log/nginx/access.log
报错日志 sudo cat /var/log/nginx/error.log
会走HTTP/1.X头Upgrade至HTTP/2的ALPN协商流程: curl --http2 localhost:8080
说明 - Connection头此处是为了告诉中间代理,Upgrade头仅用于首跳使用,不要原样向上游传。
Connection: Upgrade
Upgrade: h2
不走HTTP/1.X头Upgrade至HTTP/2的ALPN协商流程(无需配置证书): curl --http2-prior-knowledge localhost:8080 会输出 HTTP/2.0、request.isSecure() == false
server {
listen 8080 http2;
location / {
return 200 "it's ok!";
}
}
http跳转https:
server {
listen 80 default_server; server_name _;
return 301 https://$host$request_uri;
}
反向代理:
proxy_pass 缓存状态取决于目标网页的响应头配置,或自定义proxy_cache。
最简配置:
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
最佳实践:
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
location = /manifest.json {
default_type application/json;
return 200 '{"name": "应用名字", "id": "唯一标识"}';
}
上传文件Size限制:
location ^~ /main/apis/more/default/public/upload-to-storage {
try_files $uri @backend;
client_max_body_size 10m; # 暂用前端限制,Nginx只设最大值。
}
CORS跨域:
location / {
# Access-Control-Allow-Origin、Access-Control-Allow-Credentials 必须同时存在于预检和实际响应中。
add_header 'Access-Control-Allow-Origin' '*'; # 允许所有域名跨域
#add_header 'Access-Control-Allow-Credentials' 'true'; # 若 fetch 配置了 credentials: "include" 则必须加上。
# GET, POST, OPTIONS 之外的请求,浏览器才会预检;缓存时效为2小时。
if ($request_method = 'OPTIONS') {
#add_header 'Access-Control-Allow-Methods' 'DELETE, PUT'; # GET, POST, OPTIONS 已列入白名单,只需写上额外请求方法。
#add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control'; # [限制传入头]未列入则预检失败。
add_header 'Access-Control-Max-Age' 7200;
add_header 'Content-Length' 0;
return 204;
}
#if ($request_method = 'POST') {
# add_header 'Access-Control-Expose-Headers' 'Content-Range'; # [不参与预检/只存在于实际响应]告知fetch置null未列入的响应头。
#}
# 其他配置 - 支持 try_files $uri @backend;
proxy_pass http://backend;
proxy_set_header Host $host;
}
SSE流式通讯:
浏览器以listen段配置的HTTP/2协议为准,且SSE从HTTP/1.1起就支持了,且 proxy_pass 对SSE的连接数未做限制,故影响不到浏览器的连接数限制(6个/HTTP2默认100个)。
location /sse {
proxy_pass http://ip:9080; # SSE不支持try_files指令
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off; # 或业务端控制 response.setHeader("X-Accel-Buffering", "no");
proxy_read_timeout 12h; # Nginx默认1分钟。
}
WebSocket配置:
location ^~ /websocket/ {
proxy_pass http://ip:9080; # 支持try_files指令?
proxy_set_header Host $host;
proxy_http_version 1.1;
#proxy_set_header Connection "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
#proxy_read_timeout 12h; # Nginx默认1分钟;要么加长超时,要么上WebSocket Ping Pong机制。
}
HTTP Web C/S
- Java HTTP Client
SSE客户端则为jakarta.ws.rs.sse.SseEventSource,或java.net.http.HttpClient响应至超时,若存在结束标记,则可调HttpClient.shutdownNow()避免一直等到超时。 implementation("org.glassfish.jersey.media:jersey-media-sse:4.0.0-M2") implementation("org.apache.cxf:cxf-rt-rs-sse:4.1.0") H2C: Netty需要显式设置HttpClient.create().protocol(HttpProtocol.H2C); 而 java.net.http.HttpClient 则不需要。var bv=Base64.getEncoder().encodeToString("clientId:clientSecret".getBytes()); var r = HttpClient.newHttpClient().send(HttpRequest.newBuilder( URI.create("https://example.com/oauth2/token")) .POST(HttpRequest.BodyPublishers.ofString("grant_type=refresh_token&refresh_token=" + rt)) .header("Content-Type", "application/x-www-form-urlencoded") .header("Authorization", "Basic " + bv) // or JS - btoa("clientId:clientSecret") .build(), HttpResponse.BodyHandlers.ofString()).body(); System.out.println(r); POST multipart/form-data数据: // implementation("org.apache.httpcomponents.client5:httpclient5:5.4.2") var me = MultipartEntityBuilder.create().addTextBody("substringLength", "36") //.addTextBody("dataText", "x", ContentType.TEXT_PLAIN.withCharset(Charset.defaultCharset())) .build(); var baos = new ByteArrayOutputStream(); me.writeTo(baos); var r = HttpClient.newHttpClient().send(HttpRequest.newBuilder( java.net.URI.create("https://localhost/path/url-slug")) .POST(HttpRequest.BodyPublishers.ofByteArray(baos.toByteArray())) //ContentType.MULTIPART_FORM_DATA.withParameters(new BasicNameValuePair("boundary", ctb)).toString() .header("Content-Type", me.getContentType()) .build(), HttpResponse.BodyHandlers.ofString()).body(); System.out.println(r);- Jetty Servlet Server
Jetty反向代理: org.eclipse.jetty.ee10.proxy.AsyncProxyServlet 或 兼容javax或jakarta的Servlet代理库 解决 ResourceHandler + setDirAllowed(true) 报 “Ambiguous URI empty segment”: server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY); 或 new HttpConfiguration().setUriCompliance(UriCompliance.LEGACY); 配置TLS证书; var scf = new SslContextFactory.Server(); scf.setKeyStorePassword("x"); scf.setKeyStorePath("C:\\keystore.p12"); new ServerConnector(server, scf, new ALPNServerConnectionFactory(), new HTTP2ServerConnectionFactory(hc)); Jetty最精简HttpServer: // implementation(platform("org.eclipse.jetty:jetty-bom:12.1.2")) // 未包含 org.eclipse.jetty.ee11 等库。 // implementation("org.eclipse.jetty:jetty-server") public static void main(String[] args) throws Exception { var server = new Server(); // Android 14 及早期版本均报 Didn't find class "java.util.ServiceLoader$Provider",可换用 jetty-server:9.x 或 NanoHTTPD;Android 15 则添加了该类;desugar_jdk_libs 补全不了。 var sc = new ServerConnector(server); sc.setPort(8080); // 入参0则端口由系统分配,取端口号:int port = ((ServerConnector) ss.getConnectors()[0]).getLocalPort(); // 或 new ServerConnector(server, sslContextFactory, new ALPNServerConnectionFactory(), new HTTP2ServerConnectionFactory(new HttpConfiguration())); server.addConnector(sc); connector.addEventListener(new NetworkConnector.Listener() { @Override public void onOpen(NetworkConnector nc) { System.out.println(nc.getLocalPort()); } }); // [可选] 通过监听器获取端口号。 server.setHandler(new Handler.Abstract() { @Override public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { System.out.println("handle"); callback.succeeded(); // org.eclipse.jetty.io.Content.copy(request, response, callback); return true; } }); /* // Servlet - implementation("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16") var context = new ServletContextHandler(); context.addServlet(staticServlet.class, "/"); server.setHandler(context); var rh = new ResourceHandler(); rh.setBaseResourceAsString(tmpDir); rh.setDirAllowed(true); // 目录别斜杠结尾,否则会变双斜杠。 server.setHandler(new ContextHandler(rh, "/.well-known/tmp")); */ server.start(); } public static class staticServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.getWriter().println("x"); } }- NanoHTTPD Web Server
implementation("org.nanohttpd:nanohttpd-webserver:2.3.1") // https://github.com/NanoHttpd/nanohttpd/blob/master/webserver/src/main/java/org/nanohttpd/webserver/SimpleWebServer.java var server = new fi.iki.elonen.SimpleWebServer("localhost", 9458, MainActivity.this.getFilesDir(), true, "*"); try { server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } catch (IOException ioe) { System.err.println("Couldn't start server:\n" + ioe); //System.exit(-1); }
HAProxy
说明:
HAProxy 1.9+ 支持 h2c,而 Nginx proxy_pass 最高只支持到 HTTP/1.1;默认上传文件大小未做限制。
frontend 配置浏览器至 HAProxy 的访问协议,backend 配置 HAProxy 反向代理的访问协议;mode 默认值为 tcp,建站则通常配置为 mode http,其值http含https。
官方文档 - https://www.haproxy.org
安装/管理:
sudo apt -y install haproxy
sudo cp /etc/haproxy/haproxy.cfg .
说明 - 该配置文件未配置frontend和backend,需自行配置;default_backend 为兜底配置。
sudo vim.tiny /etc/haproxy/haproxy.cfg
frontend f1
mode http # 默认值为 tcp
bind :80 # http to https; 可与bind :443 ...并存
#http-request redirect scheme https unless { ssl_fc }
#bind :443 ssl crt /etc/haproxy/our.pem alpn h2,http/1.1
acl a-name hdr(host) -i domain.name # 若 bind :8443 则 acl 要写明端口 domain.name:8443,证书不分端口,Service Worker 也能跑在 8443 上。
use_backend b1 if a-name
default_backend b2
backend b1
mode http
server s1 23.192.228.84:80
backend b2
mode http
server s2 23.192.228.84:80
sudo systemctl restart haproxy
curl.exe -HHost:example.com http://34.169.255.233 -I
用法:
隐式 and 条件:if { path_beg /images/ } { method POST } 若显式写上 and 则报:no such ACL : 'and'
自头匹配用 path_beg;精确匹配用 path:
http-request redirect code 301 location https://tsc.openle.com/.well-known/temporary/favicon-temporary.ico if { path /favicon.ico }
取正则表达式匹配部分:
http-request redirect code 301 location /?url=%[path,'regsub("^/main/apis/more/risky/mixed-content/(.*)$","\1")'] if risky-path
# capture.req.uri指完整URL。
http-request set-var(txn.user_id) capture.req.uri
# field指按冒号分隔取第1个。
http-request set-var(txn.user_id) capture.req.uri,field(1,:)
http-request redirect code 301 location /?url=%[var(txn.user_id)] if risky-path
H2C 配置:
说明 - H2C Servlet request.getProtocol() 输出 HTTP/2.0 即配置正确;即 mode http + proto h2(缩写自h2c/已解析https请求体)。
backend h2c
mode http
server s1 192.168.0.10:80 proto h2