欢迎光临
我们一直在努力

jenkins手把手教你从入门到放弃03-安装Jenkins时web界面出现该jenkins实例似乎已离线

北京-宏哥阅读(128)

简介

  很久没有安装jenkins了,因为之前用的的服务器一直正常使用,令人郁闷的是,之前用jenkins一直没出过这个问题。

令人更郁闷的是,我尝试了好多个历史版本和最新版本,甚至从之前的服务器把jenkins在跑的程序打包copy这个服务器。终究还是不行。

  启动时候,提示:该jenkins实例似乎已离线

可以说是非常坑!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  虽然可以离线安装,但是对于博主来说不解决怎么行呢?经过一番踩坑与资料查找终于解决了,这里与大家分享一下:

问题如图下所示:

 

解决上述问题方法:

1)    修改/var/lib/jenkins/updates/default.json

jenkins在下载插件之前会先检查网络连接,其会读取这个文件中的网址。默认是:

访问谷歌,这就很坑了,服务器网络又不能FQ,肯定监测失败呀,不得不说jenkins的开发者脑子锈了,所以将图下的google改为www.baidu.com即可,更改完重启服务。

2)    修改/var/lib/jenkins/hudson.model.UpdateCenter.xml

该文件为jenkins下载插件的源地址,改地址默认jenkins默认为:https://updates.jenkins.io/update-center.json,就是因为https的问题,此处我们将其改为http即可,之后重启jenkins服务即可。

其他国内备用地址(也可以选择使用):

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

http://mirror.esuni.jp/jenkins/updates/update-center.json

 3)安装插件那个页面,就是提示你offline的那个页面,不要动。然后打开一个新的tab,输入网址http://localhost:8080/jenkins/pluginManager/advanced。 这里面最底下有个【升级站点】,把其中的链接改成http的就好了,

http://updates.jenkins.io/update-center.json。 然后在服务列表中关闭jenkins,再启动,这样就能正常联网了

在修复完之后,我们发现离线问题已经解决,如图下所示:

这样我们就可以愉快地安装插件了。

无聊吗?写个【飞机大战】来玩吧(下篇)

web前端talk阅读(109)

上一篇介绍了如何使用cocos creator开发游戏,此篇是详细介绍功能点以及如何部署打包至微信小游戏体验。

资源管理制作

1、准备工具

cocos creatorv2.0.5官方最新版本、sublime tetx3或vscode、texturePackerGUI、微信小程序开发工具

 

2、sprite,图集资源制作

(1)图集列表类似于UI里的瀑布流图,将所有的.png .jpg .jpeg图片合并压缩为一个图集资源。

 

(2)使用texturePackerGUI工具,打开工具,将本机示例图片文件夹下所有.jpg拖入至左处empty…

 

左图为未拖入图片的,右图为已拖入图片

Data file是设置cocos图集.plist文件保存路径,texture file是.png保存路径,max size是设置最大尺寸

点击publish sprite sheet保存文件,由于系统的图片较大,max size设置为3072之后才能publish,生成后的.png文件巨大,不建议每个图片文件超过100kb以上进行合并。再从cocos creator工具中查看,图集atlas制作完毕

3、动画animationClip制作

(1)准备多个静态图,在层级下新增一个空节点,添加sprite、animation组件,sprite拖入一个静止时的背景图,选中当前节点,选择下方动画编辑器,新建clip文件,保存在animation文件夹下,此时文件夹下多了一个textAni,点击红框内编辑动画。

属性列表中选择cc.Sprite.spriteFrame,是插入帧图片属性,比如每0.1间隔插入一帧图片,每帧图片所展示的效果渐渐呈现,线性时间内播放动画。

Animation组件有
position,x,y,
scale,scaleX,scaleY,rotation,
width,height,color,opacity,
anchorX,anchorY,skewX,skewY,
cc.Sprite.spriteFrame、
cc.Sprite.fillType,
cc.Sprite.fillCenter,
cc.Sprite.fillStart,
cc.Sprite.fillRange,
这些属性都将影响到动画帧效果。

(2)将textAni Clip拖入至对应节点

4、prefab制作

(1)创建空节点拖入至资源管理器的prefab文件夹,点击该prefab,右侧可添加任意组件,例如:渲染组件->Label,修改string为“hello world!!!”

(2)如何获取prefab,新增命名为test的JavaScript脚本,编辑脚本,在properties申明

 

{
testPrefab: cc.Prefab,
btn: cc.Node,
bg: cc.Node,
_num:1
}

在onload函数中创建对象池this.pipePool = new cc.NodePool();申明createPre函数,用于挂载在开始游戏button节点,按钮每点击一次从对象池拿出prefab对象,重新计算并给定y轴位置,渲染到bg节点。

关键代码:this.pipePool.put(tPrefab);

官方说法是向缓冲池中存入一个不再需要的节点对象。这个函数会自动将目标节点从父节点上移除,但是不会进行 cleanup 操作;如果想让节点一直存在,则不使用该方法(当生成的节点过多时,占用内存过大,导致性能问题,需要及时释放)。

5、audioClip

音乐资源,和图片一样直接拖入到sound下即可,一般以.mp3文件为准

游戏部署 

1、准备工作:微信小游戏appid,微信小游戏开发权限(公众号,或小程序上可申请),可https访问的服务器管理权限;

2、cocos creator2.0以上提供了构建打包至多平台功能,可以直接打包成微信小游戏包,选择工具菜单栏->项目->构建发布,根据打包环境要求,选择发布平台,这里选择wechat game,初始场景为游戏加载时第一场景展示,填写微信小游戏appid,填写远程resource服务器地址,也可不填。

构建打包成功之后,打开微信小程序开发工具,选择小游戏,点击+选中刚打包后的项目路径;

由于微信小游戏打包发布,代码体积被限制4M之内,我们不得不优化。唯一能动的是res资源文件。

先关闭微信小游戏项目,将资源文件夹res移除,上传res文件夹到远程服务器下,可通过https访问到即可,再重新打开微信小程序开发工具,设置game.js。

wxDownloader.REMOTE_SERVER_ROOT = ‘你的远程服务器res绝对路径’;这样打包体积就缩小一半以上。点击右上角上传,显示为体验版,点击确定,填写发布信息后上传。

游戏体验

1、在游戏未正式发布前,无法直接通过小程序搜索进行游戏,需要添加体验号。

2、由于开发者本人就是管理员,可以直接进行游戏

3、微信游戏体验预览图

可落地的DDD(3)-如何利用DDD进行微服务的划分

stoneFang阅读(104)

摘要

前面两篇介绍了DDD的目标管理、DDD的工程结构调整。这篇讨论微服务的划分。微服务是目前后端比较流行的架构体系了,那么如何做好一个微服务的划分?一个微服务的粒度应该是多大呢?这篇主要介绍如何结合DDD进行领域划分。

工程结构代码

上篇介绍了可落地的DDD的(2)-为什么说MVC工程架构已经过时
很多朋友留言说,有没有sample code,要不然太湿了,不是很明白。这里写了个sample。

就以一个博客网站为例
page1:博客列表页: 展示所有用户发表的博客

page2: 个人介绍页:有个人简介和博客列表

page3:博客详情页.

不同的业务展示的用户/博客的字段不一致

建模


后期应该会有用户和博客交互的需求。这期只有用户创建博客这层关联关系。

MVC架构

使用mvc模式写出来的代码,就是一路到底。
具体代码见mvc-structure

DDD

使用DDD写出来的工程结构就是,blog和user的交互只有一个地方,OpenXXXService
具体代码见DDD structure

MVC VS DDD

从两张依赖图可以看出,DDD的依赖图清晰了,user和blog这两个领域之间的交互变的清晰了,user这个领域不用管blog领域发生了什么变更。只依赖OpenBlogService,而MVC模式呢,user和blog之间的交互太多了,如果再增加其他功能,这些模块之间就是够筹交错,理不清楚。

不同领域之间的交互多了,意味着一旦发生变更,需要修改逻辑了,那么需要修改的地方就是几何倍数的相关类。

比如业务发生了变更,统计【个人中心】的博客计数是用户已发表的文章。而这个已发表的可能随着业务的发展,包含多重含义

  1. 用户写完了,对外开放了,
  2. 运营审核通过
  3. 博客未被软删除的。
    这些逻辑都会影响相关的查询,而这些逻辑的实现可能在数据库层面做,可能在redis中做,或者其他的方式。以MVC的写法,需要的需要修改的地方很多,以DDD的方式,不管这个逻辑怎么变,其他领域不需要知道,只有blog领域知道,只用更改blog领域的代码。

微服务划分

初版

确定了以DDD作为我们领域划分的指导原则后,我们首先按照领域对我们的业务进行了全面的分析,区分出哪些领域。然后按照如下标准进行了微服务的拆分

  1. 一个领域一个服务的规则去拆分,
  2. 同时为了保证领域的纯洁性,我们区分了领域服务,和前台服务。领域服务就是领域逻辑,不直接对前端暴露。前台服务组装各个领域服务,暴露给前端。
  3. 同时为了保持扩展,我们预留了一个微服务作为服务孵化器。对于领域不清晰的(比如大部分的新的业务),放在这个服务里面孵化,然后等领域足够大的时候再拆分出去。

如下图

前台应用:
pc: pc端的页面展示
mobile: 移动端的页面展示
mini:小程序的页面展示

领域服务:
blog: 博客领域
user: 用户领域
growth: 领域孵化器

按照这样的标准去拆分后,一段时间后,很多问题暴露了。

  • 服务热点问题
    我们是一个新的业务,在业务迭代的过程中,大部分新需求都是领域不清晰,不知道能不能迭代下去的。所以按照之前的标准,都往growth服务里面去写代码,这样导致几乎团队里面的所有的人都在开发这一个项目,失去了拆分微服务的意义。

  • 服务依赖太严重
    无论写什么需求,都需要写多个应用,领域服务1个,前台如果有pc,需要在pc服务上开发,移动端要展示,要在mobile服务开发。服务之间的调用需要写rpc client接口,需要发版本,因为同时开发的人多,经常发生版本混乱,依赖问题。服务上线也很头疼,改一个小需求,需要部署多个服务。微服务一个很重要的点是去耦合,可独立部署。多了一层UI层作为微服务显然不是很合适。

  • 领域划分有问题
    一个领域一个服务,粒度太小,有些东西不知道放在哪个服务里面,比如用户收藏博客,是放在用户服务里面,还是放在博客领域呢。

如何解决

不拆分单体应用不知道,一拆分问题一大堆。那么我们是怎么解决的呢?下期再见。

相关阅读
可落地的DDD(1)-目标讨论
可落地的DDD的(2)-为什么说MVC工程架构已经过时

关注【方丈的寺院】,第一时间收到文章的更新,与方丈一起开始技术修行之路

ABP开发框架前后端开发系列—(5)Web API调用类在Winform项目中的使用,ABP开发框架前后端开发系列—(4)Web API调用类的封装和使用

admin阅读(129)

在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开发需求,一旦整个模式比较成熟,并以一种比较固化的模式来指导开发,那么就可以很方便的应用在实际项目开发当中了。本篇随笔是基于前面几篇的基础上,在Winform项目上进一步改造为实际项目的场景,把我原来基于微软企业库底层的数据库访问方式的Winform框架或者混合框架的字典模块界面改造为基于ABP框架基础上的字典应用模块。

1)APICaller层接口的回顾

