利用HTTP协议获取163的联系人列表(2)

 二、Java核心实现

2.1 Cookie的实现

JAVA平台,访问URL资源是通过一系列协议处理器(protocol handler)来实现的。URL的起始部分指定了URL使用的协议。比如某个URL是以file:开头的,这表明这个URL资源是保存在本地文件系统的。J2SE50定义了几个必须实现的协议:httphttpsfilejar

作为http协议处理器实现的一部分,J2SE5.0增加了一个CookieHandler。这个类提供了一些用于管理cookies的接口。Cookie是保存在浏览器缓存中的一小块数据。当你访问一个网站然后再次访问的时候,这个cookie数据用于鉴别你的身份。Cookies能够用于保存信息,譬如一个在线商店用于保存以购商品信息。Cookie可以是短期的,为一个单独的web事务保存数据,直到关闭浏览器;也可以是长期的,保存数据一个星期或一年。

J2SE5中并没有设置默认的CookieHandler。不过你可以注册一个Handler以便程序能够保存cookies并且在http连接的时候得到这些cookies。这一节首先讨论Cookie的实现。

Cookie类的两个方法:hasExpired()matches().hasExpired()方法用于表明这个cookie是否已经过期;matches()方法用于检验这个cookie与某个URI是否匹配. 对于一个已经过期的cookie,matchs方法总是返回false:

public boolean matches(URI uri) {

     if (hasExpired()) {

      return false;

 }

 String path = uri.getPath();

  if (path == null) {

     path = "/";

  }

  return path.startsWith(this.path);

}

public   boolean  hasExpired() { 

  if  (expires ==  null ) { 

    return   false ; 

  } 

  Date now = new  Date(); 

  return  now.after(expires); 

} 

 

public  String toString() { 

  StringBuilder result = new  StringBuilder(name); 

  result.append("=" ); 

  result.append(value); 

  return  result.toString(); 

}

Cookie的构造函数部分,你需要从URI以及报文头里面解析出所需要的信息.其中的cookie有效日期信息的格式是确定的,虽然其它信息对不同的网站有不同的格式.但只需要把cookie路径,有效日期,域名这些信息保存下来即可。

