libed2k源碼導讀:(三)網絡IO

目錄

 

第三章 網絡IO

3.1 數據序列化和反序列化

3.1.1 以向服務器發送數據爲例

3.1.2 序列化和反序列化對象的細節

3.1.3 序列化集合類對象

3.1.4 Tag,tag_list和它們的序列化

3.2 和emule服務器的網絡通信實現

3.3 其他發送給服務器的消息

3.3.1 登錄消息及其響應

3.3.2 服務器消息(Server message)

3.3.3 獲取服務器列表(Get list of servers)

3.3.4 接收服務器狀態(Server status)

3.3.5 服務器鑑定(Server identification)

3.3.6 獲取文件源及其響應(Get sources)

3.3.7 文件查找及其服務器響應消息的處理

3.3.8 其他消息


第三章 網絡IO

這一章分析和網絡傳輸有關的實現,主要內容包括和emule服務器的通信和數據的序列化以及反序列化。

 

3.1 數據序列化和反序列化

libed2k使用了boost提供的序列化模板,通過這個模板可以簡化從對象到字節流的生成方式。按照它提供的機制,需要在可序列化的對象上添加save/load方法以告知序列化模板怎麼將自己轉換爲字節流和從一個字節流中構造自己。

 

3.1.1 以向服務器發送數據爲例

每個數據包都包含包頭(header)和包體(body),頭部形式爲{1字節:協議,4字節長度, 1字節類型(opcode)}統一爲6個字節,body的長度因數據包的類型而異。

待發送數據的格式是被組織爲一個message deque,每個message都是一個std::pair<libed2k_header, std::string>類型的鍵值對,它的鍵爲數據包頭而值爲數據包體。

數據包的序列化在server_connection.hpp的server_connection::do_write方法中,實現大致如下:

  • 它先使用archive::ed2k_oarchive序列化message的數據包body部分。
  • 計算出body的長度後將這個長度值填充進message的數據包頭部(即爲下面代碼中的m_write_order.back().first.m_size賦值)
  • 填充頭部的數據包類型(又稱opcode,下面代碼中的m_write_order.back().first.m_type)
  • 填充頭部的協議碼m_write_order.back().first.m_protocol。

最後再將這個message轉爲一個std::vector<boost::asio::const_buffer>類型的緩衝區對象並通過boost::asio::async_write發送出去。參見下方的代碼:

template<typename T> 
void server_connection::do_write(T& t) 
{ 
    // skip all requests to server before connection opened 
    if (current_operation != scs_handshake && current_operation != scs_start) 
        return; 
    CHECK_ABORTED() 
    last_action_time = time_now(); 
    bool write_in_progress = !m_write_order.empty(); 
    m_write_order.push_back(std::make_pair(libed2k_header(), std::string())); 
    boost::iostreams::back_insert_device<std::string> inserter(m_write_order.back().second); 
    boost::iostreams::stream<boost::iostreams::back_insert_device<std::string> > s(inserter); 
    // Serialize the data first so we know how large it is. 
    archive::ed2k_oarchive oa(s); 
    oa << t; s.flush(); 
    std::string compressed_string = compress_output_data(m_write_order.back().second); 
    if (!compressed_string.empty()) 
    { 
        m_write_order.back().second = compressed_string; 
        m_write_order.back().first.m_protocol = OP_PACKEDPROT;
    } 
    m_write_order.back().first.m_size = m_write_order.back().second.size() + 1; 
    // packet size without protocol type and packet body size field
    m_write_order.back().first.m_type = packet_type<T>::value;
    //DBG("server_connection::do_write " << packetToString(packet_type<T>::value) << " size: " << m_write_order.back().second.size() + 1); 
    if (!write_in_progress) 
    { 
        std::vector<boost::asio::const_buffer> buffers;
        buffers.push_back(boost::asio::buffer(&m_write_order.front().first, header_size)); 
        buffers.push_back(boost::asio::buffer(m_write_order.front().second)); 
        boost::asio::async_write(m_socket, 
            buffers, 
            boost::bind(&server_connection::handle_write, 
                self(), 
                boost::asio::placeholders::error, 
                boost::asio::placeholders::bytes_transferred));
    } 
}

 

3.1.2 序列化和反序列化對象的細節

執行下面序列化作爲數據包體(body)的泛型T對象t時,

archive::ed2k_oarchive oa(s); 
oa << t;

首先使用輸出流對象s初始化ed2k_oarchive對象oa,然後“oa<<t;”調用的是ed2k_oarchive的成員函數serialize_impl,而對於class類型的參數serialize_impl的實現是

//Classes need to be serialize using their special method 
template<typename T> 
inline void serialize_impl(T & val, 
    typename boost::enable_if<boost::is_class<T> >::type* = 0) 
{ 
    val.serialize(*this); 
}

最終調用的是t.serialize(*this)(即當t爲類的實例時oa << t等同於t.serialize(oa))。

 

下面以T = search_request_block爲例分析具體實現細節。當T爲search_request_block類型時,最終上面的代碼就是執行下面對象的serialize方法:

/** * simple wrapper for use in do_write template call */ 
struct search_request_block 
{ 
    search_request_block(search_request& ro) 
    : m_order(ro)
    {
    } 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        //使用ar對象將search_request集合中的元素逐個序列化 
        for (size_t n = 0; n < m_order.size(); n++) 
            ar & m_order[n]; 
    } 
    search_request& m_order; 
};

search_request_block由一個search_request(即deque<search_request_block>)對象初始化,寫入時按次序往Archive對象(這裏是archive::ed2k_oarchive)寫入。

注意archive::ed2k_oarchive中關於‘&’操作符的重載

template<typename T> 
ed2k_oarchive& operator&(T& t) 
{ 
    *this << t; 
    return (*this); 
} 

template<typename T> 
ed2k_oarchive& operator<<(T& t) 
{ 
    serialize_impl(t); 
    return *this; 
} 

ed2k_oarchive& operator <<(std::string& str) 
{ 
    raw_write(str.c_str(), str.size()); 
    return *this; 
} 

// special const 
ed2k_oarchive& operator <<(const std::string& str) 
{ 
    raw_write(str.c_str(), str.size()); return *this; 
}

當ar對象的實例是archive::ed2k_oarchive oa時上方的“ar & m_order[n]”等於“oa << m_order[n]”

總結一下序列化search_request_block時做的事情(上面的函數調用下面的函數):

search_request_block srb(ro); do_write(srb); [注:這裏是server_connection::do_write]

    ==> archive::ed2k_oarchive oa(s); oa << srb; [注:s是輸出流]

        ==> srb.serialize<archive::ed2k_oarchive>(oa) [search_request_block::serialize]

            ==> 循環執行oa & srb.m_order[n]

                ==> oa << srb.m_order[n] [注:m_order是search_request_entry的deque]

                    ==> srb.m_order[n].serialize(oa)

                        ==> search_request_entry::LIBED2K_SERIALIZATION_SPLIT_MEMBER(宏分析見下面)

                            ==> libed2k::archive::split_member(ar, *this) [注:this指向上面的srb.m_order[n]]

                                ==> access::member_save(ar, srb.m_order[n])

                                    ==> srb.m_order[n].save(ar) [注:最終循環調用的是search_request_entry::save方法]