在上一篇随笔《ABP开发框架前后端开发系列—(4)Web API调用类的封装和使用》中,我介绍了Web API调用类的封装和使用,并介绍了在.net 控制台程序中,测试对ApiCaller层的调用,并能够顺利返回我们所需要的数据。测试代码如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
    {
        var caller = client.Object;

        Console.WriteLine("Logging in with TOKEN based auth...");
        var token = caller.Authenticate("admin", "123qwe").Result;
        Console.WriteLine(token.ToJson());

        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

        Console.WriteLine("Get All ...");
        var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10 };
        var result = caller.GetAll(pagerDto).Result;
        Console.WriteLine(result.ToJson());

        Console.WriteLine("Get All by condition ...");
        var pagerdictDto = new DictTypePagedDto() { Name = "民族" };
        result = caller.GetAll(pagerdictDto).Result;
        Console.WriteLine(result.ToJson());
        
        Console.WriteLine("Get count by condition ...");
        pagerdictDto = new DictTypePagedDto() {};
        var count = caller.Count(pagerdictDto).Result;
        Console.WriteLine(count);
        Console.WriteLine();

        Console.WriteLine("Create DictType...");
        var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test" };
        var dictDto = caller.Create(createDto).Result;
        Console.WriteLine(dictDto.ToJson());

        Console.WriteLine("Update DictType...");
        dictDto.Code = "testcode";
        var updateDto = caller.Update(dictDto).Result;
        Console.WriteLine(updateDto.ToJson());

        if (updateDto != null)
        {
            Console.WriteLine("Delete DictType...");
            caller.Delete(new EntityDto<string>() { Id = dictDto.Id });
        }

    }
    #endregion

这些ApiCaller对象的接口测试代码,包括了授权登录,获取所有记录,获取条件查询记录,创建、更新、删除这些接口都成功执行,验证了我们对整体架构的设计改良,并通过对ApiCaller层基类的设计,减少我们对常规增删改查接口的编码,我们只需要编写我们的自定义业务接口代码封装类即可。

其中基类的代码如下所示。

针对Web API接口的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

也就是说,上面我们全部是基于基类接口的调用,还不需要为我们自定义接口编写任何一行代码,已经具备了常规的各种查询和数据处理功能了。

我们完整的字典类型ApiCaller类的代码如下所示。

namespace MyProject.Caller
{
    /// <summary>
    /// 字典类型对象的Web API调用处理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供单件对象使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

        /// <summary>
        /// 默认构造函数
        /// </summary>
        public DictTypeApiCaller()
        {
            this.DomainName = "DictType";//指定域对象名称,用于组装接口地址
        }

        public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?dictTypeId={0}", dictTypeId);

            var result = await apiClient.GetAsync<Dictionary<string, string>>(url);
            return result; 
        }

        public async Task<IList<DictTypeNodeDto>> GetTree(string pid)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?pid={0}", pid);

            var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);
            return result;
        }
    }

这里面的函数定义才是我们需要根据实际的自定义接口封装的调用类函数代码。

前面我们介绍了,我们把ApiCaller层的项目设计为.net Standard的类库项目,因此可以在.net core或者在.net framework中进行使用,并且也在基于.net core的控制台程序中测试成功了。

下面就重点介绍一下,基于.net framework的Winfrom程序中对ABP框架的Web API接口的调用,如果以后Winform支持.net core了(据说9月份出的.net core3就包含了),那么也一样的模式进行调用。

 

2)Winform对ApiCaller层的调用

我们先来看看字典模块,通过封装对ABP框架的Web API调用后,实际的功能界面效果吧。

先设计一个授权登录的界面获取访问令牌信息。

字典管理界面,列出字典类型,并对字典类型下的字典数据进行分页展示,分页展示利用分页控件展示。

新增或者编辑窗体界面如下

这个界面是来自于我的框架里面的字典模块界面,不过里面对数据的处理代码确实已经更改为适应ABP框架的Web API接口的调用的了(基于ApiCaller 层的调用)。

我们下面来一一进行分析即可。

登陆界面,我们看看主要的逻辑就是调用获取授权令牌的接口,并存储起来供后续界面中的业务类进行调用即可。

由于我们自己封装的ApiCaller类,都是基于异步的方式封装的,因此我们可以看到很多地方调用都使用await的关键字,这个是异步调用的关键字,如果方法需要定义为异步,就需要增加async关键字,一般这两个关键字是配套使用的。

如果我们在事件处理代码里面使用了异步,那么事件的函数也需要标记为async,如下是字典管理模块窗体的加载函数,也是用了async声明 和await调用异步方法标记。

        private async void FrmDictionary_Load(object sender, EventArgs e)
        {
            await InitTreeView();

            this.lblDictType.Text = "";
            await BindData();
            
            //分页控件事件处理代码
            this.winGridViewPager1.OnPageChanged += new EventHandler(winGridViewPager1_OnPageChanged);
            this.winGridViewPager1.OnStartExport += new EventHandler(winGridViewPager1_OnStartExport);
            this.winGridViewPager1.OnEditSelected += new EventHandler(winGridViewPager1_OnEditSelected);
            this.winGridViewPager1.OnAddNew += new EventHandler(winGridViewPager1_OnAddNew);
            this.winGridViewPager1.OnDeleteSelected += new EventHandler(winGridViewPager1_OnDeleteSelected);
            this.winGridViewPager1.OnRefresh += new EventHandler(winGridViewPager1_OnRefresh);
            this.winGridViewPager1.AppendedMenu = this.contextMenuStrip2;

            this.winGridViewPager1.BestFitColumnWith = false;
            this.winGridViewPager1.gridView1.DataSourceChanged += new EventHandler(gridView1_DataSourceChanged);
        }

我们的数据,主要是在BindData里面实现,这个函数是我们自己加的,由于使用了异步方法,因此也用async进行声明。

整个对于分页的数据获取和控件的数据绑定过程,代码如下所示。

        /// <summary>
        /// 获取数据
        /// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<DictDataDto>> GetData()
        {
            //构建分页的条件和查询条件
            var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
            {
                DictType_ID = string.Concat(this.lblDictType.Tag)
            };
            var result = await DictDataApiCaller.Instance.GetAll(pagerDto);
            return result;
        }

        /// <summary>
        /// 绑定数据
        /// </summary>
        private async Task BindData()
        {
            #region 添加别名解析
            this.winGridViewPager1.DisplayColumns = "Name,Value,Seq,Remark,EditTime";
            this.winGridViewPager1.AddColumnAlias(Id_FieldName, "编号");
            this.winGridViewPager1.AddColumnAlias("DictType_ID", "字典大类");
            this.winGridViewPager1.AddColumnAlias("Name", "项目名称");
            this.winGridViewPager1.AddColumnAlias("Value", "项目值");
            this.winGridViewPager1.AddColumnAlias("Seq", "字典排序");
            this.winGridViewPager1.AddColumnAlias("Remark", "备注");
            this.winGridViewPager1.AddColumnAlias("Editor", "修改用户");
            this.winGridViewPager1.AddColumnAlias("EditTime", "更新日期");
            #endregion

            if (this.lblDictType.Tag != null)
            {
                var result = await GetData();

                //设置所有记录数和列表数据源
                this.winGridViewPager1.DataSource = result.Items;
                this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount;
            }
        }

其中注意的是GetAll方式是传入一个条件查询的对象,这个就是DictDataPagedDto是我们定义的,放入我们DictDataDto里面的常见属性,方便我们根据属性匹配精确或者模糊查询。

    /// <summary>
    /// 用于根据条件查询
    /// </summary>
    public class DictDataPagedDto : PagedResultRequestDto
    {
        /// <summary>
        /// 字典类型ID
        /// </summary>
        public virtual string DictType_ID { get; set; }

        /// <summary>
        /// 类型名称
        /// </summary>
        public virtual string Name { get; set; }

        /// <summary>
        /// 指定值
        /// </summary>
        public virtual string Value { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        public virtual string Remark { get; set; }
    }

我们在调用的时候,让它限定为一个类型的ID进行精确查询,如下代码

//构建分页的条件和查询条件
var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
    DictType_ID = string.Concat(this.lblDictType.Tag)
};

这个精确或者模糊查询,则是在应用服务层里面定义规则的,这个之前没有详细介绍了,这里稍微补充说明一下。

在应用服务层接口类里面,重写CreateFilteredQuery可以设置GetAll的查询规则,重写ApplySorting则可以指定列表的排序顺序。

再次回到Winform界面的调用上来,删除类型下面字典数据的事件的处理函数如下所示。

        private async void menu_ClearData_Click(object sender, EventArgs e)
        {
            TreeNode selectedNode = this.treeView1.SelectedNode;
            if (selectedNode != null && selectedNode.Tag != null)
            {
                string typeId = selectedNode.Tag.ToString();
                var dict = await DictDataApiCaller.Instance.GetDictByTypeID(typeId);
                int count = dict.Count;

                var format = "您确定要删除节点:{0},该节点下面有【{1}】项数据";
                format = JsonLanguage.Default.GetString(format);
                string message = string.Format(format, selectedNode.Text, count);

                if (MessageDxUtil.ShowYesNoAndWarning(message) == DialogResult.Yes)
                {
                    try
                    {
                        await DictDataApiCaller.Instance.DeleteByTypeID(typeId);
                        await InitTreeView();
                        await BindData();
                    }
                    catch (Exception ex)
                    {
                        LogTextHelper.Error(ex);
                        MessageDxUtil.ShowError(ex.Message);
                    }
                }
            }
        }

我们看看编辑窗体界面的后台处理,编辑和更新数据的逻辑代码如下所示。

                #region 编辑大类
                var info = await DictTypeApiCaller.Instance.Get(new EntityDto<string>(ID));
                if (info != null)
                {
                    SetInfo(info);

                    try
                    {
                        var updatedDto = await DictTypeApiCaller.Instance.Update(info);
                        if (updatedDto != null)
                        {
                            MessageDxUtil.ShowTips("保存成功");
                            this.DialogResult = DialogResult.OK;
                        }
                    }
                    catch (Exception ex)
                    {
                        LogTextHelper.Error(ex);
                        MessageDxUtil.ShowError(ex.Message);
                    }
                } 
                #endregion

最后来一段gif动图,展示程序的操作功能吧。

好了,这些事件的使用规则一旦确定了,我们好利用代码生成工具对窗体界面的代码进行统一规则的生成,就好像我前面对于我Winform框架和混合框架里面的Winform窗体界面的生成一样,我们只需要稍微修改一下代码生成工具的NVelocity模板,利用上数据库表的元数据就可以快速生成整个框架所需要的代码了。

这样基于整个ABP框架,而快速应用起来的项目,其实开发项目的工作量看起来也不会很多,而且我们可以把字典、权限控制、整体框架等基础设施建设好,就会形成一整套的开发方法和思路了,这样对于我们利用ABP框架来开发业务系统,是不是有事半功倍的感觉。

一旦某个东西你很喜欢,你就会用的越来越好。

我的程序人生 (三)在百人团队参与游戏研发体验,我的程序人生 (二)工作两年多的的回忆经历,我的程序人生 (一)学生时代

不敲代码了去打网球了阅读(112)

序言     

  不知不觉中,已经在某厂(1000-9999规模纯游戏公司)工作快一年半了。也是时候该总结总结啦,故,写该篇之。(假装很文艺)

这是第三篇,那么肯定也有前面两篇,给出链接:

第一篇: 我的程序人生 (二)工作两年多的的回忆经历

第二篇:我的程序人生 (一)学生时代

一. 自我介绍      

      楼主目前约三年工作经验,一直从事的是游戏开发,还未做过其他,跟的项目团队总人数目前120,其中程序40人的游戏研发团队。

我呐,当然是做前端开发的普普通通一名奋斗在最前线的“一线”做开发的客户端程序员。做的活不是很简单的,但是也不是最核心的,

独立负责开发&维护某个模块,例如工会系统啊,好友系统,最近也负责战斗核心模块(负责核心模块并不是说负责最难的技术,游戏这边是渲染,最难的,我0涉及)。

自我感觉团队中,技术不是最好的,但是也不是最差的。至于认真程度,感觉也不是最好的,也不是最差的,真的是很普通的一名程序员。

二.团队&项目介绍

  团队:感觉整体我感觉很好。主策划很给力,很认真负责的那种,能力也超级强,真的绝非池中之物。果然,在大厂能当一线干活老大

的绝非等闲之辈。主程(就是我的总老大),人超级好,超级和蔼,技术实力综合也很强,各方面也都为我们程序着想,情商都是很高的,我当然是那种很顺从的

态度。这里,还要介绍一下我的直系老大,因为程序40人,还被分为若干个小组,也就是我的组长,我的直系老大,人也是特别好,真的真的那种

