Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🗜️ Understanding Nginx
    • Part 1 - Nginx 能帮我们做什么

      • Chapter 1 - 研究 Nginx 前的准备工作
      • Chapter 2 - Nginx 的配置
    • Part 2 - 如何编写 HTTP 模块

      • Chapter 7 - Nginx 提供的高级数据结构
    • Part 3 - 深入 Nginx

      • Chapter 8.1-8.2 - Nginx 基础架构
      • Chapter 8.3-8.6 - Nginx 框架核心结构体
      • Chapter 8.7 - Nginx 内存池
      • Chapter 9.1-9.3 - 事件处理框架
      • Chapter 9.4-9.6 - 事件驱动模块与 EPOLL
      • Chapter 9.7-9.8 - 定时器事件与事件驱动框架处理流程
      • Chapter 9.9-9.10 - 文件的异步 I/O 与 TCP
      • Chapter 10.1-10.2 - HTTP 框架的配置解析与合并
      • Chapter 10.3-10.7 - HTTP 阶段划分与框架初始化
      • Chapter 11 - HTTP 框架的执行流程
      • Chapter 12.1-12.4 - Upstream 与上游服务器通信
      • Chapter 12.5 - 接收上游服务器响应并处理
      • Chapter 12.6 - 12.9 - 转发响应并结束请求
      • Chapter 13.1-13.5 - 邮件代理模块 - 认证服务器
      • Chapter 13.6-13.7 - 邮件代理模块 - 上游
      • Chapter 14 - 进程间通信机制
      • Chapter 16 - slab 共享内存

Chapter 13.6-13.7 - 邮件代理模块 - 上游

Created by : Mr Dk.

2020 / 08 / 04 12:19

Nanjing, Jiangsu, China


13.6 与上游邮件服务器间的认证交互

对于 POP3、SMTP、IMAP 协议的邮件服务器来说,与客户端交互的方式都大不相同。Nginx 会先与上游的邮件服务器进行独立的交互,直到邮件服务器认为可以进行到处理阶段,才会开始 透传 协议。

13.6.1 ngx_mail_proxy_ctx_t 结构体

是 ngx_mail_session_t 结构体中 proxy 成员指向的结构体。其中维护着 Nginx 与上游服务器的连接,以及通信时接收上游消息的缓冲区。

typedef struct {
    ngx_peer_connection_t   upstream; // 与上游服务器的连接
    ngx_buf_t              *buffer; // 接收响应的缓冲区
} ngx_mail_proxy_ctx_t;

13.6.2 向上游邮件服务器发起连接

ngx_mail_proxy_init() 函数可以启动 Nginx 与上游邮件服务器之间的交互:

void
ngx_mail_proxy_init(ngx_mail_session_t *s, ngx_addr_t *peer)
{
    ngx_int_t                  rc;
    ngx_mail_proxy_ctx_t      *p;
    ngx_mail_proxy_conf_t     *pcf;
    ngx_mail_core_srv_conf_t  *cscf;

    s->connection->log->action = "connecting to upstream";

    cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);

    // 创建 ngx_mail_proxy_ctx_t 结构体
    p = ngx_pcalloc(s->connection->pool, sizeof(ngx_mail_proxy_ctx_t));
    if (p == NULL) {
        ngx_mail_session_internal_server_error(s);
        return;
    }

    // 设置、初始化
    s->proxy = p;

    p->upstream.sockaddr = peer->sockaddr;
    p->upstream.socklen = peer->socklen;
    p->upstream.name = &peer->name;
    p->upstream.get = ngx_event_get_peer;
    p->upstream.log = s->connection->log;
    p->upstream.log_error = NGX_ERROR_ERR;

    // 发起非阻塞的 TCP 连接
    rc = ngx_event_connect_peer(&p->upstream);

    if (rc == NGX_ERROR || rc == NGX_BUSY || rc == NGX_DECLINED) {
        ngx_mail_proxy_internal_server_error(s);
        return;
    }

    // 将连接的读事件添加到定时器中
    ngx_add_timer(p->upstream.connection->read, cscf->timeout);

    p->upstream.connection->data = s;
    p->upstream.connection->pool = s->connection->pool;

    // 下游连接的读事件回调设置为不读取内容 (Nginx 不与下游客户端交互)
    s->connection->read->handler = ngx_mail_proxy_block_read;
    // 上游连接的写事件回调设置为什么事都不做 (不通过事件驱动框架调度)
    p->upstream.connection->write->handler = ngx_mail_proxy_dummy_handler;

    pcf = ngx_mail_get_module_srv_conf(s, ngx_mail_proxy_module);

    // 建立 Nginx 与邮件服务器间的内存缓冲区
    s->proxy->buffer = ngx_create_temp_buf(s->connection->pool,
                                           pcf->buffer_size);
    if (s->proxy->buffer == NULL) {
        ngx_mail_proxy_internal_server_error(s);
        return;
    }

    s->out.len = 0;

    // 根据用户请求的协议设置上游连接的读事件回调函数
    switch (s->protocol) {

    case NGX_MAIL_POP3_PROTOCOL:
        p->upstream.connection->read->handler = ngx_mail_proxy_pop3_handler;
        s->mail_state = ngx_pop3_start;
        break;

    case NGX_MAIL_IMAP_PROTOCOL:
        p->upstream.connection->read->handler = ngx_mail_proxy_imap_handler;
        s->mail_state = ngx_imap_start;
        break;

    default: /* NGX_MAIL_SMTP_PROTOCOL */
        p->upstream.connection->read->handler = ngx_mail_proxy_smtp_handler;
        s->mail_state = ngx_smtp_start;
        break;
    }
}

