Eric 发布的文章

chrome 插件 SwitchyOmega 突然不能用 ERR_MANDATORY_PROXY_CONFIGURATION_FAILED

今天早上到公司, 任何网站都打不开了, 本地起的服务器本地端口都打不开. 出现如下页面:
not.png

症状

  1. 如果不用 SwitchyOmega 插件, 网站是可以打开的.
  2. 如果单单使用代理能打开墙外网站.
  3. 如果使用 Auto Switch 全都打不开.

我有一个墙外 proxy, 一个公司 PAC script. 然后使用前面2个组装一个 Auto Switch.

分析

根据症状分析 Auto Switch 里面的 公司 PAC script 出问题了. 于是检查 PAC script. 如下图:
php.png

竟然是一个 php 脚本, 这是???

说明公司的 PAC 脚本的服务器应该是 php 的, 它现在直接源文件返回了. 如果查看里面的php 内容, 发现它其实是产生 PAC 的脚本.

最后发现原来是公司这个 PAC 的服务器间歇性出问题, 有时候返回真正的 PAC, 有时候返回 php.

由 Transfer-Encoding chunked 引起的 site issue

Transfer-Encoding: chunked 介绍

Transfer-Encoding 是 HTTP 1.x 版本的一个header, 设置 payload 传输时候的一种编码. 可能的编码格式有: chunked, compress, deflate, gzip. 可以同时设置多个兼容的值. 这个header 只适用于 hop to top, 不适用于整个连接. 如果你想在整个连接上使用压缩算法, 应该使用 Content-Encoding header.

使用 Transfer-Encoding 的例子:

Transfer-Encoding: gzip, chunked

为什么要使用 chunked

假如一开始就知道要传输多长的payload 数据, 就可以使用 Content-Length header.
chunked: 一般因为一开始并不知道要传输多长的payload数据, 所以要一块一块传输, 在每一块的头上标注这一块有多长.
例子:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
11\r\n
Developer Network\r\n
0\r\n
\r\n

payload header

HTTP 1.x 的 header 分为

由 Transfer-Encoding chunked 引起的 site issue

HTTP GET 请求

http GET 请求是最简单的请求类型. 在浏览器输入一个URL, 直接回车, 就是发送一个 http GET 请求. 一个简单的例子:

GET /path/to/resource?query=string HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, sdch
Connection: keep-alive

上面的最后一行header 之后, 会在发送一行只有\r\n 的行, 表示请求结束.

Transfer-Encoding: chunked 介绍

Transfer-Encoding 是 HTTP 1.x 版本的一个header, 设置 payload 传输时候的一种编码. 可能的编码格式有: chunked, compress, deflate, gzip. 可以同时设置多个兼容的值. 这个header 只适用于 hop to top, 不适用于整个连接. 如果你想在整个连接上使用压缩算法, 应该使用 Content-Encoding header.

使用 Transfer-Encoding 的例子:

Transfer-Encoding: gzip, chunked

为什么要使用 chunked

假如一开始就知道要传输多长的payload 数据, 就可以使用 Content-Length header.
chunked: 一般因为一开始并不知道要传输多长的payload数据, 所以要一块一块传输, 在每一块的头上标注这一块有多长.
例子:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
11\r\n
Developer Network\r\n
0\r\n
\r\n

当 HTTP GET 遇到 Transfer-Encoding: chunked

根据上面的介绍, HTTP GET 请求不应该包含 payload. 所以如果在header 里面误发了Transfer-Encoding: chunked,会发生什么事情呢?

不同的服务器可能有不同的处理方式, 有的快速返回, 有的等待接受payload.

Tomcat 的处理方式

根据作者本地 debug 的实践, 到现在为止(20240713), Tomcat 的最新版本仍然是等待接收 chunked payload, 直到 socket read timeout.

这是某个Tomcat 版本等待读取 payload 的栈:

