30756 字
154 分钟
Tiny Webserver
2023-10-01

http连接处理#

接下来的部分就是对http连接的处理了,在TinyWebServer项目中,http请求的处理与响应至关重要,关系到用户界面的跳转与反馈。

下面首先对==http处理流程==进行简要介绍,后面再具体介绍http类的定义和服务器接收http请求的具体过程。

拆解03-HTTP连接处理流程#

1.http报文处理流程#

HTTP的处理流程主要包括请求的发送和响应的接收两个阶段。下面是HTTP的处理流程:

  1. 建立连接:客户端通过==TCP/IP==协议与服务器建立连接。这通常是通过==三次握手==来完成的,建立==可靠的通信连接==。
  2. 发送请求:客户端构造HTTP请求报文,包括请求行、请求头和请求体等信息。常见的请求方法有GET、POST、PUT、DELETE等。请求报文被==封装==成==TCP报文==并发送给服务器。
  3. 服务器处理请求:服务器接收到客户端发送的==请求报文==后,开始处理请求。服务器首先解析请求报文,提取请求方法、请求路径和请求头等信息。然后根据请求路径和服务器的路由规则,确定要执行的处理程序。
  4. 处理请求:服务器执行处理程序,==根据请求的内容进行相应的处理==,如==读取数据库==、==生成动态内容==等。服务器可能会与其他服务器、数据库或文件系统进行交互,以获取所需的数据。
  5. 生成响应:服务器根据处理结果构造==HTTP响应报文==,包括状态行、响应头和响应体。状态行指定了响应的状态码和状态信息,响应头包含了响应的元数据信息,响应体包含了实际的响应内容。
  6. 发送响应:服务器将构造好的HTTP响应报文封装成TCP报文发送给客户端。客户端接收到响应报文后进行解析。
  7. 解析响应:客户端解析响应报文,提取状态行、响应头和响应体的内容。客户端根据响应的状态码和响应内容进行相应的处理,如显示网页内容、保存文件等。
  8. 关闭连接:客户端和服务器在完成请求和响应后,可以选择断开连接,==释放网络资源==。可以通过==关闭TCP连接==或==使用持久连接(Keep-Alive)==来实现。

附加内容#

Q:http连接的事务是什么?

A:

在HTTP连接中,事务(Transaction)指的是一次完整的请求-响应过程,它涉及到一系列相互关联的操作,下面是事务的几个主要步骤:

  1. 建立连接:首先,客户端通过TCP/IP协议与服务器建立连接。这是HTTP事务的开始。
  2. 发送请求:客户端向服务器发送一个HTTP请求。请求中包含了请求方法(如GET、POST、PUT等)、URL(统一资源定位符)和相关的请求头(如Accept、Content-Type等)。此外,如果有必要,客户端还可以发送请求体(例如,表单数据、JSON数据等)。
  3. 服务器处理请求:服务器接收到客户端发送的请求后,根据请求的内容进行处理。这可能涉及验证身份、检索数据、执行业务逻辑等操作。
  4. 服务器生成响应:服务器根据请求的处理结果生成一个HTTP响应。响应中包括了响应状态码(如200 OK、404 Not Found等)、响应头(如Content-Type、Set-Cookie等)以及响应体(例如,HTML页面、JSON数据等)。
  5. 发送响应:服务器将生成的HTTP响应发送回客户端。
  6. 客户端接收响应:客户端接收到服务器发送的响应。
  7. 处理响应:客户端根据接收到的响应进行处理。这包括解析响应头和响应体,以获取所需的数据。
  8. 关闭连接:HTTP事务结束后,客户端和服务器关闭连接,释放相关的资源。

HTTP事务是一次完整的请求和响应过程,其中客户端向服务器发送请求,服务器对请求进行处理并生成响应,然后将响应发送回客户端。事务的完成需要满足请求和响应之间的关联性,以确保通信的一致性和可靠性。

在上面http报文处理流程中,涉及到了一些基础知识,这里对这些知识进行介绍

请求报文#

请求报文包含请求行、请求头、请求体,==请求头和请求体之间要加一个空行==。

图中,请求行包含的应该不是请求地址URL,看别人文章中这里应该包含的是请求路径URI,这里存疑?

Q:http连接中,服务器处理接收到的请求报文时,请求路径与请求地址是相同的吗?

A:

==请求路径是请求地址的一部分==

  • 请求路径(Request-URI):只是URL中去除协议、主机名和端口号后的部分,表示服务器上的==具体资源路径==。

  • 请求地址(Request URL):是客户端在请求中指定的==完整URL==,包括==协议==、==主机名==、==端口号==和==路径等==。

举一个例子,对于下面的部分请求报文:

GET /example.html HTTP/1.1
Host: www.example.com
......
  • 请求路径为请求行中的/example.html
  • 请求地址是请求行中的请求路径、HTTP协议版本和请求头中的域名、端口号组合在一起的http://www.example.com/example.html,这才是一个完整的请求地址

Q:我可以理解为URL是在浏览器与服务器建立连接前就存在的,而请求报文是在建立连接后才生成的吗?

A:

是的,您的理解基本正确。URL通常是在构建客户端请求时就已经存在,而请求报文是在客户端与服务器建立连接后生成的。下面我会更详细地解释。

当用户在浏览器中输入URL并按下回车键时,浏览器会先解析该URL,确定要访问的资源的位置和名称,然后尝试与该资源所在的服务器建立连接。在建立连接前,浏览器和服务器之间还没有交换任何数据,此时只有URL存在。

一旦浏览器成功建立了与服务器的TCP连接,就可以开始发送HTTP请求。请求包括一个报文头和(可选的)报文体两个部分。报文头包含许多元数据,例如请求方法、请求URI等。在这个过程中,当前的URL被转换为请求报文中的URI,请求报文则是根据请求方法和其他相关信息生成的。因此,请求报文是在客户端与服务器建立连接后才生成的。

当服务器收到请求后,会根据请求中的URI和其他信息来寻找并返回对应的资源。如果服务器无法找到对应的资源,将会返回一个错误响应。

需要注意的是,对于浏览器的某些预处理请求(如预加载和预连接),可能会在浏览器请求资源之前就生成请求报文。但对于普通的HTTP请求,请求报文是在浏览器与服务器建立连接后才生成的。

超详细的HTTP协议请求报文、响应报文教程!这篇文章中发现了一个比较好的例子:

POST请求报文:

---- 请求行 ----
POST /xmweb?host=mail.itcast.cn&_t=1542884567319 HTTP/1.1 # POST请求方式 请求资源路径 HTTP协议版本
---- 请求头 ----
Host: mail.itcast.cn # 服务器的主机地址和端口号,默认是80
Connection: keep-alive # 和服务端保持长连接
Content-Type: application/x-www-form-urlencoded # 告诉服务端请求的数据类型
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 # 客户端的名称
---- 空行 ----
---- 请求体 ----
username=hello&pass=hello # 请求参数

POST原始报文:

POST /xmweb?host=mail.itcast.cn&_t=1542884567319 HTTP/1.1\r\n
Host: mail.itcast.cn\r\n
Connection: keep-alive\r\n
Content-Type: application/x-www-form-urlencoded\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36\r\n
\r\n
username=hello&pass=hello

这个请求报文包含三个部分,每个部分的具体内容如下:

  • 请求行(Request Line):请求行包含了HTTP请求的==请求方法==、==请求路径==和==HTTP协议版本==。在上面的例子中,请求行为POST /xmweb?host=mail.itcast.cn&_t=1542884567319 HTTP/1.1,表示使用POST方法请求/xmweb?host=mail.itcast.cn&_t=1542884567319路径,并使用HTTP/1.1协议。

  • 请求头(Request Headers):用来提供关于请求的附加信息,请求头以==键值对==的形式组织,每个键值对之间使用==换行符(回车换行\r\n)==分隔。以下是常见的一些请求头字段及其作用:

    • Host:指定了服务器的域名(主机名)和端口号,端口号默认是80,在上面的例子中,Host字段为Host: mail.itcast.cn。当没有域名时,域名也可以替换为IP地址,例如Host: 192.0.2.1

    • User-Agent:指定了发送请求的用户代理(浏览器、客户端等)的信息。在上面的例子中,User-Agent字段为User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36

    • Connection:指定了是否保持连接的方式。在上面的例子中,Connection字段为Connection: keep-alive,表示保持长连接。

  • 请求体(Request Body):请求体是可选的,用于传递客户端向服务器发送的数据。一般==使用GET方法的请求报文中通常没有请求体==。

注意,在请求头和请求体之间要有一个==空行==,这个空行实际上是一个==空行字符(CRLF)==,即回车符和换行符的组合(==\r\n==)

请求头#
  • HOST,给出请求资源所在服务器的域名。
  • User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中==自动发送==等。
  • Accept,说明用户代理可处理的媒体类型。
  • Accept-Encoding,说明用户代理支持的内容编码。
  • Accept-Language,说明用户代理能够处理的自然语言集。
  • Content-Type,说明实现主体的媒体类型。
  • Content-Length,说明实现主体的大小。
  • Connection,连接管理,可以是Keep-Alive或close。
请求方法#

请求方法主要有四种,其中开发中用的最多的是get和post,另外还有put和delete。

  • get用于获取资源,没有Body,参数放在URL中

  • post用于添加资源,发送的内容写在Body里面

  • put用于修改资源,发送的内容写在Body里面

  • delete用于删除资源,没有Body

【注意】:4种不同的请求方式是为了分清楚不同请求的目的,但是并不代表用了post就一定要修改数据,用get就不能修改资源

Q:4种不同的请求方式是为了分清楚不同请求的目的,但是并不代表用了post就一定要修改数据,用get就不能修改资源,这个理解是正确的吗?

A:

是的,您的理解是正确的。HTTP中的不同请求方法(GET、POST、PUT、DELETE等)用于在客户端和服务器之间传递不同类型的请求,并且并不意味着使用POST就一定要修改数据,或者使用GET就不能修改资源。

虽然常规的约定是使用GET请求来获取资源,而使用POST请求来提交或修改数据,但这只是一种约定,并没有强制规定。实际上,根据HTTP规范,不同的请求方法可以执行多种操作。

例如,虽然GET请求通常用于获取资源,并且应该是安全和幂等的(即不应该对服务器状态进行更改),但在某些情况下,GET请求也可以修改资源。而POST请求通常用于提交数据,但在某些情况下,也可以用于获取数据或执行其他操作。

重要的是,使用这些请求方法时要遵循HTTP协议和相关的最佳实践,确保请求与服务器的预期行为相一致,并避免造成混淆或安全问题。

因此,尽管通常约定使用不同的请求方法来分别处理不同的请求目的,但并不绝对限制了它们的用法。具体使用哪种请求方法,取决于实际场景和服务器的支持。

其中GET,PUT,DELETE都是幂等的,POST不是幂等的

==幂等==:一个操作,操作一次或者操作多次,结果是一样的

比较官方一点的解释如下:

  • GET:用于请求服务器发送指定资源。GET 方法是幂等的,也就是说多个相同的 GET 请求不会对服务器产生影响。该方法将请求参数放在 URL 的查询字符串中,通常没有请求体。
  • POST:用于向服务器提交数据,通常用于向服务器发送表单数据或上传文件等。POST 方法不是幂等的,多次发送相同的 POST 请求会对服务器产生不同的结果。该方法将请求参数放在请求体中。
  • PUT:用于向服务器上传新的资源或更新现有资源。PUT 方法是幂等的,多次发送相同的 PUT 请求会具有相同的效果。该方法将请求参数放在请求体中。
  • DELETE:用于请求服务器删除指定的资源。DELETE 方法是幂等的,多次发送相同的 DELETE 请求会对服务器产生相同的结果。
附加内容#

这篇文章非常详细的介绍了HTTP的请求报文

干货 |超详细HTTP协议请求报文归纳总结

Q:GET方法获取请求时,他的参数具体放在哪里?

A:

在使用GET方法进行请求时,参数通常会放在URL的查询字符串中。查询字符串是URL中问号(?)后面的部分,它由一个或多个键值对组成,每个键值对之间使用 ampersand(&)分隔。

例如,假设我们有以下URL:

https://www.example.com/api/resource?id=123&name=John&age=25

上面的URL中,idnameage是参数的键,而123John25则是相应参数的值。这些参数可以通过在URL中指定来传递给服务器,服务器可以通过解析URL中的查询字符串来获取这些参数的值。

需要注意的是,由于GET请求将参数放在URL中,因此它们对于用户来说是可见的。敏感信息(如密码)不应该作为GET请求的参数直接暴露在URL中,而应该使用POST等其他方法来传递。

整理了【Http协议、get和post请求】应该是全网最全了,收藏起来慢慢看这篇文章中看到了一个比较有意思的内容:

POST 方法会产生两个TCP 数据包,浏览器会先将请求头发送给服务器,待服务器响应100 continue,浏览器再发送请求数据,服务器响应200 ok(返回数据)。这么看起来 GET 请求的传输会比 POST 快上一些(因为GET 方法只发送一个 TCP 数据包),但是实际上在网络良好的情况下它们的传输速度基本相同。

误区四:“GET产生一个TCP数据包;POST产生两个TCP数据包。”

这一点理解起来还是有一定难度的,实际上,不论哪一种浏览器,在发送 POST 的时候都没有带Expect 头,server 也自然不会发 100continue。通过抓包发现,尽管会分两次,body 就是紧随在header 后面发送的,根本不存在『等待服务器响应』这一说。