特别好的那种,基本有什么不明白的,我只要问,都会悉心告知我。当然我也不是那种总问的。(有那种总是喜欢问的….)

  项目:关于项目呐,额,这个不方便透露太多,总之,团队给力,里面老大给力,项目也不会多差的,进度方面,都很好。

三.经历

  试用期:  

    历时三个月。

    真的是我这辈子最紧张,最绝望的三个月试用期…

    也许是老大给我一个机会,(我毕竟是普通二本院校毕业,本科,16年毕业,毕业其实2年而已)。试用期给我做的一个东西,真的是巨难。

据说,这个任务是我的组长专门没做,留着,放着(我感觉应该也是不好做,刚好给我考验我),所以给我这个新人来做的(真的惨),我真的是尝试了无数方案,感觉就算是纯图形学方面专家才能实现。

记得第一个月的时候,先是组长让我做,并且给了我插件,让我做,最终没有实现策划要的结果;于是乎,第二个月,自己重新又自己找插件,跟公司申请买插件,但是还是没做出来,就这样,整整耗费又一个月;当时两个月啥东西都没弄出来的我,真的感觉自己是不是要被辞退了,一度怀疑自己,难道能力匹配不上现在的公司吗?于是吃不好,睡不着,很焦虑。即使真的被开除了,也能理解,毕竟2个月零产出啊,但是我的老大对我还是很和颜悦色,也一直在鼓励我,也有时候会帮助我,一起寻找探讨解决方案。转眼又到第三个月,有一天,组长说他突然灵光一闪,跟我说了个思路,其实就是给了我两个字,真就只是两个字,但是我就是靠这两个字,朝着那个思路,自己整整又捣鼓了一个月,最后终于是做出来了。

    程序员东西做不出来,真的好可怜,好惨的。尤其是我这类,越是做不出来,就越吃不下去饭,睡不着觉,尤其晚上睡觉的时候,即使身体很累,也会大脑特别兴奋的在想那个东西怎么做,真的是各种想。还有就是吃饭没胃口,不知道有没有人跟我一样。

              我以为,难道我之后做的东西都是这类“研究性质”的功能吗,也太伤身体了。哈哈哈哈,实践证明,难得东西,也就那么一点点吧。

        转正后的平淡期:

    终于是转正了,毕竟,难得东西总是那么一点,之后一直到现在都在做一些功能模块方面的东西,肯定是能做出来,但是做的好坏,又是另外一回事。我当然是尽可能把他做得好一些。一直的一直,都在做着功能性的东西,有点忙,算是996吧。但是毕竟人数那么多,有时候因为需要做的功能对应的策划同学并没有把相应的策划案弄出来,因为策划压力也是很大的,幸好我不做策划,因为策划同学出的策划案,有概率会被boss刷下说No,重做,在这里心疼策划同学。于是就会“闲”一段时间。这个时候,团队中有些人就浑水摸鱼,而有些人,就自己主动学习。我呐,当然是那类主动学习的啦。当然,其实也有那么一段时间“浑水摸鱼”,但是我只是“偷懒”了那么一小段时间,很快就幡然醒悟。大部分时间,无论忙还是空闲,自己都会早上提前半小时到一个小时到公司,看点书,记点笔记之类,毕竟程序员是一个需要不断学习的职业,毕竟拿这么高工资嘛。但是又不进则退。感觉自己最大的优势,就是真心喜欢这门技术吧。

  游戏上线期:

    这个时候,当然是最忙的。但是,也终于明白了一句话,就是很多招聘都要完整成功游戏上线经验,为何呐?我现在终于明白他的重要性,因为你做的东西上线,进行各种阶段测试,什么公司内部测试,粉丝内部1000人测试等等。各种你做的东西的bug就显示出来,极其考验代码质量。当然还有最考验的是你写的东西的性能如何,因为要兼容各种地段手机。然后你得需要各种对象池,初始化缓存,资源尽量细粒化等等的占用性能最少的要求。平时不注意的优化,这时候都需要去做了。在游戏上线期这段时间,成长是最明显的。

    对啦,游戏上线期前,还有个很锻炼我的事,就是我的组长离职了….然后,他的任务的半壁江山给了我。不要以为比自己高一个级别,写出来的代码就很6,也充斥着各种乱七八糟,“goushi”一样的代码。其实这也不怪他,因为毕竟是游戏开发,游戏开发的项目需求变更是最为频繁的,他的代码也做的最久,核心功能模块开发。于是,我维护着“历史最悠久”的代码,当然自己也在不断尝试优化这些。其实这对我来说算是好事吧。因为有句话说得好,天将降大任于斯人也。

  总结:

       在大厂中,的确会有空闲期,但是你不能因此就真的空闲,要把这个空闲转为优势,自己主动学习总结,主动总结学习到的东西真的有一大半吧。总的来说,我大概算是工作中学习到一半,自己空闲时间学习到的东西占一半吧。

     2019,告诉自己,继续保持,保持一颗平淡的心,每天保持一点主动学习就好。

        

 

USB之基本协议和数据波形1

Vedic阅读(194)

 

=============  本系列参考  =============

《圈圈教你玩USB》、《Linux那些事儿之我是USB》

协议文档:https://www.usb.org/document-library/usb-20-specification  usb_20_20190524/usb_20.pdf

====================================

前言:

  我们先不一上来讲USB大而全的协议规范文档, 会让人退而却步, 只要有协议, 在数据传输上波形就有规律可循, 翻译成数据, 也先不管USB1.1/2.0等版本, 因为最终的传输单元是一样的

 

一. 最基本传输单位 –包(packet)

1.电气信号:

  a. 采用D+/D-差分信号传输, LSB在前, NRZI编码也就是0反转, 1不反转, 遇到连续6个1强插一个0

 

  b. 低速Lowspeed 1.5Mb/s, 全速Fullspeed 12Mb/s, 高速Highspeed 480Mb/s, USB1.1支持L/F  USB2.0支持L/F/H USB3.0也支持L/F/H 同时支持OTG功能

  c. OTG(on the go) 就是多了根ID线, 用于判断主控器作Host还是device

  d. L/F S采用电压传输(3.3v), HS采用电流传输(等效电阻后示波器显示400mv)

  e. 传输方向以Host为准, 即IN表示device数据到Host, OUT表示Host数据到device

  f. 插入上电波形分析(下面单独抽出来分析)

2. packet格式

      SYNC同步域  +  PID域  +  数据域  +  CRC  +  EOP

  a. 同步域: L/FS 固定00000001, HS前面31个0后一个1

  b. PID占一个字节,高4bit是低4bit的反码, 用于校验PID本身, 而PID[3:0] 表示该packet的类型(协议文档8.3.1):

    

    打*表示USB1.1不支持的, 而USB2.0全支持, 这里要特别注意令牌包, 任何事务传输, 必须先发个令牌包说明意图, 至于后面是否需要数据包还是握手吧取决事务类型(下面会说)

  c. 数据域是可选的, 取决PID是不是数据包(DATA0/1/2/M) 

  d. CRC也是可选的, PID自校验,所以只对数据域校验,  若数据域没有那CRC也没有, 令牌包的数据域采用CRC5校验, 数据包的数据域采用CRC16校验

  e. EOP结束包, 对于L/FS是两个数据位宽的SE0信号(D+/D-都是0),  对于HS使用故意位填充表示(待具体解释)

  f. 空闲状态, 在SYNC同步域前和EOP后 总线上处于空闲状态, L/FS是一根高电平一根低电平(也就是J或K状态, 后面会讲), HS是SE0表示空闲状态

  g. SYNC同步域、EOP、CRC是硬件发射器自动添加和硬件接收器自动解析的, 软件看到的只有PID域和数据域

    

2. packet类型

  根据PID[3:0]可以将包的类型分成4类

  a. 令牌包: 一次USB传输必须首发令牌包, 告知意图, 同时后面数据表示跟哪个设备及端点通信, 这点很重要, 设想一下一个Host接了很多外设, Host发出的信号会到达所有hub和普通外设, 如何避免串扰呢?

        那就是总线某一时刻只有一个外设与Host通信, 外设硬件接口只响应令牌包, 因为令牌包的数据域表示设备的地址和端点地址, 外设可以解析是否和自己匹配, 如果是则响应(使能硬件接收数据), 以及后续的数据包交互, 如果不是

        就不响应, 当然后续的数据包也会被外设硬件屏蔽, 不理会总线信号, 除非一段时间后又检测到令牌包, 再次进行地址匹配, 符合才使能硬件接收总线上的信号

   IN OUT SETUP 包的数据域包含7bit设备地址和4bit端点地址, 所以一个Host能够最多接127个设备(0是外设刚插入时的默认地址, 握手后必须赋值非0, 不然下一个设备也是0就冲突了),  一个设备端点最多只能16个(端点0是必须的, 所以其他最多15个)

      

 

 

   SOF(帧起始包) 相当于心跳包, 让所有外设知道Host还在活动(哪怕Host不是跟该设备通信但起码知道跟其他设备通信), L/FS每隔1ms发一次, 每发一次11bit帧号加1, HS把1ms分割8份即每隔125us发一次, 但这8份里面的11bit帧号是相同的

  

    这个心跳包主要用于休眠唤醒用的, 当Host没有发SOF超过3ms时(一般是Host自己进入休眠或者想外设休眠), 外设设置自己进入低功耗状态(如果支持), 然后进入监听模式如果检测到总线有信号变化(只要跟睡眠前不一样)立即唤醒,

  可能是Host要召唤设备了, 当然设备也可以唤醒Host, Host进入休眠也会设置监听总线状态,外设被人为唤醒改变总线信号接着唤醒Host

 

  b. 数据包: 这没啥好说的就是PID表明自己是数据包(DATA0还是DATA1主要用于 确保对方收到), 后面就是字节数据了, 这里需要注意就是没有告知这个数据包到底多少个数据, 所以我猜想外设接收PID域后, 每接收一个字节counter计数器加1

        直到EOP, 然后减2 CRC16校验值就是数据量, 接着对FIFO数据CRC16和最后两个字节对比, 不一致就产生数据错误中断, 一致就产生数据成功中断并将数据量填充RX counter寄存器

     

  c. 握手包: 告知对方状态, 比如Host发送IN令牌包, 接着设备发送数据包, 然后Host接收完发送ACK握手包告知设备成功接收

    不用数据域!

  d. 特殊包主要用于高速, 比如上面Host发完IN令牌包后, 设备应该要发数据包的, 但设备还没准备好数据, 导致Host等待超时, Host可以再次发IN包让设备进入发送数据, Host切换等待接收数据状态, 

    这里有两个小问题, 一是设备数据未准备好, 却没有有效方式告知Host, 只能啥都不做靠超时告知, 浪费Host时间, 二是IN包让设备进入发送数据模式, 设备有数据早发了还等你吹, 还让外设进入发送模式影响准备数据

    而PING特殊包就是当第一次超时后, Host不发IN包改发PING包询问设备准备好没, 设备若准备好了回复ACK握手包, 接着Host再发IN包, 如果还没准备好就发NAK告知, Host就知道设备还没准备好而不用死等超时,

    其他几个读者可自行查阅

 

   总结: 总线是一个一个packet传输的, 且信号达到所有外设, 当发送SYNC域所有外设接收并调整时钟采样点做好同步, 接着解析PID域,  如果是非令牌包就不理会(只有已被选中的外设才理会), 如果是令牌包就解析后面地址是否和自己匹配,

      不匹配继续不会理, 匹配的使能硬件接收数据功能, 并根据PID是IN OUT SETUP SOF再细分, 如果是OUT,产生OUT中断, 软件应该清空使能FIFO准备接收数据, 如果是IN, 产生IN中断, 软件要填充好即将发的数据然后使能端点发送,

      如果是SETUP包(Host会接着发DATA0数据包数据域包含8个字节的标准请求), 设备要清空特殊FIFO并做好接受下一个数据, 接受完才产生SETUP中断, 软件就解析FIFO里的8byte标准请求, 然后准备数据, 比如是获取设备描述符请求

      那软件得准备好设备描述符缓存并ACK(必须ACK不能NAK)回复, 然后Host会发IN包, 接着设备IN中断将刚才准备好的设备描述符缓存丢到端点0发出去!

      如果是SOF包, 设备会重置时间计数器, 当3ms内没有新的SOF包, 就会产生中断, 设备知道总线现在是空闲状态, 可以自行决定是否休眠

 

 

 二、 事务–四种传输类型

  一个个packet只是一盘散沙, 通过组织起来作为一个有效传输我们称之为事务, 所以一个事务起码包含:

  一个令牌包, 通过地址选中具体外设

  可选的数据包, 如果是IN/OUT/SETUP包那后续有数据包, 如果是 SOF则数据包和握手吧都没有

  可选的握手包, 像视频聊天这种实时传输不需要ACK应该, 丢了就丢了, 省下带宽不如用来发数据

  因此, 根据具体的使用场景, 事务可以分成四种传输类型:

1. 批量传输(Bulk transfers

  一个批量事务包含三个阶段, 令牌包阶段 + 数据包阶段 + 握手包阶段, 其中数据包阶段可以发一个或多个数据包

              

 

  以Beagle USB 480 逻辑分析仪抓U盘上电时序时为例, 期间Host(PC机)会读取U盘数据(bluk传输), 我们可以猜测应该发一个读取U盘根目录命令, 然后读取扇区信息, 如下:    

                 

  一个读取扇区信息命令分别为 Command + Data + Status, Command是一个写操作, 往设备发送数据告知想干嘛, 然后就是读数据, 最后检查状态, 可以看到这些操作都由三个packet构成 IN/OUT令牌包 + 数据包 + 握手包

  因为U盘每次操作只能512byte/block, 所以想读取多个扇区只能分多次IN操作(传输最大字节数端点描述符有说明)

 

2. 中断传输(Interrupt transfers

   一个中断事务跟批量事务类似, 不同在于传输量比较少, 且希望Host每隔一段时间来访问设备(不是靠硬件中断告知系统, 而是端点描述符有个时间间隔变量, 告知Host最好小于这个时间间隔来访问设备), 像鼠标键盘都是这类传输模式, 

   以Beagle USB 480 逻辑分析仪抓键盘为例:

  

  这里可以看出三点, 一是Host每间隔x时间就发起一次读取键盘数据操作(还是老样子 IN包 + DATA0包 + ACK包); 二是如果我没敲键盘, 则设备NAK告知Host没有数据; 三是间隔时间约 72/10  344/44 = 8ms

  查看键盘端点描述符bInterval=1, 根据datasheet代表1ms, 即键盘希望Host每隔1ms读取一次数据, 但采不采纳在于Host端

          

 

 3. 等时传输(Isochronous transfers

  等时事务跟前两种也差不多, 不同在于对时间敏感, 对数据准确性不关心, 所以不需要握手包, 主要用于音频、视频类设备

 

4. 控制传输(Control transfers

   控制传输稍微复杂一点, 上面三个一个传输就是一个事务, 但控制传输有三个状态, 每个状态对应一个事务, 所以需要三次事务

三次过程分别为:

  建立过程:SETUP令牌包 + DATA0数据包(标准请求就在这) + ACK握手包(设备必须返回ACK, 不能NAK 如果设备连这个都不能保证的话就别玩了)

  数据过程: 可选, 如上面是获取设备描述符这里就是 IN令牌包 + DATA1数据包 + ACK握手包; 如果是设置地址请求, 地址在请求内部了, 不需要数据过程 

  状态过程: 上面的数据过程必须是同一个方向的, 如果方向改变, 则就是状态过程, 如果没有数据过程, 则这个数据包就是状态过程不管哪个方向

      

  以Beagle USB 480 逻辑分析仪抓U盘为例:

     

  从捕捉的数据可看到, 建立过程的数据包包含着标准请求 80 06 00 01 00 00 12 00 (小端排序) , 前面的C3是PID, 后面E0 F4 是CRC16, 可以通过http://www.ip33.com/crc.html 验证

 80 06 0100 0000 0012
struct usb_ctrlrequest {
    __u8 bRequestType; //0x80
    __u8 bRequest;    //0x06
    __le16 wValue;     //0x100   
    __le16 wIndex;    //0
    __le16 wLength;  //0x12
} __attribute__ ((packed));
具体请参考协议文档9-4

 

  上面log还有个有趣的现象: 状态过程发送1字节0x00数据包,  U盘竟然返回NAK, 不知为何,  由于是高速模式下, 所以Host接下来会发PING包探测U盘是否ready, 直到U盘回复ACK才再次发送OUT包,如果是L/FS则继续发OUT包直到接收ACK

 

剩余数据的解析将在下一篇博文讲解!

 

 

 

 

 

 

 

 

 

B树概述与简单应用示例(C#)

漂亮的猫阅读(102)

引言:

  天不生仲尼,万古如长夜。在计算机科学中,也有一个划时代的发明,B树(多路平衡查找树)及其变体(B树,b*树,b+树);

由德国科学家(鲁道夫·拜尔 Rudolf Bayer),美国科学家(爱德华·M·麦克特 Edward Meyers McCreight)于1970年共同发明;

B树这种数据结构特别适合用于数据库与文件系统设计中,是人类精神财富的精华部分,B树不诞生,计算机在处理大数据量计算时会变得非常困难。

 

用途:

  基本上都是软件产品最底层的,最核心的功能。

如:各种操作系统(windows,Linux,Mac)的文件系统索引,各种数据库(sqlserver、oracle、mysql、MongoDB、等等),

基本上大部分与大数据量读取有关的事务,多少都与B树家族有关,因为B树的优点太明显,特别是读取磁盘数据效率非常的高效,

查找效率O(log n),甚至在B+树中查询速度恒定,无论多少存储多少数据,查询任何一个速度都一样。简直就是天才的发明。

 

诞生的原因:

  在上世纪时期,计算机内存储器都非常的小,以KB为单位,比起现在动不动以G计算,简直小的可怜。

计算机运算数据时,数据是在内存中进行操作的,比如一些加减乘除、正删改查等。

举个简单的栗子:从一个数组 int a[1,2,3,4,5,6,7,8,9]中找出3,那非常简单;大概步骤如下:

  1、在内存中初始化这个数组

  2、获取数组指针遍历这个数组,查到3就完成

  但是这个数组很大,比如包含1亿个数字怎么办?如果数组容量大大超过内存大小,那这种比较就不现实了。现在的做法都是把文件

数据存放在外存储器,比如磁盘,U盘,光盘;然后把文件分多次的拷贝数据至内存进行操作。但是读取外存储器效率对比读取内存,

差距是非常大的,一般是百万级别的差距,差6个数量级,所以这个问题不解决一切都是空谈。

  好在操作系统在设计之初,就对读取外存储器进行了一定的优化,引入了“逻辑块”概念,当做操作文件的最小单元,而B树合理地利用这个“逻辑块”

功能开发的高效存储数据结构;在介绍B树特性之前,先来了解一下磁盘的基本工作原理。

 

磁盘简单介绍:

1)磁盘结构介绍 

 

  网上引用的两张图,将就看看,基本结构是:磁盘 > 盘面 > 磁道 > 扇区

  左边是物理图,这个大家应该都是经常见到了,一般圆形的那部分有很多层,每一层叫盘片;右边的是示意图,代表左图的一个盘面。

每个盘面有跟多环形的磁道,每个磁道有若干段扇区组成,扇区是磁盘的最小组成单元,若干段扇区组成簇(也叫磁盘块、逻辑块等)

先看看我电脑的磁盘簇与扇区大小

  可以看到我的E盘每个扇区512个字节,每个簇4096字节,这个先记下来,后边有用到

扇区是磁盘组成的最小单元,簇是虚拟出来的,主要是为了操作系统方便读写磁盘;由于扇区比较小,数量非常多,

在寻址比较麻烦,操作系统就将相邻的几个扇区组合在一起,形成簇,再以簇为每次作文件的最小单元。比如加载一个磁盘文件内容,

操作系统是分批次读取,每次只拷贝一个簇的单位数据,我的电脑就是一次拷贝4096字节,知道文件全部拷贝完成。

2)读写速度

  磁盘读取时间是毫秒级别的一般几毫秒到十几毫秒之间,这个跟磁盘转速有点关系,还有就是数据所在磁道远近有关系;

CPU处理时间是纳秒级别,毫秒:纳秒 = 1:1000000,所以在程序设计中,读取文件是时间成本非常高的,应该尽量合理设计;

 

B树简介(维基百科):

  B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,

都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree)一个节点可以拥有最少2个子节点。

与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。B树减少定位记录时所经历的中间过程,从而加快存取速度。

B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

  一个 m 阶的B树是一个有以下特性:

  • 每一个节点最多有 m 个子节点
  • 每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
  • 如果根节点不是叶子节点,那么它至少有两个子节点
  • 有 k 个子节点的非叶子节点拥有 k − 1 个键
  • 所有的叶子节点都在同一层

 

  好吧,上边这一段看了等于没看的定义可以不看,这里有个重要的B树特性需要了解,就是B树的阶,对于阶的定义国内外是有分歧的,有的定义为度

阶指的是节点的最大孩子数,度指的是节点的最小孩子数,我查阅了很多资料,基本上可以理解为: 

1度 = 2阶,比如说3度B树,可以理解为6阶B树。这点有些疑问,有更好的说法的可以留言讨论一下。

 

1)内部节点:

  内部节点是除叶子节点和根节点之外的所有节点。每个内部节点拥有最多 U 个,最少 L 个子节点。元素的数量总是比子节点指针的数量少1。

U 必须等于 2L 或者 2L-1。这个L一般是度数。

2)根节点:根节点拥有的子节点数量的上限和内部节点相同,但是没有下限。

3)叶子节点:叶子节点对元素的数量有相同的限制,但是没有子节点,也没有指向子节点的指针。

4)为了分析方便举例3阶3层B树

 

                        图1 

从上图中可以得出以下几个信息:

  • 红色数字标示整个节点(即3、6在同一个节点内,图中总共9个节点),黑色数字表示每个节点内的键值。
  • 所有数据插入B树后,都是从左到右顺序排列,从根节点开始,节点左边孩子键值都小于节点键值,右边孩子键值都大于节点键值。
  • 树的阶数指的是每个节点的最大孩子节点数,图中最多孩子节点数为3,即阶数=3,键值数量最少为:1,最大为:阶数 -1

 

数据检索分析:

  依据上图分析,因为整棵树已经在内存中,相当于一个变量,数据检索首先是从根节点开始;

1)如果要查询9,首先从根节点比较,那比较一次就得到结果,

2)如果要查询第二层的3、4,首先判断根节点键值,没有匹配到,但是可以判断要检索的键值比根节点小,

   所以接下来是从左孩子树继续检索,12、15也是类似,总共需要2次比较就得到结果

3)如果查询叶子节点键值,类似2),只需要3次比较就能得到结果。

4)对比普通的数组遍历查询,B树检索的时间成本没有随数据量增加而线性增加,效率大大提高。

 

B树的应用分析:

  前面已经提到,如果树已经在内存中,那当然好办,直接遍历就好了。如果B树仅仅如此,那也和数组差别不大,同样受限于内存大小;

所以,在内存中创建整棵B树是不现实的,这不是B树的正确打开方式。

  前面也已经提到,操作系统加载磁盘文件的时候,如果文件超过大小(即4096个字节),那会分多次的读取磁盘,直到拷贝数据完成。

这里看似一个加载动作,其实这个动作包含了N次磁盘寻址,而我们已经知道,每次磁盘寻址直至拷贝数据开销是非常大的;是CPU指令耗时百万倍以上;

这种操作应该尽量少地执行,而B树这种数据结构就是为了解决磁盘读取瓶颈这个问题而产生的。

  实际应用中,B树会持久化到磁盘,然后只在内存保留一个根节点的指针。已上图1为例:

  每个节点大小刚好等于大小,这样只需一次磁盘IO就可以获取到一整个节点的所有键值,及其所有子树的指针。

