13.7 透传上游邮件服务器与客户端间的流
ngx_mail_proxy_handler方法同时负责处理上、下游间的4个事件(2个读事件、2个写事件)。该方法将完全实现上、下游邮件协议之间的透传,本节将通过直接研究这个方法来看看如何用固定大小的缓存实现透传功能(有些类似于upstream机制转发响应时仅用了固定缓存的模式,但upstream机制只是单向的转发,而透传则是双向的转发)。
下面先来介绍双向转发TCP流时将会用到的两个缓冲区:ngx_mail_session_t中的buffer缓冲区用于转发下游客户端的消息给上游的邮件服务器,而ngx_mail_proxy_ctx_t中的buffer缓冲区则用于转发上游邮件服务器的消息给下游的客户端。在这两个ngx_buf_t类型的缓冲区中,pos指针指向待转发消息的起始地址,而last指针指向最后一次接收到的消息的末尾。当pos等于last时,意味着全部缓存消息都转发完了,这时会把pos和last都指向缓冲区的首部start指针,相当于清空缓冲区以便再次复用完整的缓冲区。相关代码如下所示。
static void ngx_mail_proxy_handler(ngx_event_t*ev)
{
//当前的动作,用于记录日志
charaction,recv_action,*send_action;
/size变量具有两种含义:在读取消息时,size表示recv方法中空闲缓冲区的大小,在发送消息时,表示将要发送的消息长度/
size_t size;
//send或者recv方法的返回值
ssize_t n;
//b表示用于接收TCP消息的缓冲区,或者指向用于发送消息的缓冲区
ngx_buf_t*b;
//do_write标志位决定本次到底是发送还是接收TCP消息
ngx_uint_t do_write;
/每次透传TCP,上、下游的客户端与邮件服务器之间必然有一个负责提供消息,另一个负责接收Nginx转发的消息。src用来表示Nginx与提供消息一方之间的连接,而dst表示Nginx与接收消息一方之间的连接/
ngx_connection_tc,src,*dst;
ngx_mail_session_t*s;
ngx_mail_proxy_conf_t*pcf;
/注意,事件ev既可能属于Nginx与下游间的连接,也有可能属于Nginx与上游间的连接,此时无法判断连接c究竟是来自于上游还是下游/
c=ev->data;
/无论是在上游连接还是下游连接上,ngx_connection_t结构体的data成员都将指向ngx_mail_session_t结构体/
s=c->data;
//无论上、下游,只要接收或者发送消息出现了超时,都需要终止透传操作
if(ev->timedout){
……
//ngx_mail_proxy_close_session方法会同时关闭上、下游的TCP连接
ngx_mail_proxy_close_session(s);
return;
}
/*注意,ngx_mail_session_t结构体中的connection成员一定指向Nginx与下游客户端间的TCP
连接*/
if(c==s->connection){
//以下分支意味着收到了下游连接上的事件(无论是可读事件还是可写事件)
if(ev->write){
/当下游可写事件被触发时,意味着本次Nginx将负责把接收自上游的缓存消息发送给下游/
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{
/当下游可读事件被触发时,意味着本次Nginx将负责先读取下游的响应到缓存中,为下一步转发给上游做准备/
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){
/当上游可写事件被触发时,意味着本次Nginx将负责把接收自下游的缓存消息发送给上游/
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{
/当上游可读事件被触发时,意味着本次Nginx将负责接收上游的消息到缓存中,为下一步把这个消息转发给下游做准备/
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;
}
}
//当前触发事件的write标志位将决定do_write本次是发送消息还是接收消息
do_write=ev->write?1:0;
/进入向dst连接发送消息或者由src连接上接收消息的循环,直到套接字上暂无可读或可写事件时才退出/
for(;){
if(do_write){
//如果本次将发送TCP消息,那么首先计算出要发送的消息长度
size=b->last-b->pos;
//检查需要发送的消息长度size是否大于0,以及目标连接当前是否可写
if(size&&dst->write->ready){
c->log->action=send_action;
//调用send方法向dst目标连接上发送TCP消息
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;
}
}
}
}
//为下面读取TCP消息做准备,先计算接收缓冲区上的空闲空间大小
size=b->end-b->last;
//检查空闲缓冲区大小是否大于0,以及源连接上当前是否可读
if(size&&src->read->ready){
c->log->action=recv_action;
//调用recv方法由src源连接上接收TCP消息
n=src->recv(src,b->last,size);
//如果没有读取到内容,或者对方主动关闭了TCP连接,则跳出循环
if(n==NGX_AGAIN||n==0){
break;
}
if(n>0){
//如果读取到了消息,则应试图在本次ngx_mail_proxy_handler方法的执行中将它
立即发送出去*/
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;
c->log->action=action;
ngx_mail_proxy_close_session(s);
return;
}
//下面将会把Nginx与上、下游TCP连接上的4个读/写事件再次添加到epoll中监控
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);
}
}
可以看到,ngx_mail_proxy_handler方法很简单,不过百行代码就完成了透传功能。至于在这一阶段中客户端究竟与邮件服务器交换了哪些消息,作为邮件代理服务器Nginx并不关心。这个阶段将会一直持续下去,直到客户端或者邮件服务器有一方关闭了TCP连接,或者发送、接收TCP消息达到了超时时间的限制为止。