精华内容
下载资源
问答
  • RFC1050_RPC远程步骤呼叫协议说明书

    千次阅读 2004-11-04 00:20:00
    调用消息有三个无符号的字段:远程程序号,远程程序版本号和远程过程号。这三个字段唯一地标识了被调用的过程。程序号由一些主要的权威来管理(就象 Sun 公司)。一旦执行者有了一个程序号,他就可以执行他的远程...

    组织:中国互动出版网(http://www.china-pub.com/

    RFC文档中文翻译计划(http://www.china-pub.com/compters/emook/aboutemook.htm

    E-mailouyang@china-pub.com

    译者:马东辉(eaststone    ma_donghui@263.net

    译文发布时间:2001-3-28

    版权:本翻译文档可以用于非商业用途自由转载,但必须保留本文档的翻译及组织信息。

     

    Network Working Group                             Sun Microsystems, Inc.

    Request for Comments: 1050                        April 1988

     

     

    远程过程调用协议规范

    RFC1050---Remote Procedure Call Protocol Specification

    摘要

    远程过程调用(Remote Procedure Call)可以使程序调用远方节点上的过程象调用本地过程一样方便。本文就是远程过程调用协议规范的中文版。

     

    目录

     

    1.简介  1

    2.术语  2

    3RPC的模型       2

    4.传输和语义       2

    5. 绑定与集合独立性    3

    6.认证  4

    7RPC协议的要求       4

    8. RPC消息协议    7

    9.认证协议       10

    10.记录标记的标准       16

    11.RPC语言       16

    附录A:端口映射器程序协议       18

     

    1.简介

       此文档详细说明了一个使用在实现Sun公司的远程过程调用(RPC)包中的消息协议。此消息协议是由外部数据描述(XDR)语言[9]来定义的。这篇文档假定读者对XDR非常熟悉,它并不试图去证明使用RPC的好处。这里推荐由Birrell and Nelson [1]所写的文章,作为了解RPC的背景。

    2.术语

       此文档讨论了服务器,服务,程序,过程,客户和版本这些术语。服务器就是实现网络服务的软件。网络服务是一个或多个远程程序的集合。一个远程程序实现了一个或多个远程过程;这些过程的参数和结果已经存档在特定的程序协议规范中(见附录A中的例子)。网络客户是向服务发出远程过程调用的软件。一个服务器可能支持不止一种版本的远程程序,这样以便于与改变的协议兼容。

     

       例如,一个网络文件服务可能由两个程序组成。一个程序处理诸如文件系统访问控制和锁定这样的高层应用。另一个程序处理低层的文件I/O,拥有象“read”和“write”这样的过程。网络文件服务的客户机根据自己用户的需要将会调用服务中与这两个程序关联的过程。

     

    3RPC的模型

        远程过程调用模型与本地过程调用模型非常相似。在本地过程调用中,调用者把要传给过程的参数放在明确定义好的位置上(例如一个结果记录)。然后调用者将把控制转交到被调用的过程中,最后得到返回的控制。在返回点上,被调用的过程的结果从明确定义好的位置上取出,调用者继续执行。

     

       远程过程调用也是相似的,通过两个进程逻辑地运行在一起构成一条控制主线。一个是调用者进程,另一个是服务器进程。也就是说,调用者进程发送给服务器进程一条调用消息,并等待(阻塞)直到收到响应的消息。调用消息包含被调用的过程的参数等。响应消息包含着被调用过程的结果等。一旦收到响应消息,被调用过程的结果就会被取出,调用者将恢复执行。

     

       在服务器一侧,一个进程处于休眠状态来等待调用消息的到达。当一条调用消息到达后,服务器进程从消息中取出被调用过程的参数,计算出结果,发送响应消息,然后等待下一条调用消息。

     

       注意:这种模式中,在任一个给定的时间上,两个进程中只有一个是激活的。但是,这种模式只是作为一个例子。RPC协议在并发模型的实现上不作限制,也可能存在着其它的实现并发模型的方法。例如,一种实现可能选择RPC调用是异步进行的,这样客户机就可以在等待服务器的响应中,做其它有用的工作。另一种可能性是使服务器创建一个新的任务来处理输入的请求,这样服务器就可以自由地接收其它的请求。

     

    4.传输和语义

       RPC协议不依赖于传输协议。也就是说,RPC不关心消息是怎样从一个进程传递到另一个进程中去的。这个协议仅仅处理协议的规范和消息的解释。

     

       还有必要指出RPC并不去试图实现任何一种可靠性,所以应用程序必须考虑在RPC下层的传输层协议。如果应用程序知道自己运行在象TCP/IP[6]这样的可靠传输层的上层的时候,它就知道保证可靠性的大部分工作已经做好了。在另一方面,如果应用程序运行在象UDP/IP[7]这样的不可靠传输层的上层,那么它必须实现自己的重传和超时策略,而这些服务RPC层是不提供的。

     

    因为独立于传输层,RPC协议并不把特殊的语义附加到远程过程上。可以从下面的传输层推断出来(但是应该有明确的定义)。例如,考虑RPC运行在不可靠传输层UDP/IP的上层。如果应用程序在很短的超时后重传RPC消息,当没有接收到响应的时候,它能够判断的唯一的事情就是过程没有执行或者执行了一次以上。当收到响应的时候,它可以推断出过程至少执行了一次。

     

       服务器可能希望记住以前准许的从客户端发来的请求。为了在某种程度上确保至多只执行一次的语义,服务器不再重新批准这些请求。服务器通过利用打包在RPC请求中的事务ID来实现这个功能。事务的主要用处就是客户端的RPC层用它来匹配对请求的响应。但是,客户应用程序当重传一个请求的时候可以选择再使用它以前的事务ID。服务器应用程序在知道了这个事实后,可以选择在准许了一个请求后记住这个事务ID,为了获得在某种程度上至多只执行一次的语义,服务器对于具有相同ID的请求不再重新批准。除了可以进行检验相等的操作之外,不允许服务器使用其它的方法来检查这个ID

     

        另一方面,如果使用了一个象TCP/IP这样的可靠传输,应用程序能够从一条响应消息推断出过程已经正确地执行了一次。但是,如果它没有接收到响应消息,则不能假设远程过程没有执行。注意:即使使用了象TCP这样的面向连接的协议,应用程序仍然需要超时和重新连接的功能来处理服务器崩溃的情况。

     

       除了数据报或者面向连接的协议之外,还存在其它的传输层协议的可能性。例如,一种象VMTP[2]这样的请求响应协议对RPC来说也许是最自然的传输层协议。

     

       注意:在Sun,RPC当前实现在TCP/IP以及UDP/IP的上层。

     

    5. 绑定与集合独立性

       绑定一个客户端到一个服务上的行为并不是远程过程调用规范的一部分。这个重要且必要的功能留给了一些上层的软件(这个软件可能本身就使用RPC,见附录A

     

       执行者应该把RPC协议看作是网络上的跳转到子程序指令("JSR";装载程序(绑定者)使JSR可用,装载程序本身使用JSR来完成它自己的任务。同样,网络使RPC可用,使用RPC来完成它的任务。

     

    6.认证

        RPC协议提供了一些必要的字段使客户能向服务标识它自己,反过来服务也要用一些字段来向客户标识自己。安全和访问控制机制建立在消息认证之上。一些不同的认证协议能够得到支持。在RPC报头中有一个字段用来指出使用哪一种认证协议。关于更详细的认证协议的信息在第九节“认证协议”中讨论。

     

    7RPC协议的要求

        RPC协议必须要能提供以下的功能:

     

    1)一个调用过程的唯一规范

    2)匹配响应消息和请求消息的规则

    3)服务鉴别调用者和调用者鉴别服务的规则

     

         除了以上的要求,还需要能检测出以下由于协议,实现,用户和网络管理中所产生的错误:

     

    1RPC协议不匹配

    2)远程程序协议版本不匹配

          3)协议错误(诸如错误指定了过程参数)

    4)远程认证失败的原因

    5)不能调用想要的过程的其它原因 

     

    7.1 RPC程序和过程

        RPC调用消息有三个无符号的字段:远程程序号,远程程序版本号和远程过程号。这三个字段唯一地标识了被调用的过程。程序号由一些主要的权威来管理(就象Sun公司)。一旦执行者有了一个程序号,他就可以执行他的远程程序;第一次执行很可能有一个版本号1。因为大多数新的协议都发展成更好,更稳定,更成熟的协议,所以调用消息中的版本字段标识了调用者使用了协议的哪一个版本。版本号使得通过相同的服务器进程运行旧协议和新协议成为可能。

     

    过程号标识着被调用的过程。这些号码已经归档在特定的程序的协议规范中。例如,文件服务协议规范可能把它的过程号5定义为“read”,过程号12定义为“write”。

     

        就像远程程序协议在一些版本上可能有一些变化一样。实际的RPC消息协议也可能有所改变。因此,调用消息也有它自己的RPC版本号。对于在这里描述的RPC来说,这个版本号总是等于2

     

        请求消息的响应消息具有足够的信息来区分下面的错误情况:

     

    (1) RPC的远程实现应该使用协议的第二版,RPC协议支持的最低和最高版本号

    将返回。

     

    2)远程程序在远程系统中不可用。

     

    3)远程程序不支持请求的版本号。远程程序支持的最低和最高版本号将返回。

     

        4)请求的过程号不存在。(这总是因为调用者一方的协议或者程序出错)

     

    5)传给远程过程的参数在服务器看来是无用的(这实际上是由客户端和服务器之间的协议没有达成一致造成的)。

     

    7.2 认证

       服务器对调用者的认证和调用者对服务器的认证的规则是RPC协议的一部分。调用消息有两个认证字段,证书和校验符响应消息有一个认证字段,即响应校验符。RPC协议规范把这三个字段都定义成不透明的类型。

     

             enum auth_flavor {

                AUTH_NULL       = 0,

                AUTH_UNIX       = 1,

                AUTH_SHORT      = 2,

                AUTH_DES        = 3

                /* 更多的定义 */

             };

     

             struct opaque_auth {

                auth_flavor flavor;

                opaque body<400>;

             };

     

       用简单的英语表示,任何一个"opaque_auth"结构就是由一个"auth_flavor"枚举类型后跟一些对RPC协议实现来说是不透明的字节组成的。

     

       在认证字段中包含的数据的语义和解释是个别指定的,独立于认证协议规范。(第九节定义了不同的认证协议)

     

       如果认证参数被拒绝,响应消息将包含说明为什么会被拒绝的信息。

     

    7.3 程序号分配

       程序号根据下表以十六进制20000000(十进制536870912)为一组来进行分发。

     

                     0 - 1fffffff       Sun公司定义

              20000000 - 3fffffff   .   Sun公司定义

              40000000 - 5fffffff       暂时的

              60000000 - 7fffffff       保留

              80000000 - 9fffffff       保留

              a0000000 - bfffffff       保留 

              c0000000 - dfffffff       保留

              e0000000 - ffffffff        保留

     

    第一组是属于升阳微系统公司(Sun公司)管理的号码范围。在所有场所都应该是一致的。第二组号码范围对应用程序来说对特定场所是特有的。这个范围主要是用来调试新程序的。当在一个场所中开发的一个应用程序要引起公众的注意,这个应用程序就应该在第一组号码范围中分配一个号码。第三组号码范围对应用程序来讲是动态地产生程序号。最后的那些号码范围组是为将来使用保留的,目前还没有使用。

     

    7.4 RPC协议的其它使用

        使用RPC协议的主要目的就是为了调用远程的过程。也就是说,每一个调用消息都与一个响应消息匹配。但是,这个协议本身是一个消息传送协议,其它协议(非RPC协议)也可以通过这个协议来实现。Sun当前正在为下面两种非RPC协议使用    RPC消息协议,这两种协议是批处理(或者管道)和广播RPC,下面将讨论,但是不详细说明。

     

    7.4.1 批处理

     

       批处理允许客户发送一个任意大的调用消息序列给服务器;它典型地使用可靠字节流协议(象TCP/IP)来作为它的传输层。在批处理的时候,客户从来不等待服务器的响应,服务器也不发送对批处理请求的响应。一个批处理调用序列总是由合法的RPC终止,这是为了刷新管道(以肯定确认)。

    7.4.2广播RPC

     

       在基于广播RPC的协议中,客户向网络中发送一个广播数据包,然后等待响应。广播RPC使用不可靠的数据报协议(象UDP/IP)作为它的传输层。支持广播协议的服务器只有在请求被成功执行后才进行响应。在出错的时候服务器将保持沉默。广播RPC使用端口映射器RPC服务来获得它的语义。(要获得更多的信息请参见附录A

     

    8. RPC消息协议

    这一节用XDR数据定义语言来定义RPC消息协议。这些消息是用一种严谨的风格来定义的。.

     

           enum msg_type {

              CALL  = 0,

              REPLY = 1

    };

     

      /*

             *调用消息的响应可以呈现出两种形式;

    *消息要么被接受要么被拒绝

             */

           enum reply_stat {

              MSG_ACCEPTED = 0,

              MSG_DENIED   = 1

           };

     

           /*

             *如果一条调用消息被接受,下面是试图调用远程过程的状态。

            */

           enum accept_stat {

              SUCCESS       = 0,     /*  RPC执行成功  */

              PROG_UNAVAIL  = 1,    /* 远程没有输出程序 */

              PROG_MISMATCH = 2,   /* 远程不支持版本号 */

              PROC_UNAVAIL  = 3,    /* 程序不支持过程 */

              GARBAGE_ARGS  = 4   /* 过程不能解释参数 */

           };

     

           /*

            *  调用消息被拒绝的原因

            */

           enum reject_stat {

              RPC_MISMATCH = 0,  /* RPC版本号不等于2  */

              AUTH_ERROR = 1    /* 远程不能认证调用者 */

           };

     

           /*

            *   认证失败的原因

            */

           enum auth_stat {

              AUTH_BADCRED      = 1,  /*  错误的证书 (封装被打破) */

              AUTH_REJECTEDCRED = 2,  /* 客户必须开始新会话 */

              AUTH_BADVERF      = 3,  /* 错误的校验符 (封装被打破)   */

              AUTH_REJECTEDVERF = 4,  /*  校验符过期或者重放 */

              AUTH_TOOWEAK     = 5   /* 因为安全原因拒绝 */

           };

     

           /*

            * RPC消息

            *所有消息以一个事务标识符xid开始,接下来是有判别的两个分支的联合。

    *这个联合的判别式是msg_type等于两类消息的一种。

    *响应消息的xid总是匹配开始调用消息的xid

    *客户端使用这个字段把收到的响应消息与调用消息配备,

    *服务器端使用这个字段来检测重传。

    *服务器端不能把这个id看作为任何一种类型的序列号。

            */

           struct rpc_msg {

              unsigned int xid;

              union switch (msg_type mtype) {

              case CALL:

                 call_body cbody;

              case REPLY:

                 reply_body rbody;

              } body;

           };

     

           /*

            * RPC请求调用的实体:

            *RPC协议规范的版本2中,rpcvers字段必须等于2

    *字段prog, vers,proc确定了远程程序,以及程序的版本号

    *和在远程程序中被调用的过程。在后面是两个认证参数

    *cred(认证证书)和verf(认证校验符)。认证参数的下面是远程过程的参数,

    *这些参数是由具体的程序协议来指定的。

            */

           struct call_body {

              unsigned int rpcvers;          /*  必须等于2  */

              unsigned int prog;

              unsigned int vers;

              unsigned int proc;

              opaque_auth cred;

              opaque_auth verf;

              /* 过程的特定参数从这里开始 */

           };

     

           /*

            * RPC请求的响应实体;

            * 调用消息要么被接受要么被拒绝

            */

           union reply_body switch (reply_stat stat) {

           case MSG_ACCEPTED:

              accepted_reply areply;

           case MSG_DENIED:

              rejected_reply rreply;

           } reply;

     

    /*

                   *   当服务器接受RPC请求时的响应:

            *即使请求被接受,也可能会有错误。

            * 第一个字段是服务器产生的认证校验符,为了使调用者能够识别服务器。

            *接下来是一个判别式是枚举类型accept_stat的联合。

    *联合中SUCCESS的一个分支是特定的协议。

            *联合中The PROG_UNAVAIL, PROC_UNAVAILGARBAGE_ARGS

    *的分支为空。PROG_MISMATCH分支确定了服务器支持的远程

    *程序的最低和最高版本号。

            */

           struct accepted_reply {

              opaque_auth verf;

              union switch (accept_stat stat) {

              case SUCCESS:

                 opaque results[0];

                 /*

                  *具体的过程结果从这里开始

                  */

               case PROG_MISMATCH:

                  struct {

                     unsigned int low;

                     unsigned int high;

                  } mismatch_info;

               default:

                  /*

    *空。这些情况包括PROG_UNAVAIL,

    * PROC_UNAVAILGARBAGE_ARGS.

                   */

                  void;

               } reply_data;

           };

     

           /*

            *当服务器拒绝RPC请求时的响应:

            *请求可能由于两种原因被拒绝:一种是服务器没有运行RPC协议的

    *兼容版本(RPC_MISMATCH),另一种是服务器不认证调用者(AUTH_ERROR)

    *RPC版本不匹配的情况下,服务器返回所支持的最低和最高版本号。

    *在不认证的情况下,返回失败状态。

            */

           union rejected_reply switch (reject_stat stat) {

           case RPC_MISMATCH:

              struct {

                 unsigned int low;

    unsigned int high;

            } mismatch_info;

           case AUTH_ERROR:

              auth_stat stat;

           };

     

    9.认证协议

       象前面所说的那样,认证参数是不透明的,但是对于RPC协议的其余部分是可调整的。这一节说明了在Sun中实现(或是由Sun支持的)的认证的“特征值”。在其它的场所中可以自由得创造新的认证类型。特征值的分配的规则和程序号的分配规则是一样的。

    9.1 不认证

       通常过程会在调用者不知道它是谁或者服务器不关心调用者是谁的情况下调用。在这种情况下RPC消息中的证书,校验符和响应校验符的“特征值”(opaque_auth的联合的判别式)为"AUTH_NULL"opaque_auth的实体中的字节没有定义。建议这些opaque长度为0

    9.2 UNIX 认证

       远程过程的调用者可能希望象在UNIX系统中那样标识它自己。RPC调用消息中的证书判别式的值是"AUTH_UNIX"。这些证书的opaque实体编码成下面这样的结构:

     

             struct auth_unix {

                unsigned int stamp;

                string machinename<255>;

                unsigned int uid;

                unsigned int gid;

                unsigned int gids<10>;

             };

     

       "stamp"是一个调用者机器产生的二进制标识符(ID)。"machinename"是调用者机器的名字(例如 krypton”)。 "uid"是调用者的有效用户标识符ID "gid"是调用者的有效组标识符ID "gids"是一个包含调用者作为成员的组的计数数组。伴随着证书的校验符应该是"AUTH_NULL"(上面定义的)

     

       从服务器收到的响应消息中的响应校验符的判别式的值可能是"AUTH_NULL" ,或者 "AUTH_SHORT"。在"AUTH_SHORT"的情况下,响应校验符的字符串字节编码成不透明的结构。这个新的不透明结构现在可以传递给服务器以代替原来的"AUTH_UNIX"特征值证书。服务器保留一个缓冲区用来把这个速记下来的不透明结构(通过一个"AUTH_SHORT"类型响应校验符传回)映射到调用者原来的证书上。调用者可以使用新的证书节省网络带宽和服务器上的cpu周期。

     

       服务器可能在任一时间刷新这个速记下来的不透明结构。如果这种情况发生,远程过程调用消息将由于认证错误被拒绝。失败的原因将是"AUTH_REJECTEDCRED"。在这时,调用者可能希望试试原来的"AUTH_UNIX"证书类型。

    9.3 DES认证

        UNIX认证遇到了下面两个主要问题:

     

             (1) 命名太面向于UNIX

             (2) 没有校验符,所以证书很容易伪造

     

        DES认证努力来解决这两个问题。

     

    9.3.1 命名

     

       处理第一个问题的方法是通过使用一个简单的字符串代替操作系统特定的整数来定位调用者。这个字符串就作为 "netname"或者调用者的网络名。除了在鉴别调用者之外,不允许服务器在任何其它的方面解释调用者名字中的内容。因此,网络名对于在Internet中的每一个调用者来说都应该是唯一的。

     

       由每一个操作系统上的DES认证实现来为它的用户产生网络名,以确保当用户造访远程服务器的时候,他的网络名是唯一的。操作系统也知道怎样区分本地系统中的用户。把这种机制扩展到网络上通常存在着一些小问题。例如,在Sun上的具有用户ID号为515UNIX用户可能被分配给下面的网络名: unix.515@sun.com"。这个网络名包含三项以使服务器确认它是唯一的。在Internet网上叫做“sun.com”的命名域只有一个。在这个域中用户ID515UNIX用户也仅有一个。但是,在其它的操作系统上可能有另一个用户,例如在VMS上,也在同样的命名域中,碰巧也有同样的用户ID。为了确保两个用户被区分,我们加入操作系统名。所以一个用户是"unix.515@sun.com",另一个是“vms.515@sun.com”。

     

       第一个字段实际上是一种命名方法,而不是一个操作系统名。现在仅仅是碰巧在命名方法和操作系统之间存在一对一对应。如果全世界在命名标准上取得一致,第一个字段应该是标准的名字,而不是一个操作系统名。

     

    9.3.2 DES认证校验

       不像UNIX认证,DES认证有一个校验符,这可以使服务器验证客户的证书(反之客户也可以验证服务器的证书)。校验符的内容主要是一个加密的时间戳。服务器可以解密这个时间戳。如果它接近真实的时间,那么客户一定已经正确的加密了它。客户端能加密它的唯一方法是知道这个RPC会话的“会话密钥”。如果客户端知道这个会话密钥,那么它就是真正的客户端。

     

       会话密钥是由一个由客户端产生的DES密钥[5],在第一次RPC调用时通知服务器。会话密钥在第一次事务中由一个公钥模式加密。使用在DES认证中的特别的公钥模式是Diffie-Hellman [3],它具有128比特位的密钥长度。这种加密方法的细节以后讨论。

     

        为了完成工作,客户端和服务器需要相同的时间概念。如果网络时间同步不能保证,那么客户端可以在开始会话前与服务器建立同步。也许要与Internet时间服务器协商(TIME [4])

     

        服务器判断一个客户端时间戳是否有效的方法有点复杂。对于除了第一次事务之外的其它事务,服务器只检查两件事。

     

    1 这个时间戳比来自同一个客户端的前一个时间戳大

     

      2 这个时间戳没有过期。

     

       如果服务器时间比客户端的时间戳与客户端的窗口值(window)相加后的总和还要晚,这个时间戳过期。 window”是客户端在它的第一次事务中传递(已加密过)给服务器的一个数字。你可以把它看作是证书的生存期。

     

        以上说明了除第一次事务的每一次事务。在第一次事务中,服务器仅检查时间戳没有过期。如果就是所作的全部工作的话,对于客户端来说,发送随机的数据代替时间戳将会非常容易,这会有很大的成功机会。作为一个附加的检查,客户端在第一次事务中发送一个叫“窗口校验符”的加密项,它必须等于窗口值减1,否则服务器将拒绝这个证书。

     

        客户端也必须检查从服务器返回的校验符,以确定服务器是合法的。服务器把它从客户端收到的加密时间戳减一秒后再送回给客户端。如果客户端得到数据与此不同,它将拒绝。

     

    9.3.3 昵称和时钟同步

       

    在第一次事务之后,服务器DES认证子系统在它返回给客户端的校验符中有一个 “昵称” 整数,客户端可以在以后的每次事务中使用这个昵称来代替它的网络名,加密的DES密钥和窗口。昵称更像一个在服务器上表的索引,在这张表中存储着每一个客户端的网络名,DES密钥译文和窗口。

     

        尽管客户端和服务器的时钟在开始时被同步,但是他们可能再次变得不同步。当这种情况发生后,客户端RPC子系统可能在应该需要再次同步的时候得到"RPC_AUTHERROR"

     

        客户端即使在与服务器同步的情况下仍然可能得到"RPC_AUTHERROR"错误。这个原因就是服务器的昵称表大小有限,它可以在它需要的时候随时刷新表中的条目。在这种情况下,客户端应该重新发送它的原来的证书,服务器将给它一个新的昵称。如果服务器崩溃,昵称表中的条目得到刷新,所有的客户端都将必须重新发送它的原来的证书。

     

    9.3.4  DES认证协议规范(用XDR语言描述)

         /*

    *有两种证书:一种是客户端使用它的全称网络名,另一种是客户端使用

    *由服务器发给它的昵称(仅仅是一个无符号整数)。在第一次与服务器的事务中,

    *客户端必须使用它的全称名。服务器将返回给客户端它的昵称。

    *客户端可以在以后的与服务器的事务中使用它的昵称。并不是一定要使用昵称,

    *但是使用昵称将是一个明智的选择。

         */

    enum authdes_namekind {

           ADN_FULLNAME = 0,

           ADN_NICKNAME = 1

        };

     

        /*

         *一个由DES加密的64比特数据块。

         */

        typedef opaque des_block[8];

     

        /*

         * 网络用户名的最大长度

         */

        const MAXNETNAMELEN = 255;

     

        /*

    * 全称包括客户端的网络名,一个加密的会话密钥和窗口。窗口实际上就是

    * 证书的生存期。如果在校验符中的时间戳指示的时间加上窗口后过期,

    *那么服务器应该使请求过期,而且不承认它。为了确保请求不会重放,

    *服务器应该保持时间戳比前一个看到的时间戳大,除非这是第一个事务。

    *在第一个事务中,服务器只检查窗口校验符是一个小于窗口的值。

         */

        struct authdes_fullname {

           string name<MAXNETNAMELEN>;  /* 客户端名字 */

           des_block key;               /* 公用密钥加密过的会话密钥*/

           unsigned int window;         /*  加密过的窗口  */

        };

     

        /*

         *一个证书要么是全称要么是昵称

         */

        union authdes_cred switch (authdes_namekind adc_namekind) {

        case ADN_FULLNAME:

           authdes_fullname adc_fullname;

        case ADN_NICKNAME:

           unsigned int adc_nickname;

        };

     

        /*

         *时间戳从197011日午夜开始给时间编码

         */

        struct timestamp {

             unsigned int seconds;    /* */

             unsigned int useconds;   /* 微秒 */

    };

     

        /*

         * 校验符:客户端的种类

         * 窗口的校验符仅使用在第一次事务中,在与全称证书的关联中,

    *这些项目在加密前打包在下面的结构中。

         *

         * struct {

         *     adv_timestamp;        --  one DES block

         *     adc_fullname.window;  --   one half DES block

         *     adv_winverf;          --   one half DES block

         * }

         *这个结构以一个输入的0向量使用CBC加密模式加密

         * 所有的其它时间戳加密是使用ECB加密模式加密

         */

        struct authdes_verf_clnt {

           timestamp adv_timestamp;    /* 加密的时间戳     */

           unsigned int adv_winverf;   /* 加密的窗口校验符  */

        };

     

        /*

         * 校验符:服务器种类

    * 服务器把这个加密的时间戳减一秒后,返回给客户端。

    * 服务器也告诉客户端它的昵称(未加密),已备在将来的事务中使用。

         */

        struct authdes_verf_svr {

           timestamp adv_timeverf;     /* 加密的校验符  */

           unsigned int adv_nickname;  /* 客户端新的昵称*/

        };

     

    9.3.5  Diffie-Hellman 加密方法

     

        在这种模式中,有两个常数"PROOT" "MODULUS"SunDES认证协议选择了特定的值:

            const PROOT = 2;

            const MODULUS = "b520985fb31fcaf75036701e37d8b857"; /* 十六进制 */

     

       这种模式工作的过程最好用一个例子来说明。假设有两个人“A”和“B”想互相发送加密的消息。所以,AB都随机产生一个“秘密”密钥,这个密钥他们并不想让其它人知道。把这两个密钥表示为SK(A) SK(B)。他们也在一个公开的姓名地址录中发布他们的“公用”密钥。公用密钥由以下方式计算出。

     

                PK(A) = ( PROOT ** SK(A) ) mod MODULUS

                PK(B) = ( PROOT ** SK(B) ) mod MODULUS

     

       "**"符号在这里表示求幂。现在AB都能得到在他们之间的“公共”密钥,而不需要暴露他们的秘密密钥。公共密钥在这里描述为CK(A, B),

     

             A电脑:

     

                CK(A, B) = ( PK(B) ** SK(A)) mod MODULUS

     

              B电脑:

     

                CK(A, B) = ( PK(A) ** SK(B)) mod MODULUS

     

             这两个值是相等的:

     

                (PK(B) ** SK(A)) mod MODULUS = (PK(A) ** SK(B)) mod MODULUS

     

             我们去掉"mod MODULUS"的取模算法部分来进行简化。

     

                PK(B) ** SK(A) = PK(A) ** SK(B)

     

              然后,用以前B计算得到的结果来替换PK(B),对PK(A)也进行这样的步骤

     

                ((PROOT ** SK(B)) ** SK(A) = (PROOT ** SK(A)) ** SK(B)

     

             这将导致:

     

                PROOT ** (SK(A) * SK(B)) = PROOT ** (SK(A) * SK(B))

     

        不使用公共密钥CK(A, B)来加密在协议中使用的时间戳。它只用于加密会话密钥,而使用这个会话密钥来加密时间戳。这样做的原因是因为应该尽可能少的使用公共密钥,以防公共密钥被破译。会话密钥即使被破译,造成的损失也较小,因为会话的时间总是相对较短。

     

       会话密钥是使用56比特DES密钥加密的。而这个公共密钥是128比特位。为了减少比特的数量,象下面这样从公共密钥中选择56比特。从公共密钥中选择最中间的8个字节,然后把奇偶校验加在每一个字节的最低比特位。这样就产生了一个带有8比特奇偶校验位的56比特密钥。

    10.记录标记的标准

       RPC消息在一个字节流协议(象TCP/IP)上层传送的时候,有必要在一个消息和另一个消息之间划定界线,这样是为了检测出用户协议的错误,并可能对错误进行恢复。这就叫做记录标记(RM)。Sun使用RM/TCP/IP来在TCP流上传送RPC消息。一个RPC消息适配一个RM记录。

     

       一个记录是由一个或者多个记录片断组成。一个记录片断是4字节的头,后跟0(2**31)-1字节的片断数据。片断头的字节编码成一个无符号的二进制数;象是XDR中的整数一样,字节的顺序是从高到底的。这个数字编码成两个部分的值:一个部分是布尔值,指示这个片断是否是记录的最后一个记录片断(值1表明此片断是最后一个片断),另一部分是一个31比特的无符号二进制值,它是这个片断数据用字节计数时的长度。布尔值是这个片断头的最高位比特;长度是低位的31比特。(注意这个记录规范并不是XDR的标准形式!)

    11.RPC语言

       就象在一个正式的语言中需要定义XDR数据类型一样。也需要在正式的语言中定义操作XDR数据类型的过程。为了这个目的,我们使用RPC语言。它是XDR语言的扩展。使用下面的例子来描述这种语言的精髓。

    RPC语言描述的服务的例子

      这里有一个简单ping程序的例子说明。

     

             /*

              *简单的ping程序

              */

             program PING_PROG {

                /*

                 * 最近和最好的版本

                 */

                version PING_VERS_PINGBACK {

                   void           PINGPROC_NULL(void) = 0;

     

                   /*

                    *  Ping这个调用者,返回往返时间(用微秒来表示)。如果操作超时,

    *  返回-1

                  */

                   int         PINGPROC_PINGBACK(void) = 1;

                } = 2;

     

                /*

                 *  原来的版本

                 */

                version PING_VERS_ORIG {

                   void           PINGPROC_NULL(void) = 0;

                } = 1;

             } = 1;

     

             const PING_VERS = 2;      /* 最近的版本*/

     

       在第一个版本中,PING_VERS_PINGBACK有两个过程PINGPROC_NULL PINGPROC_PINGBACKPINGPROC_NULL不需要参数,也不返回结果,但是它对计算从客户端到服务器再回到客户端的往返时间很有用。在会话中,任何RPC协议的过程0都应该有同样的语义,不需要任何种类的认证。客户端用第二个过程来使服务器对客户端进行一个反向的ping操作,并且它返回使用这个操作的时间值(用微秒表示)。下一个版本PING_VERS_ORIG是这个协议的原来的版本,它不包含PINGPROC_PINGBACK过程。这对兼容旧的客户端程序很有用,随着程序的升级,它可能从整个协议中删去。

     

    11.1 RPC语言规范

       RPC语言除了加入了"program-def"的定义,与XDR语言一样。

     

          program-def:

             "program" identifier "{"

                 version-def

                 version-def *

             "}" "=" constant ";"

     

          version-def:

             "version" identifier "{"

                 procedure-def

                 procedure-def *

             "}" "=" constant ";"

     

    procedure-def:

          type-specifier identifier "(" type-specifier ")"

             "=" constant ";"

     

    11.2语法的注意事项

       (1) 下面的关键字不能用作标识符:"program" and "version";

     

    2)版本名不能在一个程序定义的范围内出现超过一次。

    同样,版本号在这个程序定义的范围里也不能出现超过一次。

     

    (3)  过程名不能在一个版本定义的范围内出现超过一次。

    同样,过程号在这个版本定义的范围里也不能出现超过一次。

     

       (4) 程序的标识符有象常数标识符和类型标识符同样的名字空间。

     

     (5) 只有无符号的常数可以分配给程序,版本和过程。

     

    附录A:端口映射器程序协议

        端口映射器程序把RPC程序和版本号映射到特定的传输端口号上。这个程序可以对远程的程序进行动态绑定。

     

       这种方法是符合需要的,因为保留的端口号的范围是有限的,而潜在的远程程序是很多的。在一个保留端口号上运行端口映射器,只要查询这个端口映射器就可以确定其它远程成程序的端口号。

     

       端口映射器也在广播RPC中使用。一个给定的RPC程序在不同的机器上经常绑定到不同的端口号上,所以没有方法直接广播到所有的这些程序。而端口映射器有固定的端口号。要向给定的程序发送广播,客户端实际上把消息发送到广播地址上的端口映射器。获得广播的每一个端口映射器调用由客户端指定的本地服务。当端口映射器收到本地服务的响应,它再把这个响应发送给客户端。

     

    A.1端口映射器协议规范(用RPC语言)

    const PMAP_PORT = 111;      /* 端口映射器的端口号*/

     

          /*

           *(程序,版本,协议)到端口的映射

           */

          struct mapping {

             unsigned int prog;

             unsigned int vers;

             unsigned int prot;

             unsigned int port;

          };

     

          /*

           * "prot"字段支持的值

           */

          const IPPROTO_TCP = 6;      /* TCP/IP的协议号 */

          const IPPROTO_UDP = 17;     /* UDP/IP的协议号*/

     

          /*

           * 一个映射列表

           */

          struct *pmaplist {

             mapping map;

             pmaplist next;

          };

          /*

           * 调用的参数

           */

          struct call_args {

             unsigned int prog;

             unsigned int vers;

             unsigned int proc;

             opaque args<>;

          };

          /*

           * 调用的结果

           */

          struct call_result {

             unsigned int port;

             opaque res<>;

          };

     

         /*

          * 端口映射器的过程

          */

          program PMAP_PROG {

             version PMAP_VERS {

                void        PMAPPROC_NULL(void)         = 0;

                bool        PMAPPROC_SET(mapping)       = 1;

     

                bool        PMAPPROC_UNSET(mapping)     = 2;

     

                unsigned int   PMAPPROC_GETPORT(mapping)   = 3;

     

                pmaplist      PMAPPROC_DUMP(void)         = 4;

     

                call_result    PMAPPROC_CALLIT(call_args)  = 5;

             } = 2;

          } = 100000;

     

    A.2 端口映射器操作

       端口映射器程序当前支持两种协议(UDP/IPTCP/IP)。端口映射器在这两种协议中的任一种中都分配在端口111(SUNRPC [8])。下面是每一个端口映射器过程的描述:

     

          PMAPPROC_NULL:

     

             这个过程不工作。按照习惯,任何协议的0号过程不接收参数,也不返回结果。

     

          PMAPPROC_SET:

     

             当一个程序在一个机器上可用时,它要向同一台机器上的端口映射器注册它自己。

              这个程序传递它的程序号 "prog",版本号 "vers", 传输协议号 "prot"和端口号

             "port"。在 "port"这个端口号上,程序等待服务的请求。这个过程返回一个布尔值,如果过程成功建立了映射,这个值为"TRUE"。否则,这个值为"FALSE"

              如果已经存在"(prog, vers, prot)"元组,这个过程拒绝建立映射。

     

          PMAPPROC_UNSET:

     

            当程序变为不可用的时候,它将在同一台机器上的端口映射器中注销它自己。

            它的参数和结果和"PMAPPROC_SET"有同样的含义。参数中的协议和端口

    号字段将被忽略。

     

    PMAPPROC_GETPORT:

     

            当被给出了程序号 "prog",版本号 "vers"和传输协议号 "prot",这个过程将返回

            此程序等待调用请求的端口号。返回一个0的端口值意味着这个程序没有注册。

            参数中的 "port"字段将被忽略。

     

          PMAPPROC_DUMP:

     

             这个过程列举了端口映射器数据库中的所有条目。

             此过程不需要参数,它返回一个程序,版本,协议,和端口值的列表。

     

          PMAPPROC_CALLIT:

     

              这个过程允许调用者调用在同一台机器上的另一个远程过程,而不需要知道这个远程过程的端口号。通过众所周知的端口映射器的端口号,它支持二进制远程程序的广播。 参数"prog", "vers", "proc","args"分别是程序号,版本号,过程号和远程过程的参数。

               注意:

     

                (1)  如果过程成功执行,过程仅发送一个响应。否则保持沉默(不响应)。

     

                (2)  端口映射器只使用UDP/IP协议与远程程序通信。

     

              这个过程返回远程程序的端口号和远程过程执行的结果。

     

    参考书目

     

       [1]  Birrel, A. D., and Nelson, B. J., "Implementing Remote

            Procedure Calls", XEROX CSL-83-7, October 1983.

     

       [2]  Cheriton, D., "VMTP: Versatile Message Transaction Protocol",

            Version 0.7, RFC-1045, Stanford University, February 1988.

     

       [3]  Diffie & Hellman, "Net Directions in Cryptography", IEEE

            Transactions on Information Theory IT-22, November 1976.

     

       [4]  Postel, J., and Harrenstien, K., "Time Protocol", RFC-868,

            Network Information Center, SRI, May 1983.

     

    [5]  National Bureau of Standards, "Data Encryption Standard",

            Federal Information Processing Standards Publication 46,

            January 1977.

     

       [6]  Postel, J., "Transmission Control Protocol - DARPA Internet

            Program Protocol Specification", RFC-793; Network Information

            Center, SRI, September 1981.

     

       [7]  Postel, J., "User Datagram Protocol", RFC-768, Network

            Information Center, SRI, August 1980.

     

       [8]  Reynolds, J. and Postel, J.; "Assigned Numbers", RFC-1010,

            Network Information Center, SRI, May 1987.

     

       [9]  Sun Microsystems; "XDR:  External Data Representation

            Standard", RFC-1014; Sun Microsystems, June 1987.

     

     

                                                 

    RFC1050---Remote Procedure Call Protocol Specification     远程过程调用协议规范

     

     

    展开全文
  • 原因可能为: 1.目录权限  2.操作频繁 ...在“服务”里找到这三个服务,都去启动  Distributed Transaction Coordinator  Remote Procedure Call (RPC)  Security Accounts Manager  如果:Distribu

            最近在进行Word相关的开发时遇到了这个错误,在网上找了很多资料,发现就那么几个解决方案(方案3和方案4),但是都没有很好的解决这个问题。后面在stackoverflow看到一个歪果仁提供的一个思路(方案1),完美解决了这个问题。而后无意中浏览到了MSDN,发现原来微软早就提供了这个问题的解决方案(方案2),可以从根本上避免此类异常的发生,可恶的微软隐藏的这么深。下面特意把这些方案整理出来。


    方案1 捕捉异常,利用委托(delegate)和泛型重复执行异常的方法。

            这个方案可以很完美的解决该问题,主要的思路就是捕获异常。如果获取的异常为该异常重复调用发生异常的方法,知道能够顺利调用完成为止。这个方案定义了两类泛型函数,函数的参数是委托。我们调用时就是把我们可能抛出异常的方法放到委托中通过下面类似的泛型函数进行调用。

            利用委托Action进行泛型的定义:
     public static void RunWithOutRejected<T>(Action<T> action, T t)
            {
                bool hasException;
                do
                {
                    try
                    {
                        action(t);
                        hasException = false;
                    }
                    catch (System.Runtime.InteropServices.COMException e)
                    {
                        if (e.ErrorCode == -2147418111)
                        {
                            hasException = true;
                        }
                        else
                        {
                            throw;
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                } while (hasException);
            }
            
             利用委托Func进行泛型的定义:
    public static T RunWithOutRejected<T>(Func<T> func)
            {
                var result = default(T);
                bool hasException;
                do
                {
                    try
                    {
                        result = func();
                        hasException = false;
                    }
                    catch (System.Runtime.InteropServices.COMException e)
                    {
                        if (e.ErrorCode == -2147418111)
                        {
                            hasException = true;
                        }
                        else
                        {
                            throw;
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                } while (hasException);
                return result;
            }
    
    

            使用泛型的一个例子:
    public void ClearWordRange(Word.Range range)
            {
                Action<Word.Range> action = ExceptionHandlerInnerClearWordRange;
                ExceptionHandler.RunWithOutRejected(action, range);
            }
            private void ExceptionHandlerInnerClearWordRange(Word.Range range)
            {
                range.Text = "";
                range = null;
            }


    方案2 引入IOleMessageFilter,从根本上杜绝这种异常情况的出现

          MSDN地址:https://msdn.microsoft.com/zh-cn/library/ms228772(v=vs.120).aspx

            按照微软的意思,这个异常产生的根本原因是我们自己写的程序和外部的WORD程序间线程征用所致,它定义了一个MessageFilter类继承自IOleMessageFilter来进行这些线程的管理。在它的示例代码中,还引入并打开了IDE,我想它只是为了演示MessageFilter的用法,真正能解决问题的只是在你程序的开始和结束位置分别进行MessageFilter.Register()和MessageFilter.Revoke()就可以了。


    方案3 目录权限问题

            若是权限问题,按如下步骤:

    步骤1打开dcomcnfg 

            在运行里面输入: 
    dcomcnfg 
            在“服务”里找到这三个服务:Distributed Transaction Coordinator、 Remote Procedure Call (RPC) 、Security Accounts Manager ,找到之后都去启动。如果 Distributed Transaction Coordinator 启动不了,就先在运行里面输入: 
    msdtc -resetlog 
            然后再开启:Distributed Transaction Coordinator 。接着关闭服务组件窗体 ,并重新打开,这个时候每个组建就有属性了。然后在命令行中输入:
    dcomcnfg

    步骤2 进行DCOM 配置

            打开“组件服务->计算机->我的电脑->DCOM 配置”,找到“Microsoft Word文档”,单击右键,选择“属性”。在“属性”对话框中单击“标识”选项卡,选择“交互式用户””,关闭“组件服务”管理器。 

             PS:这个方案没有仔细研究。不过操作Word和Excel应该用到的是COM接口,这里却要进行DCOM相关配置,有些让人费解。反正这个方案在我电脑上是无效的。


    方案4 操作频繁问题,在代码中加入延时。

    int m_iErrCnt=0;
    while( true )
    {
          try
        {
         bm0.Range.Text=bandvalue;
         break;
        }
        catch(SystemException err)
       {
                m_iErrCnt++;
               if( m_iErrCnt<10 )
              {
              System.Threading.Thread.Sleep(1000);
                }
              else
              { throw err;}
       }
    
    }
            PS:这个方案可以解决问题,但是不是很完美,因为中间Sleep的时间不好把控,而且Sleep会影响程序的效率。若是在界面主程序中Sleep还会造成界面的无响应状态。

    Github位置:
    https://github.com/HymanLiuTS/OfficeTestByC-
    克隆本项目:
    git clone git@github.com:HymanLiuTS/OfficeTestByC-.git
    获取本文源代码:
    git checkout L16


    展开全文
  • 就是在一相对集中的场所,由一批服务人员组成的服务机构,通常利用计算机通讯技术,处理来自企业、顾客的咨询需求。例如10086热线客服电话就是一call center的例子。 为什么要搭建呼叫中心系统? 伴随着国内经济...

    什么是呼叫中心(call center)?
    呼叫中心(英文是Call Center),博主从事多年AI技术,外呼中心搭建,需要演示站联系博主。就是在一个相对集中的场所,由一批服务人员组成的服务机构,通常利用计算机通讯技术,处理来自企业、顾客的咨询需求。例如10086热线客服电话就是一个call center的例子。

    为什么要搭建呼叫中心系统?
    伴随着国内经济的发展,企业对服务的要求变得越来越重视,大多数企业开始慢慢意识到呼叫中心系统的强大作用。有了呼叫中心后,企业将会提升对客户的服务水平和沟通效率,又或者说呼叫中心是对客户的基本服务平台。更有一些企业利用呼叫中心系统作为进行外呼销售自己产品的渠道。

    搭建一个企业呼叫中心有什么方式?

    一个企业建立呼叫中心一般有以下几种方式。
    第三方采购
    通过呼叫中心第三方销售公司采购完整的呼叫中心系统。
    优点:前期投入生产的速度快,不需要自己招聘相关的专业员工,相对来说系统经过了市场的筛选比较稳定。
    缺点:成本高,市面上的呼叫中心系统都是基于坐席数量进行收费,普通的一般是2000一个坐席,管理人员的一般到3-5K一个坐席,如果企业较大,这种方式成本特别高;另外,由于是购买的第三方的呼叫中心软件使用权,如果需要将呼叫中心与自己本身的业务相结合,基本除了定制外,没有其他的路可走,这种方案也不利于公司长期发展。
    外包公司定制
    通过外包公司定制的方案优劣势与上面一个差不多。最大的区别在于第一个方案是已有成品售卖,一般不会提供特殊定制,而外包公司定制这种,则是根据公司本身的需求进行呼叫中心功能定制,甚至还可以购买源码。
    自主研发呼叫中心系统
    这方式很多稍微大型点的互联网企业可能都会自己做掉。这样,做出来的系统会更适合自己的业务需求,与自己的业务系统越整合方便多了。前期投入肯定是很高的人力成本和基本硬件成本。而且企业自己需要培养一些呼叫中心方面的技术人员,以后都要自己企业里解决各种维护。

    一个呼叫中心系统应该怎样搭建?
    目的
    首先,企业需要明确的是呼叫中心系统搭建的目的。搭建这个呼叫中心是想用来做什么呢?
    是为了企业当做呼入型的客服使用?
    还是用来当电话外呼使用?
    是企业想做营销用呢还是政府单位办公使用?
    是要做外包服务呢还是自己用?
    是想挣钱用呢还是想做客户服务使用?
    需求
    呼叫中心的搭建肯定有需求,这就要把一份需求文档写出来。比如想要做什么,每一点写的清清楚楚:
    需求包括现在有什么?
    碰到了什么问题?
    以后想整成什么样?
    设置,扩容以及升级的快速,成本与灵活性
    一个企业的业务、流程与规模有时候随着市场的快速成长会有很大的变化。这时候,企业的呼叫中心系统就要能够快速的适应市场,能让企业做出适当的调整。比如,呼叫中心系统需要根据要求进行调整座席数量、IVR、ACD等。也就是说,运维人员或管理人员需要能根据多变的需求快速地调整系统的配置。
    与企业自身的通讯系统能够进行整合
    呼叫中心对于一家大企业通常只是一个部门,这个部门只有与企业的其他的部门协同工作,整个企业才能最大程度上发挥它的作用。为了尽可能快速解决客户的一些复杂问题,就会需要呼叫中心与企业现有的电话系统集成为一体,前台和后台能够协同完成工作。比如有一种情况是,当需要设置非客服中心平台的专业空闲座席时,可以方便地将普通分机电话升级为座席电话。

    呼叫中心系统与业务软件
    业务软件是坐席人员用来处理来电记录的办公软件,呼叫中心的软硬件都可以和业务软件分开上系统。比如,一个理想的状态是企业先拥有属于自己的CRM或ERP系统,然后再进行呼叫中心系统集成。这是为了让信息的流转和通话的顺畅度提升的方式。

    搭建呼叫中心有哪些准备步骤?

    现在讲呼叫中心搭建的前期准备步骤。
    电话接入方式
    首先是选择的电话接入方式。呼叫中心有个特性,同一个号码可以N多个人拨打。那么如何让我们的呼叫中心做到一个号码可以多人同时拨打并且同时在线呢?就是中继。中继是把通讯公司的局端交换机与企业内部的用户级交换机(PBX)相连的通讯线路。不同于普通电话,普通电话由于没有用户级交换机,所以是从局端交换机直接用普通电话线接入到座机电话中。
    中继又分为三类:
    模拟中继 – 是普通电话线。
    数字中继 – 会由用户级交换机将局端的光信号转换为30路模拟信号,也就是,一条数字中继线就包含了30路模拟线路。
    IP中继 – 类似于网络电话,是通过网络传输电话信号的一种类型。
    国内呼叫中心目前使用最广泛的一种是数字中继。一条数字中继线有30路模拟线路,与普通电话一样,一条线只能同时有一个通话在工作,所以一条E1的中继线,只能同时有30个通话。
    硬件提供商
    接着是选择硬件提供商。呼叫中心通讯设备厂商,国际知名的Awaya(被收购),国内知名的主要就是东进、三汇、华为、思科。
    板卡类型与坐席规模
    最后,需要选择板卡类型以及确认坐席规模。
    由于选择了数字中继,板卡就一定是是数字板卡。板卡的选型中,需要确定承载CTI与板卡的服务器主板插槽类型以及数量。三汇的数字语音板卡,单卡分别可提供1个、2个、4个、8个中继。比如,采用的单卡是4个中继(即一张板卡支持4条中继线路),每条中继30路,共120条外线通道。再选择SHD-120D-CT/PCI一张以及SHD-30C-CT/PCI。

    坐席规模
    坐席的规模就是呼叫中心需要多少个与CTI连接的大坐席容量电话盒子,还有多少张与电话盒子连接的内线板卡。比如,240个内线坐席,使用了一块240路的SHD-240D-CT/PCI/CAS。这样其实坐席数量很多,只有服务器那么小一个箱子,板卡那么小。所以用到了大容量电话盒子 – SHT-30A/Chbank(30V),每个有30个接到坐席电话的接口,一共8个。
    这样总共是3张PCI板卡,加上要考虑到以后的扩容,服务器主板的插槽配置数是6个PCI插槽。

    一个呼叫中心由哪些组件组成的?

    PSTN(Public Switched Telephone Network,公共交换电话网络):通讯公司提供的电话网络。
    PBX(Private Branch Exchange,用户级交换机):即通讯公司安装在企业内部的局端用户级交换机,整个呼叫中心的出入口设备。
    CTI(Computer Telephony Integration,计算机电话集成):操作PBX与坐席端的互联调度系统。
    ACD(Automatic Call Distribution,自动电话分配):也称排队机 ,更确切地说它是呼叫中心整个前台接入系统逻辑功能的描述,把接入的呼叫转接到正确的座席员桌前。
    IVR(Interactive Voice Response,互动/交互式语音应答):根据用户的输入以及播放语音提示,完成用户的语音业务操作。
    TSR(Telephone Service Representative,电话服务代表/坐席):即各种客服人员终端。
    数据库,存储呼叫中心数据的设备
    录音服务器,存储呼叫中心的通话录音文件
    业务服务器,自有业务系统服务器
    CTILink,一般是双绞线,用于将电话信号输入到CTI服务器。
    WebCall:网页呼叫中心处理系统,一般用户对呼叫中心进行管理,也可作为坐席软件存在。
    IP远端坐席:用于银行等大型企业,这种企业在全国各地都有分支机构,这些分支机构之间相互拨打电话如果通过局端,则会无形中加大公司的成本,而且也不能用于用户呼入后的电话转接。IP远端坐席是将电话连接到互联网上,这样通话就是通过互联网,而不是通过通讯公司,不存在通讯费用。也可称为:网络电话。

    呼叫中心系统架构怎样划分?

    呼叫中心系统的主要模块,可以分为十大模块:
    语音板卡 语音板卡在前面已经提到过在此系统中这里代表物理层。

    CTI 计算机电话集成系统。在此系统中,我们将本应该独立出来的IVR(交互式自动语音应答系统)与ACD(自动呼叫分配)都统一放到CTI服务中,主要是考虑到基于板卡的呼叫中心本身并不能胜任大型的呼叫中心(200坐席以上就非常麻烦了),没有必要再独立出来增加系统的复杂度。

    管理系统 主要对系统的IVR流程进行设计、中继线号码进行配置等。配置系统配置系统,主要对系统运行的环境进行配置,如数据库连接、系统初始化等。

    工单服务 工单服务,用于提交、修改、查看工单信息。

    坐席服务 用于提供TCP接口给坐席客户端进行坐席的签入签出等功能实现,并且将坐席的状态向上报告给CTI的ACD模块。

    报表服务 用于提供WCF接口给坐席客户端进行通话记录查询、接听率查询等功能实现。

    客户关系服务(CRM)用于提供WCF接口给坐席客户端进行客户资料调取、录入等功能实现。

    电话终端 即话务耳机或者手摘电话。

    坐席客户端 坐席员用的客户端系统。

    展开全文
  • 在发起呼叫之前,发起方需要在本地做一些初始化工作,创建两重要的对象:PeerConnectionFactory和PeerConnection,这两C++对象提供了建立WebRTC会话的API(注意:在JS层,没有PeerConnectionFactory,只有...

    1 引言

    当WebRTC的端与信令服务器建立连接之后,可以通过与服务器的信令交互获知对等端点的存在,进一步通过信令向对端发起呼叫。在发起呼叫之前,发起方需要在本地做一些初始化工作,创建两个重要的对象:PeerConnectionFactory和PeerConnection,这两个C++对象提供了建立WebRTC会话的API(注意:在JS层,没有PeerConnectionFactory,只有PeerConnection,是基于C++ API层的全局方法以及PeerConnectionFactory和PeerConnection对象的进一步封装)。

    WebRTC源码分析-呼叫建立过程之二(创建PeerConnectionFactory) 文中已经对PeerConnectionFactory的创建及其功能进行了详尽的分析。 本文将对PeerConnection的创建及其功能进行分析,创建的时机如图中红色字体所示。
    在这里插入图片描述

    2 PeerConnection对象的创建

    在example/peerconnection_client工程中,发起方调用如下代码来创建PeerConnection对象。

      webrtc::PeerConnectionInterface::RTCConfiguration config;
      config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
      config.enable_dtls_srtp = dtls;
      webrtc::PeerConnectionInterface::IceServer server;
      server.uri = "stun:stun.l.google.com:19302";
      config.servers.push_back(server);
    
      peer_connection_ = peer_connection_factory_->CreatePeerConnection(
          config, nullptr, nullptr, this);
    

    2.1 CreatePeerConnection方法参数解析

    WebRTC中PeerConnectionFactory提供了两个重载的CreatePeerConnection方法,其声明如下:

      rtc::scoped_refptr<PeerConnectionInterface> CreatePeerConnection(
          const PeerConnectionInterface::RTCConfiguration& configuration,
          std::unique_ptr<cricket::PortAllocator> allocator,
          std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator,
          PeerConnectionObserver* observer) override;
    
      rtc::scoped_refptr<PeerConnectionInterface> CreatePeerConnection(
          const PeerConnectionInterface::RTCConfiguration& configuration,
          PeerConnectionDependencies dependencies) override;
    

    实际上,第一个方法的后三个参数用于填充PeerConnectionDependencies结构体,然后作为了第二个方法的入参。因此,可以将CreatePeerConnection的参数分为两类:

    • RTCConfiguration: 表征PeerConnection的全局配置项。全局配置项是提供给WebRTC内部使用的参数信息,可以通过参数来控制WebRTC的内部逻辑、行为方式;
    • PeerConnectionDependencies:表征PeerConnection的依赖项。依赖定义了由用户提供的可执行代码,用于执行用户定义的逻辑,其中最重要的就是PeerConnectionObserver,是PeerConnection的事件回调,应用层通过实现这些回调方法来作出自己想要实现的逻辑。

    2.1.1 PeerConnectionDependencies依赖

    PeerConnectionDependencies声明的源码如下:

    struct PeerConnectionDependencies final {
      explicit PeerConnectionDependencies(PeerConnectionObserver* observer_in);
      // This object is not copyable or assignable.
      PeerConnectionDependencies(const PeerConnectionDependencies&) = delete;
      PeerConnectionDependencies& operator=(const PeerConnectionDependencies&) =
          delete;
      // This object is only moveable.
      PeerConnectionDependencies(PeerConnectionDependencies&&);
      PeerConnectionDependencies& operator=(PeerConnectionDependencies&&) = default;
      ~PeerConnectionDependencies();
      // Mandatory dependencies
      PeerConnectionObserver* observer = nullptr;
      // Optional dependencies
      std::unique_ptr<cricket::PortAllocator> allocator;
      std::unique_ptr<webrtc::AsyncResolverFactory> async_resolver_factory;
      std::unique_ptr<rtc::RTCCertificateGeneratorInterface> cert_generator;
      std::unique_ptr<rtc::SSLCertificateVerifier> tls_cert_verifier;
    };
    

    有如下几点需要注意:

    • PeerConnectionDependencies被final修饰,不可以被继承,如果有新的依赖,那么只能修改PeerConnectionDependencies结构本身,并将该依赖以unique_ptr来引入,就如可选依赖那样。这样使得依赖能被PeerConnection持有(将使用move语义),从而可以控制这些依赖的生命周期,使得依赖注入比较安全;
    • PeerConnectionDependencies对象不可以复制或者赋值,只可以被移动。
    • Mandatory dependencies(强制性依赖):只有一个强制性依赖,不可为空,那就是PeerConnectionObserver对象,给应用层提供了介入WebRTC执行逻辑的回调方法,用来监听PeerConnection的所有事件,并执行用户逻辑。
    • Optional dependencies(可选性依赖):可选依赖是外部传入参数可以为空,如果为空,内部将创建默认的;这些参数包括PortAllocator端口分配器,AsyncResolverFactory异步地址解析器,RTCCertificateGenerator证书产生器,SSLCertificateVerifier证书验证器。

    2.1.1.1 强制性依赖PeerConnectionObserver

    PeerConnectionObserver是PeerConnection的回调接口,应用层可以必须提供回调接口的实现,以便响应PeerConnection的事件。这些接口大致分为如下几类:

    几个状态相关回调:

    • OnSignalingChange:信令状态改变。
    • OnConnectionChange:PeerConnection状态改变。

    远端流或者轨道的添加或者移出:

    • OnAddStream:收到远端Peer的一个新stream。
    • OnRemoveStream:收到远端Peer移出一个stream。
    • OnAddTrack:当一个receiver和它的track被创建时。Plan B 和 Unified Plan语法下都会被调用,但是Unified Plan语法下更建议使用OnTrack回调,OnAddTrack只是为了兼容之前的Plan B遗留的接口,二者在同样的情况下被回调。
    • OnTrack:该方法在收到的信令指示一个transceiver将从远端接收媒体时被调用,实际就是在调用SetRemoteDescription时被触发。该接收track可以通过transceiver->receiver()->track()方法被访问到,其关联的streams可以通过transceiver->receiver()->streams()获取。只有在Unified Plan语法下,该回调方法才会被触发。
    • OnRemoveTrack:该方法在收到的信令指示某个track中将不再收到媒体数据时触发。Plan B语法下,对应的receiver将被从PeerConnection中移出,并且对应track将被设置为muted状态;Unified Plan语法下, 对应的receiver将被保留,对应的transceiver将改变direction为仅发送sendonly 或者非活动inactive状态

    ICE过程相关:

    • OnRenegotiationNeeded:需要重新协商时触发,比如重启ICE时。
    • OnIceCandidate:收集到一个新的ICE候选项时触发。
    • OnIceCandidateError:收集ICE选项时出错。
    • OnIceCandidatesRemoved:当候选项被移除时触发。
    • OnStandardizedIceConnectionChange:符合标准的ICE连接状态改变。
    • OnIceGatheringChange:ICE收集状态改变。
    • OnIceConnectionReceivingChange:ICE连接接收状态改变。
    • OnIceSelectedCandidatePairChanged:ICE连接所采用的候选者对改变。

    DataChannel相关:

    • OnDataChannel:当远端打开data channel通道时触发。

    2.1.2 RTCConfiguration配置参数

    RTCConfiguration声明的源码如下,由于源码太长,此处删减了RTCConfiguration的构造函数,删减了RTCConfiguration的getter和setter方法。参数罗列如下,分四类:

    • 静态参数3个;
    • 标准参数6个:与W3C标准提供RTCConfiguration参数一致https://w3c.github.io/webrtc-pc/#rtcconfiguration-dictionary,是最常用的控制参数。
    • Deprecated参数8个:提供给约束使用;
    • 非标准参数31个:涉及到ICE过程相关的参数,音频jitterbuffer的参数,Sdp语法设置等等,这些给与Native开发提供更多的定制化选项。
     struct RTC_EXPORT RTCConfiguration {
     	// 静态参数
        static const int kUndefined = -1;
        // Default maximum number of packets in the audio jitter buffer.
        static const int kAudioJitterBufferMaxPackets = 50;
        // ICE connection receiving timeout for aggressive configuration.
        static const int kAggressiveIceConnectionReceivingTimeout = 1000;
        
        // 标准参数
        IceServers servers;
        IceTransportsType type = kAll;
        BundlePolicy bundle_policy = kBundlePolicyBalanced;
        RtcpMuxPolicy rtcp_mux_policy = kRtcpMuxPolicyRequire;
        std::vector<rtc::scoped_refptr<rtc::RTCCertificate>> certificates;
        int ice_candidate_pool_size = 0;
    		
    	// 提供给约束使用的参数,已被放弃
        bool disable_ipv6 = false;
        bool disable_ipv6_on_wifi = false;
        int max_ipv6_networks = cricket::kDefaultMaxIPv6Networks;
        bool disable_link_local_networks = false;
        bool enable_rtp_data_channel = false;
        absl::optional<int> screencast_min_bitrate;
        absl::optional<bool> combined_audio_video_bwe;
        absl::optional<bool> enable_dtls_srtp;
    
    	// 非标准参数
        TcpCandidatePolicy tcp_candidate_policy = kTcpCandidatePolicyEnabled;
        CandidateNetworkPolicy candidate_network_policy =
            kCandidateNetworkPolicyAll;
        int audio_jitter_buffer_max_packets = kAudioJitterBufferMaxPackets;
        bool audio_jitter_buffer_fast_accelerate = false;
        int audio_jitter_buffer_min_delay_ms = 0;
        bool audio_jitter_buffer_enable_rtx_handling = false;
        int ice_connection_receiving_timeout = kUndefined;
        int ice_backup_candidate_pair_ping_interval = kUndefined;
        ContinualGatheringPolicy continual_gathering_policy = GATHER_ONCE;
        bool prioritize_most_likely_ice_candidate_pairs = false;
        struct cricket::MediaConfig media_config;
        bool prune_turn_ports = false;
        bool presume_writable_when_fully_relayed = false;
        bool enable_ice_renomination = false;
        bool redetermine_role_on_ice_restart = true;
        absl::optional<int> ice_check_interval_strong_connectivity;
        absl::optional<int> ice_check_interval_weak_connectivity;
        absl::optional<int> ice_check_min_interval;
        absl::optional<int> ice_unwritable_timeout;
        absl::optional<int> ice_unwritable_min_checks;
        absl::optional<int> ice_inactive_timeout;
        absl::optional<int> stun_candidate_keepalive_interval;
        absl::optional<rtc::IntervalRange> ice_regather_interval_range;
        webrtc::TurnCustomizer* turn_customizer = nullptr;
        absl::optional<rtc::AdapterType> network_preference;
        SdpSemantics sdp_semantics = SdpSemantics::kPlanB;
        bool active_reset_srtp_params = false;
        bool use_media_transport = false;
        bool use_media_transport_for_data_channels = false;
        absl::optional<CryptoOptions> crypto_options;
        bool offer_extmap_allow_mixed = false;
    

    更多的关于RTCConfiguration 将于以后学习到各个参数所起作用再详述,本章只介绍与W3C标准一致的选项信息。

    2.1.2.1 ICE服务器信息列表 IceServers

    IceServers servers是一个IceServer的列表,每一个列表项IceServer用于存储一个STUN or TURN服务器信息,Peer可以向STUN或者TURN服务器查询候选ip地址。

      struct IceServer {
        std::string uri;
        std::vector<std::string> urls;
        std::string username;
        std::string password;
        std::string hostname;
        TlsCertPolicy tls_cert_policy = kTlsCertPolicySecure;
        std::vector<std::string> tls_alpn_protocols;
        std::vector<std::string> tls_elliptic_curves;
      };
    
    • uri: server的地址,可以存储多个与服务器相关的地址,其格式定义在RFC7064和RFC7065中,单个地址如文章开头的示例所示:“stun:stun.l.google.com:19302”;
    • urls:server的地址列表,用于替代uri;
    • username, password:用于服务器进行用户验证;
    • hostname:当uri或者是urls为直接ip地址时,该字段用来存储hostname;
    • tls_alpn_protocols: TLS的扩展,用于支持应用层协商;
    • tls_elliptic_curves: TLS扩展,椭圆曲线加密算法;
    • tls_cert_policy :TLS证书策略,如下源码和注释。
      // TLS certificate policy.
      enum TlsCertPolicy {
        // 对于基于TLS的协议,确保不会绕过证书验证
        kTlsCertPolicySecure,
        // 对于基于TLS协议,跳过证书验证忽略安全性检查
        kTlsCertPolicyInsecureNoCheck,
      };
    

    2.1.2.2 IceTransportsType type

    这是一个枚举,决定了ICE过程中需要收集哪些候选地址,并且只使用这些地址进行联通性检测。候选地址分为如下几类:主机地址、反射地址、replay中继地址。默认收集所有地址,即kAll,这种方式可以显著的减小使用TurnServer的资源,因为会优先使用host和反射地址。可以通过修改该参数来改变默认行为。

      enum IceTransportsType {
        kNone,    // 不收集ICE候选地址
        kRelay,   // 只收集relay地址
        kNoHost,  // 不收集主机地址
        kAll      // 收集所有地址
      };
    

    2.1.2.3 BundlePolicy bundle_policy

    这是一个枚举,决定了音频轨,视频轨等是否绑定传输,以及如何绑定传输的。参数的详细分析以及如何影响SDP参数的,见 https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-4.1.1。 默认多路音频和多路视频按照媒体类型分别绑定到各自的传输通道,即kBundlePolicyBalanced。

      enum BundlePolicy {
        kBundlePolicyBalanced,   // 多路音频,多路视频按照媒体类型分别绑定传输
        kBundlePolicyMaxBundle,  // 多路音频,多路视频都绑定到一个传输通道
        kBundlePolicyMaxCompat   // 每路音频,每路视频都分开传输。
      };
    
    • kBundlePolicyBalanced:SDP中每种媒体类型(音频、视频、应用数据)第一个m=段(mLine)将包含传输参数。同一种媒体类型的第二个mLine(对于kUnifiedPlan的SDP语法而言)或者是同一媒体类型子序列的mLine(对于kPlanB的SDP语法而言)将被标记为bundle-only。这将导致如果有N个不同的媒体类型,那么候选项将被收集用来产生N个媒体传输流。这个策略平衡了多路复用需求和仍旧可以使用历史遗留接口来协商基本音频和视频的需求。当被呼叫端收到的offer sdp中没有bundle group的信息,那么实现将拒绝所有其他的mLine,只接受每种媒体类型的第一个mLine。
    • kBundlePolicyMaxBundle:仅第一个mLine包含传输参数;所有的其他的的流(除去第一个mLine描述的)将被标记为bundle-only。这个策略目标在于:最小化候选项的收集和最大化的传输通道复用,但是与遗留端点不兼容。当接受端收到这个offer sdp时,实现将只接受第一个mLine和与第一个mLine同属一个bundle group的mLine,其他mLine将被拒绝。
    • kBundlePolicyMaxCompat:所有mLine将包含传输参数,所有mLine将不会标记为bundle-only。这个传输策略允许所有流被“非绑定感知”的端所接收,但对于每个流都需要收集单独的候选项,也即每个流都采用单独的传输通道。

    2.1.2.4 RtcpMuxPolicy rtcp_mux_policy

    这是一个枚举,决定了RTP和RTCP是否复用同一个传输通道。当不复用时,比如RTP采用传输端口8000,那么对应的RTCP端口一般采用8001。参数的详细分析以及如何影响SDP参数的,见 https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-4.1.1

      enum RtcpMuxPolicy {
        kRtcpMuxPolicyNegotiate,  // 双方协商决定
        kRtcpMuxPolicyRequire,    // 必须复用同一个通道
      };
    
    • kRtcpMuxPolicyNegotiate:实现将收集RTP和RTCP的候选项,并且同时会在offer中提供"a=rtcp-mux"这样的属性,这样可以允许接收端点可以选择是采取RTP/RTCP复用还是不复用同一个传输通道。
    • kRtcpMuxPolicyRequire:实现将只收集RTP的候选项,并且同时会在offer中为每个mLine提供"a=rtcp-mux-only"这样的属性,这样将减半需要收集的候选项。应用一个sdp,该sdp中存在mLine没有"a=rtcp-mux"属性,将导致错误并返回。

    2.1.2.5 证书RTCCertificate

    这是一个证书列表std::vector<rtc::scoped_refptr<rtc::RTCCertificate>> certificates,对于每个传输通道都需要一个证书,用于建立连接时进行安全性验证。

    2.1.2.6 候选项池大小ice_candidate_pool_size

    一个典型的WebRTC应用一般在调用setLocalDescription方法后才开始进行ICE过程,开启候选者收集,因为setLocalDescription得到得本地SDP信息中指示了需要的ICE组件个数(跟需要建立的传输通道数相同,而传输通道数与媒体轨道个数、媒体轨道的绑定策略BundlePolicy、RTP/RTCP的通道复用策略RtcpMuxPolicy都有关系)以及哪些候选项(与IceTransportsType参数有关)需要被收集。然而,为了加速媒体通道建立,一些应用如果提前知道需要的ICE组件个数,那么它可以提前(在调用setLocalDescription之前就开始)收集一个池子的ICE候选项来帮助快速的建立媒体通道。

    当setLocalDescription被调用时,开始收集需要的ICE候选者,首先要检查候选项池中是否已经有可用的候选项了。如果候选池中已经有可用的,应该立马通过ICE候选项事件来回调告知应用层已经收集到可用的候选项。如果候选池将耗尽,或者是需要使用的ICE组件个数比预期的要大,或者是候选池没有足够的时间去收集完候选项,那么剩余的候选项将在调用setLocalDescription时照常收集。这个只会出现在首次的offer/answer交换过程中,这个过程结束后候选池将被清空不再被使用。

    举个列子:比如一个应用在不久将来的某个时间点有来电呼叫,并希望尽可能减少建立连接所需要的时间。通过预收集候选项到候选池,使得可以在接收到来电呼叫时,可以立马交换候选项并进行联通性检测。注意持有这些预收集的候选项,并保持这些候选项的有效性,应用将占用STUN/TURN资源。

    该配置选项影响到上述候选池的大小。默认候选池大小为0,也即不进行预收集候选项。

    2.1.2.7 SDP语法 SdpSemantics

    该配置不属于标准配置,但是非常重要,SdpSemantics sdp_semantics,该参数影响到了整个SDP数据的格式。有两种类型的语法:kPlanB 和 kUnifiedPlan

       // For users who wish to send multiple audio/video streams and need to stay
       // interoperable with legacy WebRTC implementations or use legacy APIs,
       // specify kPlanB.
       //
       // For all other users, specify kUnifiedPlan.
    enum class SdpSemantics { kPlanB, kUnifiedPlan };
    
    • WebRTC 1.0 规范必须使用kUnifiedPlan;
    • RtpTransceiver相关的API也只能使用kUnifiedPlan;
    • kPlanB会引发PeerConnection在创建Off或者创建answer的时候,最多一个音频mLine和一个视频mLine。然后,每个mLine中的a=ssrc个数决定了将创建多少个RtpSenders或者RtpReceivers。并且kPlanB会引发PeerConnection忽略SDP中每种媒体类型中的除第一个mLine之外的其他mLine。
    • kUnifiedPlan会引发PeerConnection在创建Off或者创建answer的时候,每一个媒体Track一个mLine。多个mLine映射到一个RtpSender和RtpReceiver(也即一个RtpTransceiver),要么每个都是音频,要么都是视频。 并且kUnifiedPlan会引发PeerConnection忽略每个mLine中除了第一个a=ssrc之外的其他a=ssrc。
    • 只有在希望发送多个音频/视频流并且需要与旧版WebRTC实现保持互操作性或使用旧版API的用户时,才需要指定为kPlanB,其他情况,必须指定为kUnifiedPlan

    2.2 CreatePeerConnection方法的实现

    PeerConnectionFactory.CreatePeerConnection位于pc/peer_connection_factory.cc中。源码如下所示:

    rtc::scoped_refptr<PeerConnectionInterface>
    PeerConnectionFactory::CreatePeerConnection(
        const PeerConnectionInterface::RTCConfiguration& configuration,
        PeerConnectionDependencies dependencies) {
      // 1 断言:改方法必须在信令线程上执行;
      //        外部传入的PortAllocator与PacketSocketFactory只能有其一
      RTC_DCHECK(signaling_thread_->IsCurrent());
      RTC_DCHECK(!(dependencies.allocator && dependencies.packet_socket_factory))
          << "You can't set both allocator and packet_socket_factory; "
             "the former is going away (see bugs.webrtc.org/7447";
    
      // 2 可选依赖为空,此处来创建
      // 2.1 创建证书生成器RTCCertificateGenerator
      if (!dependencies.cert_generator) {
        dependencies.cert_generator =
            std::make_unique<rtc::RTCCertificateGenerator>(signaling_thread_,
                                                           network_thread_);
      }
      // 2.2 创建端口分配器PortAllocator,PortAllocator需要在网络线程中创建,
      //     PacketSocketFactory是构造PortAllocator的参数
      if (!dependencies.allocator) {
        rtc::PacketSocketFactory* packet_socket_factory;
        if (dependencies.packet_socket_factory)
          packet_socket_factory = dependencies.packet_socket_factory.get();
        else
          packet_socket_factory = default_socket_factory_.get();
    
        network_thread_->Invoke<void>(RTC_FROM_HERE, [this, &configuration,
                                                      &dependencies,
                                                      &packet_socket_factory]() {
          dependencies.allocator = std::make_unique<cricket::BasicPortAllocator>(
              default_network_manager_.get(), packet_socket_factory,
              configuration.turn_customizer);
        });
      }
      // 2.3 创建ICE传输工厂
      if (!dependencies.ice_transport_factory) {
        dependencies.ice_transport_factory =
            std::make_unique<DefaultIceTransportFactory>();
      }
    
     // 3. 做一些初始化工作
     // 3.1 在网络线程上执行PortAllocator.SetNetworkIgnoreMask方法,使得端口分配器在进行操作时忽略特定类型的网络。
     //     默认情况network_ignore_mask为ADAPTER_TYPE_LOOPBACK,也即默认情况下忽略回环地址
     //     网络类型有如下几类:
     //         ADAPTER_TYPE_ETHERNET:以太网
     //         ADAPTER_TYPE_WIFI:无线WIFI网络
     //         ADAPTER_TYPE_CELLULAR:蜂窝网络(2g,3g,4g,5g)
     //         ADAPTER_TYPE_VPN:VPN
     //         ADAPTER_TYPE_LOOPBACK:回环地址
      network_thread_->Invoke<void>(
          RTC_FROM_HERE,
          rtc::Bind(&cricket::PortAllocator::SetNetworkIgnoreMask,
                    dependencies.allocator.get(), options_.network_ignore_mask));
      // 3.2 在工作线程上调用PeerConnectionFactory.CreateRtcEventLog_w来创建RtcEventLog对象。
      //     该对象能提供什么功能暂且不提,但注意跟RTC中的一般日志是区别的,RTC_LOG宏与RtcEventLog
      //     是两个不相干的系统。
      std::unique_ptr<RtcEventLog> event_log =
          worker_thread_->Invoke<std::unique_ptr<RtcEventLog>>(
              RTC_FROM_HERE,
              rtc::Bind(&PeerConnectionFactory::CreateRtcEventLog_w, this));
      // 3.3 在工作线程上调用PeerConnectionFactory.CreateCall_w创建Call对象。
      //     Call对象提供了创建AudioReceiveStream/AudioSendStream
      //     /VideoSendStream/VideoReceiveStream的功能
      std::unique_ptr<Call> call = worker_thread_->Invoke<std::unique_ptr<Call>>(
          RTC_FROM_HERE,
          rtc::Bind(&PeerConnectionFactory::CreateCall_w, this, event_log.get()));
    
      // 4. 创建并初始化PeerConnection对象
      // 4.1 构造PeerConnection对象:传入之前创建的RtcEventLog和Call对象
      rtc::scoped_refptr<PeerConnection> pc(
          new rtc::RefCountedObject<PeerConnection>(this, std::move(event_log),
                                                    std::move(call)));
      // 4.2 测试用:在构造PeerConnection对象与初始化PeerConnection对象之间塞入测试代码
      //     该方法为虚方法,{}中无具体代码,因此,什么也不干。测试时,可以继承并实现该方法
      //     以达测试的目的。
      ActionsBeforeInitializeForTesting(pc);
      // 4.3 初始化PeerConnection对象:外部传入的全局配置参数和依赖参数用来初始化PeerConnection
      if (!pc->Initialize(configuration, std::move(dependencies))) {
        return nullptr;
      }
      
      // 5. 创建并返回PeerConnectionProxy对象。
      return PeerConnectionProxy::Create(signaling_thread(), pc);
    }
    

    CreatePeerConnection创建PeerConnection过程大致分为5步,如源码以及注释所示,此处不再赘述。仍有如下几点需要注意。

    • 在步骤3中 PortAllocator设置需要忽略网络类型,以及创建RtcEventLog、Call对象时,都需要在特定的线程上去执行。此时,是通过Thread.Invoke方法提供的这样的能力,该方法的原理分析见文章 WebRTC源码分析-线程基础之消息循环,消息投递
    • 最后 CreatePeerConnection方法最终返回给应用层的是PeerConnectionProxy对象,而非PeerConnection对象。正如上一篇介绍PeerConnectionFactory文章作用一样,PeerConnectionProxy可以使得应用层调用的公有方法,都被代理到PeerConnection对象上执行对应的方法,并且是在恰当的线程中执行。这个是WebRTC中防止线程乱入的通用操作,详细分析见WebRTC源码分析-线程安全之Proxy,防止线程乱入

    2.2.1 创建RtcEventLog对象

    RtcEventLog对象的创建直接依赖于工厂对象RtcEventLogFactory。创建过程如下图所示
    在这里插入图片描述
    需要留意的有如下几点:

    • RtcEventLogFactory在创建PeerConnectionFactory时被创建出来,并被PeerConnectionFactory持有。
    • RtcEventLog对象在创建PeerConnection时被创建出来,并被PeerConnection持有。
    • RtcEventLog类只是Interface,实体类是RtcEventLogImpl,因此,想要详细了解RtcEventLog工作原理,需要进一步对RtcEventLogImpl类进行剖析。
    • PeerConnection持有RtcEventLog类,通过RtcEventLog实现并对外暴露运行事件日志系统、记录事件日志、停止事件日志系统的几个接口。

    RtcEventLog作为WebRTC中的重要模块,将单独列一篇文章来分析 WebRTC源码分析——RtcEventLog事件日志

    2.2.2 创建Call对象

    Call对象的创建直接依赖于工厂对象CallFactory。创建过程如下图所示:
    在这里插入图片描述
    需要留意的有如下几点:

    • CallFactory在创建PeerConnectionFactory时被创建出来,并被PeerConnectionFactory持有。
    • Call对象在创建PeerConnection时被创建出来,并被PeerConnection持有。
    • PeerConnection持有Call,并利用Call对应用层提供了发送码率设置(包含最大码率、最小码率、初始码率,初始码率作为编码器的初始参数以及带宽估计的先验值);提供获取传输统计数据途径(包含估算的可用发送带宽、估算的可用接收带宽、平滑发送引入的延迟、RTT估计值、累计的最大填充bit);提供获取所有发送的数据包回调;另外其还持有PacketReceiver对象,因此,所有接收到RTP/RTCP数据包,也将经过Call。
    • Call对象可以包含多个发送/接收流,且这些流对应同一个远端端点,并共享码率估计等。其对内部还提供了其他重要的功能,最重要的莫过于创建AudioReceiveStream、AudioSendStream、VideoSendStream、VideoReceiveStream的功能。

    Call模块是WebRTC会话中的特别重要的模块,将单列一篇文章来分析 WebRTC源码分析——Call模块

    3 PeerConnection简介

    PeerConnection对象是WebRTC对应用层暴露的重要的API对象,其持有了大量的低层次内部对象,并提供了相当多的功能。一篇文章是不可能尽述的,因此,本文只做粗浅的分析,并着重分析CreatePeerConnection方法中调用的PeerConnection构造函数以及PeerConnection初始化函数Initialize。

    PeerConnection实体类的位于 pc/peer_connection.h 和 pc/peer_connection.cc中,其声明如下:

    // PeerConnection is the implementation of the PeerConnection object as defined
    // by the PeerConnectionInterface API surface.
    // The class currently is solely responsible for the following:
    // - Managing the session state machine (signaling state).
    // - Creating and initializing lower-level objects, like PortAllocator and
    //   BaseChannels.
    // - Owning and managing the life cycle of the RtpSender/RtpReceiver and track
    //   objects.
    // - Tracking the current and pending local/remote session descriptions.
    // The class currently is jointly responsible for the following:
    // - Parsing and interpreting SDP.
    // - Generating offers and answers based on the current state.
    // - The ICE state machine.
    // - Generating stats.
    class PeerConnection : public PeerConnectionInternal,
                           public JsepTransportController::Observer,
                           public RtpSenderBase::SetStreamsObserver,
                           public rtc::MessageHandler,
                           public sigslot::has_slots<> {
                           ...
    }
    

    PeerConnection继承了PeerConnectionInternal、JsepTransportController::Observer、RtpSenderBase::SetStreamsObserver、rtc::MessageHandler、sigslot::has_slots<>,从继承中获取了不少的功能,同时其本身自带了很多功能。尤其注意sigslot::has_slots<>所带来的信号-槽机制,可以让PeerConnection对象接收低层次对象所发射的信号,以获知某个事态的发生、状态的改变,并在自己的方法中对信号进行响应处理。比如后续将要提到的JsepTransportController对象会发射一些列的连接状态,ICE状态相关的信号SignalXxx,PeerConnection将以OnXxx的方法予以绑定信号,响应并处理。

    这些功能大致可以从两方面简要说明(正如PeerConnection英文注释告知的那样):

    一方面PeerConnection单独提供如下功能:

    • 管理WebRTC的会话状态(信令状态);
    • 创建和初始化WebRTC内部的低层次对象,比如PortAllocator、BaseChannels;
    • 持有RtpSender/RtpReceiver、track对象,并管理它们的生命周期;
    • 跟踪当前的local/remote sdp。

    一方面PeerConnection与其他对象一起提供如下功能:

    • 解析sdp;
    • 根据当前状态生成offer sdp和answer sdp;
    • 管理ICE状态机;
    • 产生统计数据。

    3.1 PeerConnection构造

    源码如下:

    PeerConnection::PeerConnection(PeerConnectionFactory* factory,
                                   std::unique_ptr<RtcEventLog> event_log,
                                   std::unique_ptr<Call> call)
        : factory_(factory),
          event_log_(std::move(event_log)),
          event_log_ptr_(event_log_.get()),
          operations_chain_(rtc::OperationsChain::Create()),
          datagram_transport_config_(
              field_trial::FindFullName(kDatagramTransportFieldTrial)),
          datagram_transport_data_channel_config_(
              field_trial::FindFullName(kDatagramTransportDataChannelFieldTrial)),
          rtcp_cname_(GenerateRtcpCname()),
          local_streams_(StreamCollection::Create()),
          remote_streams_(StreamCollection::Create()),
          call_(std::move(call)),
          call_ptr_(call_.get()),
          local_ice_credentials_to_replace_(new LocalIceCredentialsToReplace()),
          data_channel_controller_(this),
          weak_ptr_factory_(this) {}
    

    PeerConnection的构造无非就是给成员赋值,虽然简单,但也给我们提供了一些值得注意的信息,其中想要重点说明的是rtcp_cname_这个成员,接下来的一小节对RTCP CNAME进行比较详细的介绍。

    3.1.1 CNAME

    一个PeerConnection仅有一个RTCP CNAME成员rtcp_cname_,该成员是一个长达16字符的随机字符串,随着PeerConnection的创建,由GenerateRtcpCname()方法创建这个字符串。ietf的RTC规范中详细的描述了RTCP CNAME的作用:https://tools.ietf.org/html/draft-ietf-rtcweb-rtp-usage-26#section-4.9

    以下是对规范中关于RTCP CNAME介绍的一个翻译,有可能解释的不够清楚,请查看上述英文原文:

    RTCP规范名称(CNAME) 为一个RTP端点提供了持久的传输层次的唯一标识。RTP端点的SSRC标识可能会因为检测到和其他RTP端点的SSRC冲突 或者 当RTP应用重启时发生更改,但是CNAME是不会更改的,只要RTCPeerConnection对象没有更改。因此,RTP端点可以很容易的在相关RTP会话集合中识别出与其相关流的RTP包

    一个RTP端点必须至少有一个CNAME,并且在RTCPeerConnection中是唯一的。CNAME可以标识一个特别的同步上下文,所有与这个CNAME关联的SSRCs共享同一个参考时钟。如果一个端点有多个SSRCs,它们关联多个不同步的参考时钟,因此,是不同的同步上下文。那么就需要不同的CNAMEs,每个同步上下文一个CNAME。

    一个WebRTC端点在单个RTCPeerConnection中有且仅有一个CNAME用于属于RTCPeerConnection的多个RTP会话(因为一个RTCPeerConnection就是一个同步上下文)。 RTP middleboxes可能会产生关联不同CANME的多个流,这样就可以避免来自不同端点的多方RTP会话对这些流进行媒体时钟重新同步。

    3.2 PeerConnection初始化

    源码及分析如下:

    bool PeerConnection::Initialize(
        const PeerConnectionInterface::RTCConfiguration& configuration,
        PeerConnectionDependencies dependencies) {
      // 1. 确保初始化是在信令线程中 
      RTC_DCHECK_RUN_ON(signaling_thread());
      TRACE_EVENT0("webrtc", "PeerConnection::Initialize");
    
      // 2. 检查全局参数配置的有效性
      RTCError config_error = ValidateConfiguration(configuration);
      if (!config_error.ok()) {
        RTC_LOG(LS_ERROR) << "Invalid configuration: " << config_error.message();
        return false;
      }
    
      // 3. 依赖参数——PortAllocator和PeerConnectionObserver不可为空
      if (!dependencies.allocator) {
        RTC_LOG(LS_ERROR)
            << "PeerConnection initialized without a PortAllocator? "
               "This shouldn't happen if using PeerConnectionFactory.";
        return false;
      }
      if (!dependencies.observer) {
        // TODO(deadbeef): Why do we do this?
        RTC_LOG(LS_ERROR) << "PeerConnection initialized without a "
                             "PeerConnectionObserver";
        return false;
      }
    
      // 4. 成员赋值
      observer_ = dependencies.observer;
      async_resolver_factory_ = std::move(dependencies.async_resolver_factory);
      port_allocator_ = std::move(dependencies.allocator);
      ice_transport_factory_ = std::move(dependencies.ice_transport_factory);
      tls_cert_verifier_ = std::move(dependencies.tls_cert_verifier);
    
      // 5. 处理STUN server和TURN server
      // 5.1 解析并获取stun_servers和turn_servers
      cricket::ServerAddresses stun_servers;
      std::vector<cricket::RelayServerConfig> turn_servers;
      RTCErrorType parse_error =
          ParseIceServers(configuration.servers, &stun_servers, &turn_servers);
      if (parse_error != RTCErrorType::NONE) {
        return false;
      }
      // 5.2 给所有的turn_server配置日志id
      // Add the turn logging id to all turn servers
      for (cricket::RelayServerConfig& turn_server : turn_servers) {
        turn_server.turn_logging_id = configuration.turn_logging_id;
      }
      // 5.3 给stun_servers和turn_servers进行端口分配器的初始化
      // The port allocator lives on the network thread and should be initialized
      // there.
      const auto pa_result =
          network_thread()->Invoke<InitializePortAllocatorResult>(
              RTC_FROM_HERE,
              rtc::Bind(&PeerConnection::InitializePortAllocator_n, this,
                        stun_servers, turn_servers, configuration));
      // 5.4 通知STUN_SERVER和TURN_SERVER被使用
      // If initialization was successful, note if STUN or TURN servers
      // were supplied.
      if (!stun_servers.empty()) {
        NoteUsageEvent(UsageEvent::STUN_SERVER_ADDED);
      }
      if (!turn_servers.empty()) {
        NoteUsageEvent(UsageEvent::TURN_SERVER_ADDED);
      }
    
      // 6. 发送IPV4/IPv6状态
      // Send information about IPv4/IPv6 status.
      PeerConnectionAddressFamilyCounter address_family;
      if (pa_result.enable_ipv6) {
        address_family = kPeerConnection_IPv6;
      } else {
        address_family = kPeerConnection_IPv4;
      }
      RTC_HISTOGRAM_ENUMERATION("WebRTC.PeerConnection.IPMetrics", address_family,
                                kPeerConnectionAddressFamilyCounter_Max);
    
      const PeerConnectionFactoryInterface::Options& options = factory_->options();
    
      // 7. 创建64位有符号整型会话id
      // RFC 3264: The numeric value of the session id and version in the
      // o line MUST be representable with a "64 bit signed integer".
      // Due to this constraint session id |session_id_| is max limited to
      // LLONG_MAX.
      session_id_ = rtc::ToString(rtc::CreateRandomId64() & LLONG_MAX);
    
      // 8. 1) 填充JSEP传输控制的参数
      //    2) 并创建JsepTransportController,
      //    3) 关联JsepTransportController的信号与PeerConnection的槽方法
      // 8.1.1 创建JSEP的参数结构体JsepTransportController::Config
      JsepTransportController::Config config;
      // 8.1.2 用应用层传入的全局配置参数来填充JSEP传输控制的参数
      config.redetermine_role_on_ice_restart =
          configuration.redetermine_role_on_ice_restart;
      config.ssl_max_version = factory_->options().ssl_max_version;
      config.disable_encryption = options.disable_encryption;
      config.bundle_policy = configuration.bundle_policy;
      config.rtcp_mux_policy = configuration.rtcp_mux_policy;
      // TODO(bugs.webrtc.org/9891) - Remove options.crypto_options then remove this
      // stub.
      config.crypto_options = configuration.crypto_options.has_value()
                                  ? *configuration.crypto_options
                                  : options.crypto_options;
      config.transport_observer = this;
      // It's safe to pass |this| and using |rtcp_invoker_| and the |call_| pointer
      // since the  JsepTransportController instance is owned by this PeerConnection
      // instance and is destroyed before both |rtcp_invoker_| and the |call_|
      // pointer.
      config.rtcp_handler = [this](const rtc::CopyOnWriteBuffer& packet,
                                   int64_t packet_time_us) {
        RTC_DCHECK_RUN_ON(network_thread());
        rtcp_invoker_.AsyncInvoke<void>(
            RTC_FROM_HERE, worker_thread(), [this, packet, packet_time_us] {
              RTC_DCHECK_RUN_ON(worker_thread());
              // |call_| is reset on the worker thread in the PeerConnection
              // destructor, so we check that it's still valid before propagating
              // the packet.
              if (call_) {
                call_->Receiver()->DeliverPacket(MediaType::ANY, packet,
                                                 packet_time_us);
              }
            });
      };
      config.event_log = event_log_ptr_;
    #if defined(ENABLE_EXTERNAL_AUTH)
      config.enable_external_auth = true;
    #endif
      config.active_reset_srtp_params = configuration.active_reset_srtp_params;
      // 8.1.3 外部应该提供MediaTransportFactory时,WebRTC内部应该使用使用DatagramTransport接口取代dtls。
      //       此处填充相关的4个参数。
      use_datagram_transport_ = datagram_transport_config_.enabled &&
                                configuration.use_datagram_transport.value_or(
                                    datagram_transport_config_.default_value);
      use_datagram_transport_for_data_channels_ =
          datagram_transport_data_channel_config_.enabled &&
          configuration.use_datagram_transport_for_data_channels.value_or(
              datagram_transport_data_channel_config_.default_value);
      use_datagram_transport_for_data_channels_receive_only_ =
          configuration.use_datagram_transport_for_data_channels_receive_only
              .value_or(datagram_transport_data_channel_config_.receive_only);
      if (use_datagram_transport_ || use_datagram_transport_for_data_channels_) {
        if (!factory_->media_transport_factory()) {
          RTC_DCHECK(false)
              << "PeerConnecton is initialized with use_datagram_transport = true "
                 "or use_datagram_transport_for_data_channels = true "
              << "but media transport factory is not set in PeerConnectionFactory";
          return false;
        }
    
        config.use_datagram_transport = use_datagram_transport_;
        config.use_datagram_transport_for_data_channels =
            use_datagram_transport_for_data_channels_;
        config.use_datagram_transport_for_data_channels_receive_only =
            use_datagram_transport_for_data_channels_receive_only_;
        config.media_transport_factory = factory_->media_transport_factory();
      }
      // 8.1.4 判断DataChannel传输方式,使用SCTP则填充sctp_factory
      //     首先,根据根据证书是否存在,证书生成器是否存在,是否允许加密来判断是不是dtls使能
      //     然后,确定datachannel传输数据的协议:
      //          DCT_DATA_CHANNEL_TRANSPORT:使用UDP传输,无dtls
      //          DCT_DATA_CHANNEL_TRANSPORT_SCTP:使用UDP传输,失败可以回退到使用SCTP
      //          DCT_RTP:使用RTP传输
      //          DCT_SCTP:使用SCTP传输
      //     最后,在DCT_DATA_CHANNEL_TRANSPORT_SCTP和DCT_SCTP情况下需要填充sctp_factory
      // Obtain a certificate from RTCConfiguration if any were provided (optional).
      rtc::scoped_refptr<rtc::RTCCertificate> certificate;
      if (!configuration.certificates.empty()) {
        // TODO(hbos,torbjorng): Decide on certificate-selection strategy instead of
        // just picking the first one. The decision should be made based on the DTLS
        // handshake. The DTLS negotiations need to know about all certificates.
        certificate = configuration.certificates[0];
      }
      if (options.disable_encryption) {
        dtls_enabled_ = false;
      } else {
        // Enable DTLS by default if we have an identity store or a certificate.
        dtls_enabled_ = (dependencies.cert_generator || certificate);
        // |configuration| can override the default |dtls_enabled_| value.
        if (configuration.enable_dtls_srtp) {
          dtls_enabled_ = *(configuration.enable_dtls_srtp);
        }
      }
      sctp_factory_ = factory_->CreateSctpTransportInternalFactory();
      if (use_datagram_transport_for_data_channels_) {
        if (configuration.enable_rtp_data_channel) {
          RTC_LOG(LS_ERROR) << "enable_rtp_data_channel and "
                               "use_datagram_transport_for_data_channels are "
                               "incompatible and cannot both be set to true";
          return false;
        }
        if (configuration.enable_dtls_srtp && !*configuration.enable_dtls_srtp) {
          RTC_LOG(LS_INFO) << "Using data channel transport with no fallback";
          data_channel_controller_.set_data_channel_type(
              cricket::DCT_DATA_CHANNEL_TRANSPORT);
        } else {
          RTC_LOG(LS_INFO) << "Using data channel transport with fallback to SCTP";
          data_channel_controller_.set_data_channel_type(
              cricket::DCT_DATA_CHANNEL_TRANSPORT_SCTP);
          config.sctp_factory = sctp_factory_.get();
        }
      } else if (configuration.enable_rtp_data_channel) {
        // Enable creation of RTP data channels if the kEnableRtpDataChannels is
        // set. It takes precendence over the disable_sctp_data_channels
        // PeerConnectionFactoryInterface::Options.
        data_channel_controller_.set_data_channel_type(cricket::DCT_RTP);
      } else {
        // DTLS has to be enabled to use SCTP.
        if (!options.disable_sctp_data_channels && dtls_enabled_) {
          data_channel_controller_.set_data_channel_type(cricket::DCT_SCTP);
          config.sctp_factory = sctp_factory_.get();
        }
      }
      // 8.1.5 填充ICE传输工厂
      config.ice_transport_factory = ice_transport_factory_.get();
      // 8.2 创建JsepTransportController
      transport_controller_.reset(new JsepTransportController(
          signaling_thread(), network_thread(), port_allocator_.get(),
          async_resolver_factory_.get(), config));
      // 8.3 绑定JsepTransportController的信号与PeerConnection相应的槽函数
      //     让PeerConnection能够响应JsepTransportController状态的改变
      transport_controller_->SignalIceConnectionState.connect(
          this, &PeerConnection::OnTransportControllerConnectionState);
      transport_controller_->SignalStandardizedIceConnectionState.connect(
          this, &PeerConnection::SetStandardizedIceConnectionState);
      transport_controller_->SignalConnectionState.connect(
          this, &PeerConnection::SetConnectionState);
      transport_controller_->SignalIceGatheringState.connect(
          this, &PeerConnection::OnTransportControllerGatheringState);
      transport_controller_->SignalIceCandidatesGathered.connect(
          this, &PeerConnection::OnTransportControllerCandidatesGathered);
      transport_controller_->SignalIceCandidateError.connect(
          this, &PeerConnection::OnTransportControllerCandidateError);
      transport_controller_->SignalIceCandidatesRemoved.connect(
          this, &PeerConnection::OnTransportControllerCandidatesRemoved);
      transport_controller_->SignalDtlsHandshakeError.connect(
          this, &PeerConnection::OnTransportControllerDtlsHandshakeError);
      transport_controller_->SignalIceCandidatePairChanged.connect(
          this, &PeerConnection::OnTransportControllerCandidateChanged);
    
      // 9. 初始化两个数据统计收集器
      stats_.reset(new StatsCollector(this));
      stats_collector_ = RTCStatsCollector::Create(this);
    
      // 10. 保存全局的配置参数
      configuration_ = configuration;
    
      // 11. 从全局配置参数中抽取ICE相关的参数,并设置到JsepTransportController中
      transport_controller_->SetIceConfig(ParseIceConfig(configuration));
    
      // 12. 设置音视频相关的网络传输相关参数
      video_options_.screencast_min_bitrate_kbps =
          configuration.screencast_min_bitrate;
      audio_options_.combined_audio_video_bwe =
          configuration.combined_audio_video_bwe;
      audio_options_.audio_jitter_buffer_max_packets =
          configuration.audio_jitter_buffer_max_packets;
      audio_options_.audio_jitter_buffer_fast_accelerate =
          configuration.audio_jitter_buffer_fast_accelerate;
      audio_options_.audio_jitter_buffer_min_delay_ms =
          configuration.audio_jitter_buffer_min_delay_ms;
      audio_options_.audio_jitter_buffer_enable_rtx_handling =
          configuration.audio_jitter_buffer_enable_rtx_handling;
    
      // 13. 1)创建WebRtcSessionDescriptionFactory对象
      //     2)绑定WebRtcSessionDescriptionFactory信号与PeerConnection的槽
      //     3)WebRtcSessionDescriptionFactory参数赋值
      // 13.1 创建WebRtcSessionDescriptionFactory对象
      // Whether the certificate generator/certificate is null or not determines
      // what PeerConnectionDescriptionFactory will do, so make sure that we give it
      // the right instructions by clearing the variables if needed.
      if (!dtls_enabled_) {
        dependencies.cert_generator.reset();
        certificate = nullptr;
      } else if (certificate) {
        // Favor generated certificate over the certificate generator.
        dependencies.cert_generator.reset();
      }
      webrtc_session_desc_factory_.reset(new WebRtcSessionDescriptionFactory(
          signaling_thread(), channel_manager(), this, session_id(),
          std::move(dependencies.cert_generator), certificate, &ssrc_generator_));
      // 13.2 绑定WebRtcSessionDescriptionFactory信号与PeerConnection的槽
      webrtc_session_desc_factory_->SignalCertificateReady.connect(
          this, &PeerConnection::OnCertificateReady);
      // 13.3 WebRtcSessionDescriptionFactory参数赋值
      if (options.disable_encryption) {
        webrtc_session_desc_factory_->SetSdesPolicy(cricket::SEC_DISABLED);
      }
      webrtc_session_desc_factory_->set_enable_encrypted_rtp_header_extensions(
          GetCryptoOptions().srtp.enable_encrypted_rtp_header_extensions);
      webrtc_session_desc_factory_->set_is_unified_plan(IsUnifiedPlan());
    
      // 14 Plan B SDP下,添加默认的音视频transceivers
      // Add default audio/video transceivers for Plan B SDP.
      if (!IsUnifiedPlan()) {
        transceivers_.push_back(
            RtpTransceiverProxyWithInternal<RtpTransceiver>::Create(
                signaling_thread(), new RtpTransceiver(cricket::MEDIA_TYPE_AUDIO)));
        transceivers_.push_back(
            RtpTransceiverProxyWithInternal<RtpTransceiver>::Create(
                signaling_thread(), new RtpTransceiver(cricket::MEDIA_TYPE_VIDEO)));
      }
      int delay_ms =
          return_histogram_very_quickly_ ? 0 : REPORT_USAGE_PATTERN_DELAY_MS;
      signaling_thread()->PostDelayed(RTC_FROM_HERE, delay_ms, this,
                                      MSG_REPORT_USAGE_PATTERN, nullptr);
    
      // 15 创建视频比特分配器工厂
      if (dependencies.video_bitrate_allocator_factory) {
        video_bitrate_allocator_factory_ =
            std::move(dependencies.video_bitrate_allocator_factory);
      } else {
        video_bitrate_allocator_factory_ =
            CreateBuiltinVideoBitrateAllocatorFactory();
      }
      return true;
    }
    

    关于PeerConnection初始化过程如源码注释,分了15个部分进行分割阐述。其中比较重要或者复杂的是

    • 处理STUN server和TURN server;
    • 64位有符号整型会话id的创建;
    • JsepTransportController会话传输控制器的创建、信号-槽绑定。这个过程中比较重要的是:外部是否提供MediaTransportFactory,这个会影响到媒体传输方式;DataChannel传输方式的确定,可以是直接的UDP传输,带fallback的UDP传输(fallback到stcp),RTP传输,SCTP传输;JsepTransportController——PeerConnection的信号-槽绑定。
    • SDP工厂类WebRtcSessionDescriptionFactory的创建,信号-槽绑定,参数设置等。

    3.2.1 会话ID——Session ID

    如上对PeerConnection.Initialize方法的分析过程得知:此处会创建 “64位有符号整型"的Session ID。使用的是rtc_base/helpers.cc中随机数生成函数CreateRandomId64()来产生。WebRTC中的随机值生成系统的分析可见另外的文章——WebRTC源码分析——随机值(数、字符串)生成模块

    会话id将会出现在SDP的o line中,如下一个简单的Offser SDP:

    v=0
    o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
    s=
    c=IN IP4 host.anywhere.com
    t=0 0
    m=audio 49170 RTP/AVP 0
    a=rtpmap:0 PCMU/8000
    m=video 51372 RTP/AVP 31
    a=rtpmap:31 H261/90000
    m=video 53000 RTP/AVP 32
    a=rtpmap:32 MPV/90000
    

    o line格式如下

    o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
    

    各字段含义如下:

    • username:发起者的用户名,不允许存在空格,如果应用不支持用户名,则为-。
    • sess-id:会话id,由应用自行定义,规范的建议是NTP(Network Time Protocol)时间戳。
    • sess-version:会话版本,用途由应用自行定义,只要会话数据发生变化时(比如编码),sess-version随着递增就行。同样的,规范的建议是NTP时间戳。
    • nettype:网络类型,比如IN表示Internet。
    • addrtype:地址类型,比如IP4、IV6
    • unicast-address:域名,或者IP地址。

    示例中的2890844526就是取得Session ID值。

    4 总结

    至此,PeerConnection对象的创建过程已经阐述完毕,也对PeerConnection对象提供的能力也做了基本的介绍。简单回顾下整个源码分析过程,有这么几点提炼出来以作最后的总结:

    • PeerConnection的创建需要两方面的数据:依赖项参数 和 全局配置项参数
    • 依赖项参数PeerConnectionDependencies是应用层提供的源码级别的内容,要么作为某个功能模块提供给WebRTC内部调用来影响WebRTC的行为,比如PortAllocator;要么是作为PeerConnection的事件回调,监听PeerConnection的状态,在应用层来做出某些响应,比如PeerConnectionObserver。
    • 全局配置项参数RTCConfiguration是应用层提供给WebRTC全局参数,WebRTC内部通过检查这些参数值做出不同的行为。其中比较重要的参数为SdpSemantics 、IceServers 、IceTransportsType 、BundlePolicy 、RtcpMuxPolicy 、ice_candidate_pool_size等。
    • 与PeerConnectionFactory的创建一样,对于应用层,返回的并非是PeerConnection实体对象,而是PeerConnectionProxy。通过代理方式使得应用层可以安全、方便的使用PeerConnection所带来的功能
    • 注意CNAME的概念与作用。
    展开全文
  • GSM呼叫流程

    千次阅读 2007-01-30 09:58:00
    GSM呼叫流程MS主叫流程设一移动台处于开机并且处于...其实移动台和网络需经过许多步骤才能将呼叫建立起来一、呼叫建立过程移动台首先需建立一与MSC的主信令链路,并要进行鉴权加密及TMSI重分配的过程,详细过程见第
  • 在大部分的企业客户的电话呼叫业务中,特别是从运营商到企业IPPBX端的呼入业务中,有很多不同的呼叫涉及了多种SIP流程的操作,而且其流程和实际的IPPBX,代理和SIP终端存在着非常密切的关系。排查这些技术问题耗费...
  • 在使用word作为模板绑定数据,生成报表时,在循环中易出现错误:  foreach (FormField bm0 in _doc.FormFields) ...System.Runtime.InteropServices.COMException (0x80010001): 被呼叫方拒绝接收呼叫。 (Exce
  • 示例页面应用程序使用WebRTC技术实现了一对多的视频呼叫。换句话说,它是一基于页面的视频广播应用。 5.2.1 运行示例程序 运行这DEMO之前,需要先安装 Kurento Media Server. 另外,还需要先安装JDK (at ...
  • Linux的系统呼叫

    千次阅读 2008-01-13 14:13:00
    第五章5.5 Linux的系统呼叫5.5.1系统呼叫介面系统呼叫(通常称为syscalls)是Linux內核与上层应用程式进行交互通信的唯一介面,参见图5-4所示。从对中断机制的說明可知,用戶程式透过 直接或间接(透过程式库函数)呼叫...
  • 不同类型的呼叫中心技术 让我们面对一事实,即尽可能多的人不熟悉,什么是呼叫中心的基本思想是,如何更与呼叫中心的技术?几问题都被扔在大多数的呼叫中心网上论坛对呼叫中心的大多数人都对呼叫中心技术相关的...
  • 具体的处理流程经过11个步骤: 现在,我们配合SIP消息来进一步说明如何实现即时消息转发方式。 首先,Alice对Bob发送INVITE消息(F1): Bob回复180(F2) : 紧接着Bob对Alice发送 200 OK(F3): Alice对Bob发送...
  • 最近我和@呆呆所在公司的销售谈妥了河南一客户的...不过@呆呆说Asterisk处理300并发,其实是完全没问题的,他以前有过实战案例。@呆呆建议我用winsip在公司机器上做一下压力测试,顺便测试一下新发布的DotAsteris...
  • H323学习笔记四之呼叫流程的建立

    千次阅读 2019-07-29 21:38:50
    前一篇文章提到:H323系统中的一次完整的点到点呼叫通信由五阶段组成:呼叫建立、通信初始化和能力交换、视听通信的建立、呼叫服务、呼叫终止。他们有各自的特点和功能,接下来按序介绍: 呼叫建立 ...
  • 无线病房呼叫系统的设计

    千次阅读 2008-02-23 17:14:00
    摘要:介绍利用单片机的串行全双工通信和高频发射接收电路设计的多路无线医院病房呼叫系统,该系统具有床号、呼叫次数显示、翻查、回复等功能,用于医院病房对护理总台的呼叫,省略了复杂的布线,操作简单,使用方便...
  • 原因可能为: 1.目录权限  ...在“服务”里找到这三个服务,都去启动  Distributed Transaction Coordinator  Remote Procedure Call (RPC)  Security Accounts Manager  如果:Distribu
  • 呼叫中心的知识管理策略

    千次阅读 2005-01-10 10:06:00
    随着越来越多的大公司和组织在考虑把呼叫中心作为他们和用户交互的主要渠道,无论从哪个角度来讲,呼叫中心的业务都将不断增长。 呼叫中心具有明显的业务优势,比如提高效率,增加运营时间,降低成本和高灵活性等等...
  • 1.专业名词 1.1.Abandon Rate —— 电话放弃率 系统已经接通,但在座席应答之前就挂机或下线的电话呼叫占全部接通...(但也有其他的原因)一般来说,系统可以追踪到以下两数据,来电者在挂断电话前等待时长和...
  • 携程高级技术副总裁叶亚明:从呼叫中心到移动互联网的演进   CSDN、ITValue共同发起首届最具价值CTO评选,携程高级技术副总裁叶亚明已参与评选。近期,CTO俱乐部对其进行了专访,请他分享了携程的技术架构...
  • SIP IP 电话系统的呼叫路由

    千次阅读 2005-04-15 11:27:00
    其中用户代理用于发起和接收呼叫, 代理服务器负责对呼叫请求和响应消息进行转发。注册服务器接受用户代理的注册请求并更新定位服务器中用户的地址映射信息。本文将在[1]的基础上对SIP 的呼叫路由过程进行介绍。主要...
  • 基于WebRTC源码下example/peerconnect_client,example/peerconnect_server工程打算写一典型的呼叫建立过程的源码分析系列文章,本文是一序章。example/peerconnect_client与,example/peerconnect_server实现了...
  • [分享]Asterisk 实现模拟彩铃形式呼叫

    千次阅读 2015-10-19 22:39:20
    简单的讲彩铃就是主叫呼叫被叫时,主叫将听到一段音乐或语音..而不是普通的振铃音。   在Asterisk 中 dial() 中加入小写m 即可设置实现播放语音,即实现彩铃。如下示例:  复制代码 exten => _X.,...
  • FSGUI是一免费的FreeSwitch开源VoIP的GUI封装,提供了再次开发的接口,让不懂通信的人也可以快速使用起来。
  • 对接支付宝支付接口开发详细步骤

    万次阅读 多人点赞 2017-08-07 10:25:28
    对接支付宝支付接口,官方文档已经写的很清楚了,但是也有很多像我一样的小白,第一次对接支付宝支付接口,会有些迷茫,所以我在此写下这...创建应用(创建应用需要审核)提供资料审核吧,这时候我们就不用管他了...
  • 呼叫中心业务系统收集到大量数据之后,要将这些信息进行有效利用,从而为... 本文共分三大部分,介绍了数据仓库及数据集市的概念,并对如何获取高质量信息、数据仓库的设计和实现、数据仓库系统中的三个工具层以及
  • 索引使用StepEditorStepEditor流程创建任务...接下来,我们在Step Content List面板中创建了三个任务步骤: 编辑任务步骤属性 选中第一个任务步骤,我们编辑他的属性: 1.勾选Instant:标注此步骤为立即完...
  • CRM的三个级别和四个周期

    千次阅读 2002-07-10 09:19:00
    CRM的三个级别和四个周期 2001-02-05· ·潘维民··中国计算机报 一个企业实施CRM的主要目的是要达到提高竞争力并带来较高的投资回报率。那么一个企业实施的CRM项目能不能达到这个目的和怎样达到这个目的是一个...
  • 工业4.0的三个基本特征三个特征—垂直整合:各机器及生产线的自控系统、工厂的制造执行系统(MES)、以及ERP等系统的整合,打破信息化系统与自动化系统之间的历史鸿沟,使工厂与企业的生产制造能力得以优化。—水平整合...
  • 摘要:本文从快速原型(Rapid Prototyping,RP)这一软件生命周期模型的原理出发,结合呼叫中心(Call Center,CC)软件项目外包的现状,提出应用快速原型模型于呼叫中心软件项目的外包管理,试图以软件工程的工程化...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,368
精华内容 6,947
关键字:

呼叫处理的三个步骤