从另一个角度说,TCP 是传输层协议。别人问你应用层协议里的 GET 和POST 有啥区别,GET和POST还有一个重大区别,简单的说:GET产生一个TCP数据包;POST产生两个TCP数据包。长的说:对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要跑一趟就送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一东西来,你们打开门迎接我”,然后再回头把东西送过去。

  • GET与POST都有自己的语义,不能随便混用。
  • 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
  • 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次

Q:POST的请求信息是没有长度限制的吗?

A:

在HTTP协议中,没有对POST请求的消息主体长度做出具体的限制。HTTP规范并没有规定POST请求的消息主体(请求体)的最大长度限制。

然而,实际上,在实际应用中,POST请求的消息主体大小可能会受到一些因素的限制:

  1. 服务器配置:服务器可能会对请求的消息主体大小进行限制,以保护自身免受恶意或过大的请求的影响。这通常可以在服务器的配置文件中进行设置。

  2. 网络传输限制:尽管HTTP协议本身没有对请求消息主体大小进行限制,但在网络层面上,存在可能会对请求大小进行限制的因素,例如网络设备的MTU(最大传输单元)。

要确保在发送POST请求时不会遇到消息主体大小的限制,可以参考以下建议:

  1. 在设计应用程序时,合理控制请求消息主体的大小,避免发送过大的请求体。

  2. 如果需要传输大量数据,可以考虑将数据进行分片或采用其他方式进行批量处理,以避免单个请求消息主体太大。

  3. 了解服务器的配置和限制,并适当调整服务器的配置参数,以适应较大的请求消息主体。

总之,HTTP协议本身并没有对POST请求消息主体的长度做出具体限制,但实际应用中可能存在服务器配置和网络传输的限制。因此,在设计和发送POST请求时,需要合理控制请求的消息主体大小,以避免潜在的问题。

Q:GET请求长度受到哪些方面限制?

A:

GET请求的长度限制主要受到以下因素的影响:

  1. 浏览器限制:大多数浏览器对GET请求的URL有长度限制。虽然没有明确的标准规定URL的最大长度,但通常限制在2,048个字符左右。超过这个限制的URL可能会被截断或被浏览器拒绝。
  2. 服务器限制:服务器也可以对接收的URL长度进行限制。服务器软件、配置和处理机制可能对URL长度设置了具体的限制。一些服务器会对URL长度进行限制,以保护服务器免受恶意或过大的请求的影响。
  3. 网络传输限制:在网络层面上,可能存在会对GET请求的URL长度进行限制的因素,例如网络设备的MTU(最大传输单元)。如果URL长度超过MTU,那么可能需要进行分片传输或其他处理。

需要注意的是,GET请求将参数直接附加在URL中,而POST请求将参数放在请求体中。因此,GET请求的URL长度限制要严格得多。

为了避免超出URL长度限制,可以考虑以下建议:

  1. 尽量避免在GET请求中传递大量数据,特别是敏感数据,因为URL可能会被保存在浏览器历史记录、日志文件或其他地方。
  2. 如果需要传递较大的数据,可以考虑使用POST请求,将参数放在请求体中。
  3. 如果必须使用GET请求,并且URL长度超过浏览器和服务器的限制,可以考虑分片传输数据或使用其他方式进行处理。

总之,在使用GET请求时需要注意URL长度的限制,尽量控制URL的长度,并根据实际情况选择适当的请求方法和数据传输方式。

网络-3 【HTTP报文、请求方式、GET与POST】这篇文章中看到了对连接的介绍:

1.串行连接 处理完一次连接断开一次重新TCP连接

2.持久化连接:(connection)连接不会关闭

3.管道持久化连接(把所有请求放到发送队列里,不等响应,一个一个发送请求的同时接收相应的响应)

这篇文章里提到POST不能管道化传输。

Q:POST不能进行管道化传输?

A:

是的,POST请求不能进行管道化传输。

在HTTP/1.1规范中,管道化(Pipelining)是一种优化技术,可以在不等待响应的情况下,连续发送多个HTTP请求到服务器,并在后续接收响应。这样可以减少请求和响应之间的延迟时间。

然而,虽然HTTP/1.1规范允许对GET请求进行管道化,但对于POST请求,规范明确说明:不管是请求还是响应中含有实体,都禁止进行管道化。

这是因为POST请求通常会包含请求体(request body),而请求体中可能包含客户端要发送的重要数据,如表单数据、JSON数据等。由于请求体的长度和内容可能不同,服务器可能需要在接收完整的请求体之后,才能进行处理和生成响应。如果进行管道化传输,服务器无法准确知道每个请求的边界和完成时间,从而导致混乱和错误。

因此,为了避免数据错乱和不一致性,POST请求在HTTP/1.1中被禁止进行管道化传输。只有GET请求可以安全地进行管道化传输,以提高性能和效率。

响应报文#

响应报文封装成TCP报文#

客户端解析响应报文#

这里的请求报文和响应报文应该放到解析请求报文和生成响应报文的代码部分#

拆解04-http连接与处理的http_conn.h#

这里定义了各种变量和函数,后面的http_conn.cpp中编写代码时代码量较大,记不住变量和函数干嘛的可以参考一下这个文档

#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <map>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
#include "../timer/lst_timer.h"
#include "../log/log.h"
class http_conn {
public:
// 设置读取文件的名称 m_real_file 的长度
static const int FILENAME_LEN = 200;
// 设置读缓冲区 m_read_buf 大小
static const int READ_BUFFER_SIZE = 2048;
// 设置写缓冲区 m_write_buf 大小
static const int WRITE_BUFFER_SIZE = 1024;
// 报文的请求方法,本项目只用到 GET 和 POST
enum METHOD {
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
// 主状态机的状态
enum CHECK_STATE {
CHECK_STATE_REQUESTLINE = 0,
CHECK_STATE_HEADER,
CHECK_STATE_CONTENT
};
// 请求报文解析后的处理结果
enum HTTP_CODE {
NO_REQUEST, // 请求不完整,需要继续读取请求报文数据
GET_REQUEST, // 获得了完整的HTTP请求
BAD_REQUEST, // HTTP请求报文有语法错误
NO_RESOURCE,
FORBIDDEN_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR, // 服务器内部错误
CLOSED_CONNECTION
};
// 从状态机的状态
enum LINE_STATUS {
LINE_OK=0,
LINE_BAD,
LINE_OPEN
};
public:
http_conn() {}
~http_conn() {}
public:
// 初始化套接字地址,函数内部会调用私有方法 init
// ???别的参数干嘛的
void init(int sockfd, const sockaddr_in& addr, char*, int, int, string user, string passwd, string sqlname);
// 关闭 http 连接
void close_conn(bool real_close = true);
// ?????
void process();
// 读取浏览器端发来的全部数据
bool read_once();
// 相应报文写入函数
bool write();
// ????????获取??地址的函数
sockaddr_in* get_address() {
return &m_address;
}
// 同步线程初始化数据库读取表
void initmysql_result(connection_pool* connPool);
// ????????
int timer_flag;
// ???????????
int improv;
private:
// 初始化方法
void init();
// 从读缓冲区 m_read_buf 读取,并处理请求报文
HTTP_CODE process_read();
// 向写缓冲区 m_write_buf 写入响应报文数据
bool process_write(HTTP_CODE ret);
// 主状态机解析报文中的请求行数据
HTTP_CODE parse_request_line(char* text);
// 主状态机解析报文中的请求头数据
HTTP_CODE parse_headers(char* text);
// 主状态机解析报文中的请求体数据
HTTP_CODE parse_content(char* text);
// 生成响应报文
HTTP_CODE do_request();
// m_start_line 是已解析的字符
// get_line 用于将指针向后偏移,指向未处理的字符
char* get_line() {
return m_read_buf + m_start_line;
};
// 从状态机读取一行,分析是请求报文的哪一部分
LINE_STATUS parse_line();
void unmap();
// 根据响应报文格式,生成对应 8 个部分,以下函数均由 do_request 调用
// ?????这个参数真的可以?????
bool add_response(const char* format, ...);
bool add_content(const char* content);
bool add_status_line(int status, const char* title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();
public:
static int m_epollfd;
static int m_user_count;
MYSQL* mysql;
int m_state; // 读为 0,写为 1
private:
int m_sockfd;
sockaddr_in m_address;
// 存储读取的请求报文数据
char m_read_buf[READ_BUFFER_SIZE];
// m_read_buf 中读入数据的最后一个字节的下一个位置
int m_read_idx;
// m_read_buf 读取的位置 m_checked_idx
int m_checked_idx;
// m_read_buf 中已经解析的字符个数
int m_start_line;
// 存储发出的响应报文数据
char m_write_buf[WRITE_BUFFER_SIZE];
// 指示 buffer 中的长度
int m_write_idx;
// 主状态机的状态
CHECK_STATE m_check_state;
// 请求方法
METHOD m_method;
// 以下为解析请求报文中对应的 6 个变量
// 存储读取文件的名称
char m_read_file[FILENAME_LEN];
char* m_url;
char* m_version;
char* m_host;
int m_content_length;
bool m_linger;
char* m_file_address; // 读取服务器上的文件地址
struct stat m_file_stat;
struct iovec m_iv[2]; // io 向量机制 iovec
int m_iv_count;
int cgi; // 是否启用的 POST
char* m_string; // 存储请求头数据
int bytes_to_send; // 剩余发送字节数
int bytes_have_send; // 已发送字节数
char* doc_root;
map<string, string> m_users;
int m_TRIGMode;
int m_close_log;
char sql_user[100];
char sql_passwd[100];
char sql_name[100];
};
#endif

拆解05-http_conn.cpp需要的头文件#

头文件#

http_conn.cpp源文件中用到的头文件

#include "http_conn.h" // 这是我们写好的头文件
#include <mysql/mysql.h> // MySQL C/C++ 客户端库的头文件
#include <fstream> // C++ 标准库中的文件输入输出相关操作的头文件

mysql/mysql.h#

是 MySQL C/C++ 客户端库的头文件,它包含了用于编写与 MySQL 数据库进行交互的函数和类型声明。

mysql/mysql.h提供的常用功能如下:

  • 连接管理:声明和定义了 MySQL 相关的数据结构和函数,提供了建立连接、断开连接等方法

    • mysql_init():初始化一个MySQL结构体,用于存储MySQL连接的信息
    • mysql_real_connect():根据指定的连接参数,与 MySQL 数据库建立连接
    • mysql_close():关闭MySQL数据库连接
  • 执行sql语句:可以向MySQL 数据库发送 SQL 查询,但是不直接返回查询结果,返回值为 0 表示执行成功,返回非零值表示执行出错;要获取查询结果集,还需要使用其他函数,例如 mysql_store_result()mysql_fetch_row()

    • mysql_query():执行一条 SQL 查询语句

    • mysql_real_query():执行一条 SQL 查询语句,与 mysql_query() 类似,但可以指定要执行的 SQL 语句的长度

  • 结果集处理:提供了处理查询结果集的函数和类型声明,如MYSQL_RES结构

    • mysql_store_result():获取查询结果集, 当查询为 SELECT 语句时使用
    • mysql_fetch_row():返回结果集中的下一行数据
  • 错误处理:定义了处理 MySQL 错误的函数和类型,如MYSQL 结构

    • mysql_error():获取上一次操作产生的错误信息。
    • mysql_errno():获取上一次操作产生的错误号。

fstream#

<fstream> 是 C++ 标准库中的文件输入输出流相关操作的头文件。它定义了用于文件读写和操作的类和函数。

具体而言,fstream 头文件定义了以下三个类:

  1. std::ifstream:用于从文件中读取数据的输入流类。可以打开文件并从中读取数据。
  2. std::ofstream:用于向文件中写入数据的输出流类。可以打开文件并将数据写入文件中。
  3. std::fstream:同时具有读取和写入功能的文件流类。可以打开文件并进行读取和写入操作。

关于fstream如何使用,参考下面的代码:

#include <fstream>
#include <iostream>
#include <string>
int main() {
// 使用 std::ofstream 类创建一个输出文件流对象 outputFile
/*
注意,这里本应该调用 open() 函数打开 "output.txt" 文件,但是我们没有显式的
去调用 open() 函数就进行文件的操作,直接调用了其默认的打开方式,因为在 stream
类的构造函数中调用了 open() 函数,并拥有同样的构造函数,所以在这里可以直接使用
流对象进行文件的操作
*/
std::ofstream outputFile("output.txt");
// 通过 is_open() 函数判断文件是否打开
if (outputFile.is_open()) {
outputFile << "Hello, World!" << std::endl;
outputFile.close(); // 关闭文件流
}
// 使用 std::ifstream 类创建一个输入文件流对象 inputFile
// 这里也本应该使用 open() 函数打开文件的,与上面同理
std::ifstream inputFile("input.txt");
// 通过 is_open() 函数判断文件是否打开
if (inputFile.is_open()) {
std::string line;
while (std::getline(inputFile, line)) {
std::cout << line << std::endl;
}
inputFile.close(); // 关闭文件流
}
// 需要注意的是,关闭文件流是一个良好的编程习惯,能确保资源得到正确释放
return 0;
}

拆解06-建立连接并初始化#

在http连接的过程中,浏览器与服务器首先会通过三次握手建立TCP连接,建立连接后,双方才能进行数据传输。

在这个TinyWebServer中,http_conn.cpp文件中带参的init()函数

