前四章介绍了系统层的设计,从这一章开始进入服务层的设计。

连接断开

  在常见的服务器场景中,客户端断开连接的方式为被动关闭。即作为客户端请求完服务器的服务后,选择主动关闭同服务器的连接。在服务器的角度看,服务器是客户端连接套接字read系统调用返回0,触发关闭逻辑,服务器本地是被动关闭连接的。

  但是在某些场景中,客户端虽然已经实际断开了与服务器的连接,但是服务器并不能及时检测出此时维护的连接已经断开的情景。在这种情况下,由于被动关闭的缘故,服务器并不会主动释放与该连接有关的资源。这些不能被释放的资源包括文件描述符、系统内存等系统稀缺资源。如果服务器系统中出现大量类似僵死连接得不到及时处理,将会导致系统资源耗尽的问题,可能会严重影响到服务器的性能,甚至导致服务器崩溃。

  一般而言,判断一个连接是否断开,只需调用write,返回为0,则为掉线。但是在长连接下,很有可能很长的一段时间都没有数据往来。理论上,这个长连接是一直保持连接的,但是实际情况中,如果中间节点出现某些故障是难以知道的。甚至有一些防火墙会自动把一定时间内没有数据交互的连接给断开。

  在TCP的机制中,本身存在SO_KEEPALIVE的选项。通过该选项可以设置2小时的心跳额度。但是它检查不到机器断电、网线拔出、防火墙导致断线等这些情况。而且通过该选项也很难在逻辑层对断线进行处理。

  综上原因,我们需要制定某种机制来帮助服务器检查连接是否已经断开,对疑似断开连接的客户进行主动关闭,并释放相关资源。

心跳机制介绍

  常用软件一般是通过心跳机制来解决检查连接的问题。通常是客户端必须每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线,并传输一些可能必要的数据。而服务器则维护每个连接的连接计时,每当每个连接发送有效消息后,就刷新该连接的计时。如果一段时间内服务器没有收到某个连接的计时,则服务器判定该连接失活,并对该连接执行强制关闭和资源释放工作。

  服务器如何维护每个连接的连接计时,一般有两种方法:

  • l  每个连接保存最后收到数据的时间,每当该连接收到数据则刷新该时间。然后用一个系统级的定时器,每秒钟遍历一遍所有连接,判断每个连接的最后收到数据的时间。一旦该时间超时,则判定该连接超时。
  • l  每个连接设置一个定时器,设定超时时间,每当收到数据时就去更新该定时器时间。一旦某个连接的定时器生效,则判定该连接超时。

  在方案一中,虽然只有一个定时器的开销,但是每次都需遍历全部连接,如果连接数目比较大,每次遍历工作将会有较大开销。同时执行超时检测的线程可能和连接管理的线程并非同一线程,当超时检测线程读取连接的最后收到数据时间时,可能在另一个线程中正在对该连接的该时间进行修改。因此我们还需要通过读写锁的机制对连接最后收到数据的时间进行保护。

  而方案二中,将会给每个连接都创建一个定时器,同时每个连接每次收到一次数据就需修改这个定时器的超时时间。这样将每个连接的定时管理与该连接的管理置于同一个线程之中,避免了多线程环境下的锁开销。但如果连接数目较大,且会有频繁更新定时器的操作,可能会对反应器的定时机制造成较大压力。

  以上两种方案中,实现均并不困难,但是均比较粗暴,都可能随着连接和更新次数的增加而对服务器性能造成影响,有必要在这两种方案的基础上进行优化。

超时队列设计

  在之前的方案一中,最大的开销是每次检查超时连接时都需遍历所有连接的接收时间,时间复杂度为O(n)。我们可以在此基础上对其进行优化,通过某种策略让检查超时的操作在尽可能短的时间内找出所有的超时连接。

  通过分析超时时间可知,对于所有连接而言,系统设定的超时时间是相同的。在均未收到新数据的情况下,上次先收到数据的连接肯定比后收到数据的连接先超时。这类似于一个先进先出的队列。因此我们尝试用队列对超时连接进行管理。