java.lang.Object.wait(Native Method)
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1333)
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1234)
org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:785)
org.apache.coyote.http11.Http11InputBuffer.access$400(Http11InputBuffer.java:41)
org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1185)
org.apache.coyote.http11.filters.ChunkedInputFilter.readBytes(ChunkedInputFilter.java:310)
org.apache.coyote.http11.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:338)
org.apache.coyote.http11.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:164)
org.apache.coyote.http11.filters.ChunkedInputFilter.end(ChunkedInputFilter.java:229)
org.apache.coyote.http11.Http11InputBuffer.endRequest(Http11InputBuffer.java:644)
org.apache.coyote.http11.Http11Processor.endRequest(Http11Processor.java:1184)
org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:430)
org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
java.lang.Thread.run(Thread.java:750) 

Tomcat 的逻辑是先逐行读取 header 信息, 直到读到 \r\n 行, 然后根据header设置需要的 InputFilter 列表(虽然是列表, 可能只有一个). 常见的 InputFilter 有:

  1. VoidInputFilter - 当 GET, HEAD 请求时用.
  2. ChunkedInputFilter - 当 chunked 的时候用.

一个请求的例子

下面是使用 python 写的一个发送 GET 请求并且设置 Transfer-Encoding: chunked 的例子:

import socket
from concurrent.futures import ThreadPoolExecutor
 
# Configuration
host = 'www.tianxiaohui.com'
port = 80
buffer_size = 4096
read_timeout = 100000  # Set read timeout to 10 seconds
 
def call():
    # Create a socket object using IPv4 and TCP protocols
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
    # Set the read timeout on the socket
    client_socket.settimeout(read_timeout)
 
    try:
        # Connect to the server
        client_socket.connect((host, port))
 
        # Prepare the HTTP request data
        http_request = ("GET /sell/marketing/v1/ad_campaign?limit=100&offset=0 HTTP/1.1\r\n"
                        f"Host: {host}\r\n"
                        "accept: application/json, text/json, text/x-json, text/javascript\r\n"
                        "accept-encoding: application/gzip, deflate\r\n"
                        "Transfer-Encoding: chunked\r\n"
                        "\r\n")
 
        # Send the HTTP request to the server
        client_socket.sendall(http_request.encode())
 
        # Receive the response from the server
        response = ''
        while True:
            part = client_socket.recv(buffer_size).decode()
            if not part:
                break
            response += part
 
    except socket.timeout:
        print("Read timed out")
        response = None
    finally:
        # Close the socket
        client_socket.close()
 
    # Return the response
    return response
 
# Number of parallel calls
num_calls = 1
 
# Use ThreadPoolExecutor to execute the calls in parallel
with ThreadPoolExecutor(max_workers=num_calls) as executor:
    # Submit all calls to the executor
    future_calls = [executor.submit(call) for _ in range(num_calls)]
     
    # Wait for all futures to complete and print their results
    for future in future_calls:
        response = future.result()
        if response is not None:
            print("Response:")
            print(response)

如果改成对着本地的 tomcat 调用, 可以看到它等在那里20ms, 这20ms 就是读取完 header 之后, 等待读取 chunked payload, 却迟迟等不来的结果, 最后只有等到 read timeout.

这是在最新的 Tomcat 10.1.25 上得到的栈:

java.lang.Thread.State: TIMED_WAITING (on object monitor)
    at java.lang.Object.wait(java.base@17.0.4.1/Native Method)
    - waiting on <0x000000061a3aea90> (a java.util.concurrent.Semaphore)
    at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1280)
    - locked <0x000000061a3aea90> (a java.util.concurrent.Semaphore)
    at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1181)
    at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:789)
    at org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1195)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.readBytes(ChunkedInputFilter.java:254)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.fill(ChunkedInputFilter.java:295)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.parseChunkHeader(ChunkedInputFilter.java:328)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.doRead(ChunkedInputFilter.java:136)
    at org.apache.coyote.http11.filters.ChunkedInputFilter.end(ChunkedInputFilter.java:181)
    at org.apache.coyote.http11.Http11InputBuffer.endRequest(Http11InputBuffer.java:646)
    at org.apache.coyote.http11.Http11Processor.endRequest(Http11Processor.java:1188)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:429)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
    at java.lang.Thread.run(java.base@17.0.4.1/Thread.java:833)

