先普及一下什么是条件GET,以下摘自<<Restful Web Service>>:
条件HTTP GET(conditional HTTP GET)既能几声服务器带宽,又能节省客户端带宽,作用是避免服务器 重复向一个客户端发送相同的表示。它是通过两个响应报头(Last-Modified和ETag)和两个请求报头 (IfModified-Since和If-None-Match)实现的。 这是无法靠客户端或服务器单方面解决的,若客户端在获取一个表示后,不再与服务器联系,那么该客户 端将无法获知服务器上的表示是否有变化。由于服务器并不保存应用状态,所以它也无法知道一个客户端 上次获取某个表示发生在何时。HTTP不是一个可靠的协议,所以客户端第一次就没有收到表示是有可能的 。当客户端请求一个表示时,服务器并不知道客户端之前有没有申请过这个表示--除非客户端能够提供 相关信息(作为部分应用状态)。 条件HTTP GET需要由客户端和服务器共同参与完成。服务器发送表示时,应当设置一些响应报头:Last- Modified或ETag。客户端重复请求一个表示时,也应当设置一些报头:IfModified-Since和If-None- Match--服务器根据这些信息决定是否重新发送表示。
下面看看在Restlet中,如何实现条件GET。首先看服务器端代码:
@Override public void handleGet() { Date modifiedSince = getRequest().getConditions().getUnmodifiedSince(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date lastModifiedAtServer = null; try { lastModifiedAtServer = format.parse("2009-07-29 00:00:00"); } catch (ParseException e) { e.printStackTrace(); } if (modifiedSince == null || modifiedSince.getTime() < lastModifiedAtServer.getTime()) { // get user data from database through user id User user = null; Representation representation = new StringRepresentation( getUserXml(user), MediaType.TEXT_PLAIN); representation.setMediaType(MediaType.TEXT_PLAIN); getResponse().setStatus(Status.SUCCESS_OK); getResponse().setEntity(representation); } else { getResponse().setStatus(Status.REDIRECTION_NOT_MODIFIED); } }
客户端请求代码:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date lastModified = null; try { lastModified = format.parse("2009-07-28 00:00:00"); } catch (ParseException e) { e.printStackTrace(); } Reference reference = new Reference("http://localhost:8080/restlet/resources/users/1"); Request get = new Request(Method.GET, reference); get.getConditions().setUnmodifiedSince(lastModified); Client client = new Client(Protocol.HTTP); Response res = client.handle(get); if(res.getStatus().equals(Status.REDIRECTION_NOT_MODIFIED)){ //it means the representation has not been changed and server didn't send the representation //then get representation from Cache or File or Database }else if(res.getStatus().equals(Status.SUCCESS_OK)){ try { System.out.println(res.getEntity().getText()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
我来解释一下这两段代码,正如刚开始解释条件GET中所说,如果客户端只请求一次而从此不在请求,那么永远不存在条件GET一说,因为第一次请求肯定不是条件GET,当第二次或者以后的请求,才可能满足条件GET的条件。所以,首先从客户端代码开始分析。
当然为了测试方便,上述代码的变更时间都是硬编码,实际环境中,应该是保存到数据库或者文件中.先说一下大概的流程:
客户端要发起请求,客户端上次从服务器接收表示的时间是2009-07-28,按照今天的时间来算,就是最后一次接收表示的时间是昨天。而客户端设置最后的变更时间为2009-07-28,实际上也是告诉服务器客户端的最后的表示的变更时间。
而服务器收到请求,并查看客户端是否附送了最后的变更时间,如果存在,则与当前服务器端表示的最后的变更时间做比较,如果在客户端附送的时间之后,服务器端的表示有变更,则重新发送新的表示和最新的变更时间,否则发送响应代码304,也就是Not Modified。
客户端如果收到响应是304,则知道服务器端的表示没有变更,所以,直接从缓存或者之前保存的文件或者数据库中取出之前的表示。而如果客户端接收新的表示,则根据架构的设计重新缓存或者保存表示以及最后变更时间到文件和数据库。
上面的解释如果觉得有点糊涂,没关系,来个具体的例子,以Customer为例:
客户端请求的URI是:/customers/{customerId}, 返回客户端的表示的类型是XML,象如下这样:
<customer id="1" name="ajax"> <address></address> <phone></phone> <fax></fax> </customer>
name是根据Id得到,而客户端发送请求的目的是想知道id为1的customer的address、phone、fax是否有变化。而服务器端则会监控customer表中的这三个属性,如果任何一个有变化,则更新最后变更时间,这个最后变更时间应该有一个单独的数据库字段记录。所以,当客户端发送请求时,并附送客户端最后的接收表示时,从服务器端获取的变更时间。然后服务器会把数据库保存的时间跟客户端送来的时间做对比,来决定是否发送表示。
上面的解决方案貌似完美,但是即使服务器提供Last-Modified,也不是完全可靠,因为资源变化时间是无法百分之百精确记录。所以人们需要一种可靠的方式检查一个表示自从上次获取以后有没有发生变化。Etag响应报头就是用于此目的。Etag是一个无意义的字符串,它会随着对应的表示的变化而变化。
这也是HTTP/1.1中把Last-Modified作为weak validator的原因,关于什么是强校验器,什么是弱校验器,这里不作解释,感兴趣的可以查看HTTP/1.1的相关资料。
ok,基于上述,来看一下代码应该是什么样子,首先在客户端代码加入:
String customerToMD5 = Engine.getInstance().toMd5("1/ajax/shanghai/1388888888/12345678"); List<Tag> tags = new ArrayList<Tag>(); tags.add(new Tag(customerToMD5)); get.getConditions().setNoneMatch(tags);
上述代码的tag是硬编码产生的,仅仅为了测试。实际环境应该是存储在某个地方,发送的时候从存储的位置取出来即可。
当服务器端返回响应后,如果有新的表示返回,一般会有新的Tag,所以应该保存这个新的Tag,以便下次请求时发送:
//Cache the last modified date or save it into File or Database System.out.println(res.getEntity().getModificationDate()); //Cache the tag or save the tag into File or Database System.out.println(res.getEntity().getTag());
服务器端代码:
@Override public void handleGet() { Date modifiedSince = getRequest().getConditions().getUnmodifiedSince(); List<Tag> tags = getRequest().getConditions().getNoneMatch(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date lastModifiedAtServer = null; try { lastModifiedAtServer = format.parse("2009-07-29 00:00:00"); } catch (ParseException e) { e.printStackTrace(); } String customerToMD5 = Engine.getInstance().toMd5("1/ajax/shanghai/1388888888/12345678"); boolean noneMatch = tags.contains(new Tag(customerToMD5)); if (modifiedSince == null || modifiedSince.getTime() < lastModifiedAtServer.getTime() || noneMatch) { // get user data from database through user id User user = null; Representation representation = new StringRepresentation( getUserXml(user), MediaType.TEXT_PLAIN); representation.setMediaType(MediaType.TEXT_PLAIN); representation.setTag(new Tag(customerToMD5)); representation.setModificationDate(lastModifiedAtServer); getResponse().setStatus(Status.SUCCESS_OK); getResponse().setEntity(representation); } else { getResponse().setStatus(Status.REDIRECTION_NOT_MODIFIED); } }
如果一个服务同时提供了-Modified和Etag,那么客户端可以在随后的请求里同时提供If-Modified-Since和If-None-Match。服务器应当做双重检查,并且仅当二者满足时菜返回表示。