由于历史遗留,不少网络服务使用的是gbk编码,当前需要对某站点模拟post提交含有中文的参数,却因为node默认仅仅支持UTF-8编码,经过urlEncode的字符会有偏差,与目标字符并不对应.

常见字符编码

iso8859-1

最多能表示的字符范围是0-255,应用于英文系列。比如,字母a的编码为0x61=97,iso8859-1编码表示的字符范围很窄,无法表示中文字符。但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用iso8859-1编码来表示。而且在很多协议上,默认使用该编码。

GB2312/GBK

汉字的国标码(GB拼音’国标’首字母),专门用来表示汉字,是双字节编码,而英文字母和iso8859-1一致(兼容iso8859-1编码)。其中gbk是微软公司对此的扩展,此编码能够用来同时表示繁体字和简体字,而gb2312只能表示简体字,gbk兼容gb2312编码.

unicode

统一的编码,可以用来表示各国文字的字符,是定长双字节或者四字节编码,包括英文字母在内。定长编码便于计算机处理(注意GB2312/GBK不是定长编码),而unicode又可以用来表示所有字符,所以在很多软件内部是使用unicode编码来处理的,比如java,javascript也是同样的,不难料,unicode可以支持emoji,所以甚至可以用表情字符来编程.

UTF

因为unicode编码不兼容iso8859-1编码,因为对于英文字母,unicode也需要两个字节来表,所以很浪费空间。nicode不便于传输和存储,由此产生了utf编码,utf编码兼容iso8859-1编码,同时也可以用来表示所有语言的字符 utf编码是不定长编码,每一个字符的长度从1-6个字节不等。一般来讲,英文字母都是用一个字节表示,而汉字使用三个字节。

urlEncode

为什么要使用它

一产品名称为A&T Plastic,在产品列表中就产生了这样的一个联接<a href=”product.asp?name=A&T Plastic”>A&T Plastic</a> 在服务器端接收此参数的时候怎么也无法接收到准确的产品名。

当时就问我,如何解决,也许是当时忙吧,随口告诉他用HTMLENCODE方法,对方试告诉并没有能解决这个问题。我当时没有再给予回答,偶尔想起实在是对不起,我讲错了。今日闲暇就整理了一下如何处理GET方式提交的含有特殊字符的参数,以表内心的愧疚。

特殊特殊字符的含义

  • # 用来标志特定的文档位置 %23
  • % 对特殊字符进行编码 %25
  • & 分隔不同的变量值对 %26
  • + 在变量值中表示空格 %2B
  • / 表示目录路径 %2F
  • = 用来连接键和值 %3D
  • ? 表示查询字符串的开始 %3F

  • 当键值中含有以上列表中的一些字符时就无法准确的接收其中的值,urlEncode就像源码中\一样起到了转义的作用,使得各方面语义不再混淆

    EncType

    是html中form元素的属性,常用的有application/x-www-form-urlencodedmultipart/form-data,默认使用前者.

    使用application/x-www-form-urlencoded会将表单数据进行urlEncode,即使它不是get请求,会将表单的键值对拼接成get请求的query一样.并且urlEnocde和服务端的urlDecode(urlEncode的反处理)是配对的,不能一方面修改属性,否则参数将无法正确获取.

    Buffer

    node支持的字符编码

    1. ‘ascii’ -仅用于7位的ASCII数据。这种编码方法非常快速,并且一旦设置便会抛弃高位数据。注意,这个编码方式会将空字符(’\0’ 或 ‘\u0000’)转换成0x20(空格的字符编码)。如果想把空字符转换成0x00,得使用’utf8’。


    2. ‘utf8’ -多字节编码的Unicode字符。许多网页以及其它文档格式会使用UTF-8编码。


    3. ‘ucs2’ -仅用2个字节编码的Unicode字符。它仅可对BMP(基本多文种平面或第零平面,从U+0000到U+FFFF)进行编码。


    4. ‘base64’ - Base64字符串编码。


    5. ‘binary’ -经使用每个字符的头8位把原始二进制数据编码成字符串的一种途径。这是一个已经被废弃的编码方法。 且为了能让缓冲器对象取代这个编码方法,应避免使用它。在Node的未来版本中也会移除掉这个编码方法。


    6. ‘hex’ - 把每个字节编码成两个十六进制字符。

    7. Buffer类

      Javascript对于字符串(String)的操作比较擅长而且友好的,但是在Node中,还需要处理网络流,文件等二进制数据,对于处理这些二进制数据,javascript自有的字符串操作机制就不能满足这些需求,所以Buffer的出现就是为了解决这个问题。

      Buffer类似一个数组,为两位数的16进制数,即(0-255)范围,Buffer内存的申请是在V8引擎堆外,是一个javascript与C++结合的一个模块.

      new Buffer(str, [encoding]);//按照指定编码  
      buf.toString([encoding]);//返回解码的字符串  
      

      `

      注意Buffer的拼接

      国外使用英文编码,对于一下读取文本内容的示例代码是没有任何问题.

      var fs = require('fs');  
      var rs = fs.createReadStream('testdata.md');  
      var data = '';  
      rs.on("data", function (trunk){  
          data += trunk;
      });
      rs.on("end", function () {  
          console.log(data);
      });
      

      如果用来读取中文就可能出现问题,在上面代码中

      //data += trunk 隐藏了隐性转换
      data = data.toString() + trunk.toString();  
      

      按照单个字节去拼接字串就会出现乱码问题.

      以下是一个正确的代码示例.

      var buffers = [];  
      var nread = 0;  
      readStream.on('data', function (chunk) {  
          buffers.push(chunk);
          nread += chunk.length;
      });
      readStream.on('end', function () {  
          var buffer = null;
          switch(buffers.length) {
              case 0: buffer = new Buffer(0);
                  break;
              case 1: buffer = buffers[0];
                  break;
              default:
                  buffer = new Buffer(nread);
                  for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {
                      var chunk = buffers[i];
                      chunk.copy(buffer, pos);
                      pos += chunk.length;
                  }
              break;
          }
      });
      

      如果是非UTF-8编码,可以在readStream.on('end',function (){//})里面使用第三方包进行解码.

      试探

      在线的urlEncode服务,中文会,会按照16位字节呈现,然后加以%进行链接

      蛤
      //GBK编码字符经过urlEncode
      %B8%F2
      ///UTF-8编码字符经过urlEncode
      %E8%9B%A4
      

      node端使用iconv-lite这个库进行gbk编码的字符串处理,可以方便得对各种编码字符与字节串进行相互转换.

      var iconv = require('iconv-lite');  
      var word = '蛤',  
          word_gbk = iconv.encode(word,'gbk'),
          word_unicode = iconv.encode(word,'utf8');
      console.log(word_gbk,word_unicode)  
      

      执行这段代码之后输出结果如下

      <Buffer b8 f2> <Buffer e8 9b a4>  
      

      对比两个输出结果可见urlEncode的处理过程.

      使用node api

      http.request(options[, callback])

      用来产生若干的http request请求,option是一个有键值的对象,或者是一个字符串.但是字符串必须是按照urlEncode,使用 url.parse()进行处理后,或者符合对应的规范.

      Options:

      • host: A domain name or IP address of the server to issue the request to. Defaults to 'localhost'.
      • hostname: To support url.parse() hostname is preferred over host
      • port: Port of remote server. Defaults to 80.
      • localAddress: Local interface to bind for network connections.
      • socketPath: Unix Domain Socket (use one of host:port or socketPath)
      • method: A string specifying the HTTP request method. Defaults to 'GET'.
      • path: Request path. Defaults to '/'. Should include query string if any. E.G. '/index.html?page=12'. An exception is thrown when the request path contains illegal characters. Currently, only spaces are rejected but that may change in the future.
      • headers: An object containing request headers.
      • auth: Basic authentication i.e. 'user:password' to compute an Authorization header.
      • keepAlive: {Boolean} Keep sockets around in a pool to be used by other requests in the future. Default = false
      • keepAliveMsecs: {Integer} When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. Default = 1000. Only relevant if keepAlive is set to true.
      • agent: Controls Agent behavior. When an Agent is used request will default to Connection: keep-alive. Possible values:
      • undefined (default): use global Agent for this host and port.
      • Agent object: explicitly use the passed in Agent.
      • false: opts out of connection pooling with an Agent, defaults request to Connection: close.

      详细说明请参考官方文档http.request(options[, callback])

      模拟客户端

      先用java编写我们需要的客户端index.jsp.默认使用的是UTF-8编码.

      <%=request.getParameter("msg")%>  
      

       

      按照文档一个简单demo

      // 表单
      var postData = querystring.stringify({  
        'msg' : '蛤'
      });
      
      var options = {  
        hostname: 'localhost',
        port: 8080,
        path: '/test/',
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Content-Length': postData.length
        }
      };
      
      var req = http.request(options, function(res) {  
        console.log('STATUS: ' + res.statusCode+'\n');
        console.log('HEADERS: ' + JSON.stringify(res.headers)+'\n');
        res.setEncoding('binary');
        res.on('data', function (chunk) {
          console.log('BODY: ' + chunk+'\n');
        });
      });
      
      req.on('error', function(e) {  
        console.log('problem with request: ' + e.message);
      });
      
      // 经过urlEncode的表单,使用UTF-8编码
      console.log(postData+'\n');  
      // write data to request body
      req.write(postData);  
      req.end();  
      

      执行结果

      msg=%E8%9B%A4
      
      STATUS: 200
      
      HEADERS: {"server":"Apache-Coyote/1.1","set-cookie":["JSESSIONID=1E4CE1F397021A0EF08A1062470DEA33; Path=/test/; HttpOnly"],"content-type":"text/html;charset=ISO-8859-1","content-length":"3","date":"Mon, 31 Aug 2015 14:59:09 GMT","connection":"close"}
      
      BODY: 蛤  
      

      如果在服务器端,将接收的的参数当做GBK编码处理,修改index.jsp

      <%@ page contentType="text/html;charset=GBK"%> //页面显示编码  
      <%  
      response.setContentType("text/html;charset=GBK");//页面输出参数编码  
      request.setCharacterEncoding("GBK");//获取请求参数编码  
      String msg=request.getParameter("msg");  
      out.write(msg+"source: 蛤");  
      %>
      

      用UTF-8请求,返回的结果就会出现乱码

      msg=%E8%9B%A4
      
      STATUS: 200
      
      HEADERS: {"server":"Apache-Coyote/1.1","set-cookie":["JSESSIONID=84E9501059367C18376212007C47FE8D; Path=/test/; HttpOnly"],"content-type":"text/html;charset=GBK","content-length":"6","date":"Mon, 31 Aug 2015 15:16:25 GMT","connection":"close"}
      
      BODY:  
      ????
      
      BODY: ?  
      

      貌似jsp输出的字符编码并不是GBK的,对编码设置无用?

      JSP的编码巨坑

      Tomcat容器默认的编码会将接收的参数进行ISO-8859-1解码!!即使你声明了一大堆GBK,编码还是会错乱,解决办法是将字符按照ISO-8859-1解码回去,再用GBK编码.由此我们的index.jsp修改成这个样子.

      <%@ page language="java" contentType="text/html; charset=GBK"  
          pageEncoding="GBK"%>
      <!DOCTYPE>  
      <html>  
      <head>  
      <title>GBK page</title>  
      </head>  
      <body>  
          <p>GBK编码:</p>
      <% if (request.getParameter("msg")!=null){ %>  
          <span><%= new String( request.getParameter("msg").getBytes("ISO-8859-1"),"GBK") %></span>
      <% } %>  
      </body>  
      </html>  
      

      正确显示的GBK编码的网页

      接下来我们使用rest-client对这个测试页面提交请求

      方法选择POST,提交选项为原样字符,内容类型同样为默认,其他默认 这里填入的字符是按照GBK进行了UrlEncode处理

      服务器端正确按照编码处理,返回显示完整的结果

      再看node api

      response.write(chunk[, encoding][, callback])

      如果这个方法被回调,但是response.writeHead()还没有被回调,它会切换到没有完成的header模式,然后强制刷入header.

      它会发送请求主体,这个方法可能会调用多次去分块发送请求主体.

      请求的数据块可能是一个字符串或者字节数组,如果数据块是一个字符串,会默认将字符串进行utf8编码然后发送,如果是字节数组就直接发送出去,数据全部发送后调用回调.

      由此我们可以将经过目标编码的表单的字节数组写入达到目的.

      但是试验发现node的api有坑!之前的的demo使用了querystring进行url编码,默认是按照UTF-8编码,node本来也不支持GBK.

      var postData = querystring.stringify({  
        'msg' : '蛤'
      });
      

      querystring.stringify(obj[, sep][, eq][, options])

      它介绍了一个gbk中文编码的组件,貌似可以达到目的.

      // Suppose gbkEncodeURIComponent function already exists,
      // it can encode string with `gbk` encoding
      querystring.stringify({ w: '中文', foo: 'bar' }, null, null,  
        { encodeURIComponent: gbkEncodeURIComponent })
      // returns
      'w=%D6%D0%CE%C4&foo=bar'  
      

      结果在node命令行模式执行的结果如下.

      > var r = querystring.stringify({ w: '中文', foo: 'bar' }, null, null,
      ...   { encodeURIComponent: gbkEncodeURIComponent });
      ReferenceError: gbkEncodeURIComponent is not defined  
          at repl:2:23
          at REPLServer.defaultEval (repl.js:132:27)
          at bound (domain.js:254:14)
          at REPLServer.runBound [as eval] (domain.js:267:12)
          at REPLServer.<anonymous> (repl.js:279:12)
          at REPLServer.emit (events.js:107:17)
          at REPLServer.Interface._onLine (readline.js:214:10)
          at REPLServer.Interface._line (readline.js:553:8)
          at REPLServer.Interface._ttyWrite (readline.js:830:14)
          at ReadStream.onkeypress (readline.js:109:10)
      > 
      

      嗯,gbkEncodeURIComponent并不是node内置的,是需要Options包含一个encodeURIComponent方法,默认的是使用querystring.escape.

      现在查看node querystring模块的源码,这里是stringify方法段.

      //这里是判断类型,使得返回都是字符串
      var stringifyPrimitive = function(v) {  
        if (typeof v === 'string')
          return v;
        if (typeof v === 'number' && isFinite(v))
          return '' + v;
        if (typeof v === 'boolean')
          return v ? 'true' : 'false';
        return '';
      };
      
      QueryString.stringify = QueryString.encode = function(obj, sep, eq, options) {  
        sep = sep || '&';
        eq = eq || '=';
      
        var encode = QueryString.escape;
        if (options && typeof options.encodeURIComponent === 'function') {
          encode = options.encodeURIComponent;
        }
      
        if (obj !== null && typeof obj === 'object') {
          var keys = Object.keys(obj);
          var len = keys.length;
          var flast = len - 1;
          var fields = '';
          for (var i = 0; i < len; ++i) {
            var k = keys[i];
            var v = obj[k];
            var ks = encode(stringifyPrimitive(k)) + eq;//这里进行编码
      
            if (Array.isArray(v)) {
              var vlen = v.length;
              var vlast = vlen - 1;
              for (var j = 0; j < vlen; ++j) {
                fields += ks + encode(stringifyPrimitive(v[j]));
                if (j < vlast)
                  fields += sep;
              }
              if (vlen && i < flast)
                fields += sep;
            } else {
              fields += ks + encode(stringifyPrimitive(v));
              if (i < flast)
                fields += sep;
            }
          }
          return fields;
        }
        return '';
      };
      

      完善urlEncode模块

      javascript的数值与字符转换

      > escape('蛤');//转unicode
      '%u86E4'  
      > parseInt('86E4',16);//转16进制
      34532  
      > parseInt('1010',2);//转
      10  
      > parseInt('1010',2).toString(2);//必须强制转换为Int后调用用toString方法
      '1010'  
      

      处理仅有的中文

      Buffer是一个数组,循环拼接转换的字符.

      var gbk = iconv.encode('蛤','gbk'),  
          i = 0,
          str='';
      for (i;i<gbk.length;i++){  
        str += '%' + gbk[i].toString(16);
      };
      str = str.toUpperCase();  
      console.log(str);  
      
      $ node app.js 
      %B8%F2
      

      结果与目标%B8%F2一致.

      构造encodeURIComponent

      其实很简单,2333.首先要看懂文档,并且要记在心里javascript方法也是对象,可以赋值引用.

      var postData = querystring.stringify({ w: '蛤', foo: 'bar' }, null, null,  
        { encodeURIComponent: function (str){
            var chinese = new RegExp(/[^\x00-\xff]/g);
            var gbkBuffer = null;
            var i = 0;
            var tempStr = '';
            if (chinese.test(str)){//用正则判断它是不是中文字符
              gbkBuffer = iconv.encode(str,'gbk');//进行gbk编码
            for (i;i<gbkBuffer.length;i++){
              tempStr += '%' + gbkBuffer[i].toString(16);//拼接字符串
            };
            tempStr = tempStr.toUpperCase();//都转成大写
          }else{
              return querystring.escape(str);//否则使用默认的编码
          }
      }});
      console.log(postData);  
      

      GBK和UTF-8编码的英文数字符都是一样的,因为都是基于iso8859-1,使用一个字节.

      w=%B8%F2&foo=bar  
      [Finished in 0.3s]
      

      完成

      var form = {  
          msg:'蛤',
          psw:'2333'
      }
      
      var postData = querystring.stringify(form, null, null,  
        { encodeURIComponent: function (str){
          var chinese = new RegExp(/[^\x00-\xff]/g);
            var gbkBuffer = null;
            var i = 0;
            var tempStr = '';
            if (chinese.test(str)){//
              gbkBuffer = iconv.encode(str,'gbk');
            for (i;i<gbkBuffer.length;i++){
              tempStr += '%' + gbkBuffer[i].toString(16);
            };
            tempStr = tempStr.toUpperCase();
              return tempStr;
          }else{
              return querystring.escape(str);
          }
        }
      });
      
      var options = {  
        hostname: 'localhost',
        port: 8080,
        path: '/test/',
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Content-Length': postData.length
        }//Cookie也是放在请求头里面
      };
      
      var req = http.request(options, function(res) {  
        console.log('STATUS: ' + res.statusCode);
        //响应的Cookie在res.header['set-cookie']
        console.log('HEADERS: ' + JSON.stringify(res.headers));
        // res.setEncoding('binary');//接收参数的时候先不要解码
        res.on('data', function (chunk) {
          console.log('BODY: ' + iconv.decode(chunk,'gbk'));//gbk解码
        });
      });
      
      req.on('error', function(e) {  
        console.log('problem with request: ' + e.message);
      });
      
      // write data to request body
      req.write(postData);  
      req.end();  
      

      正确显示

      STATUS: 200  
      HEADERS: {"server":"Apache-Coyote/1.1","set-cookie":["JSESSIONID=8B8D1713441C8445C8B89720DA4FD019; Path=/test/; HttpOnly"],"content-type":"text/html;charset=GBK","content-length":"128","date":"Tue, 01 Sep 2015 12:11:07 GMT","connection":"close"}  
      BODY:  
      <!DOCTYPE>  
      <html>  
      <head>  
      <title>GBK page</title>  
      </head>  
      <body>  
          <p>GBK编码:</p>
      
          <span>蛤</span>
      
      </body>  
      </html>