WebRTC源代码探索之旅——多线程篇-5.1

来源:互联网 发布:阿里云80端口是电信 编辑:程序博客网 时间:2024/05/18 13:11

5 physicalsocketserver

 

physicalsocketserver.h/physicalsocketserver.cc文件实现了一个基本的多路信号分离器。这个多路信号分离器的实现代码可以横跨Windows、Linux、OSX、Android、ios等平台,非常复杂。其实在/trunk/talk/base目录下,还有多个SocketServer的实现,从这些SocketServer的名字就可以看出是针对哪些平台特化实现,比如talk_base::MacCocoaSocketServer。在这里我就不对这些SocketServer进行分析,只要能够明白talk_base::PhysicalSocketServer的原理,再加上对平台API的了解应该很容易读懂这些平台特化的SocketServer。

 

在该文件中将大量使用socket相关的系统调用。由于本篇章的内容为多线程编程,所以尽量不做过多的涉及,并且忽略引用其他文件定义的socket相关类(比如talk_base::PhysicalSocket)。

 

5.1 talk_base::Dispatcher

 

该类定义了事件分发器的纯虚基类。事件分发器主要是将IO或Event信号对应到一定的处理函数上去。该纯虚基类在Windows和Posix2个平台下定义的函数接口有很大的区别。但是主要的功能大致差不多。

 

5.2 talk_base::PhysicalSocket

 

该类是physicalsocketserver.cc文件的内部私有的类,不对外暴露,它主要是对Socket的跨平台封装。由于Windows也提供了基本和BSDsocket一致的Socket API所以该类的代码不难理解。

 

talk_base::PhysicalSocket的主要组件包括:

s_:socket句柄/文件描述符

enabled_events_:需要监听的IO事件

udp_:通信方式是否为UDP

error_:最后出错码(Last Error)

state_:连接状态

resolver_:异步的网址解析器

 

talk_base::PhysicalSocket的主要成员函数包括:

构造函数:创建并初始化talk_base::PhysicalSocket对象

参数说明:

ss:管理的talk_base::SocketServer

s:封装的系统socket句柄/文件描述符;如果不提供该参数, talk_base::PhysicalSocket将创建一个;如果提供talk_base::PhysicalSocket将对其进行封装

 

talk_base::PhysicalSocket::Create:创建系统socket句柄/文件描述符

参数说明:

family:socket的寻址方案(AF_INET/AF_INET6);从这个参数可以看出WebRTC是支持IPV6的

type:socket的类型(TCP/UDP)

 

talk_base::PhysicalSocket::Connect:连接指定的地址和端口,如果地址尚未解析,调用异步的地址解析器

参数说明:

addr:需要连接的地址(使用talk_base::SocketAddress,该类包括地址和端口号)

 

talk_base::PhysicalSocket::EstimateMTU:获取socket连接的MTU(最大传输单元)

参数说明:

mtu:返回最大传输单元

返回值:出错码

原理:

Windows平台调用Ping库获取

IOS和OSX平台没有简单地方法获取,直接返回出错

Linux平台调用getsockopt(s_,IPPROTO_IP, IP_MTU, &value, &vlen)获取

 

由于Windows提供了基本和BSD socket一致的API,所以在Windows的代码和Linux的代码几乎一致。一下将简单对比一下API:

 

socket、getsocketname、getpeername、bind、connect、recv、recvfrom、accept、closesocket

Linux平台下完全一致

 

getsockopt、setsockopt

Linux平台下完全一致(仅有一些特殊的选项不同)

 

send、sendto

在Linux平台下将最后一个参数flag设置为MSG_NOSIGNAL,屏蔽SIGPIPE

 

talk_base::PhysicalSocket需要注意2点:

1虽然它继承自talk_base::AsyncSocket,但是它管理的内部系统socket句柄/描述符是阻塞的(它的子类talk_base::SocketDispatcher会将系统socket句柄/描述符转为非阻塞的)。继承自talk_base:: AsyncSocket的原因可能是为了避免多继承造成类继承结构过于复杂。

2虽然该类具有很多和talk_base::SocketServer相关的成员变量和成员函数,但是talk_base::PhysicalSocketServer不能直接管理talk_base::PhysicalSocket。能被talk_base::PhysicalSocketServer管理的是talk_base::PhysicalSocket的一个子类talk_base::SocketDispatcher,它也是一个physicalsocketserver.cc内部私有的一个类。

当然以上这2点只是实现的细节问题,这些类都是不对外暴露的,我们仅仅需要懂得如何使用对外暴露的接口就可以了。