比如,查询键值8:

  1)第一步,读取根节点得到键值9,以及2个子树指针,分别指向左右孩子节点,因为9 > 8,所以下一步加载左孩子节点

  2)第二部,加载节点2,得到键值3、6,以及3个子树指针,因为3、6 < 8,所以下一步要加载节点2的右孩子节点

  3)第三部,加载节点6,得到键值7、8,因为是叶子节点所以没有子树指针,遍历键值匹配到8,返回。

 

总结:

  在这个3阶3层的B树中,无论查找哪一个键值,最多只需要3次磁盘操作,就算平均每次耗时10毫秒,总共需要耗时30毫秒(CPU运算耗时可以忽略);

以此类推,3阶4层的B树,需要读取4次磁盘,耗时40毫秒,5层50毫秒,6层60毫秒,7层,8层,,,,

  这样一看貌似也没什么,几十毫秒已经不能说快了,但是别忘了我们这颗树只有3阶,即一个节点保存2个键值。一个簇最多能有4096/4=1024个键值;

如果创建一个1024阶的B树,分别控制在3、4、5层的话,根据B树高度公式:,H为层数,T为1024,n为数据总数

耗时如下:

  3阶3层:能容纳2147483648(20亿)个键值,检索耗时也将30毫秒内

  3阶4层:能容纳2147483648(20亿) ~ 2199023255552(2兆亿)个键值,检索耗时也将40毫秒内,当然这已经超出键值表达范围了

  3阶5层:不可思议。。。

 

  当然实际运用当中达不到1024阶,因为树持久化到磁盘时,索引结构体一般都是超过4个字节,比如12个字节,那一个簇最多能有4096/12=341个键值。

如果阶数按341来算:

  3阶3层:能容纳79303642(7千万)个键值,检索耗时也将30毫秒内

  3阶4层:能容纳79303642(7千万) ~ 27042541922(200亿)个键值,检索耗时也将40毫秒内

  也是非常多了。。

 

B树简单示例:

1)首先,我们把B树基本信息定义出来

1 public class Consts
2 {
3     public const int M = 3;                  // B树的最小度数
4     public const int KeyMax = 2 * M - 1;     // 节点包含关键字的最大个数
5     public const int KeyMin = M - 1;         // 非根节点包含关键字的最小个数
6     public const int ChildMax = KeyMax + 1;  // 孩子节点的最大个数
7     public const int ChildMin = KeyMin + 1;  // 孩子节点的最小个数
8 }

先写个简单的demo,因为最小度数为3,那就是6阶。先实现几个简单的方法,新增,拆分,其余的合并,删除比较复杂以后有机会再看看

2)定义BTreeNode,B树节点


  1     public class BTreeNode
  2     {
  3         private bool leaf;
  4         public int[] keys;
  5         public int keyNumber;
  6         public BTreeNode[] children;
  7         public int blockIndex;
  8         public int dataIndex;
  9 
 10         public BTreeNode(bool leaf)
 11         {
 12             this.leaf = leaf;
 13             keys = new int[Consts.KeyMax];
 14             children = new BTreeNode[Consts.ChildMax];
 15         }
 16 
 17         /// <summary>在未满的节点中插入键值</summary>
 18         /// <param name="key">键值</param>
 19         public void InsertNonFull(int key)
 20         {
 21             var index = keyNumber - 1;
 22 
 23             if (leaf == true)
 24             {
 25                 // 找到合适位置,并且移动节点键值腾出位置
 26                 while (index >= 0 && keys[index] > key)
 27                 {
 28                     keys[index + 1] = keys[index];
 29                     index--;
 30                 }
 31 
 32                 // 在index后边新增键值
 33                 keys[index + 1] = key;
 34                 keyNumber = keyNumber + 1;
 35             }
 36             else
 37             {
 38                 // 找到合适的子孩子索引
 39                 while (index >= 0 && keys[index] > key) index--;
 40 
 41                 // 如果孩子节点已满
 42                 if (children[index + 1].keyNumber == Consts.KeyMax)
 43                 {
 44                     // 分裂该孩子节点
 45                     SplitChild(index + 1, children[index + 1]);
 46 
 47                     // 分裂后中间节点上跳父节点
 48                     // 孩子节点已经分裂成2个节点,找到合适的一个
 49                     if (keys[index + 1] < key) index++;
 50                 }
 51 
 52                 // 插入键值
 53                 children[index + 1].InsertNonFull(key);
 54             }
 55         }
 56 
 57         /// <summary>分裂节点</summary>
 58         /// <param name="childIndex">孩子节点索引</param>
 59         /// <param name="waitSplitNode">待分裂节点</param>
 60         public void SplitChild(int childIndex, BTreeNode waitSplitNode)
 61         {
 62             var newNode = new BTreeNode(waitSplitNode.leaf);
 63             newNode.keyNumber = Consts.KeyMin;
 64 
 65             // 把待分裂的节点中的一般节点搬到新节点
 66             for (var j = 0; j < Consts.KeyMin; j++)
 67             {
 68                 newNode.keys[j] = waitSplitNode.keys[j + Consts.ChildMin];
 69 
 70                 // 清0
 71                 waitSplitNode.keys[j + Consts.ChildMin] = 0;
 72             }
 73 
 74             // 如果待分裂节点不是也只节点
 75             if (waitSplitNode.leaf == false)
 76             {
 77                 for (var j = 0; j < Consts.ChildMin; j++)
 78                 {
 79                     // 把孩子节点也搬过去
 80                     newNode.children[j] = waitSplitNode.children[j + Consts.ChildMin];
 81 
 82                     // 清0
 83                     waitSplitNode.children[j + Consts.ChildMin] = null;
 84                 }
 85             }
 86 
 87             waitSplitNode.keyNumber = Consts.KeyMin;
 88 
 89             // 拷贝一般键值到新节点
 90             for (var j = keyNumber; j >= childIndex + 1; j--)
 91                 children[j + 1] = children[j];
 92 
 93             children[childIndex + 1] = newNode;
 94             for (var j = keyNumber - 1; j >= childIndex; j--)
 95                 keys[j + 1] = keys[j];
 96 
 97             // 把中间键值上跳至父节点
 98             keys[childIndex] = waitSplitNode.keys[Consts.KeyMin];
 99 
100             // 清0
101             waitSplitNode.keys[Consts.KeyMin] = 0;
102 
103             // 根节点键值数自加
104             keyNumber = keyNumber + 1;
105         }
106 
107         /// <summary>根据节点索引顺序打印节点键值</summary>
108         public void PrintByIndex()
109         {
110             int index;
111             for (index = 0; index < keyNumber; index++)
112             {
113                 // 如果不是叶子节点, 先打印叶子子节点. 
114                 if (leaf == false) children[index].PrintByIndex();
115 
116                 Console.Write("{0} ", keys[index]);
117             }
118 
119             // 打印孩子节点
120             if (leaf == false) children[index].PrintByIndex();
121         }
122 
123         /// <summary>查找某键值是否已经存在树中</summary>
124         /// <param name="key">键值</param>
125         /// <returns></returns>
126         public BTreeNode Find(int key)
127         {
128             int index = 0;
129             while (index < keyNumber && key > keys[index]) index++;
130 
131             // 该key已经存在, 返回该索引位置节点
132             if (keys[index] == key) return this;
133 
134             // key 不存在,并且节点是叶子节点
135             if (leaf == true) return null;
136 
137             // 递归在孩子节点中查找
138             return children[index].Find(key);
139         }
140     }

View Code

3)B树模型


 1     public class BTree
 2     {
 3         public BTreeNode Root { get; private set; }
 4 
 5         public BTree() { }
 6 
 7         /// <summary>根据节点索引顺序打印节点键值</summary>
 8         public void PrintByIndex()
 9         {
10             if (Root == null)
11             {
12                 Console.WriteLine("空树");
13                 return;
14             }
15 
16             Root.PrintByIndex();
17         }
18 
19         /// <summary>查找某键值是否已经存在树中</summary>
20         /// <param name="key">键值</param>
21         /// <returns></returns>
22         public BTreeNode Find(int key)
23         {
24             if (Root == null) return null;
25 
26             return Root.Find(key);
27         }
28 
29         /// <summary>新增B树节点键值</summary>
30         /// <param name="key">键值</param>
31         public void Insert(int key)
32         {
33             if (Root == null)
34             {
35                 Root = new BTreeNode(true);
36                 Root.keys[0] = key; 
37                 Root.keyNumber = 1;  
38                 return;
39             }
40 
41             if (Root.keyNumber == Consts.KeyMax)
42             {
43                 var newNode = new BTreeNode(false);
44 
45                 newNode.children[0] = Root; 
46                 newNode.SplitChild(0, Root);
47 
48                 var index = 0;
49                 if (newNode.keys[0] < key) index++;
50 
51                 newNode.children[index].InsertNonFull(key);
52                 Root = newNode;
53             }
54             else
55             {
56                 Root.InsertNonFull(key);
57             }
58         }
59     }

View Code

4)新增20个无序键值,测试一下

 1             var bTree = new BTree();
 2 
 3             bTree.Insert(4);
 4             bTree.Insert(5);
 5             bTree.Insert(6);
 6             bTree.Insert(1);
 7             bTree.Insert(2);
 8             bTree.Insert(3);
 9             bTree.Insert(10);
10             bTree.Insert(11);
11             bTree.Insert(12);
12             bTree.Insert(7);
13             bTree.Insert(8);
14             bTree.Insert(9);
15             bTree.Insert(13);
16             bTree.Insert(14);
17             bTree.Insert(18);
18             bTree.Insert(19);
19             bTree.Insert(20);
20             bTree.Insert(15);
21             bTree.Insert(16);
22             bTree.Insert(17);
23 
24             Console.WriteLine("输出排序后键值");
25             bTree.PrintByIndex();

5)运行

 

B树持久化: 

  上文提到,B数不可能只存在内存而无法落地,那样没有意义。所以就需要将整棵树持久化到磁盘文件,并且还要支持快速地从磁盘文件中检索到键值;

要持久化就要考虑很多问题,像上边的简单示例是没有实际意义的,因为节点不可能只有键值与孩子树,还得有数据指针,存储位置等等,大概有以下一些问题:

  • 如何保存每个节点占有字节数刚好等于一个簇大小(4096字节),因为这样就符合一次IO操作的数据交换上限?
  • 如何保存每个节点的所有键值,以及这个节点下属所有子树关系?
  • 如何保存每个键值对应的数据指针地址,以及指针与键值的对应关系如何维持?
  • 如何保证内存与磁盘的数据交换中能够正确地还原树结构,即重建树的某部分层级与键值和子树的关系?
  • 等等。。

  问题比较多,非常麻烦。具体的过程就不列举了,以下展示以下修改后的B树模型。

 

1、先定义一个结构体

 1 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
 2 public struct BlockItem
 3 {
 4     public int ChildBlockIndex;
 5     public int Key;
 6     public int DataIndex;
 7 
 8     public BlockItem(int key, int dataIndex)
 9     {
10         ChildBlockIndex = -1;
11         Key = key;
12         DataIndex = dataIndex;
13     }
14 }

 

  结构体总共12字节,为了能够持久化整棵B树到磁盘,加入了ChildBlockIndex子孩子节点块索引,根据这个块索引在下一次重建子孩子树层级关系时就知道从

文件的那个位置开始读取;Key键值,DataIndex数据索引,数据索引也是一个文件位置记录,跟ChildBlockIndex差不多,这样检索到key后就知道从

文件哪个位置获取真正的数据。为了更形象了解B树应用,我画了一个结构体的示意图:

0、总共3个节点,每个节点由N个结构体组成,最末尾只有孩子指针,没有数据与键值

1、黄色为子树块索引,即ChildBlockIndex,指向这个子孩子树所有数据在文件中的位置

2、红色为键值,即Key,键值一般是唯一的,不允许重复

3、蓝色为数据块索引,即DataIndex,指向键值对应的数据在文件中的什么位置开始,然后读取一个结构体的长度即可

4、底下绿色的一块是数据指针指向的具体数据块