注意上方LIBED2K_SERIALIZATION_SPLIT_MEMBER這個宏的作用是定義它所在類的serialize函數,在這個函數中根據流的類型將調用當前對象的序列化或反序列化方法,如果是輸出流則調用save方法,反之調用load方法。它的實現是:

// split member function serialize funcition into save/load 
#define LIBED2K_SERIALIZATION_SPLIT_MEMBER() 
\ template<class Archive> 
\ void serialize(Archive& ar)
\ { 
\ libed2k::archive::split_member(ar, *this); 
\ } 

template<class Archive, class T> 
inline void split_member( Archive & ar, T & t) 
{ 
    typedef BOOST_DEDUCED_TYPENAME boost::mpl::eval_if< BOOST_DEDUCED_TYPENAME Archive::is_saving, boost::mpl::identity<member_saver<Archive, T> >, boost::mpl::identity<member_loader<Archive, T> > >::type typex; 
    typex::invoke(ar, t); 
}

上面的typex::invoke定義爲:當Archive::is_saving有效時類型爲member_saver<Archive, T>否則爲member_loader<Archive, T>。關於boost::mpl::eval_if的介紹見這裏。注意Archive::is_saving在class ed2k_oarchive中的定義爲:

typedef boost::mpl::bool_<false> is_loading; 
typedef boost::mpl::bool_<true> is_saving;

在class ed2k_iarchive中的定義爲:

typedef boost::mpl::bool_<true> is_loading; 
typedef boost::mpl::bool_<false> is_saving;

在這個server_connection::do_write的例子中Archive泛型類型是ed2k_oarchive(因爲ed2k_oarchive::is_saving == false),所以這裏typex等於boost::mpl::identity<member_saver<Archive, T> >即:

template<class Archive, class T> 
struct member_saver 
{ 
    static void invoke(Archive & ar,T & t) 
    { 
        access::member_save(ar, t); 
    } 
};

上面調用的access:member_save定義如下:

class access 
{ 
public: 
// pass calls to users's class implementation 
template<class Archive, class T> 
static void member_save(Archive & ar, T& t) 
{ t.save(ar); } 

template<class Archive, class T> static void member_load(Archive& ar, T& t) 
{ t.load(ar); } 

};

最終調用的是T類型實例的save方法,在這個例子中即爲search_request_entry::save(oarchive)方法,它的實現如下:

void search_request_entry::save(archive::ed2k_oarchive& ar) 
{ 
    ar & m_type; 
    if (m_type == SEARCH_TYPE_BOOL) 
    { 
        DBG("write: " << toString(static_cast<search_request_entry::SRE_Operation>(m_operator))); 
        // it is bool operator 
        ar & m_operator; 
        return; 
    } 
    if (m_type == SEARCH_TYPE_STR || m_type == SEARCH_TYPE_STR_TAG) 
    { 
        DBG("write string: " << m_strValue); 
        // it is string value 
        boost::uint16_t nSize = static_cast<boost::uint16_t>(m_strValue.size()); 
        ar & nSize; 
        ar & m_strValue; 
        boost::uint16_t nMetaTagSize; 
        // write meta tag if it exists 
        if (m_strMetaName.is_initialized()) 
        { 
            nMetaTagSize = m_strMetaName.get().size(); 
            ar & nMetaTagSize; 
            ar & m_strMetaName.get(); 
        } 
        else if (m_meta_type.is_initialized()) 
        { 
            nMetaTagSize = sizeof(m_meta_type.get()); 
            ar & nMetaTagSize; 
            ar & m_meta_type.get(); 
        } 
        return; 
    } 
    if (m_type == SEARCH_TYPE_UINT32 || m_type == SEARCH_TYPE_UINT64) 
    { 
        (m_type == SEARCH_TYPE_UINT32) ? (ar & m_nValue32) : (ar & m_nValue64); ar & m_operator; 
        boost::uint16_t nMetaTagSize; 
        // write meta tag if it exists 
        if (m_strMetaName.is_initialized()) 
        { 
            nMetaTagSize = m_strMetaName.get().size(); 
            ar & nMetaTagSize; 
            ar & m_strMetaName.get(); 
        } 
        else if (m_meta_type.is_initialized()) 
        { 
            nMetaTagSize = sizeof(m_meta_type.get()); 
            ar & nMetaTagSize; 
            ar & m_meta_type.get(); 
        } 
        return; 
    } 
}

可以看到當寫入一個search_request_entry到網絡流時是先序列化類型“ar & m_type;”到流中,然後根據不同類型寫入不同的值。

由於搜索功能不是我們當前程序的重點,關於search_request_entry序列化再往下的細節這裏就不再分析。

 

使用(基於boost的)序列化框架的小結:

當我們需要序列化和反序列化一個類型爲ClassA對象時,在此ClassA中需要:

  1. 定義從字節流生成對象的方法“void load(archive::ed2k_iarchive&)”
  2. 定義從對象到字節流的方法“void save(archive::ed2k_oarchive& ar);”
  3. 添加LIBED2K_SERIALIZATION_SPLIT_MEMBER()宏,它將視序列化流的類型自動調用save或者load。

當序列化對象時只需要執行以下代碼就可以將對象a序列化到流中了:

archive::ed2k_oarchive oa(output_stream); 
ClassA a(some_value); 
oa << a;

類似的,通過下面的代碼可以從流中反序列化出一個對象:

archive::ed2k_iarchive ia(in_stream); 
ClassA a; 
ia >> a;

 

3.1.3 序列化集合類對象

container_holder是常見的可序列化集合類的容器,它的定義如下:

/** 
 * common container holder structure 
 * contains size_type for read it from archives and some container for data 
 */ 
template<typename size_type, class collection_type> 
struct container_holder 
{ 
    size_type m_size; 
    collection_type m_collection; 
    typedef typename collection_type::iterator Iterator; 
    typedef typename collection_type::value_type elem; 
    container_holder() 
        : m_size(0)
    {} 

    container_holder(const collection_type& coll) 
        : m_size(coll.size()), m_collection(coll) 
    { } 

    void clear()
    { 
        m_collection.clear(); 
        m_size = 0; 
    } 

    template<typename Archive> 
    void save(Archive& ar)
    { 
        // save collection size in field with properly size 
        m_size = static_cast<size_type>(m_collection.size()); 
        ar & m_size; 
        for(Iterator i = m_collection.begin(); i != m_collection.end(); ++i) 
        { 
            ar & *i; 
        } 
    } 