5.3 talk_base::EventDispatcher

 

talk_base::EventDispatcher类实现了跨平台的等价于Win32自动重置(autoreset)WSAEvent的功能。在Windows平台上有网络多线程开发经验的读者应该很熟悉WSAEvent,我就不再多做介绍了。

 

我在1.1节讲解过talk_base::Event类。那talk_base::PhysicalSocketServer还需要talk_base::EventDispatcher来模拟Event呢?这是应为talk_base::Event仅仅实现了Win32的WaitForSingleObject函数的功能,这无法应用到多路信号分离器里面去。多路信号分离器需要有能力在一个阻塞函数里等待多个event和IO信号的能力,在Windows平台上就是WSAWaitForMultipleEvents函数的功能。

 

与之相类似的在Linux平台上可以使用的API有select函数,不过select函数只能等待IO信号不能等待其他Event。这就很难实现一个阻塞函数同时等待IO信号和消息队列的功能。这对于网络服务器端的开发并不是非常重要,但是对于图形用户界面客户端的开发没有这个功能有时会变得很麻烦。而talk_base::EventDispatcher的职责就是为我们模拟出这个功能。

 

既然select函数已经具有了我们需要的一半功能,怎样才能获得另一半功能呢?比较直接的方法就是将一个event的signal语义转化为IO信号,是的WebRTC就是这么做的。Linux版本的talk_base::EventDispatcher的一个成员变量是一对管道(pipe)的文件描述符。如果需要signal一个talk_base::EventDispatcher,只要对这个管道发送一个字节(内容无所谓),就能打开select函数的阻塞状态。紧接着talk_base::EventDispatcher会立即将管道内的数据读出,talk_base::EventDispatcher重新回到unsignal的状态。这就实现了event的auto reset语义。当然,这种实现并不完美,比如说多个select等待在同一个talk_base::EventDispatcher上就会出问题。WebRTC的开发人员也明白这一点,所以对talk_base::EventDispatcher加上了以下注释:

 

[cpp] view plain copy
  1. // It is not possible to perfectly emulate an auto-resetting event with  
  2. // pipes.  This simulates it byresetting before the event is handled.  

虽然存在以上的问题,但这并不影响talk_base::EventDispatcher配合talk_base::PhysicalSocketServer的工作。因为每一个talk_base::EventDispatcher实例仅隶属于一个talk_base::PhysicalSocketServer,所以不存在多个talk_base::PhysicalSocketServer等待同一个talk_base::EventDispatcher的状况。这也可能是为什么talk_base::EventDispatcher是physicalsocketserver.cc文件私有的原因。

 

通过上面的讲解我们应该可以理解Linux版的talk_base::EventDispatcher的工作原理。以下我将Windows版本和Linux版本的API调用做一下简单类比:

 

WSACreateEvent

pipe函数,用于创建管道

 

WSACloseEvent

close函数,用于关闭管道

 

WSASetEvent

write函数,向管道内写入一个字节数据,用以解锁阻塞的select函数

 

WSAResetEvent

read函数,从管道内读出所有数据,清除管道的可读状态,下次调用select函数时恢复阻塞

5.4 talk_base::PosixSignalHandler

 

talk_base::PosixSignalHandler和talk_base::PosixSignalDispatcher这两个类只有Linux版本。并且在整个WebRTC的源代码中没有任何地方使用过这两个类。因此,对它们的代码分析主要是为了帮助Windows开发人员从实用的角度学习如何使用Linux平台下的部件。

 

talk_base::PosixSignalHandler类主要实现了将Linux的Signal机制纳入到多路分离器的架构中去。对于Windows开发人员来说,Signal机制是一个比较陌生的东西。而且对它的处理比较麻烦。因为Signal会在程序运行的任何时候出现,一旦触发就会调用注册的处理函数,开发人员没法假定这时程序中哪些工具是否可用。正如在talk_base::PosixSignalHandler::OnPosixSignalReceived函数的注释中所说的在出错的时候我们甚至无法记录log:

 

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // Nothing we can do here. If there's an error somehow then there's  
  2. // nothing we can safely do from a signal handler.  
  3. // No, we can't even safely log it.  


 

此外,Linux的有些signal相关的函数在不同版本的Linux/Unix平台以及的表现完全不同。因为,signal机制是一个古老的历史遗留问题,在当年Unix主导一切,各个大公司又各自为战的年代,要提供一个公认完备的标准确实不易。幸运的是从WebRTC的代码来看,几乎没有什么组件使用了signal机制,那就说明绝大多数的现代程序都是可以不使用signal机制就能实现自己想要的功能的。

 

