PrintWriter返回乱码的分析及解决

用response得到输出流,即response.getOuptStream(); 返回值为ServletOutputStream 对象,即JSP的out对象,要么用response得到输出对象PrintWriter即response.getWriter()。

  1. protected void doGet(HttpServletRequest request,      
  2.     HttpServletResponse response) throws ServletException,   
  3.         IOException {  
  4.  
  5.     PrintWriter pw = response.getWriter();        
  6.     response.setCharacterEncoding("utf-8");        
  7.     response.setContentType("text/html; charset=utf-8");        
  8.     pw.print("中文"); 
protected void doGet(HttpServletRequest request,     
    HttpServletResponse response) throws ServletException,  
        IOException { 

    PrintWriter pw = response.getWriter();       
    response.setCharacterEncoding("utf-8");       
    response.setContentType("text/html; charset=utf-8");       
    pw.print("中文");
}

输出乱码。为什么呢,已经设置了字符编码啊?难道设置的无效。

在API中找到方法说明:


  1. PrintWriter getWriter() throws IOException  
  2.  
  3. Returns a PrintWriter object that can send character text to the client. The PrintWriter uses the character encoding returned by getCharacterEncoding().  
  4.  
  5. If the response's character encoding has not been specified as described in getCharacterEncoding (i.e., the method just returns the default value ISO-8859-1), getWriter updates it to ISO-8859-1.  
PrintWriter getWriter() throws IOException 

Returns a PrintWriter object that can send character text to the client. The PrintWriter uses the character encoding returned by getCharacterEncoding(). 

If the response's character encoding has not been specified as described in getCharacterEncoding (i.e., the method just returns the default value ISO-8859-1), getWriter updates it to ISO-8859-1. 

就是讲,在返回一个PrintWriter对象的时候,charactor encoding就已经确定了,就已经设置好了字符集了。什么时候设置的呢? setCharacterEncoding方法的实现时发现如下代码:


  1. public void setCharacterEncoding(String charset) {     
  2.     
  3.         if (isCommitted())     
  4.             return
  5.              
  6.         // Ignore any call from an included servlet     
  7.         if (included)     
  8.             return
  9.              
  10.         // Ignore any call made after the getWriter has been invoked     
  11.         // The default should be used     
  12.         if (usingWriter)     
  13.             return
  14.     
  15.         coyoteResponse.setCharacterEncoding(charset);     
  16.         isCharacterEncodingSet = true
  17.     }   
public void setCharacterEncoding(String charset) {    
   
        if (isCommitted())    
            return;
            
        // Ignore any call from an included servlet    
        if (included)    
            return;
            
        // Ignore any call made after the getWriter has been invoked    
        // The default should be used    
        if (usingWriter)    
            return;
   
        coyoteResponse.setCharacterEncoding(charset);    
        isCharacterEncodingSet = true;
    }  
其中usingWriter 标志为getPrinteWriter方法中设定,可见其控制逻辑为一旦返回了PrintWriter,本函数即不再生效。


ServletOutputStream out = response.getOutputStream();

out.print("中文");

情况1:正常,浏览器按utf-8方式查看
//response.setContentType("text/html; charset=utf-8");
           
情况2:浏览器缺省按简体中文查看,手动设为utf-8方式查看正常
//response.setCharacterEncoding("utf-8");
说明:这种方式不仅不需要在调用getOutputStream()之前设定字符集,甚至在print输出后设定都有效。

结论:

1.在servlet中输出中文,如果采用PrintWriter方式,需要在调用getPrintWriter()之前调用setContentType 或者 setCharacterEncoding;采用ServletOutputStream方式,不受此限。

2.setContentType 和 setCharacterEncoding两方法中设定characterEncoding的方法对服务器效果一致,不需要反复调用。在输出文本内容时, 采用response.setContentType("text/html; charset=utf-8");似乎更为方便。

3.PrintWriter自身并没有处理编码的职责,它还是应该看成一个装饰器比较好:它就是为了输出更方便而设计的,提供print、println、printf等便利方法。要设置编码的话,可以在它的底层Writer上设置:(这里以OutputStreamWriter为底层Writer),参考:


  1. new PrintWriter(new OutputStreamWriter(new FileOutputStream("yourfilepath"), "UTF-8"));  
  1. new PrintWriter(new OutputStreamWriter(new FileOutputStream("yourfilepath"), "UTF-8"));  
<think>好的,我现在要解决用户的问题,用户提到了在使用`PrintWriter`时遇到了无法写出数据的问题,具体代码是: ```java try (PrintWriter writer = response.getWriter()) { if (isInvalidChunk(chunk)) return; processChunkContent(chunk, totalContent); String sseMessage = buildSSEMessage(chunk.getChoices().get(0).getMessage().getContent()); writer.write(sseMessage); writer.flush(); } ``` 用户指出`writer`无法写出数据。首先,我需要分析可能的原因。根据经验,这类问题通常涉及输出流的配置、响应头设置、字符编码、缓冲机制或者异常处理。 首先,用户提到使用自动资源管理的try-with-resources,这通常没问题,但需要确保`response.getWriter()`正确获取了Writer对象。如果response对象没有正确初始化或者已经被提交,可能会导致获取Writer失败。 接下来,检查是否设置了正确的响应头,特别是SSE需要`Content-Type: text/event-stream`,以及相关的缓存控制头。如果响应头设置不正确,客户端可能无法正确解析数据。例如,缺少`text/event-stream`类型会导致浏览器不识别SSE事件。 字符编码也是一个常见问题。如果服务器和客户端使用的编码不一致,可能导致数据乱码或无法传输。需要确认response的字符编码设置为UTF-8,并且Writer使用相同的编码。 缓冲机制方面,服务器可能会缓冲输出,直到达到一定大小或显式调用flush。用户代码中已经调用了`writer.flush()`,但需要确认是否在flush之前有足够的输出数据,或者服务器配置是否禁用了缓冲。此外,检查是否有其他代码在之后关闭了Writer,导致后续写入失败。 空指针异常的可能性也存在。例如,`chunk.getChoices().get(0)`如果为null,会导致调用`getMessage()`时抛出异常,提前终止代码执行,使得writer没有机会写出数据。需要添加空值检查,使用Optional或者条件判断来处理可能的null值。 此外,检查是否正确处理了异常。如果代码块中有未捕获的异常,可能导致Writer未正确刷新或关闭,从而数据未发送。建议添加try-catch块来捕获异常,并确保资源被正确释放。 还有网络问题,比如客户端断开连接,此时写入会抛出IOException。需要处理这种异常,避免服务器端日志报错。 用户之前的对话中提到了`Choice::getMessage`的问题,可能相关的类路径或版本问题是否已经解决?如果`chunk.getChoices()`返回空列表,那么`get(0)`会抛出IndexOutOfBoundsException,导致后续代码无法执行,writer也无法写入数据。因此,必须确保`chunk`对象及其内部结构有效。 总结可能的原因: 1. 响应头未正确设置,特别是Content-Type和缓存控制头。 2. 字符编码不一致导致数据无法正确传输。 3. 缓冲机制导致数据未及时发送,尽管调用了flush。 4. 空指针异常或未处理的异常导致代码提前终止。 5. 客户端断开连接导致写入失败。 6. chunk对象内部数据不完整或为空。 接下来,需要针对这些可能的原因逐一验证,并提供相应的解决方案。例如,检查响应头的设置,添加空值检查,处理异常,确认编码设置等。同时,建议用户添加日志记录,查看在哪个步骤数据未按预期处理,帮助进一步定位问题。</think>以下是针对SSE流输出异常的详细分析解决方案: ### 一、核心问题定位 流式响应失效的关键表现为: $$ \text{数据未到达客户端} = \begin{cases} \text{响应头配置错误} \\ \text{缓冲机制干扰} \\ \text{空指针连锁反应} \\ \text{IO异常未处理} \end{cases} $$ ### 二、分步解决方案 #### 1. 强化响应头配置 必须设置SSE专用响应头: ```java response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); ``` #### 2. 增加防御性空值校验 ```java // 修改原有校验方法 private boolean isInvalidChunk(ChatCompletionChunk chunk) { return chunk == null || chunk.getChoices() == null || chunk.getChoices().isEmpty() || chunk.getChoices().get(0).getMessage() == null; } ``` #### 3. 优化写入逻辑 ```java try (PrintWriter writer = response.getWriter()) { if (isInvalidChunk(chunk)) { writer.write("data: [ERROR]invalid chunk\n\n"); // 错误通知 return; } processChunkContent(chunk, totalContent); // 添加安全访问链 String content = Optional.ofNullable(chunk.getChoices()) .filter(list -> !list.isEmpty()) .map(list -> list.get(0)) .map(ChatCompletionChoice::getMessage) .map(ChatMessage::getContent) .orElse(""); String sseMessage = buildSSEMessage(content); // 强制缓冲区刷新 writer.write(sseMessage); writer.flush(); // 诊断输出(调试后移除) System.out.println("[DEBUG] Sent SSE: " + sseMessage); } catch (IOException e) { // 处理客户端断开连接 if (e.getMessage().contains("Broken pipe")) { System.out.println("Client disconnected"); } else { e.printStackTrace(); } } ``` ### 三、SSE消息构建规范 正确格式化SSE消息体: ```java private String buildSSEMessage(String content) { return new StringBuilder() .append("data: ").append(content.replace("\n", "\\n")) // 转义换行 .append("\n\n") // 必须双换行 .toString(); } ``` ### 四、Tomcat服务器配置优化 在`conf/server.xml`中添加: ```xml <Connector connectionTimeout="20000" disableUploadTimeout="false" socket.soKeepAlive="true" socket.soReuseAddress="true" socket.tcpNoDelay="true"/> <!-- 禁用Nagle算法 --> ``` ### 五、客户端连接诊断 浏览器端调试方法: ```javascript const eventSource = new EventSource('/stream'); eventSource.onmessage = (e) => { console.log('Received:', e.data); }; eventSource.onerror = (err) => { console.error('SSE Error:', err); eventSource.close(); }; ``` ### 六、网络层验证工具 使用curl测试流式响应: ```bash curl -N -v \ -H "Accept: text/event-stream" \ http://localhost:8080/api/stream ``` ### 七、典型故障对照表 | 现象 | 检查点 | 解决方案 | |-----------------------|-------------------------------|--------------------------| | 无任何数据到达 | 响应头Content-Type设置 | 确认text/event-stream | | 数据延迟到达 | Tomcat缓冲配置 | 添加response.flushBuffer | | 中文乱码 | 字符编码一致性 | 统一使用UTF-8 | | 连接自动断开 | 心跳机制缺失 | 定期发送空注释行 | 建议在代码中添加心跳维持: ```java // 每15秒发送心跳 ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { try { writer.write(": heartbeat\n\n"); writer.flush(); } catch (Exception e) { scheduler.shutdown(); } }, 0, 15, TimeUnit.SECONDS); ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值