这些数据块以文本形式在客户端、服务器和代理

作者: 编程  发布:2019-11-29

这个http server的实现源代码我放在了我的github上,有兴趣的话可以点击查看哦。

通过本文可以了解编写自己的 HTTP 应用程序所需掌握的大部分内容。具体来说,你会理解下列概念:

HTTP报文

在上一篇文章中,讲述了如何编写一个最简单的server,但该程序只是接受到请求之后马上返回响应,实在不能更简单。在正常的开发中,应该根据不同的请求做出不同的响应。要做到上述的功能,首先要解析客户端发来的请求报文。

  • 报文是如何流动的;
  • HTTP 报文的三个组成部分(起始行、首部和实体的主体部分);
  • 请求和响应报文之间的区别;
  • 请求报文支持的各种功能(方法);
  • 和响应报文一起返回的各种状态码;
  • 各种各样的 HTTP 首部都是用来做什么的。

一、      报文流

HTTP报文是在HTTP应用程序之间发送的数据块,这些数据块以文本形式在客户端、服务器和代理之间流动。报文会像河水一样向下游流动,所有报文的发送者都在接受者的上游。

报文在不同的上下文情景下有不同的理解,本文所说的报文都是在HTTP上下文中描述的名词。

报文流

  • HTTP 报文是在HTTP 应用程序之间发送的数据块。这些数据块以一些文本形式的元信息(meta-information)开头,这些信息描述了报文的内容及含义,后面跟着可选的数据部分。这些报文在客户端、服务器和代理之间流动

  • HTTP 使用术语流入(inbound)和流出(outbound)来描述事务处理(transaction)的方向

  • 不管是请求报文还是响应报文,所有报文都会向下游(downstream)流动

9159.com 1

二、      报文的组成部分

报文是简单的格式化数据块,或包含客户端的请求或服务器的响应。

HTTP报文是什么

在HTTP程序中,报文就是HTTP用来搬运东西的包裹,也可以理解为程序之间传递信息时发送的数据块。这些数据块以一些文本形式的元信息开头,这些信息描述了报文的内容和含义,后面跟着可选的数据部分。

报文的组成部分

HTTP报文的组成部分:对报文进行描述的起始行、包含属性的头部块、可选的,包含数据的主体部分

9159.com 2

HTTP 报文的三个部分

  • 起始行

报文的第一行就是起始行,在请求报文中用来说明要做些什么,在响应报文中说明出现了什么情况。

  • 首部字段

起始行后面有零个或多个首部字段。每个首部字段都包含一个名字和一个值,为了便于解析,两者之间用冒号(:)来分隔。首部以一个空行结束。添加一个首部字段和添加新行一样简单。

  • 主体

空行之后就是可选的报文主体了,其中包含了所有类型的数据。请求主体中包括了要发送给Web 服务器的数据;响应主体中装载了要返回给客户端的数据。起始行和首部都是文本形式且都是结构化的,而主体则不同,主体中可以包含任意的二进制数据(比如图片、视频、音轨、软件程序)。当然,主体中也可以包含文本。

1.      三个组成部分:起始行、首部、主体

HTTP/1.0 200 OK                        起始行             [对报文进行描述]

Content-type: text/plain          首部                  [报文的属性]

Content-length                           首部                  [报文的属性]

                                                        [空行]               [在主体与起始行、首部之间空一行]

This is a msg!                              主体                  [可选的报文数据主体]

 

起始行和首部是以行分割的ASCII文本,每行都以两个字符作为结束(即:回车符+换行符)

报文的主体部分是一个可选的数据块,可以包含文本、二进制数据也可以为空

 

如图:

 

 

报文的流动

HTTP使用属于流入和流出来描述报文的传递方向。HTTP报文会像合水一样流动。不管时请求报文还是响应报文,都会向下游流动,所有报文的发送者都在接受者的上游。下图展示了报文向下游流动的例子。

9159.com 3

报文的语法

