Java开发笔记(一百一十)GET方式的HTTP调用
所谓术业有专攻,一个程序单靠自身难以吃成大胖子,要想让程序变得血肉丰满,势必令其与外界多加交流,汲取天地之精华,方能练就盖世功夫。那么程序应当如何与外部网络进行通信呢?计算机网络的通信标准主要采取tcp/ip协议组,该协议组又可分为三个层次:网络层、传输层和应用层。其中网络层包括ip协议、icmp协议、arp协议等等,传输层包括包含tcp协议与udp协议,而应用层拥有ftp、http、telnet、smtp等协议。在应用程序的开发过程中,最常见的网络编程是http协议的接口编码,java为http编程提供的开发工具名叫httpurlconnection,通过它可以实现绝大多数的网络数据交互功能。
获取httpurlconnection实例的办法很简单,只要调用url对象的openconnection方法,即可在开启网络连接的同时得到http连接对象。由此看来,获取http连接对象只需以下两行代码:
url url = new url(address); // 根据网址字符串构建url对象
// 打开url对象的网络连接,并返回httpurlconnection连接对象
httpurlconnection conn = (httpurlconnection) url.openconnection();
不过获取http连接对象仅是访问网络的第一步,后面还有更多更复杂的操作,本着先易后难的原则,下面先列出httpurlconnection工具的几个基础方法:
setrequestmethod:设置连接对象的请求方式,主要有get和post两种。
setconnecttimeout:设置连接的超时时间,单位毫秒。
setreadtimeout:设置读取应答数据的超时时间,单位毫秒。
connect:开始连接,之后才能获取该网址返回的应答报文信息。
disconnect:断开连接。
getresponsecode:获取应答的状态码。200表示成功,403表示禁止访问,404表示页面不存在,500表示服务器内部错误。
getinputstream:获取连接的输入流对象,从输入流中可读出应答报文。
getcontentlength:获取应答报文的长度。
getcontenttype:获取应答报文的类型。
getcontentencoding:获取应答报文的压缩方式。
根据以上的方法说明,若要从对方网址获取应答报文,只需将输入流转为字符串就行,寥寥几行的转换代码示例如下:
//http数据解析用到的工具类
public class streamutil {
// 把输入流中的数据转换为字符串
public static string istostring(inputstream is) throws ioexception {
byte[] bytes = new byte[is.available()]; // 创建临时存放的字节数组
is.read(bytes); // 从文件输入流中读取字节数组
return new string(bytes); // 把字节数组转换为字符串并返回
}
}
接着展开连接对象的方法调用,以get方式为例,按照顺序大致分为下列四个步骤:
1、设置各项请求参数,包括请求方式、连接超时、读取超时等等;
2、调用connect方法开启连接;
3、调用getinputstream方法得到输入流,并从中读出字符串形式的应答报文;
4、调用disconnect方法断开连接;
下面是指定网址发起get调用,并获取应答报文的方法代码例子:
// 对指定url发起get调用
private static void testcallget(string callurl) {
try {
url url = new url(callurl); // 根据网址字符串构建url对象
// 打开url对象的网络连接,并返回httpurlconnection连接对象
httpurlconnection conn = (httpurlconnection) url.openconnection();
conn.setrequestmethod("get"); // 设置请求方式为get调用
conn.setconnecttimeout(5000); // 设置连接的超时时间,单位毫秒
conn.setreadtimeout(5000); // 设置读取应答数据的超时时间,单位毫秒
conn.connect(); // 开始连接
// 打印http调用的应答内容长度、内容类型、压缩方式
system.out.println( string.format("应答内容长度=%d,内容类型=%s,压缩方式=%s",
conn.getcontentlength(), conn.getcontenttype(), conn.getcontentencoding()) );
// 从输入流中获取默认的字符串数据,既不支持gzip解压,也不支持gbk编码
string content = streamutil.istostring(conn.getinputstream());
// 打印http调用的应答状态码和应答报文
system.out.println( string.format("应答状态码=%d,应答报文=%s",
conn.getresponsecode(), content) );
conn.disconnect(); // 断开连接
} catch (exception e) {
e.printstacktrace();
}
}
然后尝试通过上述的testcallget方法获取实际业务信息,比如利用中国天气网的开放接口来查询北京天气,便给该方法填入北京天气的查询地址,调用代码如下所示:
testcallget("http://www.weather.com.cn/data/sk/101010100.html"); // 查询北京天气
运行上面的天气接口调用代码,输出了以下的天气预报日志:
应答内容长度=-1, 内容类型=text/html, 压缩方式=null
应答状态码=200, 应答报文={"weatherinfo":{"city":"北京","cityid":"101010100","temp":"27.9","wd":"南风","ws":"小于3级","sd":"28%","ap":"1002hpa","njd":"暂无实况","wse":"<3","time":"17:55","sm":"2.1","isradar":"1","radar":"jc_radar_az9010_jb"}}
原来http接口调用这么简单呀,那再来访问一个股指接口,利用新浪财经的公开接口查询上证指数,调用代码如下所示:
testcallget("https://hq.sinajs.cn/list=s_sh000001"); // 查询上证指数
运行上面的股指接口调用代码,输出了以下的上证指数日志:
应答内容长度=74, 内容类型=application/javascript; charset=gbk, 压缩方式=null
应答状态码=200, 应答报文=var hq_str_s_sh000001="??????,3246.5714,30.2762,0.94,4691176,47515638";
咦,为啥这次的返回报文出现了类似“??????”的乱码?此处的乱码位置原本应该返回汉字,之所以没有显示汉字却显示乱码,是因为程序未能正确处理字符编码。目前的接口访问代码,默认采取国际通用的utf-8编码,但中文世界有自己独立的一套gbk编码,股指接口返回的内容类型“application/javascript; charset=gbk”就表示本次返回的应答报文采取gbk编码。使用gbk编码的中文字符,反过来使用utf-8来解码,二者的编码标准不一致,难怪解出来变成乱码了。之前天气接口的内容类型未明确指定字符编码,则默认使用utf-8编码,然后调用方同样使用utf-8来解码,因此收到的应答报文是正常的中文。
与字符编码类似的情况还有数据压缩的编码标准,多数时候服务器返回的报文采用明文传输,但有时为了提高传输效率,服务器会先压缩应答报文,再把压缩后的数据送给调用方,这样同样的信息只耗费较小的空间,从而降低了网络流量的占用。然而一旦把压缩数据当作明文来解析,无疑也会产生不知所云的乱码,正确的做法是:调用方先获取应答报文的压缩方式,如果发现服务器采用了gzip方式压缩数据,则调用方要对应答数据进行gzip解压;如果服务器未指定具体的压缩方式,则表示应答数据使用了默认的明文,调用方无需进行解压操作。
综合考虑字符编码与数据压缩的兼容处理,则要根据getcontenttype方法返回的内容类型,以及getcontentencoding方法返回的压缩方式分别加以校验。其一判断内容类型是否包含charset字样,若有则按照指定的字符编码标准处理,若无则按照默认的utf-8标准处理。其二判断压缩方式是否包含gzip字样,若有则通过压缩输入流工具gzipinputstream对数据解压,若无则不做解压处理。据此重新编写应答报文的获取方法,具体的方法代码示例如下:
// 把输入流中的数据按照指定字符编码转换为字符串
public static string istostring(inputstream is, string charset) throws ioexception {
byte[] bytes = new byte[is.available()]; // 创建临时存放的字节数组
is.read(bytes); // 从文件输入流中读取字节数组
return new string(bytes, charset); // 把字节数组按照指定的字符编码转换为字符串并返回
}
// 从http连接中获取已解压且重新编码后的应答报文
public static string getunzipstring(httpurlconnection conn) throws ioexception {
// 获取应答报文的内容类型(包括字符编码)
string contenttype = conn.getcontenttype();
string charset = "utf-8"; // 默认的字符编码为utf-8
if (contenttype != null) {
if (contenttype.tolowercase().contains("charset=gbk")) { // 应答报文采用gbk编码
charset = "gbk"; // 字符编码改为gbk
} else if (contenttype.tolowercase().contains("charset=gb2312")) { // 应答报文采用gb2312编码
charset = "gb2312"; // 字符编码改为gb2312
}
}
// 获取应答报文的压缩方式
string contentencoding = conn.getcontentencoding();
// 获取http连接的输入流对象
inputstream is = conn.getinputstream();
string result = "";
if (contentencoding != null && contentencoding.contains("gzip")) { // 应答报文使用了gzip压缩
// 根据输入流对象构建压缩输入流
try (gzipinputstream gis = new gzipinputstream(is)) {
// 把压缩输入流中的数据按照指定字符编码转换为字符串
result = istostring(gis, charset);
} catch (exception e) {
e.printstacktrace();
}
} else {
// 把输入流中的数据按照指定字符编码转换为字符串
result = istostring(is, charset);
}
return result; // 返回处理后的应答报文
}
接下来把http访问过程中的streamutil.istostring方法,改为调用getunzipstring方法,也就是换成了下面这行代码:
// 对输入流中的数据进行解压和字符编码,得到原始的应答字符串
string content = streamutil.getunzipstring(conn);
之后重新运行上回的股指查询代码,从以下的上证指数日志可知应答报文里的中文正常显示出来了:
应答内容长度=74, 内容类型=application/javascript; charset=gbk, 压缩方式=null
应答状态码=200, 应答报文=var hq_str_s_sh000001="上证指数,3246.5714,30.2762,0.94,4691176,47515638";
get方式除了支持从服务地址获取应答报文,还支持直接下载网络文件。二者的区别在于:应答报文是从连接对象的输入流中获取字符串,而文件下载要把输入流中的数据写入本地文件。下面是通过get方式来下载网络文件的代码例子:
// 从指定url下载文件到本地
private static void testdownload(string filepath, string downloadurl) {
// 从下载地址中获取文件名
string filename = downloadurl.substring(downloadurl.lastindexof("/"));
// 把本地目录与文件名拼接成为本地文件的完整路径
string fullpath = filepath + "/" + filename;
// 根据指定路径构建文件输出流对象
try (fileoutputstream fos = new fileoutputstream(fullpath)) {
url url = new url(downloadurl); // 根据网址字符串构建url对象
// 打开url对象的网络连接,并返回httpurlconnection连接对象
httpurlconnection conn = (httpurlconnection) url.openconnection();
conn.setrequestmethod("get"); // 设置请求方式为get调用
conn.connect(); // 开始连接
inputstream is = conn.getinputstream(); // 从连接对象中获取输入流
// 以下把输入流中的数据写入本地文件
byte[] data = new byte[1024];
int len = 0;
while((len = is.read(data)) > 0){
fos.write(data, 0, len);
}
// 打印http下载的文件大小、内容类型、压缩方式
system.out.println( string.format("文件大小=%dk, 内容类型=%s, 压缩方式=%s",
conn.getcontentlength()/1024, conn.getcontenttype(), conn.getcontentencoding()) );
// 打印http下载的应答状态码和文件保存路径
system.out.println( string.format("应答状态码=%d, 文件保存路径=%s",
conn.getresponsecode(), fullpath) );
conn.disconnect(); // 断开连接
} catch (exception e) {
e.printstacktrace();
}
}
然后给这个testdownload方法填入本地目录、待下载的文件链接,具体的调用代码例子如下所示:
testdownload("d:/", "https://img-blog.csdnimg.cn/2018112123554364.png");
运行上述的下载代码,观察到以下的日志文字:
文件大小=120k, 内容类型=image/png, 压缩方式=null
应答状态码=200, 文件保存路径=d://2018112123554364.png
从下载日志可知,文件链接返回的内容类型为png图像,它的大小是120k,下载后的文件路径在d://2018112123554364.png。
更多java技术文章参见《java开发笔记(序)章节目录》
上一篇: 小技巧
下一篇: Java运行时数据区域
推荐阅读