可以看到,与上游服务器的交互过程中,写事件回调 (即发送请求) 不由事件驱动模块触发,读事件回调 (接收响应) 根据具体的邮件协议而不同。为什么发送请求不由事件驱动模块触发呢?因为邮件协议中的交互是一来一回的,所以可以在读事件回调中发送请求。

13.6.3 与邮件服务器认证交互

每种邮件协议服务器与 Nginx 的交互内容各不相同,以 POP3 协议为例:

static void
ngx_mail_proxy_pop3_handler(ngx_event_t *rev)
{
    u_char                 *p;
    ngx_int_t               rc;
    ngx_str_t               line;
    ngx_connection_t       *c;
    ngx_mail_session_t     *s;
    ngx_mail_proxy_conf_t  *pcf;

    ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0,
                   "mail proxy pop3 auth handler");

    // Nginx 与上游服务器的连接
    c = rev->data;
    // Nginx 的 ngx_mail_session_t 结构体
    s = c->data;

    // 读取上游邮件服务器响应超时
    if (rev->timedout) {
        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
                      "upstream timed out");
        c->timedout = 1;
        ngx_mail_proxy_internal_server_error(s);
        return;
    }

    // 读取响应到缓冲区中
    rc = ngx_mail_proxy_read_response(s, 0);

    // 没有读取完整,等待被再次调度
    if (rc == NGX_AGAIN) {
        return;
    }

    // 消息不合法
    if (rc == NGX_ERROR) {
        ngx_mail_proxy_upstream_error(s);
        return;
    }

    // 根据当前所处的状态进行不同处理
    switch (s->mail_state) {

    // 构造用户信息发送给邮件服务器
    case ngx_pop3_start:
        ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail proxy send user");

        s->connection->log->action = "sending user name to upstream";

        line.len = sizeof("USER ")  - 1 + s->login.len + 2;
        line.data = ngx_pnalloc(c->pool, line.len);
        if (line.data == NULL) {
            ngx_mail_proxy_internal_server_error(s);
            return;
        }

        p = ngx_cpymem(line.data, "USER ", sizeof("USER ") - 1);
        p = ngx_cpymem(p, s->login.data, s->login.len);
        *p++ = CR; *p = LF;

        s->mail_state = ngx_pop3_user;
        break;

    // 构造用户密码发送给邮件服务器
    case ngx_pop3_user:
        ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail proxy send pass");

        s->connection->log->action = "sending password to upstream";

        line.len = sizeof("PASS ")  - 1 + s->passwd.len + 2;
        line.data = ngx_pnalloc(c->pool, line.len);
        if (line.data == NULL) {
            ngx_mail_proxy_internal_server_error(s);
            return;
        }

        p = ngx_cpymem(line.data, "PASS ", sizeof("PASS ") - 1);
        p = ngx_cpymem(p, s->passwd.data, s->passwd.len);
        *p++ = CR; *p = LF;

        s->mail_state = ngx_pop3_passwd;
        break;

    // 与邮件服务器的用户名、密码认证通过
    case ngx_pop3_passwd:
        // 将 Nginx 与上游、下游连接的读写事件回调全部设置为 ngx_mail_proxy_handler
        s->connection->read->handler = ngx_mail_proxy_handler;
        s->connection->write->handler = ngx_mail_proxy_handler;
        rev->handler = ngx_mail_proxy_handler;
        c->write->handler = ngx_mail_proxy_handler;

        pcf = ngx_mail_get_module_srv_conf(s, ngx_mail_proxy_module);
        ngx_add_timer(s->connection->read, pcf->timeout);
        ngx_del_timer(c->read);

        c->log->action = NULL;
        ngx_log_error(NGX_LOG_INFO, c->log, 0, "client logged in");

        // 对下游连接的写事件调用回调函数,开始透传
        ngx_mail_proxy_handler(s->connection->write);

        return;

    default:
#if (NGX_SUPPRESS_WARN)
        ngx_str_null(&line);
#endif
        break;
    }

    // 向上游邮件服务器发送验证消息
    // 当前函数是 Nginx 与上游服务器连接的读事件回调函数
    // 因此发送验证消息不由事件驱动模块触发,而是由上游连接的读事件触发
    // 因为只有接收到了邮件服务器的消息,才会向邮件服务器发送消息,且发送的消息非常短小
    if (c->send(c, line.data, line.len) < (ssize_t) line.len) {
        /*
         * we treat the incomplete sending as NGX_ERROR
         * because it is very strange here
         */
        ngx_mail_proxy_internal_server_error(s);
        return;
    }

    // 设置 buffer 缓冲区指针 (相当于清空缓冲区)
    s->proxy->buffer->pos = s->proxy->buffer->start;
    s->proxy->buffer->last = s->proxy->buffer->start;
}