    template<typename Archive> void load(Archive& ar) 
    { 
        ar & m_size; 
        // avoid huge memory allocation 
        if (static_cast<size_t>(m_size) > MAX_COLLECTION_SIZE) 
        { 
            throw libed2k_exception(libed2k::errors::decode_packet_error); 
        } 
        m_collection.resize(static_cast<size_t>(m_size)); 
        for (size_type i = 0; i < m_size; i++) 
        { 
            ar & m_collection[i]; 
        } 
    } 
    //...略過部分代碼 
    LIBED2K_SERIALIZATION_SPLIT_MEMBER() 
};

container_holder的定義中包含了LIBED2K_SERIALIZATION_SPLIT_MEMBER宏(這個宏在上節有詳細說明),所以container_holder序列化的函數爲save,反序列化的函數爲load。

container_holder的第一個泛型參數爲序列化或反序列化“數組大小”時的類型,這個值視協議而定,通常在emule協議中如果消息內包含了一個集合那麼對應的形式一般都是[長度][列表元素1][...][列表元素N],“長度”域的類型與消息的類型有關,1個字節、2個字節、4個字節都有可能。如果協議中某條消息規定了代表一個隊列的長度的域爲2個字節則在container_holder中第一泛型參數可以指定爲ushort,如果協議中定義一個“長度”的域爲4個字節那麼則需要使用boost::uint32_t。注意如果用錯類型會破壞數據結構而導致無法正確序列化和反序列化。

container_holder第二個泛型參數通常是一個STL集合類如queue, vector, list。

在save代碼中可以看到,在序列化container_holder集合時,先將內部大小寫入輸出流中然後再逐個序列化內部集合中的元素。在load的實現中和save相對應,先讀取(反序列化)集合的大小,然後逐個反序列化集合的元素。

 

3.1.4 Tag,tag_list和它們的序列化

 

tag在libed2k中被大量的使用,在emule協議的6.1.3中描述了tag:

Tags are TLV-like (Type, Length, Value) structures which are used for appending optional

data to eMule messages. There are several types of tags, all of them are listed in this section.

When referring to specific tags in protocol messages only the tag type is designated, the reader

should use this section as a reference to determine the exact structure of a protocol message.

Each tag has 4 fields, not all of them serialized into the message:

  1. type - 1 byte integer
  2. name - could be one of the following:
    1. variable length string
    2. 1 byte integer
  3. value - could be one of the following:
    1. 4 byte Integer
    2. 4 byte floating point number
    3. variable length String.
  4. special - 1 byte integer, special tag designator

Tags with Integer values are called Integer tags, similarly we have String and Float tags.

  • The type of a String tag is 2
  • the type of an Integer tag is 3
  • a Float tag is 4.

When tags are encoded on the wire they are encoded in the above order e.g. the type. then the name and finally the value.

  • The `type` is encoded in a single byte.
  • The `name` is encoded in a [2 byte] length-value scheme which is used both for String and Integer names. For example the Integer name 0x15 is encoded by the sequence 0x01 0x00 0x15.
  • Fixed `value` fields (like Integer and Float numbers) are written as they are, string values are encoded by the same length-value scheme as the name.

Note: The names given to the tags have no special protocol meaning and are here only to

ease future reference in protocol message description.

在libed2k中tag對應的數據結構爲base_tag和它的各種繼承類(注:base_tag有純虛函數無法實例化)。base_tag定義如下:

class base_tag 
{ 
public: 
    //...省略部分
    typedef base_tag(tg_nid_type nNameId, bool bNewED2K = true) 
        : m_strName(""), m_nNameId(nNameId), m_bNewED2K(bNewED2K) 
    {} 

    base_tag(const std::string& strName, bool bNewED2K = true) 
        : m_strName(strName), m_nNameId(0), m_bNewED2K(bNewED2K) 
    {} 

    const std::string getName() const; 

    tg_nid_type getNameId() const; 

    virtual void load(archive::ed2k_iarchive& ar); 

    virtual void save(archive::ed2k_oarchive& ar); 

    virtual boost::uint64_t asInt() const; 
    //...省略其他asXXX 
    //...省略“==”重載 

    LIBED2K_SERIALIZATION_SPLIT_MEMBER() 
protected: 
    base_tag(const std::string& strName, tg_nid_type nNameId); 
private: 
    std::string m_strName; 
    tg_nid_type m_nNameId; 
    bool m_bNewED2K; 
};

上面提到對於Tag至少有3個屬性:Type/Name/Value,而Name屬性可以是(一個字節的)整數也可以是一個變長的字符串。在base_tag類中定義的是就是這個Name屬性。從base_tag的構造函數中可以看到,用戶在創建對象時要麼給它指定一個整形的Name屬性要麼就給它一個字符串形式的Name屬性,這二者只有一個有意義且在構造完成之後這個值就不可改變(沒有提供set方法)。

下面以base_tag的派生類typed_tag爲例,說明完整的一個tag是怎麼實現的,typed_tag<T>用於存放固定長度(如uint64,uint32,float)T類型的tag:

template<typename T> 
class typed_tag : public base_tag 
{ 
public: 
    //... 忽略部分聲明 
    typed_tag(tg_nid_type nNameId) 
        : base_tag(nNameId)
    {} 

    typed_tag(const std::string& strName) 
        : base_tag(strName)
    {} 

    // 構造時指定值 
    typed_tag(T t, tg_nid_type nNameId, bool bNewED2K) 
        : base_tag(nNameId, bNewED2K), m_value(t)
    {} 

    typed_tag(T t, const std::string& strName, bool bNewED2K) 
        : base_tag(strName, bNewED2K), m_value(t)
    {} 

    //忽略部分函數 
    virtual tg_type getType() const { return tag_type_number<T>::value; } 

    virtual tg_type getUniformType() const { return tag_uniform_type_number<T>::value; } 

    // can't call one serialize because base class is split 
    void load(archive::ed2k_iarchive& ar) { ar & m_value; } 

    void save(archive::ed2k_oarchive& ar) { base_tag::save(ar); ar & m_value; } 

    virtual bool is_equal(const base_tag* pt) const; 

    virtual boost::uint64_t asInt() const
    {
        return (base_tag::asInt());
    } 

    //... 省略其他asXXX 
    operator T() const { return (m_value); } 
    LIBED2K_SERIALIZATION_SPLIT_MEMBER() 

protected: 
    typed_tag(const std::string& strName, tg_nid_type nNameId) 
        : base_tag(strName, nNameId) 
    { } 

private: 
    T m_value; 
};

在typed_tag類中,值被放在成員“T m_value;”中,在構造函數中可以爲它指定一個值,也可以暫時不指定。上面提到Tag至少有3個屬性:Type/Name/Value中現在還缺少一個Type(基類指定Name),這個屬性可以通過getType獲取。

