2.5 HTTP 头中的奥妙
对于HTTP头,我们并不陌生。我们在上一节中成功运用到了HTTP头中的Cookie属性。接下来,我们将继续发挥它的威力,看看它还能为我们做些什么。
我们先学习一下HTTP请求的定义。
2.5.1 HTTP请求
HTTP请求分为HTTPRequest和HTTPResponse两种。但无论哪种请求,都由header和body两部分组成。
1.HTTP Body
Body部分就是存放数据的地方,回顾一下我们在HTTPRequest类中封装的网络请求:
1)对于get形式的HTTPRequest,要发送的数据都以键值对的形式存放在URL上,比如aaa.apik1=va&k2=va。它的Body是空的,如下所示:
if (urlData.getNetType().equals(REQUEST_GET)) { // 添加参数 final StringBuffer paramBuffer = new StringBuffer(); if ((parameter != null) && (parameter.size() > 0)) { // 这里要对key进行排序 sortKeys(); for (final RequestParameter p : parameter) { if (paramBuffer.length() == 0) { paramBuffer.append(p.getName() + "=" + BaseUtils.UrlEncodeUnicode(p.getValue())); } else { paramBuffer.append("&" + p.getName() + "=" + BaseUtils.UrlEncodeUnicode(p.getValue())); } } newUrl = url + "?" + paramBuffer.toString(); } else { newUrl = url; } request = new HttpGet(newUrl); }
2)对于post形式的HTTPRequest,要发送的数据都存在Body里面,也是以键值对的形式,所以代码编写与get情形完全不同,如下所示:
else if (urlData.getNetType().equals(REQUEST_POST)) { request = new HttpPost(url); // 添加参数 if ((parameter != null) && (parameter.size() > 0)) { final List<BasicNameValuePair> list = new ArrayList<BasicNameValuePair>(); for (final RequestParameter p : parameter) { list.add(new BasicNameValuePair( p.getName(), p.getValue())); } ((HttpPost) request).setEntity( new UrlEncodedFormEntity(list, HTTP.UTF_8)); } }
2.HTTP Header
与Body相比,HTTP header就丰富的多了。它由很多键值对(key-value)组成,其中有些key是标准的,兼容于各大浏览器,比如:
·accept
·accept-language
·referrer
·user-agent
·accept-encoding
此外,我们还可以在MobileAPI端自定义一些键值对,然后要求App在调用MobileAPI时把这些信息传递过来。比如MobileAPI可以定义一个check-value这样的key,然后要求App将AppId(同一公司的不同App编号)、ClientType(Android还是iPhone、iPad)这些值拼接在一起经过MD5加密后,作为这个key的值传递给MobileAPI,然后由MobileAPI再去分析这些数据。
对于App开发人员而言,只要按照MobileAPI的要求,把这些key所需要的值拼接成HTTPRequest头正确传递过去即可。如下所示:
void setHttpHeaders(final HttpUriRequest httpMessage) { headers.clear(); headers.put(FrameConstants.ACCEPT_CHARSET, "UTF-8,*"); headers.put(FrameConstants.USER_AGENT, "Young Heart Android App "); if ((httpMessage != null) && (headers != null)) { for (final Entry<String, String> entry : headers.entrySet()) { if (entry.getKey()!=null) { httpMessage.addHeader(entry.getKey(), entry.getValue()); } } } }
我们在组装Cookie之前调用setHttpHeaders方法:
// 添加必要的头信息 setHttpHeaders(request); // 添加Cookie到请求头中 addCookie(); // 发送请求 response = httpClient.execute(request);
而在返回数据时,也可以从HTTP Response头中把所需要的数据解析出来。Android SDK将其封装成了若干方法以供调用。我们在下面的章节将会看到。
前面我们介绍过Cookie,其实也是HTTP头的一部分。它的作用我们已经见识过了。下面将讨论HTTP头中的另几个重要字段。
2.5.2 时间校准
接下来要介绍的是HTTP Response头中另一重要属性:Date,这个属性中记录了MobileAPI当时的服务器时间。
为什么说这个属性很重要呢?App开发人员经常遇到的一个bug就是,App显示的时间不准,经常会因为时区问题前后差几个小时,而接到用户的投诉。
为了解决这个问题,要从MobileAPI和App同时做一些工作。MobileAPI永远使用UTC时间。包括入参和返回值,都不要使用Date格式,而是减去UTC时间1970年1月1日的差值,这是一个long类型的长整数。
在App端比较麻烦。这里我们只讨论中国,比如国内航班时间、电影上映时间等等,那么我们把MobileAPI返回的long型时间转换为GMT8时区的时间就万事大吉了——只需要额外加8个小时。无论使用的人身在哪个时区,他们看到的都应该是一个时间,也就是GMT8的时间。
由于App本地时间会不准,比如前后差十几分钟,又比如设置了GMT9的时区,这样在取本地时间的时候,就会差一个小时。遇到这种情况,就要依赖于HTTP Response头的Date属性了。
每调用一次MobileAPI,就取出HTTP Response头的Date值,转换为GMT时间后,再减去本地取出的时间,得到一个差值delta。这个值可能是因为手机时间不准而差出来的那十几分钟,也可能是因为时区不同导致的1个小时差值。我们将这个delta值保存下来。那么每当取本地当前时间的时候,再额外加上这个delta差值,就得到了服务器GMT8的时间,就做到了任何人看到的时间是一样的。
因为App会频繁调用MobileAPI,所以这个delta值也会频繁更新,不用担心长期不调用MobileAPI而导致的这个delta值不太准的问题。
接下来我们修改AndroidLib框架,以支持上述的这些功能。
1)首先,在HTTPRequest类提供一个用于更新本地时间和服务器时间差值的方法updateDeltaBetweenServerAndClientTime,如下所示,由于我们在这里补上了UTC和GMT8相差的那8个小时,所以App其他地方不再需要考虑时差的问题,如下所示:
void updateDeltaBetweenServerAndClientTime() { if (response != null) { final Header header = response.getLastHeader("Date"); if (header != null) { final String strServerDate = header.getValue(); try { if ((strServerDate != null) && !strServerDate.equals("")) { final SimpleDateFormat sdf = new SimpleDateFormat( "EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH); TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); Date serverDateUAT = sdf.parse(strServerDate); deltaBetweenServerAndClientTime = serverDateUAT .getTime() + 8 * 60 * 60 * 1000 - System.currentTimeMillis(); } } catch (java.text.ParseException e) { e.printStackTrace(); } } } }
我们会在发起MobileAPI网络请求得到响应结果后,执行该方法,更新这个差值:
// 发送请求 response = httpClient.execute(request); // 获取状态 final int statusCode = response.getStatusLine().getStatusCode(); // 设置回调函数,但如果requestCallback,说明不需要回调,不需要知道返回结果 if ((requestCallback != null)) { if (statusCode == HttpStatus.SC_OK) { // 更新服务器时间和本地时间的差值 updateDeltaBetweenServerAndClientTime();
因为我们的App会频繁的调用MobileAPI,所以为了避免频繁读写文件,我们没有将deltaBetweenServerAndClientTime存到本地文件,而是放在了内存中,当作一个全局变量来使用。
2)我们把这个deltaBetweenServerAndClientTime方法暴露出来,供外界调用:
public static Date getServerTime() { return new Date(System.currentTimeMillis() + deltaBetweenServerAndClientTime); }
现在我们就可以模拟一个场景了。我把手机的时间改成任意一个值,然后再进入到WeatherByFastJsonActivity页面,因为页面加载的时候会调用MobileAPI获取天气的接口,所以本地会保存一个deltaBetweenServerAndClientTime差值。点击WeatherByFastJsonActivity页面上的“获取服务器时间”按钮,会因为我调用了AndroidLib中封装好的getServerTime方法,而弹出GMT8的当前时间:
btnShowTime.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String strCurrentTime = Utils.getServerTime().toString(); new AlertDialog.Builder(WeatherByFastJsonActivity.this) .setTitle("当前时间是:").setMessage(strCurrentTime) .setPositiveButton("确定", null).show(); } });
我见过一个App中的ntp.hosts文件,里面罗列了若干亚洲区的时间校准服务器,比如说:cn.pool.ntp.org。 [1]
我猜这是为了解决手机系统时间与服务器时间不同步的问题,身处不同时区是一种情况,另一种情况则是用户故意把手机时间提前五分钟,以防止赶不上班车。但不管怎样,那种通过App强行修改手机系统时间的做法是不负责任的。
对于手机系统时间不准的问题,本文给出了比较好的解决方案,即通过每次调用MobileAPI来计算时间差,然后每次本地获取时间就加上这个时间差。
对于用户身处不同时区的问题,App仍然返回同一个时间,只是要在App上注明这些时间都是北京时间,而不能是北京用户显示飞机9点起飞而日本用户显示10点起飞。另一方面,这两个时区的用户在一起聊天是个麻烦的事情,即使有人在日本时区10点说句话,对于北京用户而言,看到的也应该是9点发的消息,反之亦然。而服务器则要使用格林威治一套时间,具体怎么显示,那是App的事情。有些App就存在这样的bug,出国旅游收不到即时聊天消息,到了晚上会莫名其妙冒出来几百条消息,就是因为这个时区问题没有处理好导致的。
2.5.3 开启gzip压缩
接下来要介绍的内容和gzip有关。HTTP协议上的gzip编码是一种用来改进Web应用程序性能的技术。大流量的Web站点常常使用gzip压缩技术来减少传输量的大小,减少传输量大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输时,可以减少传输的时间。
使用gzip的流程如下:
1)在App发起请求时,在HTTPRequest头中,添加要求支持gzip的key-value,这里的key是Accept-Encoding,value是gzip。如下所示,我们需要修改setHttpHeaders方法:
void setHttpHeaders(final HttpUriRequest httpMessage) { headers.clear(); headers.put(FrameConstants.ACCEPT_CHARSET, "UTF-8,*"); headers.put(FrameConstants.USER_AGENT, "Young Heart Android App "); headers.put(FrameConstants.ACCEPT_ENCODING, "gzip"); if ((httpMessage != null) && (headers != null)) { for (final Entry<String, String> entry : headers.entrySet()) { if (entry.getKey() != null) { httpMessage.addHeader(entry.getKey(), entry.getValue()); } } } }
2)MobileAPI的逻辑是,检查HTTP请求头中的Accept-Encoding是否有gzip值,如果有,就会执行gzip压缩。
如果执行了gzip压缩,那么在返回值也就是HTTPResponse的头中,有一个content-encoding字段,会带有gzip的值;否则,就没有这个值。
3)App检查HTTPResponse头中的content-encoding字段是否包含gzip值,这个值的有无,导致了App解析HTTPResponse的姿势不同,如下所示(以下代码参见HTTPRequest这个类):
String strResponse = ""; if ((response.getEntity().getContentEncoding() != null) && (response.getEntity().getContentEncoding() .getValue() != null)) { if (response.getEntity().getContentEncoding() .getValue().contains("gzip")) { final InputStream in = response.getEntity() .getContent(); final InputStream is = new GZIPInputStream(in); strResponse = HttpRequest.inputStreamToString(is); is.close(); } else { response.getEntity().writeTo(content); strResponse = new String(content.toByteArray()).trim(); } } else { response.getEntity().writeTo(content); strResponse = new String(content.toByteArray()).trim(); }
到此,一个比较完备的网络底层封装就全部完成了。
[1] 关于NTP,请参见http://www.cnblogs.com/TianFang/archive/2011/12/20/2294603.html。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论