随笔-90  评论-224  文章-23  trackbacks-0
 
描述
   原始套接字具有广泛的用途,特别是用于自定义协议(标准协议TCP、UDP和ICMP等外)的数据收发。在Linux下拦截套接字IO的一般方法是拦截对应的套接字系统调用,对于发送为sendmsg和sendto,对于接收为recvmsg和recvfrom。这种方法虽然也能拦截原始套接字IO,但要先判断套接字的类型,如果为SOCK_RAW(原始套接字类型),那么进行拦截处理,这样一来由于每次IO都要判断套接字类型,性能就比较低了。因此为了直接针对原始套接字来拦截,提高性能,发明了本方法。
   本方法可用于防火墙或主机防护系统中,丢弃接收和发送的攻击或病毒数据包。

特点
   运行在内核态,直接拦截所有进程的原始套接字IO,支持IPv4和IPv6。


实现
   原理
      在Linux内核网络子系统中,struct proto_ops结构提供了协议无关的套接字层到协议相关的传输层的转接,而IPv4协议族中内置的inet_sockraw_ops为它的一个实例,对应着原始套接字。因此先找到inet_sockraw_ops,再替换它的成员函数指针recvmsg和sendmsg,就可以实现拦截了。下面以IPv4为例(IPv6同理),说明几个流程。

   搜索inet_sockraw_ops
      该流程在挂钩IO前进行。由于inet_sockraw_ops为Linux内核未导出的内部符号,因此需要通过特别的方法找到它,该特别的方法基于这样的一个事实:
       所有原始套接字接口均存放在以SOCK_RAW为索引的双向循环链表中,而inet_sockraw_ops就在该链表的末尾。
       内核提供了注册套接字接口的API inet_register_protosw,对于原始套接字类型,该API将输入的套接字接口插入到链表头后面。
      算法如下
      
      注册p前或注销p后,链表如下
      注册p后,链表如下

   挂钩IO
      该流程在内核模块启动时进行。

   卸钩IO
      该流程在内核模块退出时进行。


运行部署

   该方法实现在Linux内核模块中,为了防止其它内核模块可能也注册了原始套接字接口,因此需要在操作系统启动时优先加载。  
posted @ 2016-07-14 10:27 春秋十二月 阅读(1491) | 评论 (3)编辑 收藏
描述
   TCP连接跟踪是网络流控和防火墙中的一项重要的基础技术,当运用于主机时,连接必与进程相关联,要么是主动发出的,要么是被动接受的,当后代进程被动态创建时,由于文件描述符的继承,一个连接就会被这个进程树中的所有进程共享;当一个进程发出或接受多个连接时,就拥有了多个连接。本方法可用于网络安全产品中,监控TCP连接及所属进程,能准确并动态地知道一个连接被哪些进程共享,一个进程拥有哪些连接。

特点

   操作系统自带的netstat工具只是关联到了一个根进程,无法看到拥有该连接的所有进程,查看进程拥有的全部连接也不方便。该方法的特点是实时跟踪、查看连接与进程相关信息方便、支持连接的管控。


实现

   本方法通过内核安全的十字链表实现了连接与进程的相关性,连接信息结构体含有一个所属进程链表头,进程信息结构体含有一个拥有连接链表头,通过十字链表结点链接,x方向链接到进程的连接链表,y方向链接到连接的进程链表,如下图所示
   进程1为根进程,进程2,...,进程n为进程1的后代进程;连接1,连接2,...,连接n为进程1产生的连接。node(x,y)为十字链表结点,用于关联连接与进程,x对应进程编号,y对应连接编号,每个node包含了所属的连接和进程指针,每行和每列都是一个双向循环链表(循环未画出),每个链表用一个自旋锁同步操作。
   动态跟踪的过程包括4个方面:进程创建、进程退出、连接产生、连接销毁。在Linux下,可通过拦截内核函数do_fork挂钩进程创建,拦截do_exit挂钩进程退出;可通过拦截inet_stream_ops的成员函数connect和accept挂钩连接产生,拦截成员函数release挂钩连接销毁。下面为4个方面对应的流程图,由于所有外层加锁前已禁止本地中断和内核抢占,因此内层加锁前就不必再禁止本地中断和内核抢占了。

   进程创建
   将copy_node插入到c的进程链表末尾,即为y方向增加(下同);插入到p的连接链表末尾,为x方向增加(下同)。

   进程退出
   从c的进程链表中移除node,即为y方向移除(下同);再从p的连接链表中移除node,即为x方向移除(下同)。


   连接产生
      当进程发出连接或接受连接时,调用此流程。

   连接销毁
      当某个进程销毁连接时,调用此流程。