getType函數返回一個tag_type_number<T>::value,這個tag_type_number的定義如下:

template<typename T> 
struct tag_type_number; 

template<> struct tag_type_number<boost::uint8_t> { static const tg_type value = TAGTYPE_UINT8; }; 

template<> 
struct tag_type_number<boost::uint16_t> { static const tg_type value = TAGTYPE_UINT16; }; 

template<> 
struct tag_type_number<boost::uint32_t> { static const tg_type value = TAGTYPE_UINT32; }; 

template<> struct tag_type_number<boost::uint64_t> { static const tg_type value = TAGTYPE_UINT64; }; 

template<> 
struct tag_type_number<float> { static const tg_type value = TAGTYPE_FLOAT32; }; 

template<> 
struct tag_type_number<bool> { static const tg_type value = TAGTYPE_BOOL; }; 

template<> 
struct tag_type_number<md4_hash> { static const tg_type value = TAGTYPE_HASH16; };

由代碼可見md4_hash對應的tag type是TAGTYPE_HASH16。這裏的其他TAGTYPE_UINT8,TAGTYPE_UINT16等等就是上文協議中說明的“1個字節長度”的值,自此tag的三個屬性就已經集全了。完整的TAG定義見ctag.hpp:

enum tg_types
{
    TAGTYPE_UNDEFINED   = 0x00, // special tag definition for empty objects
    TAGTYPE_HASH16      = 0x01,
    TAGTYPE_STRING      = 0x02,
    TAGTYPE_UINT32      = 0x03,
    TAGTYPE_FLOAT32     = 0x04,
    TAGTYPE_BOOL        = 0x05,
    TAGTYPE_BOOLARRAY   = 0x06,
    TAGTYPE_BLOB        = 0x07,
    TAGTYPE_UINT16      = 0x08,
    TAGTYPE_UINT8       = 0x09,
    TAGTYPE_BSOB        = 0x0A,
    TAGTYPE_UINT64      = 0x0B,

    // Compressed string types
    TAGTYPE_STR1        = 0x11,
    TAGTYPE_STR2,
    // ……
}

 

從base_tag中還派生了兩個類型string_tag和array_tag,分別用於存放字符串值和BLOB值,這三個類的主要區別在於序列化和反序列化,其他的實現都大致相同,我們接下來分析一下各種類型的tag是怎麼序列化的。

首先看值是固定長度的Tag類typed_tag的序列化和反序列化:

// can't call one serialize because base class is split 
void load(archive::ed2k_iarchive& ar) { ar & m_value; } 

// T m_value; T Can be boost::uint64_t,boost::uint32_t... 
void save(archive::ed2k_oarchive& ar) { base_tag::save(ar); ar & m_value; }

注意:

  • 序列化時先調用base_tag::save寫入Type和Name屬性(根據ed2k協議版本的不同會有所區別,詳見base_Tag::save的實現),這裏基類會調用getType純虛函數獲取派生類的Type屬性(因爲base_tag只存放Name)。然後再寫入值。
  • 反序列化時不讀取Type和Name屬性而僅僅讀取Value屬性,所以不可以使用ed2k_iarchive直接反序列化一個Tag對象。而且需要注意基類的base_tag::load不做任何事情,爲tag對象寫入Type和Name屬性需要依賴tag_list的反序列化實現。

Tag列表被定義爲tag_list,它的泛型參數和container_holder的第一個參數一樣用於指定列表大小在序列化的時候需要幾個字節。tag_list的定義如下:

/** 
 * class for tag list representation 
 * used to decode/encode tag list appended sequences in ed2k packets 
 * facade on std::deque stl container 
 */ 
template<typename size_type> class tag_list 
{ 
public: 
    typedef boost::shared_ptr<base_tag> value_type; 
    //...忽略其他成員 
private: 
    std::deque<value_type> m_container; 
};

在libed2k中base_tag是typed_tag<T>、string_tag和array_tag的基類,在上面代碼中的std::deque<value_type> m_container中實際存放的元素實際是這些派生類的值,由於m_container存放的是智能指針,所以在一個tag_list中可以存放所有類型(這裏指具有不同Type屬性和Name屬性的)Tag對象。

tag_list的序列化比較簡單,因爲base_tag以及它的派生類提供了序列化所有Tag屬性的方法。tag_list的序列化代碼如下:

template<typename size_type> 
void tag_list<size_type>::save(archive::ed2k_oarchive& ar) 
{ 
    size_type nSize = static_cast<size_type>(m_container.size()); 
    ar & nSize; 
    for (size_t n = 0; n < m_container.size(); n++) 
    { 
        ar & *(m_container[n].get()); 
    } 
}

先獲取大小,寫入大小,然後逐個序列化內部的Tag對象。

tag_list的反序列化很複雜,前面提到Tag對象的反序列化依賴tag_list的反序列化實現,代碼很長下面只截取一部分:

template<typename size_type> 
void tag_list<size_type>::load(archive::ed2k_iarchive& ar) 
{ 
    size_type nSize; 
    ar & nSize;
    // 首先是獲取tag列表的元素個數 
    for (size_t n = 0; n < nSize; n++)
    { 
        // 開始讀單個tag 
        //(1)讀取Tag的Name屬性和Tag屬性 
        tg_type nType = 0; 
        tg_nid_type nNameId = 0; 
        std::string strName; 
        ar & nType; 
        if (nType & 0x80){ 
            nType &= 0x7F; 
            //注意這是新版協議才支持的數據結構 
            ar & nNameId; 
        } 
        else { 
            boost::uint16_t nLength; 
            ar & nLength; 
            if (nLength == 1){ 
            ar & nNameId; 
        }else{ 
            strName.resize(nLength); ar & strName; 
        } 
    }
    // (2)然後構造typed_tag/array_tag/string_tag對象,塞到內部隊列裏。 
    // don't process bool arrays 
    if (nType == TAGTYPE_BOOLARRAY){} 
    //...省略部分代碼 
    // TODO - add correct bsob handler instead of this stub 
    if (nType == TAGTYPE_BSOB) {} 
    //...省略部分代碼 
    switch (nType) 
    { 
    case TAGTYPE_UINT64: 
        m_container.push_back(value_type(new typed_tag<boost::uint64_t>(strName, nNameId)));
        break; 
        //...忽略UINT32/UINT16/UINT8等 
    case TAGTYPE_HASH16: 
        m_container.push_back(value_type(new typed_tag<md4_hash>(strName, nNameId))); 
        break; 
    case TAGTYPE_BLOB: 
        m_container.push_back(value_type(new array_tag(strName, nNameId))); 
        break; 
    //...忽略N個TAGTYPE 
    default: 
        throw libed2k_exception(errors::invalid_tag_type);
        break; 
    }; 
    // (3) 反序列化typed_tag/array_tag/string_tag對象,在這次纔會寫入Tag的Value屬性。 
    ar & *m_container.back(); 
    } 
}

 

