回顾上一节,我们介绍了Socket是啥,如何建立C/S模式下的双向通信。
这次我们来看一看HTTP协议是什么样的,如何基于Socket建立HTTP请求。
基于Kotlin,我推荐使用spring-boot搭建server端,方便快捷。搭建方式不是终点,附上链接有兴趣的话可以自行查看:https://projects.spring.io/spring-boot/#quick-start
重点一:HTTP请求格式
一次完整的HTTP请求过程,从TCP三次握手的建立成功后开始,客户端按照指定的数据格式向服务端发送请求数据(即HTTP请求),服务端接受请求后,解析这些数据,处理完成业务逻辑,最后返回一个HTTP的响应给客户端。HTTP的响应内容同样是有标准的格式。无论是什么客户端或服务端,只要遵循该HTTP规范组织数据,它一定是通用的。
HTTP请求格式主要有四部分组成,分别是:请求行
,请求头
,空行
,消息体
。下面我们以GET方法为例,说明每一部分的数据格式和字段意义。
- 请求行:是请求消息的第一行,由三部分组成,分别是:请求方法(GET/POST/DELETE/PUT/HEAD)、请求资源的URI路径、HTTP的版本号。
GET /index.html HTTP/1.1
- 请求头:请求头中的信息有跟缓存相关的头(Cache-Control,If-Modified-Since)、客户端身份信息(User-Agent, Cookie)等。例如:
Cache-Control: max-age=0
Cookie:id=0x1123;stoken=fr9hfr87w7e68932%&*();ptoken=&fdospajpfejwp89@@#!
User-Agent: Mozilla/3.0
- 消息体:请求体是客户端发给服务端的请求数据,比如一些参数等,该部分不是必须的。
- 空行:属于协议结构的一部分,专门用来区分请求头和消息体。在编码中的体现就是
\r\n
重点二:HTTP响应格式
服务器接收处理完请求后会返回一个HTTP响应消息给客户端。HTTP响应消息的格式包括:状态行、响应头、空行、消息体。
- 状态行: 位于响应消息的第一行,有HTTP协议版本号,状态码和状态说明三部分构成。
HTTP/1.1 200 OK
- 响应头:服务器传递给客户端用于说明服务器的一些信息,以及将来继续访问该资源时的策略。
Connection:keep-alive
Content-Type: application/json;charset=UTF-8
Date: Thu, 08 Mar 2018 07:49:14 GMT
Content-Length: 35
- 响应体:服务端返回给客户端的数据部分,比如:视频流、图片、json字符串等。
- 空行:专门用于区分响应头和响应体的协议,编码中同样体现为
\r\n
下面我们基于上一节所讲的Socket基础,实现简单的get请求获取数据。
import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import java.net.Socket
import java.nio.charset.Charset
class SfSocket : Runnable {
override fun run() {
sendSocket()
}
val HOST = "10.59.47.206"
val PORT = 8001
fun sendSocket() {
val socket = Socket(HOST, PORT)
val path = "/hello"
val pw = PrintWriter(socket.getOutputStream())
val input = socket.getInputStream()
val sb = StringBuilder()
/**
* 为了成为一个合法的HTTP请求,我们需要做如下的组装,构造请求头及空行。
*/
val request = sb.append("GET $path HTTP/1.1\r\n")
.append("Host: $HOST\r\n")
.append("Connection: Keep-Alive\r\n")
.append("Accept-Encoding: gzip\r\n")
.append("Accept: application/json\r\n")
.append("User-Agent: sfhttp/0.0.1\r\n")
.toString() //请求头构造结束
pw.write("$request\r\n")//请求头下增加空行,标志请求头到此结束。
pw.flush()
var line = ""
var contentLength = 0
do {
line = readLine(input)
//如果有Content-Length消息头时取出
if (line.startsWith("Content-Length")) {
contentLength = Integer.parseInt(line.split(":")[1].trim())
}
//打印响应头部信息
Log.e("sfhttp:", "Header---$line")
//如果遇到了一个单独的回车换行(空行),则表示响应头结束。
} while (line != "\r\n")
val bodyStr = readBody(socket.getInputStream(), contentLength)
Log.e("sfhttp:", "Body---$bodyStr")
input.close()
pw.close()
socket.close()
}
@Throws(IOException::class)
fun readBody(inputstream: InputStream, contentLength: Int): String {
var byte: Byte = 0
var list = ArrayList<Byte>()
var total = 0
do {
byte = inputstream.read().toByte()
list.add(byte)
total++
} while (total < contentLength)
return String(list.toByteArray(), Charset.forName("UTF-8"))
}
@Throws(IOException::class)
private fun readLine(`is`: InputStream): String {
val lineByteList = ArrayList<Byte>()
var readByte: Byte
do {
readByte = `is`.read().toByte()
lineByteList.add(java.lang.Byte.valueOf(readByte))
} while (readByte.toInt() != 10)
val byteArr = lineByteList.toByteArray()
return String(byteArr, Charset.forName("UTF-8"))
}
}
顺便贴一下server端的核心代码:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by ghostinmatrix on 2018/3/5.
*/
@RestController
class HelloController {
@GetMapping(value = "/hello",produces="application/json;charset=UTF-8")
@ResponseBody
public String hello(HttpServletResponse rsp) throws IOException {
System.out.println("in hello");
return "{\"url\":\"hello from spring-boot\"}";
}
}
日志打印出来的结果可以看出,Response 成功200,数据格式为json,数据长度为33,最后包含一个空行作为响应头的结束标志。Body内为我们根据Content-Length读出的数据。
03-08 16:50:24.617 27716-31350/com.sf.sfhttp E/sfhttp:: Header---HTTP/1.1 200
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Content-Type: application/json;charset=UTF-8
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Content-Length: 33
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---Date: Thu, 08 Mar 2018 08:50:25 GMT
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Header---
03-08 16:50:24.618 27716-31350/com.sf.sfhttp E/sfhttp:: Body---{"url":"hello from spring-boot"}
总结:
1.明确了HTTP 协议规则,空行\r\n的意义是区分请求/响应头和请求/响应体而专门设计的。
2.试验了,只要按照上述HTTP协议格式组织请求数据,就能够作为真正的HTTP请求得到响应。
3.说明了,市面上所存在的这些框架(Okhttp、UrlConnection、HttpClient等),其根本都是基于Socket和HTTP协议进行的封装。只不过,我们的demo非常简单,没有任何的验证措施和安全保障。但我们可以基于已有的demo继续进行封装。