posted @ 2016-07-13 11:24 春秋十二月 阅读(1089) | 评论 (0)编辑 收藏
描述
   在P2P应用系统中,当需要与处于不同私网的对方可靠通信时,先尝试TCP打洞穿透,若穿透失败,再通过处于公网上的代理服务器(下文简称proxy)转发与对方通信,如下图所示  
   终端A先连接并发消息msg1到proxy(连接为c1);proxy再发消息msg2给P2P服务器,P2P服务器收到消息后通过已有的连接发消息msg3给终端B,B收到msg3后,连接并发消息msg4到proxy(连接为c2)。这4个消息格式由应用层协议决定,但必须遵守的规范如下:
    msg1至少包含B的设备ID。
    msg2至少包含B的设备ID和c1的连接ID。
    msg3至少包含c1的连接ID和连接代理指示。
    msg4至少包含B的设备ID和c1的连接ID。
   proxy为多线程架构,当接受到若干连接时,如果数据相互转发的两个连接(比如上图中的c1和c2)不在同一线程,由于一个连接写数据到另一连接的发送队列,而另一连接从发送队列读数据以发送,那么就要对另一连接的发送队列(或缓冲区)加锁,这样一来由于频繁的网络IO而频繁地加解锁,降低了转发效率,因此为了解决这一问题,就需要调度TCP连接到同一线程。


特点
   本方法能将数据转发的两边连接放在同一线程,从而避免了数据转发时的加锁,由于是一对一的连接匹配,因此也做到了每个线程中连接数的均衡。


实现
   工作原理
      proxy的主线程负责绑定知名端口并监听连接,工作线程负责数据转发。当接受到一个连接时,按轮转法调度它到某个工作线程,在那个线程内接收分析应用层协议数据,以识别连接类型,即判断连接是来自数据请求方还是数据响应方,为统一描述,这里把前者特称为客户连接,后者为服务连接,连接所在的线程为宿主线程。如果是客户连接,那么先通知P2P服务器请求对应的客户端来连接代理,并等待匹配;如果是服务连接,那么就去匹配客户连接,在匹配过程中,如果服务连接和客户连接不在同一线程内,那么就会调度到客户连接的宿主线程,匹配成功后,就开始转发数据。
      为了加快查找,分2个hash表存放连接,客户连接放在客户连接hash表中,以连接ID为键值;服务连接放在服务连接hash表中,以设备ID为键值。

   TCP连接调度
      包括新连接的轮转、识别连接类型、匹配客户连接1、匹配客户连接2和关闭连接共5个流程,如下一一所述。
      新连接的轮转
         该流程工作在主线程,如下图所示
      索引i初始为0,对于新来的连接,由于还不明确连接类型,所以先放入客户连接表中。

      识别连接类型
         该流程工作在工作线程,当接受到一个连接时开始执行,如下图所示
      当连接类型为服务连接时,从客户连接表转移到服务连接表。

      匹配客户连接1
         该流程工作在工作线程,当接受到一个服务连接时开始执行,如下图所示

      匹配客户连接2
         该流程工作在工作线程,当服务连接移到客户连接的宿主线程时开始执行,如下图所示
      这里的流程和匹配客户连接流程1有些类似,看起来好像做了重复的判断操作,但这是必要的,因为在服务连接转移到另一线程这个瞬间内,客户连接有可能断开了,也有可能断开后又来了一个相同连接ID的其它客户连接,所以要重新去客户连接表查找一次,然后进行4个分支判断。 
   
      关闭连接
         该流程工作在工作线程内,当连接断开或空闲时执行。当一边读数据出错时,不能马上关闭另一边连接,得在另一边缓冲区数据发送完后才能关闭;当一边连接写数据出错时,可以马上关闭另一边连接,如下图所示
posted @ 2016-07-12 16:59 春秋十二月 阅读(983) | 评论 (0)编辑 收藏
     摘要:    为了使nginx支持windows服务,本文阐述以下主要的改进实现。ngx_main函数   为了在SCM服务中复用main函数的逻辑,将其重命名为ngx_main,并添加第3个参数is_scm以兼容控制台运行方式,声明在core/nginx.h中。    Code highligh...  阅读全文
