mini_ngx_实现三-http模块

Catalogue
  1. 1. @http_process_init 主要启动函数
  2. 2. 主要流程
    1. 2.1. @handler 处理新连接事件
    2. 2.2. @http_init_connection 初始化http请求
    3. 2.3. @http_init_request 解析http协议
  3. 3. http核心处理函数

@http_process_init 主要启动函数

上面cycle初始化的时候start_module启动的就是当前http入口模块函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//这里其实应该是核心http模块的启动,主要是启动监听端口
//但是端口配置添加 listening_t 在nginx中是通过nginx.conf配置解析时添加的
//我们这里作为演示就直接放到http core模块启动方法中
static int_t http_process_init(cycle_t *cycle)
{
log_info(cycle->log,"http: process init");
http_listen_opt_t lsopt;
struct sockaddr_in serv_addr;
memzero(&lsopt, sizeof(http_listen_opt_t));
memzero(&serv_addr,sizeof(serv_addr));
// listen 127.0.0.1:8000;
// listen 127.0.0.1 不加端口,默认监听80端口;
// listen 8000
// listen *:8000
// listen localhost:8000

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8089);

lsopt.sockaddr = (struct sockaddr *)&serv_addr;
lsopt.socklen = sizeof(serv_addr);

lsopt.backlog = BACKLOG;
lsopt.rcvbuf = -1;
lsopt.sndbuf = -1;

listening_t *ls;
ls = http_add_listening(cycle, &lsopt);
if (ls == NULL) {
return ERROR;
}
//初始化对应socket
return open_listening_sockets(cycle);
}

之前有讲过,nginx会在启动期间init_cycle会去解析nginx.conf配置并且将所有匹配的配置项传递给对应的模块,其实就是寻找ngx_command_t定义的模块配置项,可以看模块开发文章相关介绍:http://wiki.brewlin.com/wiki/blog/nginx/http_%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91%E7%9A%84%E6%AD%A5%E9%AA%A4%EF%BC%88%E4%B8%80%EF%BC%89/

那么当发现一个listen 80;配置后就会立即调用如上方法,新增初始化一个listening_t对象保存到cycle->listening上用于后面监听对应端口

我们这里也模仿对应的事件,因为不是配置行为,所以在这里手动添加了一个端口,模仿解析到了配置listen

主要流程

@handler 处理新连接事件

每个监听对象上面都会有一个listening->handler回调事件,每个模块都会去重写他,那么在http模块中为 http_init_connection

1
2
3
src/http/http.c:71

ls->handler = http_init_connection;

该方法在event事件监测到该端口有新连接到来时,会立即accept然后调用但当前http_init_connection方法表明接下来都会进行http相关操作

这种方法很好的解耦操作,不同协议之间只需要替换handler就可替换不同的实现

@http_init_connection 初始化http请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void http_init_connection(connection_t *c)
{
event_t *rev;
rev = c->read;
rev->handler = http_init_request;
c->write->handler = http_empty_handler;

if (rev->ready) {
rev->handler(rev);
return;
}

if (handle_read_event(rev, 0) != OK) {
http_close_connection(c);
return;
}
}
  1. 将该新的http连接的read事件取出来,判断是否已标示为可读状态
  2. 如果当前事件已就绪,则直接执行对应的handler也就是http_init_request
  3. 如果不是的话,就将该事件在放回到epoll中,等待下次事件就绪,在从连接中拿出来继续处理上次中断的地方

@http_init_request 解析http协议

走到这里,说明tcp连接以就绪,客户端已发送了http数据包,准备解析,如果没有则如上面一样继续丢到epoll中继续监听,直到http协议数据包就绪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void http_init_request(event_t *rev)
{
connection_t *c;
http_connection *hc;
c = rev->data;
hc = c->data;
if (hc == NULL) {
hc = pcalloc(c->pool, sizeof(http_connection));
if (hc == NULL) {
http_close_connection(c);
return;
}
}
c->data = hc;
hc->connection = c;
hc->log = c->log;

connection_init(hc);
rev->handler = connection_handler;
// connection_handler(hc);
// connection_close(hc);

// http_close_connection(c);
rev->handler(rev);
}

  1. 分配一个http_connection_t http请求对象并挂载到connection中
  2. 将事件handler置为connection_handler以后事件触发默认就走connection_handler
  3. 直接执行对应事件 rev->handler(rev)

我们这里没有关闭连接,而是一气呵成在connection_handler去关闭他,而真实nginx处理的时候要复杂的多。 因为现在是全部基于事件来处理对应流程,所以每个函数可能会多次调用,那么释放的问题就变的头疼,所以nginx真正对于http_request的释放是增加了引用计数的机制,也就是每个事件都负责引用计数+1当该函数执行完毕-1并且判断是否为0,为0则真正释放连接,不为0说明有其他事件被派生出来了,所以每个事件只需要关注当前自己的session即可

而我们的程序只有单一流程,所以不需要做引用计数,只需要读取本地html文件,响应客户端,然后在connection_handler中关闭连接释放资源即可

http核心处理函数

上面说了,nginx一个请求的处理可能涉及到数十个`子模块子过程`所以在哪里释放就是一个重要的问题,nginx用引用计数来解决了这个问题,而我们的mini版只涉及一个流程,所以不用引用计数来实现

直接在当前函数处理请求、响应请求、关闭请求、释放资源即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* HTTP请求处理函数
* - 从socket中读取数据并解析HTTP请求
* - 解析请求
* - 发送响应
* - 记录请求日志
*/
// int connection_handler(http_connection *con) {
void connection_handler(event_t *ev) {
connection_t *sock = (connection_t *)ev->data;
http_connection *con = sock->data;
char buf[512];
int nbytes;


while ((nbytes = sock->recv(sock,(u_char*)buf,sizeof(buf))) > 0) {
string_append_len(con->recv_buf, buf, nbytes);

if (http_request_complete(con) != 0)
break;
}

if (nbytes <= 0) {
if (nbytes == 0) {
log_info(con->log, "socket %d closed", sock->fd);
http_close_connection(sock);
return;

} else if (nbytes == AGAIN) {
if (handle_read_event(ev, 0) != OK) {
http_close_connection(sock);
return;
}
log_error(con->log, "read: %s", strerror(errno));
return;
}
}
http_request_parse(con);
http_response_send(con);
log_request(con);
http_close_connection(sock);
}

  1. 当我们调用sock->recv的时候,可能会返回AGAIN,说明该连接可能没有数据可读,我们只需要再次加入epoll监听即可,
  2. sock->recv返回0 ,说明对方主动关闭,我们也只需要关闭资源,释放连接即可
  3. 其他情况,我们只需要读出用户态缓冲区数据,解析http协议,处理请求,并释放资源就可以了
  4. http_request_parse: 解析http请求
  5. http_response_send: 响应客户端数据
  6. log_request(con) : 记录请求日志
  7. http_close_connection : 回收资源、连接等