  • init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname)

就是在双方建立连接后初始化连接,为后面数据传输做准备的

该函数 init() 是在浏览器与服务器建立 TCP 连接后被调用的,用于初始化该连接的一些属性和状态,为后续的数据传输和处理做准备。

在建立 TCP 连接后,需要进行一些初始化工作以确保连接的正确运行。这些初始化工作可能涉及到设置套接字选项、绑定地址、添加到 epoll 实例中等操作。此外,还可能需要设置一些连接相关的参数,如网站根目录、数据库信息等。

通过调用 init() 函数,可以将这些初始化操作集中在一个地方进行,并且在建立连接后立即执行。这样可以提高代码的可读性和可维护性,并为后续的数据传输和处理提供一个合适的环境。

建立HTTP连接后服务器处理流程#

下面是对建立HTTP连接后服务器处理流程的详细描述:

  1. 服务器在建立HTTP连接后会对该连接进行初始化,包括设置相关参数和数据结构。
  2. 服务器会将该连接的==套接字文件描述符==注册到内核事件表中,并开启EPOLLONESHOT选项。EPOLLONESHOT选项表示==一次只处理一个事件==,==避免多个线程同时处理同一个连接==。
  3. 服务器将套接字的文件描述符设置为非阻塞模式。非阻塞模式使得服务器可以在没有数据可读的情况下立即返回,而不必等待数据到达。
  4. 服务器通过监听套接字,等待客户端发送的请求报文。
  5. 当有数据到达套接字缓冲区时,服务器的工作线程从套接字中读取请求报文。
  6. 工作线程根据HTTP协议规范对请求报文进行解析,包括解析请求行、请求头和请求体等部分。
  7. 工作线程根据具体业务逻辑处理请求报文,并生成相应的响应结果。
  8. 工作线程将生成的响应结果写入套接字缓冲区。
  9. 服务器通过套接字发送响应结果给客户端。
  10. 如果还有其他请求需要处理,服务器重复步骤5-9;否则关闭连接,并进行资源释放。

需要注意的是,具体的实现方式可能因服务器框架、编程语言或网络库的不同而有所不同。上述描述仅为一种常见的处理流程。在实际情况中,服务器可能会采用多线程、多进程或异步IO等技术来提高并发性能和响应能力。此外,服务器还可能会对连接进行超时处理、错误处理和异常处理等。

有参init()源代码#

//初始化连接,外部调用初始化套接字地址
/*
init()函数的参数:
sockfd:套接字文件描述符
addr:套接字地址结构体
root:网站根目录
TRIGMode:触发模式
close_log:是否关闭日志
user:数据库用户名
passwd:数据库密码
sqlname:数据库名
*/
void http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode,
int close_log, string user, string passwd, string sqlname)
{
// 将套接字文件描述符和套接字地址存到该 http 连接对象的成员变量中
m_sockfd = sockfd;
m_address = addr;
// 调用 addfd() 函数,将套接字文件描述符添加到指定的 epoll 实例中,
// 以便进行事件监听,具体介绍在epoll相关代码部分
addfd(m_epollfd, sockfd, true, m_TRIGMode);
// 增加用户数统计
m_user_count++;
//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
// 设置网站根目录路径
doc_root = root;
// 保存传入的触发模式和关闭日志的选项值
m_TRIGMode = TRIGMode;
m_close_log = close_log;
// 将传入的数据库用户名、密码、数据库名拷贝到相应的成员变量中
strcpy(sql_user, user.c_str());
strcpy(sql_passwd, passwd.c_str());
strcpy(sql_name, sqlname.c_str());
// 调用无参的 init() 函数进行进一步的初始化
init();
}

无参init()源代码#

//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = GET;
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
cgi = 0;
m_state = 0;
timer_flag = 0;
improv = 0;
memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}

epoll相关代码#

在浏览器与服务器建立连接后,服务器会调用init()函数对连接进行初始化,在init()函数中,调用了用于向epoll实例注册文件描述符的函数addfd(),通过该函数可以将指定的文件描述符添加到指定的epoll实例中,并设置对应的事件类型。

项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种,其中内核事件表注册事件就是这个addfd()函数

  • 非阻塞模式
  • 内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启
  • 内核事件表删除事件
  • 重置EPOLLONESHOT事件

1.非阻塞模式setnonblocking()#

在addfd()函数中调用了setnonblocking()函数对套接字文件描述符设置非阻塞

setnonblocking()源代码#
//对套接字文件描述符设置非阻塞
int setnonblocking(int fd)
{
/*
获取当前套接字文件描述符 fd 的文件状态标志
old_option 即为 fd 的套接字文件描述符标志
*/
int old_option = fcntl(fd, F_GETFL);
/*
通过位或运算符 | 将 O_NONBLOCK 标志与当前套接字文件描述符标志
old_option 进行逻辑或操作,生成一个新的套接字文件描述符标志
new_option,这个新的标志是非阻塞模式
*/
int new_option = old_option | O_NONBLOCK;
/*
将新的标志 new_option 设置到套接字文件描述符 fd 上,将套接字
设置成非阻塞模式
*/
fcntl(fd, F_SETFL, new_option);
// 将设置前的套接字文件描述符标志返回
return old_option;
}
从套接字中非阻塞式读取数据#

要从套接字中使用非阻塞模式读取数据,可以按照以下步骤进行操作:

  1. 设置套接字为非阻塞模式:

    • 首先,通过使用 fcntl() 函数或 ioctl() 函数将套接字的文件描述符设置为非阻塞模式。例如:

      #include <sys/socket.h>
      #include <fcntl.h>
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      // 设置套接字为非阻塞模式
      int flags = fcntl(sockfd, F_GETFL, 0);
      fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
  2. 使用 recv() 函数读取数据:

    • 使用 recv() 函数从套接字中尝试读取数据。如果没有立即可用的数据,则 recv() 函数会返回 -1,并将错误号设置为 EWOULDBLOCKEAGAIN

    • 可以使用 errno 来检查错误码,并根据需要重试或处理错误。例如:

      #include <unistd.h>
      #include <errno.h>
      char buffer[1024];
      ssize_t bytesRead;
      while (true) {
      bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);
      if (bytesRead == -1) {
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
      // 没有可用数据,稍后再次尝试读取
      usleep(1000); // 等待一段时间后再次尝试读取
      } else {
      // 其他读取错误
      break;
      }
      } else if (bytesRead == 0) {
      // 对方已关闭连接
      break;
      } else {
      // 处理读取到的数据
      write(1, buffer, bytesRead);
      }
      }
  3. 根据实际需求进行循环读取或使用事件驱动的方式进行读取:

    • 如果需要持续从套接字中读取数据,可以在一个循环中反复调用 recv() 函数。在每次调用之前,可以使用 selectpoll 等函数检查套接字是否准备好读取,避免阻塞之前的操作。
    • 另一种方法是使用事件驱动的方式,如使用 selectepoll 等函数监听套接字的可读事件,并在有数据可读时进行处理。

请注意,以上示例代码仅为演示如何在非阻塞模式下从套接字中读取数据,并不包含完整的错误处理和循环读取机制。实际应用中,可能需要根据具体情况进行更复杂的逻辑处理和错误处理。

套接字文件描述符与套接字文件描述符标志#

套接字文件描述符(Socket File Descriptor)是一个整数值,用于在操作系统中==唯一标识一个打开的套接字==。每当创建一个新的套接字时,操作系统会分配一个文件描述符来表示该套接字。

套接字文件描述符标志(Socket File Descriptor Flags)是一个==用于控制套接字行为和属性的位标志集合==。通过设置不同的标志,可以改变套接字的==工作模式==、==阻塞方式==、==错误处理==和==传输属性==等。

在Linux和类Unix系统上,可以使用 fcntl() 函数获取和修改套接字文件描述符标志。常见的套接字文件描述符标志包括:

  • O_NONBLOCK:非阻塞模式。当套接字处于非阻塞模式时,相关的I/O操作(如读取或写入数据)不会被阻塞,而是立即返回。如果没有足够的数据可读取或无法立即写入,相应的读取或写入操作可能会返回一个错误或特定的返回码。

  • O_CLOEXEC:在 exec 函数调用时关闭套接字。这个标志用于确保在执行新程序时,套接字文件描述符被正确关闭,防止在子进程中继承无用的文件描述符。

  • O_ASYNC:启用异步通知。当套接字有可读或可写的数据时,会通过信号或I/O事件通知应用程序。

  • O_REUSEADDR:允许地址重用。当套接字处于 TIME_WAIT 状态时,可以立即重新绑定相同的地址和端口。

  • O_KEEPALIVE:启用保活机制。当套接字空闲一段时间后,会自动发送心跳包以保持连接的活性。

这些标志可以通过逻辑或运算符将多个标志组合在一起,并应用到套接字文件描述符上,以改变套接字的行为。使用 fcntl() 函数的 F_GETFL 参数可以获取当前的套接字文件描述符标志,F_SETFL 参数则用于设置新的套接字文件描述符标志。

总而言之,套接字文件描述符用于唯一标识打开的套接字,而套接字文件描述符标志用于控制套接字的工作模式和属性。它们是操作系统中与套接字操作和设置相关的关键概念。

fcntl()将文件描述符设置成非阻塞#

fcntl() 函数是一个 POSIX 标准库函数,用于对文件描述符进行各种控制操作,其中之一就是将文件描述符设置为非阻塞模式。在网络编程中,我们可以使用 fcntl() 函数将套接字设置为非阻塞模式,以实现非阻塞式的 I/O 操作。

fcntl() 函数的原型如下:

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
  • fd 是要进行操作的文件描述符,例如套接字的文件描述符。
  • cmd 是要执行的命令,其中之一是 F_SETFL,用于设置文件状态标志。
  • arg 是可选参数,用于指定命令的附加参数。

下面是将套接字设置为非阻塞模式的示例代码:

#include <sys/socket.h>
#include <fcntl.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 获取当前文件描述符的标志
int flags = fcntl(sockfd, F_GETFL, 0);
// 设置文件描述符为非阻塞模式
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

在上述示例中,我们首先使用 fcntl(sockfd, F_GETFL, 0) 获取套接字当前的文件描述符标志。然后,通过按位或运算符 |O_NONBLOCK 标志添加到当前的标志中,表示将套接字设置为非阻塞模式。最后,我们使用 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) 将新的标志设置回套接字。

将套接字设置为非阻塞模式后,对于非阻塞的 I/O 操作,recv()send()accept() 等函数将立即返回,并不会阻塞等待数据或连接。当没有立即可用的数据或连接时,这些函数可能会返回一个特定的错误码(如 EWOULDBLOCKEAGAIN),需要根据情况进行处理。

需要注意的是,在设置非阻塞模式后,可能需要在适当的时候重试 I/O 操作,因为并不保证一次调用就能完成所有操作。此外,==非阻塞模式==的套接字需要通过==循环==等方式来实现==持续==的数据读取或写入。

2.内核事件表注册新事件#

用于向epoll实例注册文件描述符的函数addfd(),通过该函数可以将指定的文件描述符添加到指定的epoll实例中,并设置对应的事件类型。

Q:内核事件表和epoll实例有什么关系?

A:内核事件表(Kernel Event Table)是操作系统内核中用于管理和跟踪各种事件的数据结构。它负责记录所有在系统中发生的事件,例如网络连接、IO操作、定时器等。内核事件表是一个高效的数据结构,可以在事件发生时快速通知应用程序。

而epoll是Linux环境下一种基于事件驱动的IO多路复用机制,是对内核事件表的一种封装和利用。它通过与内核事件表交互,实现了高效的事件监听和响应能力。具体来说,epoll使用内核提供的系统调用(如epoll_create、epoll_ctl、epoll_wait)来注册、更新和等待事件,以及返回已发生的事件。==应用程序通过epoll接口与内核事件表进行交互==,从而实现对事件的管理和处理。

简而言之,epoll是基于内核事件表的一种机制,通过它可以注册、等待和处理事件。内核事件表是在操作系统内核中维护的数据结构,记录所有发生的事件。epoll利用了内核事件表提供的功能,使得应用程序可以高效地监听和处理事件。

以下是对代码的解释和注释:

void addfd(int epollfd, int fd, bool one_shot)
{
epoll_event event;
event.data.fd = fd;
#ifdef ET
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; // 使用边缘触发模式
#endif
#ifdef LT
event.events = EPOLLIN | EPOLLRDHUP; // 使用水平触发模式
#endif
if (one_shot)
event.events |= EPOLLONESHOT; // 设置EPOLLONESHOT事件
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); // 将文件描述符添加到epoll实例中
setnonblocking(fd); // 设置文件描述符为非阻塞模式
}

此函数主要用于将文件描述符注册到epoll实例中,以监听相应的事件。解释如下:

  • 首先,创建一个epoll_event结构体对象event,并设置其data.fd成员为待添加的文件描述符。
  • 根据预定义的宏(ET或LT),对event.events设置不同的值。EPOLLIN表示可读事件,EPOLLET表示边缘触发模式,EPOLLRDHUP表示对端关闭连接的事件。根据不同的宏定义,可以选择使用边缘触发模式或水平触发模式。
  • 如果one_shot参数为真,将event.events的值加上EPOLLONESHOT,表示只监听一次事件。
  • 使用epoll_ctl()函数将文件描述符添加到epoll实例中,参数epollfd是epoll实例的文件描述符,fd是要添加的文件描述符,&event给出要添加的事件类型。
  • 最后,使用setnonblocking()函数将文件描述符设置为非阻塞模式,以便进行异步操作。