posted @ 2016-07-12 15:31 春秋十二月 阅读(3900) | 评论 (0)编辑 收藏
结构定义
 1 struct state_machine {
 2     int state;
 3     
 4 };
 5 
 6 enum { 
 7     s1,
 8     s2,
 9     
10     sn
11 };
   假设s1为初始状态,状态变迁为s1->s2->...->sn。

常规实现 
   状态机处理函数state_machine_handle通常在一个循环内或被事件驱动框架调用,输入data会随时序变化,从而引起状态的变迁,伪代码框架如下。
 1void handle_s1(struct state_machine *sm, void *data)
 2{
 3    //do something about state 1
 4    if(is_satisfy_s2(data))
 5        sm->state = s2;
 6}

 7
 8void handle_s2(struct state_machine *sm, void *data)
 9{
10    //do something about state 2
11    if(is_satisfy_s3(data))
12        sm->state = s3;
13}

14
15void handle_sn_1(struct state_machine *sm, void *data)
16{
17    //do something about state n-1
18    if(is_satisfy_sn(data))
19        sm->state = sn;
20}

21
22void state_machine_handle(struct state_machine *sm, void *data)
23{
24    switch(sm->state){
25        case s1:
26            handle_s1(sm,data);
27            break;
28            
29        case s2:
30            handle_s2(sm,data);
31            break;            
32            
33        case sn:
34            handle_sn(sm,data);
35            break;
36    }

37}
   sm->state初始化为s1。

改进实现
   为了免去丑陋的switch case分支结构,在state_machine内用成员函数指针handler替代了state,改进后的框架如下。
 1struct state_machine;
 2typedef void (*state_handler)(struct state_machine*void*);
 3
 4struct state_machine {
 5    state_handler handler;
 6    
 7}
;
 8
 9void handle_s1(struct state_machine *sm, void *data)
10{
11    //do something about state 1
12    if(is_satisfy_s2(data))
13        sm->handler = handle_s2;
14}

15
16void handle_s2(struct state_machine *sm, void *data)
17{
18    //do something about state 2
19    if(is_satisfy_s3(data))
20        sm->handler = handle_s3;
21}

22
23void handle_sn_1(struct state_machine *sm, void *data)
24{
25    //do something about state n-1
26    if(is_satisfy_sn(data))
27        sm->handler = handle_sn;
28}

29
30void state_machine_handle(struct state_machine *sm, void *data)
31{
32    sm->handler(sm, data);
33}
   sm->handler初始化为handle_s1,该方法在性能上应略优于常规方法,而且逻辑更清晰自然,非常适合于网络流的处理,在nginx中分析http和email协议时,得到了广泛应用。
posted @ 2016-05-05 09:46 春秋十二月 阅读(2659) | 评论 (1)编辑 收藏
脚本概述
   由于使用objdump反汇编linux内核的输出太多(2.6.32-220.el6.x86_64统计结果为1457706行),而很多时候只是想查看特定部分的机器码与汇编指令,例如函数的入口、堆栈、调用了哪个函数等,为了高效和通用,因此编写了一个简单的awk脚本,其命令行参数说明如下:
   ● SLINE表示匹配的起始行号(不小于1),SPAT表示匹配的起始行模式,这两个只能有一个生效,当都有效时,以SLINE为准。
   ● NUM表示从起始行开始的连续输出行数(不小于1,含起始行),EPAT表示匹配的结束行模式,这两个只能有一个生效,当都有效时,以NUM为准。