3.2 和emule服務器的網絡通信實現

 

相關的實現在server_connection.cpp中。

首次發起連接:

  1. session::server_connect根據參數指定的服務器IP和端口在IO_SERVICE中異步調用server_connection::start以發起連接。
  2. 在start方法中發起異步調用執行boost::asio::ip::tcp::resolver對象的async_resolve方法解析host和端口,並設置回調函數爲on_name_lookup。
  3. 解析完成後在異步回調on_name_lookup中得到boost::asio::ip::tcp::endpoint,即服務器的IP地址以及端口。然後以它爲參數調用tcp::socket對象的async_connect,並設置建立連接後的回調函數爲on_connection_complete。
  4. 建立連接後的on_connection_complete回調函數中:
    1. 向io_service提交一個異步讀網絡的請求(讀取6個字節數據包頭部數據),設置讀回調爲handle_read_header。
    2. 組織並序列化一個cs_login_request登錄請求對象然後發送出去。
  5. 當asio讀取到6個字節後handle_read_header將被調用(前邊提交了一個異步讀請求),在這個回調中做下列事情:
    1. 檢查頭部{1字節:協議類型,4字節:長度,1字節:opcode}是否有效。
    2. 計算本次傳輸尚未完成的字節數,通常等於(頭部的"長度"成員-1)。僅在opcode∈{OP_SENDINGPART,OP_SENDINGPART_I64,OP_COMPRESSEDPART, OP_COMPRESSEDPART_I64}的特殊情況下,剩餘長度等於與對應opcode有關的固定的一個值。
    3. 如果這個類型的數據包只有頭部(頭部的“長度”成員=1時)則直接調用handle_read_packet,否則再次調用async_read讀取剩餘長度的數據並將回調函數handle_read_packet傳遞給它。

收到完整數據包後的處理:

當程序讀取到數據包剩餘的數據時,回調函數handle_read_packet將被調用,在這個回調函數中將根據協議和操作碼(opcode)的不同分別處理。

 

  1. 首先如果協議是OP_PACKEDPROT(0xD4)則意味着該條數據是壓縮過的,先解壓他然後繼續處理,否則跳過這一步。
  2. 轉換接收到數據爲輸入流以方便接下來解析數據。
  3. 然後按照不同操作碼分別處理,具體見下表3-1。

操作碼

what is it

how to process

OP_REJECT(5)

(see 6.2.16)A message sent from the server to the client indicating that the server rejected the last

command sent by the client. The size of the message is 6 (? didn’t print one ?) bytes. The

receiving client logs the message and discards it.

IGNORE

OP_DISCONNECT(0x18)

未在文檔中。

IGNORE

OP_SERVERMESSAGE(0x38)

(see 6.2.2)Server messages are variable length message that are sent from the server to client on various

occasions, the first, immediately after a client login request. A single server-message may

contain several messages separated by new line characters (’\r’,’\n’ or both). Messages that

start with ”server version”, ”warning”, ”error” and ”[emDynIP: ” have special meaning for

the client. Other messages are simply displayed to the user.

POST `server_message_alert`

OP_SERVERLIST(0x32)

(see 6.2.7)Sent from the server to the client. The message contains information about additional eMule

servers to be used to expand the client’s server list. The message size varies (depends on the

number of servers transmitted).是客戶端請求(6.2.5)Get list of servers

的迴應。

IGNORE

OP_SERVERSTATUS(0x34)

(see 6.2.6)Sent from the server to the client. The message contains information on the current number

of users and files on the server. The information in this message is both stored by the client

and also displayed to the user. The message size is 14 bytes.

POST `server_status_alert`

OP_USERS_LIST(0x43)

未在文檔中。

IGNORE

OP_IDCHANGE(0x40)

(see 6.2.3)The ID Change message is sent by the server as a response to the login request message and

signifies that the server has accepted the client connection. The message size is 14 or 10 bytes

depends on sending the optional TCP connection bitmap.

 

保存ID,然後POST一個 `server_connection_initialized_alert`通知

OP_SERVERIDENT(0x41)

(see 6.2.8)Sent from the server to the client. Contains a server hash (TBD) ,the server IP address and

TCP port (which may be useful when connecting through a proxy) and also server description

information. The message size varies.

保存服務器的md4hash ID,然後Post一個`server_identity_alert`

OP_FOUNDSOURCES(0x42)

(see 6.2.12)A message sent from the server to the client with sources (other clients) for a file requested

by the client for a file. The message size varies.

保存爲found_file_sources並傳遞給on_found_peers

OP_SEARCHRESULT(0x33)

未在文檔中。

保存search_result並post `shared_files_alert`(this alert throws on server search results and on user shared files)

OP_CALLBACKREQUESTED(0x35)

(見6.2.14,見下方注1)A message sent from the server to the client indicating another client asks the receiving client

to connect to it. The message is sent when the receiving client has a low ID (see section 2.4).

The size of the message is 12 bytes. The receiving client tries to connect to the IP and port

specified by the callback request packet.

connect to requested client.

session.add_peer_connection(cb.m_network_point, ec);

OP_CALLBACK_FAIL(0x36)

見下方注1

IGNORE

表3-1 接收到的消息類型說明和處置方法

 

注1:關於回調的說明。

The callback mechanism is designed to overcome the inability of low ID clients to accept

incoming connections and thus share their files with other clients. The mechanism is simple:

in case a clients A and B are connected to the same eMule Server and A requires a file that is

located on B but B has a low ID, A can send the server a callback request (see section 6.2.13),

requesting the server to ask B to call him back. The server, which already has an open TCP

connection to B, sends B a callback requested (section 6.2.14) message, providing him with

A’s IP and port. B can then connect to A and send him the file without further overhead on

the server. Obviously, only a high ID client can request low ID clients to call back (a low ID

client is not capable of accepting incoming connections). Figure 2.6 illustrates the callback

message exchange.

There is also a feature allowing two low ID clients to exchange files through their server

 

Figure 2.6: Callback sequence

connection, using the server as a relay. most of the servers no longer support this option

because of the overhead it incurs on the server.

 

3.3 其他發送給服務器的消息

 

在3.2中整理了libed2k連接服務器的過程,同時在表3.1中描述了從服務器發往客戶端的迴應消息,這些消息中有些是連上之後服務器就會主動發送給客戶端,而另一些則需要客戶端發送請求時纔會迴應。在這一節中我們整理libed2k庫有哪些給服務器發送消息的方法以及它怎麼處理對應的服務器響應消息。

 

3.3.1 登錄消息及其響應

 

在3.2中介紹首次連接時曾提到過連接上服務器後發送的首條消息是登錄消息,它對應的消息體對象是cs_login_request:

/** 
 * login request structure - contain some info and 4 tag items 
 */ 
struct cs_login_request 
{ 
    md4_hash m_hClient; 
    net_identifier m_network_point; 
    tag_list<boost::uint32_t> m_list; 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
    ar & m_hClient & m_network_point & m_list; 
    } 
};

