还是先介绍一下问题背景:公司产品中,Java应用服务器会调用CGI进行一些系统操作,比如系统的备份、恢复等,这时候Java相当于一个客户端去请求Apache服务器。
但有时候系统操作很费时间,比如备份一个很大的数据库,可能需要几个小时,这种情况下首先要避免http请求超时,其次还要动态地给客户端以进度信息,提高系统可用性。
系统可用性因人而异,暂且不提。可能有有同学想,在建立http连接的时候将timeout设置为0,也就是不超时,是不是就可以了呢?但是,apache服务器本身也有超时时间的限制,HTTP连接上长时间没有输入输出,服务器会回收连接资源,服务器超时后,会抛下面的异常:
java.io.IOException: Premature EOF
at sun.net.www.http.ChunkedInputStream.readAheadBlocking(ChunkedInputStream.java:565)
at sun.net.www.http.ChunkedInputStream.readAhead(ChunkedInputStream.java:609)
at sun.net.www.http.ChunkedInputStream.read(ChunkedInputStream.java:696)
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(HttpURLConnection.java:3053)
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.Reader.read(Reader.java:140)
在分析这个问题的时候,发现有两种方案:
1. Java端发出http请求,apache服务器收到请求后马上返回成功,然后继续进行系统操作,在执行过程中调用由Java服务器提供的REST API来刷新进度。这种方案问题之一是要实现REST API,另外一个问题是Perl的CGI模块是否支持这种方式也未可知。
2. Server Push的方案,apache服务器不时将系统操作执行进度推送给Java端。这种方案只需要改造一下Java端代码来支持动态更新进度。
对方案二进行了验证,发现可行。
严格来说,这不是一个典型的Server Push应用,因为并没有用到标准里提到的那些技术,如内置iframe、Ajax长轮询、XMLHttpRequest长轮询等,但是也利用了Server Push的基本特征:http长连接,服务器主动推送数据。
方案的本质其实非常简单,Http 1.1以后,连接方式默认都是长连接,客户端和服务器建立连接后,服务器可以在任意时间向这个连接中写数据,而客户端可以从这个连接的输入流中定期读取数据,获得服务器端的状态,然后更新进度。借用网上的一张图来说明:
也就是说,并不是一次请求一次响应这种传统的WEB模式,而是在连接建立后,服务器能够多次直接向客户端推送数据。
验证代码:
Java Code
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class TestServerPush {
public static void main(String[] args) {
HttpURLConnection conn = null;
try {
URL url = new URL("HTTP", "10.208.135.246", 8001, "/cgi-bin/testServerPush.pl");
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
int retCode = conn.getResponseCode();
if (200 != retCode) {
System.out.println("retCode is " + retCode);
return;
}
InputStream content = (InputStream) conn.getContent();
InputStreamReader rdr1 = new InputStreamReader(content);
char[] cbuf = new char[200];
int pos = rdr1.read(cbuf);
while (-1 != pos) {
System.out.println(new String(cbuf)); // 这里更新执行进度
java.util.Arrays.fill(cbuf, '\0'); // 清空缓存
Thread.sleep(1000);
pos = rdr1.read(cbuf);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
}
Perl Code
#!/usr/bin/perl
use CGI;
use CGI::Carp qw ( fatalsToBrowser warningsToBrowser );
use warnings;
use strict;
$| = 1; ## turn autoflush on
$ENV{"PATH"}="/bin:/sbin:/usr/bin:/usr/sbin";
$<=$>;
my $q= new CGI;
print $q->header({-type=>'text/plain'});
my $count = 0;
while ($count < 5) {
print "$count---".`date`;
$count++;
sleep(2);
}
先用CURL验证一下
可以看到每两秒服务器推送一次数据,静态的图片可能看不清楚,真正执行一下可以看出是定时推送的。
使用Java代码执行,类似的效果。