图4-1 超时队列

  如图4-1所示,我们创建了一个先入先出队列用来管理超时连接。为了保证该队列的线程安全,每次对该队列进行添加和删除操作均需在加锁情况下进行。在每个队列节点中,我们都保存了对应连接的上次接收消息时间。并且我们此时不考虑该连接又收到新的消息导致接收消息时间更新的情景。

  每当有新连接建立时,我们尝试获取超时队列锁,然后创建一个新的队列节点,并在该节点中保存该连接信息和当前时间。最后我们将这个新连接对应的节点添加到超时队列的头部。此时该节点之后的超时队列中已有连接1至5对应的节点,且每个节点对应连接的接收时间依次递增,即连接1的接收时间早于连接2,连接2的接收时间早于连接3,以此类推。

  系统建立了一个定时器,每隔一段时间检查超时队列中接收时间已经过期的连接。首先会检查超时队列最尾部的节点,如果对该节点对应连接的接收时间进行判断,显示未超时,则整个超时队列中的其他节点对应连接也均未超时,此次定时事件直接结束处理。如果判定该队尾节点对应连接已经超时,则记录该连接信息,并将该节点从队尾移除。然后再判断新的队尾节点是否已超时,如超时则同样记录并且移除,直到新的队尾节点未超时为止。最后系统将会向此次检测超时的所有连接发送超时通知,并最终强制断开这些超时连接。

  因为连接1的接收时间早于连接2,所以如果连接1未超时,连接2也显然为超时;但如果连接1此时已经超时,则无法判断之后的连接是否超时,因此还需再检测连接2,直到从超时队列中检测到一个未超时的节点,该节点及之后节点也均未超时。

  此时我们的超时队列已经能够满足无时间更新时的工作了。我们再添加它对接收到新的消息后,对接收时间的更新工作。依旧是如图的场景,此时连接3接收到了一条新的心跳消息,我们需要将该连接的计时重置。相应的操作其实很简单,获取超时队列的锁,找到该连接在超时队列中的节点,将该节点重新移到队列的头部,并更新节点中保存的上次接收数据时间即可。此时该节点所对应的连接成为超时队列管理的所有连接中时间最新的连接,当前连接均不变的话,该连接将最后超时。

  其中还是存在一个问题,连接如何确定自己对应的超时队列中的节点。如果不建立某种映射信息,我们从超时队列中查找某次连接的节点将需遍历整个超时队列中的所有节点,而获取连接对应节点又是更新超时时间必须的操作。如果每个连接每次更新超时时间都需要遍历超时队列,而超时队列又维护了成千上万的连接,那这将给整个系统带来极大的性能开销。

  我们可以采用STL库的std::list作为超时队列的底层实现,list数据结构保证其中的每个节点在整个生命周期内对应的内存空间不变。因此我们可以在每个连接对应对象中保存一个指向超时队列中对应节点的指针,当我们需要定位到该连接的超时节点时,只需对该指针取引用就行了,免去了遍历列表的开销。

  但是这种方式同样存在一定危险。因为该指针所指节点为超时队列的内部数据,我们将该指针暴露给连接对象,也就相当于把超时队列内部的数据泄露了出去,这可能会对整个队列的安全产生影响。比如原来多线程环境下对该超时队列的所有访问都需要添加线程锁保证线程安全,但是现在可以通过该指针绕过线程锁访问甚至修改超时队列中的数据。

  在系统实现中,我们设计了名为LinkedHashMap的数据结构,通过该结构来维护超时队列,同时建立了键值对映射机制,来确保通过某个Key就可获取对应的队列节点。