脚本实现
   检查传值
   由于向脚本传入的值在BEGIN块内没生效,在动作块{}和END块内有效,但若在{}内进行检查则太低效,因为处理每条记录都要判断,所以为了避免在{}内进行多余的判断,就在BEGIN块内解析命令行参数来间接获得传值,当传值无效时,给出提示并退出。
 1for(k=1;k<ARGC;++k){
 2        str=ARGV[k]
 3        if(1==match(str,"SLINE=")){
 4            SLINE = substr(str,7)
 5        }else if(1==match(str,"SPAT=")){
 6            SPAT = substr(str,6)
 7        }else if(1==match(str,"NUM=")){
 8            NUM = substr(str,5)
 9        }else if(1==match(str,"EPAT=")){
10            EPAT = substr(str,6)
11        }
12    }
13
14  if(SLINE<=0 && SPAT==""){
15      print "Usage: rangeshow must specifies valid SLINE which must be greater than 0, or SPAT which can't be empty"
16      exit 1    
17  }
18
19  if(NUM<=0 && EPAT==""){
20      print "Usage: rangeshow must specifies valid NUM which must be greater than 0, or EPAT which can't be empty"
21    exit 1
22}

   结束处理
   当处理了NUM条记录或匹配了结束行模式时,应退出动作块{}。   
 1if(0==start_nr){ 
 2      
 3}else{
 4    if(NUM>0) {
 5        if(NR<start_nr+NUM) {
 6            ++matched_nr
 7            print $0            
 8        }else
 9            exit 0
10
11    }else{
12        ++matched_nr
13        print $0        
14        if(0!=match($0,EPAT))
15            exit 0
16    }
17}

   完整脚本下载:rangeshow

脚本示例
   查看linux内核第10000行开始的10条指令,如下图
   
   
   查看linux内核函数do_fork入口开始的10条指令,如下图    
   

   查看linux内核第10000行开始到callq的一段指令,如下图
   

   查看linux内核函数do_exit入口到调用profile_task_exit的一段指令,如下图   
   
posted @ 2015-10-27 15:36 春秋十二月 阅读(1167) | 评论 (1)编辑 收藏
   本文根据RFC793协议规范和BSD 4.4的实现,总结了TCP分组丢失时的状态变迁,如下图所示:实线箭头表示客户端的状态变迁,线段虚线箭头表示服务端的状态变迁,圆点虚线箭头表示客户端或服务端的状态变迁;黑色文字表示正常时的行为,红色文字表示分组丢失时的行为。

   这里假设重传时分组依然会丢失,当在不同状态(CLOSED除外)分组丢失后,最终会关闭套接字而回到CLOSED状态。下面逐个分析各状态时的情景。

SYN_SENT
   连接阶段第1次握手,客户端发送的SYN分组丢失,因此超时收不到服务端的SYN+ACK而重传SYN,尝试几次后放弃,关闭套接字。

SYN_RCVD
   1)连接阶段第2次握手,服务端响应的SYN+ACK分组丢失,因此超时收不到客户端的ACK而重传SYN+ACK,尝试几次后放弃,发送RST并关闭套接字。
   2)连接阶段第3次握手,客户端发送的ACK分组丢失,因此服务端超时收不到ACK而重传SYN+ACK,尝试几次后放弃,发送RST并关闭套接字。
   3)同时打开第2次握手,本端响应的SYN+ACK分组丢失,因此对端超时收不到SYN+ACK而重传SYN、尝试几次后放弃、发送RST并关闭套接字,而此时本端收到RST。

ESTABLISHED
   1)连接阶段第3次握手,客户端发送ACK分组后,虽然丢失但会进入该状态(因为ACK不需要确认),但此时服务端还处于SYN_RCVD状态,因为超时收不到客户端的ACK而重传SYN+ACK、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。
   2)数据传输阶段,本端发送的Data分组丢失,因此超时收不到对数据的确认而重传、尝试几次后放弃、发送RST并关闭套接字,而此时对端收到RST。

FIN_WAIT_1
   1)关闭阶段第1次握手,客户端发送的FIN分组丢失,因此超时收不到服务端的ACK而重传FIN,尝试几次后放弃,发送RST并关闭套接字。
   2)关闭阶段第2次握手,服务端响应的ACK分组丢失,因此客户端超时收不到ACK而重传FIN,尝试几次后放弃,发送RST并关闭套接字。
 
FIN_WAIT_2
   关闭阶段第3次握手,服务端发送的FIN分组丢失,因此超时收不到客户端的ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。
 
CLOSING
   同时关闭第2次握手,本端发送的ACK分组丢失,导致对端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时本端收到RST。

TIME_WAIT
   关闭阶段第4次握手,客户端响应的ACK分组丢失,导致服务端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时客户端收到RST。

CLOSE_WAIT
   关闭阶段第2次握手,服务端响应的ACK分组丢失,导致客户端超时收不到ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字,而此时服务端收到RST。

LAST_ACK
   关闭阶段第3次握手,服务端发送的FIN分组丢失,导致超时收不到客户端的ACK而重传FIN、尝试几次后放弃、发送RST并关闭套接字。