这段代码主要用于通过epoll实现高效的I/O复用,以监听指定文件描述符的特定事件,并可以选择设置边缘触发模式或水平触发模式。

3.内核事件表删除事件#

这段代码是用于从epoll实例中删除文件描述符的函数removefd(),通过该函数可以将指定的文件描述符从指定的epoll实例中移除。

以下是对代码的解释和注释:

void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0); // 从epoll实例中删除文件描述符
close(fd); // 关闭文件描述符
}

此函数主要用于从epoll实例中移除文件描述符,并关闭相应的文件描述符。解释如下:

  • 使用epoll_ctl()函数,设置参数epollfd为epoll实例的文件描述符,EPOLL_CTL_DEL表示删除操作,fd为要删除的文件描述符,最后一个参数为忽略的参数,设置为0即可。
  • 调用close()函数关闭文件描述符,使其不再可用。

这段代码的作用是将指定的文件描述符从epoll实例中移除,并关闭对应的文件描述符。通常在不再需要监听该文件描述符的事件时调用该函数。

4.重置EPOLLONESHOT事件#

这段代码是用于修改epoll实例中文件描述符的事件类型的函数modfd(),通过该函数可以修改指定文件描述符在epoll实例中已注册的事件类型。

以下是对代码的解释和注释:

void modfd(int epollfd, int fd, int ev)
{
epoll_event event;
event.data.fd = fd;
#ifdef ET
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP; // 使用边缘触发模式
#endif
#ifdef LT
event.events = ev | EPOLLONESHOT | EPOLLRDHUP; // 使用水平触发模式
#endif
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event); // 修改文件描述符的事件类型
}

此函数主要用于修改已经在epoll实例中注册的文件描述符的事件类型。解释如下:

  • 首先,创建一个epoll_event结构体对象event,并设置其data.fd成员为待添加的文件描述符。
  • 根据预定义的宏(ET或LT),对event.events设置不同的值。如果使用边缘触发模式,需要将事件类型设置为ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP,即当前事件类型加上边缘触发模式、ONESHOT模式、以及对端关闭连接的事件类型;如果使用水平触发模式,需要将事件类型设置为ev | EPOLLONESHOT | EPOLLRDHUP,即当前事件类型加上ONESHOT模式和对端关闭连接的事件类型。
  • 使用epoll_ctl()函数修改文件描述符在epoll实例中的事件类型,参数epollfd是epoll实例的文件描述符,fd是要修改的文件描述符,&event给出相应的新的事件类型。

这段代码的作用是修改epoll实例中已经注册的文件描述符的事件类型。通常在需要修改文件描述符感兴趣的事件类型时调用该函数。

拆解07-read_once()从套接字中读取请求报文存到读缓冲区#

上面介绍了,在建立http连接后,服务器对该连接进行初始化,然后在内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,并将该连接的套接字的文件描述符设置非阻塞模式。完成了上面的操作后,才从套接字中读取请求报文

read_once()读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buf中,并更新m_read_idx

这两个变量的数据类型如下:

  • char m_read_buf[READ_BUFFER_SIZE]:字符数组,用于存储读取的请求报文数据;
  • int m_read_idx:整形,是m_read_buffer中数据的最后一个字节的下一个位置

read_once()代码#

// 循环读取客户数据,直到无数据可读或对方关闭连接
// 非阻塞 ET 工作模式下,需要一次性将数据读完
bool http_conn::read_once() {
// 读缓冲区大小 READ_BUFFER_SIZE 不够大,直接返回 false
if (m_read_idx >= READ_BUFFER_SIZE) {
return false;
}
// 初始化读取字节数为 0
int bytes_read = 0;
// LT 读取数据,持续通知,有可读时内核会持续通知应用程序直到数据被处理完
if (0 == m_TRIGMode) {
/*
recv() 函数:
参数:
使用 recv() 函数从套接字文件描述符 m_sockfd 对应套接字的缓冲区
中读取数据;
并将这些数据存储到读缓冲区 m_read_buf + m_read_idx
以后,在 m_read_idx 以前已经有数据了,读缓冲区的数据结构是数组;
第三个参数指的是读缓冲区原长度减掉已经存储的长度,因为 m_read_idx
是读缓冲区读取数据的最后一个字节的下一个位置;
第四个参数置为 0, 说明接收操作设置为默认;
返回值:
成功收到数据,返回读取到的字节数;
连接已关闭,返回 0;
出现错误,返回 -1,并将错误码存到 errno 里
附加解释:
recv() 函数为阻塞函数,没有数据接收时,它会一直等待数据到达,直到
超时或出现错误;但是不代表recv()不能非阻塞式读取
*/
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read; // 更新读缓冲区数据末尾的下标
// 如果连接已关闭(0)或发生错误(-1),返回 false
if (bytes_read <= 0) {
return false;
}
return true;
}
// ET 读取数据,边缘触发,只在状态变化时通知应用程序,
// 如果应用程序没有读完所有数据,不会重复通知,所以要一次性读完
else {
while (true) {
// 与上面一样,使用阻塞函数 recv() 读取数据
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
// 如果发生错误
if (bytes_read == -1) {
// 没有可用数据,说明已经处理完所有数据了,退出循环
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
// 不是EAGAIN和EWOULDBLOCK的话,发生别的错误,返回 false
return false;
}
else if (bytes_read == 0) { // 为 0, 连接已关闭,返回 false
return false;
}
// 没发生错误也没关闭连接,正确的读取了数据,更新读缓冲区结尾下标
m_read_idx += bytes_read;
}
return true;
}
}

recv()从套接字读取数据#

recv()函数是一个套接字API提供的函数,用于在TCP连接中从套接字接收数据。下面是对recv()函数的介绍:

函数原型:

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:表示需要接收数据的==套接字文件描述符==。
  • buf:指向一个接收缓冲区,用于存储接收到的数据。
  • len:表示接收缓冲区的长度,即buf指向的内存区域的大小。
  • flags:可选参数,可以用来控制接收操作的行为,如设置为0表示默认行为。

返回值:

  • 如果成功接收到数据,返回接收到的字节数。
  • 如果==连接已关闭==,返回==0==。
  • 如果出现==错误==,返回==-1==,并将错误码存储在errno中。

函数功能:

  • recv()函数用于从套接字接收数据。它会==阻塞等待==直到==接收到至少一个字节的数据==,或者==发生了错误==。
  • 接收到的数据会被存储在提供的接收缓冲区中,并使用buf参数传递给调用者。
  • 函数会==返回实际接收到的字节数==(通常与请求接收的字节数相等),但==可能会少于请求的字节数==。
  • 当接收到的字节数为0时,表示连接已关闭。
  • 通过设置flags参数,可以控制接收操作的行为,如设置标志位MSG_WAITALL可以保证一次性接收到请求的字节数。

需要注意的是,recv()函数是一个==阻塞函数==,==当没有数据可接收时,它会一直等待数据到达,直到超时或出现错误==。而当套接字为非阻塞模式时,在调用recv()函数读取数据时,如果没有数据可用,该函数将==立即返回==并返回一个错误码EAGAIN或EWOULDBLOCK,不会阻塞等待数据的到达。

LT模式与ET模式#

在从套接字(socket)中读取数据时,LT(Level-Triggered)模式和ET(Edge-Triggered)模式是两种不同的事件触发方式。

  1. LT(Level-Triggered)模式:

    • 当套接字的可读事件就绪时(即有数据可读),如果采用LT模式,应用程序会立即得到通知,然后可以进行读取操作。
    • 在LT模式中,如果应用程序没有将所有可用的数据读取完毕,下次==仍然会收到读取事件通知==,直到==把缓冲区中的数据读取完全==才会停止通知。
    • 即使应用程序没有及时处理读取事件,==内核仍然会继续通知==应用程序直到数据被处理完为止。
    • 相对简单,适合处理非阻塞I/O操作
  2. ET(Edge-Triggered)模式:

    • 当套接字的可读事件就绪时,如果采用ET模式,应用程序只会在==状态变化时得到一次通知==。也就是说,当缓冲区从无可读变为有可读时,应用程序会收到一次读取事件通知。
    • ==如果应用程序没有读取完所有的数据,即使缓冲区中还有可读数据,内核也不会再次通知应用程序==。直到缓冲区的状态从有可读变为无可读,然后==再从无可读变为有可读时==,应用程序才会收到新的读取事件通知。
    • 需要更加精确的管理读取和处理的状态,适合处理==高性能==的网络应用

总结:

  • LT模式是一种==持续通知==的模式,无论应用程序是否及时处理读取事件,都会持续通知直到数据被处理完。
  • ET模式是一种==边缘触发==的模式,只在==状态变化==时通知应用程序,如果应用程序没有读取完所有的数据,不会重复通知。

选择LT和ET模式主要取决于应用程序的设计和需求。LT模式相对简单,适合处理非阻塞的 I/O 操作;而ET模式则需要更加精确的管理读取和处理的状态,适合处理高性能的网络应用。

错误号的EAGAIN与EWOULDBLOCK#

在错误号中,“EAGAIN” 和 “EWOULDBLOCK” 都表示非阻塞 I/O 操作的错误或状态码。

  1. EAGAIN:

    • “EAGAIN” 是一个宏定义,表示 “Try again”(再试一次)。
    • 当进行非阻塞的读取或写入操作时,如果没有立即可用的数据或空间,系统会返回 “EAGAIN” 错误。
    • 这种情况下,应用程序可以稍后再次尝试相同的操作。
  2. EWOULDBLOCK:

    • “EWOULDBLOCK” 同样是一个宏定义,表示 “Operation would block”(操作将会阻塞)。
    • 与 “EAGAIN” 类似,当进行非阻塞的读取或写入操作时,如果没有立即可用的数据或空间,系统会返回 “EWOULDBLOCK” 错误。
    • 这种情况下,应用程序可以稍后再次尝试相同的操作。

这两个错误码通常在非阻塞 I/O 操作过程中出现,用于指示==当前操作无法立即完成==,但应用程序可以稍后再次尝试。在实际编程中,通常需要检查这两个错误码,并根据具体情况进行相应的处理,比如等待、重试或以其他方式处理与 I/O 相关的操作。

附加内容#

Q:http连接中,客户端与服务器建立TCP连接后,服务器会为该连接创建一个套接字,那么服务器有哪些方式从这个套接字中读取请求报文的数据呢

A:

在HTTP连接中,服务器有以下几种方式从套接字中读取请求报文的数据:

  • 阻塞式读取(Blocking I/O):服务器可以使用阻塞式的套接字读取操作,调用套接字API提供的读取函数(如recv()),在没有数据到达之前会一直阻塞等待,直到==获取到完整的请求报文==或者==达到超时时间==。
  • 非阻塞式读取(Non-blocking I/O):服务器可以通过调用套接字API中的相关函数(如==fcntl()==或ioctl())来==将套接字设置为非阻塞模式==。非阻塞模式下,读取操作会立即返回,即使没有数据到达。服务器可以通过轮询套接字状态,判断是否有数据可读,然后进行读取操作。
  • 多路复用(Multiplexing):服务器可以使用多路复用技术,例如select、poll或epoll,来同时监听多个套接字的状态。通过将套接字添加到事件驱动的I/O复用机制中,服务器可以在有数据到达时立即得到通知,并进行相应的读取操作。
  • 异步IO(Asynchronous I/O):服务器可以使用异步IO模型,如Proactor模式或使用异步网络库等。在异步IO模型中,服务器发起读取操作后,不会阻塞等待数据的到达,而是注册一个回调函数并继续处理其他任务。当数据到达时,服务器会通过回调函数处理已读取的数据。

以上是常见的几种方式,服务器可以根据具体的需求和应用场景选择适合的方式来从套接字中读取请求报文的数据。需要注意的是,在高并发环境下,采用非阻塞式、多路复用或异步IO等技术能够更有效地处理多个客户端连接。

Q:套接字文件描述符有什么用?

A:

套接字文件描述符是操作系统为了标识和管理网络连接而提供的一种机制。在大多数操作系统中,包括Unix/Linux和Windows,套接字被视为一种特殊的文件类型,通过文件描述符来引用。

套接字文件描述符是一个==非负整数==,它在应用程序中用于==标识和引用套接字==。当应用程序创建一个套接字时,操作系统会分配一个==唯一==的文件描述符给该套接字,并通过该文件描述符来==访问==和==操作==套接字。

数据实际上并不存储在套接字文件描述符指向的地方。套接字文件描述符只是作为一个标识符或句柄,用于应用程序与操作系统内核之间进行通信和交互。实际上,数据存储在操作系统内核维护的==套接字缓冲区==中。

当应用程序使用套接字进行数据传输时,可以通过套接字文件描述符进行读取和写入操作。通过合适的系统调用(如send()recv()write()read()等),应用程序可以使用套接字文件描述符向套接字缓冲区中写入数据或从套接字缓冲区中读取数据。

拆解-处理请求报文#

在http连接过程中,想以非阻塞方式从套接字中读取数据请求报文,我的理解是在建立tcp连接初始化连接后,先将套接字设置为非阻塞再从套接字中读取请求报文,我的理解对吗?

