当前位置: 首页 > news >正文

基于Tars高并发IM系统的设计与实现-实战篇5

基于Tars高并发IM系统的设计与实现-实战篇5

群聊服务 GroupChatServer

群聊服务既可以接受来自BrokerServer的用户请求,也需要接收来自其他服务的RPC请求;所以本服务提供两套RPC接口:通用RPC接口和专用RPC接口。

通用RPC接口

通用RPC接口主要处理如下请求:

  • 创建群聊
  • 群聊加成员
  • 群聊减成员
  • 修改群资料
  • 发群消息
  • 换群主
  • 解散群聊
  • 同步用户群聊
  • 获取群成员
  • 解散群聊
  • 判断一个人是否为群成员

针对以上每个业务,根据用户请求的类型进行不同的业务逻辑处理,处理代码如下:

  switch(req.header.type){case otim::PT_MSG_GROUP_CHAT:this->sendMsg(clientContext, req, resp);break;case otim::PT_GROUPCHAT_SYNC:this->syncGroup(clientContext, req, resp);break;case otim::PT_GROUPCHAT_CREATE:this->createGroup(clientContext, req, resp);break;case otim::PT_GROUPCHAT_JION:this->joinGroup(clientContext, req, resp);break;case otim::PT_GROUPCHAT_QUIT:this->quitGroup(clientContext, req, resp);break;case otim::PT_GROUPCHAT_DISMISS:this->dismissGroup(clientContext, req, resp);break;case otim::PT_GROUPCHAT_UPDATE_CREATOR:this->updateGroupCreator(clientContext, req, resp);break;case otim::PT_GROUPCHAT_INFO_UPDATE:this->updateGroupInfo(clientContext, req, resp);break;case otim::PT_GROUPCHAT_MEMBERS_GET:this->getGroupMember(clientContext, req, resp);break;default:MLOG_DEBUG("the type is  invalid:"<<otim::etos((otim::PACK_TYPE)req.header.type));return otim::EC_PROTOCOL;}

