返回介绍

2.5 HTTP 头中的奥妙

发布于 2024-08-17 23:46:12 字数 8454 浏览 0 评论 0 收藏 0

对于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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文