Java開發筆記(一百一十)GET方式的HTTP調用

所謂術業有專攻,一個程序單靠自身難以吃成大胖子,要想讓程序變得血肉豐滿,勢必令其與外界多加交流,汲取天地之精華,方能練就蓋世功夫。那麼程序應當如何與外部網絡進行通訊呢?計算機網絡的通訊標準主要採起TCP/IP協議組,該協議組又可分爲三個層次:網絡層、傳輸層和應用層。其中網絡層包括IP協議、ICMP協議、ARP協議等等,傳輸層包括包含TCP協議與UDP協議,而應用層擁有FTP、HTTP、TELNET、SMTP等協議。在應用程序的開發過程當中,最多見的網絡編程是HTTP協議的接口編碼,Java爲HTTP編程提供的開發工具名叫HttpURLConnection,經過它能夠實現絕大多數的網絡數據交互功能。
獲取HttpURLConnection實例的辦法很簡單,只要調用URL對象的openConnection方法,便可在開啓網絡鏈接的同時獲得HTTP鏈接對象。由此看來,獲取HTTP鏈接對象只需如下兩行代碼:javascript

			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:獲取應答報文的壓縮方式。
根據以上的方法說明,若要從對方網址獲取應答報文,只需將輸入流轉爲字符串就行,寥寥幾行的轉換代碼示例以下:html

//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方式爲例,按照順序大體分爲下列四個步驟:java

一、設置各項請求參數,包括請求方式、鏈接超時、讀取超時等等;
二、調用connect方法開啓鏈接;
三、調用getInputStream方法獲得輸入流,並從中讀出字符串形式的應答報文;
四、調用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"); // 查詢上證指數

 

運行上面的股指接口調用代碼,輸出瞭如下的上證指很多天志:app

應答內容長度=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開發筆記(序)章節目錄

相關文章
相關標籤/搜索