首先,让我们来看一下talk_base::PosixSignalHandler类的工作原理。它被伪装成了一个singleton,而它其实是一个全局唯一的对象,创建后永不释放,直到程序退出时内存泄露。注意,这不是开发人员不小心泄露了内存,而是主动的泄露了内存。在WebRTC中有一个专用的宏(LIBJINGLE_DEFINE_STATIC_LOCAL)用来定义定义这种类实例。如果,在程序中有限的几个类实例被设置成LIBJINGLE_DEFINE_STATIC_LOCAL理论上来说是没有什么负面效果的。因为它不会造成程序在运行期间不断地积累内存泄露直到拖垮整个系统。但是使用这种手法依然需要谨慎。在使用talk_base::PosixSignalHandler的时候,talk_base::PhysicalSocketServer会将 talk_base::PosixSignalHandler::OnPosixSignalReceived函数通过sigaction注册到系统以响应感兴趣signal。当系统发出被监听的signal后OnPosixSignalReceived函数会被调用。该函数会在成员变量received_signal_(数组)中相应的位置上设置为true,并在pipe中写入1个字节的数据(是的,它的核心工作原理和talk_base::EventDispatcher是一样的),这样就能解锁阻塞等待在select函数上的talk_base::PhysicalSocketServer。talk_base::PhysicalSocketServer通过talk_base::PosixSignalHandler::IsSignalSet函数来检查received_signal_数组以确定哪个signal被激活,并调用相应的处理函数。

 

在talk_base::PosixSignalHandler中主要使用的Linux API包括:

pipe:创建管道

 

fcntl:设置文件描述符的选项;该函数在构造函数中被调用,将新创建的一对pipe文件描述符设置为非阻塞。调用代码为:fcntl(afd_[0], F_SETFL, O_NONBLOCK)。不过,根据EventDispatcher构造函数的代码来看,这一步好像没有必要。考虑到EventDispatcher是一个被重度使用的对象,可以确信没有必要把pipe的文件描述符设置为非阻塞(pipe默认应该就是阻塞的,并且在绝大多数系统中IO默认都是阻塞的)。

 

read:从管道读出数据,解除管道文件描述符的可读状态(该函数在talk_base::PosixSignalDispatcher中使用)

 

write:向管道写入数据,以解锁阻塞的select函数

 

close:关闭管道

 

sigaction:将signal处理函数注册到系统,当signum指定编号的signal触发时,系统会调用相应的处理函数(该函数在talk_base::PhysicalSocketServer::InstallSignal中使用)

 

5.5 talk_base::PosixSignalDispatcher

 

该类也是Linux独有的一个类,主要是作为代表talk_base::PosixSignalHandler的分发器,通过将该对象添加入talk_base::PhysicalSocketServer可以将实现接收PosixSignal。它的原理已经在上一节talk_base::PosixSignalHandler中讨论过了,这里就不再多做分析。它的主要函数如下:

 

talk_base::PosixSignalDispatcher::SetHandler:将signal的响应函数加入到分发器

参数说明:

signum:需要响应的signal编号

handler:当signal触发时,响应的处理函数

 

5.6 talk_base::SocketDispatcher

 

talk_base::SocketDispatcher类主要将talk_base::PhysicalSocket封装成一个分发器。所以,在实现上该类仅仅就是为talk_base::PhysicalSocket添加一些talk_base::Dispatcher接口需要的一些成员函数,以及一些状态维护代码。仅有在Linux版本仅有的成员函数talk_base::SocketDispatcher::IsDescriptorClosed中有些比较特殊的情况。通过这个函数中的注释我们发现尚无一些可靠的手段判断一个socket文件描述符是否已经被关闭,所以实现代码使用了::recv(s_, &ch, 1, MSG_PEEK)来判断。在Window版本的talk_base::SocketDispatcher中没有这个成员函数。除此之外,talk_base::SocketDispatcher类的Windows实现和Linux实现基本一致。以下对比一下2个平台API调用的情况:

 

ioctlsocket

fcntl函数用于设置文件描述符的选项(比如阻塞或非阻塞)

 

5.7 talk_base::FileDispatcher

 

talk_base::FileDispatcher类是一个Linux平台独有的类,它的功能是简单将文件描述符封装成Dispatcher。不过它在WebRTC中基本上没有使用过。该类在创建的时候(构造函数中),接受并保存文件描述符,并通过fcntl函数将文件描述符设置为非阻塞。此外它还实现了一些talk_base::Dispatcher接口要求的一些函数,并维护一些状态变量。该类并没有什么难点。

 