Java 内存分析工具 MAT 使用技巧

本文列出作者在日常使用 Java 内存分析工具 MAT 的过程中用到的一些技巧

导出长字符串

有时候我们经常要从 heap 中复制出某些很长的字符串, 来观察它到底有哪些数据. 通常我们通过: 在某个字符串上点击右键 - Copy -> Value. 但是通常这个复制出的内容都有长度限制.

比如下面的例子, 我想从 HTTP request 的 HeapByteBuffer 复制出它已经读取请求的内容, 但是通过上面的方法只能复制出几千的字符:
copy.png

但是如果其内容远超这个数量, 拿到的就是部分数据, 根据部分数据可能得出错误的结论.
如何完全导出其内容?
在上面的菜单中选择 -> Save Value To File. 就能导出全部内容.

不过对于上面例子中的 HeapByteBuffer 要特别注意, 它是通过当前的位置(pos)来标记那里是有效数据的, pos 位置之后可能还有数据, 只不过是无效数据.

根据字段值分组统计

SQL 里面有 select * from table_0 group by column_0. 可是 OQL 里面却没有这个语句. 但是MAT 却提供了这样的功能.
根据下面的菜单栏, 就能找到 Group By Value 选项.
groupBy.png

然后填入你想分组的类名字和要分组的字段. 下面以 java.util.regex.PatternnormalizedPattern 来分组:
pattern.png

最终看到每个 normalizedPattern 的统计个数:

p_result.png

http keep-alive 实验

之前一篇讲道客户端和服务端是如何处理 http keep-alive 的, 其中很多都是一笔带过. 本篇补充一些细节.

版本

对于 http keep-alive 的概念, 这里的讨论只局限于 http 1.0, http 1.1. 对于 HTTP/2, HTTP/3 这里的讨论不适用.

http header - Connection & Keep-Alive

http 1.1 里面默认是持久连接. 但是我们可以看到下面的默认情况:
Chrome:

  1. 默认发送 Connection: keep-alive 头, 但是不发 Keep-Alive.

curl:

  1. Ubuntu 上的 curl 7.81.0 默认连 Connection 都没发.
  2. Mac 上的 curl 8.6.0 也没发 Connection header.

python requests:

GET / HTTP/1.1
Host: www.henu.edu.cn
User-Agent: python-requests/2.31.0
Connection: keep-alive

Java:

GET / HTTP/1.1
User-Agent: Java/17.0.4.1
Host: www.henu.edu.cn
Connection: keep-alive

JDK 里面的 Java client 默认的处理细节

Keep-Alive header 里面的 timeout 和 max 分别对应 JDK HttpClient 里面的字段:
timeout - keepAliveTimeout
max - keepAliveConnections.

keepAliveTimeout

首先, 在 HttpClient 里面定义了一个 keepAliveTimeout 字段. JDK 21 到链接:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L136C9-L136C25.

这个字段有4种取值可能:

  1. 正值 - timeout 的秒数, 对应 Keep-Alive header 里面的 timeout 值.
  2. 0 - 对方明确不需要 Keep-Alive.
  3. -1: 需要保持连接, http 1.1 设置或者不设 Connection: keep-alive, 但是没有设置 timeout 值.
  4. -2: 明确在 Keep-alive header 里面设置了 timeout: 0 这个值.

如果把我们上面实验的客户端的结果反过来看成对方发来的response 来看, 都属于上面的第3种: -1 类型.

真实的JDK java 客户端的解析过程在这些代码中:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L907-L917

真正的使用这个值的地方

真正使用这个值的地方在:
https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/KeepAliveCache.java#L162-L177

keepAliveConnections

这个值全部都是在 HttpClient 里面使用的.

parse:

  1. 若头部带来了 Keep-Alivemax 则使用这个值.
  2. 若没带来 max, 则如果使用代理, 则是50, 否则是5.
    细节代码: https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902-L904