飞信2010分析 – 使用HTTP模式和服务器通信

时间:2021-05-03 10:18:05

飞信2010分析 – 使用HTTP模式和服务器通信

2010年8月1日  | 分类: 飞信哪些事儿  | 标签:

在 飞信2010分析 – SIPC验证 文章里,简单的提到了飞信使用HTTP传输模式。
HTTP模式是所有传输方式中效率最低,是在其他传输方式都建立失败后才会采用HTTP传输模式,可见飞信在网络环境适应上下了很大的功夫的。

在第一步获取自适应的配置中,曾经提到下面几个参数:
含义 位置 结果举例
SIPC-PROXY /config/server/sipc-proxy 221.130.46.141:8080
SIPC-SSL /config/server/sipc-ssl-proxy 221.130.46.141:443
HTTP-TUNNEL /config/server/http-tunnel HTTP://221.130.46.141/ht/sd.aspx
飞信建立连接主要就是这三个传输方式。

来看下飞信的建立连接的顺序:
第一步: 尝试连接建立 SIPC-PROXY连接 。 即SIPC服务器端口为8080的连接。 这是最稳定的,效率最高的传输方式。
第二步: 如果第一步失败,则尝试建立SIPC-SSL连接。 即SIPC服务器端口为443的连接。 和SIPC-PROXY连接方式完全一致,就是端口变了。虽说是SSL端口,却仍然没有加密;
第三步: 如果第二步失败,则尝试建立HTTP-TUNNEL连接。 即HTTP代理连接。 采用长连接的模式,效率比较低,而且不太稳定。不过只要能访问HTTP,就可以连接上飞信服务器。
第四步: 如果第三步建立失败,则飞信客户端和飞信服务器建立连接失败。

可以看出如果要分析HTTP模式,则需要把SIPC-PROXY和SIPC-SSL的IP和端口堵上。一般都是相同的IP不同的端口,可以使用防火墙来完成。
HTTP分析可以使用HTTPAnalyzer帮助分析。 在我的飞信资源整合那篇文章中有这两个工具的下载。

在分析之前,还是简单的介绍长连接模式。因为HTTP模式下就是使用了长连接的方式。
我们知道HTTP是一个请求-回复类型的协议。在发起一个请求之后,服务器需要返回一个回复。可以理解为是单向的,请求只能是浏览器发送,服务器只能被动的接受请求处理请求,并不能主动的和客户端发送消息。
在Socket通信模型中,客户端和服务器的通信是双向的,客户端可以主动的向服务器发送信息,服务器也可以主动的向客户端发送信息。这在即时通信里面很有必要,假如其他用户主动的发消息给你,服务器可以在第一时间内主动的发消息通知你。
飞信是需要双向通信的模式,但HTTP服务器是不能主动的给客户端发送信息的。于是产生了长连接的技术,也就是Comet,来模拟服务器主动给客户端发送消息。
长连接其实很简单,客户端主动的发起一个HTTP请求给服务器,服务器收到这个请求后,会查看是否有消息可以返回给客户端,如果有把消息放入回复体内,立即返回给客户端。如果没有,就阻塞掉这个请求,不返回,直到服务器有可以返回的消息或者超时才结束这个请求。客户端检查这个请求,如果是超时,就立即发起另外一个请求,如果不是超时,就处理服务器的返回的数据,处理完了继续发起下一个请求。
这样服务器就始终保留了一个客户端的连接,如果收到消息后就可以立即返回给客户端,完全可以模拟Socket双向的通信模式。

只不过这种模式效率低,而且不太稳定,服务器资源占用率大(因为要维持多个连接),不过在一些特别的情景下需要使用。目前WebQQ和WebFetion等即时通信的WEB都是采用了这样的通信模式。
详细的长连接信息请参见IBM工程师写的文章 – Comet:基于 HTTP 长连接的“服务器推”技术 。

分析很简单的,不罗嗦过程,直接写结果吧。
HTTP模式下,假如HTTP://221.130.46.141/ht/sd.aspx为获取自适应配置返回的HTTP-TUNNEL地址:

下面是一个HTTP请求的例子:

01. POST /ht/sd.aspx?t=s&i=2 HTTP/1.1
02. Cookie: ssic=CRIOAAA0L0hiEKIY+8MORa3QNc4OJcB3rACWXefS7rmhOaQ5fifHEGsFh8+wPK91WlYD9M4zEqvgSG/hiReF2X30HurJRVRinJLO12MtuQdrY9lN4fpni9AvwA6Qj/FDkKr0wT8AAA==
03. Accept: */*
04. Pragma: xz4BBcV9df9de4e-0cd9-49c5-bf32-6adc28fcee63
05. Host: 221.130.46.141
06. Content-Length: 134
07. Content-Type: application/oct-stream
08. User-Agent: IIC2.0/PC 4.0.0000
09. Connection: Keep-Alive
10. Cache-Control: no-cache
11.  
12. R fetion.com.cn SIP-C/4.0
13. F: 685592830
14. I: 1
15. Q: 1 R
16. CN: 258dbcc7cc8a1633c677ebb0abaa5168
17. CL: type="pc",version="4.0.3340"
18.  
19. SIPP
20. ------------------------------------
21. HTTP/1.1 200 OK
22. Connection: close
23. Date: Sun, 01 Aug 2010 03:14:12 GMT
24. Server: Microsoft-IIS/6.0
25. X-Powered-By: ASP.NET
26. X-AspNet-Version: 2.0.50727
27. Cache-Control: private
28. Content-Type: application/oct-stream
29. Content-Length: 4
30.  
31. SIPP

就着这个例子,逐步的分析。
请求的方式为POST。
在URL上面有两个参数:
t : 请求的模式,取值有 i,s,d,下面会有详细的说明;
i : 请求的序号,随着请求的次数递增;
还传递了一个Cookie: ssic。 ssic是SSI登录成功后,会在HTTP头中使用标准set-cookie方式返回;
在请求头中还有一个Pragma域,在整个请求中不变,可能是标志唯一的客户端。前面七个字符固定为xz4BBcV,后面是一个GUID,可以随机生成
其他可以保持不变。

请求体就是 SIPC信令包+SIPP。 请注意这里一定要加上SIPP四个字符,如果没有SIPC信令包,也要发送SIPP四个字符。信令包的内容和过程和SIPC-PROXY连接发送的内容是完全一样的。
但有一个包有点区别,就是第二步验证的时候 后面的值不同,在SIPC-PROXY和SIPC-SSL连接中这个值为1FFF,在HTTP模式下这个值为AFF。这个值的含义,是表明了客户端支持的特性。如果有时间可能会写文章来分析这个值代表的含义,但现在只需要知道,这个值影响的后面和在线的好友建立对话的方式。

请求的回复头没有什么有价值的东西,回复体就是 SIPC信令包+SIPP 。注意这里还是有个SIPP四个字符。如果有信令包需要返回给客户端就把信令包直接放在回复体中。服务器也可以不返回任何信令包,只返回一个SIPP。

基本的格式就是这样。下面看下过程。
简单的过程和建立一个对话很相似:建立连接 -> 发送消息,接收消息,发送消息,接收消息…..-> 关闭连接。

1、建立连接的过程很简单,参数t的值为i,POST的内容只有一个SIPP四个字符,如果返回200,建立连接成功;
2、发送和接收消息,请求体放置了需要发送的SIPC信令包,回复体中放置需要接收的SIPC信令包。 注意这里可能有多个信令包可以放入一个请求体或者回复体中。
如果没有任何SIPC信令包可以发送,仍需发送空的请求包(即只含有SIPP四个字符的请求体)给服务器建立长连接。服务器有消息就会把消息放入回复体中,立即返回这个请求,如果没有就阻塞这个请求,直到超时。
3、关闭连接的过程也很简单,参数的t为d,POST的内容只有一个SIPP四个字符,如果返回200,关闭连接成功;

下面是SSISignV4Test例子中的HttpTranfer.java的部分代码,实现了HTTP传输方式,各位可以参考下,完整的例子可以在首页的公告找到下载地址。

01. public void startTransfer() throws Exception
02. {
03.     Runnable r = new Runnable(){
04.         public void run()
05.         {
06.             try{
07.                 ByteWriter writer =  new ByteArrayWriter();
08.                 //最开始执行了一个i请求
09.                 writer.write("SIPP".getBytes());
10.                 if(!tryExecuteRequest("i", requestId++, writer, 1))
11.                     throw new Exception("Init Http Transfer failed..");
12.  
13.                 while(!closeFlag) {
14.                     writer.clear();
15.                     BytesEntry entry = bytesEntryQueue.poll(5,TimeUnit.SECONDS);        //等待五秒,如果没有元素也返回
16.                     if(entry!=null) {
17.                         writer.writeBytes(entry.getBytes(), entry.getOffset(), entry.getLength());  //
18.                         while(bytesEntryQueue.size()>0)  {
19.                             entry = bytesEntryQueue.poll();
20.                             writer.writeBytes(entry.getBytes(), entry.getOffset(), entry.getLength());
21.                         }
22.                     }
23.                     writer.write("SIPP".getBytes());
24.  
25.                     //尝试发送这个请求,如果超过指定次数,传递传输异常
26.                     if(!tryExecuteRequest("s",requestId++,writer,3)) {
27.                         closeFlag = true;
28.                         throw new Exception("execute http request failed..");
29.                     }
30.                 }
31.  
32.                 //结束
33.                 writer.clear();
34.                 writer.write("SIPP".getBytes());
35.                 tryExecuteRequest("d", requestId++, writer, 1);
36.  
37.             }catch(Throwable e) {
38.                  throw new RuntimeException(e);
39.             }
40.         }
41.     };
42.  
43.     this.runThead = new Thread(r);
44.     this.runThead.setName("HttpTransfer");
45.     this.runThead.start();
46. }
47.  
48.  /**
49.      * 执行一个请求
50.      * @param s          类型(i,s,d)
51.      * @param i          请求的序号
52.      * @param writer     数据
53.      * @throws IOException
54.      */
55.     private void executeRequest(String s, int i,ByteWriter writer) throwsIOException
56.     {
57.         String turl = this.url+"?";
58.         turl += "t="+s;
59.         turl += "&i="+ Integer.toString(i);
60.  
61.         URL realURL = new URL(turl);
62.         HttpURLConnection connection = (HttpURLConnection) realURL.openConnection();
63.         connection.setRequestMethod("POST");
64.         connection.setRequestProperty("User-Agent""IIC2.0/PC 4.0.3340");
65.         connection.setRequestProperty("Pragma"this.pragma);
66.         connection.setRequestProperty("Content-Type""application/oct-stream");
67.         connection.setRequestProperty("Accept""*/*");
68.         connection.setRequestProperty("Cookie""ssic="+this.ssic);
69.         connection.setDoOutput(true);
70.         OutputStream out = connection.getOutputStream();
71.         out.write(writer.toByteArray(), 0, writer.size());
72.         out.flush();
73.  
74.         if(connection.getResponseCode()==200) {
75.             int contentLength = connection.getContentLength();
76.             if(contentLength>4)  {
77.                 InputStream in = connection.getInputStream();
78.                 writer.clear();
79.                 while(contentLength>4){
80.                     writer.writeByte(in.read());
81.                     contentLength--;
82.                 }
83.                 if(writer.size()>0)  {
84.                     this.bytesReceived(writer.toByteArray(), writer.size());
85.                 }
86.             }
87.         }else {
88.             throw new IOException("Invalid response stateCode="+connection.getResponseCode());
89.         }
90.     }

其实HTTP模式下的连接很简单,就是传输SIPC信令包的方式不同而已,其他的没有太大的变化。可以想象HTTP传输方式就是把SIPC-PROXY传输的信令包换了一种方式传输而已。
但有一点有很大的区别,就是HTTP模式下和PROXY,SSL模式下和在线好友建立对话的方式完全不同。这个会在下几篇文章中详细的分析。

HTTP模式就分析到这,下一篇分析在SIPC-PROXY,SIPC-SSL这两种模式下,如何和在线的好友建立会话。