2、数据结构体

 1 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
 2 public struct SDataTest
 3 {
 4     public int Idx;
 5     public int Age;
 6     public byte Sex;
 7 
 8     [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
 9     public byte[] Name;
10 
11     public byte Valid;
12 };

 

3、B树节点类修改改一下,这个就不解释了,复习一下程序员基本功,啃代码。


  1     public class BTreeNode
  2     {
  3         private BTree tree;
  4         private bool leaf;
  5 
  6         public int keyNumber;
  7         public BlockItem[] keys;
  8         public BTreeNode[] children;
  9 
 10         public int blockIndex;
 11         public int findIndex;
 12 
 13         public BTreeNode(BTree tree, bool leaf)
 14         {
 15             this.tree = tree;
 16             this.leaf = leaf;
 17             keys = new BlockItem[Consts.KeyMax];
 18             children = new BTreeNode[Consts.ChildMax];
 19             blockIndex = Consts.BlockIndex++;
 20         }
 21 
 22         /// <summary>在未满的节点中插入键值</summary>
 23         /// <param name="key">键值</param>
 24         public void InsertNonFull(BlockItem item)
 25         {
 26             var index = keyNumber - 1;
 27 
 28             if (leaf == true)
 29             {
 30                 // 找到合适位置,并且移动节点键值腾出位置
 31                 while (index >= 0 && keys[index].Key > item.Key)
 32                 {
 33                     keys[index + 1] = keys[index];
 34                     index--;
 35                 }
 36 
 37                 // 在index后边新增键值
 38                 keys[index + 1] = item;
 39                 keyNumber = keyNumber + 1;
 40             }
 41             else
 42             {
 43                 // 找到合适的子孩子索引
 44                 while (index >= 0 && keys[index].Key > item.Key) index--;
 45 
 46                 // 如果孩子节点已满
 47                 if (children[index + 1].keyNumber == Consts.KeyMax)
 48                 {
 49                     // 分裂该孩子节点
 50                     SplitChild(index + 1, children[index + 1]);
 51 
 52                     // 分裂后中间节点上跳父节点
 53                     // 孩子节点已经分裂成2个节点,找到合适的一个
 54                     if (keys[index + 1].Key < item.Key) index++;
 55                 }
 56 
 57                 // 插入键值
 58                 children[index + 1].InsertNonFull(item);
 59             }
 60         }
 61 
 62         /// <summary>分裂节点</summary>
 63         /// <param name="childIndex">孩子节点索引</param>
 64         /// <param name="waitSplitNode">待分裂节点</param>
 65         public void SplitChild(int childIndex, BTreeNode waitSplitNode)
 66         {
 67             var newNode = new BTreeNode(tree, waitSplitNode.leaf);
 68             newNode.keyNumber = Consts.KeyMin;
 69 
 70             // 把待分裂的节点中的一般节点搬到新节点
 71             for (var j = 0; j < Consts.KeyMin; j++)
 72             {
 73                 newNode.keys[j] = waitSplitNode.keys[j + Consts.ChildMin];
 74 
 75                 // 清0
 76                 waitSplitNode.keys[j + Consts.ChildMin] = default(BlockItem);
 77             }
 78 
 79             // 如果待分裂节点不是也只节点
 80             if (waitSplitNode.leaf == false)
 81             {
 82                 for (var j = 0; j < Consts.ChildMin; j++)
 83                 {
 84                     // 把孩子节点也搬过去
 85                     newNode.children[j] = waitSplitNode.children[j + Consts.ChildMin];
 86 
 87                     // 清0
 88                     waitSplitNode.children[j + Consts.ChildMin] = null;
 89                 }
 90             }
 91 
 92             waitSplitNode.keyNumber = Consts.KeyMin;
 93 
 94             for (var j = keyNumber; j >= childIndex + 1; j--)
 95                 children[j + 1] = children[j];
 96 
 97             children[childIndex + 1] = newNode;
 98 
 99             for (var j = keyNumber - 1; j >= childIndex; j--)
100                 keys[j + 1] = keys[j];
101 
102             // 把中间键值上跳至父节点
103             keys[childIndex] = waitSplitNode.keys[Consts.KeyMin];
104 
105             // 清0
106             waitSplitNode.keys[Consts.KeyMin] = default(BlockItem);
107 
108             // 根节点键值数自加
109             keyNumber = keyNumber + 1;
110         }
111 
112         /// <summary>根据节点索引顺序打印节点键值</summary>
113         public void PrintByIndex()
114         {
115             int index;
116             for (index = 0; index < keyNumber; index++)
117             {
118                 // 如果不是叶子节点, 先打印叶子子节点. 
119                 if (leaf == false) children[index].PrintByIndex();
120 
121                 Console.Write("{0} ", keys[index].Key);
122             }
123 
124             // 打印孩子节点
125             if (leaf == false) children[index].PrintByIndex();
126         }
127 
128         /// <summary>查找某键值是否已经存在树中</summary>
129         /// <param name="item">键值</param>
130         /// <returns></returns>
131         public BTreeNode Find(BlockItem item)
132         {
133             findIndex = 0;
134             int index = 0;
135             while (index < keyNumber && item.Key > keys[index].Key) index++;
136 
137             // 遍历全部都未找到,索引计数减1
138             if (index > 0 && index == keyNumber) index--;
139 
140             // 该key已经存在, 返回该索引位置节点
141             if (keys[index].Key == item.Key)
142             {
143                 findIndex = index;
144                 return this;
145             }
146 
147             // key 不存在,并且节点是叶子节点
148             if (leaf == true) return null;
149 
150             // 重建children[index]数据结构
151             var childBlockIndex = keys[index].ChildBlockIndex;
152             tree.LoadNodeByBlock(ref children[index], childBlockIndex);
153 
154             // 递归在孩子节点中查找
155             if (children[index] == null) return null;
156             return children[index].Find(item);
157         }
158     }

View Code

 

4、B树模型也要修改一下 ,不解释


  1     public class BTree
  2     {
  3         private FileStream rwFS;
  4 
  5         public BTreeNode Root;
  6 
  7         public BTree(string fullName)
  8         {
  9             rwFS = new FileStream(fullName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
 10 
 11             // 创建10M的空间,用做索引存储
 12             if (rwFS.Length == 0)
 13             {
 14                 rwFS.SetLength(Consts.IndexTotalSize);
 15             }
 16 
 17             // 从数据文件重建根节点,内存只保存根节点
 18             LoadNodeByBlock(ref Root, 0);
 19         }
 20 
 21         public void LoadNodeByBlock(ref BTreeNode node, int blockIndex)
 22         {
 23             var items = Helper.Read(rwFS,blockIndex);
 24             if (items.Count > 0)
 25             {
 26                 var isLeaf = items[0].ChildBlockIndex == Consts.NoChild;
 27 
 28                 node = new BTreeNode(this, isLeaf);
 29                 node.blockIndex = blockIndex;
 30                 node.keys = items.ToArray();
 31                 node.keyNumber = items.Count;
 32             }
 33         }
 34 
 35         /// <summary>根据节点索引顺序打印节点键值</summary>
 36         public void PrintByIndex()
 37         {
 38             if (Root == null)
 39             {
 40                 Console.WriteLine("空树");
 41                 return;
 42             }
 43 
 44             Root.PrintByIndex();
 45         }
 46 
 47         /// <summary>查找某键值是否已经存在树中</summary>
 48         /// <param name="item">键值</param>
 49         /// <returns></returns>
 50         public BTreeNode Find(BlockItem item)
 51         {
 52             if (Root == null) return null;
 53 
 54             return Root.Find(item);
 55         }
 56         public BTreeNode Find(int key)
 57         {
 58             return Find(new BlockItem() { Key = key });
 59         }
 60 
 61         /// <summary>新增B树节点键值</summary>
 62         /// <param name="item">键值</param>
 63         private void Insert(BlockItem item)
 64         {
 65             if (Root == null)
 66             {
 67                 Root = new BTreeNode(this, true);
 68                 Root.keys[0] = item;  
 69                 Root.keyNumber = 1;  
 70             }
 71             else
 72             {
 73                 if (Root.keyNumber == Consts.KeyMax)
 74                 {
 75                     var newNode = new BTreeNode(this, false);
 76 
 77                     newNode.children[0] = Root;
 78                     newNode.SplitChild(0, Root);
 79 
 80                     var index = 0;
 81                     if (newNode.keys[0].Key < item.Key) index++;
 82 
 83                     newNode.children[index].InsertNonFull(item);
 84                     Root = newNode;
 85                 }
 86                 else
 87                 {
 88                     Root.InsertNonFull(item);
 89                 }
 90             }
 91         }
 92 
 93         public void Insert(SDataTest data)
 94         {
 95             var item = new BlockItem()
 96             {
 97                 Key = data.Idx
 98             };
 99 
100             var node = Find(item);
101             if (node != null)
102             {
103                 Console.WriteLine("键值已经存在,info:{0}", item.Key);
104                 return;
105             }
106 
107             // 保存数据
108             item.DataIndex = Helper.InsertData(rwFS, data);
109 
110             // 保存索引
111             if (item.DataIndex >= 0)
112                 Insert(item);
113         }
114 
115         /// <summary>持久化整棵树</summary>
116         public void SaveIndexAll()
117         {
118             SaveIndex(Root);
119         }
120 
121         /// <summary>持久化某节点以下的树枝</summary>
122         /// <param name="node">某节点</param>
123         public void SaveIndex(BTreeNode node)
124         {
125             var bw = new BinaryWriter(rwFS);
126             var keyItem = default(BlockItem);
127 
128             // 第一层
129             var nodeL1 = node;
130             if (nodeL1 == null) return;
131 
132             for (var i = 0; i <= nodeL1.keyNumber; i++)
133             {
134                 keyItem = default(BlockItem);
135                 if (i < nodeL1.keyNumber) keyItem = nodeL1.keys[i];
136 
137                 SaveIndex(bw, 0, i, nodeL1.children[i], keyItem);
138 
139                 // 第二层
140                 var nodeL2 = nodeL1.children[i];
141                 if (nodeL2 == null) continue;
142 
143                 for (var j = 0; j <= nodeL2.keyNumber; j++)
144                 {
145                     keyItem = default(BlockItem);
146                     if (j < nodeL2.keyNumber) keyItem = nodeL2.keys[j];
147 
148                     SaveIndex(bw, nodeL2.blockIndex, j, nodeL2.children[j], keyItem);
149 
150                     // 第三层
151                     var nodeL3 = nodeL2.children[j];
152                     if (nodeL3 == null) continue;
153 
154                     for (var k = 0; k <= nodeL3.keyNumber; k++)
155                     {
156                         keyItem = default(BlockItem);
157                         if (k < nodeL3.keyNumber) keyItem = nodeL3.keys[k];
158 
159                         SaveIndex(bw, nodeL3.blockIndex, k, nodeL3.children[k], keyItem);
160 
161                         // 第四层
162                         var nodeL4 = nodeL3.children[k];
163                         if (nodeL4 == null) continue;
164 
165                         for (var l = 0; l <= nodeL4.keyNumber; l++)
166                         {
167                             keyItem = default(BlockItem);
168                             if (l < nodeL4.keyNumber) keyItem = nodeL4.keys[l];
169 
170                             SaveIndex(bw, nodeL4.blockIndex, l, nodeL4.children[l], keyItem);
171 
172                             // 第五层
173                             var nodeL5 = nodeL4.children[l];
174                             if (nodeL5 == null) continue;
175 
176                             for (var z = 0; z <= nodeL5.keyNumber; z++)
177                             {
178                                 keyItem = default(BlockItem);
179                                 if (z < nodeL5.keyNumber) keyItem = nodeL5.keys[z];
180 
181                                 SaveIndex(bw, nodeL5.blockIndex, z, nodeL5.children[z], keyItem);
182                             }
183                         }
184                     }
185                 }
186             }
187         }
188         private void SaveIndex(BinaryWriter bw, int blockIndex, int num, BTreeNode node, BlockItem item)
189         {
190             bw.Seek((blockIndex * Consts.BlockSize) + (num * Consts.IndexSize), SeekOrigin.Begin);
191             bw.Write(node == null ? Consts.NoChild : node.blockIndex);
192             bw.Write(item.Key);
193             bw.Write(item.DataIndex);
194             bw.Flush();
195         }
196 
197         public SDataTest LoadData(int dataIndex)
198         {
199             return Helper.Load(rwFS, dataIndex);
200         }
201     }

View Code

 

5、写测试

 1 private static void InsertTest(ref BTree bTree)
 2 {
 3     // 新增测试数据
 4     for (int i = 1; i <= Consts.TotalKeyNumber; i++)
 5     {
 6         bTree.Insert(new SDataTest()
 7         {
 8             Idx = i,
 9             Age = i,
10             Sex = 1,
11             Name = Helper.Copy("Name(" + i.ToString() + ")", 20),
12             Valid = 1
13         });
14     }
15 
16     Console.WriteLine("测试数据添加完毕,共新增{0}条数据", Consts.TotalKeyNumber);
17 }

 

6、读测试

 1 private static void FindTest(ref BTree bTree)
 2 {
 3     var count = 0;
 4 
 5     // 校验数据查找
 6     for (int i = 1; i <= Consts.TotalKeyNumber; i++)
 7     {
 8         var node = bTree.Find(i);
 9         if (node == null)
10         {
11             //Console.WriteLine("未找到{0}", i);
12             continue;
13         }
14 
15         //Console.WriteLine("findIndex:{0},key:{1},dataIndex:{2}", node.findIndex, node.keys[node.findIndex].Key, node.keys[node.findIndex].DataIndex);
16 
17         count++;
18         if (count % 10000 == 0)
19         {
20             var data = bTree.LoadData(node.keys[node.findIndex].DataIndex);
21             var name = Encoding.Default.GetString(data.Name).TrimEnd('\0');
22             Console.WriteLine("Idx:{0},Age:{1},Sex:{2},Name:{3},Valid:{4}", data.Idx, data.Age, data.Sex, name, data.Valid);
23         }
24     }
25 
26     Console.WriteLine("有效数据个数:{0}", count);
27 }

 

7、最后测试一下

 

 8、测试查询时间

 1 private static void CheckLoadTime(ref BTree bTree, int key)
 2 {
 3     var start = DateTime.Now;
 4     var node = bTree.Find(key);
 5     if (node == null) return;
 6 
 7     Console.WriteLine("查找{0},耗时:{1}", key.ToString(), (DateTime.Now - start).TotalMilliseconds.ToString());
 8 
 9     var data = bTree.LoadData(node.keys[node.findIndex].DataIndex);
10     var name = Encoding.Default.GetString(data.Name).TrimEnd('\0');
11     Console.WriteLine("Idx:{0},Age:{1},Sex:{2},Name:{3},Valid:{4}", data.Idx, data.Age, data.Sex, name, data.Valid);
12     Console.WriteLine();
13 }
1      CheckLoadTime(ref bTree, 1000);
2      CheckLoadTime(ref bTree, 10000);
3      CheckLoadTime(ref bTree, 50000);
4      CheckLoadTime(ref bTree, 100000);

 

 9、重新生成10000000条数据,测试查询效率

1      CheckLoadTime(ref bTree, 100000);
2      CheckLoadTime(ref bTree, 1000000);
3      CheckLoadTime(ref bTree, 3000000);
4      CheckLoadTime(ref bTree, 5000000);
5      CheckLoadTime(ref bTree, 8000000);
6      CheckLoadTime(ref bTree, 10000000);

 

全是1毫秒内返回,数据检索效率非常高,

 

学习历程:

  实际上最初在学校潦草学了一遍【数据结构】之后,工作那么多年都用不着这方面的知识点,早就忘得一干二净了。

重新引起我兴趣的是2017年下半年,当时一个项目需要用到共享内存作为快速读写数据的底层核心功能。在设计共享内存存储关系时,

就遇到了索引的快速检索要求,第一次是顺序检索,当数据量达到5万以上时系统就崩了,检索速度太慢;后来改为二分查找法,轻松达到20万数据;

达到20万后就差不多到了单机处理性能瓶颈了,因为CPU不够用,除了检索还需要做其他的业务计算;

  那时候就一直在搜索快速查找的各种算法,什么快速排序算法、堆排序算法、归并排序、二分查找算法、DFS(深度优先搜索)、BFS(广度优先搜索),

基本上都了解了一遍,但是看得头疼,没去实践。最后看到树结构,引起我很大兴趣,就是园友nullzx的这篇:B+树在磁盘存储中的应用

这让我了解到原来数据库是这样读写的,这很有意思,得造个轮子自己试一次。最后性能优化的事丢一边,学习B树去了,哈(掩面笑.jpg)。

      学习毫无疑问是枯燥反人性的,我基本就是看一下,玩一下,一直断断续续的;上下班地铁上

刷一下B树的介绍与运用。周末窝在宿舍,看着电脑里的算法导论就想睡觉;周末去旁边的铁仔山,山崖地下坐着无事,又看了看磁盘的构造与工作原理。在铁仔山顶,远眺着远处的珠江入海口,时不时地想着B树的持久化方法;

        上班,生活,忙碌又有点平淡。整天做码农也不好,总得找点有趣的事情做做是吧。

  粗陋仓促写成,恐怕有很多地方有漏洞,所以如果文中有错误的地方,欢迎留言讨论,但是拒绝一波流的吐槽,我可是会删低级评论的。

 

神秘常量0x077CB531,德布莱英序列的恩赐

游戏程序员刘宇阅读(96)

转载请注明来源https://www.cnblogs.com/xiaohutu/p/10950011.html

某天我在优化游戏的算法,在将一个个关键数据结构优化全部成位操作后,最终来到最后一座大山前,如何快速计算出这个数值的二进制表示中最后一位的1在哪一位?

首先,我们已知:

将二进制只保留最后一位1的算法:

v & -v 的原理
已知IEEE对有符号整数中负数的定义是所有数值位取反+1,首位填1,首位这样正负数加起来既可以为0。
例如:一个8位的整数 
A = 0001 1000, 取反 0110 0111, 取反加1 0110 1000,首位填1得到  -A = 1110 1000
A + -A 正好加到最高一位进位后为 0000 0000

因为取反的时候加1,所以A最后一个为1的位取反后为0,下面我们称为第N位
取反后的第N位为0,后面全为1,再加1后的数值上第N位变成1,后面全为0
此时A和-A里,第N位之后的位全为0,第N位之前的位全为反
所以两个数进行与操作,只有第N位为1
即: 0001 1000 & 1110 1000 = 0000 1000

那么,如何将v&-v转换成N呢?

德布莱英序列

我看到了一段代码:

unsigned int v;   
int r;           
static const int MultiplyDeBruijnBitPosition[32] = 
{
  0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 
  31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];

计算过程可以理解为:

0x077CB531U的二进制:

00000111011111001011010100110001

乘以 v&-v,即左移N位,再右移27位,得到的常数在MultiplyDeBruijnBitPosition里查表,得到的结果即是N。

例如乘以 100 0000,(6个0,左移6位)
 00000111011111001011010100110001
-> 11011111001011010100110001000000
再右移27位
-> 11011
得到的数字是27,在数组里是6

很神奇,不是吗?

仔细分析一下这个数字,可以发现,这个数字从每一位分别开始看,连续5位(到结尾循环),是所有5位的二进制数字的全集,而且左移28-31位时,结尾填0,正好序列开始的几个数字也是0。

那么不难理解,从这个数列的第X位任意取5位,都可以得到一个0-31的数字,并且根据查表取出这个数字对应是左移过几位。

 

为什么会存在这样的序列

把二进制依次写出,如果是两位,我们让每个两位数字的最后一位等于下一个两位数字的第一位, 00-01-11-10,写出 0011,长度为4。

三位,我们让每个三个数字的后两位等于下一个数字前两位,001-011-111-110-101-010-000,写出00111010,长度为8。

四位,见图:

 

 

 

依此类推,到第N位,我们可以让每个数的后N-1位等于下一个数字的前N-1位,得到长度为 2的N次方长度的2进制序列。

这就是德布莱英原理:一定存在长度为2的N次方长度的二进制串,循环来看,一位位移动,可以完整描述所有N位长度的二进制数字的集合。

链接1:https://en.wikipedia.org/wiki/De_Bruijn_sequence

链接2:https://baike.baidu.com/item/德布莱英序列/18898516?fr=aladdin

 

 我们可以任意生成这样的序列吗

稍微经过研究可以发现,Debrujin序列是密码学中运用很广泛的序列,已知原理,可以编程来实现自动求序列的代码。

1. 暴力遍历

2. 递归法 https://blog.csdn.net/lusongno1/article/details/51104737

3. 本原多项式方法 https://blog.csdn.net/sea_sky_cloud/article/details/80932402

(7)处理ASP.NET Core 中的错误

admin阅读(136)

1.前言

ASP.NET Core处理错误环境区分为两种:开发环境和非开发环境。
开发环境:开发人员异常页。
非开发环境:异常处理程序页、状态代码页。
在Startup.Configure方法里面我们会看到如下代码:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
       //开发环境
    } 
    else
    {
       //非开发环境
    }
}

