--- title: 'Http 通訊 API - Apache、Java' disqus: kyleAlien --- Http 通訊 API - Apache、Java === ## Overview of Content 如有引用參考請詳註出處,感謝 :cat: [TOC] ## Http 概述 Http 是一個協議,**在 IOS 7 層協議中屬於應用層協議**,這些協議相互之間並沒有綁定,也就是說應用層的 Http 協議可以不用一定與 TCP/IP 協議綁定,它可以使用其他底層協議 ```mermaid graph TB; subgraph 應用層 Http end subgraph 傳輸層 TCP UDP end subgraph 網路層 IP end Http -.-> TCP Http -.-> UDP TCP --> IP UDP --> IP ``` * Http 協議是提供一種可靠的傳輸 1. 客戶端發起一個請求到服務器端口(Port)建立連線 2. 當伺服器處理完要回應給客戶的數據時,會透過 Http 協議的規範(狀態行、頭訊息、資料體)通知客戶 ### 請求 Http 的工具包 * Android 開發中最常使用的三種 Http 通訊 API 如下表 | API 來源 | 相關包名 | 其他 | | - | - | - | | Java SE | `java.net.*`、`java.io.*`、`java.nio.*` | ![image](https://hackmd.io/_uploads/SyyjQ00Ua.png) | | Apache | `org.apache.http.*` | ![image](https://hackmd.io/_uploads/BJnKm0CU6.png) | | Android | `android.net.*`、 | ![image](https://hackmd.io/_uploads/BJEnQCC8T.png) | ## Apache API 在早期 Android 中最常使用的是 Apache API,它也是很多 Library 訪問網路的基礎 由於新版 Android 已經不推薦使用 Apache API 來訪問網路,所以要進行開發需要先在 Gradle 中設置 Apache 依賴 ```kotlin= dependencies { implementation("org.apache.httpcomponents:httpclient:4.5.13") } ``` * 以下為 Apache 相關類的功能如下表 | 類 | 概述 | | - | - | | URIBuilder | URI 的創建,可以在這裡設定 URI 的參數、取得 URI 的參數路徑 | | RequestConfig | 請求設定,可以設定這次請求的 timeout 時間、Cookie、最大轉傳次數... | | HttpClientBuilder | Http 請求器的 Builder,最終可以創建 HttpClient | | HttpClient | 網路請求的對象,可以想成一個瀏覽器 | :::info * 如果要限定請求的 Scheme 可以使用 ConnectionManager 註冊指定 Scheme > 以下限定 `http`/`https` 兩個 Scheme ```kotlin= val register = RegistryBuilder .create<ConnectionSocketFactory>() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build() val connectionManager = PoolingHttpClientConnectionManager(register) val httpClient = HttpClientBuilder.create() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build() ``` ::: ### HttpGet - Get 請求 HTML * 我們在本地做一個簡單的 Servlet 服務端,並透過 Apache API 訪問這個本地服務端提供的 GET API ```java= @WebServlet( name = "Hello", urlPatterns = { "/hello-servlet/*" }, loadOnStartup = 1 ) public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); String name = request.getParameter("name"); // Hello PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1> Hello! " + name + " !</h1>"); out.println("</body>"); out.close(); } } ``` * Apache API 使用 GET 請求範例如下 ```kotlin= fun get(url: String) { val urlWithParams = URIBuilder(url).apply { setParameter("name", "Kyle") this. } val requestConfig = RequestConfig.custom() .setConnectTimeout(10_000) // http timeout .setSocketTimeout(10_000) // socket timeout .build() val httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestConfig) .build() val httpGet: HttpUriRequest = HttpGet(urlWithParams.build()) httpClient.execute(httpGet).apply { if (statusLine.statusCode != 200) { throw IllegalStateException("Request api failure.") } val inputData = entity.content val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { println("Response: \n${String(buf, Charset.defaultCharset())}") } } } ``` * 使用 Android 單元測試來測試以上代碼 ```kotlin= @Test fun testGet() { ApacheApi().get("http://127.0.0.1:8080/JavaEEHelloWorld-1.0-SNAPSHOT/hello-servlet") } ``` > ![image](https://hackmd.io/_uploads/S1_OSyJPT.png) ### HttpPost - Post 請求 Json * 使用 Apache 使用進行 Http 的 Post 請求,範例如下 > 這裡使用 [**wanandroid**](https://www.wanandroid.com/blog/show/2;jsessionid=F3BD2FDE53D1BE4E1243B1D19AB860AB) 對外提供的 API ```kotlin= fun post(url: String) { val uri = URIBuilder(url).build() val httpClient = HttpClientBuilder.create() .build() val params: MutableList<NameValuePair> = ArrayList().apply { // 創建 Post 參數 add(BasicNameValuePair("username", "Yoyo")) add(BasicNameValuePair("password", "123456")) } val httpPost = HttpPost(uri).apply { // 設定 Post 參數 entity = UrlEncodedFormEntity(params) } // Post 請求 httpClient.execute(httpPost).apply { if (statusLine.statusCode != 200) { throw IllegalStateException("Request api failure.") } val inputData = entity.content val outPutStream = ByteArrayOutputStream() val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { outPutStream.write(buf) } println("Response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}") } } ``` * 使用 Android 單元測試來測試以上代碼 ```kotlin= @Test fun testPost() { ApacheApi().post("https://www.wanandroid.com/user/login") } ``` > ![image](https://hackmd.io/_uploads/SyDeTfkwp.png) ### POST 上傳 MIME 文件 如果要使用 Apache API 來上傳不同種類的 MIME 裝置,那需要先添加另一個 Library ```kotlin= dependencies { implementation("org.apache.httpcomponents:httpmime:4.5.13") } ``` * Servlet 服務端的範例程式如下(Java) ```java= @MultipartConfig @WebServlet( "/upload.part" ) public class UploadServletPart extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); Part part = req.getPart("usePart"); String header = part.getHeader("Content-Disposition"); String fileName = header.substring(header.indexOf("filename=\"") + "filename=\"".length(), header.lastIndexOf("\"")); PrintWriter out = resp.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h3> fileName: " + fileName + "</h3>"); out.println("</body>"); writeTo(fileName, part); } private void writeTo(String fileName, Part part) throws IOException { try( InputStream inputStream = part.getInputStream(); OutputStream outputStream = new FileOutputStream("/home/alien/IdeaProjects/JavaEEHelloWorld/" + fileName) ) { byte[] bytes = new byte[1024]; int len; while ((len = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, len); } } } } ``` * **使用 Apache API 訪問以上面 Servlet** 使用 MultipartEntityBuilder 建立 MultipartEntity 物件,並設定到 HttpPost 的 `entity` 成員並請求,就可以透過 Post 上傳數據 1. **使用 `addPart` 方法上傳圖片** ```kotlin= fun postPart(url: String, fileName: String) { val uri = URIBuilder(url).build() val httpClient = HttpClientBuilder.create() .build() val inputStream = FileInputStream(File(fileName)) val inputStreamBody = InputStreamBody(inputStream, fileName) val entityBuilder = MultipartEntityBuilder.create().apply { // addPart("usePart", inputStreamBody) } val httpPost = HttpPost(uri).apply { entity = entityBuilder.build() } httpClient.execute(httpPost).apply { if (statusLine.statusCode != 200) { throw IllegalStateException("Request api failure. ${statusLine.statusCode}") } val inputData = entity.content val outPutStream = ByteArrayOutputStream() val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { outPutStream.write(buf) } println("Response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}") } } ``` 2. **使用 `addBinaryBody` 方法上傳圖片**:直接使用 InputStream 並設定 ContentType(MIME 類型)為「`multipart/form-data`」 ```kotlin= fun postPart(url: String, fileName: String) { val uri = URIBuilder(url).build() val httpClient = HttpClientBuilder.create() .build() val inputStream = FileInputStream(File(fileName)) val entityBuilder = MultipartEntityBuilder.create().apply { // 上傳 binary body addBinaryBody("usePart", inputStream, ContentType.MULTIPART_FORM_DATA, fileName) } val httpPost = HttpPost(uri).apply { entity = entityBuilder.build() } httpClient.execute(httpPost).apply { if (statusLine.statusCode != 200) { throw IllegalStateException("Request api failure. ${statusLine.statusCode}") } val inputData = entity.content val outPutStream = ByteArrayOutputStream() val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { outPutStream.write(buf) } println("Response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}") } } ``` * 測試程式 ```kotlin= @Test fun testPart() { ApacheApi().postPart("http://127.0.0.1:8080/JavaEEHelloWorld-1.0-SNAPSHOT/upload.part", "test_pic.png") } ``` > ![image](https://hackmd.io/_uploads/rJNgqhWDa.png) 上傳結果 > ![image](https://hackmd.io/_uploads/HkXYonWP6.png) :::info * HML 使用 POST 的設定方式 ```xml= <!-- HTML 內的使用方式 --> <form method="post" enctype="multipart/form-data" action="upload.part"> <br>File: <label> <input type="file" name="usePart"/> </label> <button>Upload</button> </form> ``` ::: ## Java API 以下使用 `java.net` 包,在 Android 開發中也可以直接使用(簡單用來判斷、做 HTTP 請求都可以),而不用依賴 Android API ### InetAddress - Host IP 地址 ```mermaid classDiagram InetAddress <|-- Inet4Address InetAddress <|-- Inet6Address ``` 1. 使用 Host 取得 **Host 的 IP 地址** ```kotlin= fun getHostAddress(hostName: String) { try { val address: InetAddress = InetAddress.getByName(hostName) println("Host($hostName) ip: $address") } catch (e: UnknownHostException) { e.printStackTrace() } } ``` > ![image](https://hackmd.io/_uploads/rkaexa-Pa.png) 2. 取得 IPv4、IPv6 兩個型態的 IP Address ```java= fun getAllHostAddress(hostName: String) { try { val address: Array<InetAddress> = InetAddress.getAllByName(hostName) address.forEach { addr -> println("Host($hostName) ip: $addr") } } catch (e: UnknownHostException) { e.printStackTrace() } } ``` > IPv6 由 64 位元(Bit)組成 > > ![image](https://hackmd.io/_uploads/rkCIbpZPa.png) ### URL/UrlConnection - 訪問 HTTP * 我們使用以上做的 Servlet 服務端,並透過 Java API(URL) 訪問這個本地服務端提供的 GET API ```java= @WebServlet( name = "Hello", urlPatterns = { "/hello-servlet/*" }, loadOnStartup = 1 ) public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); String name = request.getParameter("name"); // Hello PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1> Hello! " + name + " !</h1>"); out.println("</body>"); out.close(); } } ``` * Java API(URL) 進行 HTTP 請求範例如下: 透過 URL#`openStream()` 取得服務端輸入到應用端的 inputStream ```kotlin= fun accessUrl(urlStr: String) { val url = URL(urlStr) val inputData = url.openStream() val outPutStream = ByteArrayOutputStream() val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { outPutStream.write(buf) } println("Response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}") } ``` * 使用 Android 單元測試來測試以上代碼 ```kotlin= @Test fun testAccessUrl() { JavaApi().accessUrl("http://127.0.0.1:8080/JavaEEHelloWorld-1.0-SNAPSHOT/hello-servlet?name=Kyle") } ``` > ![image](https://hackmd.io/_uploads/BkZnKa-Da.png) * **`URLConnectin` 使用範例**: 從上面可以看到,我們無法在 URL 中設定 Http 的 Header,這時我們就可以 **使用 URLConnectin 類來設定 HTTP Header** * 顯示所有 Header 的 Servlet 服務 ```java= @WebServlet( urlPatterns = { "/show.header" } ) public class ShowHeaderServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException { PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); Enumeration<String> headers = req.getHeaderNames(); while (headers.hasMoreElements()) { String headerName = headers.nextElement(); out.println("<br> name: " + headerName + ", value: " + req.getHeader(headerName) + "</br>"); } out.println("</body>"); out.close(); } } ``` * Java API(URL) 進行 HTTP 請求範例如下: 透過 URL#`openConnection()` 取得 `UrlConnection` 類來設定 Header ```kotlin= fun accessUrlWithConnection(urlStr: String) { val url = URL(urlStr) val urlConnection = url.openConnection().apply { doInput = true doOutput = true setRequestProperty("Content-type", "text/html") setRequestProperty("Accept-Charset", "big5, utf-8") } urlConnection.connect() val inputData = urlConnection.getInputStream() val outPutStream = ByteArrayOutputStream() val buf = ByteArray(1024) while ((inputData.read(buf)) != -1) { outPutStream.write(buf) } println("Response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}") } ``` * 使用 Android 單元測試來測試以上代碼 ```kotlin= @Test fun testAccessUrlWithConnection() { JavaApi().accessUrlWithConnection("http://127.0.0.1:8080/JavaEEHelloWorld-1.0-SNAPSHOT/show.header") } ``` > ![image](https://hackmd.io/_uploads/By5MhaWDa.png) ### Socket - 訪問 HTTP * Socket 也稱為套接字,他是透過在客戶端建立一個通道與伺服器端通訊…(像是 Pipe) 只需要客戶端簡單的對 Socket 傳遞資料,伺服器就會收到資料 > `java.net.Socket` 的建構子不支援直接傳入 URL,而是需要傳入主機名稱和連接埠號 ```kotlin= fun accessUrlBySocket(urlStr: String) { val url = URL(urlStr) val socket = Socket(url.host, url.port) println("socket.inetAddress: ${socket.inetAddress}") socket.close() } ``` > ![image](https://hackmd.io/_uploads/H16kDabvp.png) :::success * 除了基礎的 Socket 之外,Java 還有提供其他 Socket 類,常見的如下表 | Socker 相關類 | 概述 | | - | - | | DatagramSocket | 使用在 **UDP** 協定 | | MulticastSocket | 用於 **多點傳送的 Socket** | | ServerSocket | 使用在伺服器端,在伺服器端監聽客戶端的 Socket | ::: ## Android 使用網路 API 上述的 Apache、Java API… 都可以直接使用在 Android 開發中 ### 讀取圖片 * 這裡我們搭配 Android 提供的 Bitmap 來加載 Http 請求後的圖片數據流 ```java= fun requestHttpForPic() { Thread { val url = URL("http://10.0.2.2:8080/JavaEEHelloWorld-1.0-SNAPSHOT/image.get") // val inputStream = url.openStream() val connection = url.openConnection().apply { connect() } val inputStream = connection.inputStream runOnUiThread { val imageView = findViewById<AppCompatImageView>(R.id.waitImageView) BitmapFactory.decodeStream(inputStream).apply { imageView.setImageBitmap(this) } } }.start() } ``` :::info * 如果使用 Android 模擬器連接本機的網路(`127.0.0.1`),那就要以下設置 1. 連接本機網路時,要使用 `10.0.2.2` 訪問本機的 `127.0.0.1` 2. 如果要請求 Http,那就需要設置 `network_security_config.xml` ```xml= <?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true" /> </network-security-config> ``` 並將這個檔案設置到 `AndoirdManifest.xml` 中 ```xml= <application ... android:usesCleartextTraffic="true" android:networkSecurityConfig="@xml/network_security_config"> ... </application> ``` ::: ## Appendix & FAQ :::info ::: ###### tags: `網路開發`