从此

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 均已支持。

Nginx

综合:
  Debian 13 默认Nginx版本为1.26;首选多支持一些额外模块的 nginx-full;若基本使用只需安装 nginx-core(即nginx)。
  命令:
    禁用开机启动 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);

Jetty最精简HttpServer: 
    // implementation("org.eclipse.jetty:jetty-server:12.0.16")
    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();
        server.addConnector(sc);
        server.setHandler(new Handler.Abstract() {
            @Override
            public boolean handle(Request request, Response response, Callback callback) {
                System.out.println("handle complete - http://localhost:8080/");
                callback.succeeded();
                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

其他