是的,你的理解是正确的。在进行 HTTP 连接的非阻塞读取时,可以按照以下步骤进行:

  1. 建立 TCP 连接:首先,使用套接字函数(如 socketconnect)建立 TCP 连接。在连接建立之后,该套接字将变为可读。

  2. 设置套接字为非阻塞模式:通过调用 fcntl 函数或使用 O_NONBLOCK 标志来将套接字设置为非阻塞模式。这意味着从该套接字读取数据时不会阻塞线程,而是立即返回。

  3. 非阻塞读取请求报文:使用非阻塞的方式从套接字中读取请求报文。可以使用 read 函数或其他适用于你所使用的编程语言的相应函数。

需要注意的是,在非阻塞模式下,从套接字读取数据时可能会出现以下情况:

  • 如果套接字中没有数据可读,则读取操作会立即返回,并且返回值为 0。这表示当前暂时没有数据可用。

  • 如果套接字中有部分数据可读,但不足以构成完整的请求报文,则读取操作会尽可能读取可用的数据,并返回已读取的字节数。需要在后续的读取操作中考虑处理剩余的数据。

  • 如果套接字出现错误,例如连接断开或出现其他错误,则读取操作会返回一个负值,表示读取操作失败。

因此,在非阻塞模式下,需要根据返回值来判断读取操作的状态,并做出相应的处理。

总结起来,你的理解是正确的:在建立 TCP 连接之后,将套接字设置为非阻塞模式,并使用非阻塞方式从中读取请求报文。这样可以避免线程在读取操作时被阻塞,提高程序的并发性能。

epoll相关知识点

主线程创建http对象接收请求,不是在http_conn.cpp里?这在哪?

⚪使用read_once()将所有数据读入对应buffer

?将该对象插入任务队列

?工作线程从任务队列中取出一个任务进行处理

猜测没完成这几个都在别的文件里,等遇到再看

⚪LT持续通知与ET边缘触发的源码理解一下写到read_once()那,感觉源码在.cpp里,那个的思路与原来的不同,应该不是处理报文的,处理别的的后面再看

拆解08-process()工作线程处理任务的函数#

浏览器端发出http连接请求,服务器端主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列后,工作线程从任务队列中取出一个任务进行处理。

在以上过程中,各工作线程通过process函数对任务进行处理,调用process_read函数和process_write函数分别完成报文解析与报文响应两个任务。

以下是对给定代码的解释和注释:

void http_conn::process()
{
// http请求报文的处理结果
HTTP_CODE read_ret = process_read();
// 如果读取结果为 NO_REQUEST(没有请求),则将套接字的事件改为只读,继续监听下次读事件
if (read_ret == NO_REQUEST)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
return;
}
// 处理写入请求的结果
bool write_ret = process_write(read_ret);
// 如果写入结果为 false(写入失败),则关闭连接
if (!write_ret)
{
close_conn();
}
// 将套接字的事件改为只写,继续监听下次写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}

该代码是一个处理 HTTP 请求的函数 process(),用来处理读取请求报文和写入响应报文的操作。下面是对代码中各部分的详细解释和注释:

  • HTTP_CODE read_ret = process_read();:调用 process_read() 函数,获取读取请求报文的结果。

  • if (read_ret == NO_REQUEST):如果读取结果为 NO_REQUEST,即没有完整的请求报文被读取,则将套接字的事件改为只读,并继续监听下次读事件。这是为了保证下次可以继续读取请求报文。

  • bool write_ret = process_write(read_ret);:调用 process_write() 函数,处理写入响应报文的操作,并将读取请求报文的结果作为参数传递。

  • if (!write_ret):如果写入响应报文的结果为 false,即写入失败,则关闭连接,因为无法正常响应客户端的请求。

  • modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);:将套接字的事件改为只写,这是为了监听下次写事件。当有完整的响应报文需要写入时,会触发该写事件。

总体来说,该函数处理了读取请求报文和写入响应报文的操作,并根据处理结果调整套接字的事件。它保证了在有请求需要处理时,持续监听读事件;在有响应需要发送时,监听写事件。同时,它也处理了读取和写入失败的情况,关闭了连接以确保正常的通信流程。

HTTP_CODE含义#

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析时只涉及到四种。

  • NO_REQUEST

    • 请求不完整,需要继续读取请求报文数据
  • GET_REQUEST

    • 获得了完整的HTTP请求
  • BAD_REQUEST

    • HTTP请求报文有语法错误
  • INTERNAL_ERROR

    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

process_read()从读缓冲区中读取并解析请求报文#

解析报文整体流程#

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。

  • 判断条件

    • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体

    • 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部

    • 两者为或关系,当条件为真则继续循环,否则退出

  • 循环体

    • 从状态机读取数据

    • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text

    • 主状态机解析text

上面的process()函数中调用了process_read()函数,下面就介绍一下这个函数,以下是对代码的注释:

//m_start_line是行在buffer中的起始位置,将该位置后面的数据赋给text
//此时从状态机已提前将一行的末尾字符\r\n变为\0\0,所以text可以直接取出完整的行进行解析
char* get_line(){
return m_read_buf+m_start_line;
}
http_conn::HTTP_CODE http_conn::process_read()
{
// 初始化从状态机状态、HTTP请求的解析结果
LINE_STATUS line_status = LINE_OK; // 完整读取一行
HTTP_CODE ret = NO_REQUEST; // 请求不完整,需要继续读取请求报文
char *text = 0;
// 这里写的两个判断条件在主状态机逻辑中会讲解
// 循环读取并解析请求,parse_line() 为从状态机的具体实现
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
{
text = get_line(); // 获取当前行的文本
// m_start_line 是每一个数据行在 m_read_buf 中的起始位置
// m_checked_idx 表示从状态机在 m_read_buf 中读取的位置
m_start_line = m_checked_idx;
LOG_INFO("%s", text); // 打印日志,记录请求内容
// 根据当前检查状态和解析的文本进行不同的处理
// 主状态机的三种状态转移逻辑:请求行,请求头,请求体
switch (m_check_state)
{
case CHECK_STATE_REQUESTLINE:
{
// 解析请求行函数,返回值为解析状态 HTTP_CODE
ret = parse_request_line(text);
// 请求行语法错误,返回该解析状态
if (ret == BAD_REQUEST)
return BAD_REQUEST;
break;
}
case CHECK_STATE_HEADER:
{
// 解析请求头函数,返回值为解析状态 HTTP_CODE
ret = parse_headers(text);
// 请求头语法错误,返回该解析状态
if (ret == BAD_REQUEST)
return BAD_REQUEST;
// 完整解析 GET 请求后,跳转到报文响应函数 do_request()
else if (ret == GET_REQUEST)
{
return do_request();
}
break;
}
case CHECK_STATE_CONTENT:
{
// 解析请求体
ret = parse_content(text);
// 完整解析 POST 请求后,跳转到报文响应函数 do_request()
if (ret == GET_REQUEST)
return do_request();
// 解析完请求体就解析完请求报文了,更新 line_status,避免再次进入循环
line_status = LINE_OPEN; // 读取的行不完整,当 line_status 为完整读取一行 LINE_OK 时才进入循环
break;
}
default:
return INTERNAL_ERROR; // 未知检查状态
}
}
return NO_REQUEST;
}

该函数通过循环读取和解析HTTP请求的每一行,根据解析状态和当前行的内容进行不同的处理:

  1. 定义line_status变量,表示当前行解析的状态,默认为LINE_OK
  2. 定义ret变量,表示HTTP请求处理的结果,默认为NO_REQUEST
  3. 定义text指针,用于指向当前行的文本。
  4. 循环读取并解析请求,条件满足以下两个条件之一时继续循环:
    • 检查状态为CHECK_STATE_CONTENT且当前行解析状态为LINE_OK,用于处理POST请求的请求体。
    • 调用parse_line()函数解析当前行的状态为LINE_OK,即成功解析一行数据。
  5. 在每次循环中,获取当前行的文本,并更新m_start_line变量为下一行的起始位置。
  6. 打印日志,记录当前行的文本内容。
  7. 根据当前检查状态和解析的文本进行不同的处理:
    • 如果是CHECK_STATE_REQUESTLINE,则调用parse_request_line()函数解析请求行。
    • 如果是CHECK_STATE_HEADER,则调用parse_headers()函数解析请求头。
      • 如果返回结果为BAD_REQUEST,表示请求格式有误,直接返回BAD_REQUEST
      • 如果返回结果为GET_REQUEST,表示是GET请求,直接处理请求并返回处理结果。
    • 如果是CHECK_STATE_CONTENT,则调用parse_content()函数解析请求内容(通常用于处理POST请求)。
      • 如果返回结果为GET_REQUEST,表示是GET请求,直接处理请求并返回处理结果。
      • 设置line_statusLINE_OPEN,用于继续解析下一行的内容。
  8. 如果在上述处理过程中出现了未知的检查状态,则直接返回INTERNAL_ERROR
  9. 在所有行解析完成后,返回NO_REQUEST表明还需要继续读取请求。

总体来说,该函数按顺序解析请求的请求行、请求头和请求内容,并根据解析结果进行相应处理,最终返回需要进行的下一步操作。

do_request()报文响应函数#

该代码是一个HTTP请求处理函数的实现。以下是对代码的注释:

http_conn::HTTP_CODE http_conn::do_request()
{
// 将文件根目录复制到m_real_file中
strcpy(m_real_file, doc_root);
int len = strlen(doc_root);
const char *p = strrchr(m_url, '/'); // 找到最后一个斜杠的位置
// 处理cgi请求
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
{
// 根据标志判断是登录检测还是注册检测
char flag = m_url[1];
// 分配足够大的空间来存储真实的URL
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2);
strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
free(m_url_real);
// 提取用户名和密码
// user = 123 && passwd = 123
char name[100], password[100];
int i;
for (i = 5; m_string[i] != '&'; ++i)
name[i - 5] = m_string[i];
name[i - 5] = '\0';
int j = 0;
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
password[j] = m_string[i];
password[j] = '\0';
if (*(p + 1) == '3')
{
// 如果是注册,先检测数据库中是否有重名的
// 没有重名的,进行增加数据
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");
if (users.find(name) == users.end())
{
m_lock.lock();
int res = mysql_query(mysql, sql_insert);
users.insert(pair<string, string>(name, password));
m_lock.unlock();
if (!res)
strcpy(m_url, "/log.html");
else
strcpy(m_url, "/registerError.html");
}
else
strcpy(m_url, "/registerError.html");
}
// 如果是登录,直接判断
// 若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
else if (*(p + 1) == '2')
{
// 如果是登录
if (users.find(name) != users.end() && users[name] == password)
strcpy(m_url, "/welcome.html"); // 登录成功
else
strcpy(m_url, "/logError.html"); // 登录失败
}
}
// 处理其他请求
if (*(p + 1) == '0')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/register.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '1')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '5')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/picture.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '6')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/video.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else if (*(p + 1) == '7')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/fans.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
free(m_url_real);
}
else
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
// 检查文件状态
if (stat(m_real_file, &m_file_stat) < 0)
return NO_RESOURCE;
if (!(m_file_stat.st_mode & S_IROTH))
return FORBIDDEN_REQUEST;
if (S_ISDIR(m_file_stat.st_mode))
return BAD_REQUEST;
// 打开文件并将其映射到内存
int fd = open(m_real_file, O_RDONLY);
m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
return FILE_REQUEST;
}

这段代码是一个HTTP请求处理函数,根据请求的URL和CGI参数进行不同的处理。主要流程如下:

  1. 将文件根目录复制到m_real_file中。
  2. 找到URL最后一个斜杠的位置。
  3. 如果是需要进行CGI处理的请求(cgi==1),根据标志位判断是登录检测还是注册检测。
    • 如果是注册检测,先提取用户名和密码,然后检查数据库中是否已存在该用户名,若不存在则插入新记录,否则返回注册错误页面。
    • 如果是登录检测,提取用户名和密码,然后在数据库中查找相应记录,并比较密码是否正确,若正确则返回欢迎页面,否则返回登录错误页面。
  4. 根据URL的不同,在m_real_file后面拼接相应的文件路径。
  5. 检查文件状态,包括是否存在、是否可读等。
  6. 如果是目录,则返回错误的请求类型。
  7. 打开文件并将其映射到内存,准备进行读取。

根据处理的结果,将对应的枚举值返回给调用者。

拆解09-主从状态机处理报文#

主状态机负责全局控制和处理报文首部,而子状态机则负责处理特定请求方法下的报文内容。通过灵活的状态转移,可以有效地解析和处理不同类型的请求报文。

流程图部分,描述主、从状态机调用关系与状态转移过程。

代码部分,结合代码对http请求报文的解析进行详解。

流程图#

从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

主状态机#

三种状态,标识解析位置。

  • CHECK_STATE_REQUESTLINE,解析请求行
  • CHECK_STATE_HEADER,解析请求头
  • CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机#

三种状态,标识解析一行的读取状态。

  • LINE_OK,完整读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完整

parse_line()从状态机#

在HTTP报文中,每一行的数据由==\r\n==作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。