所有的HTTP报文都可以分为两类,请求报文和响应报文。请求和响应报文的基本报文结构大致是相同的,只有起始行的语法有所不同。

  • 请求报文
    它会向Web服务器请求一个动作

  • 请求报文的格式:
    起始行: <method> <request-URL> <version>
    头部: <headers>
    主体: <entity-body>

  • 响应报文
    它会将请求的结果返回给客户端。

  • 响应报文的格式:
    起始行: <version> <status> <reason-phrase>
    头部: <headers>
    主体: <entity-body>

下面是对各部分的简要描述:
1、方式(method):客户端希望服务器对资源执行的动作,是一个单独的词,比如,GET、POST或HEAD

2、请求URL(request-URL):要直接与服务器进行对话,只要请求URL是资源的绝对路径就可以了,服务器可以假定自己是URL的主机/端口

3、版本(version):报文所使用的HTTP版本。其格式:HTTP/<主要版本号>.<次要版本号>

4、状态码(status-code):状态码是三位数字,描述了请求过程中所发生的情况。每个状态码的第一位数字都用于描述状态的一般类别(比如,“成功”、“出错”等等)

5、原因短语(reason-phrase):数字状态码的可读版本,包含行终止序列之前的所有文本。原因短语只对人类有意义,因此,尽管响应行HTTP/1.0 200 NOT OK和HTTP/1.0 200 OK中原因短语的含义不同,但同样都会被当作成功指示处理

6、头部(header):可以有零个或多个头部,每个首部都包含一个名字,后面跟着一个冒号(:),然后是一个可选的空格,接着是一个值,最后是一个CRLF首部是由一个空行(CRLF)结束的,表示了头部列表的结束和实体主体部分的开始

7、实体的主体部分(entity-body):实体的主体部分包含一个由任意数据组成的数据块,并不是所有的报文都包含实体的主体部分,有时,报文只是以一个CRLF结束。

展示一些假想的请求和响应报文:

9159.com 4

image.png

2.      报文的语法格式:

请求报文:

<请求方法>  <请求的URL>  <HTTP版本>

<请求首部>

 

<请求的主体部分 >

 

响应报文:

<HTTP版本>  <状态码>  <原因短语 >

<响应首部>

 

<响应的主体部分>

 

简要描述

 

         请求方法:     客户端需要服务器执行的操作,如:GET、HEAD、POST

         请求URL:     请求资源的URL路径,完整的URL

         HTTP版本:   报文所使用的HTTP版本,格式为: HTTP/主版本号.次要版本号,如HTTP/1.1

         原因短语:     状态码的可读版本,如 HTTP/1.0 200 OK,其中“OK”就是原因短语

         首部部分:     一个报文可以有零个或多个首部,每个首部都包含一个名字,后面跟个冒号,然后是一个可选的空格在接着一个值,最后是一个回车换行。如:Server: BWS/1.0

         主体部分:     任意数据组成的数据块,可选。偶尔报文只是以一个回车换行[CRLF]结束

报文的组成

报文由三个部分组成:

  • 对报文进行描述的起始行
  • 包含属性的首部块
  • 可选的、包含数据的主体部分

起始行和首部是由行分隔的ASCII文本。每行都以一个由两个字符(回车符--ASCII码13和换行符--ASCII码10)组成的行终止序列结束。可以写做CRLF

尽管规范说明应该用CRLF来表示行终止,但稳健的应用程序也应该接受单个换行作为行的终止。笔者仅支持以CRLF换行的解析,因为我觉得既然有了规范,那就需要遵循,遵循相同的协议的程序才能互相通信。

实体是一个可选的数据块。与起始行和首部不同的是,主体中可以包含主体或二进制数据,也可以为空(比如仅仅GET一个页面或文件)。

下面来看看报文的语法的格式和规则。

起始行

  • 请求行
    请求报文请求服务器对资源进行一些操作。请求报文的起始行,或称为请求行,包含了一个方法和一个请求URL,这个方法描述了服务器应该执行的操作,请求URL描述了要对哪个资源执行这个方法。请求行中还包含HTTP 的版本,用来告知服务器,客户端使用的是哪种HTTP。所有这些字段都由空格符分隔
  • 响应行
    响应报文承载了状态信息和操作产生的所有结果数据,将其返回给客户端。响应报文的起始行,或称为响应行,包含了响应报文使用的HTTP 版本、数字状态码,以及描述操作状态的文本形式的原因短语。 所有这些字段都由空格符进行分隔。
    例如:HTTP/1.1 200 OK

