压缩后的 Java String 的长度与作为 WebSocket 消息发送时的 content-length 不相等

Gid*_*eon 5 javascript java string character-encoding websocket

我试图通过压缩String我通过 WebSocket 从 Springboot 应用程序发送到浏览器客户端的 JSON (这是在permessage-deflateWebSocket 扩展之上)来减少带宽消耗。此场景使用以下String长度为 383 个字符的JSON :

{"headers":{},"body":{"message":{"errors":{"password":"Password length must be at least 8 characters.","retype":"Retype Password cannot be null.","username":"Username length must be between 6 to 64 characters."},"links":[],"success":false,"target":{"password":"","retype":"","username":""}},"target":"/user/session/signup"},"statusCode":"UNPROCESSABLE_ENTITY","statusCodeValue":422}
Run Code Online (Sandbox Code Playgroud)

为了进行基准测试,我从服务器发送压缩和未压缩的字符串,如下所示:

Object response = …,

SimpMessageHeaderAccessor simpHeaderAccessor =
    SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
simpHeaderAccessor.setSessionId(sessionId);
simpHeaderAccessor.setContentType(new MimeType("application", "json",
    StandardCharsets.UTF_8));
simpHeaderAccessor.setLeaveMutable(true);
// Sends the uncompressed message.
messagingTemplate.convertAndSendToUser(sessionId, uri, response,
    simpHeaderAccessor.getMessageHeaders());

ObjectMapper mapper = new ObjectMapper();
String jsonString;

try {
    jsonString = mapper.writeValueAsString(response);
}
catch(JsonProcessingException e) {
    jsonString = response.toString();
}

log.info("The payload is application/json.");
log.info("uncompressed payload (" + jsonString.length() + " character):");
log.info(jsonString);

String lzStringCompressed = LZString.compress(jsonString);
simpHeaderAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
simpHeaderAccessor.setSessionId(sessionId);
simpHeaderAccessor.setContentType(new MimeType("text", "plain",
    StandardCharsets.UTF_8));
simpHeaderAccessor.setLeaveMutable(true);
// Sends the compressed message.
messagingTemplate.convertAndSendToUser(sessionId, uri, lzStringCompressed,
    simpHeaderAccessor.getMessageHeaders());

log.info("The payload is text/plain.");
log.info("compressed payload (" + lzStringCompressed.length() + " character):");
log.info(lzStringCompressed);
Run Code Online (Sandbox Code Playgroud)

在 Java 控制台中记录以下行:

The payload is application/json.
uncompressed payload (383 character):
{"headers":{},"body":{"message":{"errors":{"password":"Password length must be at least 8 characters.","retype":"Retype Password cannot be null.","username":"Username length must be between 6 to 64 characters."},"links":[],"success":false,"target":{"password":"","retype":"","username":""}},"target":"/user/session/signup"},"statusCode":"UNPROCESSABLE_ENTITY","statusCodeValue":422}
The payload is text/plain.
compressed payload (157 character):
??????????¼??????????????p??!-??7??????????????????????????????????u??????????????????????·}???????????????????????????????????????/??R??b,??????m??????????
Run Code Online (Sandbox Code Playgroud)

然后浏览器收到服务器发送的两条消息,并被这个javascript捕获:

stompClient.connect({}, function(frame) {
    stompClient.subscribe(stompClientUri, function(payload) {
        try {
            JSON.parse(payload.body);
            console.log("The payload is application/json.");
            console.log("uncompressed payload (" + payload.body.length + " character):");
            console.log(payload.body);

            payload = JSON.parse(payload.body);
        } catch (e) {
            try {
                payload = payload.body;
                console.log("The payload is text/plain.");
                console.log("compressed payload (" + payload.length + " character):");
                console.log(payload);

                var decompressPayload = LZString.decompress(payload);
                console.log("decompressed payload (" + decompressPayload.length + " character):");
                console.log(decompressPayload);

                payload = JSON.parse(decompressPayload);
            } catch (e) {
            } finally {
            }
        } finally {
        }
    });
});
Run Code Online (Sandbox Code Playgroud)

在浏览器的调试控制台中显示以下几行:

The payload is application/json.
uncompressed payload (383 character):
{"headers":{},"body":{"message":{"errors":{"password":"Password length must be at least 8 characters.","retype":"Retype Password cannot be null.","username":"Username length must be between 6 to 64 characters."},"links":[],"success":false,"target":{"password":"","retype":"","username":""}},"target":"/user/session/sign-up"},"statusCode":"UNPROCESSABLE_ENTITY","statusCodeValue":422}
The payload is text/plain.
compressed payload (157 character):
??????????¬??????????????p??!-??7??????????????????????????????????u?????????????????????ú}???????????????????????????????????????/?ÂR??b,??????m?????????? 
decompressed payload (383 character):
{"headers":{},"body":{"message":{"errors":{"password":"Password length must be at least 8 characters.","retype":"Retype Password cannot be null.","username":"Username length must be between 6 to 64 characters."},"links":[],"success":false,"target":{"password":"","retype":"","username":""}},"target":"/user/session/sign-up"},"statusCode":"UNPROCESSABLE_ENTITY","statusCodeValue":422}
Run Code Online (Sandbox Code Playgroud)

在这一点上,我现在可以验证String我的 Springboot 应用程序压缩的任何值,浏览器都可以解压缩并获取原始String. 但是有一个问题。当我检查浏览器调试器时,如果传输的消息的大小实际上减少了,它告诉我事实并非如此。

这是未压缩的原始消息 (598B):

