所谓术业有专攻,一个程序单靠自身难以吃成大胖子,要想让程序变得血肉丰满,势必令其与外界多加交流,汲取天地之精华,方能练就盖世功夫。那么程序应当如何与外部网络进行通信呢?计算机网络的通信标准主要采取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开发笔记(一百一十)GET方式的HTTP调用的更多相关文章

  1. Java开发笔记(八十八)文件字节I/O流

    前面介绍了如何使用字符流读写文件,并指出字符流工具的处理局限,进而给出随机文件工具加以改进.随机文件工具除了支持访问文件内部的任意位置,更关键的一点是通过字节数组读写文件数据,采取字节方式比起字符方式 ...

  2. Java开发笔记(八十六)通过缓冲区读写文件

    前面介绍了利用文件写入器和文件读取器来读写文件,因为FileWriter与FileReader读写的数据以字符为单位,所以这种读写文件的方式被称作“字符流I/O”,其中字母I代表输入Input,字母O ...

  3. Java开发笔记(八十五)通过字符流读写文件

    前面介绍了文件的信息获取.管理操作,以及目录下的文件遍历,那么文件内部数据又是怎样读写的呢?这正是本文所要阐述的内容.File工具固然强大,但它并不能直接读写文件,而要借助于其它工具方能开展读写操作. ...

  4. Java开发笔记(二十八)布尔包装类型

    前面介绍了数值包装类型,因为不管是整数还是小数,它们的运算操作都是类似的,所以只要学会了Integer的用法,其它数值包装类型即可一并掌握.但是对于布尔类型boolean来说,该类型定义的是“true ...

  5. Java开发笔记(二十九)大整数BigInteger

    早期的编程语言为了节约计算机的内存,给数字变量定义了各种存储规格的数值类型,比如字节型byte只占用一个字节大小,短整型short占用两个字节大小,整型int占用四个字节大小,长整型long占用八个字 ...

  6. Java开发笔记(三十)大小数BigDecimal

    前面介绍的BigInteger只能表达任意整数,但不能表达小数,要想表达任意小数,还需专门的大小数类型BigDecimal.如果说设计BigInteger的目的是替代int和long类型,那么设计Bi ...

  7. Java开发笔记(三十二)字符型与整型相互转化

    前面提到字符类型是一种新的变量类型,然而编码实践的过程中却发现,某个具体的字符值居然可以赋值给整型变量!就像下面的例子代码那样,把字符值赋给整型变量,编译器不但没报错,而且还能正常运行! // 字符允 ...

  8. Java开发笔记(三十四)字符串的赋值及类型转换

    不管是基本的char字符型,还是包装字符类型Character,它们的每个变量只能存放一个字符,无法满足对一串字符的加工.为了能够直接操作一连串的字符,Java设计了专门的字符串类型String,该类 ...

  9. Java开发笔记(三十五)字符串格式化

    前面介绍了字符串变量的四种赋值方式,对于简单的赋值来说完全够用了,即便是两个字符串拼接,也只需通过加号把两个目标串连起来即可.但对于复杂的赋值来说就麻烦了,假设现在需要拼接一个很长的字符串,字符串内部 ...

  10. Java开发笔记(三十八)利用正则表达式校验字符串

    前面多次提到了正则串.正则表达式,那么正则表达式究竟是符合什么定义的字符串呢?正则表达式是编程语言处理字符串格式的一种逻辑式子,它利用若干保留字符定义了形形色色的匹配规则,从而通过一个式子来覆盖满足了 ...

随机推荐

  1. How to change hostname on debian

    How to change hostname on Debian 10 Linux last updated July 13, 2019 in CategoriesDebian / Ubuntu, L ...

  2. Xamarin.Forms 开发热加载利器 HotReload 推荐

    https://github.com/AndreiMisiukevich/HotReload

  3. Redis-5.0.5集群配置

    版本:redis-5.0.5 参考:http://redis.io/topics/cluster-tutorial. 集群部署交互式命令行工具:https://github.com/eyjian/re ...

  4. 洛谷P1270 访问美术馆

    题目 树形DP,首先考虑递归建图,类似于线段树的中序遍历.然后取状态dp[i][j]表示i点花费j时间所偷到的最多的画,有方程: \(dp[now][nwt] = max(dp[now][nwt], ...

  5. 在windows环境下可以裁剪.jpg格式的图片

    发现在windows操作系统下,可以利用图片编辑器裁剪.jpg格式的尺寸大小.其四方有四个推子.可以移动.注意点击右方的“确定”按钮.

  6. Mac的移动硬盘不能装载该如何解决?

    昨天拔硬盘时,不能弹出,赶着要睡觉,就直接拔掉USB接口,谁料到今天再插进去,电脑不能识别,无法装载了. 我的天那, 里面很多重要资料,我以为硬盘坏了,要重新格盘了...T T 还好在网上找到了大神们 ...

  7. 缺陷描述(Description)

    [tips1] 缺陷报告的用途在于: 记录bug 对bug进行分类(发现者.日期.版本.模块.严重程度.优先级) 跟踪bug(new-open-fixed-closed) 对bug进行统计分析.总结 ...

  8. 关于js中onload事件的部分报错。

    当使用onload获取元素时,建议在onload事件之前定义需要获取的元素名称,在onload里面只执行获取操作,这样获取到的元素在后面才能顺利使用. <!DOCTYPE html> &l ...

  9. Web前端开发规范之脚本文件和动态文本文件命名规则

    脚本文件:一般使用脚本功能的英文小写缩写命名 实际模块:例如广告条的javascript文件名为ad.js,弹出窗口的javascript文件名为pop.js 公用模块:js文件命名:英文命名,后缀j ...

  10. Mysql创建测试大量测试数据

    修改mysql配置 max_heap_table_size=4000M innodb_flush_log_at_trx_commit=0sync_binlog=500 创建测试数据库 create d ...