關於tag,tag_list的介紹見3.1.4節。

cs_login_request對應的消息頭對象是:

template<> 
struct packet_type<cs_login_request>
{ 
static const proto_type value = OP_LOGINREQUEST; //0x01 
static const proto_type protocol = OP_EDONKEYPROT; //0xE3 
};//!< on login to server

從cs_login_request的serialize可以看消息體的組織是|HASH 16|ID 4|PORT 2|1 Tag_set|,這個消息的定義見eMule協議(英文版)6.2.9節【The eMule Protocol Specification,Yoram Kulbak and Danny Bickson,2005】,格式如下圖:

在libed2k中:

  • cs_login_request::m_hClient對應了上圖中的User Hash
  • cs_login_request::m_network_point.m_nIP 對應上圖中的Client ID
  • cs_login_request::m_network_point.m_nPort對應上圖中的TCP Port
  • cs_login_request::m_list對應了上圖中的Tag Count以及剩餘項。

tag_list類的聲明在include\libed2k\ctag.hpp。填寫cs_login_request各成員的代碼如下:

cs_login_request login; //!< generate initial packet to server 
boost::uint32_t nVersion = 0x3c; 
boost::uint32_t nCapability = CAPABLE_ZLIB | CAPABLE_AUXPORT | CAPABLE_NEWTAGS | CAPABLE_UNICODE | CAPABLE_LARGEFILES; 
boost::uint32_t nClientVersion = (LIBED2K_VERSION_MAJOR << 24) | (LIBED2K_VERSION_MINOR << 17) | (LIBED2K_VERSION_TINY << 10) | (1 << 7); 
login.m_hClient = settings.user_agent; 
login.m_network_point.m_nIP = 0; 
login.m_network_point.m_nPort = settings.listen_port; 
login.m_list.add_tag(make_string_tag(std::string(settings.client_name), CT_NAME, true)); 
login.m_list.add_tag(make_typed_tag(nVersion, CT_VERSION, true)); 
login.m_list.add_tag(make_typed_tag(nCapability, CT_SERVER_FLAGS, true)); 
login.m_list.add_tag(make_typed_tag(nClientVersion, CT_EMULE_VERSION, true));

