spice源碼解析之 client-server 通信機制

spice客戶端建立連接從 application.cpp 開始,下面整理下spice客戶端和服務端的連接機制。

1. 首先,spice客戶端打開監聽,添加監聽端口;

static int reds_init_net(void)
{
    if (spice_port != -1) {
        reds->listen_socket = reds_init_socket(spice_addr, spice_port, spice_family);
       ...
        reds->listen_watch = core->watch_add(reds->listen_socket,SPICE_WATCH_EVENT_READ,
                                             reds_accept, NULL);
        ...
    }//初始化監聽服務

    if (spice_secure_port != -1) {
        reds->secure_listen_socket = reds_init_socket(spice_addr, spice_secure_port,
                                                      spice_family);
        ...
        reds->secure_listen_watch = core->**watch_add**(reds->secure_listen_socket,
                                                    SPICE_WATCH_EVENT_READ,
                                                    reds_accept_ssl_connection, NULL);
        ...
    }
    if (spice_listen_socket_fd != -1 ) {
        reds->listen_socket = spice_listen_socket_fd;
        reds->listen_watch = core->watch_add(reds->listen_socket,SPICE_WATCH_EVENT_READ,
                                             reds_accept, NULL);
        ...
    }
    return 0;
}

其中,watch_add 是在qemu中實現的,qemu所做的具體工作就是在一個socket隊列中增加一個新網絡socket,並註冊其對應的網絡事件處理函數reds_accept。當此socket可讀或可寫時,Qemu便調用reds_accept函數來處理網絡事件。

2. application啓動

對應客戶端生成一個_client參數,用來表示客戶端;初始化庫,解析命令行參數,平臺初始化顯示、鍵盤和窗口,初始化監視器、菜單(快捷鍵)和事件偵聽,當一個時間需要實現,會調用process_loop的run()函數。

app->init_remainder();
ret = app->run();

int Application::run()
{
    _exit_code = ProcessLoop::run();
    ...
}

2.processloop::run()

int ProcessLoop::run()
{
    _thread = pthread_self();
    _started = true;
    on_start_running();
   ...
}//獲取線程ID並執行;

3.調用application::on_start_running()–>connect()

void Application::on_start_running()
{
    _foreign_menu.reset(new ForeignMenu(this, _active));
    if (_enable_controller) {
        _controller.reset(new Controller(this));
        return;
    }//使能控制器;
#ifdef USE_GUI
    if (_gui_mode == GUI_MODE_FULL) {
        show_gui();
        return;
    }
#endif 
    connect();//執行與服務器端的連接;
}

下面是連接過程;

bool Application::connect(const std::string& host, int port, int sport, const std::string& password)
{
    if (_state != DISCONNECTED) {
        return false;
    }
    _client.set_target(host, port, sport);//主機:端口號;
    _client.set_password(password);//密碼;
    if (!set_channels_security(port, sport)) {
        return false;
    }//設置通道安全類型;
    register_channels();//初始化各種管道寄存器;
    connect();//建立連接;
    return true;
}
...//建立連接並且進行sll 驗證;

使用RedChannelBase的connect(),進行連接安全類型的設置:

...
RedPeer::connect_unsecure(host, options.unsecure_port);
                link(connection_id, password, protocol);
...
ASSERT(options.allow_secure());
        RedPeer::connect_secure(options, host);
        link(connection_id, password, protocol);
``
檢查連接之後,設置安全連接和非安全連接後調用link()執行連接過程;

void RedChannelBase::link(uint32_t connection_id, const std::string& password, int protocol)

在連接的頭部封裝上連接的信息:
link_mess.connection_id = connection_id;
link_mess.channel_type = _type;
link_mess.channel_id = _id;
link_mess.num_common_caps = get_common_caps().size();
link_mess.num_channel_caps = get_caps().size();
link_mess.caps_offset = sizeof(link_mess);
...
send(buffer, p - buffer);//向服務端發送請求;
delete [] buffer;//信息發送後,刪除buffer,等待服務器迴應;

4. 服務端對服務端進行驗證授權;

調用reds_accept()函數進行對信息進行接收;