public  Cookie(URI uri, String header) { 

  String attributes[] = header.split(";" ); 

  String nameValue = attributes[0 ].trim(); 

  this .uri = uri; 

  this .name = nameValue.substring( 0 , nameValue.indexOf('=')); 

  this .value = nameValue.substring(nameValue.indexOf('=')+ 1 ); 

  this .path =  "/" ; 

  this .domain = uri.getHost(); 

 

  for  ( int  i= 1 ; i attributes.length; i++) { 

    nameValue = attributes[i].trim(); 

    int  equals = nameValue.indexOf('='); 

    if  (equals == - 1 ) { 

      continue ; 

    } 

    String name = nameValue.substring(0 , equals); 

    String value = nameValue.substring(equals+1 ); 

    if  (name.equalsIgnoreCase( "domain" )) { 

      String uriDomain = uri.getHost(); 

      if  (uriDomain.equals(value)) { 

        this .domain = value; 

      } else  { 

        if  (!value.startsWith( "." )) { 

          value = "."  + value; 

        } 

        uriDomain = 

          uriDomain.substring(uriDomain.indexOf('.' )); 

        if  (!uriDomain.equals(value)) { 

          throw   new  IllegalArgumentException( 

            "Trying to set foreign cookie" ); 

        } 

        this .domain = value; 

      } 

    } else   if  (name.equalsIgnoreCase( "path" )) { 

      this .path = value; 

    } else   if  (name.equalsIgnoreCase( "expires" )) { 

      try  { 

        this .expires = expiresFormat1.parse(value); 

      } catch  (ParseException e) { 

        try  { 

          this .expires = expiresFormat2.parse(value); 

        } catch  (ParseException e2) { 

          throw   new  IllegalArgumentException( 

            "Bad date format in header: "  + value); 

        } 

      } 

    } 

}

注意:Cookie规范中要求同时检查域名以及路径,为了简单起见,我们这里只检查了路径. 附录三中给出了Cookie的具体实现。

 

2.2 Cookie Handler的实现

回到CookieHandler这个类,这是个具有两组相关联方法的抽象类.第一组方法让你能得到当前已经设置的Handler或设置你自己的Handler:

        * getDefault()

        * setDefault(CookieHandler)

    对于安装了安全管理器的应用来说,得到或设置handler需要特别的权限.通过设置handlernull可以清除当前设置的handler.正如之前提到的,没有设置默认的handler.

第二组方法允许你从一个你维持的cookie缓存得到cookies,或将cookies保存到这个cookie缓存.

       * get(URI uri,MapString,ListString>>requestHeaders)

       * put(URI uri,MapString,ListString>>responseHeaders)

    get()方法从cookie缓存中的到之前保存的cookie并保存到requestHeaders.put()方法从response headers 中提取cookies并保存到cookie缓存.

在附录一中,你可能注意到这些应答信息里面包含了这个URL站点所用的web服务器以及其日期时间.同意可以看到里面包含了两行Set-Cookie,这就是报头里面携带的cookies.这些cookie能够保存下来,然后在下一次请求的时候被发送.

下面我们来建立一个CookieHandler,我们得实现CookieHandler的两个抽象方法:get()put()。其中put()方法将所有报头中的cookies保存到一个缓存中.为了实现put()方法,首先要从responseHeaders中得到"Set-Cookie"对应的List.

     ListString setCookieList = responseHeaders.get("Set-Cookie");

当你得到cookies对应的List,List中所有的值保存下来.如果这个cookie已经存在,就将已保存的替换掉。

if  (setCookieList !=  null ) {

for  (String item : setCookieList) {

Cookie cookie = new  Cookie(uri, item); 

        // Remove cookie if it already exists in cache  

        // New one will replace it

               for  (Cookie existingCookie : cache) {

               ... 

               }

            System.out.println("Adding to cache: "  + cookie);
            cache.add(cookie); 

} 

}

这里的"cache"可以是一个数据库或者是一个Collections Framework中的List.其中的Cookie类将在下面定义.从本质上说,这些就是put()方法所要做的事:对于响应报头中每一个cookie,这个方法将cookie保存到缓存中.

get()方法做的是相反的事情:将缓存中所有与URI匹配cookie添加到请求报头中,如果存在多个cookie,则建立一个用','分隔的列表.方法get()返回一个Map,而且用一个包含已有报文头的map作为参数,你应该将cookie缓存与之相匹配的cookie添加这个map里面去,但是这个Map是只读的,所以你应该首先新建另一个map,并将参数map中的内容复制过去,然后再将cookie添加进去,最后返回一个只读的map.

为了实现get()方法,首先要从cookie缓存中查找与URI相匹配的cookie,然后删除那些已经过期的cookie:

// Retrieve all the cookies for matching URI  

    // Put in comma-separated list  

    StringBuilder cookies = new  StringBuilder(); 

    for  (Cookie cookie : cache) { 

      // Remove cookies that have expired  

      if  (cookie.hasExpired()) { 

        cache.remove(cookie); 

      } else   if  (cookie.matches(uri)) { 

        if  (cookies.length() 0 ) { 

          cookies.append(", " ); 

        } 

        cookies.append(cookie.toString()); 

      } 

    } 

get()方法余下部分将上面的StringBuilder中的文本添加到一个Map,与之对应的key"Cookie"

MapString, ListString>> cookieMap = 

  new  HashMapString, ListString>>(requestHeaders); 

 

// Convert StringBuilder to List, store in map  

if  (cookies.length()   0 ) { 

  ListString list = 

    Collections.singletonList(cookies.toString()); 

  cookieMap.put("Cookie" , list); 

} 

return  Collections.unmodifiableMap(cookieMap);

Cookie Handler的具体实现可以参见附录三。

 

2.3 POST方法的实现

    在模拟HTTP POST请求的时候,需要首先设置好HTTP POST请求的头部。即connectionsetRequestProperty方法。在进行POST提交的时候,同时也需要设置POST请求的内容体。这时候需要首先获得connectionoutputStream,然后将需要提交的内容写入流中即可。

        URL url = new URL(urlAddress);

              HttpURLConnection connection = (HttpsURLConnection) url.openConnection();

 

              connection.setRequestProperty("Accept", ACCEPT);

              connection.setRequestProperty("Referer", REFFERER);

              connection.setRequestProperty("Accept-Language", ACCEPT_LANGUAGE);

              connection.setRequestProperty("Content-Type", CONTENT_TYPE);

              connection.setRequestProperty("UA-CPU", UA_CPU);

              connection.setRequestProperty("Accept-Encoding", Accept_Encoding);

              connection.setRequestProperty("User-Agent", USERAGENT);

              connection.setDoOutput(true);

 

              OutputStream outputStream = connection.getOutputStream();

              String requestContent = "verifycookie=1&style=-1&product=mail163&username=" + userName + "&password=" + password + "&selType=-1&remUser=on&secure=on&%B5%C7%C2%BC%D3%CA%CF%E4=";

              outputStream.write(requestContent.getBytes());

              outputStream.flush();

              outputStream.close();

 

2.4 GET方法的实现

在模拟HTTP GET请求的时候,同样需要首先设置好HTTP GET请求的头部。即connectionsetRequestProperty方法。这里同POST方法有所不同的是,connection需要设置GET方法。

URL url = new URL(urlAddress);

              HttpURLConnection connection = (HttpURLConnection) url.openConnection();

 

              connection.setRequestProperty("Accept", ACCEPT);

              connection.setRequestProperty("Accept-Language", ACCEPT_LANGUAGE);

              connection.setRequestProperty("UA-CPU", UA_CPU);

              connection.setRequestProperty("Accept-Encoding", Accept_Encoding);

              connection.setRequestProperty("User-Agent", USERAGENT);

 

              connection.setRequestMethod("GET");

              connection.connect();

在处理应答时,还有一点需要注意。这就是在发送请求后,得到的应答码。若正常返回则应答码为200,若是Redirect则应答码为302HTTPURLConnection中分别由HTTP_OKHTTP_MOVED_TEMP这两个常量表示。

 

2.5 读取应答头部

连接中的信息可能有一部分是属于报文头,这与所用的协议有关.我们可以通过URLConnection来得到这些报文头消息,这个类提供了一些能提取报文头信息的方法,包括:

 *  getHeaderFields() - Gets a Map of available fields.

 *  getHeaderField(String name) - Gets header fields by name.

 *  getHeaderFieldDate(String name, long default) - Gets the header field as a date.

 *  getHeaderFieldInt(String name, int default) - Gets the header field as a number.

     *  getHeaderFieldKey(int n) or getHeaderField(int n) - Gets the header field by position.

我们常常需要获取输出流的字符集编码

private String getcharSetNameFromHeaders(HttpURLConnection connection) {

              // Content-Type = [text/html; charset=GB2312]

              String retValue = UTF8;

              String content = connection.getHeaderField("Content-Type");

              int startIndex = content.indexOf("=") + 1;

              if (startIndex != 0)

                     retValue = content.substring(startIndex);

              return retValue;

}

 

2.6 读取应答体

在处理HTTP请求的应答时,需要读出HTTP应答的具体内容。根据HTTP协议规范,应答头部有content-length来标明此时的将要返回多少字节的数据流。我们可以利用connectiongetContentLength方法获得这个数值。

但有时getContentLength方法的返回值为-1,这时表明,HTTP应答是chunked。我们不知道本次返回多少字节长度的数据流。因此就不能按照上面的流程进行处理。这里我们采用循环等待WAITINGSECONDS秒后,若仍然没有可用的数据流,我们便终止本次读取。

private byte[] readByteArrayFromInputStream(InputStream inputStream, int contentLength) throws Exception {

              byte[] array = new byte[contentLength];

 

              int readBytesNumber = 0;

              while (readBytesNumber < contentLength) {

                     int readBytesNumberThisTime = inputStream.read(array, readBytesNumber, contentLength - readBytesNumber);

                     if (readBytesNumberThisTime == -1) {

                            throw new Exception("读字节错误");

                     }

                     readBytesNumber += readBytesNumberThisTime;

              }

              return array;

       }

 

private String getResponseContent(HttpURLConnection connection, String charsetName) throws Exception {

              InputStream inputStream = connection.getInputStream();

              int contentLength = connection.getContentLength();

              StringBuilder content = new StringBuilder();

 

              if (contentLength == -1) {

                     int length = inputStream.available();

                     int time = 0;

                     while (time < WAITINGSECONDS) {

                            if (length > 0) {

                                   time = 0;

                                   content.append(readStringFromInputStream(inputStream, length, charsetName));

                            } else {

                                   Thread.sleep(1000);

                                   time++;

                            }

                            length = inputStream.available();

                     }

              } else {

                     content.append(readStringFromInputStream(inputStream, contentLength, charsetName));

              }

 

              return content.toString();

       }

 

       private String readStringFromInputStream(InputStream inputStream, int contentLength, String charSetName) throws Exception {

              return new String(readByteArrayFromInputStream(inputStream, contentLength), charSetName);

       }

 

2.7 读入GZIP数据流

1.2节所示, GET请求返回的应答Content-Encoding可能为gzip,我们就不能直接获取返回的内容信息。我们首先要根据应答的Content-Length,读入对应长度的字节流。然后为了获取联系人列表相关信息,将读入的字节流按照gzip的格式进行解压。这里需要注意的是在读取的过程中,我们可以一次性读入BUFFER_SIZE长度的字节,放入缓冲区。

private String getZipInputStreamContent(InputStream inputStream, int length, String charSet) throws Exception {

              byte[] array = readByteArrayFromInputStream(inputStream, length);

 

              ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(array);

              GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);

 

              ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(3 * length);

              byte[] buf = new byte[BUFFER_SIZE];

              int num;

 

              while ((num = gzipInputStream.read(buf, 0, BUFFER_SIZE)) != -1) {

                     byteArrayOutputStream.write(buf, 0, num);

              }

              gzipInputStream.close();

              byteArrayOutputStream.close();

 

              return new String(byteArrayOutputStream.toByteArray(), charSet);

}

 

