SER研究笔记

导读:
  我们项目组现在的测试环境就是用SER作为SIP Proxy。SER以C写成,据说其性能比其他任何SIP Proxy都好,并且是高度可配置的。
  默认配置下,SER在响应中加入了类似如下的头部:
  Server: Sip EXpress router (0.9.6 (i386/linux))
  后来,从ekiga.net(Ekiga是FedoraCore4及以上附带的开源软电话,而ekiga.net是其默认SIP Proxy的FQDN)返回的SIP响应看出它也是部署了SER。可见SER的使用范围比较广。
  [附注]几种开源SIP协议栈:5种SIP协议栈各有千秋,OPAL有发展潜力,VOCAL比较完善,sipX兼容性好,ReSIProcate教稳定,oSIP小巧而快速。所以要根据应用的不同选择恰当的协议栈进行研究开发。Fedora Core附带的Ekiga(前身是GnomeMeeting)基于OPAL。
  研究重点:
  (1)TCP/UDP进程结构 *
  (2)SIP解析与SIP报头的增/删/改 *
  (3)Stateful proxy如何做到的(tm模块) *
  (4)FIFO(共享内存没必要研究了) *
  (5)mysql数据库编程
  (6)registrar模块处理注册
  (7)usrloc模块用户定位
  1. 工程结构
  研究一个软件,首先要弄清楚其工程结构。SER源码根目录下的所有的代码用于编译出可执行文件ser,modules目录下的各个目录用于编译出各个模块(每个模块都是一个实现特定功能的动态链接库,例如提供认证的acc.so)。make install能将编译出的所有东西打包成一个tar.gz,将其在目标机的根路径下解压缩就完成了SER的安装。SER安装后布局:可执行文件ser用于启动服务,文件ser.cfg对ser服务装载的模块以及采用的路由规则等做了配置,各个模块都位于系统中适当位置供ser装载和使用。
  SER的Makefile包含了如下几个makefile:
  (1)Makefiles.defs
  定义了一些重要变量供其他makefile使用。最重要的是NAME,这是编译出的Executable的名字,默认为ser。
  (2)Makefile.sources
  定义了变量sources包括[1]配置文件语法和词法文件对应的C文件;[2]以下目录下所有C文件:SER根目录, mem, parser, parser/digest, parser/contact, db。定义了objs就是将sources由C文件列表转换成.o文件列表。注意:main()函数位于SER根目录的main.c。
  (3)Makefile.rules
  定义了NAME到objs的规则。由此可以看出:SER源码根目录下的所有的代码用于编译出可执行文件ser。
  SER的Makefile还直接给出了所有modules的编译规则。
  2. 理解配置文件
  SER的配置灵活性是其显著优点之一。SER用编译技术做到了配置的极大灵活性。ser.cfg乍看有点像C语法。cfg.y和cfg.lex分别是配置文件的语法和词法文件。ser.cfg中的路由规则针对SIP Request;而所有SIP Reply都是直接转发的(receive_msg解析并处理一个SIP消息,见其对函数forward_reply()的调用)。
  2.1 语句后的分号
  按照cfg.y对配置文法的定义,
  route_stm: ROUTE LBRACE actions RBRACE { push($3, &rlist[DEFAULT_RT]); }
  | ROUTE LBRACK NUMBER RBRACK LBRACE actions RBRACE {
  if (($3 =0)){
  push($6, &rlist[$3]);
  }else{
  yyerror("invalid routing "
  "table number");
  YYABORT; }
  }
  | ROUTE error { yyerror("invalid route statement"); }
  ;
  stm: cmd
  | if_cmd
  | LBRACE actions RBRACE
  ;
  actions: actions action {$$=append_action($1, $2); }
  | action {$$=$1;}
  | actions error { $$=0; yyerror("bad command"); }
  ;
  action: cmd SEMICOLON
  | if_cmd
  | SEMICOLON /* null action */
  | cmd error
  ;
  if_cmd: IF exp stm
  | IF exp stm ELSE stm
  ;
  这导致如下实际上被认为是两个action:
  if(!t_check_msg())
  break;
  第一个是if_cmd,第二个是SEMICOLON。但由于actions第一规则的动作append_action,在action为SEMICOLON时被忽略。
  C语法如何处理作为语句间隔的分号?
  2.2 解决神秘的移进/规约冲突(shift/reduce conflict)
  SER 0.9.4版本的cfg.y存在一个移进/规约冲突。以bison的"-v"选项生成状态机描述文件(《Lex与Yacc》很好地描述了如何理解此文件)。
  [kenny@kenny ser-0.9.4]$ bison -v cfg.y
  cfg.y: conflicts: 1 shift/reduce
  查看状态机描述文件cfg.output可知如下文法片段存在典型的“if-then-else”冲突:
  stm: cmd
  | if_cmd
  | LBRACE actions RBRACE
  ;
  if_cmd: IF exp stm
  | IF exp stm ELSE stm
  ;
  以下这段话摘自Bison info手册:
  “Bison被设计成选择移进来解决这些冲突, 除非有其它的操作符优先级的指导。冲突存在的原因是由于语法本身有歧义: 任一种简单的if语句嵌套的分析都是合法的. 已经建立的惯例是通过将else从句依附到最里面的if语句来解决歧义; 这就是Bison为什么选择移进而不是归约的原因。”
  有以下几个解决此冲突的办法(参考Bison info手册以及《Lex与Yacc》):
  (1)改写if语句语法,优点是彻底,缺点是导致语法复杂化。
  (2)为冲突的两规则指定优先级以隐藏这个你知道并理解的冲突,但是特别要注意不要隐藏其他任何冲突。
  我按照办法2这样做:
  在cfg.y序幕部分加如下两个的操作符:
  %nonassoc LOWER_THAN_ELSE
  %nonassoc ELSE
  修改不带else分支的那个规则,使其优先级低于带else分支的那个规则:
  if_cmd: IF exp stm %prec LOWER_THAN_ELSE
  | IF exp stm ELSE stm
  ;
  (3)为了避免Bison警告那些可以预见的合法的移进/归约冲突, 我们可以使用%expect n声明. 当移进/归约冲突的数目恰好是n的时候, Bison不会做出任何警告。由于Bison默认的移进动作正是所期望的,所以此法可行。
  2.3 消息的标记
  struct sip_msg{
  ...
  flag_t flags;
  }
  flags字段允许在消息上设置多个标记,可被用于简单的模块间通信或记住已到的处理状态。BII做的配置文件中有“setflag(3);”之类的命令,是由函数setflag()实现的。flags的各个比特的含义没有文档描述,
  配置文件中设置怎样的标记值才能使SER内部正确处理这个SIP报文?这个只能是研究各个module之后才能知道!仔细查看所有调用函数isflagset()的地方!由于core和各模块对标记的使用并不一致,要尽量避免借助神秘的标记值来做通信!
  parser/msg_parser.h:
  #define FL_FORCE_RPORT 1 /* force rport */
  #define FL_FORCE_ACTIVE 2 /* force active SDP */
  #define FL_SDP_IP_AFS 4 /* SDP IP rewritten */
  #define FL_SDP_PORT_AFS 8 /* SDP port rewritten */
  modules/usrloc/ucontact.h:
  typedef enum flags {
  FL_NONE = 0, /* No flags set */
  FL_NAT = 1 <<0, /* Contact is behind NAT */
  FL_INVITE = 1 <<1, /* Contact supports INVITE and related methods */
  FL_N_INVITE = 1 <<2, /* Contact doesn't support INVITE and related methods */
  FL_MESSAGE = 1 <<3, /* Contact supports MESSAGE */
  FL_N_MESSAGE = 1 <<4, /* Contact doesn't support MESSAGE */
  FL_SUBSCRIBE = 1 <<5, /* Contact supports SUBSCRIBE and NOTIFY */
  FL_N_SUBSCRIBE = 1 <<6, /* Contact doesn't support SUBSCRIBE and NOTIFY */
  FL_PERMANENT = 1 <<7, /* Permanent contact (does not expire) */
  FL_MEM = 1 <<8, /* Update memory only -- used for REGISTER replication */
  FL_ALL = 0xFFFFFFFF /* All flags set */
  } flags_t;
  2.4 break命令
  配置文件中的break生成了DROP_T类型的一个action,在函数run_actions()运行一个actions列表过程中跳过剩下的actions。注意,这与C语言的关键字“break”含义有些相似,但有明显的差别:C的break/continue用于打断循环/跳过剩余语句继续循环。SER配置文件没有循环逻辑。配置文件的break用于跳过一个语句块的剩余语句,并且,如果是在第一层break且某个模块注册了onbreak handlers则调用这些handlers。
  函数do_action()执行一个action。函数run_actions()执行一系列actions。这两个函数相互嵌套:一方面,run_actions()对各个action逐个调用do_action();另一方面,do_action()在处理if/route之类语句对应的action时调用了run_actions()。函数run_actions()中的局部静态变量rec_lev记录了嵌套的深度,main route自身深度为0,main route层的actions深度为1,这些actions的下一层actions深度为2,等等。
  [疑问1]SER生成的ser.cfg在main route的“if(uri==myself)”分支内出现了"if(!uri==myself)"。为什么?
  lookup()函数可能改写RequestURI:“Lookup contact in the database and rewrite Request-URI”。
  [疑问2]SER生成的ser.cfg在main route的“if (!uri==myself)”成立时,会执行两次route(1)?由于break并不是跳过剩余所有actions,而只是跳过本层剩余actions。可能是route(1)的sl_reply_error()已经转发了消息,之后的动作会全部忽略?
  [疑问3]SER生成的ser.cfg只是在本域UA被呼叫时做proxy_authorize,在收到注册包时做www_authorize,而本域UA呼叫外部UA时并不做任何鉴权(按道理,这时应当做proxy_authorize)。
  2.5 判断Request URI是否包含自己的监听地址(IPv4/v6/FQDN + port)
  在词法文件cfg.lex中,uri和myself均为Token。
  语法文件cfg.y中有如下规则:
  uri_type: URI {$$=URI_O;}
  | FROM_URI {$$=FROM_URI_O;}
  | TO_URI {$$=TO_URI_O;}
  ;
  exp_elem:
  ...
  | uri_type equalop MYSELF { $$=mk_elem( $2, MYSELF_ST,
  $1, 0);
  }
  配置文件中的"uri==myself"或"uri!=muself"被编译为表达式的一个emement,在函数eval_elem()中计算此元素的值。
  case URI_O:
  if(msg->new_uri.s){
  if (e->subtype==MYSELF_ST){
  if (parse_sip_msg_uri(msg)<0) ret=-1;
  else ret=check_self_op(e->op, &msg->parsed_uri.host,
  msg->parsed_uri.port_no?
  msg->parsed_uri.port_no:SIP_PORT);
  }else{
  ret=comp_strstr(&msg->new_uri, e->r.param,
  e->op, e->subtype);
  }
  }else{
  if (e->subtype==MYSELF_ST){
  if (parse_sip_msg_uri(msg)<0) ret=-1;
  else ret=check_self_op(e->op, &msg->parsed_uri.host,
  msg->parsed_uri.port_no?
  msg->parsed_uri.port_no:SIP_PORT);
  }else{
  ret=comp_strstr(&msg->first_line.u.request.uri,
  e->r.param, e->op, e->subtype);
  }
  }
  break;
  从函数parse_sip_msg_uri()可以看出,URI_O指的是SIP首行的那个URI(无论Request或Reply)。
  可见,如果SIP Request URI如果不出现端口信息,则相当于出现SIP默认端口。
  不明白的是:为何涉及msg->new_uri?这个URI似乎是改写过的RequestURI。
  2.6 core支持的命令(全部列举在cfg.y关于cmd的文法中)
  SER为core支持的每个命令设计了不同的action类型。在解析配置文件过程中遇到这样一个命令,就调用mk_action()/mk_action3()创建一个action并添加到动作链尾。
  ser.cfg有如下文法:
  cmd: FORWARD LPAREN host RPAREN { $$=mk_action( FORWARD_T,
  STRING_ST,
  NUMBER_ST,
  $3,
  0);
  route_struct.h定义了action结构:
  struct action{
  int type; /* forward, drop, log, send ...*/
  int p1_type;
  int p2_type;
  int p3_type;
  union {
  long number;
  char* string;
  void* data;
  }p1, p2, p3;
  struct action* next;
  };
  而函数mk_action()/mk_action3()只是简单的将命令名称和各参数类型及值(参数数目不得超过3)记录到结构体action中(参数数目不足3个的,类型和参数值都设置为0)。
  SER处理一个SIP Request时总是从main route开始运行动作链。(见receive_msg()对run_actions()的调用。)每个动作都是以do_action()来执行。do_action()根据动作类型和参数做相关操作。do_action()返回0表示跳过一个动作链的剩余动作。
  2.7 由模块导出的命令
  参考开发文档的"finding-exported"一节。
  cfg.lex有以下词法:
   {ID} { count(); addstr(&s_buf, yytext, yyleng);
  yylval.strval=s_buf.s;
  memset(&s_buf, 0, sizeof(s_buf));
  return ID; }
  cfg.y有如下文法:
  cmd:...
  | ID LPAREN RPAREN
  | ID LPAREN STRING RPAREN
  | ID LPAREN STRING COMMA STRING RPAREN
  { f_tmp=(void*)find_export($1, 2, rt);
  if (f_tmp==0){
  if (find_export($1, 2, 0)) {
  yyerror("Command cannot be used in the block/n");
  } else {
  yyerror("unknown command, missing"
  "loadmodule?/n");
  }
  $$=0;
  }else{
  $$=mk_action3( MODULE_T,
  CMDF_ST,
  STRING_ST,
  STRING_ST,
  f_tmp,
  $3,
  $5
  );
  }
  }
  以上几个规则的规约动作都相似:首先调用函数find_export()查找具有指定名称及参数数目的命令实现,然后像core支持的命令那样调用mk_action()或是mk_action3()创建一个action(不同的是传入mk_action()的前两个参数值必须分别为MODULE_T和CMDF_ST)放入规则链。
  每个模块的导出信息就是本模块的全局变量struct module_exports exports。(注意:开发文档关于struct module_exports的说明有些过时了,结构体定义已经发生了变化。)每个实现函数都必须具有如下原型:
  typedef int (*cmd_function)(struct sip_msg*, char*, char*);
  第一个参数是SIP消息。其余两个参数由实现函数根据具体情况决定利用0/1/2个均可,在struct cmd_export_中必须记录这儿有效的参数个数。
  在cfg.y编译期间mk_action()将每个命令翻译为一个action。那些由core支持的命令,每个命令对应一个action类型。那些由模块导出的命令,其action类型均为MODULE_T,且记录其对应的实现函数。action执行时(函数do_action())利用了以上信息。
  当前,配置文件中出现了以下命令('/'表示core直接支持,否则是由独立模块导出的):
  命令 模块 实现函数及参数个数
  mf_process_maxfwd_header maxfwd w_process_maxfwd_header(1)
  record_route rr record_route(0)
  loose_route rr loose_route(0)
  append_hf textops append_hf(1)
  t_relay tm w_t_relay(0)
  sl_send_reply sl w_sl_send_reply(1)
  www_authorize auth_db www_authorize(2)
  www_challenge auth www_challenge(2)
  save registrar save(1)
  lookup registrar lookup(1)
  break / case DROP_T
  setflag / case SETFLAG_T
  force_rport / case FORCE_RPORT_T
  3. 支持多个传输层协议
  SER的效率是其显著优点之二。网络软件的体系结构是性能关键。SER支持UDP/TCP/TLS多个传输层协议,这是怎样做到的?我维护的一个SIP UA(称为webcam),将UDP/TCP/TLS插口(所有插口都设置为非阻塞方式)统一抽象为一个传输层接口,在一个线程内不断轮询所有传输层接口,调用该接口的任务回调--这是效率最低下的实现方式,必然导致CPU占用率接近100%且吞吐量不高。SER分别对待各个传输层协议,因为不同的传输层协议有不同的最佳实现方式:对TCP/TLS,要考虑接受连接、超时断开、多插口并发、不能读完整报文等;对UDP则没有以上复杂性,但要考虑多播。
  3.1 TCP/TLS
  其实,并发技术不外乎:(1)同步事件多路分解,如select();(2)多线程或者多进程;(3)异步I/O;(4)以上某几个的混合体。
  我不熟悉异步IO,似乎这个比较少用,以下不予考虑。
  (1)每连接对应一个线程/进程--效率低。
  (2)在单个线程/进程内采用技术1,称为反应式编程,稍作提炼就是反应器模式(见《面向模式的软件体系结构,卷2:用于并发和网络化对象的模式》)--效率较高。ACE_Select_Reactor就是此模式的例子。缺陷:这种单线程事件处理模型对事件处理进行串行化,在处理耗时较长的客户请求事件或阻塞客户请求事件时降低了整体服务器的性能。另外,单线程服务器不能明显地受益于多处理器平台。
  (3)SER的做法:主进程专注于接受TCP连接以及子进程退回的TCP插口描述符,并把新连接对应TCP插口描述符以及发生事件的TCP插口描述符通过一个UNIX域插口发送给负载最低的那个子进程让其负责监听和处理。每个子进程都是若干TCP插口和上述UNIX域插口上的反应器。子进程还可以把TCP插口描述符返回给主进程让其监听。因为多个进程各自独立执行事件处理,所以效率高于单纯的反应器。
  (4)领导者-追随者模式是一个线程池反应器(见《面向模式的软件体系结构,卷2:用于并发和网络化对象的模式》)。与SER的做法相比,每个子线程并不负责特定插口,而是协调地轮流作为所有插口之上的反应器。这个方案效率最高,但实现难度较大。ACE_TP_Reactor就是此模式的例子。
  所有进程都在全局变量pt作了记录。全局变量pt是进程描述表,每一项都是如下结构:
  struct process_table {
  int pid;
  #ifdef USE_TCP
  int unix_sock; /* unix socket on which tcp main listens */
  int idx; /* tcp child index, -1 for other processes */
  #endif
  char desc[MAX_PT_DESC];
  };
  全局变量process_no记录了除main()进程之外所有进程总数,在创建UDP/TCP/FIFO子进程时此值都自增1。
  3.1.1 TCP主进程
  线索:
  main_loop()
  ->tcp_main_loop()。
  SER创建了唯一一个TCP主进程。tcp_listen是SER监听的所有TCP服务地址列表。首先把此列表中所有插口放入描述符集master_set。然后把所有子进程UNIX域插口(本函数之前就已经初始化,而子进程的创建在更靠后的地方)也放入master_set。然后开始主循环:
  在maister_set上等待事件发生,然后:
  (1)遍历TCP监听插口,调用handle_new_connect()。如果插口有事件发生,则acctpt()接受连接,init_sock_opt()初始化新插口,tcpconn_new()创建新TCP连接结构体并添加到全局散列表tcpconn_id_hash,send2child()将新TCP连接结构体发送给当前负载最低的那个子进程让其负责监听。send2child()最终调用了库函数sendmsg()。而利用sendmsg()/recvmsg()在进程之间传递插口描述符,这是一种常见的TCP Server实现方式。
  (2)遍历全局散列表tcpconn_id_hash,如果一个TCP连接结构体有事件发生则清除标记,并调用send2child()将此TCP连接结构体发送给当前负载最低的那个子进程让其负责监听。
  (3)遍历所有TCP子进程UNIX域插口tcp_children[r].unix_sock--这个插口主要作用:主进程调用recv_all()接收子进程发来的的命令(主要用于通告某个TCP需要释放,或子进程把一连接发送给主进程让其监听)。子进程在tcp_receive_loop()中通过此插口接受父进程发来的TCP插口描述符。
  (4)遍历所有进程UNIX域插口pt[r].unix_sock--这个插口主要作用:[1]子进程在tcp_send()中通过此插口由TCP连接结构体查询对应的TCP插口文件描述符(TCP连接结构明明包含了这个信息,为什么要这么做?)。[2]子进程在tcp_snd()(子进程tcp_read_req()引发的一个调用链)中通过此插口通知父进程新建立了一个TCP连接,让主进程监听这个插口。(后续考虑这两个UNIX插口是否可以合并为一个?)
  (5)调用tcpconn_timeout()处理全局散列表tcpconn_id_hash中那些超时的TCP连接。
  观察send_fd()被调用情况。
  TCP主进程 TCP子进程
  tcp_main_loop()利用pt[r].unix_sock tcp_sned()利用unix_tcp_sock -->sockfd[]
  send2child()利用tcp_children[idx].unix_sock -->reader_fd[]
  观察receive_fd()被调用情况。
  TCP主进程 TCP子进程
  tcp_main_loop()利用pt[r].unix_sock tcp_sned()利用unix_tcp_sock -->sockfd[]
  tcp_receive_loop()利用reader_fd[1]
  观察send_all()被调用情况。
  TCP主进程 TCP子进程
  tcp_sned()利用unix_tcp_sock -->sockfd[]
  tcp_receive_loop->release_tcpconn() -->reader_fd[]
  观察recv_all()被调用情况。
  TCP主进程 TCP子进程
  tcp_main_loop()利用pt[r].unix_sock -->sockfd[]
  tcp_main_loop()利用tcp_children[r].unix_sock -->reader_fd[1]
  3.1.2 TCP子进程
  线索:
  main_loop()
  ->tcp_init_children()
  --->init_child()
  --->tcp_receive_loop()
  ----->tcp_read_req()
  ------->tcp_read_headers()
  ------->receive_msg()
  每个TCP子进程与其他子进程的不同之处:(1)在全局变量pt表的相应记录中多了unix_sock字段(一个UNXI域插口sockfd[0]);(2)以全局变量tcp_children记录了所有TCP子进程的又一个描述信息:负载程度busy,已处理请求数n_reqs,另一个UNIX域插口reader_fd[0]。
  TCP子进程内部将全局变量unix_tcp_sock赋值为sockfd[1]。tcp_send()是一个仅被TCP子进程调用的函数,TCP子进程在此函数内调用了send_all()/send_fd()/receive_fd()与TCP主进程交换TCP连接的监听责任。
  (unix_tcp_sock的用法很让人糊涂:start_fifo_server()创建的子进程,init_unixsock_children()创建的子进程,main_loop创建的UDP子进程---都设置了全局变量unix_tcp_sock,但都没有实际利用它。)
  TCP子进程内部将reader_fd[1]加入监听列表(这是tcp_init_children调tcp_receive_loop时的传入参数)。
  函数tcp_receive_loop()是TCP子进程的主循环所在处。它对UNIX域插口unix_sock和一些TCP插口作反应。如果unix_sock可读(这是TCP主进程发送一个TCP连接结构体及其描述符),则接收一个TCP连接结构体及其描述符并添加到监听描述符列表。如果TCP插口可读,则调用函数tcp_read_req()--这个函数调用tcp_read_headers()读至少一个完整的SIP报文,再调用receive_msg()解析SIP报文并处理。UDP方面的主函数udp_rcv_loop()也依赖于receive_msg()。
  3.1.2.1 在非阻塞TCP插口建立TCP连接--函数tcp_blocking_connect()(文件tcp_main.c)
  并不像我之前想象中的一个connect()库函数搞定那么简单,而是用了connect(),select(),getsockopt()三个库函数。查看connect()的man可知,非阻塞面向连接的socket比其他socket多了两个错误码EINPROGRESS和EALREADY。
  3.1.2.2 在非阻塞TCP插口发送数据--函数tcp_blocking_write()(文件tcp_main.c)
  调用send()库函数有几个可能的结果:被信号打断、只发送了部分数据、发送完整数据。要完整地发送数据,实现起来就复杂了。这儿用到了send(),select()两个库函数。
  3.2 UDP
  main_loop()
  ->socketpair()
  ->fork()
  ->init_child()
  ->udp_rcv_loop()
  --->recvfrom()
  --->receive_msg()
  UDP没有TCP连接之类的复杂性。UDP服务器的并发性只有唯一的实现方式:多线程/进程。 在函数main_loop()中,为UDP监听地址列表udp_listen中的每个都创建children_no个子进程,这children_no个子进程有相等的全局变量bind_address(多进程并发地读写同一插口,由于每个进程完全相同,所以不会有什么问题)。每个子进程的主函数都是udp_rcv_loop(),不断地接收SIP报文、解析并处理。
  4. FIFO支持
  FIFO就是 first-in first-out special file, named pipe。FIFO是UNIX环境IPC手段之一(前面说的UNIX域插口也是IPC手段之一)。SER利用FIFO实现如下特征:在SER运行期间,管理员可以(编程或者脚本?)通过此FIFO执行某些命令,而这些命令也像ser.cfg内的action那样分为core直接支持的以及模块提供的。
  如果配置文件指定了FIFO文件名,SER就会启动FIFO Server。此Server打开指定的FIFO读取命令并执行,执行结果写入另一个FIFO。客户端在命令中指定存储结果的FIFO名字,且之前就已经以读方式打开(否则Server以写方式打开FIFO将失败--见man FIFO)。
  fifoserver.c集中实现了所有FIFO辅助函数。
  4.1 FIFO Server的配置
  允许以下3项配置:
  [1] FIFO Server接收命令的FIFO文件的全路径;
  [2] 命令结果FIFO文件都必须位于一指定的目录下;
  [3] fifo_db_url不知何用?
  cfg.y关于FIFO有以下文法规则:
  assign_stm:...
  | FIFO EQUAL STRING { fifo=$3; }
  | FIFO_DIR EQUAL STRING { fifo_dir=$3; }
  | FIFO_DB_URL EQUAL STRING { fifo_db_url=$3; }
  变量fifo,fifo_dir,fifo_db_url默认值分别为NULL,"/tmp/",NULL就是以上3个配置项。
  4.2 FIFO命令的注册
  typedef int (fifo_cmd)( FILE *fifo_stream, char *response_file );
  int register_fifo_cmd(fifo_cmd f, char *cmd_name, void *param);
  模块必须在其初始化函数mod_init()中调用它注册其支持的FIFO命令,例如tm模块注册了"t_reply"等FIFO命令。
  core函数register_core_fifo()(在fifoserver.c)注册了其提供的FIFO命令,如下所示:
  名称 函数
  "print" print_fifo_cmd
  "uptime" uptime_fifo_cmd
  "version" print_version_cmd
  "which" which_fifo_cmd
  "ps" ps_fifo_cmd
  "arg" arg_cmd
  "pwd" pwd_cmd
  "kill" kill_fifo_cmd
  fifo命令函数原型第一个参数是已打开的FIFO流。FIFO Server负责读出一个FIFO命令的首行,而一个命令可以占多行,例如tm模块提供的FIFO命令vm_reply格式如下:
  ":vm_reply:[response file]/n
  code/n
  reason/n
  trans_id/n
  to_tag/n
  [new headers]/n
  /n
  [Body]/n
  ./n
  /n"
  fifo命令函数原型第二个参数是一个文件名。大多数(并非全部)FIFO命令的处理结果需要以某种形式返回给客户端。客户端必须确认文件response_file存在于目录fifo_dir(此项可在配置文件中设置)之下(见函数trim_filename()对文件名的要求)且是一个FIFO,并且应当在发送FIFO命令前就以读方式打开此FIFO。fifo命令函数(例如which_fifo_cmd)一般首先以写方式打开response_file,如果遭遇错误且一段时间内不能恢复,则不再做进一步处理。
  4.3 初始化FIFO Server
  int init_fifo_server();
  如果配置文件中没有指定FIFO文件名,则本函数直接返回;如果已存在此FIFO则删除并重新创建它;最后打开FIFO并设置读写模式为阻塞式。
  4.4 启动FIFO Server
  int start_fifo_server();
  在FIFO Server初始化之后应当调用本函数以启动FIFO Server。FIFO Server是fork出的子进程,运行一个永远不返回的函数fifo_server()。函数fifo_server()首先调用register_core_fifo()注册core支持的FIFO命令,然后进入一个无限循环不断地从FIFO读一行并处理:查找FIFO命令的实现函数并调用它。
  注意:FIFO是流式的,且不能预知一行有多长,所以这里特别编写了函数read_line(),consume_request(),read_eol(),read_line_set(),read_body()等函数辅助读FIFO。其中最基础的是read_line(),读长度适宜的一行并删除行未的所有空白字符。函数read_body()读FIFO直到遇到一空白行。
  4.5 使用FIFO管理SER
  SER配置文件/usr/local/etc/ser/ser.cfg默认就有如下行:
  fifo="/tmp/ser_fifo"
  这说明SER会从/tmp/ser_fifo这一FIFO接受命令。
  配置文件没有出现fifo_dir,这表明采用了默认值"/tmp"。
  以下演示如何FIFO命令pwd查询SER进程的当前目录。
  (1)首先,在fifo_dir(即/tmp)下创建一个FIFO,假设名字是f
  [root@v6router ~]# cd /tmp/
  [root@v6router tmp]# mkfifo f
  [root@v6router tmp]# ls -l f
  prw-r--r-- 1 root root 0 Feb 1 10:30 f
  (2)以读方式打开刚才建立的FIFO。注意,在bash这个Shell环境里无法得知打开的文件描述符,也就无法在命令执行之后读响应,所以这儿建立了一个Job监视FIFO的变化。如果用C或Python之类的编程语言来做就要更方便一些。
  [root@v6router tmp]# tail f &
  [4] 1085
  (3)向FIFO服务器发送命令
  [root@v6router tmp]# echo :pwd:f >>/tmp/ser_fifo
  [root@v6router tmp]# 200 ok
  /
  [4] Done tail f
  [root@v6router tmp]#
  从输出可以看出,SER运行期的当前目录就是"/"。由于FIFO Server写完响应后关闭了文件,所以后台进程tail收到了文件结尾标记EOF而退出,这个Job也就完成了。
  5. SIP报文解析与修改、再生成
  SER的SIP解析器位于目录parser下。此解析器主要利用了有限状态自动机(编写这样的逐字符解析代码比较繁琐,但比用正则表达式或者YACC生成的解析器的效率要高得多,因为它只需要一趟扫描)。另外,解析器还利用了各种技巧提高了效率,例如将连续4字节做成一整数并基于此来分支,避免了书写大量的switch语句(见作废的文件obsolete/parse_hname.c)。
  parser目录下文件的逻辑关系
  解析特定头部域(parse_diversion.c, parse_rpid.c)
  |
  V
  解析所有头部域(msg_parse.c) <--- 解析报头(msg_paser.c) ---> 解析首行(parse_fline.c)
  |
  |->解析头部域的名称(parse_hname2.c,case_*.h,keys.h,hf.h,hf.c,parse_def.h)
  |->解析特定头部域的体(parse_*.c,contact和digest目录下所有文件)
  5.1 解析整个SIP消息、SIP报头
  解析器最主要的文件是msg_parser.h和msg_parser.c;最重要的结构是sip_msg;最重要的函数是parse_msg()和parse_headers()。
  parse_headers()是一个渐进式解析器。渐进式的关键是传入的第二三个参数。第三个参数为0(一般是这样)时,第二个参数传入的flags表示要解析的头部域集合。如果此集合中的某头部域还没有没有出现并且缓存(从msg->unparsed到msg->buf+msg->len范围)中还有字符串未解析则继续。(parse_headers()调用的函数get_hdr_field())解析出头部域类型和体的范围之后,如果域类型是HDR_VIA/HDR_CSEQ/HDR_TO/HDR_CONTENTLENGTH四类之一则解析其体,否则不解析头部域的体。
  parse_headers()的渐进式解析特性使得许多地方都可以调用它。例如在route.c的函数eval_elem()中调用的parse_from_header(),调用了parse_headers以确认FROM头部域被解析。
  /* parse the headers and adds them to msg->headers and msg->to, from etc.
  * It stops when all the headers requested in flags were parsed, on error
  * (bad header) or end of headers */
  /* note: it continues where it previously stopped and goes ahead until
  end is encountered or desired HFs are found; if you call it twice
  for the same HF which is present only once, it will fail the second
  time; if you call it twice and the HF is found on second time too,
  it's not replaced in the well-known HF pointer but just added to
  header list; if you want to use a dumb convenience function which will
  give you the first occurrence of a header you are interested in,
  look at check_transaction_quadruple
  */
  int parse_headers(struct sip_msg* msg, int flags, int next)
  parse_headers()
  ->get_hdr_field()
  ->->parse_hname()
  ->->parse_via() case HDR_VIA
  ->->parse_cseq() case HDR_CSEQ
  ->->parse_to() case HDR_TO
  ->->parse_content_length() case HDR_CONTENTLENGTH
  函数parse_msg()解析了SIP报文首行和直到VIA的头部域。
  parse_msg()
  ->parse_first_line()解析报文首行
  ->parse_headers(msg,HDR_VIA,0)解析直到VIA头部域的报头。
  sip_msg::new_uri域支持了Request URI改写。
  char_msg_val()提取一个sip_msg的特征(from、to等域的体的MD5摘要), this value is used to identify a transaction during the process of reply matching.
  文件hf.h为所有头部域定义了标记值,定义了hdr_field结构。hf.c定义了几个通用的hdr_field操作函数,包括释放(free_hdr_field_lst()和clean_hdr_field())和调试输出(dump_hdr_field())。
  文件../lump_struct.h定义的结构lump支持了SIP报文的修改。***
  5.2 解析头部域名称
  [文件]parse_hname2.c,所有case_*.h头文件,头文件keys.h
  [主要函数]
  char* parse_hname2(char* begin, char* end, struct hdr_field* hdr);
  一个SIP头域的格式是:name':' body 。 ':'前后可以有若干空白字符。
  当前已定义的SIP头域有如下规律:完整域名长度大多不小于3,除了to之外;简写域名都是单字节。
  如果name长度不小于3,那么一行前4字节可以映射到一个整数,基于此整数就可以立即判断头部域的名称究竟是哪一个。如果判断失败,那么要么是to,要么是单字节的简写域名。
  函数返回值指向':'之后的第一字节;如果这一行不含有':'则返回NULL。如果域名是当前未知的,则设置hdr->type = HDR_OTHER;否则设置为相应值。
  5.3 解析URI
  parse_uri.c提供了函数parse_uri()的两个版本。主打版本基于状态机,写得像天书,状态多、宏定义多,并且不得不用goto处理错误--难以想象怎么写出来的。另一版本一步步判断URI各部件是否出现,如果出现则解析。主打版本扫描缓存仅一趟,所以效率要高一些。
  5.4 修改SIP报文
  lump_struct.h定义了lmup结构体以及相关枚举量。
  lump类型:
  enum lump_op { LUMP_NOP=0, LUMP_DEL, LUMP_ADD, LUMP_ADD_SUBST, LUMP_ADD_OPT };
  struct lump{
  int type; /* VIA, OTHER, UNSPEC(=0), ... */
  enum lump_op op; /* DEL, ADD, NOP, UNSPEC(=0) */
  union{
  int offset; /* used for DEL, MODIFY */
  enum lump_subst subst; /*what to subst: ip addr, port, proto*/
  enum lump_conditions cond; /* condition for LUMP_ADD_OPT */
  char * value; /* used for ADD */
  }u;
  int len; /* length of this header field */
  struct lump* before; /* list of headers to be inserted in front of the
  current one */
  struct lump* after; /* list of headers to be inserted immediately after
  the current one */
  struct lump* next;
  enum lump_flag flags; /* additional hints for use from TM's shmem */
  };
  lump结构的type记录了相关SIP头部域的类型,但到现在为止还没有发现哪儿依赖于这个知识?
  lump按照op成员的取值可以分为两类:
  (1)锚:记录了此lump操作锚到原SIP消息的何处。op为LUMP_NOP或者LUMP_DEL。在构造新SIP报文时,遇到这样的lump,必须先将从原报文偏移到本lump锚偏移之间的原报文部分原封不动的拷贝到新报文。
  (2)非锚:lump操作不含有任何位置信息,“就地”执行。op为LUMP_ADD, LUMP_ADD_SUBST, LUMP_ADD_OPT。
  lump链表的结构:
  (1)主链中的每个lump,无论其是否为锚,其before或after成员可以指向另一个lump子链。
  (2)在某个before或after子链内的lump,必须为非锚类lump,且其before和after成员必须都为空。
  (3)主链中的那些锚类lump在链表中的先后关系必须按照它们的锚偏移从小到大排列。
  data_lump.h, data_lump.c, data_lump_rpl.h, data_lump_rpl.c定义了lump结构的操作函数。
  创建锚类lump的函数有:
  LUMP_DEL型:del_lump().
  LUMP_NOP型: anchor_lump()。
  创建锚类lump时必须指定锚偏移和长度。
  创建非锚类lump的函数:
  (1)在主链链首或链尾创建:insert_new_lump(也许该名为prepend_new_lump更恰当),append_new_lump().
  (2)在给定锚lump的before或after子链链首创建:
  (2.1)LUMP_ADD型:insert_new_lump_after(),insert_new_lump_before().
  (2.2)LUMP_ADD_SUBST型:insert_subst_lump_after(),insert_subst_lump_before().
  (2.3)LUMP_ADD_OPT型:insert_cond_lump_after(),insert_cond_lump_before().
  销毁lump的函数(不区分lump类型):
  free_lump()<-free_lump_list()<-free_sip_msg().
  5.5 合成SIP报文
  msg_translator.c实现了SIP报文的合成。按照sip_msg中的几个lump修改原报文,即可产生新报文。
  SIP消息结构包含了几个lump结构:
  struct sip_msg {
  ......
  struct lump* add_rm; /* used for all the forwarded requests/replies */
  struct lump* body_lumps; /* Lumps that update Content-Length */
  struct lump_rpl *reply_lump; /* only for localy generated replies !!!*/
  }
  函数process_lumps()充分利用上一节中lump结构的约定,处理lumps指向的lump列表,产生新报文的一个片段。
  [原型]
  /* another helper functions, adds/Removes the lump,
  code moved form build_req_from_req */
  static inline void process_lumps( struct sip_msg* msg,
  struct lump* lumps,
  char* new_buf,
  unsigned int* new_buf_offs,
  unsigned int* orig_offs,
  struct socket_info* send_sock);
  [输入参数]
  msg 原SIP报文
  lumps 一个lump主链
  new_buf 新报文缓存起始处
  new_buf_offs I/O参数,新报文缓存偏移。处理lumps过程中产生的报文片段将从这里开始填充。
  orig_offs I/O参数,原报文偏移。处理锚类lump时需要知道原报文偏移的当前值(见上一节)。
  send_sock 发送新报文时将采用的插口。处理某些LUMP_ADD_SUBST型lump时需要知道这一信息。
  函数lumps_len计算处理一个lump主链之后报文长度的变化量,但不实际处理一个lump主链。这在修正Content-Length头部域时非常有用,因为必须生成此头部域时还没有生成SIP报文体,但此时必须知道报文体的长度。
  /* computes the "unpacked" len of a lump list,
  code moved from build_req_from_req */
  static inline int lumps_len(struct sip_msg* msg, struct lump* lumps, struct socket_info* send_sock);
  函数adjust_clen()根据报文体长度变化量构造若干lump--这些lump在之后被处理时将真正修改Content-Length头部域。(这个函数是示范如何利用lump修改报文的好例子!)
  static inline int adjust_clen(struct sip_msg* msg, int body_delta, int proto);
  构建各个小部件:
  branch_builder(), clen_builder(), id_builder(), received_builder(), rport_builder(), via_builder(), warning_builder().
  从原SIP报文构建新报文--顶层函数,实施配置文件规定的所有动作时如果需要转发/响应的话就必然会调用它们(例如:do_action()->forward_request()->build_req_buf_from_sip_req())(并不是简单的调用以上函数处理lump以构造新报文那么简单,一开始还有一些简单的逻辑,这些处理逻辑并不是由配置文件规定的,所以是不可配置的,例如转发SIP请求前添加一个VIA头部域):
  char * build_req_buf_from_sip_req( struct sip_msg* msg,
  unsigned int *returned_len,
  struct socket_info* send_sock, int proto);
  char * build_res_buf_from_sip_res( struct sip_msg* msg,
  unsigned int *returned_len);
  char * build_res_buf_from_sip_req( unsigned int code, char *text ,str *new_tag,
  struct sip_msg* msg, unsigned int *returned_len, struct bookmark *bmark);
  6. Stateful Proxy是如何做到的
  6.1 消息处理的完整流程
  receive_msg()
  ->parse_msg()解析消息
  ->exec_pre_cb()
  对请求:
  ->run_actions()
  对响应:
  ->forward_reply()
  最后:
  ->exec_post_cb()
  可见,receive_msg()在运行配置文件规定动作之前和之后,分别执行了一些回调函数。在script_cb.c定义了register_script_cb(),exec_pre_cb(),exec_post_cb()等函数用于注册/删除/调用它们。
  tm模块的初始化过程中,将本模块的script_init()注册为脚本前回调。
  mode_init(){
  ...
  /* register post-script clean-up function */
  register_script_cb( w_t_unref, POST_SCRIPT_CB,
  0 /* empty param */ );
  register_script_cb( script_init, PRE_SCRIPT_CB ,
  0 /* empty param */ );
  }
  而script_init()函数针对SIP Request,将设置全局变量rmode为MODE_REQUEST(正是这个变量影响了w_t_relay()的运行,见后面的Server Transaction小节)。
  6.2 Transaction信息的存储
  SIP Proxy总是转发SIP请求和响应,自己从来不会构造新SIP请求,所以SER内部将接收请求时创建的Server Transaction也作为转发请求的Client Transaction,用同一个struct cell来表示。
  SER将所有Transaction相关信息存储在位于共享内存内的一个桶散列表中。为SIP请求或响应查找所属Transaction的算法如下:
  (1)计算散列索引:
  p_msg->hash_index=hash( p_msg->callid->body , get_cseq(p_msg)->number ) ;
  (2)计算消息的MD5特征:
  char_msg_val( p_msg, t->md5 );
  (3)在步骤1算出的索引对应的桶内,逐一比较各个struct cell的md5属性与步骤2计算出的MD5特征是否相等。
  由于SER的多进程结构,Transaction相关信息的存储位置必须是共享内存,否则信息无法在多进程之间共享。
  /* pointer to the big table where all the transaction data
  lives */
  static struct s_table* tm_table;
  struct s_table* init_hash_table();
  struct cell* build_cell( struct sip_msg* p_msg );
  共享内存和普通内存操作混合在一起会造成麻烦,build_cell()等函数内特别注意了这一点。
  new_cell->uas.request = sip_msg_cloner(p_msg,&sip_msg_len);//将SIP消息拷贝到共享内存
  6.3 Transaction的查找与匹配
  SER转发Request时构建的branch参数值结构如下:"z9hG4bK"+hash_index的16进制+'.'+entry_label的16进制+'.'+branch_id。
  Via: SIP/2.0/UDP 10.0.4.3;branch=z9hG4bKb5b8.6a1c12a5.0
  Via: SIP/2.0/UDP 10.0.15.120:5060;rport=5060;branch=z9hG4bK13c49996880a2a79899
  SER转发一个SIP请求可以分为两步:(1)Server Transaction接收请求;(2)Client Transaction转发这个请求。以下两小节本别描述了它们。
  6.3.1 Server Transaction(遵循RFC3261,17.2)
  ser.cfg配置文件中转发请求、返回响应使用的命令、对应的命令实现函数分别为(参考第2节)
  t_relay tm w_t_relay(0)
  sl_send_reply sl w_sl_send_reply(1)
  以函数w_t_relay()为例。
  tm/tm.c文件中的函数w_t_relay()调用了t_relay_to(),而后者调用了t_newtran()和forward_request()。
  t_newtran()实现了RFC3261第17.2节对INVITE Server Transaction, non-INVITE Server Transaction状态机的规定,即Server Transaction的匹配、创建、动作(更新?超时重传?set_timer()...)。
  t_newtran()
  ->t_lookup_request()查找SIP Request对应的Transaction。首先(基于Call-ID头部域的体和CSeq头部域的体的序列号部分这两个字符串)计算报文的hash并记到p_msg->hash_index。
  ->->matching_3261()遍历get_tm_table()->entrys[p_msg->hash_index]链表。
  ->->->via_matching() /* don't try matching UAC transactions */
  ->t_release_transaction()如果找到Transaction且请求方法是METHOD_ACK,则进入Confirmed状态(RFC3261,P136,Fig7)
  ->t_retransmit_reply()如果找到Transaction且请求方法不是METHOD_ACK,则重传最近一次发出的响应(RFC3261,P140,Fig8)
  ->new_t()如果找不到匹配的Server Transaction则调用本函数创建一个并放入散列表。
  ->->build_cell()
  ->->->init_synonym_id()计算消息的MD5特征并记录到cell的md5属性。
  ->->->->char_msg_val()计算消息的MD5特征,基于以下报头:FROM体,TO体,CALL-ID体,首行URI,CSeq数值部分,第一个VIA的主机、端口及可能的branch值。
  6.3.2 Client Transaction
  上一小节提到的函数t_relay_to()还调用了forward_request()以转发请求。这一步属于Client Transaction行为。
  t_relay_to()
  ->forward_request()再计算一次报文的hash_index(实际上这次计算完全是多余的,因为在t_lookup_request()中已经以完全相同的方法计算了一遍),
  ->->char_msg_val()再计算一次报文的MD5特征(与build_cell()中的计算重复,如果上次计算后把MD5摘要存入sip_msg的某个属性中,则可以省去这次计算)
  ->->branch_builder(),填充msg->add_to_branch_s以如下内容:"z9hG4bK"+('.'+MD5)+hash_index的16进制+'.'+branch的16进制
  ->->build_req_buf_from_sip_req()内部调用的via_builder()将msg->add_to_branch_s添加到新的VIA末尾。
  SER按照响应中的VIA branch查找Transaction(遵循RFC3261,17.1)
  文件forward.c函数forward_reply()转发响应,其主要逻辑如下:
  (1)检查第一个VIA头部域是否为自己。
  (2)遍历所有模块,如果模块导出了响应处理函数(mod->exports->response_f)则调用这个处理函数。--实现Stateful Proxy的关键!
  (3)构建新的响应--调用(第5节提到的)函数build_res_buf_from_sip_res()。
  (4)按照第二个VIA头部域设置装发目的地信息--调用update_sock_struct_from_via()。
  (5)发送新的响应--调用函数msg_send()。
  (6)释放新响应所占空间。
  tm模块初始化时将文件t_reply.c中的函数reply_received()注册为响应处理函数。
  reply_received()
  ->t_check()
  ->->t_reply_matching()
  ->relay_reply()
  ->->t_should_relay_response()
  ->->run_trans_callbacks(TMCB_RESPONSE_FWDED,...)
  ->->build_res_buf_from_sip_res()
  ->->SEND_PR_BUFFER,即send_pr_buffer()
  ->->->msg_send()
  ->->run_trans_callbacks(TMCB_RESPONSE_OUT,...)
  函数t_reply_matching()根据BRANCH_SEPARATOR(即'.'字符)分割第一个via头部域中的branch参数除开头"z9hG4bK"后剩下部分。根据branch各片段的值查找链表get_tm_table()->entrys[hash_index]即可。
  reply_received()内还有Transaction处理,例如在收到对INVITE的非最终响应时做XXX。
  reply_received()找到Transaction之后,根据t->on_reply设置与否决定运行配置文件中的onply路由动作...(可见,响应的处理也可以受配置文件的影响。)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值