Jodd 是一個開源的 Java 工具集, 包含一些實用的工具類和小型框架。簡單,卻很強大!
jodd-http是一個輕巧的HTTP客戶端。現在我們以一個簡單的示例從源碼層看看是如何實現的?
HttpRequest httpRequest = HttpRequest.get("http://jodd.org"); //1. 構建一個get請求 HttpResponse response = httpRequest.send(); //2.發送請求並接受響應信息 System.out.println(response);//3.打印響應信息
構建一個get請求
先復習一下http請求報文的格式:
下圖展示一般請求所帶有的屬性
調用get方法構建http請求:
/** * Builds a GET request. */ public static HttpRequest get(String destination) { return new HttpRequest() .method("GET") .set(destination); }
method方法如下:
/** * Specifies request method. It will be converted into uppercase. */ public HttpRequest method(String method) { this.method = method.toUpperCase(); return this; }
set方法如下:
/** * Sets the destination (method, host, port... ) at once. */ public HttpRequest set(String destination) { destination = destination.trim(); // http method int ndx = destination.indexOf(' '); if (ndx != -1) { method = destination.substring(0, ndx).toUpperCase(); destination = destination.substring(ndx + 1); } // protocol ndx = destination.indexOf("://"); if (ndx != -1) { protocol = destination.substring(0, ndx); destination = destination.substring(ndx + 3); } // host ndx = destination.indexOf('/'); if (ndx == -1) { ndx = destination.length(); } if (ndx != 0) { host = destination.substring(0, ndx); destination = destination.substring(ndx); // port ndx = host.indexOf(':'); if (ndx == -1) { port = DEFAULT_PORT; } else { port = Integer.parseInt(host.substring(ndx + 1)); host = host.substring(0, ndx); } } // path + query path(destination); return this; }
上述方法,根據destination解析出一下幾個部分:
1. 方法:HTTP1.1支持7種請求方法:GET、POST、HEAD、OPTIONS、PUT、DELETE和TARCE。
2. 協議:http或者https
3. 主機:請求的服務器地址
4. 端口:請求的服務器端口
5. 路徑+查詢參數,其中參數以“?”開頭,使用“&”連接
/** * Sets request path. Query string is allowed. * Adds a slash if path doesn't start with one. * Query will be stripped out from the path. * Previous query is discarded. * @see #query() */ public HttpRequest path(String path) { // this must be the only place that sets the path if (path.startsWith(StringPool.SLASH) == false) { path = StringPool.SLASH + path; } int ndx = path.indexOf('?'); if (ndx != -1) { String queryString = path.substring(ndx + 1); path = path.substring(0, ndx); query = HttpUtil.parseQuery(queryString, true); } else { query = HttpValuesMap.ofObjects(); } this.path = path; return this; }
發送請求
先熟悉一下http響應報文的格式:
響應首部一般包含如下內容:
/** * {@link #open() Opens connection} if not already open, sends request, * reads response and closes the request. If keep-alive mode is enabled * connection will not be closed. */ public HttpResponse send() { if (httpConnection == null) { open(); } // prepare http connection if (timeout != -1) { httpConnection.setTimeout(timeout); } // sends data HttpResponse httpResponse; try { OutputStream outputStream = httpConnection.getOutputStream(); sendTo(outputStream); InputStream inputStream = httpConnection.getInputStream(); httpResponse = HttpResponse.readFrom(inputStream); httpResponse.assignHttpRequest(this); } catch (IOException ioex) { throw new HttpException(ioex); } boolean keepAlive = httpResponse.isConnectionPersistent(); if (keepAlive == false) { // closes connection if keep alive is false, or if counter reached 0 httpConnection.close(); httpConnection = null; } return httpResponse; }
1. 打開HttpConnection
/** * Opens a new {@link HttpConnection connection} using * {@link JoddHttp#httpConnectionProvider default connection provider}. */ public HttpRequest open() { return open(JoddHttp.httpConnectionProvider); } /** * Opens a new {@link jodd.http.HttpConnection connection} * using given {@link jodd.http.HttpConnectionProvider}. */ public HttpRequest open(HttpConnectionProvider httpConnectionProvider) { if (this.httpConnection != null) { throw new HttpException("Connection already opened"); } try { this.httpConnectionProvider = httpConnectionProvider; this.httpConnection = httpConnectionProvider.createHttpConnection(this); } catch (IOException ioex) { throw new HttpException(ioex); } return this; }
判斷是否有連接,若沒有連接則創建一個新的連接。
2. 創建連接實現
/** * Creates new connection from current {@link jodd.http.HttpRequest request}. * * @see #createSocket(String, int) */ public HttpConnection createHttpConnection(HttpRequest httpRequest) throws IOException { Socket socket; if (httpRequest.protocol().equalsIgnoreCase("https")) { SSLSocket sslSocket = createSSLSocket(httpRequest.host(), httpRequest.port()); sslSocket.startHandshake(); socket = sslSocket; } else { socket = createSocket(httpRequest.host(), httpRequest.port()); } return new SocketHttpConnection(socket); }
3. 創建socket
根據協議的不同,http使用SocketFactory創建socket,https使用SSLSocketFactory創建SSLSocket。最終使用SocketHttpConnection進行包裝。
SocketHttpConnection繼承自HttpConnection,實現了socket的輸入輸出流連接。注意:https創建完SSLSocket時需要進行握手。
public class SocketHttpConnection implements HttpConnection { protected final Socket socket; public SocketHttpConnection(Socket socket) { this.socket = socket; } public OutputStream getOutputStream() throws IOException { return socket.getOutputStream(); } public InputStream getInputStream() throws IOException { return socket.getInputStream(); } public void close() { try { socket.close(); } catch (IOException ignore) { } } public void setTimeout(int milliseconds) { try { socket.setSoTimeout(milliseconds); } catch (SocketException sex) { throw new HttpException(sex); } } /** * Returns <code>Socket</code> used by this connection. */ public Socket getSocket() { return socket; } }
打開Connection的輸出流發送信息,打開connection的輸入流接受返回信息。
OutputStream outputStream = httpConnection.getOutputStream();
sendTo(outputStream);
InputStream inputStream = httpConnection.getInputStream();
發送過程:
protected HttpProgressListener httpProgressListener; /** * Sends request or response to output stream. */ public void sendTo(OutputStream out) throws IOException { Buffer buffer = buffer(true); if (httpProgressListener == null) { buffer.writeTo(out); } else { buffer.writeTo(out, httpProgressListener); } out.flush(); }
將緩沖區的數據寫入輸出流,並發送。
接受數據並讀取報文內容:
/** * Reads response input stream and returns {@link HttpResponse response}. * Supports both streamed and chunked response. */ public static HttpResponse readFrom(InputStream in) { InputStreamReader inputStreamReader; try { inputStreamReader = new InputStreamReader(in, StringPool.ISO_8859_1); } catch (UnsupportedEncodingException ignore) { return null; } BufferedReader reader = new BufferedReader(inputStreamReader); HttpResponse httpResponse = new HttpResponse(); // the first line String line; try { line = reader.readLine(); } catch (IOException ioex) { throw new HttpException(ioex); } if (line != null) { line = line.trim(); int ndx = line.indexOf(' '); httpResponse.httpVersion(line.substring(0, ndx)); int ndx2 = line.indexOf(' ', ndx + 1); if (ndx2 == -1) { ndx2 = line.length(); } httpResponse.statusCode(Integer.parseInt(line.substring(ndx, ndx2).trim())); httpResponse.statusPhrase(line.substring(ndx2).trim()); } httpResponse.readHeaders(reader); httpResponse.readBody(reader); return httpResponse; }
小結
從上面的代碼,我們可以看出http使用socket來建立和destination的連接,然后通過連接的輸出流和輸入流來進行通信。
參考文獻:
【1】http://www.it165.net/admin/html/201403/2541.html
【2】http://jodd.org/doc/http.html