13.7 透传上游邮件服务器与客户端间的流

上面函数中,在开始透传后,会将 Nginx 与上下游 TCP 连接的读写事件回调函数都设置为 ngx_mail_proxy_handler()。这个函数将同时处理四个事件,实现透传。这里使用了 固定大小的缓存 来实现透传功能。

其中,ngx_mail_session_t 中的 buffer 缓冲区用于从下游客户端到上游服务器的透传;而 ngx_mail_proxy_ctx_t 中的 buffer 缓冲区用于从上游服务器透传到下游客户端。在这两个 ngx_buf_t 类型的缓冲区中,pos 指向待转发消息的起始位置,last 指向消息的末尾 - 当 pos 与 last 相等时,全部缓存消息就发送完了。

static void
ngx_mail_proxy_handler(ngx_event_t *ev)
{
    char                   *action, *recv_action, *send_action;
    size_t                  size;
    ssize_t                 n;
    ngx_buf_t              *b;
    ngx_uint_t              do_write;
    // src 和 dst 指针用于代码重用
    // 不管谁是上游、下游,src 指向转发消息的来源,dst 指向转发消息的目标
    ngx_connection_t       *c, *src, *dst;
    ngx_mail_session_t     *s;
    ngx_mail_proxy_conf_t  *pcf;

    // 连接 (可能是上游也可能是下游)
    c = ev->data;
    // 指向 ngx_mail_session_t 结构体 (无论上游下游)
    s = c->data;

    // 事件超时,终止透传
    if (ev->timedout || c->close) {
        c->log->action = "proxying";

        if (c->close) {
            ngx_log_error(NGX_LOG_INFO, c->log, 0, "shutdown timeout");

        } else if (c == s->connection) {
            ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
                          "client timed out");
            c->timedout = 1;

        } else {
            ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
                          "upstream timed out");
        }

        ngx_mail_proxy_close_session(s);
        return;
    }

    // s->connection 一定指向 Nginx 与下游的 TCP 连接
    if (c == s->connection) {
        // 收到了下游连接上的事件

        if (ev->write) {
            // 下游可写事件
            recv_action = "proxying and reading from upstream";
            send_action = "proxying and sending to client";
            // src 为上游连接
            src = s->proxy->upstream.connection;
            // dst 为下游连接
            dst = c;
            // 设置用于向下游转发的缓冲区
            b = s->proxy->buffer;

        } else {
            // 下游可读事件
            recv_action = "proxying and reading from client";
            send_action = "proxying and sending to upstream";
            // src 为下游连接
            src = c;
            // dst 为上游连接
            dst = s->proxy->upstream.connection;
            // 设置用于向上游转发的缓冲区
            b = s->buffer;
        }

    } else {
        // 收到了上游连接上的事件

        if (ev->write) {
            // 上游连接可写
            recv_action = "proxying and reading from client";
            send_action = "proxying and sending to upstream";
            // src 为下游连接
            src = s->connection;
            // dst 为上游连接
            dst = c;
            // 设置向上游转发的缓冲区
            b = s->buffer;

        } else {
            // 上游连接可读
            recv_action = "proxying and reading from upstream";
            send_action = "proxying and sending to client";
            // src 为上游连接
            src = c;
            // dst 为下游连接
            dst = s->connection;
            // 设置向下游转发的缓冲区
            b = s->proxy->buffer;
        }
    }

    // 决定本轮是读还是写
    do_write = ev->write ? 1 : 0;

    ngx_log_debug3(NGX_LOG_DEBUG_MAIL, ev->log, 0,
                   "mail proxy handler: %ui, #%d > #%d",
                   do_write, src->fd, dst->fd);

    // 进入循环
    for ( ;; ) {

        // 本轮是写操作
        if (do_write) {

            // 计算缓冲区中要写的字节数
            size = b->last - b->pos;

            // 如果有字节要写,且写事件就绪
            if (size && dst->write->ready) {
                c->log->action = send_action;

                // 发送字节
                n = dst->send(dst, b->pos, size);

                if (n == NGX_ERROR) {
                    ngx_mail_proxy_close_session(s);
                    return;
                }

                // 如果有字节成功发送
                if (n > 0) {
                    // 更新缓冲区指针
                    b->pos += n;

                    // 缓冲区中的内容被完全发送,则清空缓冲区,复用
                    if (b->pos == b->last) {
                        b->pos = b->start;
                        b->last = b->start;
                    }
                }
            }
        }

        // 计算缓冲区中的剩余空间
        size = b->end - b->last;

        // 如果还有剩余空间,且读事件就绪
        if (size && src->read->ready) {
            c->log->action = recv_action;

            // 从读事件中获取字节
            n = src->recv(src, b->last, size);

            // 没有读取到内容,则跳出
            if (n == NGX_AGAIN || n == 0) {
                break;
            }

            // 如果读取到内容
            if (n > 0) {
                // 试图直接发送读取到的内容
                do_write = 1;
                // 更新缓冲区指针
                b->last += n;

                continue;
            }

            if (n == NGX_ERROR) {
                src->read->eof = 1;
            }
        }

        break;
    }

    c->log->action = "proxying";

    // 如果上下游连接中断,则结束透传
    if ((s->connection->read->eof && s->buffer->pos == s->buffer->last)
        || (s->proxy->upstream.connection->read->eof
            && s->proxy->buffer->pos == s->proxy->buffer->last)
        || (s->connection->read->eof
            && s->proxy->upstream.connection->read->eof))
    {
        action = c->log->action;
        c->log->action = NULL;
        ngx_log_error(NGX_LOG_INFO, c->log, 0, "proxied session done");
        c->log->action = action;

        ngx_mail_proxy_close_session(s);
        return;
    }

    // 将四个事件再次加入到事件驱动模块中

    if (ngx_handle_write_event(dst->write, 0) != NGX_OK) {
        ngx_mail_proxy_close_session(s);
        return;
    }

    if (ngx_handle_read_event(dst->read, 0) != NGX_OK) {
        ngx_mail_proxy_close_session(s);
        return;
    }

    if (ngx_handle_write_event(src->write, 0) != NGX_OK) {
        ngx_mail_proxy_close_session(s);
        return;
    }

    if (ngx_handle_read_event(src->read, 0) != NGX_OK) {
        ngx_mail_proxy_close_session(s);
        return;
    }

    // 将下游客户端的读事件添加到定时器中
    // 防止僵死的客户端占用 Nginx 服务器资源
    if (c == s->connection) {
        pcf = ngx_mail_get_module_srv_conf(s, ngx_mail_proxy_module);
        ngx_add_timer(c->read, pcf->timeout);
    }
}
Edit this page on GitHub
Prev
Chapter 13.1-13.5 - 邮件代理模块 - 认证服务器
Next
Chapter 14 - 进程间通信机制