V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rookiemaster
V2EX  ›  程序员

http 响应报文中 ContentType 里的编码方式是浏览器对内容的解码方式,还是服务器对内容的编码方式

  •  
  •   rookiemaster · 261 天前 · 2331 次点击
    这是一个创建于 261 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 springboot 项目里,我有如下设置:

    server:
      servlet:
        encoding:
          charset: GBK
          force-response: true
    

    然后 Controller 如下:

    @RestController
    public class TmpController {
        @GetMapping("/test")
        public Result test() throws UnsupportedEncodingException {
            String hello =  "你好";
            String newhello = new String( hello.getBytes("GBK") , "GBK");
            System.out.println(newhello);
            return Result.success(newhello);
        }
    }
    

    就是我想把一个字符串用 gbk 编码发给浏览器,然后让浏览器用 gbk 解码,但是最后浏览器显示的是乱码,不知该如何解决。

    23 条回复    2024-02-22 19:11:52 +08:00
    infun
        1
    infun  
       261 天前 via iPhone
    是服务器告诉浏览器可以用什么编码来处理
    你看一下浏览器端拿到的信息是怎样的?
    rookiemaster
        2
    rookiemaster  
    OP
       261 天前
    这样的 {"code":0,"message":"鎿嶄綔鎴愬姛","data":"锟斤拷锟�"}
    然后 ContentType 是 application/json;charset=GBK
    NessajCN
        3
    NessajCN  
       261 天前
    @rookiemaster 你看到「锟斤拷」不就很明显了吗,
    必然是你浏览器解析用的是 gbk, 而发上来的是 utf-8 字节。
    并且这串 utf-8 同样是乱码,因为有占位符。
    所以推测是你哪里多了一步 把 gbk 转成了 utf-8 发给了浏览器
    Lax
        4
    Lax  
       261 天前
    浏览器的网页部分本体是什么编码方式?
    hfc
        5
    hfc  
       261 天前 via Android
    hello="你好"这里的 hello 是 utf8 ,getBytes("GBK")这里就错了吧
    chendy
        6
    chendy  
       261 天前
    1. 谁发的 Content-Type 就是谁的内容格式
    2. Content-Type 里带 charset **貌似**不是一种标准做法(但是是一种惯例做法)
    3. JSON **貌似**有规定自己必须是 UTF8
    4. 2024 年了,没有特殊需求忘了 GBK 吧
    jinliming2
        7
    jinliming2  
       261 天前 via iPhone   ❤️ 1
    content-type 里指定的编码,是浏览器解码的。
    你的问题是 hello.getBytes 的时候得到了 GBK 编码的字符串,但是 new String 又给还原成了 UTF-16 ,最终把还原的 String 丢给 Result ,浏览器拿到的数据是 UTF-8 编码的,但是 content-type 指定用 GBK 来解,就乱码了哇。
    Java 不熟,你可能需要看看那个 Result.success 最终能不能用 bytes 来返回,直接 getBytes 之后丢给浏览器
    jinliming2
        8
    jinliming2  
       261 天前 via iPhone
    @jinliming2 hello.getBytes 的时候得到了 GBK 编码的 byte[]
    ShinichiYao
        9
    ShinichiYao  
       261 天前
    锟斤拷,恩恩好熟悉的内容
    rookiemaster
        10
    rookiemaster  
    OP
       261 天前
    @jinliming2
    我把 Controller 换成这样,浏览器就正常了
    @RestController
    public class TmpController {
    // @GetMapping(value = "/test",produces = "application/json")
    @GetMapping(value = "/test",produces = "application/json;charset=GBK")
    public String test() throws UnsupportedEncodingException {
    String hello = "你好你好";
    String newhello = new String( hello.getBytes("GBK") , "GBK");
    return newhello;
    }
    }
    rookiemaster
        11
    rookiemaster  
    OP
       261 天前
    @rookiemaster 然后返回 hello 而不是 newhello 也是正常的
    yulgang
        12
    yulgang  
       261 天前
    烫烫烫
    chinaguaiu
        13
    chinaguaiu  
       261 天前   ❤️ 4
    先说结论:charset 字段不对 application/json 类型的媒体内容生效,无论你怎么设置 charset ,框架都只会使用 utf-8 对字符串进行编码。charset 字段只用于 text/* 类型的媒体内容生效,也就是文本内容; application/*类型的数据在规范上属于二进制数据,不应受 charset 制约(框架和浏览器会直接忽略 charset )。

    如果一定要用 GBK 传输数据,不要给 Spring 框架返回 Collection 例如 Map 类型,而是直接返回 String 类型。无论是返回哪种类型都不需要你手动进行编码了,框架会自动处理的。

    // @RestController 注解会自动将 map 转化为 json 并使用 utf-8 编码
    // http 响应的媒体类型为 application/json
    @GetMapping("/hello-json")
    public Map sayHelloByJSON() {
    Map map = new HashMap<String, String>();
    map.put("你好", "世界");
    return map;
    }

    // 按照指定的编码传输文本数据
    // http 响应的媒体类型为 text/*,具体类型要看框架的处理
    @GetMapping("/hello-gbk")
    public String sayHelloByGbkString() {
    return "你好,世界。";
    }


    -------
    1. charset 字段对 text/*文本类型的影响,见: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
    "例如,对于主类型为 text 的任何 MIME 类型,可以添加可选的 charset 参数,以指定数据中的字符所使用的字符集"

    2. JSON 文件(文本)应该使用 applicaiton/json 媒体类型进行描述,见: https://www.iana.org/assignments/media-types/media-types.xhtml 。使用 text/*类型描述 json 文本被认为是违反规范的。

    3. applicaiton/json 类型的数据不应受 charset 字段影响,见: https://datatracker.ietf.org/doc/html/rfc6838#section-4.2.1
    该段有提到两点:
    ( 1 ). 包含“payload”的文本类型不应该使用 charset 字段,而应该由本身的规范指定(例如 xml 文件内部有编码集指定字段, 而 JSON 文件唯一指定为 utf-8 , 见 https://www.rfc-editor.org/rfc/rfc8259.html ),它们不应该受 charset 字段影响(直接忽略 charset,无论 charset 是否存在)
    ( 2 ). 如果一定要使用一种默认编码,使用“UTF-8”。
    application/json 数据其实算是二进制数据,但是可以认为是上文所说"包含`payload`的文本类型".
    ------


    嘛,应该挺多人对 json 数据胡乱进行处理的,乱码嘛,正常。
    op 有兴趣的话可以看看这个讨论: https://github.com/libwww-perl/HTTP-Message/pull/90
    讨论核心就是应该如何看待并处理 http 请求中的 json 类型"文本"(从浏览器和框架的角度)。对本问有一定的参考意义。
    bestie
        14
    bestie  
       261 天前   ❤️ 1
    chinaguaiu
        15
    chinaguaiu  
       261 天前   ❤️ 2
    @chendy 我钻牛角尖研究过,

    对于 2:是否带上 charset 字段已经成了历史遗留问题,大部分浏览器的做法是符合规范的话就适用 charset 字段指定的编码,如果不符合规范(例如说对图片类型数据也指定了 charset),就忽略(无论有没有填上 charset 字段)。见 https://datatracker.ietf.org/doc/html/rfc6657#page-3 , 该建议标准提到应尽量弱化媒体类型(尤其是 text/*类型)对 charset 字段的依赖。

    对于 3:JSON 文件强制使用 UTF-8 编码。见 https://www.rfc-editor.org/rfc/rfc8259.html#section-8.1 ,"JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8 [RFC3629]."
    nothingistrue
        16
    nothingistrue  
       261 天前
    浏览器对内容的解码方式,服务器对内容的编码方式,这不是必须配对的吗?

    至于你为啥是乱码嘛,是声明的编码跟实际使用的编码不一致。默认情况下,@RestController 的结果 JSON 序列化编码是 UTF-8 。你实际使用的是 UTF-8 编码,但声明的是 GBK 编码,乱套了。你后来通过 @GetMapping 调整了编码为 GBK ,这才对上。
    nothingistrue
        17
    nothingistrue  
       261 天前
    String newhello = new String( hello.getBytes("GBK") , "GBK");

    这段代码是拖裤子放屁。存储和对外传输的时候才涉及编码,变量的值是不涉及编码的。不管你将其跟 byte[] 转换多少次,newhello 都是 String 类型对象变量,最后序列化成 JSON 的时候,它都只是一个没有编码的字符串。
    rookiemaster
        18
    rookiemaster  
    OP
       261 天前
    @chinaguaiu 你好,谢谢你的解答,很有帮助。我还想问问当返回的是 String 类型的时候,怎么指定框架的编码方式,就是您的第二段代码,我设置 charset=utf-8 或者 gbk 的时候都能正确显示,但是 utf-32 的时候出现了乱码,是因为浏览器不支持 utf-32 吗
    rookiemaster
        19
    rookiemaster  
    OP
       261 天前
    @nothingistrue 谢谢指出,我也发现了
    nothingistrue
        20
    nothingistrue  
       261 天前
    Request 是强制由框架统一解码,但 Response 是默认由框架自动编码,接口代码完全可以忽略框架自行编码。故,尽量不要用 force-response 来全局配置 Response 的编码,因为它不具备强制约束性。

    另,约定优先与配置,JSON 就老实用 UTF-8 。
    chinaguaiu
        21
    chinaguaiu  
       261 天前
    @rookiemaster #18
    utf-32 类型的话,框架和浏览器都可能会出现问题,可能需要逐一排除。


    框架问题
    -----------
    utf-32 这个编码类型用的比较少,开发者一般是直接使用 utf-8 的,所以你首先还是需要通过抓却流量确定框架真的按照你指定的 utf-32 格式进行了编码。windows 下建议使用 Wireshark 工具进行抓取分析。
    我模拟帮你抓取了一下流量,如下代码和配置下:

    // 配置:server.servlet.encoding.charset=utf-32
    // server.servlet.encoding.force-response=true
    @GetMapping("/hello-encode")
    public String sayHelloByString() {
    return "你好,世界。";
    }

    抓取到的流量分析出来框架没有直接使用 utf-32 进行编码,而是使用了 utf-32be 进行编码;同时,该报文中的 Content-type 仍然指定 charset=UTF-32 ,显然导致了浏览器乱码问题(浏览器是按照 charset 指定的格式进行解码的,除非格式不支持)。我的环境是 spring-boot 3.1.5 。


    浏览器问题
    -----------
    如果确实框架确实使用了你指定的格式进行编码并且发送数据到浏览器,才进一步考虑浏览器是否支持该编码类型;如果该浏览器不支持报文中 charset 字段指定的编码类型,那么浏览器会忽略掉 charset 字段并按照算法选取一个编码格式进行解析,显然,也容易出现乱码。各家浏览器对这个默认选取的编码格式不一样,你可以在 F12 的开发者工具控制台中输入 document.charset 或者 document.characterset 查看当前使用的编码。

    我使用谷歌浏览器,上述所说的响应报文返回到浏览器后发生了乱码,我在控制台输入 document.charset 显示编码类型为 windows-1252 ,显然我的浏览器不支持 utf-32 类型。
    longbowape
        22
    longbowape  
       261 天前
    大概率是因为你的源代码文件是 utf-8 编码的
    rookiemaster
        23
    rookiemaster  
    OP
       261 天前
    @chinaguaiu 谢谢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1030 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 23:06 · PVG 07:06 · LAX 15:06 · JFK 18:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.