我们设计的LinkedHashMap类似于HashMap,同样提供键值对映射功能,但是它保留键值对插入的顺序,也就是说它既能满足键值对数据根据插入顺序先入先出的要求,同时也能根据键数据映射快速查找出值数据。

  LinkedHashMap内部由一个STL的std::list和一个std::unordered_map组成。每当有一个新键值对传入,就在list表头创建一个节点,并保存键值数据。同时在unordered_map中创建一个该键与队列中对应节点迭代器的映射。因此我们借助list实现了根据插入顺序排序的队列,而通过键映射我们又能快速找出队列中对应节点的迭代器,从而获取队列节点中数据。

图4-2 LinkedHashMap实现的超时队列

  在系统实现中,我们为每个连接对象分配了一个唯一的ID,并为其建立了ID与连接对象的映射,通过这个ID,我们可以很方便的获取对应的连接对象。在超时管理的具体实现中,我们使用连接ID代替实际的连接对象。

  如图4-2所示,我们将ID作为了LinkedHashMap的键,ID和对应连接的最后接收消息时间作为了值,构造了以上的LinkedHashMap结构。我们依次添加了连接1至连接5的数据,并更新了连接3的最后接收消息时间,并且连接1已经由于超时原因被移除。

  当我们添加一个新的连接时,只需获取该连接的ID和最后接收消息时间,并插入LinkedHashMap中,LinkedHashMap会将新节点插入队列的头部,并建立ID与节点的映射。当我们需要更新某个连接的最后接收消息时间时,只需通过ID便可获得所在节点,并修改最后接收消息时间并将该节点重新连接到队列头部。当该节点被系统检测到超时时,我们可以根据节点中保存的ID获知具体超时的连接对象,并将该ID的相关数据从list和unordered_map中移除即可。以上对于LinkedHashMap的插入、更新和删除操作均能大致保证在常数时间完成。

  通过以上设计,我们能够高效的实现对连接的超时管理。同时整个超时机制仅通过一个ID值保持与系统实际连接对象的联系,保证了模块间的低耦合度。

