HTTP 网络请求原理

来源:互联网 发布:mac docker vs pd 编辑:程序博客网 时间:2024/05/16 08:48

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 协议客户端与服务器交互流程如下:
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 报文格式
[HTTP 报文格式]

  1. 起始行
    报文的第一行就是起始行,在请求报文中用来说明要以什么方式做什么请求,而在响应报文中粗略说明报文的执行结果。

  2. 首部字段
    起始行后面有零个或多个首部字段。每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(Connection:keep-Alive)来分隔。首部以一个空格结束。

  3. 主体
    首部字段的空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了发送给 WEB 服务器的数据;响应主体中装载了要返回给客户端的数据。起始行和首部都是结构化的文本形式,而主体则可以包含任意的二进制数据(如图片、视频、音频、软件程序),也可以包含文本形式
    GET 和 DELETE 方法的功能是获取和删除,因此只需将 URL 构造为要处理的资源即可,即所有的参数附加在资源的 URL 最后,第一个参数前通过 “?” 符号连接,然后请求参数按照“参数名=参数值”的形式进行追加,每个参数之间用 “&” 连接。GET 和 DELETE 的 URL 最长长度为 1024 字节,即 1KB
    在浏览器中输入 http://www.devtf.cn/?p=909,得到的请求报文如下:

    GET /?p=909 HTTP/1.1Host: www.devtf.cnCache-Control: no-cache

    第一行为请求行,代表请求方式是 GET,自路径为/?p=909,HTTP 版本为 1.1。后两行是请求的 HEADER 区域。
    PUT 和POST 的报文格式一般是表单形式,即这两个请求方式的参数存储在报文的请求数据(报文主体)的位置上:

    POST /api/feed/ HTTP/1.1Accept-Encoding: gzipContent-Length: 225873Content-Type: multipart/form-data; boundary=SavageLin-YeRenFatherHost: www.myhost.comConnection: Keep-Alive--SavageLin-YeRenFatherContent-Disposition: from-data; name="username"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bitSavage.Lin--SavageLin-YeRenFatherContent-Disposition: from-data; name="images"; filename="/storage/emulated/0/Camera/jdimage/lxh0e3yyfmpr2e36tdowbavrx.jpg"Content-Type: application/octet-streamContent-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-8Content-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 OKData: Sat, 31 Dec 2005 23:59:59 GMTContent-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 协议
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();    }

执行结果如下图所示:


请求结果

0 0
原创粉丝点击