a["MESSAGE destination:/user/session/broadcast
content-type:application/json;charset=UTF-8
subscription:sub-0
message-id:5lrv4kl1-1
content-length:383

{"headers":{},"body":{"message":{"errors":{"password":"Password length must be at least 8 characters.","retype":"Retype Password cannot be null.","username":"Username length must be between 6 to 64 characters."},"links":[],"success":false,"target":{"password":"","retype":"","username":""}},"target":"/user/session/sign-up"},"statusCode":"UNPROCESSABLE_ENTITY","statusCodeValue":422}
Run Code Online (Sandbox Code Playgroud)

虽然这是原始压缩消息 (589B):

a["MESSAGE destination:/user/session/broadcast
content-type:text/plain;charset=UTF-8
subscription:sub-0
message-id:5lrv4kl1-2
content-length:425

á¯¡à ¥ä¬à¢á¨á¡ä¹à®¸Ì͢¬ßäå°Ë¸â±á£ä±á¢ç¤â½Ýá®çâ©pç­æ¼¦!-ä á·7á¡å¡âº¨ç¤ç£àª®ååµÜ¸äá¡ç¡±äáÏۯĮãá´´á䫯â»Öç¹âåç­á£å¥¢âã¥â¡âæuâã¥âá²â«äáªä¸¨à²¸ä­äá¤å塬æ¶â¬»ã¶¶Ð¢\u2029ã°Í»á°Ãº}ã᥸æ²âƹâ᧸ã¦â´¼ä¶¨âæã¢¡á±¼æºæ¶¤ç°²â㺮橿äç¡ç§á®¬æâ¼ºâæ»ä¢æ¦µâ±çີâ£Ð¨ç¨àª°Ä籯/á¤ÃRå°È¨b,å¸°Ðæ°ä¥â¤ä°mãளÇä­â⧼㪠Өæä  \u0000"]
Run Code Online (Sandbox Code Playgroud)

调试控制台显示未压缩的消息以 598B 的大小传输,其中 383 个字符作为消息有效负载的大小(由content-length标题指示)。而另一方面,压缩消息的总大小为 589B,比未压缩的小 9B,消息负载大小为 425 个字符。我有几个问题:

  1. content-length的STOMP消息的以字节为单位表示,或字符?
  2. 为什么content-length未压缩消息的 383 比压缩消息的 425 小?
  3. 这是否意味着减少字符长度并不一定意味着减少大小?
  4. 为什么content-length压缩消息的 425 与 Java 控制台(使用lzStringCompressed.length())中返回的值157 不同,考虑到未压缩消息传输的 acontent-length为 383,这与 Java 控制台中的长度相同. 两者也都通过charset=UTF-8编码传输。
  5. 为什么content-length压缩消息的 425 与 Java 控制台(使用lzStringCompressed.length())中payload.length返回的值 157 不同,但 JavaScript 代码返回 157,而不是 425?
  6. 如果它真的在传输过程中变得臃肿,为什么消息application/json不受影响而只有plain/text膨胀?

虽然 9B 的差异仍然是一个差异,但我正在重新考虑压缩/解压缩消息的开销成本是否值得保留。我必须为此测试其他String值。

jcc*_*ero 5

所有的问题都是密切相关的。

  1. content-length的STOMP消息的以字节为单位表示,或字符?

正如您在STOMP 规范中所见:

所有的帧都可以包含一个content-length头部。此标头是消息正文长度的八位字节计数....

从 STOMP 的角度来看,主体是一个字节数组和标头,content-typecontent-length确定主体包含的内容以及应该如何解释它。

  1. 为什么content-length未压缩消息的383小于压缩消息的425

因为UTF-8当您将信息发送到 STOMP 服务器中的客户端时会进行转换。

您有一条消息 a String,这条消息由一系列字符组成。

无需详细说明 -如果您需要更多信息,请查看这个另一个优秀答案 - 在内部charJava 中的每一个都以 Unicode 代码单元表示。

为了在特定字符集中表示这些 Unicode 代码单元,UTF-8在您的情况下,可能需要可变数量的字节,在您的特定情况下从 1 到 4。

在未压缩的消息的情况下,必须383 charS,纯ASCII,这将被编码以UTF-8具有一个bytechar。这就是您在content-length标头中获得相同值的原因。

但压缩消息的情况并非如此:当您压缩消息时,它将为您提供任意数量的字节,对应于157 chars - Unicode 代码单元 - 具有任意信息。获得的字节数将少于原始消息。但是然后您将其编码为UTF-8. 其中一些157 chars 将用 1 表示byte,就像原始消息的情况一样,但由于压缩消息的信息的任意性,在许多情况下更有可能需要两个、三个或四个字节代表其中一些。这就是为什么您获得的字节数大于未压缩消息的字节数的原因。

  1. 这是否意味着减少字符长度并不一定意味着减少大小?

通常,在压缩数据时,您将始终获得少量信息。

如果信息足以使使用压缩变得有价值,并且您有能力发送压缩的原始二进制信息 - 类似于服务器发送指示Content-Encoding: gzip或 的信息deflate,它会给您带来很大的好处。

但是如果客户端库只能处理文本消息而不是二进制消息,例如 SockJS,如您所见,编码问题实际上可能会给您带来不适当的结果。

为了缓解这个问题,您可以首先尝试将您的信息压缩为其他中间编码,例如Base 64,这将为您提供1.6压缩字节数的大致倍数:如果此值小于未压缩的字节数,则压缩消息可能是值得的它。

在任何情况下,正如规范中所指出的,STOMP 是基于文本的,但也允许传输二进制消息。此外,它表明 STOMP 的默认编码是UTF-8,但它支持消息正文的替代编码规范。

如果您正在使用,正如您的代码所建议的那样,stomp-js请注意我没有使用过这个库,正如文档所指出的,它似乎也可以处理二进制消息。

基本上,您的服务器必须发送content-type带有 value 标头的原始字节信息application/octet-stream

然后,库可以在客户端使用类似于以下内容的方式处理此信息:

    // within message callback
    if (message.headers['content-type'] === 'application/octet-stream') {
      // message is binary
      // call message.binaryBody 
    } else {
      // message is text
      // call message.body
    }
Run Code Online (Sandbox Code Playgroud)

如果这可行,并且您可以通过这种方式发送压缩信息,如前所述,压缩可以为您带来很大的好处。

  1. 为什么content-length压缩消息的 ,即425,与 Java 控制台(使用lzStringCompressed.length())中返回的值不同,即157,考虑到未压缩消息是通过 传输content-length383,在 Java 控制台中的长度相同。两者也都与charset=UTF-8 encoding.

考虑类的length方法的Javadoc String

返回此字符串的长度。长度等于字符串中Unicode 代码单元的数量。

如您所见,该length方法将为您提供表示 所需的 Unicode 代码单元数String,同时content-length标头将为您提供表示它们所需的字节数,UTF-8如前所述。

事实上,计算字符串的长度可能是一项棘手的任务

  1. 为什么content-length压缩消息425的 与 Java 控制台中返回的值不同(使用lzStringCompressed.length()),157但 JavaScript 代码 payload.length 返回的157不是425

因为,正如您在文档中看到的,length在 Javascript 中也StringUTF-16代码单元表示对象的长度:

对象的length属性String包含字符串的长度,以UTF-16代码为单位。length是字符串实例的只读数据属性。

  1. 如果它真的在传输过程中变得臃肿,为什么消息application/json不受影响而只有text/plain膨胀?

如上所述,它Content-Type与信息的编码无关。