五、单层无中心集群

对40万用户规模以内的server。使用星形的无中心连接是较为简便的实现方式。分布在各个物理server上的服务进程共同工作。每一个进程承担若干连接。为了实现这个功能,须要解决几个关键问题。

5.1、跨server传输通道

设计在快速局域网中的连接可直接採用TCP,并用第二章介绍的网络传输工具、第三章介绍的流水线线程池共同搭建。引用上述两个工具的代码在cluster子目录的   zp_clusterterm.h中定义:

		ZPNetwork::zp_net_Engine * m_pClusterNet;
ZPTaskEngine::zp_pipeline * m_pClusterEng;

server在专用的集群网络中监听。须要參数例如以下:

1、监听的地址、port

2、本节点唯一名称

3、对server集群内其它节点公布的连接port、地址

4、对公网client公布的连接port、地址。

比方,server快速局域网网段可能是 10.129.XX.XX,而有些server可能以虚拟机(192.168.11.XX)+NAT(10.129.XX.XX)的方式在内网的子网中映射,因此。须要告诉别的server节点,怎样连接到自己。

同一时候。对公网client来说。每一个server的连接地址又不同了。非常有可能也是通过NAT的方式,把数十个内网IP映射到一个外网出口的连续port上去。这个策略的配置页面例如以下:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29sZGVuaGF3a2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

集群的连接策略是,新的server进程选取随意一个现有节点,连接后。通过集群内广播系统自己主动接收其他各个节点的地址。并继续发起连接,直到与现有节点两两相通为止。

为了支持这个策略,集群传输须要定义一些指令。

5.1.1 集群指令

集群指令在 cluster 目录的cross_svr_message.h 定义:

#ifndef CROSS_SVR_MESSAGES_H
#define CROSS_SVR_MESSAGES_H
#include <qglobal.h>
namespace ZP_Cluster{
#pragma pack (push,1)
typedef struct tag_cross_svr_message{
struct tag_header{
quint16 Mark; //Always be 0x1234
quint8 messagetype;
quint32 data_length;
} hearder;
union uni_payload{
quint8 data[1];
struct tag_CSM_heartBeating{
quint32 nClients;
} heartBeating;
struct tag_CSM_BasicInfo{
quint8 name [64];
quint8 Address_LAN[64];
quint16 port_LAN;
quint8 Address_Pub[64];
quint16 port_Pub;
} basicInfo;
struct tag_CSM_Broadcast{
quint8 name [64];
quint8 Address_LAN[64];
quint16 port_LAN;
quint8 Address_Pub[64];
quint16 port_Pub;
} broadcastMsg[1];
} payload;
} CROSS_SVR_MSG; #pragma pack(pop)
} #endif // CROSS_SVR_MESSAGES_H

指令由头部、载荷两部分组成。

头部header说明:

Mark是一个固定的起始,用于验证流解译的正确性。假设流解译不对,第二块指令的起始将不是这个值。

messagetype 是一个用来标定指令类型的字节,决定了载荷联合体该採用哪个策略解译

data_length是长度。这里代表载荷的长度

载荷 payload说明:

有三种指令结构体, 心跳结构体用来维持各个server之间的心跳,基本信息(basicInfo)用于在连接建立后,向对方告知本节点的信息。

广播结构体是用于在本机的server列表发生变更时,向全部现有节点广播新的列表。

对传输的用户数据,直接存储在data中。

5.1.2 连接流程

第一步,准备增加集群的server选取集群中任一个节点作为对象,发起P2P连接。

第二步,两方互换信息(basicInfo)

第三步。两方将对方的信息加入到本地的server节点表中。server节点表是一群zp_ClusterNode类的实例,该类由ZPTaskEngine::zp_plTaskBase派生。这个基类在第三章有介绍。server节点对象的实例负责详细的指令解译。

该列表例如以下(在cluster子目录的   zp_clusterterm.h中定义):

		//important hashes. server name to socket, socket to server name
QMutex m_hash_mutex;
QMap<QString , zp_ClusterNode *> m_hash_Name2node;
QMap<QObject *,zp_ClusterNode *> m_hash_sock2node;

节点的指针存放在映射中,一个是名称到对象的映射,一个是套接字到对象的映射