三、      报文的起始行

所有的HTTP报文都以一个起始行开始,请求报文的起始行说明要做些什么,响应报文的起始行说明发生了什么

 

  • 请求报文请求服务器对资源进行一些操作,其起始行称为请求行,其包含一个方法、一个URL和一个HTTP版本。

比如:GET /123.txt HTTP/1.1 代表请求方法为GET,请求URL为/123.txt ,版本协议为 HTTP/1.1

* *

  • 响应报文承载了状态信息和操作产生的所有结果数据,将其返回给客户端。其起始行成文响应行,包含了响应报文使用的HTTP版本、数字状态码和一个文本形式的原因短语。

比如:HTTP/1.1 200 OK 代表响应报文使用HTTP/1.1协议,状态码为200表示成功,原因短语为OK(表示之前的请求是成功的)

报文的语法

请求报文的语法:

<method> <request-URL> <version>
<headers>

<entity-body>

响应报文的语法:

<version> <status-code> <reason-phrase>
<headers>

<entity-body>

method,方法

客户端希望服务器对资源执行的操作。比如GET、POST

request-URL,请求URL

请求资源,或者URL路径组件的完整URL。

version,版本

报文所使用的HTTP版本。格式:HTTP/.。其中major(主要版本号)和minor(次要版本号)都是整数。

status-code,状态码

描述请求过程所发生的情况的数字。

reason-phrase,原因短语

数字状态码的文字描述版本。

headers,首部

每个首部包含一个名字,后面跟着一个冒号(:),然后是一个可选的空格,接着是一个值,最后是一个CRLF。可以有零个或多个首部。首部由一个CRLF结束,表示首部结束和实体主体开始。

entity-body,实体的主体部分

包含一个由任意数据组成的数据块。可以没有,此时是以一个CRLF结束。

首部

  • 每个HTTP 首部都有一种简单的语法:名字后面跟着冒号(:),然后跟上可选的空格,再跟上字段值,最后是一个CRLF。(或者换行符)
    常见的首部实例

9159.com 5

image.png

  • 首部延续行
    将长的首部行分为多行可以提高可读性,多出来的每行前面至少要有一个空格或制表符(tab)。
    HTTP/1.0 200 OK
    Content-Type: image/gif
    Content-Length: 8572
    Server: Test Server
    Version 1.0
    在这个例子中,响应报文里包含了一个 Server 首部,其值被划分成了多个延续行。
    该首部的完整值为 Test Server Version 1.0。
  • 通用首部
    这些是客户端和服务器都可以使用的通用首部。可以在客户端、服务器和其他应
    用程序之间提供一些非常有用的通用功能。比如,Date 首部就是一个通用首部,
    每一端都可以用它来说明构建报文的时间和日期:
    Date: Tue, 3 Oct 1974 02:16:00 GMT
  • 请求首部
    从名字中就可以看出,请求首部是请求报文特有的。它们为服务器提供了一些额
    外信息,比如客户端希望接收什么类型的数据。例如,下面的Accept 首部就用
    来告知服务器客户端会接受与其请求相符的任意媒体类型:
    Accept: /
    Accept首部

9159.com 6

image.png

安全请求首部

9159.com 7

image.png

  • 响应首部
    响应报文有自己的首部集,以便为客户端提供信息(比如,客户端在与哪种类型
    的服务器进行交互)。例如,下列Server 首部就用来告知客户端它在与一个版
    本1.0 的Tiki-Hut 服务器进行交互:
    Server: Tiki-Hut/1.0
  • 实体首部
    实体首部指的是用于应对实体主体部分的首部。比如,可以用实体首部来说明实
    体主体部分的数据类型。例如,可以通过下列Content-Type 首部告知应用程
    序,数据是以iso-latin-1 字符集表示的HTML 文档:
    Content-Type: text/html; charset=iso-latin-1
    内容首部