从状态机负责读取buffer中的数据,将每行数据末尾的\r\n==置为\0\0==,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。

  • 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r

    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
    • 接下来达到了buffer末尾,表示buffer还需要继续接收,返回LINE_OPEN
    • 否则,表示语法错误,返回LINE_BAD
  • 当前字节不是\r,判断是否是\n(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况

    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
  • 当前字节既不是\r,也不是\n

    • 表示接收不完整,需要继续接收,返回LINE_OPEN

parse_line()代码#

这段代码是一个从状态机,用于解析出一行内容。下面对每行代码进行注释说明:

http_conn::LINE_STATUS http_conn::parse_line()
{
char temp;
/*
m_read_idx:读缓冲区 m_read_buf 中读入数据最后一个字节的下一个位置
m_checked_idx:读缓冲区中读取的位置
*/
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
// 获取当前字符
temp = m_read_buf[m_checked_idx];
// 如果当前字符为回车符,有三种情况
if (temp == '\r')
{
// 到达了buffer末尾,表示还需要继续接收,返回 LINE_OPEN
if ((m_checked_idx + 1) == m_read_idx) // 如果回车符是最后一个字符,表示需要继续读取数据,则返回LINE_OPEN
return LINE_OPEN;
// 如果回车符后面紧跟着换行符,证明是行尾,将\r\n修改成\0\0并将 m_checkec_idx 指向下一行开头
else if (m_read_buf[m_checked_idx + 1] == '\n')
{
m_read_buf[m_checked_idx++] = '\0'; // 将回车符置为字符串结束符'\0'
m_read_buf[m_checked_idx++] = '\0'; // 将换行符置为字符串结束符'\0'
return LINE_OK; // 返回LINE_OK,表示成功读取一行内容
}
return LINE_BAD; // 其他情况,返回LINE_BAD,表示行格式错误
}
else if (temp == '\n') // 如果当前字符为换行符
{
// 判断 m_checked_idx 是否大于 1 防止越界,换行符前面是回车符,则将 \r\n 修改成 \0\0
// 并将 m_checked_idx 指向下一行开头
if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')
{
m_read_buf[m_checked_idx - 1] = '\0'; // 将回车符置为字符串结束符'\0'
m_read_buf[m_checked_idx++] = '\0'; // 将换行符置为字符串结束符'\0'
return LINE_OK; // 返回LINE_OK,表示成功读取一行内容
}
return LINE_BAD; // 其他情况,返回LINE_BAD,表示行格式错误
}
}
return LINE_OPEN; // 如果循环结束仍未找到回车符和换行符,则需要继续读取数据,则返回LINE_OPEN
}

parse_request_line()解析请求行#

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。

  • CHECK_STATE_REQUESTLINE
    • 主状态机的初始状态,调用parse_request_line()函数解析请求行
    • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
    • 解析完成后主状态机的状态变为CHECK_STATE_HEADER
// 解析请求行,获得请求方法、请求路径、http协议版本
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
// 在 HTTP 报文中,请求行用来说明请求类型、要访问的资源路径、使用的 HTTP 版本
// 其中三个部分之间通过 \t 或者空格分隔
// 所以先获取请求行中第一个空格或制表符位置的指针
/*
strpbrk():在一个字符串中查找包含指定字符集合中任意字符的第一个匹配字符,并返回指向该字符的指针
参数:
str1:要搜索的字符串
str2:要搜索的字符集合,注意这里是搜索字符集合中的字符,而不是字符集合这个字符串
返回值:
找到匹配字符,返回指向该字符的指针
没找到匹配字符,返回空指针NULL
*/
// 例如:GET /index.html http1.1/0/0,这时指针指向GET后的空格
m_url = strpbrk(text, " \t");
// 如果没有找到,则表示请求格式错误,返回BAD_REQUEST
if (!m_url)
{
return BAD_REQUEST;
}
// 将该位置处的字符修改为字符串结束符'\0',并将指针后移一位,指向请求路径
*m_url++ = '\0'; // 此时:GET\0/index.html http1.1\0\0,指针指向请求路径的第一个字符'/'
// 获取请求方法(GET或POST)
// text指向的还是请求行中请求方法的第一个字符G:GET\0/index.html http1.1\0\0
// 而\0又代表字符串结束,所以method如果输出的话就会输出GET
char *method = text;
// 判断请求方式是否为GET或POST,如果是则赋值给成员变量m_method,并标记cgi为1(表示需要执行CGI程序)
if (strcasecmp(method, "GET") == 0) // strcasecmp() 字符串比较函数
m_method = GET;
else if (strcasecmp(method, "POST") == 0) // strcasecmp() 不区分大小写
{
m_method = POST;
cgi = 1;
}
else // 否则,请求格式错误,返回BAD_REQUEST
return BAD_REQUEST;
// 使用 strspn() 函数得到 m_url 中第一个没有在 “ \t” 中出现的字符的位置,
// 即第一个不是空格或者制表符的位置,起到了跳过空格和制表符的作用
// 而目前字符串中第一个个字符为'/',正好不是“ \t”中出现的字符,
// 所以现在m_url还是指向请求路径第一个字符'/'
m_url += strspn(m_url, " \t");
// 获取HTTP协议版本号
/*
在上面代码中,m_url指向请求路径第一个字符'/',现在使用strpbrk()函数,在m_url中
找" \t"集合中任意字符的第一个匹配字符,就是在m_url后找空格或制表符,然后返回指向
这个空格或换行符的指针,由于目前指向请求路径,所以m_version接收到的指针所指位置
为请求路径后的空格,然后将这个空格改为'\0',所以现在的请求行为:
GET\0/index.html\0http1.1\0\0;并将m_version后移一位到http协议版本
的第一个字符,同时http协议版本后的回车换行符在之前已经换成了"\0\0",所以m_version
如果输出,应该输出的是:http1.1
*/
m_version = strpbrk(m_url, " \t");
if (!m_version) // 如果没有找到,则表示请求格式错误,返回BAD_REQUEST
return BAD_REQUEST;
*m_version++ = '\0'; // 将该位置处的字符修改为字符串结束符'\0',并将指针后移一位,指向HTTP协议版本号
// 跳过空格和制表符
// 目前m_version指向http协议版本的第一个字符'h',正好不是空格或制表符,所以调用strspn()
// 得到的结果为指向下标0的指针,所以m_version没有后移,还是指向http协议版本的第一个字符'h'
// 此时输出m_version,还是会输出:http1.1
m_version += strspn(m_version, " \t");
// 判断HTTP协议版本是否为HTTP1.1,如果不是,则请求格式错误,返回BAD_REQUEST
if (strcasecmp(m_version, "HTTP/1.1") != 0)
return BAD_REQUEST;
// 判断URL地址是否以http://或https://开头,如果是,则忽略掉
// 上面处理的过程中,m_url指向请求路径的第一个字符'/',请求路径后的空格或换行符也被替换成了\0
// 所以现在如果输出m_url,输出结果为:/index.html,正好就是请求路径
// 比较m_url和"http://"的前七个字符,不考虑大小写情况下相同就m_url后移7位,指向真正的请求路径
if (strncasecmp(m_url, "http://", 7) == 0)
{
m_url += 7;
// strchr()会返回在m_url中第一个'/'出现的位置指针,例如当请求路径为http://www.cfeng.ltd/index.html时,前面的http://已经跳过了,strchr()会返回index前的'/',此时输出m_url时会输出/index.html
m_url = strchr(m_url, '/');
}
// 这里的思路与上面的判断前七个字符的思路是相同的,例如当请求路径为https://www.cfeng.ltd/index.html时,调用完strchr()函数也会实现让m_url为/index.html
if (strncasecmp(m_url, "https://", 8) == 0)
{
m_url += 8;
m_url = strchr(m_url, '/'); // m_url中第一个'/'下标的指针
}
// 如果URL地址不是以'/'开头,表示请求格式错误,返回BAD_REQUEST
// 一般的不会带有上述两种符号,直接是单独的/或/后面带有访问资源
if (!m_url || m_url[0] != '/')
return BAD_REQUEST;
// 当URL地址为'/'时,默认显示judge.html欢迎页面
if (strlen(m_url) == 1)
strcat(m_url, "judge.html");
// 进入下一个解析状态CHECK_STATE_HEADER,该状态用于解析HTTP请求头
m_check_state = CHECK_STATE_HEADER;
// 返回NO_REQUEST,表示解析完HTTP请求行,但并未得到完整的HTTP请求内容(即HTTP请求头未完成)
return NO_REQUEST;
}

parse_headers()解析请求头#

解析完请求行后,主状态机继续分析请求头。在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。

  • CHECK_STATE_HEADER

    • 调用parse_headers函数解析请求头部信息
    • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
    • 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
    • connection字段判断是keep-alive还是close,决定是==长连接==还是==短连接==
    • content-length字段,这里用于==读取post请求的消息体长度==
//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
// 判断是空行还是请求头
// 首字符为\0说明是空行
if (text[0] == '\0')
{
// 通过读取请求体长度来判断是GET还是POST请求
// m_content_length:post请求体长度
if (m_content_length != 0)
{
// POST需要跳转到请求体处理状态
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST; // 请求不完整,需要继续读取请求报文数据
}
return GET_REQUEST; // 获得完整的HTTP请求
}
// 解析请求头连接字段
// strncasecmp():比较字符串前n个字符,不区分大小写,相等返回0
else if (strncasecmp(text, "Connection:", 11) == 0)
{
text += 11; // 跳过“Connection:”字符串
// 跳过空格和制表符
// strspn():返回str1中第一个没有出现在str2中字符的位置,实现跳过空格或制表符
text += strspn(text, " \t");
// strcasecmp():比较字符串,相等返回0
if (strcasecmp(text, "keep-alive") == 0)
{
// 如果是长连接,则将linger标志设置为true
m_linger = true;
}
}
// 解析请求体内容长度字段
// // strncasecmp():比较字符串前n个字符,不区分大小写,相等返回0
else if (strncasecmp(text, "Content-length:", 15) == 0)
{
text += 15; // 跳过"Content-length:"字符串
// strspn():返回str1中第一个没有出现在str2中字符的位置,实现跳过空格或制表符
text += strspn(text, " \t");
// 将请求体长度保存到m_content_length变量中
// atol():字符串中连续数字转长整型,遇到字符结束,后面的数字就不算了,返回长整型数字,如果没有数字返回0
m_content_length = atol(text); // 取请求体长度
}
// 解析请求头HOST字段
// strncasecmp():比较字符串前n个字符,不区分大小写,相等返回0
else if (strncasecmp(text, "Host:", 5) == 0)
{
text += 5; // 跳过"Host:"字符串
// strspn():返回str1中第一个没有出现在str2中字符的位置,实现跳过空格或制表符
text += strspn(text, " \t");
m_host = text; // 将主机名保存到m_host变量中
}
else
{
LOG_INFO("oop!unknow header: %s", text);
// 如果无法识别该头部,则打印日志提示
}
return NO_REQUEST; // 请求不完整,需要继续读取请求报文数据
}

m_content_length是哪里的变量,怎么实现代表请求体长度的#

parse_content()解析请求体#

附加内容#

主从状态机解析请求报文有哪些优势?

主从状态机是一种常见的解析和处理HTTP请求报文的方法。在这种方法中,状态机被分成多个状态和转移,其中主状态机负责管理不同的请求方法(如GET、POST、PUT等),而子状态机则处理特定方法下的状态转移。

使用主从状态机解析请求报文的好处包括:

  1. ==状态清晰==:采用状态机可以将复杂的请求报文处理过程分解为多个状态和转移,使得处理过程更加清晰和可控。

  2. ==可扩展==:主从状态机的设计方便新增、删除或修改请求报文的处理方式,因此可以较容易地扩展和升级程序。

  3. ==提高效率==:状态机处理请求报文的==效率通常比基于正则表达式的解析更高==,因为状态机的==状态转移是固定的==,==不需要进行多次匹配==。

  4. 更好的错误处理:状态机可以根据状态及状态转移有针对性地处理请求报文中的错误,比如语法错误、参数错误、身份认证错误等。同时,状态机处理错误的能力也便于跟踪问题并进行调试。

总之,主从状态机解析请求报文具有状态清晰、可扩展、高效率和更好的错误处理等优点,被广泛应用于Web服务器和网络应用程序的开发。

状态机处理请求报文的效率通常比基于正则表达式的解析更高,请问正则表达式处理报文是如何处理的

正则表达式是一种强大的文本匹配工具,可以用于处理各种模式匹配任务,包括处理请求报文中的数据。在处理请求报文时,基于正则表达式的解析通常涉及以下几个步骤:

  1. 正则表达式编译:首先,将正则表达式编译成内部的数据结构,以便进行后续的匹配操作。这个过程可以看作是将正则表达式转换成一种可被计算机理解和处理的形式。

  2. 请求报文输入:将待匹配的请求报文作为输入提供给正则表达式引擎。

  3. 匹配操作:正则表达式引擎 根据 编译得到的 数据结构,执行匹配操作 来确定 输入的文本 是否与 正则表达式 相匹配。这可能涉及==遍历输入文本==、==尝试不同的匹配路径==和==回溯==等操作。

  4. 提取结果:如果匹配成功,正则表达式引擎可以提取出与正则表达式中定义的模式相匹配的部分内容。这些提取的结果可以用于进一步的处理或响应生成。

需要注意的是,正则表达式引擎的性能取决于多种因素,包括正则表达式的复杂度、输入文本的长度、匹配选项的设置以及具体实现的优化程度等。在某些情况下,正则表达式匹配可能会导致性能问题,特别是当==输入文本非常大且正则表达式复杂==时。