第四步,因为节点表发生变化。因此。会触发对现有节点的广播(broadCasting)

第五步,各个节点收到广播后。会比較广播中的节点信息和自己眼下的节点信息,并发起向新增节点的连接。

终于,当一对一连接完毕,系统又一次处于稳定状态。解译这段信息的代码片段在中cluster目录zp_clusternode.cpp的deal_current_message_block方法中实现:

		switch(m_currentHeader.messagetype)
{
\\...
case 0x01://basicInfo, when connection established, this message should be used if (bytesLeft==0)
{
QString strName ((const char *)pMsg->payload.basicInfo.name);
if (strName != m_pTerm->name())
{
this->m_strTermName = strName;
m_nPortLAN = pMsg->payload.basicInfo.port_LAN;
m_addrLAN = QHostAddress((const char *)pMsg->payload.basicInfo.Address_LAN);
m_nPortPub = pMsg->payload.basicInfo.port_Pub;
m_addrPub = QHostAddress((const char *)pMsg->payload.basicInfo.Address_Pub);
if (false==m_pTerm->regisitNewServer(this))
{
this->m_strTermName.clear();
emit evt_Message(this,tr("Info: New Svr already regisited. Ignored.")+strName);
emit evt_close_client(this->sock());
}
else
{
emit evt_NewSvrConnected(this->termName());
m_pTerm->BroadcastServers();
}
}
else
{
emit evt_Message(this,tr("Can not connect to it-self, Loopback connections is forbidden."));
emit evt_close_client(this->sock());
}
}
break;
case 0x02: //Server - broadcast messages if (bytesLeft==0)
{
int nSvrs = pMsg->hearder.data_length / sizeof(CROSS_SVR_MSG::uni_payload::tag_CSM_Broadcast);
for (int i=0;i<nSvrs;i++)
{
QString strName ((const char *)pMsg->payload.broadcastMsg[i].name);
if (strName != m_pTerm->name() && m_pTerm->SvrNodeFromName(strName)==NULL)
{
QHostAddress addrToConnectTo((const char *)pMsg->payload.broadcastMsg[i].Address_LAN);
quint16 PortToConnectTo = pMsg->payload.broadcastMsg[i].port_LAN; if (strName > m_pTerm->name())
emit evt_connect_to(addrToConnectTo,PortToConnectTo,false);
else
emit evt_Message(this,tr("Name %1 <= %2, omitted.").arg(strName).arg(m_pTerm->name()));
}
}
}
break;
...

5.2 流式解析

TCP 是面向连接的流式传输。对用户发送的一个大数据包,尽管保证收发的完整性,旦接收方每次接收的数据片段长度是有限的,也是不定的。

一种简单的思路是依照指令结构体的长度。直接缓存完整的数据包,而后集中处理。这样有一个问题,在数据包非常大时。内存开销过高。

因此,本应用设计的思路是边接收、边处理。

详细步骤:

1、检查收到的头部是否合法

2、存储当前指令的头部

3、一旦得到一段载荷数据。就回调一次处理过程,处理过程依据需求等待很多其它数据,或者处理完后清空缓存。这对一次传输100MB数据的应用是非常关键的。流式处理须要完毕的步骤关键代码例如以下:

5.2.1  数据接收

在 zp_ClusterTerm的接收槽里。直接把数据片段压入zp_ClusterNode对象的队列中,并压入流水线。

	//some data arrival
void zp_ClusterTerm::on_evt_Data_recieved(QObject * clientHandle,QByteArray datablock )
{
//Push Clients to nodes if it is not exist
zp_ClusterNode * pClientNode = ...;
int nblocks = pClientNode->push_new_data(datablock);
if (nblocks<=1)
m_pClusterEng->pushTask(pClientNode);
//...
}

oushTask方法把Block压入zp_ClusterNode的处理队列m_list_Rawdata里,这部分的状态变量例如以下:

	class zp_ClusterNode : public ZPTaskEngine::zp_plTaskBase
{
//.....
//Data Process
//The raw data queue and its mutex
QList<QByteArray> m_list_RawData;
QMutex m_mutex_rawData; //The current Read Offset, from m_list_RawData's beginning
int m_currentReadOffset;
//Current Message Offset, according to m_currentHeader
int m_currentMessageSize; //Current un-procssed message block.for large blocks,
//this array will be re-setted as soon as some part of data has been
//dealed, eg, send a 200MB block, the 200MB data will be splitted into pieces
QByteArray m_currentBlock; CROSS_SVR_MSG::tag_header m_currentHeader; //...
};

变量 m_currentReadOffset 指的是队列的首部元素已经处理的偏移。比方首部的Block有2341字节。处理了1099字节,本指令已经结束,则此值为1099

变量 m_currentMessageSize 指的是当前接收的信息的大小。比方100MB 的信息。接受了23MB,这个值就是23MB

变量 m_currentBlock 是当前的缓存。这个缓存会不断的递交处理,负责处理的程序能够依据情况适时清空它。对短指令,不清也是能够的。

变量 m_currentHeader 是当前的信息头部,这个值记录了当前结构体的首部信息。

5.2.2 数据处理

在线程池中,会调用 zp_ClusterNode::run 虚拟方法。这种方法的关键代码例如以下(实际代码由于有线程同步,要复杂一些):

	int zp_ClusterNode::run()
{
//nMessageBlockSize 是静态变量。表示最多处理几个块就释放CPU给其它节点
int nMessage = m_nMessageBlockSize;
int nCurrSz = -1;
while (--nMessage>=0 && nCurrSz!=0 )
{
QByteArray block;
block = *m_list_RawData.begin();
m_currentReadOffset = filter_message(block,m_currentReadOffset);
if (m_currentReadOffset >= block.size())
{
m_list_RawData.pop_front();
m_currentReadOffset = 0;
}
nCurrSz = m_list_RawData.size();
}
if (nCurrSz==0)
return 0;
return -1;
}

当中,filter_message 是对信息进行初步处理。输入当前队列的首部、处理偏移,返回新的处理偏移

这种方法的关键代码例如以下:

	//!deal one message, affect m_currentRedOffset,m_currentMessageSize,m_currentHeader
//!return bytes Used.
int zp_ClusterNode::filter_message(QByteArray block, int offset)
{
const int blocklen = block.length();
while (blocklen>offset)
{
const char * dataptr = block.constData(); //先确保信息的头标志被接收
while (m_currentMessageSize<2 && blocklen>offset )
{
m_currentBlock.push_back(dataptr[offset++]);
m_currentMessageSize++;
}
if (m_currentMessageSize < 2) //First 2 byte not complete
continue; if (m_currentMessageSize==2)
{
const char * headerptr = m_currentBlock.constData();
memcpy((void *)&m_currentHeader,headerptr,2);
} const char * ptrCurrData = m_currentBlock.constData();
//推断头2个字节是不是1234
if (m_currentHeader.Mark == 0x1234)
//Valid Message
{
//试图接收完整的头部信息
if (m_currentMessageSize< sizeof(CROSS_SVR_MSG::tag_header) && blocklen>offset)
{
int nCpy = sizeof(CROSS_SVR_MSG::tag_header) - m_currentMessageSize;
if (nCpy > blocklen - offset)
nCpy = blocklen - offset;
QByteArray arrCpy(dataptr+offset,nCpy);
m_currentBlock.push_back(arrCpy);
offset += nCpy;
m_currentMessageSize += nCpy;
}
//假设头部还没收完则返回
if (m_currentMessageSize < sizeof(CROSS_SVR_MSG::tag_header)) //Header not completed.
continue;
//除了头部以外,还有数据可用,而且头部刚刚接收完
else if (m_currentMessageSize == sizeof(CROSS_SVR_MSG::tag_header))//Header just completed.
{
//保存头部
const char * headerptr = m_currentBlock.constData();
memcpy((void *)&m_currentHeader,headerptr,sizeof(CROSS_SVR_MSG::tag_header)); //继续处理兴许的载荷
if (block.length()>offset)
{
//确定还有多少字节没有接收
qint32 bitLeft = m_currentHeader.data_length + sizeof(CROSS_SVR_MSG::tag_header)
-m_currentMessageSize ;
//继续接收载荷
if (bitLeft>0 && blocklen>offset)
{
int nCpy = bitLeft;
if (nCpy > blocklen - offset)
nCpy = blocklen - offset;
QByteArray arrCpy(dataptr+offset,nCpy);
m_currentBlock.push_back(arrCpy);
offset += nCpy;
m_currentMessageSize += nCpy;
bitLeft -= nCpy;
}
//处理一次数据
deal_current_message_block();
if (bitLeft>0)
continue;
//This Message is Over. Start a new one.
m_currentMessageSize = 0;
m_currentBlock = QByteArray();
continue;
}
}
//除了头部以外,还有数据可用
else
{ if (block.length()>offset)
{
//确定还有多少字节没有接收
qint32 bitLeft = m_currentHeader.data_length + sizeof(CROSS_SVR_MSG::tag_header)
-m_currentMessageSize ;
//继续接收载荷
if (bitLeft>0 && blocklen>offset)
{
int nCpy = bitLeft;
if (nCpy > blocklen - offset)
nCpy = blocklen - offset;
QByteArray arrCpy(dataptr+offset,nCpy);
m_currentBlock.push_back(arrCpy);
offset += nCpy;
m_currentMessageSize += nCpy;
bitLeft -= nCpy;
}
//deal block, may be processed as soon as possible;
deal_current_message_block();
if (bitLeft>0)
continue;
//This Message is Over. Start a new one.
m_currentMessageSize = 0;
m_currentBlock = QByteArray();
continue;
}
} // end if there is more bytes to append
} //end deal trans message
else
//...
} // end while block len > offset return offset;
}

在处理当前块数据的方法 deal_current_message_block里。就可以逐一推断消息类型,加以处理了。

5.3 集群模块外部接口

集群模块仅仅负责在server之间建立连接,并提供一套传输用户数据的通路。

在集群建立连接后。用户直接通过

	void zp_ClusterTerm::SendDataToRemoteServer(QString  svrName,QByteArray  SourceArray)
{
int nMsgLen = sizeof(CROSS_SVR_MSG::tag_header) + SourceArray.size();
QByteArray array(nMsgLen,0);
CROSS_SVR_MSG * pMsg =(CROSS_SVR_MSG *) array.data();
pMsg->hearder.Mark = 0x1234;
pMsg->hearder.data_length = SourceArray.size();
pMsg->hearder.messagetype = 0x03;
memcpy (pMsg->payload.data,SourceArray.constData(),SourceArray.size());
m_hash_mutex.lock();
if (m_hash_Name2node.contains(svrName))
netEng()->SendDataToClient(m_hash_Name2node[svrName]->sock(),array);
m_hash_mutex.unlock();
}

向server svrName发送 SourceArray, 并响应

void evt_RemoteData_recieved(QString /*svrHandle*/,QByteArray  /*svrHandle*/ );

信号来接收数据。

用户不用关心传输协议的封装和解析。

可是,下面问题是不涉及的。

1、传输的数据的详细意义解释

2、全局client的UUID哈希和同步

3、client数据是否被真正接收。

这些部分留给应用相关部分来详细实现。

一种基于Qt的可伸缩的全异步C/S架构server实现(五) 单层无中心集群的更多相关文章

  1. 一种基于Qt的可伸缩的全异步C/S架构server实现(一) 综述

    本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/Sclient-服务集群应用需求而搭建. 连接监听.传输数据.数据处理均在独立的线程池中进行,依据特定任务不同,可安排负责监听.传输.处理 ...

  2. 一种基于Qt的可伸缩的全异步C/S架构server实现(二) 网络传输

    二.网络传输模块 模块相应代码命名空间    (namespace ZPNetwork) 模块相应代码存储目录    (\ZoomPipeline_FuncSvr\network) 2.1 模块结构 ...

  3. 一种基于Qt的可伸缩的全异步C/S架构服务器实现(流浪小狗,六篇,附下载地址)

    本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/S客户端-服务集群应用需求而搭建.连接监听.数据传输.数据处理均在独立的线程池中进行,根据特定任务不同,可安排负责监听.传输.处理的线程数 ...

  4. 一种基于Qt的可伸缩的全异步C/S架构服务器实现(一) 综述

    本文向大家介绍一种基于Qt的伸缩TCP服务实现.该实现针对C/S客户端-服务集群应用需求而搭建.连接监听.数据传输.数据处理均在独立的线程池中进行,根据特定任务不同,可安排负责监听.传输.处理的线程数 ...

  5. 基于 Docker 搭建 Consul 多数据中心集群

    本文介绍了在 Windows 10 上基于 Docker 搭建 Consul 多数据中心集群的步骤,包括 Consul 镜像的拉取和容器的创建,每个数据中心对应服务端节点和客户节点的创建,节点之间相互 ...

  6. 一种基于C51单片机的非抢占式的操作系统架构

    摘 要:从Keil C51的内存空间管理方式入手,着重讨论实时操作系统在任务调度时的重入问题,分析一些解决重入的基本方式与方法:分析实时操作系统任务调度的占先性,提出非占先的任务调度是能更适合于Kei ...

  7. 利用ZoomPipeline迅速实现基于线程池的全异步TCP点对点代理

    在博文<一种基于Qt的可伸缩的全异步C/S架构服务器实现>中提到的高度模块化的类可以进行任意拆解,实现非常灵活的功能.今天,我们来看一看一个公司局域网访问英特网云服务器的点对点代理例子.代 ...

  8. Galera Cluster——一种新型的高一致性MySQL集群架构

    原文链接:https://www.sohu.com/a/147032902_505779,最近被分配定位mysql的问题,学习下. 1. 何谓Galera Cluster 何谓Galera Clust ...

  9. 【分布式事务】基于RocketMQ搭建生产级消息集群?

    导读 目前很多互联网公司的系统都在朝着微服务化.分布式化系统的方向在演进,这带来了很多好处,也带来了一些棘手的问题,其中最棘手的莫过于数据一致性问题了.早期我们的软件功能都在一个进程中,数据的一致性可 ...

随机推荐

  1. h5-7 canvas

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  2. [ASP.Net] 20141228_Dapper文章搜集

    DbHelperSQL和Dapper数据访问的性能对比 给力分享新的ORM => Dapper 分享一个轻型ORM--Dapper选用理由

  3. Netty简单介绍(非原创)

    文章大纲 一.Netty基础介绍二.Netty代码实战三.项目源码下载四.参考文章   一.Netty基础介绍 1. 简介 官方定义为:”Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地 ...

  4. 2015 多校赛 第一场 1001 (hdu 5288)

    Description OO has got a array A of size n ,defined a function f(l,r) represent the number of i (l&l ...

  5. 【media-queries】媒体查询,为了响应式设计而生

    目录 简介 语法 常用尺寸 一 简介 针对现在纷杂的设备,css3中加入,可以查询你的浏览类型(screen彩色屏幕, print, all)和css属性判断. 最常用的就是查询屏幕大小,给予适合的展 ...

  6. Android 低功耗蓝牙的多设备连接与数据接收,简单实现

    在网络层,互联网提供所有应用程序都要使用的两种类型的服务,尽管目前理解这些服务的细节并不重要,但在所有TCP/IP概述中,都不能忽略他们: 无连接分组交付服务(Connectionless Packe ...

  7. seo在前端网页制作的应用

    学习了慕客网上的“SEO在网页制作中的应用‘’,下面来进行小小的学习总结,顺便梳理下知识.所谓学而不思则罔思而不学则殆.下面开始正文. 一.搜索引擎的工作原理 搜索引擎的基本工作原理包括如下三个过程: ...

  8. JavaScript中的数组创建

    JavaScript中的数组创建 数组是一个包含了对象或原始类型的有序集合.很难想象一个不使用数组的程序会是什么样. 以下是几种操作数组的方式: 初始化数组并设置初始值 通过索引访问数组元素 添加新元 ...

  9. 23个Python爬虫开源项目代码:爬取微信、淘宝、豆瓣、知乎、微博等

    来源:全球人工智能 作者:SFLYQ 今天为大家整理了23个Python爬虫项目.整理的原因是,爬虫入门简单快速,也非常适合新入门的小伙伴培养信心.所有链接指向GitHub,祝大家玩的愉快 1.Wec ...

  10. AngularJS指令进阶 -- ngModelController详解

    大家都知道AngularJS中的指令是其尤为复杂的一个部分,但是这也是其比较好玩的地方.这篇文章我们就来说一说如何在我们自定义的指令中,利用ngModel的controller来做双向数据绑定,本文对 ...