zoukankan      html  css  js  c++  java
  • key-value 多线程server的Linux C++实现

    项目需求

    设计一个基于Socket或基于HTTP的server,服务内容是提供一种简单的key/value映射关系的管 理与查询
    以下的全部操作都是通过结构体Node来传递的:
    struct Node {
    char key[KEY_SIZE];
    char value[VALUE_SIZE];
    };
    本场景中须要client和server两个程序
    client端仅仅有两种操作:
    int AddNode(const struct Node *node); // 将指定的node保存到server上,须要key和value都 完整
    int GetNode(struct Node *node); // 输入的node须要有完整的key。server负责将这个key相应 的value填到node中。或返回不存在

    server: server端就是接收client的两种请求,然后要么保存node要么查询node并返回值。
    server端怎么保存node不作要求,但要达到以下的几点:
    1. 假设将全部node都保存在内存中。那么当内存中的数据太多时,须要定期将node保存成磁 盘文件。
    2. client端在运行AddCell时,假设相应的key已经存在于server端。那么覆盖原有的值。

    1. 整体思路

    首先我们要开发一个基于网络的server,那么网络的知识是必须的,我们要开发的server是一种要求端对端的可靠的server。因此将採用基于TCP协议和套接字Socket作为服务端和client之间的通信方式。进,而我们能够观察到我们操作的数据是一个(key,value)形式。而TCP是一个以字节流传输的连接,因此须要一个字符串(char*)和数据结构(key,value)之间的相互转换函数。来将服务端和client之间的传输字节流解析成命令和数据节点(以及将命令和数据节点打包成字符串进行通信)。然后我们必须考虑server上数据的存储和查询问题,这是项目实现的关键。最后,server肯定要支持多client的同一时候连接和通信,因此还必须增加多线程或多进程。
    本项目已实现的功能

    • 基于Socket的TCP网络传输
    • 对网络传输字符的解析与打包
    • LRU Page 缓存
    • Hash-map 查找
    • 多线程多clients 同一时候put。get操作

    以下将从四个方面进行阐述项目思想:网络通信,字符解析。数据存储与查询,多线程实现。

    2. 网络通信

    基于本server的功能特性考虑。我们要求的是可靠性,因此选择TCP。
    而在TCP的连接前。
    - server首先要做的准备是:创建一个连接套接字socket。然后socket绑定(bind)在一个IP和Port上以供client寻找。监听(listen)client的请求,然后accept一直堵塞等待client的连接。


    - client须要做的准备:client的套接字附上服务端的ip地址和自己的port号。
    当服务端和client做好通信准备后,由client发起,服务端响应。经过三次握手,建立相互之间的连接。
    当一个client通信完毕后, client会主动向服务端请求释放连接。


    关于三次握手以及当一个client通信完毕,与服务端释放连接时的四次挥手的具体知识,请參考wireshark抓包图解 TCP三次握手/四次挥手具体解释

    建立连接后,服务端调用accept()函数。堵塞服务端进程。直至收到clientsend()过来的信息。
    关于网络编程中的Socket接口函数的具体解说。參考Linux的SOCKET编程具体解释

    3. 字符解析

    因为网络通信中传输的是字符,我们必须将字符解析成 命令(put,get,…etc.)和数据(key,value)。
    由上述得知,传输字符包括有:命令,key,value。我们可依据习惯。设立切割字符,如空格,分号等。

    须要注意的是每次传输的字符串切割后,得到的子字符串的个数可能是不同的,如”put 12:quinn” 分为3段,而”get 12”分为2段,”exit”仅仅有一段。

    因此依据client发信给client的命令不同,字符解析函数将它解析成不同的命令。

    而服务端首先推断第一段字符的含义(put,get,exit,save等)。来决定自己要实现的动作。


    源代码查看:https://github.com/qzxin/key-value-server/blob/master/convert.cpp

    4. 数据存储与查询

    4.1 存储管理

    因为内存空间有限。当数据量到达上限时,必须把数据转存到磁盘文件里。
    在server实现过程中,服务端须要接受client的get和put两种操作,
    - put(key, value): 在接收一定数量的数据后须要将数据保存到磁盘上,而且须要检查是否存在同样的key。
    - get(key): 向服务端查询是否存在该key;

    因为项目需求同样的key仅仅能有一个值,所以无论是put和get都必须遍历内存和磁盘文件里的数据,查找是否含有该key。假设每一次操作都须要訪问磁盘,那么效率将是极低的,因为訪问数据的时间局部性,近期訪问过的数据在近期内有更大的可能再次被訪问,因此想到了引入内存缓存系统,即将近期訪问过的节点保留在内存中。又由訪问数据的空间局部性,近期訪问过的数据周围的数据有更大的可能被訪问,想到了引入分页机制。

    • 缓存系统:当查找节点时,首先在缓存中查找。查找成功则对该节点操作。

      缓存查找失败。即缺页中断。因为内存空间限制。缓存的大小是一定的,当发生缺页中断时,要从磁盘中载入新数据,即须要不断的用近期訪问成功的新数据替换缓存中的旧数据。

    • 分页机制:当待查找数据不在缓存,即缺页中断时,假设每次都从磁盘中载入一个数据,那效率是不可接受的。因此,将页(包括N个数据节点)作为缓存和磁盘数据之间操作的基本单位。

    如上提到的缺页中断是操作系统中内存管理中的概念。

    在请求分页存储管理系统中。因为使用了虚拟存储管理技术,使得全部的进程页面不是一次性地全部调入内存,而是部分页面装入。
    这就有可能出现以下的情况:要訪问的页面不在内存。这时系统产生缺页中断。操作系统在处理缺页中断时,要把所需页面从外存调入到内存中。假设这时内存中有空暇块,就能够直接调入该页面。假设这时内存中没有空暇块,就必须先淘汰一个已经在内存中的页面,腾出空间。再把所需的页面装入。即进行页面置换。
    当缺页中断时,须要进行页面置换。而常见的页面置换算法有:FIFO。LRU和时钟算法。
    (1)FIFO是淘汰内存中存在时间最长的页,而最长的页可能是最常被訪问的。因此性能差。
    (2)LRU是淘汰内存中最久没有被訪问的页。
    (3)时钟算法是,将页连成一个环形链表,当缺页中断时。指针指向最老的页,当该页的訪问位为0。则删除该页。若该页訪问位为1,则将訪问位置0。遍历它的下一页,直至遇到一个訪问位为0的页。用新数据替换它,并把指针指向它的下一页。

    注意,本文中假设”数据缓存“存在于内存中,即内存缓存,而”磁盘中的数据文件“模拟现代OS中的虚拟内存。即本文将缓存放在内存,将磁盘文件当做缓存页。

    本文选用easy实现且性能尚可的LRU页面置换。具体实现过程已在还有一篇博文基于文件页的 LRU Cache:磁盘缓存实现中具体描写叙述,本文不再赘述。

    本文思想是,为了更便利的对页数据进行置换。将磁盘文件的大小设置为页的大小,形成映射。

    当缺页中断时,调入新的一页时,即读一个新文件到内存中。而怎样定位文件,下文分析;而被替换掉的页。假设页的dirty位为1。则又一次写入到它所属的文件,为了实现这一点,在页的数据结构中应该包括该页所属文件的编号。(这是OS中虚拟缓存的思想

    怎样定位key所在的文件?(2015/07/19更新)
    建立(key, file)映射的hash表。put操作时,将每个新key和该key将要存入的文件序号压入一个hash-map;get操作或put操作。search(key)时。假设该key不在缓存中,那么查key-file映射表,假设存在该key相应的文件,则将该文件载入进缓存中,否则返回不存在该key。注意,在server启动时,应该载入(key,file)的映射表(它们存储在一个文件里)。

    class HashCache::Page {
    public:
        int file_num_; // 页所相应的文件序号
        bool lock_;
        bool dirty_;  // 标记page是否被改动
        class Node data_[PAGE_SIZE];  // 页包括的节点数据
        class Page* next;
        class Page* prev;
        Page() {
            lock_ = false;
            dirty_ = false;
        }
    };

    4.2 数据查询

    4.1存储管理 攻克了数据的存储和载入问题,那么怎样能高速的索引到一个数据呢?两种办法,平衡二叉树O(lgn)和hash表O(1);我们知道hash表的缺点是不能有效解决冲突,而本项目中的key,value唯一。因此採用更快的hash-map实现数据的索引,当然採用时间复杂度为O(lgn)的map实现也是能够的。


    在实际操作中,当每次缺页中断,载入一页时,将新页的数据都插入到hash-map中,同一时候将被替换页的数据从hash-map中释放。而为了保证这点,在构建数据结构时,每个数据节点必须包括它所属的页号

    class HashCache::Node {
    public:
        std::string key_;
        std::string value_;
        class Page* page_;  // 该数据节点所属页
    };

    总结:由操作系统中的内存页面置换和虚拟缓存中的理论,迁移得到本项目server数据的存储和查询的实现思想。
    本节思想的具体实现步骤,已再还有一篇博文中描写叙述点击此处查看

    5. 多线程

    2015/09/30 更新
    对于多线程实现,因为线程的创建和销毁耗费时间和资源,因此对于大量的短的传输任务能够用线程池的方式实现。

    一个server肯定是要支持多client通信的。那么应该使用多进程还是多线程呢?
    由上文可知,全部的数据都是先存储到内存中。然后再转存到文件里,那么为了内存数据(缓存)的共享。选用多线程实现。
    每当有一个client和server连接成功后,新建一个线程,将连接套接字传入线程处理函数。然后分离(detach)该线程。由该线程处理该client的全部通信。

    因为是通过”共享内存“的方式实现线程之间的通信,可能存在多个client同一时候针对一个key的value做改动。同一时候有client在读取该key的value,造成数据的不同步?那应该怎样解决线程同步问题呢?

    线程同步的方式:临界区。相互排斥锁。信号量,事件

    本项目,採用相互排斥锁解决数据之间的同步问题,引入2个锁:写入锁和读取锁。当有一个正在put时,全部的put和get操作等待;当有get操作时,能够再有get操作,put操作等待。(读者写者问题的经典思想)
    C++多线程编程,详情请參阅:C++11 编写 Linux 多线程程序
    C++线程信号量和锁,详情请參阅:C++11 并发指南三(std::mutex 具体解释)

    2015/09/30 更新
    如上述所述,每一次操作缓存都要锁定整个缓存部分,能够做出例如以下改进:使用两个相互排斥量进行加锁,当读取或者写入一个页的数据时,对该页进行加锁。其它页能够正常訪问。可是,当将刚刚操作的页放到双向链表的头部时,须要对整个链表(整个缓存)进行加锁。这样粒度更小。效率更高。

    6. 待改进。未实现的想法

    • 怎样增加断电缓存重建机制?
    • 怎样增加查询超时推断?
    • 需不须要线程调度?
    • 是否能把全部的key全放到一个set里,当cache中不存在该key时,去set里查找。假设存在然后才去遍历文件。不存在则直接返回。

    • 是不是还能够。将key。page_num对存入一个hash-map,依据key直接索引到其所属的页(相应文件号)。
    • 其它參考信息:淘宝自主开发的一个分布式key/value存储系统Tair,开发本项目时没有发现~~~

    7. GitHub源代码

    本项目开发环境Linux GCC4.8.4 ,C++ 11
    源代码:https://github.com/qzxin/key-value-server
    原文:key-value 多线程server的Linux C++实现http://blog.csdn.net/quzhongxin/article/details/46927785

  • 相关阅读:
    基于注解的 Spring MVC 简单入门
    Spring MVC入门
    Java集合总结之Collection整体框架
    关于getClass().getClassLoader()
    Java生成和操作Excel文件
    Spring 实现发送电子邮件的两种方法
    java mail(发送邮件--163邮箱)
    Write operations are not allowed in read-only mode 只读模式下(FlushMode.NEVER/MANUAL)写操作不
    配置文件Struts.xml 中type属性 redirect,redirectAction,chain的区别
    关于Hibernate在反向工程时无法选择Spring DAO Type的解决方法【更新版】
  • 原文地址:https://www.cnblogs.com/blfshiye/p/5394835.html
Copyright © 2011-2022 走看看