C++服务器设计(四):超时管理机制设计的更多相关文章

  1. go web编程——session管理机制设计与实现

    原生Go语言没有实现session管理机制,所以如果使用原生Go语言进行web编程,我们需要自己进行session管理机制的设计与实现,本文将就此进行详细介绍,并实现一个简单的session管理机制. ...

  2. 一种极简的异步超时处理机制设计与实现(C#版)

    1.引言 当执行某些动作之后,会期待反馈.最终要么是得到了结果,要么就是超时了.当超时发生时,可能是期望得到通知,或是希望能自动重试,等等.于是设计了一种通用的异步超时的处理机制,以期通过简洁易理解的 ...

  3. 【大白话系统】MySQL 学习总结 之 缓冲池(Buffer Pool) 的设计原理和管理机制

    一.缓冲池(Buffer Pool)的地位 在<MySQL 学习总结 之 InnoDB 存储引擎的架构设计>中,我们就讲到,缓冲池是 InnoDB 存储引擎中最重要的组件.因为为了提高 M ...

  4. Java秒杀简单设计四:service层设计

    接上一篇 https://www.cnblogs.com/taiguyiba/p/9829191.html  封装了系统传递的数据类和异常类 本文继续设计service层设计: 1.SeckillSe ...

  5. 游戏UI框架设计(四) : 模态窗体管理

    游戏UI框架设计(四) --模态窗体管理 我们在开发UI窗体时,对于"弹出窗体"往往因为需要玩家优先处理弹出小窗体,则要求玩家不能(无法)点击"父窗体",这种窗 ...

  6. H2Engine游戏服务器设计之属性管理器

    游戏服务器设计之属性管理器 游戏中角色拥有的属性值很多,运营多年的游戏,往往会有很多个成长线,每个属性都有可能被N个成长线模块增减数值.举例当角色戴上武器时候hp+100点,卸下武器时HP-100点, ...

  7. C#高性能大容量SOCKET并发(四):缓存设计

    原文:C#高性能大容量SOCKET并发(四):缓存设计 在编写服务端大并发的应用程序,需要非常注意缓存设计,缓存的设计是一个折衷的结果,需要通过并发测试反复验证.有很多服务程序是在启动时申请足够的内存 ...

  8. GPS部标平台的架构设计(四)-百度地图设计

    部标GPS软件平台之百度地图设计 地图是客户端中不可缺少的一个模块,很多人在设计和画图时候,喜欢加上地图引擎这样高大上的字眼,显得自己的平台有内涵,说白了就是用第三方的SDK来开发,早期的GPS监 控 ...

  9. Kafka设计解析(四)Kafka Consumer设计解析

    转载自 技术世界,原文链接 Kafka设计解析(四)- Kafka Consumer设计解析 目录 一.High Level Consumer 1. Consumer Group 2. High Le ...

随机推荐

  1. ADO.NET中连接SQL Sever

    1.在配置文件中定义数据库连接信息. 在配置文件*.config中添加这段代码在<configuration>与</configuration>之间: <connecti ...

  2. OpenGL ES 2.0 符点精度

    片元着色器中使用符点相关类型的变量时与顶点着色器中有所不同,在顶点着色器中直接声明使用即可,而在片元着色器中必须指定精度. lowp 低 mediump 中 highp 高 指定整个着色器中符点相关类 ...

  3. SMA2SATA、PCIe2SATA转换模块(也有叫:Sata Test Fixtures)

    SMA2SATA.PCIe2SATA测试夹具(Sata Test Fixtures) 去年制作SMA2SATA.PCIe2SATA适配器的过程早就想写出来,但一直没有时间,今天星期六有个空儿,简单整理 ...

  4. Python学习(一) Python安装配置

    我本身是Java程序猿,听说Python很强大,所以准备学习一下Python,虽说语言都是相同的,但java跟python肯定还是有区别的.希望在此记录一下自己的学习过程. 目前,Python分2.X ...

  5. 深入理解Azure自动扩展集VMSS(3)

    在实际使用过程当中,使用VMSS有一些最佳实践的建议和限制,便于你在做自动扩展设计的时候进行考虑: 关于VMSS 如果你使用的是系统镜像,一个扩展集中虚拟机数量不能超过100 无论是在ASM还是ARM ...

  6. Core Data的使用(二)备

    一.基础概念深入 1.NSManagedObjectContext 被管理数据上下文就像便笺簿 当从数据持久层获取数据时,相当于把这些临时的数据拷贝写在便笺簿上,然后就可以随心所欲的修改这些值. 通过 ...

  7. Visual Studio 2013 Professional Key

    今天发现家里的VS2013专业版过期了,于是google百度一顿大搜,多数key都不能用,不过还是找到一个key可以使用的. Visual Studio 2013 Professional Key: ...

  8. QT5.1.0,QT4.8.0以及VC2010、VC2012的测试对比

    QT5.1.0,QT4.8.0以及VC2010.VC2012的交叉测试对比. 测试1: 用VC2012静态编译了QT5.1.0. 编译速度很慢,生成完成后,用VC2012+QT5.1.0进行程序生成, ...

  9. 《Algorithms 4th Edition》读书笔记——3.1 符号表(Elementary Symbol Tables)-Ⅰ

    3.1符号表 符号表最主要的目的就是将一个键和一个值联系起来.用例能够将一个键值对插入符号表并希望在之后能够从符号表的所有键值对中按照键值姐找到对应的值.要实现符号表,我们首先要定义其背后的数据结构, ...

  10. [转]Hulu 2013北京地区校招笔试题

    填空题: 1.中序遍历二叉树,结果为ABCDEFGH,后序遍历结果为ABEDCHGF,逆序遍历结果为? 2.对字符串HELL0_HULU中的字符进行二进制编码,使得字符串的编码长度尽可能短,最短长度为 ...