posted @ 2015-10-05 00:44 春秋十二月 阅读(2251) | 评论 (1)编辑 收藏
     摘要:    由于linux内核中的struct list_head已经定义了指向前驱的prev指针和指向后继的next指针,并且提供了相关的链表操作方法,因此为方便复用,本文在它的基础上封装实现了一种使用开链法解决冲突的通用内核Hash表glib_htable,提供了初始化、增加、查找、删除、清空和销毁6种操作,除初始化和销毁外,其它操作都做了同步,适用于中断和进程上下文。...  阅读全文
posted @ 2015-09-15 17:18 春秋十二月 阅读(1878) | 评论 (0)编辑 收藏
   nginx的域名解析器使用已连接udp(收发前先调用ngx_udp_connect)发送dns查询、接收dns响应,如上篇tcp异步连接所讲,iocp需要先投递udp的接收操作,才能引发接收完成的事件,因此要对域名解析器和udp异步接收作些改进。

发送后投递
    dns查询由ngx_resolver_send_query函数实现,定义在core/ngx_resolver.c中。
 1static ngx_int_t ngx_resolver_send_query(ngx_resolver_t *r, ngx_resolver_node_t *rn)
 2{
 3    
 4     if (rn->naddrs == (u_short) -1{
 5        n = ngx_send(uc->connection, rn->query, rn->qlen);
 6    
 7  }

 8
 9#if (NGX_HAVE_INET6)
10  if (rn->query6 && rn->naddrs6 == (u_short) -1{
11      n = ngx_send(uc->connection, rn->query6, rn->qlen);
12    
13  }

14#endif
15
16#if (NGX_WIN32) 
17    if (ngx_event_flags & NGX_USE_IOCP_EVENT){
18        uc->connection->read->ready = 1;
19        ngx_resolver_read_response(uc->connection->read);
20}

21#endif
22
23  return NGX_OK;
24}
    当nginx用于代理连接上游服务器前,要先解析域名,首次调用链为:ngx_http_upstream_init_request->ngx_resolver_name->ngx_resolver_name_locked->ngx_resolver_send_query;若5s(单次超时)后还没收到dns响应,则再发送1次查询,调用链为:ngx_resolver_resend_handler->ngx_resolver_resend->ngx_resolver_send_query,如此反复,直到收到响应或30s(默认总超时)后不再发送查询。它调用ngx_send发送dns查询,16行~21行代码为笔者添加,ngx_resolver_read_response函数用于接收并分析dns响应报文,它会调用到下面的ngx_udp_overlapped_wsarecv函数。

异步接收
   由ngx_udp_overlapped_wsarecv函数实现,定义在os/win32/ngx_udp_wsarecv.c中。
 1ssize_t ngx_udp_overlapped_wsarecv(ngx_connection_t *c, u_char *buf, size_t size)
 2{
 3    int             flags, rc;
 4    WSABUF          wsabuf;
 5    ngx_err_t       err;
 6    ngx_event_t    *rev;
 7    WSAOVERLAPPED  *ovlp;
 8    u_long             bytes;
 9    
10    rev = c->read;
11    
12    if (!rev->ready) {
13      ngx_log_error(NGX_LOG_ALERT, c->log, 0"ngx_udp_overlapped_wsarecv second wsa post");
14      return NGX_AGAIN;
15  }

16    
17  if (rev->complete) {
18   if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
19       if (rev->ovlp.error && rev->ovlp.error != ERROR_MORE_DATA{
20           ngx_connection_error(c, rev->ovlp.error, "ngx_udp_overlapped_wsarecv() failed");
21           return NGX_ERROR;
22       }

23   }

24   
25   rev->complete = 0;
26  }

27     
28    ovlp = NULL;
29    wsabuf.buf = (CHAR *) buf;
30    wsabuf.len = (ULONG) size;
31    flags = 0;
32    
33retry:    
34    rc = WSARecv(c->fd, &wsabuf, 1, (DWORD*)&bytes, (LPDWORD)&flags, ovlp, NULL);
35        
36    if (rc == -1{
37         rev->ready = 0;
38         err = ngx_socket_errno;
39    
40       if (err == WSA_IO_PENDING) {
41             return NGX_AGAIN;
42         }

43    
44       if (err == WSAEWOULDBLOCK) {
45           if (ngx_event_flags & NGX_USE_IOCP_EVENT) {                
46                 rev->ovlp.type = NGX_IOCP_IO;
47                 ovlp = (WSAOVERLAPPED *)&rev->ovlp;
48                 ngx_memzero(ovlp, sizeof(WSAOVERLAPPED));
49                
50                 wsabuf.buf = NULL;
51                 wsabuf.len = 0;
52                 flags = MSG_PEEK;
53                
54                goto retry;            
55            }

56            
57            return NGX_AGAIN;            
58        }

59            
60        ngx_connection_error(c, err, "ngx_udp_overlapped_wsarecv() failed");    
61        rev->error = 1;
62    
63        return NGX_ERROR;
64    }

65    
66    if ((ngx_event_flags & NGX_USE_IOCP_EVENT) && ovlp) {
67        rev->ready = 0;
68        return NGX_AGAIN;
69    }

70    
71    return bytes;
72}
   先以非阻塞方式接收,若发生WSAWOULDBLOCK错误,则使用MSG_PEEK标志投递一个0字节的重叠接收操作,当dns响应返回时发生完成事件,会再次进入ngx_resolver_read_response而调用到该函数,此时rev->complete为1,rev->ovlp.error为ERROR_MORE_DATA(GetQueuedCompletionStatus返回的错误),由于使用了MSG_PEEK,因此数据还在接收缓冲区中,要忽略ERROR_MORE_DATA而继续接收,这时就能成功了。不管WSARecv返回WSA_IO_PENDING错误还是成功,iocp都会得到完成通知,所以这里当重叠操作投递成功时,返回NGX_AGAIN,便于在回调内统一处理。
posted @ 2015-06-25 17:01 春秋十二月 阅读(5207) | 评论 (0)编辑 收藏
   iocp是Windows NT操作系统的一种高效IO模型,对应于Linux中的epoll和FreeBSD中的kqueue,nginx对ske(select、kqueue和epoll的首写字母组合)的支持很好,但截止到1.6.2版本,还不支持iocp。由于ske都是反应器模式,即先注册IO事件,当IO事件发生(读写通知)时,在其回调内主动调用API来读或写数据;而iocp是前摄器模式,要先投递IO操作,才能引发IO事件(完成通知)的发生,在其回调内数据已被动由操作系统读或写完成。因此,iocp的特点决定了nginx对它的支持与ske有所不同。通过hg clone http://hg.nginx.org/nginx下载的nginx源代码,虽然实现了iocp事件模块、异步接受连接、部分异步读写,但根本不能正常工作,而且不支持异步连接和SCM服务控制,笔者在参考ske模块的实现基础上,改进支持了如下特性:
      1. 异步接受连接时的负载均衡
      2. 正反向代理的异步连接
      3. 异步聚合读写
      4. 域名解析时的UDP异步接收
      5. 异步文件传输
      6. SCM服务控制
   由于2、4、6均为原创,其它几点的思路皆源于ske模块的实现(只是平台API不同),因此本文先阐述异步连接的实现。为了兼容select事件模块,所有iocp相关的代码使用NGX_HAVE_IOCP宏和(或)NGX_USE_IOCP_EVENT标志包围,其中NGX_HAVE_IOCP宏用于条件编译,在WIN32平台下,定义为1;当选择的事件模块为iocp时,全局变量ngx_event_flags才包含NGX_USE_IOCP_EVENT标志。

异步连接对端
   由ngx_event_connect_peer函数(这里省去了与异步连接无关的代码)实现,定义在event/ngx_event_connect.c中,因为connect不支持异步连接事件的完成通知,所以要使用扩展API ConnectEx。 
 1ngx_int_t ngx_event_connect_peer(ngx_peer_connection_t *pc)
 2{
 3    int                rc;
 4    ngx_int_t          event;
 5    ngx_err_t          err;
 6    ngx_uint_t         level,family;
 7    ngx_socket_t       s;
 8    ngx_event_t       *rev, *wev;
 9    
10    s = ngx_socket(family = pc->sockaddr->sa_family, SOCK_STREAM, 0);
11              
12    #if (NGX_HAVE_IOCP)
13    if((pc->local==NULL||pc->local->sockaddr->sa_family != family) 
14        && (ngx_event_flags & NGX_USE_IOCP_EVENT)){
15        if(ngx_iocp_set_localaddr(pc->log,family,&pc->local) != NGX_OK)
16            goto failed;    
17    }

18    #endif
19       
20    
21    #if (NGX_HAVE_IOCP)
22    if(ngx_event_flags&NGX_USE_IOCP_EVENT){        
23        LPWSAOVERLAPPED   ovlp;
24        ovlp = (LPWSAOVERLAPPED)&wev->ovlp;
25        ngx_memzero(ovlp,sizeof(WSAOVERLAPPED));
26        wev->ovlp.type = NGX_IOCP_CONNECT;
27        rc = ngx_connectex(s,pc->sockaddr,pc->socklen,NULL,0,NULL,ovlp) ? 0 : -1;
28    
29    }
else
30        rc = connect(s, pc->sockaddr, pc->socklen);
31    #else
32      rc = connect(s, pc->sockaddr, pc->socklen);
33    #endif
34    
35    if (rc == -1{
36        err = ngx_socket_errno;
37        if (err != NGX_EINPROGRESS
38    #if (NGX_WIN32)
39        /* Winsock returns WSAEWOULDBLOCK (NGX_EAGAIN) */
40        && err != NGX_EAGAIN
41    #if (NGX_HAVE_IOCP)
42        && err != WSA_IO_PENDING
43    #endif
44    #endif
45            ){
46            
47            ngx_log_error(level, c->log, err, "connect() to %V failed", pc->name);
48            ngx_close_connection(c);
49            pc->connection = NULL;
50          
51            return NGX_DECLINED;
52        }

53    }

54    
55}
   调用ConnectEx前要先bind本地地址,不然发生WSAEINVAL错误;由于域名解析可能返回IPv6记录,导致创建本地套接字的地址族为AF_INET6,因此bind时需要匹配IPv6地址,不然发生WSAEFAULT错误,导致nginx返回Internal Server Error错误给前端,因此绑定前要调用ngx_iocp_set_localaddr设定正确的本地地址,当且仅当pc->local为空或地址族不匹配时。

本地初始化与设定
   支持IPv6,实现在event/modules/ngx_iocp_module.c。
   地址变量定义如下。
1static struct sockaddr_in  sin;
2#if (NGX_HAVE_INET6)
3static struct sockaddr_in6  sin6;
4#endif
5static ngx_addr_t           local_addr;
   sin对应IPv4,sin6对应IPv6,作为bind的套接字本地地址。

   sin和sin6在启动iocp事件模块时调用ngx_iocp_init初始化。   
 1static ngx_int_t ngx_iocp_init(ngx_cycle_t *cycle, ngx_msec_t timer)
 2{
 3    
 4  sin.sin_family = AF_INET;
 5  sin.sin_port = 0;
 6  sin.sin_addr.s_addr = INADDR_ANY;
 7    
 8#if (NGX_HAVE_INET6)
 9    sin6.sin6_family = AF_INET6;
10    sin6.sin6_port = 0;
11    sin6.sin6_addr = in6addr_any;
12#endif
13    
14  local_addr.name.len = sizeof("INADDR_ANY"- 1;
15  local_addr.name.data = (u_char *)"INADDR_ANY";    
16    
17}
   不论IP地址或端口,都指定为0,表示由系统自动分配出口IP地址和未占用的端口。

   本地设定由ngx_iocp_set_localaddr实现。
 1ngx_int_t ngx_iocp_set_localaddr(ngx_log_t *log, in_port_t family, ngx_addr_t **local)
 2{
 3    struct sockaddr *sa;
 4    socklen_t len;
 5    
 6    if(AF_INET == family){            
 7        sa = &sin;
 8        len = sizeof(struct sockaddr_in);        
 9    }

10#if (NGX_HAVE_INET6)
11    else if(AF_INET6 == family){
12        sa = &sin6;
13        len = sizeof(struct sockaddr_in6);
14    }

15#endif
16    else{
17        ngx_log_error(NGX_LOG_ALERT, log, 0"not supported address family");
18        return NGX_ERROR;        
19    }

20
21    local_addr.sockaddr = sa;
22    local_addr.socklen = len;
23    *local = &local_addr;
24
25    return NGX_OK;
26}
   对于除IPv4和IPv6外的协议族,则记录一个错误日志。必要时也可扩展支持其它的协议族,例如NetBIOS(对应地址族为AF_NETBIOS),但要看ConnectEx是否支持。
posted @ 2015-06-24 17:02 春秋十二月 阅读(5973) | 评论 (1)编辑 收藏
仅列出标题
共9页: 1 2 3 4 5 6 7 8 9