env.IsDevelopment()是判断应用程序运行是在开发环境还是非开发环境,具体配置在Properties/launchSettings.json,找到ASPNETCORE_ENVIRONMENT属性,默认值是开发环境(Development),具体环境配置知识点后面我们再来学习下。

2.开发人员异常页

向Startup.Configure方法添加代码,以当应用在开发环境中运行时启用此页:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

开发人员异常页仅当应用程序在开发环境中运行时才会启用,而且调用UseDeveloperExceptionPage要配置于任何要捕获其异常的中间件前面。
该页包括关于异常和请求的以下信息:
●堆栈跟踪
●查询字符串参数(如果有)
●Cookie(如果有)
●request header

3.异常处理程序页

在下面的示例中,UseExceptionHandler 在非开发环境中添加异常处理中间件:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Razor Pages应用模板提供“页面”文件夹中的Error页(.cshtml)和PageModel类(ErrorModel)。 对于MVC应用,项目模板包括Error操作方法和Error视图。操作方法如下:

[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

不要使用HTTP方法属性(如HttpGet)修饰错误处理程序操作方法,因为会阻止某些请求访问的方法。同时最好允许匿名访问方法,以便未经身份验证的用户能够接收错误视图。
UseExceptionHandler中间还可以使用lambda进行异常处理:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
            await context.Response.WriteAsync("ERROR!<br><br>\r\n");
            var exceptionHandlerPathFeature = 
                context.Features.Get<IExceptionHandlerPathFeature>();
            // Use exceptionHandlerPathFeature to process the exception (for example, 
            // logging), but do NOT expose sensitive error information directly to 
            // the client.
            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
            }
            await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
            await context.Response.WriteAsync("</body></html>\r\n");
            await context.Response.WriteAsync(new string(' ', 512)); // IE padding
        });
    });
app.UseHsts();
}

4.状态代码页

一般情况下,ASP.NET Core应用程序不会为HTTP状态代码(如“404-未找到”)提供状态代码页的。但若要提供状态代码页,可以使用状态代码页中间件。

4.1 UseStatusCodePages中间件

若要启用常见错误状态代码的默认纯文本处理程序,请在Startup.Configure方法中调用 UseStatusCodePages:

app.UseStatusCodePages();

而这里有一点要注意的是,调用UseStatusCodePages中间件要在例如静态文件中间件和 MVC中间件等中间件前面调用:

app.UseStatusCodePages();
app.UseStaticFiles();
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

下面通过运行应用程序在浏览器地址栏上输入一个不存在地址看看配置该中间件后的效果:

很显然当我们输入一个不存在地址之后就会打开一个处理错误的状态代码页。
UseStatusCodePages中间件还有两种重载使用方法,具体运行效果就不一一截图了,大家自行测试。
●包含格式字符串的 UseStatusCodePages:

app.UseStatusCodePages("text/plain", "Status code page, status code: {0}");

●包含lambda的UseStatusCodePages:

app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";
    await context.HttpContext.Response.WriteAsync(
        "Status code page, status code: " +
        context.HttpContext.Response.StatusCode);
});

4.2 UseStatusCodePagesWithRedirect中间件

●向客户端发送“302 – 已找到”状态代码。
●将客户端重定向到URL模板中的位置。
下面我们在Startup.Configure方法中调用UseStatusCodePagesWithRedirect:

app.UseStatusCodePagesWithRedirects("/Error/{0}");

运行应用程序在浏览器上输入不存在地址https://localhost:44353/1看看配置该中间件后的效果,你会发觉当我们输入上述地址后会跳转到https://localhost:44353/Error/404链接去了,并显示:

这就说明白当我们输入一个不存在地址之后会重定向中间件设置的地址页面去了。

参考文献:
处理 ASP.NET Core 中的错误

ABP开发框架前后端开发系列—(4)Web API调用类的封装和使用,ABP开发框架前后端开发系列—(3)框架的分层和文件组织

admin阅读(112)

在前面随笔介绍ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以及简化了ABP框架的各个层的内容,使得我们项目结构更加清晰。上篇随笔已经介绍了字典模块中应用服务层接口的实现情况,并且通过运行Web API的宿主程序,可以在界面上进行接口测试了,本篇随笔基于前面介绍的基础上,介绍Web API调用类的封装和使用,使用包括控制台和Winform中对调用封装类的使用。

在上篇随笔《ABP开发框架前后端开发系列—(3)框架的分层和文件组织》中我绘制了改进后的ABP框架的架构图示,如下图所示。

这个项目分层里面的 03-Application.Common 应用服务通用层,我们主要放置在各个模块里面公用的DTO和应用服务接口类。有了这些DTO文件和接口类,我们就不用在客户端(如Winform客户、控制台、WPF/UWP等)重复编写这部分的内容,直接使用即可。

这些DTO文件和接口类文件,我们的主要用途是用来封装客户端调用Web API的调用类,使得我们在界面使用的时候,调用更加方便。

1)Web API调用类封装

为了更方便在控制台客户端、Winform客户端等场景下调用Web API的功能,我们需要对应用服务层抛出的Web API接口进行封装,然后结合DTO类实现一个标准的接口实现。

由于这些调用类可能在多个客户端中进行共享,因此根据我们在混合框架中积累的经验,我们把它们独立为一个项目进行管理,如下项目视图所示。

其中DictDataApiCaller 就是对应领域对象 <领域对象>ApiCaller的命名规则。

如对于字典模块的API封装类,它们继承一个相同的基类,然后实现特殊的自定义接口即可,这样可以减少常规的Create、Get、GetAll、Update、Delete等操作的代码,这些全部由调用基类进行处理,而只需要实现自定义的接口调用即可。如下是字典模块DictType和DictData两个业务对象的API封装关系。

如对于字典类型的API封装类定义代码如下所示。

    /// <summary>
    /// 字典类型对象的Web API调用处理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供单件对象使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

......

这里我们可以通过单件的方式来使用字典类型API的封装类实例 DictTypeApiCaller.Instance

对于Web API的调用,我们知道,一般需要使用WebClient或者HttpRequest的底层类进行Url的访问处理,通过提供相应的数据,获取对应的返回结果。

而对于操作方法的类型,是使用POST、GET、INPUT、DELETE的不同,需要看具体的接口,我们可以通过Swagger UI 呈现出来的进行处理即可,如下所示的动作类型。

如果处理动作不匹配,如本来是Post的用Get方法,或者是Delete的用Post方法,都会出错。

在Abp.Web.Api项目里面有一个AbpWebApiClient的封装方法,里面实现了POST方法,可以参考来做对应的WebClient的封装调用。

我在它的基础上扩展了实现方法,包括了Get、Put、Delete方法的调用。

我们使用的时候,初始化它就可以了。

apiClient = new AbpWebApiClient();

例如,我们对于常规的用户登录处理,它的API调用封装的操作代码如下所示,这个是一个POST方法。

        /// <summary>
        /// 对用户身份进行认证
        /// </summary>
        /// <param name="username">用户名</param>
        /// <param name="password">用户密码</param>
        /// <returns></returns>
        public async virtual Task<AuthenticateResult> Authenticate(string username, string password)
        {
            var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);
            var input = new
            {
                UsernameOrEmailAddress = username,
                Password = password
            };

            var result = await apiClient.PostAsync<AuthenticateResult>(url, input);
            return result;
        }

对于业务接口来说,我们都是基于约定的规则来命名接口名称和地址的,如对于GetAll这个方法来说,字典类型的地址如下所示。

/api/services/app/DictData/GetAll

另外还包括服务器的基础地址,从而构建一个完整的调用地址如下所示。

http://localhost:21021/api/services/app/DictData/GetAll

由于这些规则确定,因此我们可以通过动态构建这个API地址即可。

            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?SkipCount={0}&MaxResultCount={1}", dto.SkipCount, dto.MaxResultCount);

而对于GetAll函数来说,这个定义如下所示。

Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input)

它是需要根据一定的条件进行查询的,不仅仅是 SkipCount 和 MaxResultCount两个属性,因此我们需要动态组合它的url参数,因此建立一个辅助类来动态构建这些输入参数地址。

        /// <summary>
        /// 获取所有对象列表
        /// </summary>
        /// <param name="input">获取所有条件</param>
        /// <returns></returns>
        public async virtual Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url = GetUrlParam(input, url);

            var result = await apiClient.GetAsync<PagedResultDto<TEntityDto>>(url);
            return result;
        }

这样我们这个API的调用封装类的基类就实现了常规的功能了。效果如下所示。

而字典类型的API封装类,我们只需要实现特定的自定义接口即可,省却我们很多的工作量。

namespace MyProject.Caller
{
    /// <summary>
    /// 字典类型对象的Web API调用处理
    /// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
    {
        /// <summary>
        /// 提供单件对象使用
        /// </summary>
        public static DictTypeApiCaller Instance
        {
            get
            {
                return Singleton<DictTypeApiCaller>.Instance;
            }
        }

        /// <summary>
        /// 默认构造函数
        /// </summary>
        public DictTypeApiCaller()
        {
            this.DomainName = "DictType";//指定域对象名称,用于组装接口地址
        }

        public async Task<Dictionary<string, string>> GetAllType(string dictTypeId)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?dictTypeId={0}", dictTypeId);

            var result = await apiClient.GetAsync<Dictionary<string, string>>(url);
            return result; 
        }

        public async Task<IList<DictTypeNodeDto>> GetTree(string pid)
        {
            AddRequestHeaders();//加入认证的token头信息
            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?pid={0}", pid);

            var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);
            return result;
        }
    }
}

 

2)API封装类的调用

前面小节介绍了针对Web API接口的封装,以适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

到这里为止,我们还没有测试过具体的调用,还没有了解实际调用过程中是否有问题,当然我们在开发的时候,一般都是一步步来的,但也是确保整个路线没有问题的。

实际情况如何,是骡是马拉出来溜溜就知道了。

首先我们创建一个基于.net Core的控制台程序,项目情况如下所示。

在其中我们定义这个项目的模块信息,它是依赖于APICaller层的模块。

namespace RemoteApiConsoleApp
{
    [DependsOn(typeof(CallerModule))]
    public class MyModule : AbpModule
    {
        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
        }
    }
}

在ABP里面,模块是通过一定顺序启动的,如果我们通过AbpBootstrapper类来启动相关的模块,启动模块的代码如下所示。

//使用AbpBootstrapper创建类来处理
using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
{
    bootstrapper.Initialize();

        ..........

模块启动后,系统的IOC容器会为我们注册好相关的接口对象,那么调用API封装类的代码如下所示。

                //使用AbpBootstrapper创建类来处理
                using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
                {
                    bootstrapper.Initialize();

                    #region Role
                    using (var client = bootstrapper.IocManager.ResolveAsDisposable<RoleApiCaller>())
                    {
                        var caller = client.Object;

                        Console.WriteLine("Logging in with TOKEN based auth...");
                        var token = caller.Authenticate("admin", "123qwe").Result;
                        Console.WriteLine(token.ToJson());

                        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

                        Console.WriteLine("Getting roles...");
                        var pagerDto = new PagedResultRequestDto() { SkipCount = 0, MaxResultCount = 10 };
                        var result = caller.GetAll(pagerDto);
                        Console.WriteLine(result.ToJson());

                        Console.WriteLine("Create role...");
                        List<string> permission = new List<string>() { "Pages.Roles" };
                        var createRoleDto = new CreateRoleDto { DisplayName = "test", Name = "Test", Description = "test", Permissions = permission };
                        var roleDto = caller.Create(createRoleDto).Result;
                        Console.WriteLine(roleDto.ToJson());

                        var singleDto = new EntityDto<int>() { Id = roleDto.Id };
                        Console.WriteLine("Getting role by id...");
                        roleDto = caller.Get(singleDto).Result;
                        Console.WriteLine(roleDto);

                        Console.WriteLine("Delete role...");
                        var delResult = caller.Delete(singleDto);
                        Console.WriteLine(delResult.ToJson());

                        Console.ReadLine();
                    }
                    #endregion

上面是对角色的相关接口操作,如果对于我们之前创建的字典模块,那么它的操作代码类似,如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
    {
        var caller = client.Object;

        Console.WriteLine("Logging in with TOKEN based auth...");
        var token = caller.Authenticate("admin", "123qwe").Result;
        Console.WriteLine(token.ToJson());

        caller.RequestHeaders.Add(new NameValue("Authorization", "Bearer " + token.AccessToken));

        Console.WriteLine("Get All ...");
        var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10 };
        var result = caller.GetAll(pagerDto).Result;
        Console.WriteLine(result.ToJson());

        Console.WriteLine("Get All by condition ...");
        var pagerdictDto = new DictTypePagedDto() { Name = "民族" };
        result = caller.GetAll(pagerdictDto).Result;
        Console.WriteLine(result.ToJson());
        
        Console.WriteLine("Get count by condition ...");
        pagerdictDto = new DictTypePagedDto() {};
        var count = caller.Count(pagerdictDto).Result;
        Console.WriteLine(count);
        Console.WriteLine();

        Console.WriteLine("Create DictType...");
        var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test" };
        var dictDto = caller.Create(createDto).Result;
        Console.WriteLine(dictDto.ToJson());

        Console.WriteLine("Update DictType...");
        dictDto.Code = "testcode";
        var updateDto = caller.Update(dictDto).Result;
        Console.WriteLine(updateDto.ToJson());

        if (updateDto != null)
        {
            Console.WriteLine("Delete DictType...");
            caller.Delete(new EntityDto<string>() { Id = dictDto.Id });
        }

    }
    #endregion

测试字典模块的处理,执行效果如下所示。

删除内容,我们是配置为软删除的,因此可以通过数据库记录查看是否标记为删除了。

同时,我们可以看到审计日志里面,有对相关应用层接口的调用记录。

以上就是.net core控制台程序中对于API封装接口的调用,上面代码如果需要在.net framework里面跑,也是一样的,我同样也做了一个基于.net framework控制台程序,代码调用都差不多的,它的ApiCaller我们做成了 .net standard程序类库的,因此都是通用的。

前面我们提到,我们的APICaller的类,设计了单件的实例调用,因此我们调用起来更加方便,除了上面使用ABP的启动模块的方式调用外,我们可以用传统的方式进行调用,也就是创建一个ApiCaller的实例对象的方式进行调用,如下代码所示。

    string loginName = this.txtUserName.Text.Trim();
    string password = this.txtPassword.Text;
    AuthenticateResult result = null;
    try
    {
        result = await DictTypeApiCaller.Instance.Authenticate(loginName, password);
    }
    catch(AbpException ex)
    {
        MessageDxUtil.ShowTips("用户帐号密码不正确。\r\n错误信息:" + ex.Message);
        return;
    }

由于篇幅的原因,基于winform界面模块的调用,我在后面随笔在另起一篇随笔进行介绍吧,毕竟那是毕竟漂亮的字典模块呈现了。

 

开源SEO 更专业 更方便

联系我们投稿网址