---
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.*` |  |
| Apache | `org.apache.http.*` |  |
| Android | `android.net.*`、 |  |
## 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")
}
```
> 
### 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")
}
```
> 
### 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")
}
```
> 
上傳結果
> 
:::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()
}
}
```
> 
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)組成
>
> 
### 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")
}
```
> 
* **`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")
}
```
> 
### 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()
}
```
> 
:::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: `網路開發`