起因
昨天一个客户调用接口,使用之前的代码没有问题。但是调用另一个接口就出错。由于服务调用成功,所以感觉是编码问题。
update
字符集与字符编码
1. 完整的表达编码,要有字符集、字符编码、字库表。
2. 字库表是一个相当于所有可读或者可显示字符的数据库,字库表决定了整个字符集能够展现表示的所有字符的范围。
3. 字符集即用一个编码值code point来表示一个字符在字库中的位置
字符编码是字符集对应的存储。
一般来说都会直接将code point的值作为编码后的值直接存储。例如在ASCII中A在表中排第65位,而编码后A的数值是0100 0001也即十进制的65的二进制转换结果。
统一字库表的目的是为了能够涵盖世界上所有的字符,但实际使用过程中会发现真正用的上的字符相对整个字库表来说比例非常低。
例如中文地区的程序几乎不会需要日语字符,而一些英语国家甚至简单的ASCII字库表就能满足基本需求。而如果把每个字符都用字库表中的序号来存储的话,每个字符就需要3个字节(这里以Unicode字库为例),这样对于原本用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。算的直接一些,同样一块硬盘,用ASCII可以存1500篇文章,而用3字节Unicode序号存储只能存500篇。于是就出现了UTF-8这样的变长编码。在UTF-8编码中原本只需要一个字节的ASCII字符,仍然只占一个字节。而像中文及日语这样的复杂字符就需要2个到3个字节来存储。
结论:
参数中的k = v, v可以编码,但是’=’一定要显式给出。因为tomcat是依靠=区分参数。
经验:
utf8错误当做gbk解析,都是问号方块
gbk错误当做utf8解析,都是火星文字
utf8错误当做iso解析, 都是希腊文
var apikey = System.Configuration.ConfigurationManager.AppSettings["APIKey"];
var mobile = String.Join(",", mobileList);
var text = String.Join(",", textList.ConvertAll(t => Uri.EscapeDataString(t)));
//参数必须进行Uri.EscapeDataString编码。以免&#%=等特殊符号无法正常提交
string parameter = "apikey=" + apikey + "&text=" + text + "&mobile=" + mobile;
System.Net.WebRequest req = System.Net.WebRequest.Create(URI_SEND_MULTI_SMS);
req.ContentType = "application/x-www-form-urlencoded";
req.Method = "POST";
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(parameter);//这里编码设置为utf8
req.ContentLength = bytes.Length;
System.IO.Stream os = req.GetRequestStream();
os.Write(bytes, 0, bytes.Length);
os.Close();
System.Net.WebResponse resp = req.GetResponse();
if (resp == null) return null;
using (var myStreamReader = new System.IO.StreamReader(resp.GetResponseStream(), Encoding.UTF8))
现象
- 使用客户给的数据自己进行试验,Java代码不能直接看request提交的报文信息,Java调用成功了。
- 转而使用哪个C直接提交报文socket通信,发现在服务器端,直接传文本和对value进行URLEncode再传结果是一样的,而我们的需要的text文本信息是URLEncode。
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netdb.h>
#include <unistd.h>
#define SA struct sockaddr
#define MAXLINE 4096
#define MAXSUB 2000
#define MAXPARAM 2048
#define LISTENQ 1024
extern int h_errno;
int sockfd;
char *hname = "127.0.0.1";
char *send_sms_json = "/v1/sms/send.json";
char *get_user_json = "/v1/user/get.json";
/**
* 发http post请求
*/
ssize_t http_post(char *page, char *poststr)
{
char sendline[MAXLINE + 1], recvline[MAXLINE + 1];
ssize_t n;
snprintf(sendline, MAXSUB,
"POST %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Content-type: application/x-www-form-urlencoded\r\n"
"Content-length: %zu\r\n\r\n"
"%s", page, hname, strlen(poststr), poststr);
printf("%s\n",sendline);
write(sockfd, sendline, strlen(sendline));
while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = '\0';
printf("%s", recvline);
}
return n;
}
int main(void)
{
struct sockaddr_in servaddr;
char **pptr;
char str[50];
struct hostent *hptr;
//建立socket连接
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, str, &servaddr.sin_addr);
connect(sockfd, (SA *) & servaddr, sizeof(servaddr));
//修改为您的apikey
char *apikey = "a4b3f38430ef7be6704b03181e76e7ca";
//修改为您要发送的手机号
char *mobile = "18127813634";
//设置您要发送的内容
char *text = "【云片网】您的验证码是1234";
char *s1 = "apikey=a4b3f38430ef7be6704b03181e76e7ca&mobile=13012312312 &text=【云片网】您的验证码是1234";
// 仅加密value 传输成功
char *s2 = "apikey=a4b3f38430ef7be6704b03181e76e7ca&mobile=13012312312 &text=%e3%80%90%e4%ba%91%e7%89%87%e7%bd%91%e3%80%91%e6%82%a8%e7%9a%84%e9%aa%8c%e8%af%81%e7%a0%81%e6%98%af1234";
// 全部加密,传输失败
char *s3 = "apikey%3da4b3f38430ef7be6704b03181e76e7ca%26mobile%3d13012312312++%26text%3d%e3%80%90%e4%ba%91%e7%89%87%e7%bd%91%e3%80%91%e6%82%a8%e7%9a%84%e9%aa%8c%e8%af%81%e7%a0%81%e6%98%af1234";
http_post(send_sms_json,s1);
close(sockfd);
exit(0);
}
思考
byte[] bytes
这里设置的UTF8编码并不是URLEncode而是将其按照UTF8转为字节流。
发现的问题:
1. 为什么直接传入字符和编码一次传入结果相同?
2. 编码方式在哪里设置?
3. 到底什么是编码?
实验结果
- 用浏览器提交信息都会进行一次URLEncode,而在服务器接收到的信息是没有的。
- 在windows平台当文件以gbk保存,服务器接收到的信息是明显的gbk转utf8错误。
- 最终发现提交的报文只有是
text=urlencode(urlencode(text1).join(urlencode(text2))
才能正常提交。
原因
服务器接收过程:servlet容器比如tomcat会将接收到的二进制信息进行解码,这里的字符集使用tomcat配置中的信息。然后会对有%的进行URL解码。
解释现象
- 没有外层编码
urlencode(text1).join(urlencode(text2)
那么信息直接就会被tomcat解码,所以失败。 - 因为我的平台是Mac,默认使用utf8的方式,但如果没有
System.Text.Encoding.UTF8.GetBytes(parameter);
,c#平台默认是gbk,那样的话会解码出错。
- 没有外层编码
URL编码是将一个字符的字节码用%表示,eg:”中” 的utf8编码 4E2D那么编码就是%4E%2D,gbk 编码是D6D0,那么编码就是%D6%D0。这个和使用的编码有关。
因此我们确定servlet容器的编解码和字符集有关,tomcat默认是ISO- 8859-1,这样的话在请求的解码过程中信息会丢失。
关于编解码的理解
区分存储与编码
对应关系是指映射,eg:abc => ‘中’
程序用一种编码(utf8)进行存储,那么程序在读入时会依据前几个字节判断是什么类型的编码,然后用那种的字符表表示映射关系。可以正常输出(和显示对应起来)。Unicode和gbk可以设定映射关系charset[id]=gbkid. 这样来做到编码转换。编码转换
当这些字节按照iso-8859-1编码时,没有对应关系,所以都缺省为?(期初以为这里的转码会是逐个字节转码,错误!)那么信息就丢失了。
关于Javaweb的编码坑点
URL编码坑点
WEB应用中文字符问题
域名:端口/contextPath/servletPath/pathInfo?queryString
(1) HttpServletRequest.setCharacterEncoding()方法 仅仅只适用于设置post提交的request body的编码而不是设置get方法提交的queryString的编码。该方法告诉应用服务器应该采用什么编码解析post传过来的内容。很多文章并没有说明这一点。
(2) HttpServletRequest.getPathInfo()返回的结果是由Servlet服务器解码(decode)过的。
(3) HttpServletRequest.getRequestURI()返回的字符串没有被Servlet服务器decoded过。
(4) POST提交的数据是作为request body的一部分。
(5) 网页的Http头中ContentType(“text/html; charset=GBK”)的作用:
(a) 告诉浏览器网页中数据是什么编码;
(b) 表单提交时,通常浏览器会根据ContentType指定的charset对表单中的数据编码,然后发送给服务器的。
这里需要注意的是:这里所说的ContentType是指http头的ContentType,而不是在网页中meta中的ContentType。
对于POST方式,表单中的参数值对是通过request body发送给服务器,此时浏览器会根据网页的ContentType(“text/html; charset=GBK”)中指定的编码进行对表单中的数据进行编码,然后发给服务器。在服务器端的程序中我们可以通过Request.setCharacterEncoding() 设置编码,然后通过request.getParameter获得正确的数据。
HTMLPOST和GET编码问题(深入篇)
1.get提交
对于这种,影响的有tomcat的URIEncoding。
浏览器会根据自己的页面的编码格式作为起始编码格式(右击菜单编码有显示的),把字符使用浏览器的编码格式编码成byte字节进行传输。到了tomcat这里,tomcat会使用URIEncoding进行重新编码(解码),如果tomcat没有配置的话就会使用iso-8859-1对byte进行重新编码(解码)成字符。如果浏览器得编码格式为UTF-8,且tomcat没有配置重新编码(解码)格式的话,就可以使用下面的方式拿到正确的字符了new String(request.getParameter("text").getBytes("iso-8859-1"),"utf-8")
上的意思就是说,把刚才的字符,用iso-8859-1进行编码成byte,还原回去,再使用uft-8对byte进行重新编码(解码)成字符。(这个方法就是刚才从浏览器到tomcat过来的逆向过程)
这里为什么可以?要注意这里是针对二进制编解码!而不是字符。如果此时是对utf8存储的汉字进行iso编码则再也回不来。
2.post提交
对于这种情况,response.setCharacterEncoding有影响,当没有对response.setCharacterEncoding设置的时候值为null,则默认采用iso-8859-1来进行重新编码(解码)。
浏览器根据自己页面的编码格式作为起始编码格式,把字符进行编码成byte进行传输,到了tomcat,tomcat不进行干涉其中的重新编码(解码)格式。如果response.getCharacterEncoding为null,那么默认采用iso-8859-1进行重新编码(解码)成字符,如果设置了,就按照设置的编码格式进行重新编码(解码)字符。
jsp:pageEncoding=”GB18030” jsp页面的编码格式,即jsp会被解析成servlet时,采用的编码格式。如果不配置,默认采用iso-8859-1,当jsp文件保存编码类型和pageEncoding不一致时就会出现jsp内部解析乱码。Eclipse现在默认pageEncoding就是文件的编码格式,修改pageEncoding就会修改文件的编码格式。该参数还有一个功能,就是在JSP中不指定contentType参数,也不使用response.setCharacterEncoding方法时指定对服务器响应进行重新编码(解码)的编码,从而pageEncoding会影响浏览器的编码格式。