概述
Android开发进阶(从小工到专家)读书笔记——HTTP 网络请求原理
HTTP 是一种应用层协议,通过 TCP 实现了可靠的数据传输,能够保证数据的完整性、正确性。TCP 对于数据传输控制的有点也能够提现在 HTTP 上,使得HTTP的数据传输吞吐量、效率得到保证。
- Android开发进阶从小工到专家读书笔记HTTP 网络请求原理
- HTTP 的请求方式
- 1 GET 请求
- 2 POST 请求
- 3 PUT 请求
- 4 DELETE 请求
- 5 HEAD 请求
- 6 TRACE 请求
- 7 OPTIONS 请求
- HTTP 报文格式解析
- 1 请求报文
- 2 响应报文
- 3 常见的请求头部
- 简单模拟 HTTP 服务器
- 1 服务器端
- 2 客户端
- 3 运行结果
- HTTP 的请求方式
HTTP 协议客户端与服务器交互流程如下:
1. 客户端执行网络请求,从 URL 中解析出服务器的主机名;
2. 将服务器的主机名转换成服务器的 IP 地址;
3. 将端口号从 URL 中解析出来;
4. 建立一条客户端与 Web 服务器的 TCP连接;
5. 客户端通过输出流向服务器发送一条 HTTP 请求;
6. 服务器向客户端回送一条 HTTP 响应报文;
7. 客户端从输入流获取报文;
8. 客户端解析报文,关闭连接;
9. 客户端将结果显示在 UI 上。
1. HTTP 的请求方式
1.1 GET 请求
GET 是最常用的方法,作用是获取服务器中的某个资源。GET 请求的参数都需要放在请求的 URL 中。
1.2 POST 请求
POST 方法起初是用来向服务器传递数据的。实际上,POST 请求通常用来提交 HTML 的表单,表单中填好的数据会传输给服务器,然后服务器对这些数据进行处理。
1.3 PUT 请求
与 GET 从服务器读取资源相反,PUT 方法会向服务器写入资源。PUT 方法的语义就是让服务器用请求的主体部分来创建一个由所请求的 URL 命名的新文档,如果该 URL 已经存在,就用这个资源来代替它。
1.4 DELETE 请求
DELETE 方法会请服务器删除请求 URL 所指定的资源,但客户端无法保证删除操作一定会被执行,因为 HTTP 规范允许服务器在不通知客户端的情况下撤销请求。与 GET 请求一样,参数都要放在请求的 URL 中。
1.5 HEAD 请求
HEAD 方法与 GET 方法的行为类似,但服务器在响应中只返回首部,不会返回实体的主体部分,这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查。使用 HEAD 可以在不获取资源的情况下了解资源的情况(如判断其类型);通过查看相依中的状态码,看看某个对象是否存在;通过查看首部,测试资源是否被修改了。遵循 HTTP/1.1 规范,就必须实现 HEAD 方法。
1.6 TRACE 请求
TRACE 请求会在目的服务器发起一个“环回”诊断,行程的最后一站的服务器会弹回一条 TRACE 响应,并在响应主体中携带它收到的原始请求报文,这样客户端就可以查看在所有中间 HTTP 应用程序组成的请求/响应链上,原始报文是否以及如何被毁坏或修改过。
TRACE 方法主要用于诊断,用于验证请求是否如愿穿过了请求/响应链,可以用来查看代理和其他应用程序对用户请求所产生效果
1.7 OPTIONS 请求
OPTIONS 方法请求 WEB 服务器告知其支持的各种功能。可以询问服务器通常支持哪些方法,或者对某些特殊资源支持哪些方法。这为客户端应用程序提供了一种手段,使其不用实际访问那些资源就能判定访问各种资源的最优方式。
2. HTTP 报文格式解析
2.1 请求报文
不同的请求方式,请求格式可能不一样,通常一个 HTTP 请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成。如下图:
[HTTP 报文格式]
起始行
报文的第一行就是起始行,在请求报文中用来说明要以什么方式做什么请求,而在响应报文中粗略说明报文的执行结果。首部字段
起始行后面有零个或多个首部字段。每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(Connection:keep-Alive)来分隔。首部以一个空格结束。主体
首部字段的空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了发送给 WEB 服务器的数据;响应主体中装载了要返回给客户端的数据。起始行和首部都是结构化的文本形式,而主体则可以包含任意的二进制数据(如图片、视频、音频、软件程序),也可以包含文本形式。
GET 和 DELETE 方法的功能是获取和删除,因此只需将 URL 构造为要处理的资源即可,即所有的参数附加在资源的 URL 最后,第一个参数前通过 “?” 符号连接,然后请求参数按照“参数名=参数值”的形式进行追加,每个参数之间用 “&” 连接。GET 和 DELETE 的 URL 最长长度为 1024 字节,即 1KB。
在浏览器中输入 http://www.devtf.cn/?p=909,得到的请求报文如下:GET /?p=909 HTTP/1.1 Host: www.devtf.cn Cache-Control: no-cache
第一行为请求行,代表请求方式是 GET,自路径为/?p=909,HTTP 版本为 1.1。后两行是请求的 HEADER 区域。
PUT 和POST 的报文格式一般是表单形式,即这两个请求方式的参数存储在报文的请求数据(报文主体)的位置上:POST /api/feed/ HTTP/1.1 Accept-Encoding: gzip Content-Length: 225873 Content-Type: multipart/form-data; boundary=SavageLin-YeRenFather Host: www.myhost.com Connection: Keep-Alive --SavageLin-YeRenFather Content-Disposition: from-data; name="username" Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Savage.Lin --SavageLin-YeRenFather Content-Disposition: from-data; name="images"; filename="/storage/emulated/0/Camera/jdimage/lxh0e3yyfmpr2e36tdowbavrx.jpg" Content-Type: application/octet-stream Content-Transfer-Encoding:binary 这里是图片的二进制数据,数据太长,在此省略 --SavageLin-YeRenFather--
上述请求的含义为想 www.myhost.com/api/feed/ 这个地址发送一个 POST 请求。这个请求的数据格式(Content-Type)为 multipart/form-data, 报文的 boundary 值为 SavageLin-YeRenFather。该报文有两个参数,一个参数是文本类型的 username 参数,值为 Savage.Lin,另一个是名为 images 的二进制参数,数据是一张图片的二进制数据,这里省略了图片的二进制数据。
一个参数是以两个横杆 (–) 加上 boundary 开始的,然后是该参数的一些属性信息,如参数名、格式等,然后再加上一个空行,最后才是参数的值。如 username 参数,完整格式如下:--SavageLin-YeRenFather // 两个横杆加上 boundary 值 Content-Disposition: from-data; name="username" // 这是 3 个请求参数的 Header 属性 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit // 这是一个不可省略的空行 Savage.Lin // 这是参数值
POST 与 PUT 都必须遵循这种格式,每个参数一两个横杆和 boundary 分隔,参数 header 与参数值之间有一个空行。最后,请求数据的最后是两个横杆+boundary 值+ 两个横杆作为整个报文的结束符。
2.2 响应报文
HTTP 响应也由 3 个部分组成,分别是:状态行、消息报头、响应正文。如下所示,HTTP 响应的格式与请求的格式十分类似:
<状态行>
<响应报文header>
<空行>
[响应报文内容]
在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况,如下:
HTTP-Version Status-Code Reason-Phrase CRLF
其中,HTTP-Version 表示服务器 HTTP 协议的版本;Status-Code 表示服务器返回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态代码由 3 位数字组成,第一个数字定义了响应的类型,且有 5 中可能取值,如下
取值范围 | 含义 |
---|---|
100~199 | 指示信息——表示请求已接收,继续处理 |
200~299 | 请求成功。表示请求已被成功接收、理解、接收 |
300~399 | 重定向。要完成请求必须进行更进一步的操作 |
400~499 | 客户端错误。请求有语法错误或请求无法实现 |
500~599 | 服务器端错误。服务器未能实现合法的请求 |
常见状态代码、状态描述的说明如下:
(a) 200 OK: 客户端请求成功
(b) 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
(c) 401 Unauthorized:请求未经授权,这个状态代码必需和 WWW-Authenticate 报头域一起使用
(d) 403 Forbidden:服务器收到请求,但是拒绝提供服务
(e) 404 Not Found:请求资源不存在,举个栗子:输入了错误的URL
(f) 500 Internal Server Error:服务器发生不可预期的错误
(g) 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
eg:这是一个 GET 请求的 Response 返回示例:
HTTP/1.1 200 OK
Data: Sat, 31 Dec 2005 23:59:59 GMT
Content-Type: text/html;charset=
Content-Length: 122
<html>
<head>
<title>开发技术前线</title>
</head>
<body>
<!-- This is Body -->
</body>
</html>
该请求返回码为 200, 表示请求成功。返回的数据类型为text/html,编码为ISO-8859-1,内容长度为122。在一个空行之后就是返回的数据,即 html 页面。
2.3 常见的请求头部
请求头部有键值对组成,每行一对,关键字和值用英文冒号“:”分隔。HTTP 规范定义了几种首部字段,应用程序也可以随意发明自己所用的首部,如下表:
首部类型 | 作用 |
---|---|
通用首部 | 既可以出现在请求报文中,也可以出现在响应报文 |
请求首部 | 提供更多有关请求的信息 |
响应首部 | 提供更多有关响应的信息 |
实体首部 | 描述主体的长度和内容,或资源自身 |
扩展首部 | HTTP 规范中没有定义的新首部 |
请求头部通知服务器关于客户端请求的信息,典型的请求头有:
* Content-Type:请求数据的格式
* Content-Length:消息长度
* Host:请求的主机名,允许多个域名同处一个 IP 地址,即虚拟主机
* User-Agent:发出请求的浏览器类型,可以自行设置
* Accept:客户端可识别的内容类型列表
* Accept-Encoding:客户端可识别的数据编码
* Connection:允许客户端和服务器指定与请求/响应连接有关的选项,例如,设置为 Keep-Alive 则表示保持连接
* TransFer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式。
3. 简单模拟 HTTP 服务器
HTTP 实际上是基于 TCP 的应用层协议,在更高层封装了 TCP 的使用细节,TCP 连接是因特网上基于流的可靠连接,它为 HTTP 提供了一条可靠的比特传输管道。 TCP 的数据通过名为 IP 分组(或 IP 数据报)的小数据块来发送,HTTP 要传送一条报文是,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输。TCP 收到数据流之后,会将数据流分割成被称作段的小数据块,并将段封装在 IP 分组中,通过因特网进行传输。
HTTP、HTTPS 协议
3.1 服务器端
一个 HTTP 请求就是一个典型的 C/S 模式,服务端在监听某个端口,客户端向服务端的端口发起请求,服务端解析请求,并向客户端返回结果。
public class SimpleHttpServer extends Thread {
public static final int HTTP_PORT = 8000; // 监听端口
ServerSocket mSocket = null; // 服务端Socket
public SimpleHttpServer() {
try {
// 构造服务端Socket,监听 8000 端口
mSocket = new ServerSocket(HTTP_PORT);
} catch (IOException e) {
e.printStackTrace();
}
if (mSocket == null) {
throw new RuntimeException("服务器 Socket 初始化失败");
}
}
@Override
public void run() {
try {
while (true) { // 无限循环,进入等待连接状态
System.out.println("等待连接中...");
// 一旦接收到连接请求,构造一个线程来处理
new DeliverThread(mSocket.accept()).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
SimpleHttpServer 继承自 Thread 类,在构造函数中创建一个监听 8000 端口的服务端 Socket,并覆写 Thread 的 run 函数,在该函数中开启无限循环,在该循环中调用 ServerSocket 的 accept() 函数等待客户端的连接,该函数会阻塞,知道有客户端进行连接,接收连接之后会构造一个线程来处理该请求。即 SimpleHttpServer 本身是一个子线程,他再后台等待客户端的连接,一旦接收到连接又会创建一个线程处理该请求,避免阻塞 SimpleHttpServer 线程。
// 请求处理线程
public class DeliverThread extends Thread {
Socket mClientSocket;
// 输入流
BufferedReader mInputStream;
// 输出流
PrintStream mOutputStream;
// 请求方法 GET、POST 等
String httpMethod;
// 子路径
String subPath;
// 分隔符
String boundary;
// 请求参数
Map<String, String> mParams = new HashMap<>();
// 是否已经解析完 Header
boolean isParseHeader = false;
public DeliverThread(Socket socket) {
mClientSocket = socket;
}
@Override
public void run() {
try {
// 获取输入流
mInputStream = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));
// 获取输出流
mOutputStream = new PrintStream(mClientSocket.getOutputStream());
// 解析请求
parseRequest();
// 返回Response
handleResponse();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流和 Socket
IoUtils.closeQuitly(mInputStream);
IoUtils.closeQuitly(mOutputStream);
IoUtils.closeQuitly(mClientSocket);
}
}
// 代码省略
}
DeliverThread 也继承自Thread,在 run 函数中主要封装了如下步骤:
1. 获取客户端 Socket 的输入输出流用于读写数据
2. 解析请求参数
3. 处理、返回请求结果
4. 关闭输入、输出流、客户端 Socket
解析请求的具体实现:
private void parseRequest() {
String line;
try {
int lineNum = 0;
// 从输入流读取数据
while ((line = mInputStream.readLine()) != null) {
// 第一行为请求行
if (lineNum == 0) {
parseRequestLine(line);
}
// 判断是否是数据的结束行
if (isEnd(line)) {
break;
}
// 解析 header 参数
if (lineNum != 0 && !isParseHeader) {
parseHeaders();
}
// 解析请求参数
if (isParseHeader) {
parseRequestParams(line);
}
lineNum++;
}
} catch (IOException e) {
e.printStackTrace();
}
}
parseRequest 函数中,按照数据的分布进行解析。首先解析第一行的请求行数据,即当 lineNum 为 0 时调用parseRequestLine 函数进行解析。实现如下:
// 解析请求行
private void parseRequestLine(String lineOne) {
String[] tempStrings = lineOne.split(" ");
httpMethod = tempStrings[0];
subPath = tempStrings[1];
System.out.println("请求方式:" + tempStrings[0]);
System.out.println("子路径:" + tempStrings[1]);
System.out.println("HTTP 版本:" + tempStrings[2]);
}
请求行后面紧跟着请求 Header,因此接着解析 Header 区域,对应函数 parseHeaders 实现如下:
// 解析 header,参数为每个 header 的字符串
private void parseHeaders(String headerLine) {
// header 区域的结束符
if (headerLine.equals("")) {
isParseHeader = true;
System.out.println("-----------------------> header 解析完成n");
return;
} else if (headerLine.contains("boundary")) {
boundary = parseSecondField(headerLine);
System.out.println("分隔符:" + boundary);
} else {
// 解析普通 header 参数
parseHeaderParam(headerLine);
}
}
每个 header 为一个独立行,格式为参数名: 参数值,还有一种情况是参数名1: 参数值1;参数值2=参数值2,例如下面两个 header:
Content-Length: 1234
Content-Type: multipart/form-data; boundary=SavageLin-YeRenFather
第一个 header 参数名为 Content-Type,值为1234。第二个 header 在同一行内有两个数据,分别为值为 multipart/form-data 的 Content-Type,以及值为 SavageLin-YeRenFather 的 boundary。header 与请求参数之间有一个空行分隔,因此,当检测到 header 数据为空时则认为是 header 参数的结束行。
当一个 header 行数据中含有 boundary 字段是,则调用 parseSecondField 函数解析,实现如下:
// 解析 header 中的第二个参数
private String parseSecondField(String line) {
String[] headerArray = line.split(";");
parseHeaderParam(headerArray[0]);
if (headerArray.length > 1) {
return headerArray[1].split("=")[1];
}
return "";
}
因为 boundary 参数在 header 格式的第二个参数的位置上,因此通过分号进行分割,获取数组第二个位置的数据,也就是 boundary=SavageLin_YeRenFather,然后在进行解析。
普通的 header 则是参数名: 参数值的格式,通过 parseHeaderParam 函数解析,实现如下
// 解析单个 header
private void parseHeaderParam(String headerLine) {
String[] keyValue = headerLine.split(":");
mHeaders.put(keyValue[0].trim(), keyValue[1].trim());
System.out.println("header 参数名:" + keyValue[0].trim() +
",header 参数值:" + keyValue[1].trim());
}
解析完 header 之后开始解析请求参数,对于 POST 和 PUT 请求来说,每个参数格式都是固定的,格式如下:
–boundary 值
header-1: value-1
……
header-n: value-n
空行
参数值
根据上述格式,实现 pareRequestParams 解析函数:
// 解析请求参数
private void parseRequestParams(String paramLine) throws IOException {
if (paramLine.equals("--" + boundary)) {
// 读取 Content-Disposition 行
String ContentDisposition = mInputStream.readLine();
// 解析参数名
String paramName = parseSecondField(ContentDisposition);
// 读取参数 header 与参数值之间的空行
mInputStream.readLine();
// 读取参数值
String paramValue = mInputStream.readLine();
mParams.put(paramName, paramValue);
System.out.println("参数名:" + paramName + ",参数值:" + paramValue);
}
}
至此,整个请求的各个部分均已解析完成,后面要做的就是根据用户的请求返回结果,直接返回一个固定的Response,如下:
// 返回结果
private void handleResponse() {
// 模拟处理耗时
sleep();
// 向输出流写数据
mOutputStream.println("HTTP/1.1 200 OK");
mOutputStream.println("Content-Type: application/json");
mOutputStream.println();
mOutputStream.println("{"stCode":"success"}");
}
private void sleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
在 handleResponse 中,通过Socket的输出流向客户端写入数据,写入的数据也遵循了响应报文的基本格式,如下:
响应行
header 区域
空行
相应数据
客户端写完数据之后,就会关闭输入、输出流以及 Socket,至此,整个请求,响应流程完毕。
3.2 客户端
服务端逻辑分析完成之后再来完成客户端的实现,客户端要做的就是主动向服务器发起 HTTP 请求,他们之间的通信通道是 TCP/IP,因此也是基于 Socket 实现,模拟一个 HTTP POST 请求,如下:
public class HttpPost {
// 请求 URL
public String url;
// 请求参数
private Map<String, String> mParamsMap = new HashMap<>();
// 客户端 Socket
Socket mSocket;
public HttpPost(String url) {
this.url = url;
}
public void addParam(String key, String value) {
mParamsMap.put(key, value);
}
public void execute() {
try {
// 创建 Socket 连接
mSocket = new Socket(this.url, SimpleHttpServer.HTTP_PORT);
PrintStream outputStream = new PrintStream(mSocket.getOutputStream());
BufferedReader inputStream = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
final String boundary = "SavageLin_YeRenFather";
// 写入 header
writeHeader(boundary, outputStream);
// 写入参数
writeParams(boundary, outputStream);
// 等待返回数据
waitResponse(inputStream);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
// 代码省略
}
HttpPost 构造函数中传入请求的 URL 地址,可以通过调用 addParam 函数添加普通的文本参数,当设置好参数之后就可以通过 execute 函数执行该请求。在 execute 函数中客户端首先创建 Socket 连接,目标地址就是用户执行的 URL 以及端口,连接成功之后客户端就可以获取到输入流、输出流,通过输出流客户端可以向服务端发送数据,通过输入流可以获取服务端返回的数据,之后依次写入 header、请求参数、最后等待 Response 的返回.
将 header 固定做出如下设置:
private void writeHeader(String boundary, PrintStream outputStream) {
outputStream.println("POST /api/login/ HTTP/1.1");
outputStream.println("content-length:123");
outputStream.println("Host:" + this.url + ":" + SimpleHttpServer.HTTP_PORT);
outputStream.println("Content-Type:multipart/form-data; boundary=" + boundary);
outputStream.println("User-Agent:android");
outputStream.println();
}
然后将 mParamsMap 中的所有参数通过输出流传递给服务端,代码如下:
private void writeParams(String boundary, PrintStream outputStream) {
Iterator<String> paramsKeySet = mParamsMap.keySet().iterator();
while (paramsKeySet.hasNext()) {
String paramName = paramsKeySet.next();
outputStream.println("--" + boundary);
outputStream.println("Content-Disposition: form-data; name=" + paramName);
outputStream.println();
outputStream.println(mParamsMap.get(paramName));
}
// 结束符
outputStream.println("--" + boundary + "--");
}
每个参数都必须遵循特定的格式,上文服务器解析参数是就是按照这里设定的格式进行的,如下:
–boundary
Content-Disposition: form-data; name=”参数名”
空行
参数值
当参数结束之后需要写一个结束行,格式为:两个横杆加上 boundary 值再加上两个横杆。此时请求数据就已经发送到服务端,只需等待服务器返回数据,在对返回的数据进行处理即可。
private void waitResponse(BufferedReader inputStream) throws IOException {
System.out.println("请求结果:");
String responseLine = inputStream.readLine();
while (responseLine == null || !responseLine.contains("HTTP")) {
responseLine = inputStream.readLine();
}
// 输出 Response
while ((responseLine = inputStream.readLine()) != null) {
System.out.println(responseLine);
}
}
3.3 运行结果
此时,客户端的流程也执行完毕,接着运行这个栗子,首先启动服务器,代码如下:
public static void main(String[] args) {
new SimpleHttpServer().start();
}
服务器启动之后,就会在后台等待客户端发起连接,此时启动客户端,设置参数之后执行一个 Http POST 请求:
public static void main(String[] args) {
HttpPost httpPost = new HttpPost("127.0.0.1");
// 设置两个参数
httpPost.addParam("username", "SavageLin");
httpPost.addParam("pwd", "my_pwd123");
// 执行请求
httpPost.execute();
}
执行结果如下图所示:
最后
以上就是寒冷冷风为你收集整理的HTTP 网络请求原理Android开发进阶(从小工到专家)读书笔记——HTTP 网络请求原理的全部内容,希望文章能够帮你解决HTTP 网络请求原理Android开发进阶(从小工到专家)读书笔记——HTTP 网络请求原理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复