2.8 常量定义

    2.3节和2.4节所示,在发起HTTP GETPOST请求的时候,需要对请求的头部的一些变量进行设置。这里,给出了获取163邮箱需要定义的常量。

private static final String USERAGENT = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MyIE2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; CIBA)";

       private static final String ACCEPT = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/x-shockwave-flash, */*";

       private static final String ACCEPT_LANGUAGE = "zh-cn,en-US;q=0.5";

       private static final String UA_CPU = "x86";

       private static final String Accept_Encoding = "gzip, deflate";

       private static final String REFFERER = "http://mail.163.com/";

       private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";

 

       private static final String ADDRESSBOOKADDRESS = "http://tg 1a 4.mail.163.com/coremail/fcg/ldvcapp?funcid=address&sid=SIDVALUE&tempname=address%2Faddress.htm&ifirstv=&listnum=0&showlist=all";

       private static final String POSTFORMADDRESS = "https://reg.163.com/logins.jsp?type=1&url=http://fm163.163.com/coremail/fcg/ntesdoor2?lightweight%3D1%26verifycookie%3D1%26language%3D-1%26style%3D-1";

 

三、相关说明

(1)无意义信息

1.1节所示,登录过程中的步骤(2)主要目的是在Cookie中设置Province=025,目的是为了登录163后显示与登录相关的广告。因此,在实际操作中,可以直接从步骤(3)直接模拟。

在利用工具EffeTech HTTP SnifferIE Http Analyzer进行分析时,这个过程中可以看到许多无意义的信息(例如来自于yadaomicrosoft),我们仅需要分析163有关信息即可。

 

(2)缓冲区和网络阻塞

connection中阅读返回的信息时,一定要根据HTTP应答的头部来读对应的字节长度。但由于网络不确定等复杂情况的影响,有可能不是一次可以读取全部的信息,因此需要设置一定长度的缓冲区,一次尝试读取固定长度的字节,否则可能造成阻塞。这是多线程中经典的阅读者和写作者的示例。

这里需要注意的是,读取和发送是面向字节的,而不是面向字符的。因此在读取和写入的过程中,只能利用inputstreamoutputstream,而不能利用readerwriter

 

(3) 重定向问题

2.4节和在1.1节的步骤(5)中,应答时,正常返回的应答码为302,即2.4节所说的redirectHTTPConnection获取到本次应答为redirect,则进行自动跳转。但本步骤中,我们需要在应答头部获取SID值和将要访问的实际的163的地址,因此需要阻止跳转。我们可以在具体连接之前调用connectionsetInstanceFollowRedirects方法:

connection.setInstanceFollowRedirects(false);

改正后的示例代码如下

URL url = new URL(urlAddress);

       HttpURLConnection connection = (HttpURLConnection) url.openConnection();

 

       connection.setRequestProperty("Accept", ACCEPT);

       connection.setRequestProperty("Accept-Language", ACCEPT_LANGUAGE);

       connection.setRequestProperty("UA-CPU", UA_CPU);

       connection.setRequestProperty("Accept-Encoding", Accept_Encoding);

       connection.setRequestProperty("User-Agent", USERAGENT);

 

       connection.setRequestMethod("GET");

       connection.setInstanceFollowRedirects(false);

       connection.connect();

 

(4)网络分析

    在模拟HTTP 请求和应答的全过程,需要随时利用EffeTech HTTP SnifferIE Http Analyzer进行监控。在利用Java模拟的全过程,一定要保证对Java代码模拟的程序运行的网络监控和实际利用浏览器访问163邮箱一致,否则可能在某步骤导致模拟错误。

 

四、相关工具及文档

(1) 使用CookieHandler管理Cookie数据 http://eastsun.javaeye.com/blog/85182

(2) 163信箱登陆分析 http://uri8.cn/blog/ 2008/11/15 /163信箱登陆分析/

(3) CSDN http://topic.csdn.net/u/20080617/09/9c3b90be-7000-4f8b-bc4d-d500bd5be648.html

(4) EffeTech HTTP Sniffer 独立网络分析工具

(5) IE Http Analyzer IE植入性网络分析工具
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值