要使用 HttpURLConnection,最好对一些基础概念有所认识,比如 TCP/IP 协议,HTTP 报文, Socket 等。
先谈一些我的认识,有可能不完全正确:
- Socket 应该是 TCP 协议层的概念,如果要使用 Socket 直接通信,需要使用远程地址和端口号。其中,端口号根据具体的协议而不同,比如 HTTP 协议默认使用的端口号为 80/tcp。
- HttpURLConnection 是在底层连接上的一个请求,最终也是通过 Socket 连接网络,所谓的 underlaying Socket。本文结尾我也会附上相关帖子连接。但是使用 HttpURLConnection 不需要我们专门去处理远程地址和端口号。
- HttpURLConnection 只是一个抽象类,只能通过 url.openConection() 方法创建具体的实例。严格来说,openConection() 方法返回的是 URLConnection 的子类。根据 url 对象的不同,如可能不是 http:// 开头的,那么 openConection() 返回的可能就不是 HttpURLConnection。
- HttpURLConnection 的 connect() 和 disconnect() 方法有必要特别强调一下,我会在下文使用到的地方详细说明。
我在测试 HttpURLConnection 的时候,是分别使用 HTTP 的 GET 和 POST 方法发送消息到 http://ip.taobao.com//service/getIpInfo.php 查询 IP 地址归属地。http://ip.taobao.com/instructions.php 是 GET 方法接口说明。
下面来具体说一下 HttpURLConnection 的使用步骤。
-
获得 HttpURLConnection 对象
// 如果使用 POST 方法 URL url = new URL("http://ip.taobao.com//service/getIpInfo.php"); // 如果打算使用 GET 方法 //URL url = new URL("http://ip.taobao.com/service/getIpInfo.php?ip=xxx.xxx.xxx.xxx"); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-
设置请求属性
在连接到远程资源(可以简单理解为远端服务器,但是这么说不准确)之前,可以设置一些 HttpURLConnection 的属性。// 设置连接超时时间 connection.setConnectTimeOut(15000); // 设置读取超时时间 connection.setReadTimeOut(15000); // 设置请求参数,即具体的 HTTP 方法 connection.setRequestMethod("POST"); // 添加 HTTP HEAD 中的一些参数,可参考《Java 核心技术 卷II》 connection.setRequestProperty("Connection", "Keep-Alive"); // 设置是否向 httpUrlConnection 输出, // 对于post请求,参数要放在 http 正文内,因此需要设为true。 // 默认情况下是false; connection.setDoOutput(true); // 设置是否从 httpUrlConnection 读入,默认情况下是true; connection.setDoInput(true);
这些属性的设置要在 connect() 之前完成。如果对 HTTP 包信息的结构有很好的理解,有助于理解这些方法。
setDoOutput() 方法是为了下面 getOutputStream();
setDoInput() 方法是为了下面 getInputStream()。
按照我在手机上测试,getOutputStream 和 getInputStream 内部都会隐式的调用 connect()。不过这只是我手机上的环境,严谨的来讲,我觉得还是应该自己显示的调用 conect()。(多次调用 connect(),后面的调用自动忽略) -
调用 connect() 连接远程资源
connection.connect();
这会与服务器建立 Socket 连接,而连接以后,连接属性就不可以再修改;但是可以查询服务器返回的头信息了(header information)。
connect 成功手机上 logcat 会打印相关信息,包括目标 IP 地址。我是用魅族做的测试,其他品牌理论上也应该会打印。 -
利用 getOutputStream() 传输 POST 消息
说明一下,POST 消息才需要写数据,GET 不需要。BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8")); writer.write("ip=xxx.xxx.xxx.xxx"); writer.flush(); writer.close();
上面提到过,getOutputStream 会隐式的调用 connect()。
这里要注意的,主要是 HTTP 传输的消息要使用 URL UTF-8 编码,英文字母、数字和部分符号保持不变,空格编码成'+'。其他字符编码成 "%XY" 形式的字节序列,特别是中文字符,不能直接传输。可以考虑使用
URLEncoder.encode(string, "UTF-8") 方法。 -
查询服务器头信息
理论上,connect() 以后就可以查询服务器返回的头信息了。并且,getOutputStream 里面会隐式调用 connect()。
但是,查询服务器消息要在写完所有要传输的数据以后。
如果 getResponseCode 或者 getResponseMessage 以后,是不能向 outputStream 写消息的,报错为:cannot write request body after response has been read
这两个方法内部都调用了 getInputStream()。
因为有资料说,getInputStream() 的时候才会真正把 outputStream 里面的消息发出去。想想,这么做是有道理的:这样就允许我们关闭 outputStream 后重新打开,并且补充数据。这么理解的话,getResponseCode 内部调用了 getInputStream,导致 outputStream 已经发送;而一个 HttpURLConnection 只能发送一个请求,所以就不能再向 outputStream 写数据,否则就等于传输了两个消息。
我没有在手机上安装抓手机报文的工具,所以没有直接验证。
实际使用时,肯定是先通过 outputStream 传输数据,然后查询服务器的返回信息,所以 outputStream 消息到底是什么时候发送出去的,我们不需要太关心。
查询头信息的方法有一下几个:
// 这两个方法结合,可以查询所有消息头字段
public String getHeaderFieldKey (int n)
public String getHeaderField(int n)// 返回一个包含消息头所有字段的标准 map 对象
public Map<String,List<String>> getHeaderFields()// 为了方便使用,以下方法可以查询各标准字段
public String getContentType()
public int getContentLength()
public String getContentEncoding()
public long getDate()
public long getExpiration()
public long getLastModified() -
利用 getInputStream() 访问资源数据
使用 getInputStream() 方法获取一个输入流用以读取信息(这个输入流与 URL 类中的 openStream 方法所返回的流相同)。另一个方法 getContent 在实际操作中并不是很有用。由标准内容类型(比如 text/plain 和 image/gif)所返回的对象需要使用 com.sun 层次结构中的类来进行处理。也可以注册自己的内容处理器。
---《Java 核心技术 卷II》,CH3 网络,使用 URLConnection 获取信息private String convertStreamToString() { InputStream inputStream = connection.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream )); StringBuffer sb = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } String reponse = sb.toString(); return reponse; }
-
关闭 HttpURLConnection
本身要 HttpURLConnection 是很简单的,调用connection.disconnect()
就可以了。
这里是想说明一下,是否需要关闭,应该根据实际需要来。
当 HttpURLConnection 是 "Connection: close " 模式,那么关闭 inputStream 后就会自动断开连接。
当 HttpURLConnection 是 "Connection: Keep-Alive" 模式,那么关闭 inputStream 后,并不会断开底层的 Socket 连接。这样的好处,是当需要连接到同一服务器地址时,可以复用该 Socket。这时如果要求断开连接,就可以调用connection.disconnect()
了。
当然,HttpURLConnection 连接到底是不是 Keep-Alive 模式,除了 HttpURLConnection 请求设置为 Keep-Alive 外 (http 1.0中默认是关闭的,http 1.1中默认启用Keep-Alive),也需要服务器支持 Keep-Alive,才可以真正建立 Keep-Alive 连接。// 连接 和 断开连接 的 log,IP 地址为手机 IP I/System.out: [socket][/192.168.1.101:60330] connected I/System.out: close [socket][/192.168.1.101:60330]
-
补充一点
在我测试http://ip.taobao.com//service/getIpInfo.php 的时候,服务器一直不能正常返回 IP 地址对应的信息。最后发现,是淘宝服务器故意不响应我们这样非浏览器发起的 IP 查询请求。所以我还设置了 HttpURLConnection 的如下属性,伪装成浏览器,当然,是在 connect() 之前。connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.7 Safari/537.36");
调试联网程序的时候,出错有时候很难说是哪里的问题,用抓包软件分析是很有必要的;检查服务器的 ResponseCode 也是有必要的。
关于 HttpURLConnection 的学习,我觉得《Java 核心技术 卷II》写的不错。
我也参考了《Android 进阶之光》和下面两个链接。
关于 HTTP 的 GET 方法和 POST 方法,刚开始有些疑惑,也是看了《Java 核心技术 卷II》,以及下面两个链接。
工作中经常用到的话,有必要专门学习一下 HTTP 协议和报文。