码迷,mamicode.com
首页 > 编程语言 > 详细

key-value 多线程服务器的Linux C++实现

时间:2015-07-17 16:10:55      阅读:186      评论:0      收藏:0      [点我收藏+]

标签:多线程   key-value   服务器   

项目需求

设计一个基于Socket或基于HTTP的服务器,服务内容是提供一种简单的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. 总体思路

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

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

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

2. 网络通信

基于本服务器的功能特性考虑,我们要求的是可靠性,因此选择TCP。
而在TCP的连接前,
- 服务器首先要做的准备是:创建一个连接套接字socket,然后socket绑定(bind)在一个IP或Port上以供客户端寻找,监听(listen)客户端的请求,然后一直阻塞等待客户端的连接。
- 客户端需要做的准备:客户端的套接字附上服务端的ip地址和自己的端口号。
当服务端和客户端做好通信准备后,由客户端发起,服务端响应,经过三次握手,建立相互之间的连接。
当一个客户端通信完成后, 客户端会主动向服务端请求释放连接。
关于三次握手以及当一个客户端通信完成,与服务端释放连接时的四次挥手的详细知识,请参考wireshark抓包图解 TCP三次握手/四次挥手详解

建立连接后,服务端调用accept()函数,阻塞服务端进程,直至收到客户端send()过来的信息。
关于网络编程中的Socket接口函数的详细讲解,参考Linux的SOCKET编程详解

3. 字符解析

由于网络通信中传输的是字符,我们必须将字符解析成 命令(put,get,…etc.)和数据(key,value)。
由上述得知,传输字符包含有:命令,key,value;我们可根据习惯,设立分割字符,如空格,分号等。需要注意的是每次传输的字符串分割后,得到的子字符串的个数可能是不同的,如”put 12:quinn” 分为3段,而”get 12”分为2段,”exit”只有一段。因此根据客户端发信给客户端的命令不同,字符解析函数将它解析成不同的命令。而服务端首先判断第一段字符的含义(put,get,exit,save等),来决定自己要实现的动作。
源码查看:https://github.com/qzxin/key-value-server/blob/master/convert.cpp

4. 数据存储与查询

4.1 存储管理

由于内存空间有限,当数据量到达上限时,必须把数据转存到磁盘文件中。
在服务器实现过程中,服务端需要接受客户端的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中的虚拟内存。即本文偷换概念,将缓存当做内存,将磁盘文件当做缓存页。

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

本文思想是,为了更便利的对页数据进行置换,将磁盘文件的大小设置为页的大小,形成映射。当缺页中断时,调入新的一页时,即读一个新文件到内存中;而被替换掉的页,如果页的dirty位为1,则重新写入到它所属的文件,为了实现这一点,在页的数据结构中应该包含该页所属文件的编号。(这是OS中虚拟缓存的思想

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_;  // 该数据节点所属页
};

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

5. 多线程

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

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

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

本项目,采用互斥锁解决数据之间的同步问题,引入2个锁:写入锁和读取锁。当有一个正在put时,所有的put和get操作等待;当有get操作时,可以再有get操作,put操作等待。(读者写者问题的经典思想)
C++多线程编程,详情请参阅:C++11 编写 Linux 多线程程序

6. 待改进

  • 如何加入断电缓存重建机制?
  • 如何加入查询超时判断?
  • 需不需要线程调度?
  • 其他参考信息:淘宝自主开发的一个分布式key/value存储系统Tair,开发本项目时没有发现~~~

7. GitHub源码

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

key-value 多线程服务器的Linux C++实现

标签:多线程   key-value   服务器   

原文地址:http://blog.csdn.net/quzhongxin/article/details/46927785

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!