可以看到

  • User Hash的值是settings.user_agent,這個值默認等於libed2k::md4_hash::emule(這個值爲31D6CFE0D10EE931B73C59D7E0C06FC0,詳見src\md4.cpp以及include\libed2k\session_settings.hpp中class session_settings的定義)。
  • Client ID等於0。
  • TCP Port等於客戶設置的TCP端口。
  • Tag Count的值是4(內部有4個元素)。
  • Name Tag的值是settings.client_name即我們設置的名字。
  • Version Tag是0x3c。
  • PortTag是(CAPABLE_ZLIB | CAPABLE_AUXPORT | CAPABLE_NEWTAGS | CAPABLE_UNICODE | CAPABLE_LARGEFILES)(注意這裏和eMule協議文檔中的描述不一致,目前不知道是哪邊有誤
  • Flag tag是LIBED2K_VERSION_MAJOR.LIBED2K_VERSION_MINOR.LIBED2K_VERSION_TINY.1(即1.0.0.1)(注意這裏和eMule協議文檔中的描述不一致,目前不知道是哪邊有誤

可能是實現的協議版本不一致,可以看到這裏有兩個項和文檔中不一致,還需要查找資料確認現在最新版本的emule協議中在這個消息中是怎麼定義的。

 

登錄消息的迴應是OP_IDCHANGE,下面的說明摘自mMule協議文檔6.2.3:

The ID Change message is sent by the server as a response to the login request message and signifies that the server has accepted the client connection. The message size is 14 or 10 bytes depends on sending the optional TCP connection bitmap.

 

libed2k在server_connection::handle_read_packet中處理這個消息的對應代碼片段如下:

current_operation = scs_start; 
id_change idc; 
ia >> idc; 
m_client_id = idc.m_client_id; 
m_tcp_flags = idc.m_tcp_flags; 
m_aux_port = idc.m_aux_port; 
DBG("handshake finished. server connection opened {" << idc << "}" << (isLowId(idc.m_client_id)?"LowID":"HighID")); 
//注:下面的params是server_connection_parameters類型的對象。 
//包含了所連接服務器的信息。 
m_ses.m_alerts.post_alert_should( 
    server_connection_initialized_alert( 
        params.name, 
        params.host, 
        params.port, 
        m_client_id, 
        m_tcp_flags, 
        m_aux_port) );

 

注意在id_change序列化的實現中,m_tcp_flags和m_aux_port成員是可選的(默認爲0),如下:

#define DECREMENT_READ(n, x) if (n >= sizeof(x)) 
\ { 
\ ar & x; 
\ } 
\ else 
\ { 
\ return; 
\ } 
/** 
 * this is variable size structure contains client id and some impotant information about server 
 */ 
struct id_change 
{ 
    //...忽略部分代碼 
    // only for load 
    template<typename Archive> void serialize(Archive& ar){ 
        // always read/write client id; 
        ar & m_client_id; 
        DECREMENT_READ(ar.bytes_left(), m_tcp_flags); 
        DECREMENT_READ(ar.bytes_left(), m_aux_port); 
    } 
};

server_connection::m_client_id在客戶端與服務器的這次會話期間會一直有效,用於標識自身的身份ID。server_connection::m_aux_port在當前版本的libed2k中並未使用到,可以忽略它。server_connection::m_tcp_flags定義了傳輸選項,在很多地方都會用到。當前定義的TCP Flag包括:

#define SRV_TCPFLG_COMPRESSION 0x00000001 
#define SRV_TCPFLG_NEWTAGS 0x00000008 
#define SRV_TCPFLG_UNICODE 0x00000010 
#define SRV_TCPFLG_RELATEDSEARCH 0x00000040 
#define SRV_TCPFLG_TYPETAGINTEGER 0x00000080 
#define SRV_TCPFLG_LARGEFILES 0x00000100 
#define SRV_TCPFLG_TCPOBFUSCATION 0x00000400

 

 

3.3.2 服務器消息(Server message)

 

下面的說明摘抄自eMule協議(英文版)6.2.2節:

Server messages are variable length message that are sent from the server to client on various

occasions, the first, immediately after a client login request. A single server-message may

contain several messages separated by new line characters (’\r’,’\n’ or both). Messages that

start with ”server version”, ”warning”, ”error” and ”[emDynIP: ” have special meaning for

the client. Other messages are simply displayed to the user.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x38

The value of the OP SERVERMESSAGE op-code

Size

2

NA

The number of bytes in the remainder of the message not including the fields described so far

Messages

varies

NA

A list of server messages separated by new

lines

Special messages

1. version - Usually send in a successful connection handshake

2. error -

3. warning - Usually send when the server denies the connection or when the client has a

low ID

4. emDynIP -

服務器在各種情況下(我不知道除了登錄成功後其他什麼時候會發)會發消息到客戶端,當客戶成功登錄服務器後會立即發送第一條。

 

libed2k在server_connection::handle_read_packet中處理這條消息,如下:

server_message smsg; ia >> smsg; 
//注:下面的params是server_connection_parameters類型的對象。 
//包含了所連接服務器的信息。 
m_ses.m_alerts.post_alert_should( 
    server_message_alert( 
        params.name, params.host, params.port, smsg.m_strMessage));

server_message的定義如下:

struct server_message
{ 
    boost::uint16_t m_nLength; 
    std::string m_strMessage; 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        ar & m_nLength; 
        // allocate buffer if it needs 
        if (m_strMessage.size() != m_nLength) 
            m_strMessage.resize(m_nLength); 
        ar & m_strMessage; 
    } 
};

可以看到程序只是簡單的把服務器的消息封裝到一個server_message_alert通知裏並將它塞到通知隊列中。

 

3.3.3 獲取服務器列表(Get list of servers)

 

消息被封裝在server_get_list中,迴應消息被封裝到server_list。在libed2k中並未實際處理這個消息。發送這個消息是在server_connection::second_tick方法中,然後接收依舊實在server_connection::handle_read_packet,收到消息之後只是簡單的反序列化然後丟棄。不明白爲什麼需要這樣處理,可能需要定時向服務器發送一條消息以避免timeout?

 

3.3.4 接收服務器狀態(Server status)

 

服務器狀態消息包含登錄到服務器上的用戶數量和用戶分享的文件數量。下面的說明摘抄自eMule協議(英文版)6.2.6節:

Sent from the server to the client. The message contains information on the current number

of users and files on the server. The information in this message is both stored by the client

and also displayed to the user. The message size is 14 bytes.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x34

The value of the OP SERVERSTATUS op-

code

User Count

4

 

The number of users currently logged in to the

server.

File Count

4

 

The number of files that this server is in-

formed about

這條服務器是服務器主動發送給客戶端。libed2k處理這條消息的實現如下:

server_status sss; 
ia >> sss; 
m_ses.m_alerts.post_alert_should( 
    server_status_alert( 
        params.name, params.host, params.port, sss.m_nFilesCount, sss.m_nUserCount) 
);

server_status定義如下:

struct server_status 
{ 
    boost::uint32_t m_nUserCount; 
    boost::uint32_t m_nFilesCount; 
    template<typename Archive> 
    void serialize(Archive & ar)
    { 
        ar & m_nUserCount & m_nFilesCount; 
    } 
};

libed2k只是簡單封裝了這條消息的內容到laert並將它塞到通知隊列中。

 

3.3.5 服務器鑑定(Server identification)

 

服務器鑑定的消息是在客戶端登錄後服務器主動發送給客戶端的,因此沒有對應的客戶端到服務器的消息。下面關於服務器鑑定的說明摘抄自eMule協議(英文版)6.2.8節:

Sent from the server to the client. Contains a server hash (TBD) ,the server IP address and TCP port (which may be useful when connecting through a proxy) and also server description information. The message size varies.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x41

The value of the OP SERVERIDENT opcode

Hash

16

NA

A GUID of the server (seems to be used for debug)

Server IP

4

NA

The IP address of the server

Server Port

4

NA

The TCP port on which the server listens

Tag Count

4

NA

The number of tags at the end of the message

ServerName Tag

Varies

NA

The name of the server. The tag is a string

tag and the tag name is an integer of value 0x1

Server Descrip-tion Tag

Varies

NA

A server description string. The tag is a string tag and the tag name is an integer of value

0xB

接收處理依舊在時server_connection::handle_read_packet中:

server_info_entry se; 
ia >> se; 
se.dump(); 
m_hServer = se.m_hServer; 
m_ses.m_alerts.post_alert_should( 
    server_identity_alert( 
        params.name, params.host, params.port, se.m_hServer, 
        se.m_network_point,
        se.m_list.getStringTagByNameId(ST_SERVERNAME), 
        se.m_list.getStringTagByNameId(ST_DESCRIPTION)
    )
);

server_info_entry的定義如下:

struct server_info_entry 
{ 
    md4_hash m_hServer; 
    net_identifier m_network_point; 
    tag_list<boost::uint32_t> m_list; 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        ar & m_hServer & m_network_point & m_list; 
    } 
    void dump() const; 
};

程序先保存服務器的ID,然後將這條消息的內容封裝到server_identity_alert對象內並將它添加到通知隊列。

 

3.3.6 獲取文件源及其響應(Get sources)

從客戶端發起詢問,在服務器中查找指定文件的源。下邊這條消息的說明摘抄自eMule協議(英文版)6.2.11節:A message sent from the client to the server requesting sources (other clients) for a file. The message size is 22 bytes.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x19

The value of the OP GETSOURCES opcode

File hash

16

NA

The requested file hash

在ed2k中對應的對象是get_file_sources:

/** 
 * request sources for file 
 */ 
struct get_file_sources 
{ 
    md4_hash m_hFile; //!< file hash 
    __file_size m_file_size; //!< file size 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        ar & m_hFile; 
        // ugly eDonkey protocol need empty 32-bit part before 64-bit file size record 
        if (m_file_size.u.nHighPart > 0) 
        { 
            boost::uint32_t nZeroSize = 0; 
            ar & nZeroSize; 
        } 
        ar & m_file_size; 
    } 
};

發送的接口在session::post_sources_request ,底層實現在server_connection::post_sources_request:

void server_connection::post_sources_request(const md4_hash& hFile, boost::uint64_t nSize) 
{ 
    DBG("server_connection::post_sources_request(" << hFile.toString() << ", " << nSize << ")"); 
    get_file_sources gfs; 
    gfs.m_hFile = hFile; 
    gfs.m_file_size.nQuadPart = nSize; 
    do_write(gfs); 
}

協議中並未規定需要填寫大小,我猜測應該是和協議版本有關。

 

與這條消息對應的消息是"已搜索到文件源"消息(Found sources),下面關於這條迴應消息的說明摘抄自協議文檔的6.2.12一節。

A message sent from the server to the client with sources (other clients) for a file requested by the client for a file. The message size varies.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including

the header and size fields

Type

1

0x42

The value of the OP FOUNDSOURCES op-code

File Hash

16

NA

The requested file hash

Sources Count

1

NA

The number of sources in this message

List of sources

Varies

NA

A list of sources

Source list item format

The table below describes the format of a source list-item. Each source includes the details of an eMule client holding the requested file.

Name

Size in bytes

Default Value

Comment

Client ID

4

NA