9159.com 8

image.png

  • 扩展首部
    扩展首部是非标准的首部,由应用程序开发者创建,但还未添加到已批准的
    HTTP 规范中去。即使不知道这些扩展首部的含义,HTTP 程序也要接受它们并
    对其进行转发。

9159.com,1.      方法

方法

描述

包含报文主体

GET

从服务器获取一份文档

HEAD

只从服务器获取文档的首部

POST

向服务器发送需要处理的数据

PUT

将请求的主体部分存储在服务器上

TRACE

对经过的代理服务器传送到服务器上去的报文进行追踪

OPTIONS

获取在服务器上可以执行哪些方法

DELETE

从服务器上删除一份文档

 

PS:并不是所有的服务器都实现上表中的7种方法,个别WEB服务器可能还会实现自己的请求方法,称为扩展方法。

请求行

请求报文的起始行称为请求行。所有的HTTP报文都以一行起始行作为开始。请求行包含一个方法和一个请求URL以及HTTP的版本三个字段。每个字段都以空格分隔。

比如:GET / HTTP/1.1

请求方法为GET,请求URL为/,HTTP版本为HTTP/1.1。

方法

常见的http方法

9159.com 9

最常接触的方法是HEAD GET POST。
1.GET
通常用于请求服务器发送某个资源。

9159.com 10

2.HEAD
HEAD 方法与GET 方法的行为很类似,但服务器在响应中只返回首部。不会返回实体的主体部分。这就允许客户端在未获取实际资源的情况下,对资源的首部进行检查。

使用HEAD,可以:在不获取资源的情况下
· 了解资源的情况(比如,判断其类型);
· 通过查看响应中的状态码,看看某个对象是否存在;
· 通过查看首部,测试资源是否被修改了。

9159.com 11

3.post
通常用于向服务器提交数据。表单提交常常用到post。

9159.com 12

状态码

9159.com 13

image.png

2.      状态码

整体范围

已定义的范围

分类

100 ~ 199

100 ~ 101

信息提示

200 ~ 299

200 ~ 206

成功

300 ~ 399

300 ~ 305

重定向

400 ~ 499

400 ~ 415

客户端错误

500 ~ 599

500 ~ 505

服务器错误

 

响应行

响应报文的起始行称为响应行。响应行包含HTTP版本、数字状态码以及描述操作状态的文本形式的原因短语。三个字段也是以空格分隔。

比如:HTTP/1.1 200 OK

HTTP版本为HTTP/1.1,数字状态码是200,原因短语是OK。表示请求成功。

四、      报文的首部

HTTP首部字段向请求和响应报文中添加一些附加信息。本质上是 名/值 对的列表。分为通用首部、请求首部、响应首部、实体首部、扩展首部。

 

常见的首部实例:

 

首部实例

描述

Date: Wed, 27 Mar 2013 14:36:55 GMT

服务器产生响应的日期

Content-Length: 48

实体的主体部分包含数据的长度

Content-Type: image/jpeg

实体的主体部分是一个 JPEG图片

Accept: image/gif, image/jpeg, text/html

客户端可接收GIF、JPEG图片及HTML

首部

首部是是包含在请求和响应报文的一些附加信息。本质上,他们只是一些键值对的列表。

比如:Content-Length: 19

表示返回内容长度为19。

五、      报文的主体

HTTP报文的第三部分是可选额实体部分,实体的主体是HTTP报文的负荷,也就是HTTP要传输的内容。可以承载的类型数据:图片、视频、HTML稳定、软件应用程序电子邮件等…

实体的主体部分

简单地说,这部分就是HTTP要传输的内容。

解析请求报文

了解了报文是如何组成和各部分代表的内容之后,就对如何解析请求报文心里有数了。