static void reds_accept(int fd, int event, void *data)
{
    int socket;
    ...
    if (spice_server_add_client(reds, socket, 0) < 0)
        close(socket);
}//添加客戶端連接;
SPICE_GNUC_VISIBLE int spice_server_add_client(SpiceServer *s, int socket, int skip_auth)
{
    RedLinkInfo *link;
...
    link->skip_auth = skip_auth;
    reds_handle_new_link(link);
    return 0;
}
static void reds_handle_new_link(RedLinkInfo *link)
{
    reds_stream_set_async_error_handler(link->stream, reds_handle_link_error);
    reds_stream_async_read(link->stream,
                           (uint8_t *)&link->link_header,
                           sizeof(SpiceLinkHeader),
                           reds_handle_read_header_done,
                           link);
}//異步接收數據,需要客戶端發送數據,填充link->link_header結束;

reds_handle_read_header_done —> reds_handle_read_link_done —>
reds_handle_auth_mechanism —> reds_get_spice_ticket(link)
如果有sasl的情況調用reds_start_auth_sasl(link)進行sasl授權;
reds_handle_ticket —> reds_handle_link(link) —>

ticket是spice的安全機制,作用是驗證有授權的安全鏈接;Ticketing是一個在SPICE服務器端有密碼和有效時間段的組成。時間過期後,這個Ticketing也就過期了。這個Ticketing是加密的。爲了使用加密,服務器生成一個1024位的RSA密鑰並向客戶端發送公鑰(通過RedLinkInfo)。以上這些函數通過對ticket的驗證,完成對連接的授權。授權完成後,開始建立主通道:

static void reds_handle_main_link(RedLinkInfo *link) 
{
    RedClient *client;
    RedsStream *stream;
    SpiceLinkMess *link_mess;
    uint32_t *caps;
    uint32_t connection_id;
    MainChannelClient *mcc;
    int mig_target = FALSE;
    spice_info(NULL);
    spice_assert(reds->main_channel); 
    link_mess = link->link_mess;
    if (!reds->allow_multiple_clients) {
        reds_disconnect();
    }
    if (link_mess->connection_id == 0) {
        reds_send_link_result(link, SPICE_LINK_ERR_OK);
        while((connection_id = rand()) == 0);
        mig_target = FALSE;
    } else {
        // TODO: make sure link_mess->connection_id is the same
        // connection id the migration src had (use vmstate to store the connection id)
        reds_send_link_result(link, SPICE_LINK_ERR_OK);
        connection_id = link_mess->connection_id;
        mig_target = TRUE;
    }//判斷連接的信息是否是connection_id;
caps = (uint32_t *)((uint8_t *)link_mess + link_mess->caps_offset);
//初始化功能詞數目;
red_client_set_main(client, mcc);//將客戶和連接信息對應;
if (spice_name)
            main_channel_push_name(mcc, spice_name);//保存通道名稱;
        if (spice_uuid_is_set)
            main_channel_push_uuid(mcc, spice_uuid);//保存通道的uuid;
    } else {
        reds_mig_target_client_add(client);
    }
    main_channel_client_start_net_test(mcc, !mig_target);//對主通道進行網絡測試;
}

5.服務端進行網絡測試

用來測試通道的網絡情況,並且可以通過這個測試同步客戶端和服務端的時間信息;

void main_channel_client_start_net_test(MainChannelClient *mcc, int test_rate)
{
    if (!mcc || mcc->net_test_id) {
        return;
    }
    if (test_rate) {
        if (main_channel_client_push_ping(mcc, NET_TEST_WARMUP_BYTES)
            && main_channel_client_push_ping(mcc, 0)
            && main_channel_client_push_ping(mcc, NET_TEST_BYTES)) {
            mcc->net_test_id = mcc->ping_id - 2;
            mcc->net_test_stage = NET_TEST_STAGE_WARMUP;
        }//測試字節大小爲1024X250;
    } else {
        red_channel_client_start_connectivity_monitoring(&mcc->base, CLIENT_CONNECTIVITY_TIMEOUT);//超時;
    }
}

6.客戶端迴應測試請求

receive((uint8_t*)&link_res, sizeof(link_res));
if (link_res != SPICE_LINK_ERR_OK) {
        int error_code = (link_res == SPICE_LINK_ERR_PERMISSION_DENIED) ?
                                SPICEC_ERROR_CODE_CONNECT_FAILED : SPICEC_ERROR_CODE_CONNECT_FAILED;
        THROW_ERR(error_code, "connect failed %u", link_res);
    }
}

客戶端判斷服務端是否返回了連接錯誤信息;如果沒有返回錯誤信息,就就成功建立連接;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章