相比之下,状态机处理请求报文通常更高效。状态机是一种基于状态转换的模型,可以根据特定的规则和状态转换表来解析输入。通过状态机解析请求报文时,==每个字符只需要被处理一次==,==无需回溯或多次尝试==。这使得状态机在==某些场景下==比正则表达式更快速和高效。

综上所述,虽然正则表达式是一种灵活而强大的工具,但在处理请求报文时,基于状态机的解析通常更高效。使用状态机可以通过直接按照定义的规则进行逐个字符的处理,而无需涉及复杂的匹配和回溯操作。

如果状态机状态太多,比如有100个状态,怎么办

如果状态机的状态太多,例如有100个状态,可以考虑以下几种策略来管理和处理:

  1. 状态合并:对于类似或相似的状态,可以尝试将其合并为一个==更高级别==的状态。通过状态合并,可以减少总体的状态数量,从而简化状态机的设计和维护。这需要对状态的功能和转换条件进行仔细分析和抽象。
  2. 分层状态机:如果状态机的状态数量庞大且复杂度高,一种常见的做法是使用分层状态机的概念。分层状态机将状态分为多个层次,==每个层次包含一组相关的状态==。通过分层,可以将状态机的复杂度分解为==多个较小的==、==可管理的==部分。不同层次的状态机可以通过==状态转移==来有效地交互。
  3. 状态优化:在设计状态机时,可以==优化状态之间的转移条件和逻辑==,以减少状态的数量。通过合理的状态设计,可以简化状态机的结构,并减少需要维护的状态数量。
  4. 工具支持:使用专门的状态机设计工具或库可以简化状态机的实现和管理。这些工具通常提供了便捷的图形界面或编程接口,支持状态机的可视化设计、状态转换规则的定义以及自动代码生成等功能,有助于简化状态机的设计和维护工作。
  5. 考虑使用其他技术:在某些情况下,状态机可能不是最适合的解决方案。根据具体需求,可以考虑其他技术和模型,如解析器组合、语法分析等,来处理复杂的报文解析场景。

需要根据具体的应用场景和需求来选择适当的策略。在实际设计中,可以综合考虑上述策略并结合实际情况进行调整,以达到更好的性能和可维护性。

驱动型学习#

比如在看webserver中http连接部分处理请求报文时用到的状态机,看看状态机的代码,理解一下这个状态机的思路,然后思考为什么用状态机呢?这里用状态机有什么优势吗?就去网上找,当然这是不急的情况下,如果说我手头有很多工作很急,我肯定就把想的这些问题记下来放后面不急的时候再查了,过后效果会差一点。回归刚才的话题,网上找状态机的优势,一看,奥,状态清晰、可扩展、效率比较高,这个效率相对于正则表达式更高;欸,那为什么比正则表达式更高呢?找一下,发现,正则表达式处理报文有一个匹配操作,正则表达式引擎根据编译得到的数据结构,执行匹配操作来确定输入的文本是否与正则表达式相匹配,这时就涉及到了遍历输入文本、尝试不同的匹配路径、回溯等操作,那么对于一个字符,正则表达式可能会遍历多次,而状态机的状态转移明确,在处理请求报文时,每个字符只会被从状态机遍历一次,奥,这里效率高啊,就是这种思路去学习

拆解-unmap()取消文件映射#

以下是对给定代码的解释和注释:

void http_conn::unmap()
{
// 如果文件映射地址存在,则取消映射,并将文件映射地址指针置为0
if (m_file_address)
{
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}

该代码是一个用于取消文件映射的函数 unmap(),它通过调用 munmap() 函数来取消之前创建的文件映射。下面是对代码中各部分的详细解释和注释:

  • if (m_file_address):检查文件映射地址是否存在(不为0)。如果存在,则说明之前已经进行了文件映射。

  • munmap(m_file_address, m_file_stat.st_size):调用 munmap() 函数来取消文件映射。munmap() 函数用于释放由 mmap() 函数创建的内存映射区域。参数 m_file_address 是要取消映射的地址,m_file_stat.st_size 是之前映射的文件大小。

  • m_file_address = 0:将文件映射地址指针置为0,表示取消了文件映射。

总体来说,该函数用于释放先前创建的文件映射。通过调用 munmap() 函数取消映射,并将文件映射地址指针置为0,以确保不再使用已取消的文件映射地址。

拆解-process_write()请求处理函数中的写操作函数#

该代码是一个HTTP请求处理函数中的写操作函数,根据传入的HTTP_CODE参数进行不同的处理,并返回是否写操作成功的布尔值。以下是对代码的注释:

bool http_conn::process_write(HTTP_CODE ret)
{
switch (ret)
{
// 内部错误
case INTERNAL_ERROR:
{
// 添加状态行和头部信息
add_status_line(500, error_500_title);
add_headers(strlen(error_500_form));
// 添加内容
if (!add_content(error_500_form))
return false;
break;
}
// 错误的请求
case BAD_REQUEST:
{
// 添加状态行和头部信息
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
// 添加内容
if (!add_content(error_404_form))
return false;
break;
}
// 禁止请求
case FORBIDDEN_REQUEST:
{
// 添加状态行和头部信息
add_status_line(403, error_403_title);
add_headers(strlen(error_403_form));
// 添加内容
if (!add_content(error_403_form))
return false;
break;
}
// 文件请求
case FILE_REQUEST:
{
// 添加状态行
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0)
{
// 如果文件大小不为0,则添加头部信息和文件内容
add_headers(m_file_stat.st_size);
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else
{
// 如果文件大小为0,则返回一个空的HTTP响应报文
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
default:
return false;
}
// 设置m_iv和bytes_to_send,用于发送响应数据
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}

该函数根据传入的HTTP_CODE参数ret进行不同的处理:

  1. ret等于INTERNAL_ERROR时,表示出现了内部错误。
    • 添加状态行为500的HTTP响应报文。
    • 添加长度为error_500_form的头部信息。
    • 添加内容为error_500_form
    • 如果添加内容失败,则返回false
  2. ret等于BAD_REQUEST时,表示请求有错误。
    • 添加状态行为404的HTTP响应报文。
    • 添加长度为error_404_form的头部信息。
    • 添加内容为error_404_form
    • 如果添加内容失败,则返回false
  3. ret等于FORBIDDEN_REQUEST时,表示请求被禁止。
    • 添加状态行为403的HTTP响应报文。
    • 添加长度为error_403_form的头部信息。
    • 添加内容为error_403_form
    • 如果添加内容失败,则返回false
  4. ret等于FILE_REQUEST时,表示请求的是文件。
    • 添加状态行为200的HTTP响应报文。
    • 如果文件大小不为0,则:
      • 添加文件大小为头部信息。
      • 设置m_iv为包含两部分数据的iovec结构体数组。
      • 设置m_iv_count为2,表示有两个部分数据。
      • 设置bytes_to_send为响应数据的总字节数。
      • 返回true,表示写操作成功。
    • 否则,文件大小为0,返回一个空的HTTP响应报文。
  5. 默认情况下,返回false,表示处理失败。
  6. 设置m_iv为包含响应数据的iovec结构体数组。
  7. 设置m_iv_count为1,表示只有一个部分数据。
  8. 设置bytes_to_send为响应数据的总字节数。
  9. 返回true,表示写操作成功。

拆解-malloc与new#

malloc()free()是C语言中动态分配内存和释放内存的函数,而newdelete则是C++中动态分配内存和释放内存的运算符。

通过malloc()free()可以手动控制内存的分配和释放,但需要注意手动分配内存后不要忘记释放,否则会导致内存泄漏的问题。

而在C++中,使用newdelete可以更加方便地管理内存,避免了手动控制内存的复杂性。new运算符会自动调用构造函数,返回指向新对象的指针;而delete运算符则会自动调用析构函数,并释放内存所占用的空间。

但需要注意的是,由于newdelete是C++中的运算符,因此它们仅适用于C++中定义的类型。如果需要分配和释放其他类型的内存空间,仍需使用malloc()free()函数。

拆解-str相关函数#

strcpy()复制#

  • strcpy(): strcpy()函数用于将一个字符串复制到另一个字符串中。它接收两个参数,目标字符串和源字符串,并将源字符串的内容复制到目标字符串中,直到遇到空字符\0为止。复制完成后,目标字符串将包含与源字符串相同的内容。

strrchr()查找指定字符最后一次出现位置#

  • strrchr(): strrchr()函数用于在一个字符串中查找指定字符的最后一次出现位置。它接收两个参数,一个是要搜索的字符串,另一个是要查找的字符。该函数从字符串的末尾开始逆向搜索,返回指向最后一次出现位置的指针(或者返回NULL,如果未找到匹配的字符)。

strcar()将源字符串追加到目标字符串末尾#

  • strcat(): strcat()函数用于将源字符串的内容追加到目标字符串的末尾。它接收两个参数,目标字符串和源字符串,并将源字符串的内容添加到目标字符串的末尾,修改目标字符串。该函数会自动在目标字符串的末尾添加一个空字符\0,以表示字符串的结束。

strncpy()将字符串部分内容复制到另一个字符串中#

  • strncpy(): strncpy()函数用于将一个字符串的部分内容复制到另一个字符串中。它接收三个参数,目标字符串、源字符串以及要复制的最大字符数。函数将源字符串的指定数量的字符复制到目标字符串中,如果源字符串长度不足,则会用空字符\0填充剩余部分。需要注意的是,strncpy()函数不会自动在目标字符串的末尾添加空字符\0,如果需要以C风格字符串结束,需要手动添加。

strpbrk()在字符串中查找指定字符集合中任意字符的第一个匹配字符#

  • strpbrk():用于在一个字符串中查找包含指定字符集合中任意字符的==第一个匹配字符(单个字符不是字符串)==,并返回指向该字符的指针。它接收两个参数,要搜索的字符串,要搜索的字符集合(集合是字符串的形式,但是搜索时会搜索集合中的字符,而不是集合这个字符串);如果找到的匹配的字符,返回指向该字符的指针;如果没找到匹配的字符,则返回空指针(NULL)

strcasecmp()字符串比较且不区分大小写#

strcasecmp()函数是一个字符串比较函数,用于比较两个字符串是否相等,不区分大小写。

函数原型如下:

int strcasecmp(const char *str1, const char *str2);

参数解释:

  • str1:要比较的第一个字符串。
  • str2:要比较的第二个字符串。

返回值:

  • 如果两个字符串相等(忽略大小写),返回0。
  • 如果第一个字符串小于第二个字符串,返回一个负整数。
  • 如果第一个字符串大于第二个字符串,返回一个正整数。

使用示例:

const char *str1 = "Hello";
const char *str2 = "hello";
int result = strcasecmp(str1, str2);
if (result == 0) {
printf("两个字符串相等\n");
} else if (result < 0) {
printf("第一个字符串小于第二个字符串\n");
} else {
printf("第一个字符串大于第二个字符串\n");
}

输出结果:

两个字符串相等

在上述示例中,strcasecmp函数比较了字符串str1str2的大小,由于这两个字符串相等(忽略大小写),所以返回值为0。

需要注意的是,strcasecmp函数在比较字符串时会忽略大小写的差异。如果需要区分大小写,可以使用strcmp函数进行比较。

strncasecmp()前n个字符比较且不区分大小写#

strncasecmp 函数是 C 标准库中的字符串函数之一,用于比较两个字符串的大小写不敏感的差异,但它只比较两个字符串的前 n 个字符。它的原型在头文件 strings.h(或 string.h)中声明:

int strncasecmp(const char *s1, const char *s2, size_t n);

strncasecmp 函数会比较字符串 s1s2 的大小写不敏感的差异,但只比较它们的前 n 个字符,并返回一个整数值表示比较结果。如果 s1 的前 n 个字符小于 s2 的前 n 个字符,则返回负数;如果 s1 的前 n 个字符等于 s2 的前 n 个字符,则返回 0;如果 s1 的前 n 个字符大于 s2 的前 n 个字符,则返回正数。

strncasecmp 函数的第三个参数 n 表示要比较的字符数,也就是我们只比较两个字符串的前 n 个字符。这个参数的作用是限制比较的范围,只考虑字符串的前部分。

  • 如果 n 小于等于 0,那么无论原字符串有多长,strncasecmp 函数都会返回 0,表示两个字符串相等。这是因为没有比较的字符,所以结果是相等的。
  • 如果 n 大于字符串的长度,那么 strncasecmp 函数会比较整个字符串。它会比较两个字符串的所有字符,直到遇到其中一个字符串的结束符(\0)为止。

举个例子来说明:

const char *s1 = "Hello";
const char *s2 = "hello, world!";
int result = strncasecmp(s1, s2, 5);

在这个例子中,我们比较了字符串 s1s2 的前 5 个字符。即使 s2 有更多的字符,但是由于限制了比较字符数,所以只比较了 s1 的前 5 个字符。如果 s1s2 的前 5 个字符在忽略大小写的情况下相等,那么结果就是相等的。

例如,下面的代码演示了使用 strncasecmp 进行字符串比较的例子:

const char *s1 = "Hello, world!";
const char *s2 = "hello, everybody!";
int result = strncasecmp(s1, s2, 5);
if (result < 0) {
printf("s1 is less than s2\n");
} else if (result > 0) {
printf("s1 is greater than s2\n");
} else {
printf("s1 is equal to s2\n");
}

输出结果:

s1 is equal to s2

需要注意的是,strncasecmp 函数在比较过程中会忽略大小写的差异。如果需要进行大小写敏感的字符串比较,可以使用 strncmp 函数。

strcmp()字符串比较区分大小写#

strcmp函数是一个字符串比较函数,用于比较两个字符串是否相等,区分大小写。

函数原型如下:

int strcmp(const char *str1, const char *str2);

参数解释:

  • str1:要比较的第一个字符串。
  • str2:要比较的第二个字符串。

返回值:

  • 如果两个字符串相等,返回0。
  • 如果第一个字符串小于第二个字符串,返回一个负整数。
  • 如果第一个字符串大于第二个字符串,返回一个正整数。

使用示例:

const char *str1 = "Hello";
const char *str2 = "World";
int result = strcmp(str1, str2);
if (result == 0) {
printf("两个字符串相等\n");
} else if (result < 0) {
printf("第一个字符串小于第二个字符串\n");
} else {
printf("第一个字符串大于第二个字符串\n");
}

输出结果:

第一个字符串小于第二个字符串

在上述示例中,strcmp函数比较了字符串str1str2的大小,由于第一个字符串按照字典顺序小于第二个字符串,所以返回值为负整数。

需要注意的是,strcmp函数在比较字符串时会区分大小写。如果希望忽略大小写进行比较,可以使用strcasecmp函数。

strspn()返回str1中第一个没有出现在str2中字符的位置#

strspn 函数是 C 标准库中的字符串函数之一,其原型在头文件 string.h 中声明:

size_t strspn(const char *s, const char *accept);

它的作用是返回字符串 s 中第一个不在字符串 accept 中出现的字符的位置。换句话说,它会统计字符串 s 开头连续包含字符串 accept 中字符的长度。

例如,下面的代码可以统计字符串 s 开头有几个字母:

const char *s = "hello, world";
const char *letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
size_t n = strspn(s, letters);
printf("The length of initial letters: %zu\n", n);

输出结果为:

The length of initial letters: 5

这是因为字符串 s 开头的前五个字符都是字母。

需要注意的是,strspn 函数的实现是基于暴力枚举的,因此对于长字符串和字符集会比较慢。如果需要高效地处理字符串,可以考虑使用更加优化的算法。

strchr()在str1中搜索指定字符的第一个位置指针#

strchr() 是 C 标准库中的一个字符串函数,用于在一个字符串中搜索指定字符的第一个匹配位置,并返回一个指向该位置的指针。

strchr() 的原型如下:

char *strchr(const char *s, int c);
  • s 是要在其中搜索的字符串。
  • c 是要查找的字符(以整数形式传递)。

strchr() 函数会在字符串 s 中搜索字符 c 的第一个匹配位置,包括字符串末尾的空字符 \0。如果找到了匹配的字符,就返回一个指向它的指针;如果没有找到匹配的字符,就返回空指针。

下面是一个使用 strchr() 的简单示例:

const char *str = "Hello, world!";
char *result = strchr(str, 'o');
if (result != NULL) {
printf("找到了匹配的字符:%s\n", result);
} else {
printf("未找到匹配的字符。\n");
}

在此示例中,我们在字符串 str 中搜索字符 'o'。由于 'o' 出现在字符串中,strchr() 返回的指针将指向首次出现该字符的位置。

输出结果:

找到了匹配的字符:o, world!

需要注意的是,strchr() 函数返回的指针是指向常量字符串的指针,因此不能直接修改返回的指针指向的内容。如果需要修改字符串,则需要使用可变字符串。

希望这个解释对你有帮助。如果还有其他问题,请随时提问。

stol()字符串转长整型#

atol() 是一个 C 标准库函数,用于将字符串转换为长整型(long int)。atol() 的函数原型如下:

long int atol(const char *str);

参数 str 是一个指向以 null 结尾的字符串的指针,表示要进行转换的字符串。

atol() 函数会扫描字符串 str,跳过前导空格字符,直到遇到数字字符或正负号。然后它从指定位置开始解析连续的数字字符,直到遇到非数字字符为止。解析得到的数字将被转换为长整型并返回。

如果字符串无效或不包含可转换的数字,则返回 0。

以下是一些示例:

#include <stdio.h>
#include <stdlib.h>
int main() {
const char *str1 = "12345";
const char *str2 = " -67890";
const char *str3 = "12.34";
const char *str4 = "abc";
long int num1 = atol(str1);
long int num2 = atol(str2);
long int num3 = atol(str3);
long int num4 = atol(str4);
printf("%ld\n", num1); // 输出:12345
printf("%ld\n", num2); // 输出:-67890
printf("%ld\n", num3); // 输出:12
printf("%ld\n", num4); // 输出:0
return 0;
}

注意,atol() 函数会忽略字符串中的小数点和后续的字符,只返回整数部分。如果需要解析更复杂的数字格式,可以使用其他函数如 strtol() 或者转换库函数 sscanf()

\0在字符串中的作用#

在 C 语言中,\0 是表示字符串结束的特殊字符,也称为空字符空格字符。它是一个 ASCII 值为 0 的字符。

在 C 语言中,字符串是一系列以空字符结尾的字符组成的字符数组。当我们使用双引号括起来的字符序列时,编译器会自动在字符串的末尾添加一个空字符。

例如,声明一个包含字符串 “Hello” 的字符数组:

char str[] = "Hello";

实际上,这个字符数组被存储为连续的字符序列 'H', 'e', 'l', 'l', 'o', '\0'

空字符的作用是告诉程序字符串的结尾位置,这样在处理字符串时可以根据空字符来确定字符串的长度和边界。许多 C 字符串函数(如 strlen, strcpy, strcmp 等)都会依赖字符串中的空字符来执行相应的操作。

需要注意的是,在字符串中 \0 是一个特殊的转义序列,表示字符 0。如果要在字符串中包含字符 \0,可以使用 \\0 来表示。

总之,\0 在字符串中的作用是标识字符串的结束位置,以及在许多字符串处理函数中作为重要的判断标志。

拆解-write()将http响应报文写入套接字#

这段代码是一个HTTP连接类的成员函数write(),用于向客户端发送HTTP响应。

以下是对代码的解释和注释:

bool http_conn::write()
{
int temp = 0;
// 如果待发送的数据字节数为0,表示响应已发送完毕
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); // 修改文件描述符事件,监听可读事件
init(); // 重置HTTP连接对象的状态
return true;
}
while (1)
{
temp = writev(m_sockfd, m_iv, m_iv_count); // 使用writev函数向套接字写入数据
if (temp < 0)
{
if (errno == EAGAIN) // 若写缓冲区满,则等待下一次写事件触发再继续写
{
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); // 修改文件描述符事件,监听可写事件
return true;
}
unmap(); // 解除文件映射关系
return false;
}
bytes_have_send += temp; // 已发送的字节数增加
bytes_to_send -= temp; // 待发送的字节数减少
// 若当前发送的数据超过了m_iv[0]的长度
if (bytes_have_send >= m_iv[0].iov_len)
{
m_iv[0].iov_len = 0; // 清空m_iv[0]的长度,表示已发送完该缓冲区的数据
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx); // 更新m_iv[1]的基址,指向待发送的文件数据
m_iv[1].iov_len = bytes_to_send; // 更新m_iv[1]的长度,表示待发送的文件数据的长度
}
else
{
m_iv[0].iov_base = m_write_buf + bytes_have_send; // 更新m_iv[0]的基址,指向待发送的内存缓冲区数据
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send; // 更新m_iv[0]的长度,表示待发送的内存缓冲区数据的长度
}
// 若待发送的字节数已经为0,表示响应数据发送完毕
if (bytes_to_send <= 0)
{
unmap(); // 解除文件映射关系
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); // 修改文件描述符事件,监听可读事件
if (m_linger) // 若需要保持连接,则重置HTTP连接对象的状态
{
init();
return true;
}
else // 不需要保持连接,关闭连接
{
return false;
}
}
}
}