5.8 talk_base::Signaler

 

talk_base::Signaler的用处只有一个:实现talk_base::PhysicalSocketServer的signal_wakeup_成员变量(用于解除多路信号分离器的阻塞状态)。它的主要功能由父类talk_base::EventDispatcher实现,仅仅添加了在解除多路信号分离器阻塞状态后将talk_base::PhysicalSocketServer::fwait_成员变量设置为false的代码。talk_base::PhysicalSocketServer::fwait_成员变量在被设置为false后,talk_base::PhysicalSocketServer::Wait函数就会退出,talk_base::MessageQueue就可以及时处理消息队列的消息。

5.9 talk_base::PhysicalSocketServer

 

如果说talk_base::MessageQueue是多路信号分离器的外围,那么talk_base::PhysicalSocketServer就是多路信号分离器的真正核心。talk_base::PhysicalSocketServer主要实现了消息和IO的多路分发功能,类似于Windows平台上的WSAWaitForMultipleEvents的功能。

 

talk_base::PhysicalSocketServer的主要成员变量包括:

dispatchers_:分发器列表

signal_wakeup_:中止talk_base::PhysicalSocketServer::Wait函数的talk_base::Signaler对象(通常在talk_base::MessageQueue接收到事件时调用)

 

talk_base::PhysicalSocketServer的主要成员函数包括:

talk_base::PhysicalSocketServer::CreateSocket:创建一个talk_base::Socket实例,实质为talk_base::PhysicalSocket

参数说明:

family:socket的寻址方案(AF_INET/AF_INET6),说明WebRTC能够支持IPv6

type:socket的类型(TCP/IP)

 

talk_base::PhysicalSocketServer::CreateAsyncSocket:创建一个talk_base::AsyncSocket实例,实质是talk_base::SocketDispatcher。此外,与talk_base::PhysicalSocketServer::CreateSocket函数不同的是创建后的实例立即被添加入talk_base::PhysicalSocketServer的分发器列表(dispatchers_)。用户不需要在调用talk_base::PhysicalSocketServer::Add函数

参数说明:

family:socket的寻址方案(AF_INET/AF_INET6),说明WebRTC能够支持IPv6

type:socket的类型(TCP/IP)

 

talk_base::PhysicalSocketServer::WrapSocket:将一个系统socket句柄/文件描述符封装成talk_base::SocketDispatcher并添加入分发器列表(talk_base::PhysicalSocketServer::dispatchers_)

参数说明:

s:一个系统socket句柄/文件描述符,可以是同步(阻塞)的,也可以是异步(非阻塞)的。如果是同步的,该函数会通过fcntl(Linux)或ioctlsocket(Windows)转成异步的。

 

talk_base::PhysicalSocketServer::Add/Remove:向分发器列表添加/删除一个分发器

参数说明:

pdispatcher:一个添加/删除的talk_base::Dispatcher实例,talk_base::PhysicalSocketServer在下一次IO监听循环中会添加/删除监听它的句柄/文件描述符。注意,是下一次监听循环,新添加的分发器不会影响当前阻塞的select(Linux)函数或WSAWaitForMultipleEvents(Windows)函数。也不会唤醒当前阻塞的select(Linux)函数或WSAWaitForMultipleEvents(Windows)函数。如果开发人员需要添加/删除操作立即生效,需要自行唤醒当前阻塞的select(Linux)函数或WSAWaitForMultipleEvents(Windows)函数。

 

talk_base::PhysicalSocketServer::Wait:实现了多路信号分离器

参数说明:

cmsWait:以毫秒为单位的等待时间,kForever表示永久等待

process_io:是否处理IO

 

talk_base::PhysicalSocketServer的核心代码就在成员函数Wait中。该函数比较复杂我将分几个要点来讲解:

a. 大体流程

在talk_base::PhysicalSocketServer::Wait函数中,代码的主体为IO监听循环。在IO监听循环中,Wait函数首先将分发器列表(talk_base::PhysicalSocketServer::dispatchers_)中所有分发器的IO句柄/文件描述符加入到监听数组(需要注意的是分发器列表已经包括了talk_base::PhysicalSocketServer::signal_wakeup_,激发该分发器可以终止整个IO监听循环,导致Wait函数退出)。接着Wait函数就调用系统的IO阻塞等待函数,在Linux平台上为select,在Windows平台上为WSAWaitForMultipleEvents。Wait函数阻塞等待IO期间释放CPU资源。在阻塞等待API返回时,Wait函数先检查它的返回值。如果是因为等待超时,Wait函数将立即返回。否则,将调用被激发IO句柄/文件描述符的分发器的OnPreEvent和OnEvent函数。最后,检查talk_base::PhysicalSocketServer::fwait_,如果需要继续等待就再次执行IO监听循环,否则就退出Wait函数。整个函数流程如下图所示:

 

 