Client ID for an eMule peer holding the file

Client Port

2

NA

The TCP port of the client holding the file

在libed2k中這條消息被封裝到found_file_sources結構中:

struct found_file_sources 
{ 
    md4_hash m_hFile; 
    container_holder<boost::uint8_t, std::vector<net_identifier> > m_sources; 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        ar & m_hFile & m_sources; 
    } 
    void dump() const; 
};

處理這條消息的邏輯在server_connection::handle_read_packet中:

found_file_sources fs; ia >> fs; fs.dump(); on_found_peers(fs);

其中on_found_peers的實現:

void server_connection::on_found_peers(const found_file_sources& sources) 
{ 
    APP("found peers for hash: " << sources.m_hFile); 
    boost::shared_ptr<transfer> t = m_ses.find_transfer(sources.m_hFile).lock(); 
    if (!t) 
        return; 
    for (std::vector<net_identifier>::const_iterator i = sources.m_sources.m_collection.begin(); 
        i != sources.m_sources.m_collection.end(); ++i) 
    { 
        tcp::endpoint peer(ip::address::from_string(int2ipstr(i->m_nIP)), i->m_nPort); 
        if (isLowId(i->m_nIP) && !isLowId(m_client_id)) 
        { 
            // peer LowID and we is not LowID - send callback request 
            if (m_ses.register_callback(i->m_nIP, sources.m_hFile)) 
                post_callback_request(i->m_nIP); 
        // 注意:當前絕大多數服務器已經不再支持callback方法,這會使服務器過載。 
        } 
        else 
        { 
            APP("found HiID peer: " << peer); 
            t->add_peer(peer, peer_info::tracker); 
        } 
    } 
}

處理流程如下:

  1. 在當前的文件傳輸任務列表中查詢該文件對應的任務,如果不存在任務則跳過後續步驟不做任何事情。
  2. 在文件源列表中逐個判斷其用戶ID是否爲高ID(可上傳的用戶),如果是則調用transfer::add_peer方法將這個用戶拉到傳輸文件任務的用戶列表中。否則調用register_callback方法通過服務器中轉傳輸文件。注意當前絕大多數服務器已經不再支持callback方法,這會使服務器過載。

文件傳輸任務將在後續章節中詳細分析,在這裏先暫時跳過。

 

3.3.7 文件查找及其服務器響應消息的處理

 

客戶端以指定的一個或者多個條件向服務器查詢是否存在文件,服務器如果找到就返回符合條件的文件列表。發送請求的過程已經在2.5一節中詳細描述,我們這裏關注一下收到服務器的響應後libed2k是怎麼處理的。

服務器的響應消息在ed2k文檔的6.2.10Search result一節中,摘抄如下:

A message sent from the server to the client as a reply to a search request. The message is usually compressed. The message size varies.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x16

The value of the OP SEARCHRESULT op-code

Result Count

4

NA

The number of search results in this message

Result list

Varies

NA

A list of search results

Search result list item format

The table below describes the format of a single search result list-item. Each search result holding the file.There are also several tags describing the file attributes.The tag list

Name

Size in bytes

Default Value

Comment

File Hash

16

NA

A hash value, for unique identification of the

file

Client ID

4

NA

Client ID for an eMule peer holding the file

Client Port

2

NA

The TCP port of the client holding the file

Tag Count

4

NA

The number of descriptor tags following

Tag list

Varies

NA

A list of descriptor tags

is described below.Note that most of the tags are optional and that their order is not guaranteed. Tag encoding rules are described is detail an the beginning of this chapter.

Name

Tag name

Tag Type

Comment

File name

Integer, 0x01

String

 

File size

Integer 0x02

Integer

 

File type

Integer 0x03

String

 

File format

Integer 0x04

String

 

Sources

Integer 0x15

Integer

The number of available sources for this file

Artist

String ”Artist”

String

 

Album

String ”Album”

String

 

Title

String ”Title”

String

 

Length

String ”length”

Integer

 

Bitrate

String ”bitrate”

Integer

 

Codec

String ”codec”

Integer

 

Table 6.1: search result tag list

在libed2k的server_connection::handle_read_packet中處理這個消息:

search_result sfl; ia >> sfl; 
m_ses.m_alerts.post_alert_should( 
    shared_files_alert( 
        net_identifier(address2int(m_target.address()), 
        m_target.port()), 
        m_hServer, 
        sfl.m_files, 
        (sfl.m_more_results_avaliable != 0)
    ) 
);

這裏只是將搜索到的文件列表以alert的形式交給用戶使用。search_result定義如下:

struct search_result { 
    shared_files_list m_files; 
    char m_more_results_avaliable; // use only for load 
    template<typename Archive> 
    void load(Archive& ar) 
    { 
        m_more_results_avaliable = 0; 
        ar & m_files; 
        if (ar.bytes_left() == 1) 
        { 
            ar & m_more_results_avaliable; 
        } 
    } 
    template<typename Archive> 
    void save(Archive& ar) 
    { 
        // do nothing 
    } 
    void dump() const; 
    LIBED2K_SERIALIZATION_SPLIT_MEMBER() 
};

其中關鍵的shared_files_list類定義是:

typedef container_holder<boost::uint32_t, std::vector<shared_file_entry> > shared_files_list;

其中shared_file_entry的類定義爲:

/** 
 * shared file item structure in offer list 
 */ 
struct shared_file_entry 
{ 
    md4_hash m_hFile; //!< md4 file hash 
    net_identifier m_network_point; //!< network identification 
    tag_list<boost::uint32_t> m_list; //!< file information list 
    shared_file_entry(); 
    shared_file_entry(const md4_hash& hFile, 
        boost::uint32_t nFileId, 
        boost::uint16_t nPort); 
    bool is_empty() const 
    { 
        return !m_hFile.defined(); 
    } 
    template<typename Archive> 
    void serialize(Archive& ar)
    { 
        ar & m_hFile & m_network_point & m_list; 
    } 
    void dump() const; 
};

3.3.8 其他消息

 

關於回調在3.2的注中有說明,與回調有關的消息有:

(客戶端發送的)回調請求Callback request,回調已處理的響應消息Callback requested,以及回調失敗消息Callback failed。如在3.2中所注,當前的服務器已普遍不支持callback而且我們將來的程序也不會使用它,這裏就不再贅述。

 

服務器拒絕消息Message rejected,在emule文檔的6.2.16中有如下說明:A message sent from the server to the client indicating that the server rejected the last command sent by the client. The size of the message is 6 (? didn’t print one ?) bytes. The receiving client logs the message and discards it.

Name

Size in bytes

Default Value

Comment

Protocol

1

0xE3

 

Size

4

 

The size of the message in bytes not including the header and size fields

Type

1

0x05

The value of the OP REJECT opcode

遇到reject消息libed2k只是簡單丟棄,這裏也不再深入研究。

 

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