此函数的主要功能是通过循环不断地将HTTP响应写入套接字,直到所有响应数据都发送完毕。

  • bytes_to_send表示待发送的字节数。如果为0,表示响应已经发送完毕,可以进行下一步操作。
  • 在循环中,使用writev()函数将数据写入套接字。如果写入失败,则根据errno判断是否需要等待下一次写事件触发再继续写,或者直接返回失败。
  • 每次写入成功后,更新已发送的字节数bytes_have_send和待发送的字节数bytes_to_send
  • 如果当前发送的数据超过了m_iv[0]缓冲区的长度,将其置为0,然后更新m_iv[1]的基址和长度,指向待发送的文件数据。
  • 如果待发送的字节数已经为0,表示响应数据发送完毕。解除文件映射关系、修改文件描述符事件,并根据是否需要保持连接来重置HTTP连接对象的状态或关闭连接。

拆解-initmysql_result()从MySQL数据库中查询用户信息,并将其存入一个map容器中#

这段代码是一个HTTP连接类成员函数initmysql_result(),用于初始化数据库用户信息,从数据库中查询用户名和密码,并将其存入一个map容器中。

以下是对代码的解释和注释:

map<string, string> users; // 存储用户名和密码的 map
void http_conn::initmysql_result(connection_pool *connPool)
{
// 先从连接池中取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);
// 在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql)); // 若查询失败,输出错误信息
}
// 从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);
// 返回结果集中的列数
int num_fields = mysql_num_fields(result);
// 返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);
// 从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]); // 提取用户名
string temp2(row[1]); // 提取密码
users[temp1] = temp2; // 将用户名和密码插入到map中
}
}

此函数的主要功能是从MySQL数据库中查询用户信息,并将其存入一个map容器中,供后续使用。

  • 首先从数据库连接池中取出一个连接,并生成一个类对象connectionRAII,在构造函数中完成连接的获取,在析构函数中完成连接的释放。
  • 然后使用mysql_query()函数向数据库发送查询语句,如果查询失败,则输出错误信息。
  • 使用mysql_store_result()函数从结果集中检索完整的结果。结果被存储在MYSQL_RES结构体中,该结构体包含了表的信息以及检索的行。
  • 使用mysql_num_fields()函数返回结果集中的列数。
  • 使用mysql_fetch_fields()函数返回所有字段结构的数组。每个字段结构包括字段名、类型等信息。
  • 使用mysql_fetch_row()函数逐行获取结果集中的数据,并将其存入一个map容器中供后续操作。

扩展#

结构体字节数#

struct{char ch; int a; double b; static int c; int func(); double func1(); static int func2();};这个结构体的字节数是多少

对于给出的结构体定义:

struct {
char ch;
int a;
double b;
static int c;
int func();
double func1();
static int func2();
};

我们可以逐个成员进行分析,并考虑对齐规则:

  1. char ch:字符型变量,通常占用1个字节。

  2. int a:整型变量,通常占用4个字节。

  3. double b:双精度浮点数,通常占用8个字节。

  4. static int c:静态变量也属于结构体的成员,但不会占用结构体实例的存储空间,因此不会增加结构体的字节数。

  5. int func():函数不会直接影响结构体实例的字节数。

  6. double func1():函数不会直接影响结构体实例的字节数。

  7. static int func2():静态函数不会占用结构体实例的存储空间,因此不会增加结构体的字节数。

根据成员的类型和对齐规则,该结构体的字节数为 1 + 4 + 8 = 16(32位系统)或者 1 + 4 + 8 = 13(64位系统),再加上填充字节的数量。

需要注意的是,对于结构体而言,还有可能会有额外的填充字节用于满足对齐规则。这些填充字节的数量取决于编译器和对齐规则。因此,实际的结构体字节数可能会比上述计算的数字稍大一些。

如果您确切地知道使用的编译器和对齐规则,可以使用 sizeof 运算符来获取结构体的字节数。例如,在 C 语言中,可以使用 sizeof(struct_name) 来获取结构体实例的字节数。

在 64 位机器上,int 类型也通常占用 4 字节(32 位),但在某些特殊的情况下,可能会占用 8 字节(64 位),这个特殊情况有哪些

在大多数情况下,在64位机器上,int 类型仍然占用4字节(32位)。这是由于保持向后兼容性和与32位机器代码的互操作性的考虑。但是,也存在一些特殊情况下,int 类型可能会占用8字节(64位)。以下是其中一些特殊情况的示例:

  1. 编译器选项:某些编译器提供了选项可以改变 int 类型的大小。例如,GCC 的 -m32 选项可用于指定生成32位目标代码,而 -m64 选项则可用于指定生成64位目标代码。使用 -m64 选项时,int 类型的大小可能会扩展为8字节。

  2. 数据模型:某些特定的数据模型要求将 int 扩展为64位。例如,LP64数据模型(常见于Windows、Linux和MacOS等操作系统)中,int 类型扩展为8字节以匹配指针的大小。

  3. 特定平台或体系结构:在某些嵌入式系统、特定处理器体系结构或操作系统上,int 类型可能会被定义为8字节。

需要注意的是,这些情况属于特殊情况,不是通常情况下的默认行为。在绝大多数的64位机器上,int 类型仍然是4字节。如果对特定的平台、编译器或数据模型有疑问,建议查阅相关文档和参考资料以获取准确的信息。

Tiny Webserver
https://fuwari.cbba.top/posts/tiny-webserver/
作者
Chen_Feng
发布于
2023-10-01
许可协议
CC BY-NC-SA 4.0