b. 阻塞等待机制(Windows)

虽然talk_base::PhysicalSocketServer::Wait函数在Windows平台和Linux平台上的流程大体相同,但是在实现细节上却有很大不同。首先让我们先看一下Windows平台。Windows平台调用的等待API是WSAWaitForMultipleEvents,该函数有能力将多个IO句柄使用WSAEventSelect函数绑定到一个WSAEvent句柄上去,并在等待结束后调用WSAEnumNetworkEvents来确定到底哪些IO句柄被激发。所以,Windows版本的talk_base::Dispatcher定义有两个成员函数GetSocket和GetWSAEvent。如果能调用GetSocket函数返回一个有效的socket,那么就将这个socket句柄绑定到一个统一的WSAEvent上;如果不能返回一个有效的socket句柄就继续调用GetWSAEvent,取出分发器的WSAEvent,并把它加入到WSAWaitForMultipleEvents函数的等待数组中去。大致过程如下图所示:

 


 

c. 阻塞等待机制(Linux)

Linux的talk_base::PhysicalSocketServer::Wait相对来说比较简单。它使用select函数等待所有从talk_base::Dispatcher::GetDescriptor返回的文件描述符。所有的文件描述符一视同仁,也没有内置特殊文件描述符。select函数返回后调用相应的talk_base::Dispatcher的事件响应函数OnPreEvent和OnEvent。唯一比较复杂的就是talk_base::PhysicalSocketServer::signal_dispatcher_,具体的原理见5.4节talk_base::PosixSignalHandler。

 

d. 与talk_base::MessageQueue互动

其实,这部分内容已经在前面的章节讲述过一些了,只是比较分散。在这里我将比较全面总结一下:

i.整个多路信号分离器由talk_base::MessageQueue和talk_base::PhysicalSocketServer组成,这2个组件轮流获得控制权。talk_base::MessageQueue最先获得控制权,它会检查自己的消息队列,如果有需要立即处理的消息就马上处理,如果没有就把控制权交给talk_base::PhysicalSocketServer。talk_base::PhysicalSocketServer将等待所有位于其分发器列表(talk_base::PhysicalSocketServer::dispatchers_)的IO句柄/文件描述符。如果有IO句柄/文件描述符被激发,talk_base::PhysicalSocketServer将调用对应的talk_base::Dispatcher的消息响应函数(OnPreEvent、OnEvent)。

ii. 如果在talk_base::PhysicalSocketServer阻塞等待时talk_base::MessageQueue接收到消息,talk_base::MessageQueue将会调用talk_base::PhysicalSocketServer::WakeUp函数激发talk_base::PhysicalSocketServer::signal_wakeup_以解除talk_base::PhysicalSocketServer的阻塞状态。并将talk_base::PhysicalSocketServer::fWait_设置为false,这将导致talk_base::PhysicalSocketServer退出IO监控循环重新将控制权交给talk_base::MessageQueue。talk_base::MessageQueue获得控制权后将立即处理消息,在完成消息处理后再将控制权交给talk_base::PhysicalSocketServer。

 

由于,talk_base::PhysicalSocketServer的实现比较复杂,因此已经无法比较Windows平台和Linux平台的代码。所以,仅仅简单罗列一下Linux平台下的API:

select:用于对IO文件描述符数组进行轮询,阻塞等待IO信号

FD_ZERO:用于初始化一个IO文件描述符数组的宏

FD_SET:用于将IO文件描述符添加入由FD_ZERO初始化的IO文件描述符数组的宏

FD_ISSET:用于检查一个IO文件描述符数组是否包括指定的IO文件描述符。由于select函数在返回时会将没有激发的IO文件描述符剔除掉,所以依然存在于数组中的IO文件描述符表示已经被激发

FD_CLR:从IO文件描述符数组中删除一个指定的IO文件描述符

 

到此位置,我们已经完成了对整个多路信号分离器的分析。但是,这还不是WebRTC线程模型的全部,它还有一个重要的模块——thread,我们将在下一节对它进行分析。
0 0