現在,我們已經充分了解了 HTTP 和 Socket 的關係,也瞭解了 HTTP 報文的格式,為了讓小夥伴能夠加深對這兩個概念的理解,本文我們來看看如何利用 Socket 模擬 HTTP 請求。如果小夥伴們對 HTTP 和 Socket 的關係、HTTP 報文格式尚不熟悉的話,可以參考前面的文章 Http 和 Socket 到底是哪門子親戚?。
由於 HTTP 是基於 TCP 協議的應用層協議,因此我們可以用更為底層的方式來訪問 HTTP 服務,即直接使用 Socket 完成 HTTP 的請求和響應。我們前面說過,HTTP 的任務就是完成數據的包裝, Socket 提供了網絡的傳輸能力,所以我們只需要按照 HTTP 報文的格式來組裝數據,然後利用 Socket 將數據發送出去,就能得到回應。
POST 請求上傳數據
假設我現在有一個數據接口 http://localhost/hello,該接口每次接收一個參數 name ,調用成功之後返回給用戶一個 hello:name 字符串,那我們用 Socket 來實現這樣一個 HTTP 請求。
首先,我們要先組裝出 HTTP 的請求頭,如下(如果小夥伴對下面這個請求頭有疑問,請複習 Http 和 Socket 到底是哪門子親戚?一文):
<code>POST /hello HTTP/1.1Accept:text/htmlAccept-Language:zh-cnHost:localhostname=張三/<code>
我這裡為了簡單,只添加了三個請求頭,然後我們通過 Socket 將上面這個字符串發送出去:
<code>Socket socket = new Socket(InetAddress.getByName("localhost"), 80);OutputStream os = socket.getOutputStream();String data = "name=張三";int dataLen = data.getBytes().length;String contentType = "Content-Length:" + dataLen+"\\r\\n";os.write("POST /hello HTTP/1.1\\r\\n".getBytes());os.write("Accept:text/html\\r\\n".getBytes());os.write("Accept-Language:zh-cn\\r\\n".getBytes());os.write(contentType.getBytes());os.write("Host:localhost\\r\\n".getBytes());os.write("\\r\\n".getBytes());os.write(data.getBytes());os.write("\\r\\n".getBytes());os.flush();/<code>
我在 Serlvet 中接收這個請求並作簡單處理,如下:
<code>BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(),"UTF-8"));StringBuffer sb = new StringBuffer();String str;while ((str = br.readLine()) != null) { sb.append(str).append("\\r\\n");}System.out.println("sb:"+sb.toString());resp.setContentType("text/html;charset=utf-8");PrintWriter out = resp.getWriter();out.write(sb.toString());out.flush();out.close();/<code>
然後通過 Socket 中的輸入流我就能拿到響應結果,如下:
<code>BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));StringBuffer sb = new StringBuffer();String str;while ((str = br.readLine()) != null) { sb.append(str).append("\\r\\n");}System.out.println(sb.toString());/<code>
響應結果如下:
<code>HTTP/1.1 200 Content-Type: text/html;charset=utf-8Transfer-Encoding: chunkedDate: Sun, 03 Dec 2017 10:46:52 GMTname=張三/<code>
這是一個簡單的通過 POST 請求下載文本的案例。接下來我們再來一個 GET 請求下載圖片的案例,來加深對 Socket 的理解。
GET 請求下載圖片
這個實際上也不難,但是要實現圖片的下載需要我們首先熟悉HTTP響應的數據格式,不熟悉的小夥伴可以閱讀 Http 和 Socket 到底是哪門子親戚?一文。
下載圖片,響應頭是文本文件,響應數據是二進制文件,我們要想辦法通過空行將這兩塊數據分開,分別處理。為了解決這個問題,我首先提供一個工具類,這個工具類用來實現一行一行的解析字節流,如下:
<code>public class BufferedLineInputStream { private InputStream is; public BufferedLineInputStream(InputStream is) { this.is = is; } /** * @param buf 將數據讀入到byte數組中 * @param offset 數組偏移量 * @param len 數組長度 * @return 返回值表示讀取到的數據長度 -1表示數據讀取完畢,0表示其他異常情況 */ public int readLine(byte[] buf, int offset, int len) throws IOException { if (len < 1) { return 0; } //count用來統計已經向數組中存儲了多少數據 int count = 0, c; while ((c = is.read()) != -1) { buf[offset++] = (byte) c; count++; //如果一行已經讀完或者數組已滿 if (c == '\\n' || count == len) { break; } } return count > 0 ? count : -1; }}/<code>
然後將響應中的頭信息和圖片分別保存在不同的文件中,數據解析的核心思路就是一行一行讀取響應數據,當遇到 \\r\\n 表示頭信息已經讀取完了,要開始讀取二進制數據了,二進制數據讀取到之後,將之保存成圖片即可。核心代碼如下:
<code>fos = new FileOutputStream("E:\\333.png");pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream("E:\\222.txt")));socket = new Socket(InetAddress.getByName("localhost"), 80);OutputStream out = socket.getOutputStream();out.write("GET /1.png HTTP/1.1\\r\\n".getBytes());out.write("Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\\r\\n".getBytes());out.write("Accept-Language:zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3\\r\\n".getBytes());out.write("Host:localhost\\r\\n".getBytes());out.write("\\r\\n".getBytes());BufferedLineInputStream blis = new BufferedLineInputStream(socket.getInputStream());int len;byte[] buf = new byte[1024];while ((len = blis.readLine(buf, 0, buf.length)) != -1) { String s = new String(buf, 0, len); System.out.println(s); if (s.equals("\\r\\n")) {//表示頭信息讀取完畢 break; }}//開始解析二進制的圖片數據while ((len = blis.readLine(buf, 0, buf.length)) != -1) { fos.write(buf, 0, len);}/<code>
OK,Socket 模擬 HTTP 請求我們就先說到這裡,兩個案例,希望能夠加深小夥伴對 Socket 和 HTTP 的理解。
閱讀更多 科技伍小黑 的文章