一、整体流程图
二、全流程抓包
webrtc的server仅处理三种method的http报文:GET、POST、OPTIONS。
GET承载的是Client发送给Server的sign in out信令。
POST承载的是Client通过Server转发给另外一个Client的信令。
OPTIONS暂时没接触到,未知。
1、Client->Server:GET/sign in信令,用于知会Server创建MemberChannel资源。分配Peer ID。在Server返回给Client的200OK的时候,会将Peer ID反馈给Client,用于后续两个Client之间传递协商信令的时候,指明传递路径。
2、Client->Server:GET/wait信令,Client收到Server转发的SDP信令,需要通过wait传递的socket地址信息转发过来。并且转发完一条,就关闭一次socket。所以Client每次收到Server的响应信令的时候,都会检测一下当前通讯的Socket是否释放,若已经释放,缓存转发消息,等待下次接收到GET/wait信令,继续发送。
3、Client->Server:POST/message信令,用于Client与另外一个Client交换音视频能力集和音视频通讯的地址信息。这部分使用的是SDP协议。详细请参见https://tools.ietf.org/html/rfc4566协议文档说明。
4、Client->Server:GET/sign out信令,在Server上释放本Client的MemberChannel信息,断开音视频通讯。
5、Server->Client:HTTP/1.1 200 OK 单纯对Client发送给Server的信令的响应。
6、Server->Client:HTTP/1.1 200 OK(text/plain) Server转发一端Client的信令给另一端Client。
三、源码分解
int main(int argc, char* argv[]) {
std::string program_name = argv[0]; //程序名
std::string usage = "Example usage: " + program_name + " --port=8888";//usage 提示
webrtc::test::CommandLineParser parser; //配置监听端口参数
parser.Init(argc, argv);
parser.SetUsageMessage(usage);
parser.SetFlag("port", "8888");
parser.SetFlag("help", "false");
parser.ProcessFlags();
if (parser.GetFlag("help") == "true") {
parser.PrintUsageMessage();
return 0;
}
int port = strtol((parser.GetFlag("port")).c_str(), NULL, 10);
// Abort if the user specifies a port that is outside the allowed
// range [1, 65535].
if ((port < 1) || (port > 65535)) { //端口号合法性判断
printf("Error: %i is not a valid port.\n", port);
return -1;
}
ListeningSocket listener; //创建监听类
if (!listener.Create()) {
printf("Failed to create server socket\n");
return -1;
} else if (!listener.Listen(port)) { //指定监听端口
printf("Failed to listen on server socket\n");
return -1;
}
printf("Server listening on port %i\n", port);
PeerChannel clients; //创建客户端类,一个room对应一个Clients。
typedef std::vector<DataSocket*> SocketArray;
SocketArray sockets; //创建socket数组。
bool quit = false;
while (!quit) {
fd_set socket_set;
FD_ZERO(&socket_set);
if (listener.valid())
FD_SET(listener.socket(), &socket_set);
for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i)
FD_SET((*i)->socket(), &socket_set); //有报文输入,这里就FD_SET socket参数。
struct timeval timeout = { 10, 0 }; //设置非阻塞式,超时时间为10秒。
if (select(FD_SETSIZE, &socket_set, NULL, NULL, &timeout) == SOCKET_ERROR) {
printf("select failed\n");
break;
}
for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i) {
DataSocket* s = *i;
bool socket_done = true;//true表示该收的报文都已经接受完毕,释放socket,不进入这个检测循环。减少系统空掉时间。
if (FD_ISSET(s->socket(), &socket_set)) { //若socket没有配置,下次循环到FD_SET重新配置。一个异常保护。
if (s->OnDataAvailable(&socket_done) && s->request_received()) {//过滤不能处理的报文,仅处理收到的http报文。
ChannelMember* member = clients.Lookup(s); //仅处理"/wait", "/sign_out", "/message"这三类请求。
if (member || PeerChannel::IsPeerConnection(s)) {
if (!member) { //member资源没有申请
if (s->PathEquals("/sign_in")) {//并且是sign_in请求。在client中增加Peer成员。
clients.AddMember(s);
} else {//否则就是非法报文,直接返回client 500 Err。
printf("No member found for: %s\n",
s->request_path().c_str());
s->Send("500 Error", true, "text/plain", "",
"Peer most likely gone.");
}
} else if (member->is_wait_request(s)) {
// no need to do anything. //member资源已经创建,并且收到http wait请求,不释放socket资源。
socket_done = false; //webrtc原版代码,设计思想是在每次client要发送message报文前,都先发送几个wait出来。所以这里要hold住socket资源。
} else {//member已经创建,并且非wait消息
ChannelMember* target = clients.IsTargetedRequest(s);//除了sign in,其他的http信令都会携带对端的Peer ID,根据这个ID寻找对端的member信息。
if (target) {
member->ForwardRequestToPeer(s, target); //处理要转发给另一个Peer资源的消息。包括协商信令、bye命令等。
} else if (s->PathEquals("/sign_out")) {//对端的member已经释放,并且是sign out信息,直接返回200OK。
s->Send("200 OK", true, "text/plain", "", ""); //返回CLient登出信令200 OK响应。
} else {//找不到对端的member信息,返回Client 500 Error
printf("Couldn't find target for request: %s\n",
s->request_path().c_str());
s->Send("500 Error", true, "text/plain", "",
"Peer most likely gone.");
}
}
} else {
HandleBrowserRequest(s, &quit);//quit信令。用来销毁room和监听端口的。也就是说释放server资源的信令。
if (quit) {
printf("Quitting...\n");
FD_CLR(listener.socket(), &socket_set);
listener.Close();//释放监听端口。
clients.CloseAll();//释放所有client资源,销毁room
}
}
}
} else {
socket_done = false;
}
if (socket_done) {//client每次发给server的源端口号都一直在变化,所以只要当次收包处理完毕,没有报文再过来的时候,都要释放socket资源。防止资源泄露并且空转。
printf("Disconnecting socket\n");
clients.OnClosing(s); //当ChannelMember链接还在,仅仅释放socket。若ChannelMember链接不在,注销ChannelMember信息。
assert(s->valid()); // Close must not have been called yet.
FD_CLR(s->socket(), &socket_set);
delete (*i);//从socket队列组里面释放socket成员。
i = sockets.erase(i);//清除该socket信息。
if (i == sockets.end())
break;
}
}
clients.CheckForTimeout(); //超时判断,若30秒,peer终端没有消息给Server发送http wait消息,就会注销ChannelMember信息。
if (FD_ISSET(listener.socket(), &socket_set)) {
DataSocket* s = listener.Accept();
if (sockets.size() >= kMaxConnections) {//socket资源超过最大连接数。
delete s; // sorry, that's all we can take.
printf("Connection limit reached\n");
} else {
sockets.push_back(s);//push新的socket资源到sockets队列里面。
printf("New connection...\n");
}
}
}
for (SocketArray::iterator i = sockets.begin(); i != sockets.end(); ++i)
delete (*i);//收到quit信令,释放所有socket资源。
sockets.clear();
return 0;
}
四、超时单点分析
ChannelMember::TimedOut函数根据waiting_socket_和time判断是否超时。
bool ChannelMember::TimedOut() {
return waiting_socket_ == NULL && (time(NULL) - timestamp_) > 30;
}
检测到该Client的的waiting_socket_长达30秒,没有信令交互,注销Client的MemberChannel信息。
下面主要分析waiting_socket_和time处理流程:
- ChannelMember::ChannelMember函数:
Peer向Server发送sign_in信令,创建ChannelMember,初始化waiting_socket_指针为NULL。获取当前timestamp_时间。
ChannelMember::ChannelMember(DataSocket* socket)
: waiting_socket_(NULL), id_(++s_member_id_),
connected_(true), timestamp_(time(NULL)) {
assert(socket);
assert(socket->method() == DataSocket::GET);
assert(socket->PathEquals("/sign_in"));
name_ = rtc::s_url_decode(socket->request_arguments());
if (name_.empty())
name_ = "peer_" + int2str(id_);
else if (name_.length() > kMaxNameLength)
name_.resize(kMaxNameLength);
std::replace(name_.begin(), name_.end(), ',', '_');
}
- ChannelMember::OnClosing函数:
Server主函数main检测到socket_done为true时,调用clients.OnClosing(s)函数,当判断出要close的socket与 waiting_socket_一致,则初始化waiting_socket_指针为NULL。
void ChannelMember::OnClosing(DataSocket* ds) {
if (ds == waiting_socket_) {
waiting_socket_ = NULL;
timestamp_ = time(NULL);
}
}
- ChannelMember::SetWaitingSocket函数:
当Server收到Peer发送的wait消息的时候,当queue_里面有消息要发送,直接发送。若没有消息发送,则将新的socket赋值给waiting_socket_。
void ChannelMember::SetWaitingSocket(DataSocket* ds) {
assert(ds->method() == DataSocket::GET);
if (ds && !queue_.empty()) {
assert(waiting_socket_ == NULL);
const QueuedResponse& response = queue_.front();
ds->Send(response.status, true, response.content_type,
response.extra_headers, response.data);
queue_.pop();
} else {
waiting_socket_ = ds;
}
}
waiting_socket_ = ds;
}
}
- ChannelMember::QueueResponse函数:
PeerA转发消息给PeerB,当waiting_socket_不为空,先发送包出去,然后把waiting_socket_指针置空。若waiting_socket_为空,先把消息缓存在消息队列里面。
void ChannelMember::QueueResponse(const std::string& status,
const std::string& content_type,
const std::string& extra_headers,
const std::string& data) {
if (waiting_socket_) {
assert(queue_.size() == 0);
assert(waiting_socket_->method() == DataSocket::GET);
bool ok = waiting_socket_->Send(status, true, content_type, extra_headers,
data);
if (!ok) {
printf("Failed to deliver data to waiting socket\n");
}
waiting_socket_ = NULL;
timestamp_ = time(NULL);
} else {
QueuedResponse qr;
qr.status = status;
qr.content_type = content_type;
qr.extra_headers = extra_headers;
qr.data = data;
queue_.push(qr);
}
}
waiting_socket_ = NULL;
timestamp_ = time(NULL);
} else {
QueuedResponse qr;
qr.status = status;
qr.content_type = content_type;
qr.extra_headers = extra_headers;
qr.data = data;
queue_.push(qr);
}
}
总结下来:
1、waiting_socket_只有收到Client的wait报文,才会被赋值为有效值。
2、当转发结束peer消息到另外一端的时候,会释放waiting_socket_信息。
3、当长时间检测不到Peer与Server有通讯的时候,也会释放waiting_socket_信息。
webrtc这么做的原因是当Server和Client部署在不同的NAT后,当长时间没有socket通讯的时候,会释放NAT上的Session连接资源,下次再连接的时候,会更新Client测的IP地址和端口号信息。所以Server上保存老的Client地址信息也是无效的。干脆就删除这个注册资源。当前的视频通讯走P2P或者中转服务器路径,也用不到这个信息了。
若想长时间保留Client的MemberChannel信息,Client可以修改方案,定时给Server发送wait心跳信息,保存NAT的Session连接。