群聊相关请求实现方法:

   int syncGroup(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int createGroup(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int dismissGroup(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int updateGroupCreator(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int updateGroupInfo(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int joinGroup(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int quitGroup(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);int getGroupMember(const otim::ClientContext & clientContext, const otim::OTIMPack & req,  otim::OTIMPack & resp);
专用RPC接口

主要提供两个接口:

interface GroupChatRPCServant
{int getGroupMember(string groupId, out vector<string> memberIds);bool isGroupMember(string groupId, string memberId);};

历史消息服务 HistoryMsgServer

该服务主要处理用户历史消息即相关的业务:

  • 热会话同步
  • 历史消息存取
  • 高优先级消息存取

该服务提供通用RPC服务,主要服务对象为接入服务BrokerServer;
用户所有消息都通过该服务进行存取;为高效存取,历史消息主要存储在redis,存储量及时长可以根据需求进一步来做配置开发。

业务逻辑处理接口

该服务采用通用接口来处理客户端请求;

   tars::Int32 processHotsessionReq(const otim::ClientContext & clientContext,const otim::OTIMPack & req,otim::OTIMPack &resp);tars::Int32 processPullHistoryReq(const otim::ClientContext & clientContext,const otim::OTIMPack & reqPack,otim::OTIMPack &respPack);tars::Int32 processHighPriorMsgSyncReq(const otim::ClientContext & clientContext,const otim::OTIMPack & reqPack,otim::OTIMPack &respPack);

冷存储服务器 OlapServer

该服务主要将IM数据存储到mysql中永久保存;专用RPC服务;

业务逻辑处理接口
interface OlapServant
{int saveMsg(otim::ClientContext clientContext, OTIMPack pack, string sessionId, long seqId);
};

消息操作服务 MsgOperatorServer

该服务主要有如下功能逻辑处理:

  • 消息控制请求(包含撤回,删除,覆写)
  • 消息已读处理
业务逻辑处理接口
    tars::Int32 processMsgUnreadReq(const otim::ClientContext & clientContext,const otim::OTIMPack & reqPack,otim::OTIMPack &respPack);tars::Int32 processMsgCTRLReq(const otim::ClientContext & clientContext,const otim::OTIMPack & reqPack,otim::OTIMPack &respPack);

消息撤回,删除,覆写逻辑处理

tars::Int32 MsgOperatorServantImp::processMsgCTRLReq(const otim::ClientContext & clientContext,const otim::OTIMPack & reqPack,otim::OTIMPack &respPack)
{SCOPELOGGER(scopelogger);scopelogger<<"reqPack:"<<reqPack.header.writeToJsonString();otim::MsgControl req;otim::unpackTars<otim::MsgControl>(reqPack.payload, req);MLOG_DEBUG("clientContext:"<<clientContext.writeToJsonString()<<" req:"<<req.writeToJsonString());respPack = reqPack;respPack.header.flags |= otim::PF_ISACK;otim::CommonErrorCode respData;respData.code = otim::EC_SUCCESS;if (req.sessionId.empty() || req.seqId == 0 || req.packId.empty()){respData.code = otim::EC_PARAM;otim::packTars<otim::CommonErrorCode>(respData, respPack.payload);MLOG_DEBUG("sessionId,packId or seqId is is empty req:"<<req.writeToJsonString());return  respData.code;}otim::RedisConnPtr redis(otim::RedisPool::instance());//get old msgstd::vector<std::string> msgs;std::vector<std::string> scores;EMRStatus ret = redis->ZRangeByScoreAndLimit(otim::RKEY_MSG + req.sessionId, req.seqId, 5, msgs);if (EMRStatus::EM_KVDB_ERROR == ret){MLOG_ERROR("get msg fail!, sessionId:" << req.sessionId<<" msgId:"<<req.seqId);respData.code = otim::EC_DB_ERROR;otim::packTars<otim::CommonErrorCode>(respData, respPack.payload);return otim::EC_DB_ERROR;}MLOG_DEBUG("get old msg size:"<<msgs.size());otim::OTIMPack packOrg;for (auto item : msgs){std::vector<char> vctItem(item.begin(), item.end());otim::OTIMPack packItem;otim::unpackTars<otim::OTIMPack>(vctItem, packItem);MLOG_DEBUG("msgs :"<<packItem.header.writeToJsonString());if (packItem.header.packId == req.packId){packOrg = packItem;}}if (packOrg.header.packId.empty()){MLOG_WARN("The org msg is not exist:"<<req.sessionId<<" packId:"<<req.packId <<" seqId:"<<req.seqId);respData.code = otim::EC_MSG_NOT_EXIST;otim::packTars<otim::CommonErrorCode>(respData, respPack.payload);return respData.code;}MLOG_DEBUG("org msg:"<<packOrg.header.writeToJsonString());std::string to;if (req.command == otim::MC_REVOKE){packOrg.header.flags |= otim::PF_REVOKE;MLOG_DEBUG("revoke msg:"<<req.packId);}else if (req.command == otim::MC_OVERRIDE){packOrg.header.flags |= otim::PF_OVERRIDE;otim::MsgReq msgReq;otim::unpackTars<otim::MsgReq>(packOrg.payload, msgReq);msgReq.content = req.content;otim::packTars<otim::MsgReq>(msgReq, packOrg.payload);to = msgReq.to;MLOG_DEBUG("override msg:"<<req.packId);}else if (req.command == otim::MC_DELETE){
//        packOrg.header.flags |= otim::PF_REVOKE;MLOG_DEBUG("delete msg:"<<req.packId);}else{MLOG_WARN("The command is error:"<<req.command<<" packId:"<<req.packId);respData.code = otim::EC_MSG_OP_CMD;otim::packTars<otim::CommonErrorCode>(respData, respPack.payload);return otim::EC_MSG_OP_CMD;}ret = redis->ZSetRemoveByScore(otim::RKEY_MSG + req.sessionId, req.seqId, req.seqId);if (EMRStatus::EM_KVDB_SUCCESS != ret ){MLOG_ERROR("delete original msg fail:"<<(int)ret);}//增加新的消息if (req.command != otim::MC_DELETE){std::string msgSave;otim::packTars<otim::OTIMPack>(packOrg, msgSave);ret = redis->ZSetAdd(otim::RKEY_MSG + req.sessionId, req.seqId, msgSave);if ( EMRStatus::EM_KVDB_SUCCESS != ret ){MLOG_ERROR("save cancel msg fail!");}}//通知在线接收者其他端otim::sendPackToMySelf(clientContext, reqPack);//  send to userstd::vector<std::string> vctUserId;if (packOrg.header.type == otim::PT_MSG_SINGLE_CHAT || packOrg.header.type == otim::PT_MSG_BIZ_NOTIFY){if (to.empty()){otim::MsgReq msgReq;otim::unpackTars<otim::MsgReq>(packOrg.payload, msgReq);to = msgReq.to;}vctUserId.push_back(to);MLOG_DEBUG("single or notify chat  packId:"<<packOrg.header.packId<<" to:"<<to);}else if (packOrg.header.type == otim::PT_MSG_GROUP_CHAT){//get groupMemberotim::GroupChatRPCServantPrx groupChatRPCServantPrx = otim::getServantPrx<otim::GroupChatRPCServantPrx>(PRXSTR_GROUP_CHAT_RPC);groupChatRPCServantPrx->getGroupMember(req.sessionId, vctUserId);MLOG_DEBUG("group chat  packId:"<<packOrg.header.packId<<" to:"<<req.sessionId<<" member Size:"<<vctUserId.size());}int64_t seqId = otim::genSeqId();for (auto userId : vctUserId){otim::savePriorityMsg(redis.get(), reqPack, userId, seqId);otim::dispatchMsg(clientContext, reqPack, userId);}respData.code = otim::EC_SUCCESS;otim::packTars<otim::CommonErrorCode>(respData, respPack.payload);return otim::EC_SUCCESS;
}

Http接口服务

该服务针对第三方提供消息能力,主要提供如下接口:

  • 发送消息(简单文本消息,复杂消息)
  • 添加好友
  • 删除好友
  • 查看好友

功能实现函数

    std::string  doSendSimpleMsgCmd(TC_HttpRequest &cRequest);std::string  doSendMsgCmd(TC_HttpRequest & cRequest);std::string  doAddFriend(TC_HttpRequest &request);std::string  doDelFriend(TC_HttpRequest &request);std::string  doGetFriends(TC_HttpRequest &request);

Push推送服务

该服务主要实现IM消息的离线推送能力, APP客户端不在线的场景下,将消息通过离线通道push用户的手机上,以提高消息的触达率。

主要实现iOS APNS,Android FCM,华为,小米,oppo,vivo等厂商的离线消息推送功能;
需要根据各个厂商开放平台提供的API进行开发集成。

Android 厂商开发平台地址:

  • 华为:
    https://developer.huawei.com/consumer/cn/hms/huawei-pushkit
  • 小米:
    https://dev.mi.com/console/appservice/push.html
  • 魅族:
    http://open-wiki.flyme.cn/doc-wiki/index
  • vivo:
    https://push.vivo.com.cn/#/
  • oppo:
    https://push.oppo.com/

RPC接口

enum PushServiceType
{PS_TYPE_NONE = 0, //无 Push服务提供商PS_TYPE_IOS = 1,  //IOS Push服务提供商PS_YPE_HUAWEI = 2,   //华为 Push服务提供商PS_TYPE_XIAOMI = 3,   //小米 Push服务提供商PS_TYPE_MEIZU = 4,   //魅族 Push服务提供商PS_TYPE_VIVO = 5,  //vivi服务PS_TYPE_OPPO = 6, //oppo服务PS_TYPE_FCM = 7, //FCM服务
};struct RegInfo {0  require string packId = "";                           //消息的id1  require PushServiceType serviceType = 0;              //push服务提供商2  require string packageName = "";                          //包名3  require string userId = "";                            //用户id4  optional string appVersion = "";                       //app version
};struct PushInfo {0  require string packId = "";                           //消息的id1  require string userId = "";                            //用户id2  require int unReadCount = 0;                          //未读消息数3  require string title = "";                            /push标题4  require string content = "";                          //push内容5  optional string uri = "";                              //跳转uri6  optional string extraData="";                           //业务自定义字段
};interface PushServant
{int register(RegInfo regInfo);int pushMessage(PushInfo pushInfo);
};

服务端部署

编译打包

所有服务开发完成后,执行如下命令进行编译,打包:

make release
make clean all
make tar

程序包部署

根据前期部署好的Tars框架环境、web管理系统,将程序包逐个发布,发布后的系统如图:

在这里插入图片描述

相关文章:

基于Tars高并发IM系统的设计与实现-实战篇5

基于Tars高并发IM系统的设计与实现-实战篇5 群聊服务 GroupChatServer 群聊服务既可以接受来自BrokerServer的用户请求&#xff0c;也需要接收来自其他服务的RPC请求;所以本服务提供两套RPC接口&#xff1a;通用RPC接口和专用RPC接口。 通用RPC接口 通用RPC接口主要处理如下…...

水溶性Cyanine3 N3叠氮化物Cy3 azide星戈瑞

欢迎来到星戈瑞荧光stargraydye! ICG-DBCO点击化学反应在生物标记物探测中应用。通过将ICG-DBCO与具有炔基的生物标记物结合&#xff0c;可以实现快速、选择性和稳定的共价连接&#xff0c;从而实现生物标记和探测。 **以下是ICG-DBCO点击化学反应在生物标记物探测中的一些应用…...

客户案例 | 永续发展,低代码助力“双碳”战略历史使命

关键发现 客户痛点&#xff1a;应对企业数字化转型&#xff0c;新技术能否提升绩效的不确定性&#xff0c;投资带来的风险性&#xff0c;以及企业组织架构的适应性等难点问题。作为业务驱动型企业&#xff0c;欠缺快速构建数字化产品方案的能力。 解决方案&#xff1a;利用西门…...

[保研/考研机试] KY187 二进制数 北京邮电大学复试上机题 C++实现

描述 大家都知道&#xff0c;数据在计算机里中存储是以二进制的形式存储的。 有一天&#xff0c;小明学了C语言之后&#xff0c;他想知道一个类型为unsigned int 类型的数字&#xff0c;存储在计算机中的二进制串是什么样子的。 你能帮帮小明吗&#xff1f;并且&#xff0c;小…...

SpringBoot 热部署

一、启动热部署 1.1 开启开发者工具 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional> </dependency>…...

BLE蓝牙协议栈分析

BLE——协议层次结构 一、BLE Controller Controller实现射频相关的模拟和数字部分&#xff0c;完成最基本的数据发送和接收&#xff0c;Controller对外接口是天线&#xff0c;对内接口是主机控制器接口HCI&#xff08;Hostcontroller interface&#xff09;&#xff1b; 控制…...

flutter开发实战-BackdropFilter高斯模糊子Widget控件

flutter开发实战-BackdropFilter高斯模糊子Widget。 最近开发过程中遇到需要将控件进行模糊&#xff0c;比如iOS的effect的模糊效果。那在flutter中就需要用到了BackdropFilter 一、BackdropFilter BackdropFilter属性定义 BackdropFilter({Key key, required ImageFilter …...

嵌入式面试刷题(day3)

文章目录 前言一、怎么判断两个float是否相同二、float数据可以移位吗三、数据接收和发送端大小端不一致怎么办四、怎么传输float类型数据1.使用联合进行传输2.使用字节流3.强制类型转换 总结 前言 本篇文章我们继续讲解嵌入式面试刷题&#xff0c;给大家继续分享嵌入式中的面…...

JVM源码剖析之Java命令行参数全解

最近&#xff0c;有一位网友询问关于Java命令行参数方面的问题&#xff0c;因为在Java中参数有很多种&#xff0c;有不少的读者一直没弄明白&#xff0c;所以特意写下此篇文章。 此篇文章分2大块&#xff0c;第一块是不同参数的解释&#xff0c;第2块就是JVM源码论证&#xff…...

抽象工厂模式-java实现

介绍 抽象工厂模式基于工厂方法模式引入了“产品族”的概念&#xff0c;即我们认为具体产品是固定的&#xff0c;具体产品存在等级之分&#xff0c;比如我们常说的手机&#xff0c;有“青春版”&#xff0c;“至尊版”&#xff0c;“至臻版”。一个产品有多个版本族。这时候&a…...

机器学习笔记 - 基于Python发现最佳计算机视觉模型的神经架构搜索技术NAS

一、简述 近年来,随着深度学习技术的兴起,计算机视觉领域取得了巨大进步。事实证明,卷积神经网络 (CNN) 在图像识别任务中异常强大,但针对特定问题设计最佳架构仍然是一项具有挑战性的任务。这就是神经架构搜索(NAS)发挥作用的地方。NAS 是一种尖端技术,可以自动发现高性…...

机器学习---自编码器

自编码器过程 输入一个图片&#xff0c;经过encoder变成一个向量&#xff0c;再通过decoder将这个向量反向生成输入的图片。 这里我们希望输入和输出越接近越好。这个过程我们称为重建。 特点&#xff1a;不需要任何的标注资料。 在2006年这个思想就被提出来了&#xff1a; …...

vuejs 设计与实现 - 渲染器的设计

渲染器与响应式系统的结合 本节&#xff0c;我们暂时将渲染器限定在 DOM 平台。既然渲染器用来渲染真实 DOM 元素&#xff0c;那么严格来说&#xff0c;下面的函数就是一个合格的渲染器: // 渲染器&#xff1a; function renderer(domString, container) {container.innerHTM…...

openCV 图像对象的创建和赋值

文章目录 一、赋值二、克隆三、拷贝四、初始化 一、赋值 赋值操作是将一个cv::Mat对象的数据复制到另一个对象中。赋值操作使用的是浅拷贝&#xff08;shallow copy&#xff09;&#xff0c;即两个对象共享相同的数据内存。这意味着对一个对象的修改会影响到另一个对象 cv::M…...

idea - 刷新 Git 分支数据 / 命令刷新 Git 分支数据

一、idea - 刷新 Git 分支数据 idea 找到 fetch 选项&#xff0c;重新获取分支数据 二、命令刷新 Git 分支数据 git fetch参考链接 1. 远程Gitlab新建的分支在IDEA里不显示...

线上电影购票选座H5小程序源码开发

搭建一个线上电影购票选座H5小程序源码需要一些基本的技术和步骤。以下是一个大致的搭建过程&#xff0c;可以参考&#xff1a; 1. 确定需求和功能&#xff1a;首先要明确你想要的电影购票选座H5小程序的需求和功能&#xff0c;例如用户登录注册、电影列表展示、选座购票、订单…...

QT正则校验

文章目录 前言一、Qt正则校验1.对输入框进行校验&#xff0c;不允许输入其他字符2.直接校验字符串 二、常用正则校验表达式 前言 项目中会经常遇到需要对字符串进行校验的情况&#xff0c;需要用到正则表达式&#xff08;Regular Expression&#xff0c;通常简写为RegExp、RE等…...

ChatGPT“侵入”校园,教学评价体制受冲击,需作出调整

北密歇根大学的教授奥曼在学生作业中发现了一篇关于世界宗教的“完美论文”。“这篇文章写得比大多数学生都要好......好到不符合我对学生的预期&#xff01;”他去问ChatGPT&#xff1a;“这是你写的吗&#xff1f;”ChatGPT回答&#xff1a;“99.9%的概率是的。” ChatGPT“侵…...

函数的声明和定义

1、函数声明 //告诉编译器有一个函数叫什么&#xff0c;参数是什么&#xff0c;返回类型是什么。但是具体是不是存在&#xff0c;函数声明决定不了。 //函数的声明一般出现在函数的使用之前。要满足先声明后使用。 //函数的声明一般要放在头文件中的。 2、函数的定义 //函数…...

06微服务间的通信方式

一句话导读 微服务设计的一个挑战就是服务间的通信问题&#xff0c;服务间通信理论上可以归结为进程间通信&#xff0c;进程可以是同一个机器上的&#xff0c;也可以是不同机器的。服务可以使用同步请求响应机制通信&#xff0c;也可以使用异步的基于消息中间件间的通信机制。同…...

浅谈 React Hooks

React Hooks 是 React 16.8 引入的一组 API&#xff0c;用于在函数组件中使用 state 和其他 React 特性&#xff08;例如生命周期方法、context 等&#xff09;。Hooks 通过简洁的函数接口&#xff0c;解决了状态与 UI 的高度解耦&#xff0c;通过函数式编程范式实现更灵活 Rea…...

Xshell远程连接Kali(默认 | 私钥)Note版

前言:xshell远程连接&#xff0c;私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

23-Oracle 23 ai 区块链表(Blockchain Table)

小伙伴有没有在金融强合规的领域中遇见&#xff0c;必须要保持数据不可变&#xff0c;管理员都无法修改和留痕的要求。比如医疗的电子病历中&#xff0c;影像检查检验结果不可篡改行的&#xff0c;药品追溯过程中数据只可插入无法删除的特性需求&#xff1b;登录日志、修改日志…...

srs linux

下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935&#xff0c;SRS管理页面端口是8080&#xff0c;可…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

多模态大语言模型arxiv论文略读(108)

CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题&#xff1a;CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者&#xff1a;Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...

AI病理诊断七剑下天山,医疗未来触手可及

一、病理诊断困局&#xff1a;刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断"&#xff0c;医生需通过显微镜观察组织切片&#xff0c;在细胞迷宫中捕捉癌变信号。某省病理质控报告显示&#xff0c;基层医院误诊率达12%-15%&#xff0c;专家会诊…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用

在工业制造领域&#xff0c;无损检测&#xff08;NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统&#xff0c;以非接触式光学麦克风技术为核心&#xff0c;打破传统检测瓶颈&#xff0c;为半导体、航空航天、汽车制造等行业提供了高灵敏…...

Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?

Pod IP 的本质与特性 Pod IP 的定位 纯端点地址&#xff1a;Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址&#xff08;如 10.244.1.2&#xff09;无特殊名称&#xff1a;在 Kubernetes 中&#xff0c;它通常被称为 “Pod IP” 或 “容器 IP”生命周期&#xff1a;与 Pod …...

学习一下用鸿蒙​​DevEco Studio HarmonyOS5实现百度地图

在鸿蒙&#xff08;HarmonyOS5&#xff09;中集成百度地图&#xff0c;可以通过以下步骤和技术方案实现。结合鸿蒙的分布式能力和百度地图的API&#xff0c;可以构建跨设备的定位、导航和地图展示功能。 ​​1. 鸿蒙环境准备​​ ​​开发工具​​&#xff1a;下载安装 ​​De…...