神奇!明明是 socket,被我玩成了 http

現在,我們已經充分了解了 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 的理解。


分享到:


相關文章: