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