逐步实现TCP服务端Step04-1:改造

目前的这版程序不支持多客户并发,并非由于结构的局限性导致的无法实现。而是由于在早期不想引入不相关的复杂度而故意没去做这部分内容。

现在进行改造。主要修改netio,将监听socket置为非阻塞,让accept也参与到主循环中来。另外,还需要一个容器来存放那些已被accept了的新socket 。

这个容器要能方便地删除元素(客户离线),同时支持随机访问。基于数组存储的链式结构可以满足需求。

基于C++模板实现了一个不关注元素具体类型的List类(list.h):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template<class Type, int size>
class List {
public:
    struct Node {
        Type item;
        int next_node_idx;
        int prev_node_idx;
        int is_free_memory; // 0-no; 1-yes
        int next_free_memory_idx;
    };

    List();
    ~List();
    int AddOneItem(Type& item);
    int DelOneItem(int idx);
    int GetOneItem(int idx, Type** item);
    int GetHeadNodeIdx();
    int GetTailNodeIdx();
    int GetNextNodeIdx(int idx);
    int Clear();
    int get_cur_free_memory_idx() const {
        return cur_free_memory_idx_;
    }
    int get_list_len() const {
        return list_len_;
    }
    bool is_empty() const {
        return (list_len_ > 0 ? false : true);
    }

private:
    int max_node_cnt_;
    Node nodes_[size];
    int list_len_;
    int list_head_idx_;
    int list_tail_idx_;
    int cur_free_memory_idx_;
};

Node结构中的next_node_idx及prev_node_idx用于描述逻辑关系,next_free_memory_idx则辅助完成存储操作。它不同于之前基于ObjectIndexObjectManager组合实现的Queue,List包含了逻辑和存储。

定义SocketInfo结构作为List元素的基本类型:

1
2
3
4
5
6
7
8
struct SocketInfo {
    TCPSocket socket;
    int index;
    char ip[IP_LEN];
    unsigned short port;
    char server_ip[IP_LEN];
    unsigned short server_port;
};

其中,index字段记录了当前元素在list中的索引号。这个值很重要,有了它就能找到对应的TCPSocket对象,拿到了这个对象才可进行数据的收发。

考虑一个问题,在多客户并发的情况下,数据经netio处理后,全部汇聚在c2s_code_queue中,那么,s如何知道当前取得的code对应于哪个cilent呢?不过,对于echo服务来说,s不考虑这个问题好像也没关系。但当netio从s2c_code_queue取code准备向client发送的时候,就必须要做区分了。

在netio中,client就是一个个TCPSocket对象,这些对象被包在数个SocketInfo结构中,这些结构又由一个list管理维护。通过index可从list中检索出SocketInfo,那么只要code中携带了index值,就能确定该code对应于哪个client了。定义一个首部,附加于netio到s,以及s到netio的code之上,首部中至少记录了SocketInfo的index值。

1
2
3
struct NetHead {
    int socket_index;
};

接下来的问题是,s如何去维护每个code中携带的NetHead信息呢?如果不维护这些信息,当s向s2c_code_queue投递code时就无法构造NetHead首部。s处理的是业务,在s看来,每个client都是业务系统的“用户”,显然,每个用户都应该对应于一个用户对象,就像netio为每个client都创建了TCPSocket对象一样。如果s中维护了一组用户对象,那么把NetHead信息放在这个用户对象中就行了。

对应关系:

s在管理UserObject时,使用了HashTable,key为用户的账号。目前系统并没有账号的概念,client也没有登录的功能。我们简单地把SocketInfo中的index用作其对应用户的用户账号。

关于UserObject及其管理,类似于之前的MessageObject:

对于MessageObject,需要一个队列Queue来组织。Queue使用ObjectManager来管理对象的存储,基于Index实现队列的逻辑结构。而对于UserObject,我们希望通过Hash的方式(Key为用户账号)进行管理。需要一个跟Queue地位相当,但逻辑不同的HashTable类,HashTable同样使用ObjectManager来管理对象的存储,基于Index来实现Hash的逻辑部分。同时,由于HashTable需要对对象的Key值进行操作,所以会对Object有依赖关系。

至于又做了一个UserObjManager类是对HashTable提供的方法做一个包装,同时,UserObjManager又聚合了ObjectManager,这么做,一方面是为了给HashTable提供一个ObjectManager对象,另一方面是为了使用ObjectManager自带的基于对象ID来检索对象的方法。

也就是说,UserObjManager提供了两种方法,可基于Key或者对象ID来检索UserObject对象。

好了,基础的构件都已准备完毕,现在改造主流程。目前的流程是(左为netio,右为s):

改造后:

之所以要显式地调用发送滞留数据的方法,是因为改造后的流程是针对多客户的。所有涉及到数据的收发,都要先从list中取出SocketInfo才行。在遍历list的时候发送滞留数据是一个好时机。因为,遍历是主动进行的,不管s2c_code_queue中有没有待发code,总是会触发滞留数据的发送。

另外,图中红色的部分为netio对那些主动关闭连接的client的处理,先关闭,然后从list中删除客户信息。这么做其实有问题,因为一个客户上线后,不仅在netio中产生了对象,还会在s中产生一个用户对象,netio这么做仅仅是删除了自己管理的那些对象,而s中却还留存有对应于该client的用户对象。一旦有新的客户上线,将有可能与s中已存在的历史用户对象建立关联。netio在处理关闭的时候,应该用某种方式告知s,让s也做相应的处理。具体方式下篇讨论。

本次新增及改动的文件,由于s的主要逻辑都已封装在Control类中,所以修改s的流程其实是修改Control类:



<==  index  ==>