浅析java字符编码(在socket游走之间)

来源:互联网 发布:windows经典桌面图片 编辑:程序博客网 时间:2024/05/18 13:23

在引子中,介绍了一些在文件操作时需要注意的java字符编码细节。而在实际工程中,对java字符的操作更多情况下是在网络上以流的方式进行编解码。这篇文章就尝试着记录一些这方面的心得,与大家分享。

一般来说,工程中免不了与第三方厂商或系统交互,有时候你是作为发送方,有时候你是作为接收方。如果不采用类似于MQ这样的消息中间件,那么对于消息的交互细节,有一些是必须注意的。

自己动手,丰衣足食

为了完成一些基本的接收和发送动作,我们大可以自己动手,利用java中的socket api,实现一个如下的客户端和服务端。

Client:

   1: InetAddress addr = InetAddress.getLocalHost();
   2: int port = 9876;
   3: Socket socket = new Socket();
   4: SocketAddress socketAddr = new InetSocketAddress(addr, port);
   5:  
   6: String data = "中文123";
   7: socket.connect(socketAddr);
   8:  
   9: String path = "/";
  10: BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
  11: bw.write("POST " + path + " HTTP/1.0\r\n");
  12: bw.write("Content-Length: " + data.getBytes("UTF-8").length + "\r\n");
  13: bw.write("\r\n");
  14:  
  15: // send request with the data
  16: bw.write(data);
  17: bw.flush();
  18:  
  19: // get response
  20: BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
  21: String line = "";
  22: while ((line = br.readLine()) != null) {
  23:     System.out.println(line);
  24: }
  25: bw.close();
  26: br.close();

这里需要注意的是有几点:

1. 第12行Content-Length的大小一定要设置正确,这里的长度是POST正文数据的byte大小。一会联系server端的代码详细解释这个属性。

2. Content-Type作为Http header并不是必需的。有时候我们会发现有些流Content-Type是写着UTF-8,但实际上它的内容不是按照UTF-8编码的。所以这个属性对内容数据并没有约束力。

3. 第10行至第13行用了BufferedWriter进行数据发送,这里可以有多种选择,比如PrintStream。

Server:

刚才说到了Client发送的Content-Length指定了发送内容的byte大小,那么这个值在server端如何取得呢?一般的做法是通过一个BufferedReader对socket.getInputStream()进行adapt,然后就可以用readline()方法一行一行地将头信息读出来,由于Http Header都是ASCII字符,由于无论哪种解码方式Content-Length的值都能正确得到。

接下来读取body信息的方式就有一些陷阱在里面了。

陷阱1. 网络上的字符流不同于文件中固定的字符流,它是没有EOF的。所以什么时候停止是由你来决定的,如果你一直readLine()它是不会终止的。

陷阱2. 刚才已经得到了Content-Length,那么你是不是会尝试以下的代码呢?

   1: for (int i = 0; i < contentLength; i++) {
   2:     sb.append((char) br.read());

这段代码的结果在处理刚才发送的“中文123”时便会读了5个字符后无法往下读,这是由于br是按char来读,而并非byte,那么在读完5个字符后便停止了(但如同readLine()读完后一样,它并不会返回一个EOF)。而contentLength是9。

如果你说为什么不在发送时将contentLength指定为5呢?这就纯粹是为了解决问题而解决问题,更违背了HTTP的RFC规范。

规范的做法是首先不对socket.getInputStream()进行任何的adapt,即不进行decoding,先将body的byte[]数组完整取出来,再对其decoding。

Solution1. 如果socket的inputstream你只需要遍历一遍,那么用inputstream的read()方法先得到contentLength,就能顺利得到body的byte[]数组。

Solution2. 如果socket的inputstream你需要遍历多遍,比如首先要计算body的某一部分的md5值,那么通过socket的inputstream首先构造一个ByteArrayInputStream是更好的选择。不但可以调用mark()和reset()方法,更可以利用其有EOF直接调用readLine()方法。

   1: InputStream input = socket.getInputStream();
   2: byte[] buffer = new byte[1024];
   3: int readNum = input.read(buffer);
   4:  
   5: // wrap a local stream
   6: ByteArrayInputStream ins = new ByteArrayInputStream(buffer, 0, readNum);
   7: BufferedReader br = new BufferedReader(new InputStreamReader(ins, "UTF-8"));    // UTF-8 is necessary
   8: String line = "";
   9: long contentLength = 0;
  10: while ((line = br.readLine()) != null) {
  11:     if (line.startsWith("Content-Length")) {
  12:         contentLength = Long.parseLong(line.substring(line.indexOf(':') + 1).trim());
  13:     }
  14:     if (line.equals(""))
  15:         break;
  16: }
  17:  
  18: StringBuffer sb = new StringBuffer();
  19:  
  20: while ((line = br.readLine()) != null)
  21:     sb.append(line);
  22:  
  23: PrintStream out = new PrintStream(socket.getOutputStream());
  24: out.println("HTTP/1.0 200 OK");
  25: out.println("Content-Type: text/html;charset=UTF-8");
  26: out.println(); // blank will end http-header
  27: out.write("123中文".getBytes("UTF-8"));
  28:  
  29: socket.close();

几点说明:

1. 第2行到第3行的代码需要些小改动以适应大数据量post的情况。

2. 第23行到第27的代码与Client端利用BufferedWriter输出的情况类似,一些细节可以自行比较。

3. 你可能会注意到一个细节,为什么Client在read response(Client代码20-24行)可以毫无忌惮地使用BufferedReader来读?因为在Client端的read动作是在Server端发出close()动作之后,而这同样可以解释为什么在Server端socket.getInpuStream()没有EOF标志。

下一节将介绍一下client端及server端代表了先进生产力的API,自己制造粗糙的轮子并不是一件好事,嗯。

原创粉丝点击