1. 背景
目前基於Wi-Fi Display的應用越來越少,隨着Android AOSP放棄UI上對Wi-Fi Display的支持,後續對ChromeCast的支持力度會不斷增加。而ChromeCast 則是基於DIAL協議進行的擴展和封裝。
對於多屏幕的支持,目前除了保持對Wi-Fi Display現有的力度,後續會逐漸切換到DIAL協議或Amazon的WhisperPlay SDK的學習上。 此篇則記錄了基於最新的DIAL協議的一些簡單分析。
2. 資料
DIAL specification: http://www.dial-multiscreen.org/dial-protocol-specification(v2.1) 對應的中文版本下載(csdn需1積分):
DIAL Server/Client Sample Code:https://github.com/Netflix/dial-reference
測試設備:2臺android設備(因後續交叉編譯需要有編譯環境,使用公司設別進行測試)
測試抓取的wireshark 封包(server IP:192.168.43.105 Client IP:192.168.43.208 AP:192.168.43.1),使用filter (!(ip.addr == 192.168.43.1)&&!(ip.addr == 8.8.8.8))後只查看server和client通訊
3. 基本原理
DIAL是DIscoveryAndLaunch的縮寫,主要應用是讓second-screen上的應用發現並啓動first-screen設備上的應用。
這裏需要解釋一下一下幾個名詞:
- First-screen : a TV, Blu-ray player, set-top-box, or similar device,可以簡單認爲是大屏幕的設備
- Second-screen : a smartphone, tablet, or similar device,即小屏幕設備
- DIAL Server: a device implementing the server side of the DIAL protocol, usually a first-screen device.
- DIAL Client : a device that can discover and launch applications on a DIAL server, usually a second-screen device.
DIAL Service Discovery
M-Search
- Client端向239.255.255.250:1900地址廣播M-Search消息,消息會設定ST(Seach Target)爲urn:dail-multiscreen-org:service:dial 1。
- Server會監聽該端口,並回復該端口的廣播信息。回覆信息中會包含LOCATION URL(包含本地IP地址)或WAKEUP字段(如果支持WoL或WoWLAN)。Client收到回覆後,需要存儲支持WAKEUP字段的Server信息,用於遠程喚醒。
GET Location URL
- Client端發送HTTP GET消息用於獲取Server更多信息。
- Server回覆消息中會包含Application-URL
DIAL REST Service
4. 代碼學習
4.1 交叉編譯
4.2 Server端
struct mg_context *mg_start(mg_callback_t user_callback, void *user_data, int port) {
struct mg_context *ctx;
// Allocate context and initialize reasonable general case defaults.
// TODO(lsm): do proper error handling here.
ctx = (struct mg_context *) calloc(1, sizeof(*ctx));
//定義用戶的回調函數,參考main函數中定義的回調函數
ctx->user_callback = user_callback;
ctx->user_data = user_data;
//更新socket連接需要使用的port信息
if (!set_ports_option(ctx, port)) {
free_context(ctx);
return NULL;
}
// Ignore SIGPIPE signal, so if browser cancels the request, it
// won't kill the whole process.
(void) signal(SIGPIPE, SIG_IGN);
(void) pthread_mutex_init(&ctx->mutex, NULL);
//cond : worker_thread處理完後通知master_thread 的信號
//sq_empty : worker_thread 消耗時發送的信號
//sq_full : master_thread 接受到新連接時發送的信號
(void) pthread_cond_init(&ctx->cond, NULL);
(void) pthread_cond_init(&ctx->sq_empty, NULL);
(void) pthread_cond_init(&ctx->sq_full, NULL);
// Start master (listening) thread
start_thread(ctx, (mg_thread_func_t) master_thread, ctx);
// Start worker threads
for (int i = 0; i < NUM_THREADS; i++) {
if (start_thread(ctx, (mg_thread_func_t) worker_thread, ctx) != 0) {
cry(fc(ctx), "Cannot start worker thread: %d", ERRNO);
} else {
ctx->num_threads++;
}
}
return ctx;
}
其中SSDP對應的Port爲:56790, DIAL service對應的port爲56789。
static int set_ports_option(struct mg_context *ctx, int port) {
...
// MacOS needs that. If we do not zero it, subsequent bind() will fail.
//DIAL Server監聽本機56789端口的數據,SSDP監聽本機端口56790端口數據
memset(&ctx->local_address, 0, sock_len);
ctx->local_address.sin_family = AF_INET;
ctx->local_address.sin_port = htons((uint16_t) port);
ctx->local_address.sin_addr.s_addr = htonl(INADDR_ANY);
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500 * 1000;
if ((ctx->local_socket = socket(PF_INET, SOCK_STREAM, 6)) == INVALID_SOCKET ||
setsockopt(ctx->local_socket, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)) != 0 ||
setsockopt(ctx->local_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0 ||
bind(ctx->local_socket, (const struct sockaddr *) &ctx->local_address, sock_len) != 0 ||
// TODO(steineldar): Replace 20 (max socket backlog len in connections).
listen(ctx->local_socket, 20) != 0) {
close(ctx->local_socket);
cry(fc(ctx), "%s: cannot bind to port %d: %s", __func__, port, strerror(ERRNO));
success = 0;
} else if (getsockname(ctx->local_socket, (struct sockaddr *) &ctx->local_address, &sock_len)) {
close(ctx->local_socket);
cry(fc(ctx), "%s: %s", __func__, strerror(ERRNO));
success = 0;
}
if (!success) {
ctx->local_socket = INVALID_SOCKET;
close_all_listening_sockets(ctx);
}
return success;
}
static void master_thread(struct mg_context *ctx) {
struct socket accepted;
...
while (ctx->stop_flag == 0) {
memset(&accepted.remote_addr, 0, sock_len);
accepted.sock = accept(ctx->local_socket,
(struct sockaddr *) &accepted.remote_addr, &sock_len);
if (accepted.sock != INVALID_SOCKET) {
// Put accepted socket structure into the queue.
DEBUG_TRACE(("accepted socket %d", accepted.sock));
produce_socket(ctx, &accepted);
}
}
...
}
// Master thread adds accepted socket to a queue
static void produce_socket(struct mg_context *ctx, const struct socket *sp) {
(void) pthread_mutex_lock(&ctx->mutex);
//keep waiting until sq_empty signaled
while (ctx->sq_head - ctx->sq_tail >= (int) ARRAY_SIZE(ctx->queue)) {
(void) pthread_cond_wait(&ctx->sq_empty, &ctx->mutex);
}
assert(ctx->sq_head - ctx->sq_tail < (int) ARRAY_SIZE(ctx->queue));
// Copy socket to the queue and increment head
ctx->queue[ctx->sq_head % ARRAY_SIZE(ctx->queue)] = *sp;
ctx->sq_head++;
DEBUG_TRACE(("queued socket %d", sp->sock));
// After update the connected socket, signal sq_full to work_thread
(void) pthread_cond_signal(&ctx->sq_full);
(void) pthread_mutex_unlock(&ctx->mutex);
}
static void worker_thread(struct mg_context *ctx) {
...
while (ctx->stop_flag == 0 && consume_socket(ctx, &conn->client)) {
...
// Fill in peer IP, port info early so even if SSL setup below fails,
// error handler would have the corresponding info.
// Thanks to Johannes Winkelmann for the patch.
memcpy(&conn->request_info.remote_addr,
&conn->client.remote_addr, sizeof(conn->client.remote_addr));
// Fill in local IP info
socklen_t addr_len = sizeof(conn->request_info.local_addr);
getsockname(conn->client.sock,
(struct sockaddr *) &conn->request_info.local_addr, &addr_len);
process_new_connection(conn);
close_connection(conn);
}
...
(void) pthread_cond_signal(&ctx->cond);
...
}
static int consume_socket(struct mg_context *ctx, struct socket *sp) {
...
// If the queue is empty, wait. We're idle at this point.
while (ctx->sq_head == ctx->sq_tail && ctx->stop_flag == 0) {
pthread_cond_wait(&ctx->sq_full, &ctx->mutex);
}
...
// Copy socket from the queue and increment tail
*sp = ctx->queue[ctx->sq_tail % ARRAY_SIZE(ctx->queue)];
ctx->sq_tail++;
...
(void) pthread_cond_signal(&ctx->sq_empty);
...
}
建立連接的目的是爲了進行數據的處理, 最終進入process_new_connection(),該函數有兩個重要的API:parse_http_request()和handle_request()。
static void handle_mcast() {
...
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("239.255.255.250");
saddr.sin_port = htons(1900);
...
//239.255.255.250 & 127.0.0.1 in the group
mreq.imr_multiaddr.s_addr = inet_addr("239.255.255.250");
mreq.imr_interface.s_addr = inet_addr(ip_addr);
...
while (1) {
addrlen = sizeof(saddr);
if (-1 == (bytes = recvfrom(s, gBuf, sizeof(gBuf) - 1, 0,
(struct sockaddr *)&saddr, &addrlen))) {
perror("recvfrom");
continue;
}
...
if (!strstr(gBuf, "urn:dial-multiscreen-org:service:dial:1")) {
continue;
}
...
if (-1 == sendto(s, send_buf, send_size, 0, (struct sockaddr *)&saddr, addrlen)) {
perror("sendto");
continue;
}
}
}
下面看看DIAL REST Service的request_handler()的流程:
#define RUN_URI "/run"
#define HIDE_URI "/hide"
4.3 Client端
void *DialDiscovery::send_mcast()
{
...
if (-1 == (my_sock = socket(AF_INET, SOCK_DGRAM, 0))) {
perror("socket");
exit(1);
}
...
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("239.255.255.250");
saddr.sin_port = htons(1900);
addrlen = sizeof(saddr);
//往239.255.255.250:1900上廣播M-SEARCH消息
if (-1 == sendto(my_sock, send_buf, send_size, 0, (struct sockaddr *)&saddr, addrlen)) {
perror("sendto");
return 0;
}
...
connection.saddr = saddr;
connection.sock = my_sock;
connection.addrlen = addrlen;
receiveResponses(&connection);
...
}
調用send_mcast(),參考前面代碼。
1. Launch Netflix
調用launchApplication(),最後調用到 DialServer::sendCommand(),對應命令爲:COMMAND_LAUNCH
2. Hide Netflix
調用hideApplication(),最後調用到 DialServer::sendCommand(),對應命令爲:COMMAND_HIDE
3. Stop Netflix
調用stopApplication(),最後調用到 DialServer::sendCommand(),對應命令爲:COMMAND_KILL
4. Netflix status
調用getStatus(),最後調用到 DialServer::sendCommand(),對應命令爲:COMMAND_STATUS
5. Launch YouTube
調用launchApplication(),參考Netflix
6. Hide YouTube
調用hideApplication(),參考Netflix
7. Stop YouTube
調用stopApplication(),參考Netflix
8. YouTube status
調用getStatus(),參考Netflix
9. Run conformance tests
調用runConformance()
10. Wake up on lan/wlan
調用sendMagic(),發送特定格式的封包,用於激活支持WoL或WoWLAN的設備。該封包內容爲6Byte的0xFF
11. QUIT
退出當前loop
int DialServer::sendCommand(
string &url,
int command,
string &payload,
string &responseHeaders,
string &responseBody )
{
...
if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
{
...
}
if ((curl = curl_easy_init()) == NULL)
{
...
}
if (command == COMMAND_LAUNCH || command == COMMAND_HIDE)
{
curl_easy_setopt(curl, CURLOPT_POST, true);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, payload.size());
if( payload.size() )
{
slist = curl_slist_append(slist, "Expect:");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
}
...
}
else if (command == COMMAND_KILL)
{
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
}
...
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &responseHeaders);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
res = curl_easy_perform(curl);
if (slist!=NULL) curl_slist_free_all(slist);
curl_easy_cleanup(curl);
curl_global_cleanup();
return (res == CURLE_OK);
}
關於Curl的信息參考:https://curl.haxx.se/libcurl/c/curl_easy_setopt.html
CURLOPT_POST Issue a HTTP POST request. See CURLOPT_POST
CURLOPT_POSTFIELDSIZE The POST data is this big. See CURLOPT_POSTFIELDSIZE
CURLOPT_POSTFIELDS Send a POST with this data. See CURLOPT_POSTFIELDS
CURLOPT_HTTPHEADER Custom HTTP headers. See CURLOPT_HTTPHEADER
CURLOPT_CUSTOMREQUEST Custom request/method. See CURLOPT_CUSTOMREQUEST
CURLOPT_URL URL to work on. See CURLOPT_URL
CURLOPT_HEADERFUNCTION Callback for writing received headers. See CURLOPT_HEADERFUNCTION
CURLOPT_HEADERDATA Data pointer to pass to the header callback. See CURLOPT_HEADERDATA
CURLOPT_WRITEFUNCTION Callback for writing data. See CURLOPT_WRITEFUNCTION
CURLOPT_WRITEDATA Data pointer to pass to the write callback. See CURLOPT_WRITEDATA
其實Client沒有處理HTTP Response的代碼。這部分應該也不是代碼的關注點。