核心代码

    /* 解析请求行 */
    int parse_start_line(int sockfd, char *recv_buf, req_pack *rp)
    {
        char *p = recv_buf;
        char *ch = p;
        int i = 0;
        enum parts { method, url, ver } req_part = method;
        char *method_str;
        char *url_str;
        char *ver_str;
        int k = 0;

        if (*ch < 'A' || *ch > 'Z') {
            return -1;
        }

        while (*ch != CR) {
            if (*ch != BLANK) {
                k++;
            } else if (req_part == method) {
                method_str = (char *)malloc(k * sizeof(char *));
                memset(method_str, 0, sizeof(char *));
                strncpy(method_str, recv_buf, k);
                k = 0;
                req_part = url;
            } else if (req_part == url) {
                url_str = (char *)malloc(k * sizeof(char *));
                memset(url_str, 0, sizeof(char *));
                strncpy(url_str, recv_buf + strlen(method_str) + 1, k);
                k = 0;
                req_part = ver;
            }
            ch++;
            i++;
        }

        if (req_part == url) {
            if (k != 0) {
                url_str = (char *)malloc(k * sizeof(char));
                memset(url_str, 0, sizeof(char));
                strncpy(url_str, recv_buf + strlen(method_str) + 1, k);
                k = 0;
            } else {
                return -1;
            }
        }

        if (k == 0) {
            ver_str = (char *)malloc(8 * sizeof(char));
            memset(ver_str, 0, sizeof(char));
            strcpy(ver_str, "HTTP/1.1");
        } else {
            ver_str = (char *)malloc(k * sizeof(char));
            memset(ver_str, 0, sizeof(char));
            strncpy(ver_str,
                    recv_buf + strlen(method_str) + strlen(url_str) + 2, k);
        }

        rp->method = method_str;
        rp->url = url_str;
        rp->version = ver_str;

        return (i + 2);
    }

    /* 解析首部字段 */
    int parse_header(int sockfd, char *recv_buf, header headers[])
    {
        char *p = recv_buf;
        char *ch = p;
        int i = 0;
        int k = 0;
        int v = 0;
        int h_i = 0;
        bool is_newline = false;
        char *key_str;
        char *value_str;
        header *tmp_header = (header *)malloc(sizeof(header *));
        memset(tmp_header, 0, sizeof(header));

        while (1) {
            if (*ch == CR && *(ch + 1) == LF) {
                break;
            }
            while (*ch != COLON) {
                ch++;
                i++;
                k++;
            }
            if (*ch == COLON) {
                key_str = (char *)malloc(k * sizeof(char *));
                memset(key_str, 0, sizeof(char *));
                strncpy(key_str, recv_buf + i - k, k);
                k = 0;
                ch++;
                i++;
            }
            while (*ch != CR) {
                ch++;
                i++;
                v++;
            }
            if (*ch == CR) {
                value_str = (char *)malloc(v * sizeof(char *));
                memset(value_str, 0, sizeof(char *));
                strncpy(value_str, recv_buf + i - v, v);
                v = 0;
                i++;
                ch++;
            }
            i++;
            ch++;
            headers[h_i].key = key_str;
            headers[h_i].value = value_str;
            h_i++;
        }

        return (i + 2);
    }

解析思想

遍历recv接受到的请求字符串,检查是否遇到回车符r判断一行数据。

对于起始行,检查是否遇到空格分隔不同的字段;对于首部,检查是否遇到冒号分隔键值对的字段值;对于实体的主体部分,则先判断是否遇到CRLF字符串,然后将剩余内容全部作为实体的主体部分。

返回值是告知程序下一次遍历的起始位置。

如果遇到非法请求行则返回400的响应。

总结

解析报文的过程就是遵循HTTP协议规定的内容去解析报文,获取报文包含的信息。

由于基础知识较薄弱,代码还有很多错误以及很多地方需要优化。如果有看到错误的地方或有其它建议望各位大侠不吝赐教。^_^

这个http server的实现源代码我放在了我的github上,有兴趣的话可以点击查看哦。

原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

如果本文对你有帮助,请点下推荐吧,谢谢^_^

本文由9159.com发布于编程,转载请注明出处:这些数据块以文本形式在客户端、服务器和代理

关键词: