[原创]C#的mongodb.Driver的In查询、联合查询实例代码

mikel阅读(1165)

1、In查询

        int[] inselllistid = new int[] { 169199, 169200 };
         MongoHelper mong = new MongoHelper(mongo, "mulan");
        var builder = Builders<SellOrderList>.Filter;
        //var filter = builder.In(n => n.Partner_ID, new int[] { 1, 2, 6 }) & builder.Eq("Seller_ID", 88);
        var filter = builder.In(n=>n.Identifier, inselllistid);
        mong.DeleteMany<SellOrderList>("SellOrderList", filter);

2、联合查询

        //多条件查询
        var builder = Builders<ClientInfo>.Filter;
        var filter = builder.In(n => n.Partner_ID, new int[] { 1, 2, 6 }) & builder.Eq("Seller_ID", 88);
        filter = filter | builder.Eq("Client_Class", "公共") | builder.Eq("Client_Class", "本地");
        List<ClientInfo> cls = mong.GetDocumentsByFilter<ClientInfo>("ClientInfo", filter);
        List<ClientInfo> ls = query.Select().ToList();
        var mongodb = new MongoClient(settings);
        mong.DropCollection<ClientInfo>("ClientInfo");

        var builder = Builders<ClientInfo>.Filter;
        var filter1 = builder.Or(builder.Eq("Seller_ID", 96), builder.Eq("Client_Class", "公共"));
        var filter2 = builder.Or(builder.Eq("Client_Class", "本地"), builder.In(n => n.Partner_ID, new[] { 1, 2, 6 }));
        var filter = builder.Or(filter1, filter2);
        //排序
        SortDefinitionBuilder<ClientInfo> builderSort = Builders<ClientInfo>.Sort;
        SortDefinition<ClientInfo> sort = builderSort.Descending("Create_Date");

        List<ClientInfo> cls = business.SelectByPage<ClientInfo>(ref p, filter, sort);
        Console.WriteLine("j:" + JsonConvert.SerializeObject(query.filter));
        // Here's how you can debug the generated query
        //调试输出mognodb的查询语句
        var documentSerializer = MongoDB.Bson.Serialization.BsonSerializer.SerializerRegistry.GetSerializer<ClientInfo>();
        var renderedFilter = query.filter.Render(documentSerializer, MongoDB.Bson.Serialization.BsonSerializer.SerializerRegistry).ToString();
        Console.WriteLine(renderedFilter);

        List<ClientInfo> pls = mong.GetDocumentsByFilter<ClientInfo>("ClientInfo", query.filter);

DNS_PROBE_FINISHED_NXDOMAIN错误的解决方法_天道酬勤-CSDN博客

mikel阅读(886)

来源: DNS_PROBE_FINISHED_NXDOMAIN错误的解决方法_天道酬勤-CSDN博客

一、问题

 

1、Win10 下chrome无法访问网页,怀疑是DNS污染的问题,重置浏览器无效。

 

//
DNS_PROBE_FINISHED_NO_DOMAIN
//

 

 

 

二、解决办法

 

1、用管理员身份打开cmd后,运行如下命令即可解决问题。

//
netsh winsock reset
//

————————————————
版权声明:本文为CSDN博主「worthsen」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38880380/article/details/79291464

基于C#的MongoDB数据库开发应用(4)--Redis的安装及使用 - 伍华聪 - 博客园

mikel阅读(595)

来源: 基于C#的MongoDB数据库开发应用(4)–Redis的安装及使用 – 伍华聪 – 博客园

在前面介绍了三篇关于MongoDB数据库的开发使用文章,严格来讲这个不能归类于MongoDB数据库开发,不过Redis又有着和MongoDB数据库非常密切的关系,它们两者很接近,Redis主要是内存中的NoSQL数据库,用来提高性能的;MongoDB数据库则是文件中的NoSQL数据库,做数据序列号存储使用的,它们两者关系密切又有所区别。本篇主要介绍Redis的安装及使用,为后面Redis和MongoDB数据库的联合使用先铺下基础。

1、Redis基础及安装

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis的代码遵循ANSI-C编写,可以在所有POSIX系统(如Linux, *BSD, Mac OS X, Solaris等)上安装运行。而且Redis并不依赖任何非标准库,也没有编译参数必需添加。

1)Redis支持两种持久化方式:

(1):snapshotting(快照)也是默认方式.(把数据做一个备份,将数据存储到文件)

(2)Append-only file(缩写aof)的方式

快照是默认的持久化方式,这种方式是将内存中数据以快照的方式写到二进制文件中,默认的文件名称为dump.rdb.可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key键修改就自动做快照.

aof方式:由于快照方式是在一定间隔时间做一次的,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。aof比快照方式有更好的持久化性,是由于在使用aof时,redis会将每一个收到的写命令都通过write函数追加到文件中,当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

2)Redis数据结构

Redis 的作者antirez曾称其为一个数据结构服务器(data structures server),这是一个非常准确的表述,Redis的所有功能就是将数据以其固有的几种结构保存,并提供给用户操作这几种结构的接口。我们可以想象我们在各种语言中的那些固有数据类型及其操作。

Redis目前提供四种数据类型:string,list,setzset(sorted set)和Hash

  • string是最简单的类型,你可以理解成与Memcached一模一个的类型,一个key对应一个value,其上支持的操作与Memcached的操作类似。但它的功能更丰富。
  • list是一个链表结构,主要功能是push、pop、获取一个范围的所有值等等。操作中key理解为链表的名字。
  • set是集合,和我们数学中的集合概念相似,对集合的操作有添加删除元素,有对多个集合求交并差等操作。操作中key理解为集合的名字。
  • zset是set的一个升级版本,他在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。可以理解了有两列的mySQL表,一列存value,一列存顺序。操作中key理解为zset的名字。
  • Hash数据类型允许用户用Redis存储对象类型,Hash数据类型的一个重要优点是,当你存储的数据对象只有很少几个key值时,数据存储的内存消耗会很小.更多关于Hash数据类型的说明请见: http://code.google.com/p/redis/wiki/Hashes

3)Redis数据存储

Redis的存储分为内存存储、磁盘存储和log文件三部分,配置文件中有三个参数对其进行配置。

save seconds updatessave配置,指出在多长时间内,有多少次更新操作,就将数据同步到数据文件。这个可以多个条件配合,比如默认配置文件中的设置,就设置了三个条件。

appendonly yes/no appendonly配置,指出是否在每次更新操作后进行日志记录,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面的save条件来同步的,所以有的数据会在一段时间内只存在于内存中。

appendfsync no/always/everysec appendfsync配置,no表示等操作系统进行数据缓存同步到磁盘,always表示每次更新操作后手动调用fsync()将数据写到磁盘,everysec表示每秒同步一次。

 

4)Redis的安装

Redis可以在不同的平台运行,不过我主要基于Windows进行开发工作,所以下面主要是基于Windows平台进行介绍。

Redis可以安装以DOS窗口启动的,也可以安装为Windows服务的,一般为了方便,我们更愿意把它安装为Windows服务,这样可以比较方便管理。下载地址:https://github.com/MSOpenTech/redis/releases下载,安装为Windows服务即可。

当前可以下载到最新的Windows安装版本为3.0,安装后作为Windows服务运行,安装后可以在系统的服务里面看到Redis的服务在运行了,如下图所示。

安装好Redis后,还有一个Redis伴侣Redis Desktop Manager需要安装,这样可以实时查看Redis缓存里面有哪些数据,具体地址如下:http://redisdesktop.com/download

下载属于自己平台的版本即可

下载安装后,打开运行界面,如果我们往里面添加键值的数据,那么可以看到里面的数据了。

 

2、Redis的C#使用

Redis目前提供四种数据类型:string,list,setzset(sorted set)和Hash。因此它在C#里面也有对应的封装处理,而且有很多人对他进行了封装,提供了很多的响应开发包,具体可以访问http://redis.io/clients#c 进行了解。一般建议用ServiceStack.Redis的封装驱动比较好,具体的使用可以参考https://github.com/ServiceStack/ServiceStack.Redis

我们开发C#代码的时候,可以在NuGet程序包上面进行添加对应的ServiceStack.Redis引用,如下所示。

 

在弹出的NuGet程序包里面,输入ServiceStack.Redis进行搜索,并添加下面的驱动引用即可。

这样会在项目引用里面添加了几个对应的程序集,如下所示。

在C#里面使用Redis,首先需要实例化一个Redis的客户端类,如下所示。

        //创建一个Redis的客户端类
        RedisClient client = new RedisClient("127.0.0.1", 6379);

在使用前,我们需要清空所有的键值存储,使用FushAll方法即可,如下所示

            //Redis FlushAll 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。 
            client.FlushAll();

根据上面的驱动,可以为不同类型的处理编写一些演示代码,下面代码是摘录网上的案例进行介绍。

复制代码
            #region string类型的测试代码

            client.Add<string>("StringValueTime", "带有有效期的字符串", DateTime.Now.AddMilliseconds(10000));

            while (true)
            {
                if (client.ContainsKey("StringValueTime"))
                {
                    Console.WriteLine("String.键:StringValue, 值:{0} {1}", client.Get<string>("StringValueTime"), DateTime.Now);
                    Thread.Sleep(10000);
                }
                else
                {
                    Console.WriteLine("键:StringValue, 值:已过期 {0}", DateTime.Now);
                    break;
                }
            }

            client.Add<string>("StringValue", " String和Memcached操作方法差不多");
            Console.WriteLine("数据类型为:String.键:StringValue, 值:{0}", client.Get<string>("StringValue"));

            Student stud = new Student() { id = "1001", name = "李四" };
            client.Add<Student>("StringEntity", stud);
            Student Get_stud = client.Get<Student>("StringEntity");
            Console.WriteLine("数据类型为:String.键:StringEntity, 值:{0} {1}", Get_stud.id, Get_stud.name);
            #endregion

            #region Hash类型的测试代码

            client.SetEntryInHash("HashID", "Name", "张三");
            client.SetEntryInHash("HashID", "Age", "24");
            client.SetEntryInHash("HashID", "Sex", "男");
            client.SetEntryInHash("HashID", "Address", "上海市XX号XX室");

            List<string> HaskKey = client.GetHashKeys("HashID");
            foreach (string key in HaskKey)
            {
                Console.WriteLine("HashID--Key:{0}", key);
            }

            List<string> HaskValue = client.GetHashValues("HashID");
            foreach (string value in HaskValue)
            {
                Console.WriteLine("HashID--Value:{0}", value);
            }

            List<string> AllKey = client.GetAllKeys(); //获取所有的key。
            foreach (string Key in AllKey)
            {
                Console.WriteLine("AllKey--Key:{0}", Key);
            }
            #endregion

            #region List类型的测试代码
            /*
             * list是一个链表结构,主要功能是push,pop,获取一个范围的所有的值等,操作中key理解为链表名字。 
             * Redis的list类型其实就是一个每个子元素都是string类型的双向链表。我们可以通过push,pop操作从链表的头部或者尾部添加删除元素,
             * 这样list既可以作为栈,又可以作为队列。Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,
             * Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构 
             */
            client.EnqueueItemOnList("QueueListId", "1.张三");  //入队
            client.EnqueueItemOnList("QueueListId", "2.张四");
            client.EnqueueItemOnList("QueueListId", "3.王五");
            client.EnqueueItemOnList("QueueListId", "4.王麻子");
            long q = client.GetListCount("QueueListId");

            Console.WriteLine(client.GetItemFromList("QueueListId", 0));
            for (int i = 0; i < q; i++)
            {
                Console.WriteLine("QueueListId出队值:{0}", client.DequeueItemFromList("QueueListId"));   //出队(队列先进先出)
            }

            q = client.GetListCount("QueueListId");
            Console.WriteLine(q);

            client.PushItemToList("StackListId", "1.张三");  //入栈
            client.PushItemToList("StackListId", "2.张四");
            client.PushItemToList("StackListId", "3.王五");
            client.PushItemToList("StackListId", "4.王麻子");
            long p = client.GetListCount("StackListId");
            for (int i = 0; i < p; i++)
            {
                Console.WriteLine("StackListId出栈值:{0}", client.PopItemFromList("StackListId"));   //出栈(栈先进后出)
            }
            q = client.GetListCount("StackListId");
            Console.WriteLine(q);

            #endregion

            #region Set无序集合的测试代码
            /*
             它是string类型的无序集合。set是通过hash table实现的,添加,删除和查找,对集合我们可以取并集,交集,差集
             */
            client.AddItemToSet("Set1001", "小A");
            client.AddItemToSet("Set1001", "小B");
            client.AddItemToSet("Set1001", "小C");
            client.AddItemToSet("Set1001", "小D");
            HashSet<string> hastsetA = client.GetAllItemsFromSet("Set1001");
            foreach (string item in hastsetA)
            {
                Console.WriteLine("Set无序集合ValueA:{0}", item); //出来的结果是无须的
            }

            client.AddItemToSet("Set1002", "小K");
            client.AddItemToSet("Set1002", "小C");
            client.AddItemToSet("Set1002", "小A");
            client.AddItemToSet("Set1002", "小J");
            HashSet<string> hastsetB = client.GetAllItemsFromSet("Set1002");
            foreach (string item in hastsetB)
            {
                Console.WriteLine("Set无序集合ValueB:{0}", item); //出来的结果是无须的
            }

            HashSet<string> hashUnion = client.GetUnionFromSets(new string[] { "Set1001", "Set1002" });
            foreach (string item in hashUnion)
            {
                Console.WriteLine("求Set1001和Set1002的并集:{0}", item); //并集
            }

            HashSet<string> hashG = client.GetIntersectFromSets(new string[] { "Set1001", "Set1002" });
            foreach (string item in hashG)
            {
                Console.WriteLine("求Set1001和Set1002的交集:{0}", item);  //交集
            }

            HashSet<string> hashD = client.GetDifferencesFromSet("Set1001", new string[] { "Set1002" });  //[返回存在于第一个集合,但是不存在于其他集合的数据。差集]
            foreach (string item in hashD)
            {
                Console.WriteLine("求Set1001和Set1002的差集:{0}", item);  //差集
            }

            #endregion

            #region  SetSorted 有序集合的测试代码
            /*
             sorted set 是set的一个升级版本,它在set的基础上增加了一个顺序的属性,这一属性在添加修改.元素的时候可以指定,
             * 每次指定后,zset(表示有序集合)会自动重新按新的值调整顺序。可以理解为有列的表,一列存 value,一列存顺序。操作中key理解为zset的名字.
             */
            client.AddItemToSortedSet("SetSorted1001", "1.刘仔");
            client.AddItemToSortedSet("SetSorted1001", "2.星仔");
            client.AddItemToSortedSet("SetSorted1001", "3.猪仔");
            List<string> listSetSorted = client.GetAllItemsFromSortedSet("SetSorted1001");
            foreach (string item in listSetSorted)
            {
                Console.WriteLine("SetSorted有序集合{0}", item);
            }
            #endregion
复制代码

对于具体类型的类对象,也可以使用As方法进行转换为对应的处理对象进行处理,如下所示

IRedisTypedClient<Phone> phones = client.As<Phone>();

具体的测试代码如下所示。

复制代码
        /// <summary>
        /// Redis对对象类的处理例子
        /// </summary>
        private void btnTypeValue_Click(object sender, EventArgs e)
        {
            IRedisTypedClient<Phone> phones = client.As<Phone>();
            Phone phoneFive = phones.GetValue("5");
            if (phoneFive == null)
            {
                Thread.Sleep(50);
                phoneFive = new Phone
                {
                    Id = 5,
                    Manufacturer = "Apple",
                    Model = "xxxxx",
                    Owner = new Person
                    {
                        Id = 1,
                        Age = 100,
                        Name = "伍华聪",
                        Profession = "计算机",
                        Surname = "wuhuacong"
                    }
                };

                phones.SetEntry(phoneFive.Id.ToString(), phoneFive);
            }

            client.Store<Phone>(
                new Phone
                {
                    Id = 2,
                    Manufacturer = "LG",
                    Model = "test-xxx",
                    Owner = new Person
                    {
                        Id = 2,
                        Age = 40,
                        Name = "test",
                        Profession = "teacher",
                        Surname = "wuhuacong"
                    }
                });

            var message = "Phone model is " + phoneFive.Manufacturer + ",";
            message += "Phone Owner Name is: " + phoneFive.Owner.Name;
            Console.WriteLine(message);
        }
复制代码

以上就是关于Redis的安装以及简单的例子使用说明,在具体中,我们可以利用Redis的高性能特性,来构建我们的缓存数据,并且可以利用Redis和MongoDB数据库的完美衔接,可以整合一起做的更好,为相关的后台提供更高效的数据处理操作,毕竟在互联网的大环境下,性能是非常重要的。

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

基于C#的MongoDB数据库开发应用(1)--MongoDB数据库的基础知识和使用 - 伍华聪 - 博客园

mikel阅读(913)

来源: 基于C#的MongoDB数据库开发应用(1)–MongoDB数据库的基础知识和使用 – 伍华聪 – 博客园

在花了不少时间研究学习了MongoDB数据库的相关知识,以及利用C#对MongoDB数据库的封装、测试应用后,决定花一些时间来总结一下最近的研究心得,把这个数据库的应用单独作为一个系列来介绍,希望从各个方面来总结并记录一下这个新型、看似神秘的数据库使用过程。本文是这个系列的开篇,主要介绍一些MongoDB数据库的基础知识、安装过程、基础使用等方面。

MongoDB是一款由C++编写的高性能、开源、无模式的常用非关系型数据库产品,是非关系数据库当中功能最丰富、最像关系数据库的数据库。它扩展了关系型数据库的众多功能,例如:辅助索引、范围查询、排序等。

MongoDB主要解决的是海量数据的访问效率问题,它作为分布式数据崛起后,使用较多的一款非结构数据库,必然有其值得称道之处,它的主要功能特性如下:

1)面向集合的存储,适合存储对象及JSON形式的数据。
2)动态查询,MongoDB支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
3)完整的索引支持,包括文档内嵌对象及数组。MongoDB的查询优化器会分析查询表达式,并生成一个高效的查询计划。
4)查询监视,MongoDB包含一个监视工具用于分析数据库操作的性能。
5)复制及自动故障转移,MongoDB数据库支持服务器之间的数据复制,支持主-从模式及服务器之间的相互复制。复制的主要目标是提供冗余及自动故障转移。
6)高效的传统存储方式,支持二进制数据及大型对象(如图片或视频)。
7)自动分片以支持云级别的伸缩性,自动分片功能支持水平的数据库集群,可动态添加额外的机器。

1、MongoDB数据库和传统关系数据库的对比

MongoDB数据库有几个简单的概念需要了解一下。

 

1)MongoDB中的 database 有着和我们熟知的”数据库”一样的概念 (对 Oracle 来说就是 schema)。一个 MongoDB 实例中,可以有零个或多个数据库,每个都作为一个高等容器,用于存储数据。

2)数据库中可以有零个或多个 collections (集合)。集合和传统意义上的 table 基本一致,可以简单的把两者看成是一样的东西。

3)集合是由零个或多个 documents (文档)组成。同样,一个文档可以看成是一 row

4)文档是由零个或多个 fields (字段)组成。,对应的就是关系数据库的 columns

5)Indexes (索引)在 MongoDB 中扮演着和它们在 RDBMS 中一样的角色,都是为了提高查询的效率。

6)Cursors (游标)和上面的五个概念都不一样,但是它非常重要,并且经常被忽视,其中最重要的你要理解的一点是,游标是当你问 MongoDB 拿数据的时候,它会给你返回一个结果集的指针而不是真正的数据,这个指针我们叫它游标,我们可以拿游标做我们想做的任何事情,比如说计数或者跨行之类的,而无需把真正的数据拖下来,在真正的数据上操作。

它们的对比关系图如下所示。

数据在Mongodb里面都是以Json格式方式进行存储的,如下所示是其中的一个记录内容。

复制代码
{
    _id: ObjectID('4bd9e8e17cefd644108961bb'),
    name:'Vivek',
    class : '12th',
    subjects: [ 'physics', 'chemistry', 'math', 'english', 'computer'],
    address: {
                    house_no: '12B',
                    block: 'B',
                    sector: 12,
                    city : 'noida',
                    },
    grade: [
                    {
                    exam: 'unit test 1',
                    score: '60%'
                    },
                    {
                    exam: 'unit test 2',
                    score: '70%'
                    }
                    
                ]                
}
复制代码

在过去的很长一段时间中,关系型数据库一直是最主流的数据库解决方案,他运用真实世界中事物与关系来解释数据库中抽象的数据架构。然而,在信息技术爆炸式发展的今天,大数据已经成为了继云计算,物联网后新的技术革命,关系型数据库在处理大数据量时已经开始吃力,开发者只能通过不断地优化数据库来解决数据量的问题,但优化毕竟不是一个长期方案,所以人们提出了一种新的数据库解决方案来迎接大数据时代的到来——NoSQL(非关系型数据库),其中MongoDB数据库就是其中的NoSQL的杰出代表。在大数据时代中,大数据量的处理已经成了考量一个数据库最重要的原因之一。而MongoDB的一个主要目标就是尽可能的让数据库保持卓越的性能,这很大程度地决定了MongoDB的设计。

根据MongoDB官网的说明,MongoDB的适用场景如下:

1)网站实时数据:MongoDB非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。

2)数据缓存:由于性能很高,MongoDB也适合作为信息基础设施的缓存层。在系统重启之后,由MongoDB搭建的持久化缓存层可以避免下层的数据源过载。

3)大尺寸、低价值数据存储:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储。

4)高伸缩性场景:MongoDB非常适合由数十或数百台服务器组成的数据库。MongoDB的路线图中已经包含对MapReduce引擎的内置支持。

5)对象或JSON数据存储:MongoDB的BSON数据格式非常适合文档化格式的存储及查询。

MongoDB不适合使用场景如下:
1)高度事务性系统:例如银行或会计系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。

2)传统的商业智能应用:针对特定问题的BI数据库会对产生高度优化的查询方式。
3)需要复杂SQL查询的问题。

MongoDB大多数情况下,可以代替关系型数据库实现数据库业务。它更简单更直接、更快速并且通常对应用开发者的约束更少,不过缺乏事务支持需要慎重考虑业务需要。

 

2、MongoDB数据库的安装及基础使用

MongoDB数据的官网为:https://www.mongodb.org/,当前版本为3.2,可以直接下载安装版本在Linux或者Windows进行安装。

一般在Windows,我们默认安装的路径为C:\Program Files\MongoDB,安装后可以手动创建一个放置数据库和日志文件的目录,一般不要放在C盘就好,如下所示:

创建文件夹d:\mongodb\data\db、d:\mongodb\data\log,分别用来安装db和日志文件,我们以后运行数据库后,这个目录就用来放置我们创建的数据库和日志资源了。

一般我们安装后,为了在命令行方便调用Mongodb的命令,我们可以设置一个全局的路径变量,如下所示。

默认情况下,mongodb的工作模式,是启动一个DOS窗口,运行mongodb的数据库服务,一旦这个DOS窗口关闭,也就停止了相关的服务,在Windows平台,我们可以把它寄宿在Windows服务里面,让它随着系统的启动而启动,也不必因为误关闭窗口而停止了数据库服务了。

通过下面命令行执行数据库服务的处理。

mongod --dbpath "d:\mongodb\data\db" --logpath "d:\mongodb\data\log\MongoDB.log" --install --serviceName "MongoDB"

然后使用命令行启动服务

NET START MongoDB

创建服务并顺利启动成功后,然后就可以在系统的服务列表里查看到了,我们确认把它设置为自动启动的Windows服务即可。

启动后,我们可以在系统【运行】里面直接使用命令mongo打开窗口就可以进行相关的操作了。

上面用了一些常见的命令操作。

  • show dbs    显示数据库列表
  • use dbname    进入dbname数据库,大小写敏感,没有这个数据库也不要紧
  • show collections    显示数据库中的集合,相当于表格
  • db.<collection_name>.find(); 集合查找方法,参考上面的方式,使用pretty()函数是排版更好看的意思。

而其中find方法很强大,可以组合很多条件查询的方式,如下所示:

  • db.collection.find({ “key” : value })    查找key=value的数据
  • db.collection.find({ “key” : { $gt: value } })    key > value
  • db.collection.find({ “key” : { $lt: value } })    key < value
  • db.collection.find({ “key” : { $gte: value } })    key >= value
  • db.collection.find({ “key” : { $lte: value } })    key <= value
  • db.collection.find({ “key” : { $gt: value1 , $lt: value2 } })    value1 < key <value2
  • db.collection.find({ “key” : { $ne: value } })    key <> value
  • db.collection.find({ “key” : { $mod : [ 10 , 1 ] } })    取模运算,条件相当于key % 10 == 1 即key除以10余数为1的
  • db.collection.find({ “key” : { $nin: [ 1, 2, 3 ] } })    不属于,条件相当于key的值不属于[ 1, 2, 3 ]中任何一个
  • db.collection.find({ “key” : { $in: [ 1, 2, 3 ] } })    属于,条件相当于key等于[ 1, 2, 3 ]中任何一个
  • db.collection.find({ “key” : { $size: 1 } })    $size 数量、尺寸,条件相当于key的值的数量是1(key必须是数组,一个值的情况不能算是数量为1的数组)
  • db.collection.find({ “key” : { $exists : true|false } })    $exists 字段存在,true返回存在字段key的数据,false返回不存在字度key的数据
  • db.collection.find({ “key”: /^val.*val$/i })    正则,类似like;“i”忽略大小写,“m”支持多行
  • db.collection.find({ $or : [{a : 1}, {b : 2} ] })    $or或 (注意:MongoDB 1.5.3后版本可用),符合条件a=1的或者符合条件b=2的数据都会查询出来
  • db.collection.find({ “key”: value , $or : [{ a : 1 } , { b : 2 }] })    符合条件key=value ,同时符合其他两个条件中任意一个的数据
  • db.collection.find({ “key.subkey” :value })    内嵌对象中的值匹配,注意:”key.subkey”必须加引号
  • db.collection.find({ “key”: { $not : /^val.*val$/i } })    这是一个与其他查询条件组合使用的操作符,不会单独使用。上述查询条件得到的结果集加上$not之后就能获得相反的集合。

当然还有插入更新的处理语句也是很特别的。

db.student.insert({name:'student1',subject:['arts','music']})

特别是更新操作需要说明一下,支持常规的$set方法(修改)、$unset方法(移除指定的键),还有原子级的$inc方法(数值增减),$rename方法(重命名字段名称)等等,

db.users.update({"_id" : ObjectId("51826852c75fdd1d8b805801")},  {"$set" : {"hobby" :["swimming","basketball"]}} )
db.users.update({"_id" : ObjectId("51826852c75fdd1d8b805801")},{"$unset" : {"hobby" :1 }} )
db.posts.update({"_id" : ObjectId("5180f1a991c22a72028238e4")}, {"$inc":{"pageviews":1}})
db.students.update( { _id: 1 }, { $rename: { 'nickname': 'alias', 'cell': 'mobile' } }

upsert是一种特殊的更新操作,不是一个操作符。(upsert = up[date]+[in]sert),也就是如果存在则更新,否则就写入一条新的记录操作。这个参数是个布尔类型,默认是false。

db.users.update({age :25}, {$inc :{"age" :3}}, true)

另外,Update可以对Json的集合进行处理,如果对于subject对象是一个集合的话,插入或更新其中的字段使用下面的语句

db.student.update({name:'student5'},{$set:{subject:['music']}},{upsert:true});

如果是记录已经存在,我们可以使用索引数值进行更新其中集合里面的数据,如下所示。

db.student.update({name:'student3'},{$set:{'subject.0':'arts'}});

如果我们先在集合里面增加一个记录,而非替换的话,那么使用$push语句,如下面的语句所示。

db.student.update({name:'student3'},{$push:{'subject':'sports'}})

相反,如果要移除集合里面的某个值,使用$pop操作符,那么语句如下所示

db.student.update({name:'student3'},{$pop:{'subject':1}});

其中索引为1标识最右边的记录,-1标识为最左边的记录。

另外还可以使用$pushAll 和$pullAll来增加/移除一个或多个集合记录,如下代码所示。

db.student.update({name:'student3'},{$pushAll:{'subject':['sports','craft']}})
db.student.update({name:'student3'},{$pullAll:{'subject':['sports','craft']}})

 

mongodb的数据库的操作还是比较容易理解的,具体可以进一步参考官网里面的介绍。

https://docs.mongodb.org/manual/

https://docs.mongodb.org/getting-started/csharp/client/

http://mongodb.github.io/mongo-csharp-driver/2.2/

http://wiki.jikexueyuan.com/project/the-little-mongodb-book/

 

3、MongoDB数据库的C#驱动的使用

数据库的C#驱动使用介绍,可以参考:https://docs.mongodb.org/getting-started/csharp/ 当前已更改为https://docs.mongodb.com/drivers/csharp ,可以下载相关的DLL然后在项目中引用,当前的驱动版本为2.2,一般引入下面几个DLL即可。

    • MongoDB.Bson.dll
    • MongoDB.Driver.dll
    • MongoDB.Driver.Core.dll

也可以使用VS工具的NugGet包进行下载管理,如下所示。

然后在弹出的NugGet程序包管理界面里面搜索mongo,然后添加MongoDB.Driver的数据库驱动就可以使用了。

MongoDB数据库驱动在2.2版本(或者是从2.0开始)好像完全改写了API的接口,因此目前这个版本同时支持两个版本的API处理,一个是基于MongoDatabase的对象接口,一个是IMongoDatabase的对象接口,前者中规中矩,和我们使用Shell里面的命令名称差不多,后者IMongoDatabase的接口是基于异步的,基本上和前者差别很大,而且接口都提供了异步的处理操作。后面我会分别对这两个部分进行详细的介绍,本文基于篇幅的原因,介绍一下两者的简单差异就可以了。

我们以Mongodb的数据库连接字符串mongodb://localhost/local来进行构建

1)旧接口MongoDatabase对象的构建

            var client = new MongoClient(connectionString);
            var database = client.GetServer().GetDatabase(new MongoUrl(connectionString).DatabaseName);

2)新接口IMongoDatabase对象的构建

            var client = new MongoClient(connectionString);
            var database = client.GetDatabase(new MongoUrl(connectionString).DatabaseName);

后者已经没有了GetServer的接口了。

3)旧接口的查找对象处理

复制代码
        /// <summary>
        /// 查询数据库,检查是否存在指定ID的对象
        /// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual T FindByID(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            MongoCollection<T> collection = GetCollection();
            return collection.FindOneById(new ObjectId(id)); 
        }
复制代码

3)新接口查找对象的处理

复制代码
        /// <summary>
        /// 查询数据库,检查是否存在指定ID的对象
        /// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual T FindByID(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            return collection.Find(s=> s.Id == id).FirstOrDefault();
        }
复制代码

新接口已经没有了FindOneById等接口了,所有的操作基本上都通过Find方法进行处理。旧接口很多通过Query对象进行条件的查询,新接口又换了一个对象进行过滤条件处理了,总的来说,两个接口差异非常大。

例如旧版本接口的Query使用C#代码如下所示:

复制代码
        private void TestQuery()
        {
            Query.All("name", new List<BsonValue> { BsonValue.Create("a"), BsonValue.Create("b") });//通过多个元素来匹配数组
            Query.And(Query.EQ("name", "a"), Query.EQ("title", "t"));//同时满足多个条件
            Query.Or(Query.EQ("name", "a"), Query.EQ("title", "t"));//满足其中一个条件
            Query.EQ("name", "a");//等于
            Query.Exists("type");//判断键值是否存在
            Query.GT("value", 2);//大于>
            Query.GTE("value", 3);//大于等于>=
            Query.In("name", new List<BsonValue> { BsonValue.Create("a"), BsonValue.Create("b") });//包括指定的所有值,可以指定不同类型的条件和值
            Query.LT("value", 9);//小于<
            Query.LTE("value", 8);//小于等于<=
            Query.Mod("value", 3, 1);//将查询值除以第一个给定值,若余数等于第二个给定值则返回该结果
            Query.NE("name", "c");//不等于
            Query.Size("name", 2);//给定键的长度
            Query.Type("_id", BsonType.ObjectId);//给定键的类型
            Query.ElemMatch("children", Query.And( Query.EQ("name", "C3"),   Query.EQ("value", "C")));

            //Query.Nor(Array);//不包括数组中的值
            //Query.Not("name");//元素条件语句
            //Query.NotIn("name", "a", 2);//返回与数组中所有条件都不匹配的文档
            //Query.Where(BsonJavaScript);//执行JavaScript

            //Query.Matches("Title", str);//模糊查询 相当于sql中like  -- str可包含正则表达式
            var keyword = "abc";
            Query.Matches("Title", new BsonRegularExpression("/.*" + keyword + ".*/"));//模糊Like语法

            //通过正则表达式 1开头 第二位数0~9且只能一位数,也包含20
            var queryName = Query.Matches("Name", new BsonRegularExpression("Donma1([0-9]{1,1})$|20"));
            //查找年龄 >=10 且<=20 
            var queryAge = Query.And(Query.GTE("Age", 10), Query.LTE("Age", 20));

            var entityQuery = Query<UserInfo>.EQ(e => e.Name, "wuhuacong");
            var entityQuery2 = Query<UserInfo>.EQ(e => e.Id, "4B414D000000011613CD");
        }
复制代码

新版本的条件查询,则丢弃了Query这个对象,提供了FilterDefinition<T> 对象的处理,估计是这个可以处理的更好吧,同时新接口全部支持异步的处理操作了。

如插入记录的异步操作代码如下所示。

复制代码
        /// <summary>
        /// 插入指定对象到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        public virtual async Task InsertAsync(T t)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");

            IMongoCollection<T> collection = GetCollection();
            await collection.InsertOneAsync(t);
        }
复制代码

好了,基于篇幅的原因,把后面介绍的C#开发留到下一篇进行介绍,希望本篇文章对大家了解mongodb数据库,以及如何在C#上面使用该数据库提供了一个简要的指引,希望大家多多支持。

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

基于C#的MongoDB数据库开发应用(3)--MongoDB数据库的C#开发之异步接口 - 伍华聪 - 博客园

mikel阅读(633)

来源: 基于C#的MongoDB数据库开发应用(3)–MongoDB数据库的C#开发之异步接口 – 伍华聪 – 博客园

在前面的系列博客中,我曾经介绍过,MongoDB数据库的C#驱动已经全面支持异步的处理接口,并且接口的定义几乎是重写了。本篇主要介绍MongoDB数据库的C#驱动的最新接口使用,介绍基于新接口如何实现基础的增删改查及分页等处理,以及如何利用异步接口实现基类相关的异步操作。

MongoDB数据库驱动在2.2版本(或者是从2.0开始)好像完全改写了API的接口,因此目前这个版本同时支持两个版本的API处理,一个是基于MongoDatabase的对象接口,一个是IMongoDatabase的对象接口,前者中规中矩,和我们使用Shell里面的命令名称差不多,后者IMongoDatabase的接口是基于异步的,基本上和前者差别很大,而且接口都提供了异步的处理操作。

1、MongoDB数据库C#驱动的新接口

新接口也还是基于数据库,集合,文档这样的处理概念进行封装,只是它们的接口不再一样了,我们还是按照前面的做法,定义一个数据库访问的基类,对MongoDB数据库的相关操作封装在基类里面,方便使用,同时基类利用泛型对象,实现更强类型的约束及支持,如基类BaseDAL的定义如下所示。

    /// <summary>
    /// 数据访问层的基类
    /// </summary>
    public partial class BaseDAL<T> where T : BaseEntity, new()

利用泛型的方式,把数据访问层的接口提出来,并引入了数据访问层的基类进行实现和重用接口,如下所示。

基于新接口,如获取数据库对象的操作,则利用了IMongoDatabase的接口了,如下所示。

            var client = new MongoClient(connectionString);
            var database = client.GetDatabase(new MongoUrl(connectionString).DatabaseName);

相对以前的常规接口,MongoClient对象已经没有了GetServer的接口了。如果对创建数据库对象的操作做更好的封装,可以利用配置文件进行指定的话,那么方法可以封装如下所示。

复制代码
        /// <summary>
        /// 根据数据库配置信息创建MongoDatabase对象,如果不指定配置信息,则从默认信息创建
        /// </summary>
        /// <param name="databaseName">数据库名称,默认空为local</param>
        /// <returns></returns>
        protected virtual IMongoDatabase CreateDatabase()
        {
            string connectionString = null;
            if (!string.IsNullOrEmpty(dbConfigName))
            {
                //从配置文件中获取对应的连接信息
                connectionString = ConfigurationManager.ConnectionStrings[dbConfigName].ConnectionString;                
            }
            else
            {
                connectionString = defaultConnectionString;
            }

            var client = new MongoClient(connectionString);
            var database = client.GetDatabase(new MongoUrl(connectionString).DatabaseName);

            return database;
        }
复制代码

根据IMongoDatabase 接口,那么其获取集合对象的操作如下所示,它使用了另外一个定义IMongoCollection了。

复制代码
        /// <summary>
        /// 获取操作对象的IMongoCollection集合,强类型对象集合
        /// </summary>
        /// <returns></returns>
        public virtual IMongoCollection<T> GetCollection()
        {
            var database = CreateDatabase();
            return database.GetCollection<T>(this.entitysName);
        }
复制代码

2、查询单个对象实现封装处理

基于新接口的查询处理,已经没有FindOne的方法定义了,只是使用了Find的方法,而且也没有了Query的对象可以作为条件进行处理,而是采用了新的定义对象FilterDefinition,例如对于根据ID查询单个对象,接口的实现如下所示。

复制代码
        /// <summary>
        /// 查询数据库,检查是否存在指定ID的对象
        /// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual T FindByID(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            return collection.Find(s=> s.Id == id).FirstOrDefault();
        }
复制代码

对于利用FilterDefinition进行查询的操作,如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,如果存在返回第一个对象
        /// </summary>
        /// <param name="filter">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual T FindSingle(FilterDefinition<T> filter)
        {
            IMongoCollection<T> collection = GetCollection();
            return collection.Find(filter).FirstOrDefault();
        }
复制代码

我们可以看到,这些都是利用Find方法的不同重载实现不同条件的处理的。

对于这个新接口,异步是一个重要的改变,那么它的异步处理是如何的呢,我们看看上面两个异步的实现操作,具体代码如下所示。

复制代码
        /// <summary>
        /// 查询数据库,检查是否存在指定ID的对象(异步)
        /// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual async Task<T> FindByIDAsync(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            return await collection.FindAsync(s=>s.Id == id).Result.FirstOrDefaultAsync(); 
        }

        /// <summary>
        /// 根据条件查询数据库,如果存在返回第一个对象(异步)
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual async Task<T> FindSingleAsync(FilterDefinition<T> query)
        {
            return await GetQueryable(query).SingleOrDefaultAsync();
        }
复制代码

我们看到,上面的Collection或者GetQueryable(query)返回的对象,都提供给了以Async结尾的异步方法,因此对异步的封装也是非常方便的,上面的GetQueryable(query)是另外一个公共的实现方法,具体代码如下所示。

复制代码
        /// <summary>
        /// 返回可查询的记录源
        /// </summary>
        /// <param name="query">查询条件</param>
        /// <returns></returns>
        public virtual IFindFluent<T, T> GetQueryable(FilterDefinition<T> query)
        {
            return GetQueryable(query, this.SortPropertyName, this.IsDescending);
        }
复制代码
复制代码
        /// <summary>
        /// 根据条件表达式返回可查询的记录源
        /// </summary>
        /// <param name="query">查询条件</param>
        /// <param name="sortPropertyName">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IFindFluent<T,T> GetQueryable(FilterDefinition<T> query, string sortPropertyName, bool isDescending = true)
        {
            IMongoCollection<T> collection = GetCollection();
            IFindFluent<T, T> queryable = collection.Find(query);

            var sort = this.IsDescending ? Builders<T>.Sort.Descending(this.SortPropertyName) : Builders<T>.Sort.Ascending(this.SortPropertyName);
            return queryable.Sort(sort);
        }
复制代码

我们可以看到,它返回了IFindFluent<T, T>的对象,这个和以前返回的IMongoQuery对象又有不同,基本上,使用最新的接口,所有的实现都不太一样,这也是因为MongoDB还在不停变化之中有关。

3、GetQueryable几种方式

为了简化代码,方便使用,我们对获取MongoDB的LINQ方式的处理做了简单的封装,提供了几个GetQueryable的方式,具体代码如下所示。

复制代码
        /// <summary>
        /// 返回可查询的记录源
        /// </summary>
        /// <returns></returns>
        public virtual IQueryable<T> GetQueryable()
        {
            IMongoCollection<T> collection = GetCollection();
            IQueryable<T> query = collection.AsQueryable();

            return query.OrderBy(this.SortPropertyName, this.IsDescending);
        }
复制代码
复制代码
        /// <summary>
        /// 根据条件表达式返回可查询的记录源
        /// </summary>
        /// <param name="match">查询条件</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IQueryable<T> GetQueryable<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true)
        {
            IMongoCollection<T> collection = GetCollection();
            IQueryable<T> query = collection.AsQueryable();

            if (match != null)
            {
                query = query.Where(match);
            }

            if (orderByProperty != null)
            {
                query = isDescending ? query.OrderByDescending(orderByProperty) : query.OrderBy(orderByProperty);
            }
            else
            {
                query = query.OrderBy(this.SortPropertyName, isDescending);
            }
            return query;
        }
复制代码

以及基于FilterDefinition的条件处理,并返回IFindFluent<T,T>接口对象的代码如下所示。

复制代码
        /// <summary>
        /// 根据条件表达式返回可查询的记录源
        /// </summary>
        /// <param name="query">查询条件</param>
        /// <param name="sortPropertyName">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IFindFluent<T,T> GetQueryable(FilterDefinition<T> query, string sortPropertyName, bool isDescending = true)
        {
            IMongoCollection<T> collection = GetCollection();
            IFindFluent<T, T> queryable = collection.Find(query);

            var sort = this.IsDescending ? Builders<T>.Sort.Descending(this.SortPropertyName) : Builders<T>.Sort.Ascending(this.SortPropertyName);
            return queryable.Sort(sort);
        }
复制代码

4、集合的查询操作封装处理

基于上面的封装,对结合的查询,也是基于不同的条件进行处理,返回对应的列表的处理方式, 最简单的是利用GetQueryable方式进行处理,代码如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> Find(Expression<Func<T, bool>> match)
        {
            return GetQueryable(match).ToList();
        }
复制代码

或者如下所示

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> Find(FilterDefinition<T> query)
        {
            return GetQueryable(query).ToList();
        }
复制代码

以及对排序字段,以及升降序的处理操作如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IList<T> Find<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true)
        {
            return GetQueryable<TKey>(match, orderByProperty, isDescending).ToList();
        }

        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <param name="orderByProperty">排序字段</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IList<T> Find<TKey>(FilterDefinition<T> query, string orderByProperty, bool isDescending = true)
        {
            return GetQueryable(query, orderByProperty, isDescending).ToList();
        }
复制代码

以及利用这些条件进行分页的处理代码如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> FindWithPager(Expression<Func<T, bool>> match, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            IQueryable<T> query = GetQueryable(match);
            info.RecordCount = query.Count();

            return query.Skip(excludedRows).Take(pageSize).ToList();
        }

        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> FindWithPager(FilterDefinition<T> query, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            var find = GetQueryable(query);
            info.RecordCount = (int)find.Count();

            return find.Skip(excludedRows).Limit(pageSize).ToList();
        }
复制代码

对于异步的封装处理,基本上也和上面的操作差不多,例如对于基础的查询,异步操作封装如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual async Task<IList<T>> FindAsync(Expression<Func<T, bool>> match)
        {
            return await Task.FromResult(GetQueryable(match).ToList());
        }

        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual async Task<IList<T>> FindAsync(FilterDefinition<T> query)
        {
            return await GetQueryable(query).ToListAsync();
        }
复制代码

复杂一点的分页处理操作代码封装如下所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual async Task<IList<T>> FindWithPagerAsync(Expression<Func<T, bool>> match, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            IQueryable<T> query = GetQueryable(match);
            info.RecordCount = query.Count();

            var result = query.Skip(excludedRows).Take(pageSize).ToList();
            return await Task.FromResult(result);
        }

        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual async Task<IList<T>> FindWithPagerAsync(FilterDefinition<T> query, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            var queryable = GetQueryable(query);
            info.RecordCount = (int)queryable.Count();

            return await queryable.Skip(excludedRows).Limit(pageSize).ToListAsync();
        }
复制代码

5、增删改方法封装处理

对于常规的增删改操作,在新的MongoDB数据库驱动里面也修改了名称,使用的时候也需要进行调整处理了。

复制代码
        /// <summary>
        /// 插入指定对象到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        public virtual void Insert(T t)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");

            IMongoCollection<T> collection = GetCollection();
            collection.InsertOne(t);
        }
复制代码

异步的操作实现如下所示。

复制代码
        /// <summary>
        /// 插入指定对象到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        public virtual async Task InsertAsync(T t)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");

            IMongoCollection<T> collection = GetCollection();
            await collection.InsertOneAsync(t);
        }
复制代码

批量插入记录的操作如下所示。

复制代码
        /// <summary>
        /// 插入指定对象集合到数据库中
        /// </summary>
        /// <param name="list">指定的对象集合</param>
        public virtual void InsertBatch(IEnumerable<T> list)
        {
            ArgumentValidation.CheckForNullReference(list, "传入的对象list为空");

            IMongoCollection<T> collection = GetCollection();
            collection.InsertMany(list);
        }
复制代码

对应的异步操作处理如下所示,这些都是利用原生支持的异步处理接口实现的。

复制代码
        /// <summary>
        /// 插入指定对象集合到数据库中
        /// </summary>
        /// <param name="list">指定的对象集合</param>
        public virtual async Task InsertBatchAsync(IEnumerable<T> list)
        {
            ArgumentValidation.CheckForNullReference(list, "传入的对象list为空");

            IMongoCollection<T> collection = GetCollection();
            await collection.InsertManyAsync(list);
        }
复制代码

更新操作,有一种整个替换更新,还有一个是部分更新,它们两者是有区别的,如对于替换更新的操作,它的接口封装处理如下所示

复制代码
        /// <summary>
        /// 更新对象属性到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="id">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(T t, string id)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            bool result = false;
            IMongoCollection<T> collection = GetCollection();
            //使用 IsUpsert = true ,如果没有记录则写入
            var update = collection.ReplaceOne(s => s.Id == id, t, new UpdateOptions() { IsUpsert = true });
            result = update != null && update.ModifiedCount > 0;

            return result;
        }
复制代码

如果对于部分字段的更新,那么操作如下所示 ,主要是利用UpdateDefinition对象来指定需要更新那些字段属性及值等信息。

复制代码
        /// <summary>
        /// 封装处理更新的操作(部分字段更新)
        /// </summary>
        /// <param name="id">主键的值</param>
        /// <param name="update">更新对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(string id, UpdateDefinition<T> update)
        {
            ArgumentValidation.CheckForNullReference(update, "传入的对象update为空");
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            var result = collection.UpdateOne(s => s.Id == id, update, new UpdateOptions() { IsUpsert = true });
            return result != null && result.ModifiedCount > 0;
        }
复制代码

上面的异步更新操作如下所示。

复制代码
        /// <summary>
        /// 封装处理更新的操作(部分字段更新)
        /// </summary>
        /// <param name="id">主键的值</param>
        /// <param name="update">更新对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual async Task<bool> UpdateAsync(string id, UpdateDefinition<T> update)
        {
            ArgumentValidation.CheckForNullReference(update, "传入的对象update为空");
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            var result = await collection.UpdateOneAsync(s => s.Id == id, update, new UpdateOptions() { IsUpsert = true });

            var sucess = result != null && result.ModifiedCount > 0;
            return await Task.FromResult(sucess);
        }
复制代码

删除的操作也是类似的了,基本上和上面的处理方式接近,顺便列出来供参考学习。

复制代码
        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定对象
        /// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool Delete(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            IMongoCollection<T> collection = GetCollection();
            var result = collection.DeleteOne(s=> s.Id == id);
            return result != null && result.DeletedCount > 0;
        }

        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定指定的对象
        /// </summary>
        /// <param name="idList">对象的ID集合</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool DeleteBatch(List<string> idList)
        {
            ArgumentValidation.CheckForNullReference(idList, "传入的对象idList为空");

            IMongoCollection<T> collection = GetCollection();
            var query = Query.In("_id", new BsonArray(idList));
            var result = collection.DeleteMany(s => idList.Contains(s.Id));
            return result != null && result.DeletedCount > 0;
        }
复制代码

如果根据条件的删除,也可以利用条件定义的两种方式,具体代码如下所示。

复制代码
        /// <summary>
        /// 根据指定条件,从数据库中删除指定对象
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool DeleteByExpression(Expression<Func<T, bool>> match)
        {
            IMongoCollection<T> collection = GetCollection();
            collection.AsQueryable().Where(match).ToList().ForEach(s => collection.DeleteOne(t => t.Id == s.Id));
            return true;
        }

        /// <summary>
        /// 根据指定条件,从数据库中删除指定对象
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool DeleteByQuery(FilterDefinition<T> query)
        {
            IMongoCollection<T> collection = GetCollection();
            var result = collection.DeleteMany(query);
            return result != null && result.DeletedCount > 0;
        }
复制代码

6、数据访问子类的封装和方法调用

好了,基本上上面大多数使用的方法都发布出来了,封装的原则就是希望数据访问层子类能够简化代码,减少不必要的复制粘贴,而且必要的时候, 也可以对具体的接口进行重写,实现更强大的处理控制。

例如对于上面的基类,我们在具体的集合对象封装的时候,需要继承于BaseDAL<T>这样的方式,这样可以利用基类丰富的接口,简化子类的代码,如User集合类的代码如下所示。

复制代码
   /// <summary>
    /// User集合(表)的数据访问类
    /// </summary>
    public class User : BaseDAL<UserInfo>
    {
        /// <summary>
        /// 默认构造函数
        /// </summary>
        public User() 
        {
            this.entitysName = "users";//对象在数据库的集合名称
        }

        /// <summary>
        /// 为用户增加岁数
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <param name="addAge">待增加的岁数</param>
        /// <returns></returns>
        public bool IncreaseAge(string id, int addAge)
        {
            var collection = GetCollection();
            var update = Builders<UserInfo>.Update.Inc(s => s.Age, addAge);
            var result = collection.UpdateOne(s => s.Id == id, update);
            return result != null && result.ModifiedCount > 0;
        }

        /// <summary>
        /// 单独修改用户的名称
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <param name="newName">用户新名称</param>
        /// <returns></returns>
        public bool UpdateName(string id, string newName)
        {
            var collection = GetCollection();
            var update = Builders<UserInfo>.Update.Set(s => s.Name, newName);
            var result = collection.UpdateOne(s => s.Id == id, update);
            return result != null && result.ModifiedCount > 0;
        }
    }
复制代码

在界面层使用的时候,只需要声明一个对应的User数据访问类dal对象,就可以利用它的相关接口进行对应的数据操作了,如下代码所示。

            IList<UserInfo> members = dal.Find(s => s.Name.StartsWith("Test"));
            foreach (UserInfo info in members)
            {
                Console.WriteLine(info.Id + ", " + info.Name);
            }
            var user = dal.FindSingle(s => s.Id == "56815e6634ab091e1406ec68");
            if(user != null)
            {
                Console.WriteLine(user.Name);
            }

对于部分字段的更新处理,在界面上,我们可以利用封装好的接口进行处理,如下所示。

复制代码
        /// <summary>
        /// 测试部分字段修改的处理
        /// </summary>
        private void btnAddAge_Click(object sender, EventArgs e)
        {
            UserInfo info = dal.GetAll()[0];
            if(info != null)
            {
                Console.WriteLine("Age before Incr:" + info.Age);

                int addAge = 10;
                dal.IncreaseAge(info.Id, addAge);

                info = dal.FindByID(info.Id);
                Console.WriteLine("Age after Incr:" + info.Age);


                Console.WriteLine("Name before modify:" + info.Name);
                var update = Builders<UserInfo>.Update.Set(s => s.Name, info.Name + DateTime.Now.Second);
                dal.Update(info.Id, update);

                info = dal.FindByID(info.Id);
                Console.WriteLine("Name after modify:" + info.Name);
            }
        }
复制代码

对于异步接口的调用代码,如下所示。

复制代码
        /// <summary>
        /// 异步操作的调用
        /// </summary>
        private async void btnAsync_Click(object sender, EventArgs e)
        {
            UserInfo newInfo = new UserInfo();
            newInfo.Name = "Ping" + DateTime.Now.ToString();
            newInfo.Age = DateTime.Now.Minute;
            newInfo.Hobby = "乒乓球";
            await dal.InsertAsync(newInfo);

            var list = await dal.FindAsync(s => s.Age < 30);
            foreach (UserInfo info in list)
            {
                Console.WriteLine(info.Id + ", " + info.Name);
            }
            Console.WriteLine(newInfo.Id);
        }
复制代码

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

基于C#的MongoDB数据库开发应用(2)--MongoDB数据库的C#开发 - 伍华聪 - 博客园

mikel阅读(745)

来源: 基于C#的MongoDB数据库开发应用(2)–MongoDB数据库的C#开发 – 伍华聪 – 博客园

在上篇博客《基于C#的MongoDB数据库开发应用(1)–MongoDB数据库的基础知识和使用》里面,我总结了MongoDB数据库的一些基础信息,并在最后面部分简单介绍了数据库C#驱动的开发 ,本文继续这个主题,重点介绍MongoDB数据库C#方面的使用和封装处理过程,利用泛型和基类对象针对数据访问层进行的封装处理。

前面介绍到,当前2.2版本的数据库C#驱动的API,支持两种不同的开发接口,一个是基于MongoDatabase的对象接口,一个是IMongoDatabase的对象接口,前者中规中矩,和我们使用Shell里面的命令名称差不多;后者IMongoDatabase的接口是基于异步的,基本上和前者差别很大,而且接口都提供了异步的处理操作。

本文主要介绍基于MongoDatabase的对象接口的封装处理设置。

1、数据访问层的设计

在结合MongoDB数据库的C#驱动的特点,使用泛型和继承关系,把常规的处理接口做了抽象性的封装,以便封装适合更多业务的接口,减少子类代码及统一API的接口名称。

首先我们来看看大概的设计思路,我们把实体类抽象一个实体基类,方便使用。

我们知道,在MongoDB数据库的集合里面,都要求文档有一个_id字段,这个是强制性的,而且这个字段的存储类型为ObjectId类型,这个值考虑了分布式的因素,综合了机器码,进程,时间戳等等方面的内容,它的构造如下所示。

ObjectId是一个12字节的  BSON 类型字符串。按照字节顺序,依次代表:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值

实体基类BaseEntity包含了一个属性Id,这个是一个字符串型的对象(也可以使用ObjectId类型,但是为了方便,我们使用字符型,并声明为ObjectId类型即可),由于我们声明了该属性对象为ObjectId类型,那么我们就可以在C#代码里面使用字符串的ID类型了,代码如下所示。

复制代码
    /// <summary>
    /// MongoDB实体类的基类
    /// </summary>
    public class BaseEntity
    {        
        /// <summary>
        /// 基类对象的ID,MongoDB要求每个实体类必须有的主键
        /// </summary>
        [BsonRepresentation(BsonType.ObjectId)]        
        public string Id { get; set; }
    }
复制代码

然后利用泛型的方式,把数据访问层的接口提出来,并引入了数据访问层的基类进行实现和重用接口,如下所示。

其中,上面几个类的定义如下所示。

数据访问层基类BaseDAL的类定义如下所示,主要就是针对上面的IBaseDAL<T>接口进行实现。

有了这些基类的实现,我们对于实体类的处理就非常方便、统一了,基本上不需要在复制大量的代码来实现基础的增删改查分页实现了。

例如上面的User集合(表对象)的数据访问类定义如下所示,在对象的定义的时候,指定对应的实体类,并在构造函数里面指定对应的集合名称就可以实例化一个数据访问类了。

复制代码
    /// <summary>
    /// 数据表User对应的具体数据访问类
    /// </summary>
    public class User : BaseDAL<UserInfo>, IBaseDAL<UserInfo>
    {
        /// <summary>
        /// 默认构造函数
        /// </summary>
        public User() 
        {
            this.entitysName = "users";//对象在数据库的集合名称
        }

        .................
复制代码

2、基类各种接口的实现

前面小节我们介绍了实体基类,数据访问层基类接口和基类实现,以及具体集合对象的实现类的定义关系,通过泛型和继承关系,我们很好的抽象了各种对象的增删改查、分页等操作,子类继承了BaseDAL基类后,就自然而然的具有了非常强大的接口处理功能了。下面我们来继续详细介绍基于C#驱动的MongoDB数据库是如何进行各种增删改查等封装的。

1)构造MongoDatabase对象

首先我们需要利用连接字符串来构建MongoDatabase对象,因为所有的接口都是基于这个对象进行处理的,代码如下所示。

复制代码
        /// <summary>
        /// 根据数据库配置信息创建MongoDatabase对象,如果不指定配置信息,则从默认信息创建
        /// </summary>
        /// <param name="databaseName">数据库名称,默认空为local</param>
        /// <returns></returns>
        protected virtual MongoDatabase CreateDatabase()
        {
            string connectionString = null;

            if (!string.IsNullOrEmpty(dbConfigName))
            {
                //从配置文件中获取对应的连接信息
                connectionString = ConfigurationManager.ConnectionStrings[dbConfigName].ConnectionString;                
            }
            else
            {
                connectionString = defaultConnectionString;
            }

            var client = new MongoClient(connectionString);
            var database = client.GetServer().GetDatabase(new MongoUrl(connectionString).DatabaseName);

            return database;
        }
复制代码

2)构建MongoCollection对象

上面构建了MongoDatabase对象后,我们需要基于这个基础上再创建一个对象的MongoCollection对象,这个就是类似我们关系数据库里面的表对象的原型了。

复制代码
        /// <summary>
        /// 获取操作对象的MongoCollection集合,强类型对象集合
        /// </summary>
        /// <returns></returns>
        protected virtual MongoCollection<T> GetCollection()
        {
            MongoDatabase database = CreateDatabase();
            return database.GetCollection<T>(this.entitysName);
        }
复制代码

3)查询单个对象

利用MongoCollection对象,我们可以通过API接口获取对应的对象,单个对象的接口为FindOneById(也可以用FindOne接口,如注释部分的代码),我们具体的处理代码如下所示

复制代码
        /// <summary>
        /// 查询数据库,检查是否存在指定ID的对象
        /// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual T FindByID(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            MongoCollection<T> collection = GetCollection();
            return collection.FindOneById(new ObjectId(id)); //FindOne(Query.EQ("_id", new ObjectId(id)));
        }
复制代码

如果基于条件的单个记录查询,我们可以使用Expression<Func<T, bool>>和IMongoQuery的参数进行处理,如下代码所示。

复制代码
        /// <summary>
        /// 根据条件查询数据库,如果存在返回第一个对象
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual T FindSingle(Expression<Func<T, bool>> match)
        {
            MongoCollection<T> collection = GetCollection();
            return collection.AsQueryable().Where(match).FirstOrDefault();
        }

        /// <summary>
        /// 根据条件查询数据库,如果存在返回第一个对象
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual T FindSingle(IMongoQuery query)
        {
            MongoCollection<T> collection = GetCollection();
            return collection.FindOne(query);
        }
复制代码

4)IQueryable的接口利用

使用过EF的实体框架的话,我们对其中的IQueryable<T>印象很深刻,它可以给我提供很好的LINQ语法获取对应的信息,它可以通过使用Expression<Func<T, bool>>和IMongoQuery的参数来进行条件的查询操作,MongoCollection对象有一个AsQueryable()的API进行转换,如下所示。

复制代码
        /// <summary>
        /// 返回可查询的记录源
        /// </summary>
        /// <returns></returns>
        public virtual IQueryable<T> GetQueryable()
        {
            MongoCollection<T> collection = GetCollection();
            IQueryable<T> query = collection.AsQueryable();

            return query.OrderBy(this.SortPropertyName, this.IsDescending);
        }
复制代码

如果是通过使用Expression<Func<T, bool>>和IMongoQuery的参数,那么处理的接口代码如下所示。

复制代码
        /// <summary>
        /// 根据条件表达式返回可查询的记录源
        /// </summary>
        /// <param name="match">查询条件</param>
        /// <param name="sortPropertyName">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true)
        {
            MongoCollection<T> collection = GetCollection();
            IQueryable<T> query = collection.AsQueryable();
            if (match != null)
            {
                query = query.Where(match);
            }
            return query.OrderBy(sortPropertyName, isDescending);
        }
复制代码
复制代码
        /// <summary>
        /// 根据条件表达式返回可查询的记录源
        /// </summary>
        /// <param name="query">查询条件</param>
        /// <param name="sortPropertyName">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        public virtual IQueryable<T> GetQueryable(IMongoQuery query, string sortPropertyName, bool isDescending = true)
        {
            MongoCollection<T> collection = GetCollection();
            IQueryable<T> queryable = collection.Find(query).AsQueryable();

            return queryable.OrderBy(sortPropertyName, isDescending);
        }
复制代码

5)集合的查询处理

通过利用上面的IQueryable<T>对象,以及使用Expression<Func<T, bool>>和IMongoQuery的参数,我们很好的进行集合的查询处理操作的了,具体代码如下所示

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> Find(Expression<Func<T, bool>> match)
        {
            return GetQueryable(match).ToList();
        }

        /// <summary>
        /// 根据条件查询数据库,并返回对象集合
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> Find(IMongoQuery query)
        {
            MongoCollection<T> collection = GetCollection();
            return collection.Find(query).ToList();
        }
复制代码

对于分页,我们是非常需要的,首先在大数据的集合里面,我们不可能一股脑的把所有的数据全部返回,因此根据分页参数返回有限数量的集合处理就是我们应该做的,分页的操作代码和上面很类似,只是利用了Skip和Take的接口,返回我们需要的记录数量就可以了。

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> FindWithPager(Expression<Func<T, bool>> match, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            IQueryable<T> query = GetQueryable(match);
            info.RecordCount = query.Count();

            return query.Skip(excludedRows).Take(pageSize).ToList();
        }
复制代码

或者是下面的代码

复制代码
        /// <summary>
        /// 根据条件查询数据库,并返回对象集合(用于分页数据显示)
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <returns>指定对象的集合</returns>
        public virtual IList<T> FindWithPager(IMongoQuery query, PagerInfo info)
        {
            int pageindex = (info.CurrenetPageIndex < 1) ? 1 : info.CurrenetPageIndex;
            int pageSize = (info.PageSize <= 0) ? 20 : info.PageSize;

            int excludedRows = (pageindex - 1) * pageSize;

            IQueryable<T> queryable = GetQueryable(query);
            info.RecordCount = queryable.Count();

            return queryable.Skip(excludedRows).Take(pageSize).ToList();
        }
复制代码

6)对象的写入操作

对象的写入可以使用save,它是根据_id的来决定插入还是更新的,如下代码所示。

复制代码
        /// <summary>
        /// 保存指定对象到数据库中,根据Id的值,决定是插入还是更新
        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功指定对象信息</returns>
        public virtual T Save(T t)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");

            MongoCollection<T> collection = GetCollection();
            var result = collection.Save(t);
            return t;
        }
复制代码

插入记录就可以利用insert方法进行处理的,代码如下所示。

复制代码
        /// <summary>
        /// 插入指定对象到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Insert(T t)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");

            MongoCollection<T> collection = GetCollection();
            var result = collection.Insert(t);
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

如果是批量插入,可以利用它的insertBatch的方法进行处理,具体代码如下所示。

复制代码
        /// <summary>
        /// 插入指定对象集合到数据库中
        /// </summary>
        /// <param name="list">指定的对象集合</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool InsertBatch(IEnumerable<T> list)
        {
            ArgumentValidation.CheckForNullReference(list, "传入的对象list为空");

            MongoCollection<T> collection = GetCollection();
            var result = collection.InsertBatch(list);
            return result.Any(s => s != null && s.DocumentsAffected > 0); //部分成功也返回true
        }
复制代码

7)对象的更新操作

更新操作分为了两个不同的部分,一个是全部的记录更新,也就是整个JSON的替换操作了,一般我们是在原来的基础上进行更新的,如下代码所示。

复制代码
        /// <summary>
        /// 更新对象属性到数据库中
        /// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="id">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(T t, string id)
        {
            ArgumentValidation.CheckForNullReference(t, "传入的对象t为空");
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            bool result = false;
            MongoCollection<T> collection = GetCollection();
            var existing = FindByID(id);
            if (existing != null)
            {
                var resultTmp = collection.Save(t);
                result = resultTmp != null && resultTmp.DocumentsAffected > 0;
            }

            return result;
        }
复制代码

还有一种方式是部分更新,也就是更新里面的指定一个或几个字段,不会影响其他字段,也就不会全部替换掉其他内容的操作了。这里利用了一个UpdateBuilder<T>的对象,用来指定那些字段需要更新,以及这些字段的值内容的,具体的更新代码如下所示。

复制代码
        /// <summary>
        /// 封装处理更新的操作
        /// </summary>
        /// <param name="id">主键的值</param>
        /// <param name="update">更新对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(string id, UpdateBuilder<T> update)
        {
            ArgumentValidation.CheckForNullReference(update, "传入的对象update为空");
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            var query = Query.EQ("_id", new ObjectId(id));
            MongoCollection<T> collection = GetCollection();
            var result = collection.Update(query, update);
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

部分更新,可以结合使用Inc和Set方法来进行处理,如下是我在子类里面利用到上面的Update部分更新的API进行处理个别字段的更新操作。

复制代码
        /// <summary>
        /// 为用户增加岁数
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <param name="addAge">待增加的岁数</param>
        /// <returns></returns>
        public bool IncreaseAge(string id, int addAge)
        {
            //增加指定的岁数
            var query = Query<UserInfo>.EQ(s => s.Id, id);
            var update = Update<UserInfo>.Inc(s => s.Age, addAge);

            var collection = GetCollection();
            var result = collection.Update(query, update);
            return result != null && result.DocumentsAffected > 0;
        }

        /// <summary>
        /// 单独修改用户的名称
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <param name="newName">用户新名称</param>
        /// <returns></returns>
        public bool UpdateName(string id, string newName)
        {
            //增加指定的岁数
            var query = Query<UserInfo>.EQ(s => s.Id, id);
            var update = Update<UserInfo>.Set(s => s.Name, newName);

            var collection = GetCollection();
            var result = collection.Update(query, update);
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

8)对象的删除操作

对象的删除,一般可以利用条件进行删除,如单个删除可以使用_id属性进行处理,也可以利用批量删除的接口进行删除操作,代码如下所示。

复制代码
        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定对象
        /// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool Delete(string id)
        {
            ArgumentValidation.CheckForEmptyString(id, "传入的对象id为空");

            MongoCollection<T> collection = GetCollection();
            //var result = collection.Remove(Query<T>.EQ(s => s.Id, id));
            var result = collection.Remove(Query.EQ("_id", new ObjectId(id)));
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

其中上面注释的var result = collection.Remove(Query<T>.EQ(s => s.Id, id));代码,就是利用了强类型的对象属性和值进行移除,一样可以的。

对于批量删除,可以利用Query的不同进行处理。

复制代码
        /// <summary>
        /// 根据指定对象的ID,从数据库中删除指定指定的对象
        /// </summary>
        /// <param name="idList">对象的ID集合</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool DeleteBatch(List<string> idList)
        {
            ArgumentValidation.CheckForNullReference(idList, "传入的对象idList为空");

            MongoCollection<T> collection = GetCollection();
            var query = Query.In("_id", new BsonArray(idList));
            var result = collection.Remove(query);
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

或者基于IMongoQuery的条件进行处理。

复制代码
        /// <summary>
        /// 根据指定条件,从数据库中删除指定对象
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c>。</returns>
        public virtual bool DeleteByQuery(IMongoQuery query)
        {
            MongoCollection<T> collection = GetCollection();
            var result = collection.Remove(query);
            return result != null && result.DocumentsAffected > 0;
        }
复制代码

9)其他相关接口

一般除了上面的接口,还有一些其他的接口,如获取记录的总数、判断条件的记录是否存在等也是很常见的,他们的代码封装如下所示。

复制代码
        /// <summary>
        /// 获取表的所有记录数量
        /// </summary>
        /// <returns></returns>
        public virtual int GetRecordCount()
        {
            return GetQueryable().Count();
        }
复制代码
复制代码
        /// <summary>
        /// 根据查询条件,判断是否存在记录
        /// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns></returns>
        public virtual bool IsExistRecord(Expression<Func<T, bool>> match)
        {
            return GetQueryable(match).Any();//.Count() > 0
        }

        /// <summary>
        /// 根据查询条件,判断是否存在记录
        /// </summary>
        /// <param name="query">条件表达式</param>
        /// <returns></returns>
        public virtual bool IsExistRecord(IMongoQuery query)
        {
            return GetQueryable(query).Any();//.Count() > 0
        }
复制代码

 

非常感谢您的详细阅读,以上基本上就是我对整个MongoDB数据库的各个接口的基类封装处理了,其中已经覆盖到了基础的增删改查、分页等操作接口,以及一些特殊的条件处理接口的扩展,我们利用这些封装好的基类很好的简化了子类的代码,而且可以更方便的在基类的基础上进行特殊功能的扩展处理。

当然,以上介绍的都不是最新的接口,是2.0(或2.2)版本之前的接口实现,虽然在2.2里面也还可以利用上面的MongoDatabase对象接口,但是IMongoDatabase最新的接口已经全面兼容异步的操作,但也是一个很大的跳跃,基本上引入了不同的接口命名和处理方式,利用异步可以支持更好的处理体验,但是也基本上是需要对所有的接口进行了全部的重写了。

下一篇我会专门介绍一下基于最新的异步接口如何实现这些常规增删改查、分页等的基类实现封装处理。

 

主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
转载请注明出处:
撰写人:伍华聪  http://www.iqidi.com

MVC之实现基于token的认证 - CN_Simo - 博客园

mikel阅读(840)

来源: MVC之实现基于token的认证 – CN_Simo – 博客园

安装Nuget包

项目中添加包:dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

添加认证配置

Startup类中添加如下配置:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddAuthentication(defaultScheme: JwtBearerDefaults.AuthenticationScheme);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    }); 
}

AddAuthentication方法会向依赖注入容器添加认证服务和它所使用的其他服务,其参数defaultScheme用于指定当未指定具体的认证方案时将会使用的默认方案,上例为Bearer认证。

AddAuthentication方法的另一重载能够使用AuthenticationOptions类为认证过程中的每一个动作指明所使用的认证方案,如DefaultAuthenticateScheme、
DefaultChallengeScheme、
DefaultSignInScheme、
DefaultSignOutScheme、
DefaultForbidScheme。
如果没有为这些属性设置认证方案,则将使用DefaultScheme属性所指定的值。

当添加JwtBearer认证方式时,JwtBearerOptions对象能够配置该认证的选项,它的TokenValidationParameters属性用于指定验证Token时的规则:

复制代码
var tokenSection = Configuration.GetSection("Security:Token");
services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters{
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuer = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = tokenSection["Issuer"],
        ValidAudience = tokenSection["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(tokenSection["Key"])
        ),
        ClockSkew = TimeSpan.Zero
    };
});

TokenValidationParameters类作为Token验证参数类,它包含了一些属性,这些属性如ValidateAudience、ValidateIssuer、ValidateLifetime和ValidateIssuerSigningKey,它们都是布尔类型,用于指明是否验证相应的项;而ValidIssuer和ValidAudience属性则用于指明合法的签发者(Issuer)与接受方(Audience)。在上例中,它们的值都从配置文件中获取;IssuerSigningKey属性的值用于指定进行签名验证的安全密钥,它的值为SymmetricSecurityKey对象,即对称加密密钥;ClockSkew属性的值表示验证时间的时间偏移值。

上述代码会从配置文件中读取关于Token的信息,因此还需在appsettings.json中添加如下内容。

复制代码
"Security": {
  "Token": {
    "Issuer": "demo_issuer",
    "Audience": "demo_audience",
    "Key": "<your_secret_key>"
  }
}

为Controller添加认证

接下来,为了使用ASP.NET Core的认证功能来保护资源,应为Controller或Action添加[Authorize]特性,该特性能够实现在访问相应的Controller或Action时,要求请求方提供指定的认证方式,它位于Microsoft.AspNetCore.Authorization命名空间中。需要为AuthorController和BookController添加该特性。

复制代码
[Authorize]
public class AuthorController : ControllerBase
{
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class BookController : ControllerBase
{
}

如果使用了多个认证方式,则可以使用[Authorize]特性的AuthenticationSchemes属性指明当前Controller或Action要使用哪一种认证方式(如上例中的BookController);如果不设置,则会使用所添加认证时设置的默认方案;如果没有设置默认方案,则会出现InvalidOperationException异常,并提示未指定默认方案;此外,如果为AuthenticationSchemes属性指定了不存在的方案名称,也会出现InvalidOperationException异常。

此时再访问Book和Author资源,会出现401 Unauthorized异常:

PaYcnN

如果要允许某个Action可以被匿名访问,可以在Action方法上添加属性标记 [AllowAnonymous]:

复制代码
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<AuthorDto>>> GetAuthorsAsync([FromQuery] AuthorResourceParameters parameters)

添加认证信息生成接口

JwtBearer中间件提供了对JWT的验证功能,然而并未提供生成Token的功能。要生成Token,可以使用JwtSecurityTokenHandler类,它位于System.IdentityModel.Tokens.Jwt命名空间,它不仅能够生成JWT,由于它实现了ISecurityTokenValidator接口,因此对JWT的验证也是由它完成的。接下来,我们将创建一个Controller,它将会根据用户的认证信息生成JWT,并返回给客户端。

在Controllers文件夹中创建一个Controller,名为AuthenticateController,内容如下:

复制代码
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace Library.Api.Controllers
{
    [ApiController, Route("api/auth")]
    public class AuthenticateController : ControllerBase
    {
        public IConfiguration Configuration { get; }
        public AuthenticateController(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        [HttpPost("token", Name = nameof(GenerateToken))]
        public IActionResult GenerateToken(string username, string password)
        {
            if (username != "demouser" || password != "demopassword")
            {
                return Unauthorized();
            }

            var claims = new List<Claim>{
                new Claim(JwtRegisteredClaimNames.Sub, username)
            };

            var tokenConfigSection = Configuration.GetSection("Security:Token");
            var key = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(tokenConfigSection["Key"])
            );
            var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtToken = new JwtSecurityToken(
                issuer: tokenConfigSection["Issuer"],
                audience: tokenConfigSection["Audience"],
                claims: claims,
                expires: DateTime.Now.AddMinutes(3),
                signingCredentials: signCredential
            );

            return Ok(new {
                token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
                expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
            });
        }
    }
}

在AuthenticateController中的GenerateToken方法中,通过创建JwtSecurityToken对象,并使用JwtSecurityTokenHandler对象的WriteToken方法最终得到生成的JWT。当创建JwtSecurityToken对象时,我们可以指定issuer、audience以及当前用户的Claim信息,此外,还可以指定该Token的有效时间。这里需要注意,由于JWT不支持销毁以及撤回功能,因此在设置它的有效时间时,应设置一个较短的时间(如上例中的3分钟),这样可以有效避免Token在意外被窃取后所带来的风险。

现在就可以请求认证接口获取 token:

xrTlOF

这时重新请求资源接口,在请求头中添加Authorization项,值为Bearer ,就可以得到结果了:

CJXHS7

这次示例中,使用了固定的用户名和密码,实际情况中,用户名和密码通常是存在数据库中的,可以使用ASP.NET Core Identity来实现这一功能。

关于请求被挂起页面加载缓慢问题的追查

mikel阅读(724)

本文前戏较多,务实的同学可以直接跳到文末的结论。

由「钢的琴」网友脑洞大开延伸出了吉的他二的胡琵的琶,以及后来许嵩的「苏格拉没有底」,是否可以再拓展一下,得到哥本不爱吃哈根,哈根爱达斯等剧情乱入的关系。

上面跟本文要讨论的主题有什么关系?

没关系。

缘起

有用户反馈内部MIS系统慢,页面加载耗时长。前端同学们开组会提及此事,如何解决慢的问题。

最致命的是:偶发!你不能准确知道它抽风的时间点,无法在想要追查问题的时候必现它。 这只是一方面,另外,慢的可能实在太多了,那么问题来了,是前端导致的还是后端的问题?

对慢的定义也有待商榷,多久算慢?如果这个页面加载大量数据耗时增加那我认为这是正常的。但这个时限超过了一个合理的自然值,就变得不那么正常了,比如四五十秒,一分多钟。

最奇葩的是,如此久的耗时居然不会报超时错误,而是拿到正确返回后将页面呈现了出来!

可能的原因

初步猜测

初步的猜测可能是后端迟迟未返回造成浏览器处于等待状态。这个猜测是很合乎逻辑的,至少能够很合理地解释Chrome Dev Tool 网络面板中我们看到的状态pending。 

但我们不能停留在猜测阶段,要用事实说话,数据才不会骗人。这也正是本文将要展开的。

下面是另外一些被提出来的可能性。

Angular

Angular首当其冲。为什么,因为这个问题出现在后台MIS系统中,且这些系统多用Angular开发。

Angular :怪我咯。

因为问题多出现在基于Angular的MIS系统中,并且Angular的性能一直是被诟病的,所以听到不少的声音将矛头指向Angular。这似乎没什么好庇护的。Angular在整个项目中的前端部分扮演了很重的角色。树大招风,理所当然。

这让我想起初次接触到这个问题时,那是在七月份,芙蓉的爱马仕平台用户反馈了慢的问题,报到前端,顺便看了下,一看Pending状态,觉得是后端未返回。只是情深缘浅当时也没有深入,同时洪堂大神负责去追查了。当时那个系统,很负责地说,没有用Angular。

所以这里可以为Angular正身,将其排除。

内部封装的commonResource

内部对Angular原生的resource进行了封装,做了些数据的转换处理。既然上面Angular都被正身了,那么这里的怀疑也是站不住脚的。

Chrome插件

经查,网上好多呼声有说是Adblock等与网络有关的Chrome插件。可我不使用它已经很多年,那玩意儿太重,后来找到了算法更高级体量更轻便的µBlock。关键是后者也在我使用一段时间后放弃了,因为个人觉悟提高了(此处逼格开始膨胀),免费内容是需要广告支撑的,如果你不希望付费变成强制的话。所以现在一直是处于未开这类插件的状态。那么在未开广告屏蔽插件的情况下重现了问题,可以排除这类插件的影响了。

关于插件,此刻我的Chrome里唯一还会接管Chrome网络的便是代理插件SwitchSharp, 升级之后这货叫Switchy哦卖喝(与时俱进的我当然使用的是后者,此处逼格已经爆表)。

Chrome独家?

因为内部MIS只兼容了Chrome开发,所以不会有在除了Chrome之外的浏览器上使用的场景,并且其他浏览器上面追查问题也是很痛苦的事情。这里仅在火狐里进行了少量尝试,未复现。同时接到反馈,Safari里也未复现。但也不能肯定就只有Chrome存在问题。似乎这个对于问题的解决还不那么重要,所以先不管。

杀毒软件

后面会看到,在追查错误号ERR_CONNECTION_RESET时引出了杀毒软件可能会导致Chrome工作不正常的情况,但这个可能也在稍后被排除人。

并且,我厂使用Mac的同学并没有安装杀软,依然是可以复现的。

重现

第一件事情便是重现。虽然是偶发,为了尽可能保存现场,还是想要手动将它刷出来。天不灭我,经过良久尝试,该问题被复现。于是各种截图,保存请求数据。这个时候还没有开启chrome://net-internals/#events页面来捕获事件日志。

为以后引用方便,这里留下版本信息:

OS: Windows 7 Ultimate

Chrome:Version 39.0.2171.95 m

这是请求Pending时的请求信息: 

这是请求成功返回后: 

可以看到Stalled了1分多钟。神奇的是竟然不报超时错误而是成功返回了。

同时保存了请求头,响应头,还将本次问题请求保存成了CURL等。现场已经留下,感觉Bug不会存活太久了。

接下来就是对比正常请求跟这次异常请求的不同,一轮比较下来,未发现多少异常。

常态与变态的对比

请求头对比:

请求头的对比已丢失,但除了时间外,其余无差别。

响应头对比:

返回结果对比:

上面的对比意义不大,但还是要做的,万一发现有价值的情报了呢。

一次失败的尝试

解决问题时,习惯性地看有没有人已经碰过到类似问题,这样做的好处很明显: 如果有,站在巨人的肩上轻松地牛逼着; 如果没有,这是个机会。

于是信心满满地出发了,因为根据一条互联网准则,70%的问题已经有人解决过了,那些没有被解决的要么是现有技术达不到,要么是未被人发现。所以能够搜索出问题答案的概率还是蛮大的。

经过旷日持久的搜索,有价值的参考寥寥无几。可能是问题本身太过奇葩,遇到的人太少;也有可能问题过于晦涩,无法表述;抑或我搜索的关键词不够精准。 倒也不是说一个都没找到,但一般涉及网络日志的情况就无人问津了,无人问津了!

比如这个,一年多前被人问的,现在还没有一个回答。

还比如这个

Chrome stalls when making multiple requests to same resource?

是后来作为参考的,也是无人问津了……

甚至自己也去问了一个,依然无人问津问津……

神秘的CACHE LOCK

上面提到,Stackoverflow上找到一个问题,跟现在需要解决一有些类似点:

  • 偶发,并不是必然出现的。这里我们的问题也是偶发,很难复现,需要反复刷。
  • 也是请求被Pending了很久,从请求的时间线来看,体现在Stalled上。

这一刻,有一种感觉大概是这样的:

伟大的意大利的左后卫!他继承了意大利的光荣的传统。法切蒂、卡布里尼、马尔蒂尼在这一刻灵魂附体!格罗索一个人他代表了意大利足球悠久的历史和传统,在这一刻他不是一个人在战斗,他不是一个人!

突然看到了希望。该提问到没有给出什么建设性的意见,但它后面的追加编辑却给出了答案。过程是查看Chrome的网络日志,在事件里面发现有一个超时错误:

t=33627 [st= 5] HTTP_CACHE_ADD_TO_ENTRY [dt=20001] –> net_error = -409 (ERR_CACHE_LOCK_TIMEOUT)

耗时20秒之久!而且写得非常明显是ERR_CACHE_LOCK_TIMEOUT。根据提问者贴出来的链接,了解到Chrome有一个缓存锁的机制。

具体源于一个今年6月分实现的一个补丁,加入了这么个机制,而这个机制的引入又源于2010年的一个issue。具体信息可以通过这个这里查看,下面引用如下。

Basically here is the situation:

The site author has a long-lived XHR being used to stream a slow response from the server. This XHR response is cachable (it is just really slow). They kick off the XHR asynchronously, and as data slowly arrives on it, update the progressive load of the webpage. Cool.

Now what happens if you try to load this page in multiple tabs of Chrome is: The first page starts to load just fine, but the second one does nothing. What has happened, is the background XHR of the first page load has acquired an exclusive lock to the cache entry, and the background XHR of the second page is stalled at “Waiting for cache…” trying to get a reader access to the cache entry.

Since the first request can takes minutes, this is a problem.

eroman 同学指出了这么一个事实:

浏览器对一个资源发起请求前,会先检查本地缓存,此时这个请求对该资源对应的缓存的读写是独占的。那么问题来了,试想一下,当我新开一个标签尝试访问同一个资源的时候,这次请求也会去读取这个缓存,假设之前那次请求很慢,耗时很久,那么后来这次请求因为无法获取对该缓存的操作权限就一直处于等待状态。这样很不科学。于是有人建议优化一下。也就是上面所描述的那样。

随着问题的提出,还出了两种可能的实现方案。

(a) [Flexible but complicated] Allow cache readers WHILE writing is in progress. This way the first request could still have exclusive access to the cache entry, but the second request could be streamed the results as they get written to the cache entry. The end result is the second page load would mirror the progress of the first one.

(a) [Naive but simpler] Have a timeout on how long we will block readers waiting for a cache entry before giving up and bypassing the cache.

我猜上面第二个(a)应该是(b)。简单说第一种优化方案更加复杂但科学。之前的请求对缓存仍然是独占的,但随着前一次请求不断对缓存进行更新,可以把已经更新的部分拿给后面的请求读取,这样就不会完全阻塞后面的请求了。

第二种方案则更加简单暴力。给后来的请求设定一个读取缓存超时的时限,如果超过了这个时限,我认为缓存不可用或者本地没有缓存,忽略这一步直接发请求。

于是Chromium的开发者们选择了后者简单的实现。也就是Issue 345643003: Http cache: Implement a timeout for the cache lock 这个提交里的实现。

这个提交的描述如下:

The cache has a single writer / multiple reader lock to avoid downloading the same resource n times. However, it is possible to block many tabs on the same resource, for instance behind an auth dialog.

This CL implements a 20 seconds timeout so that the scenario described in the bug results in multiple authentication dialogs (one per blocked tab) so the user can know what to do. It will also help with other cases when the single writer blocks for a long time.

The timeout is somewhat arbitrary but it should allow medium size resources to be downloaded before starting another request for the same item. The general solution of detecting progress and allow readers to start before the writer finishes should be implemented on another CL.

于是就产生了上面题主遇到的情况。

所以他的解决方法就很明朗了,对请求加个时间戳让其变得唯一,或者服务器响应头设置为无缓存。Both will work!

那么我们的问题也会是这样的么?我幻想由于某种未知的原因造成之前的请求不正常(虽然网络面板里没有数据证明这样的阻塞请求在问题请求之前存在),然后我们的MIS里打开页面时读取不到缓存,卡了,一会儿缓存好了,正常了,于是在等待了几十秒后请求成功发出去了。

似乎不太可能。因为恰好内部MIS系统的响应头里已经加了缓存控制了 Cache-Control: no-cache

以下是一次问题请求的响应头:

HTTP/1.1 200 OK
Date: Wed, 31 Dec 2014 11:47:21 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Pragma: no-cache
Cache-Control: no-cache
tracecode: 28410188240979065866123119
tracecode: 28410188240506537994123119
Server: Apache

并且开多个标签也是无法进行有效重现的。

因此可以排除缓存的干扰。那么似乎这里的缓存锁并不是导致问题的原因,只能另寻他路。不得不说,高兴过后有点失望。

八卦时间

可喜的是,在细细口味了上面缓存机制引入的过程后,真是耐人寻味。这里不妨八卦一下。相信你也注意到了,上面提到,该缓存问题的提出是在2010年,确切地说是Jun 8, 2010。是的,2010年6月8日由eroman 同学提出。但最后针对该问题进行修复的代码提交却是在今年6月份,2014年6月24日,提交时间摆在那里我会乱说?

于是好奇为什么会拖了这么久,遂跟了一下该问题下面的回复看看发生了什么。简直惊呆了。

  • 同月14号,有了首次对这个问题的回复,那是将该问题指派给了rvargas同学。
  • 一个月过去了,也就是7月15号,rvargas同学指出了与该问题关联的另外一个issue「issue 6697
  • 接下来是8月5日,rvargas同学为该问题贴上了标签-Mstone-7 Mstone-8 ,表明将会在里程碑7或者8里面进行修复。但在后面的10月7日,这个日程又被推到了-Mstone-8 Mstone-9 
  • 再接下来11月5日,有人表示以目前的速度及bug数量,还没有时间来修复它,重点在处理优先级为p1的问题上。于是此问题又成功被顺延了,来到-mstone-9 Mstone-10 ,同时优级降为p2。Chromium人手也不够啊,看来。
  • 时间来到12月9日,因为优先级为p2的issue如果没有被标为开始状态的话又自动推到下一个里程碑了,于是顺利来到 -Mstone-10 MovedFrom-10 Mstone-11 。次年2月来到-Mstone-11 Mstone-12 。完成了一次跨年!

…………

  • 上面省略N步。如此反复,最后一次被推到了-Mstone-16 ,那是在2011年10月12日。
  • 时间一晃来到2013年,这一年很平静,前面的几个月都没有人对此问题进行回复。直到11月27日,有人看不下去了,评论道:

This bug has been pushed to the next mstone forever…and is blocking more bugs (e.g https://code.google.com/p/chromium/issues/detail?id=31014)and use-cases same video in 2 tags on one page, and adaptive bit rate html5 video streaming whenever that will kick in. Any chance this will be prioritized?

由于这个bug的无限后延也阻塞了另外一些同类问题,看来是时候解决了。这不,最初的owner 当天就进行了回复:

ecently there was someone looking at giving it another try… I’d have to see if there was any progress there.

If not, I may try to fix it in Q1.

最后一句亮瞎。敢情这之前owner就没有想过要去真正解决似的,因为有其他人在看这个问题了,所以就没管了,如果Q1还没人解决的话,我会出手的!嗯,就是这个意思。

…………

最后,也就是上文提到的,2014年6月,还是rvargas同学对这个问题进行了修复,实现了对缓存读取20秒超时的控制。

该问题就是这样从2010来到2014的。我怀疑Chrome是如何成为版本帝的。

阶段总结

仅有的希望到此似乎都没有了。不过前面的努力也不是没有作何收获,至少我得到了以下有价值的信息:

  • 谷歌的神坛光环不再那么耀眼,他们的产品也是有大堆Bug需要处理的
  • Chrome 处理issue的效率,当然不排除这种大型项目bug数量跟人力完全不匹配的情况
  • 受上面Stackoverflow问题的启发,接下来我将重点转移到了针对出问题请求的日志分析上,并且取得了突破

开始新的征程

虽然上面的努力没能定位到问题,但作为这次对解决这次问题的尝试,还是将它记录了下来,估且称作「旧的回忆」吧。

下面开始「新的征程」。

再次重现

这次受到上面的启发,开启chrome://net-internals/#events页面来捕获事件日志。看是否有错误或异常发生。

再次经过旷日持久的机械操作,重现了!这次,日志在手,天下我有。感觉Bug不会存活多久了。

Chrome Dev Tools 网络面板截图: 

由上面的截图看到,本次出问题的请求总耗时42.74秒。

问题请求的时间线信息截图: 

可以预见,通过捕获的日志完全可以看到Stalled那么久都发生了些什么鬼。

话不多说,切换到事件捕获页面,定位到出问题的请求,查看其详情。同时将该日志导出,永久保存!作为纪念,也方便以后再次导入查看。有兴趣的同学可以访问下方下载后进行导入,就可以清晰地查看到现场了,就好像你亲历了整个犯罪现场一样。

日志还原

  • 下载该日志文件
  • 在Chrome新开一个标签输入chrome://net-internals/#events
  • 切换到Import,选择刚才下载的JSON文件进行导入
  • 切换到Events,定位到http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593 这个请求

此刻右边出现的便是该问题请求的详细日志。

日志解读

下面不妨把日志文件贴出来先:

193486: URL_REQUEST
http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593
Start Time: 2015-01-02 17:51:05.323

t=    1 [st=    0] +REQUEST_ALIVE  [dt=42741]
t=    1 [st=    0]    URL_REQUEST_DELEGATE  [dt=0]
t=    1 [st=    0]   +URL_REQUEST_START_JOB  [dt=42740]
                      --> load_flags = 339804160 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | REPORT_RAW_HEADERS | VERIFY_EV_CERT)
                      --> method = "GET"
                      --> priority = "LOW"
                      --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593"
t=    2 [st=    1]      URL_REQUEST_DELEGATE  [dt=0]
t=    2 [st=    1]      HTTP_CACHE_GET_BACKEND  [dt=0]
t=    2 [st=    1]      HTTP_CACHE_OPEN_ENTRY  [dt=0]
t=    2 [st=    1]      HTTP_CACHE_ADD_TO_ENTRY  [dt=0]
t=    2 [st=    1]      HTTP_CACHE_READ_INFO  [dt=0]
t=    2 [st=    1]      URL_REQUEST_DELEGATE  [dt=0]
t=    2 [st=    1]     +HTTP_STREAM_REQUEST  [dt=2]
t=    4 [st=    3]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                          --> source_dependency = 193488 (HTTP_STREAM_JOB)
t=    4 [st=    3]     -HTTP_STREAM_REQUEST
t=    4 [st=    3]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=0]
t=    4 [st=    3]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS
                          --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1
                              Host: qa.tieba.baidu.com
                              Connection: keep-alive
                              Accept: application/json, text/plain, */*
                              User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
                              Referer: http://qa.tieba.baidu.com/project/
                              Accept-Encoding: gzip, deflate, sdch
                              Accept-Language: en-US,en;q=0.8
                              Cookie: [268 bytes were stripped]
t=    4 [st=    3]     -HTTP_TRANSACTION_SEND_REQUEST
t=    4 [st=    3]     +HTTP_TRANSACTION_READ_HEADERS  [dt=21301]
t=    4 [st=    3]        HTTP_STREAM_PARSER_READ_HEADERS  [dt=21301]
                          --> net_error = -101 (ERR_CONNECTION_RESET)
t=21305 [st=21304]        HTTP_TRANSACTION_RESTART_AFTER_ERROR
                          --> net_error = -101 (ERR_CONNECTION_RESET)
t=21305 [st=21304]     -HTTP_TRANSACTION_READ_HEADERS
t=21305 [st=21304]     +HTTP_STREAM_REQUEST  [dt=3]
t=21307 [st=21306]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                          --> source_dependency = 193494 (HTTP_STREAM_JOB)
t=21308 [st=21307]     -HTTP_STREAM_REQUEST
t=21308 [st=21307]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=3]
t=21308 [st=21307]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS
                          --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1
                              Host: qa.tieba.baidu.com
                              Connection: keep-alive
                              Accept: application/json, text/plain, */*
                              User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
                              Referer: http://qa.tieba.baidu.com/project/
                              Accept-Encoding: gzip, deflate, sdch
                              Accept-Language: en-US,en;q=0.8
                              Cookie: [268 bytes were stripped]
t=21311 [st=21310]     -HTTP_TRANSACTION_SEND_REQUEST
t=21311 [st=21310]     +HTTP_TRANSACTION_READ_HEADERS  [dt=21304]
t=21311 [st=21310]        HTTP_STREAM_PARSER_READ_HEADERS  [dt=21304]
                          --> net_error = -101 (ERR_CONNECTION_RESET)
t=42615 [st=42614]        HTTP_TRANSACTION_RESTART_AFTER_ERROR
                          --> net_error = -101 (ERR_CONNECTION_RESET)
t=42615 [st=42614]     -HTTP_TRANSACTION_READ_HEADERS
t=42615 [st=42614]     +HTTP_STREAM_REQUEST  [dt=12]
t=42627 [st=42626]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                          --> source_dependency = 193498 (HTTP_STREAM_JOB)
t=42627 [st=42626]     -HTTP_STREAM_REQUEST
t=42627 [st=42626]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=2]
t=42627 [st=42626]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS
                          --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1
                              Host: qa.tieba.baidu.com
                              Connection: keep-alive
                              Accept: application/json, text/plain, */*
                              User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
                              Referer: http://qa.tieba.baidu.com/project/
                              Accept-Encoding: gzip, deflate, sdch
                              Accept-Language: en-US,en;q=0.8
                              Cookie: [268 bytes were stripped]
t=42629 [st=42628]     -HTTP_TRANSACTION_SEND_REQUEST
t=42629 [st=42628]     +HTTP_TRANSACTION_READ_HEADERS  [dt=112]
t=42629 [st=42628]        HTTP_STREAM_PARSER_READ_HEADERS  [dt=112]
t=42741 [st=42740]        HTTP_TRANSACTION_READ_RESPONSE_HEADERS
                          --> HTTP/1.1 200 OK
                              Date: Fri, 02 Jan 2015 09:51:48 GMT
                              Content-Type: application/json; charset=UTF-8
                              Transfer-Encoding: chunked
                              Connection: keep-alive
                              Cache-Control: no-cache
                              tracecode: 31079600320335034634010217
                              tracecode: 31079600320537995786010217
                              Server: Apache
t=42741 [st=42740]     -HTTP_TRANSACTION_READ_HEADERS
t=42741 [st=42740]      HTTP_CACHE_WRITE_INFO  [dt=0]
t=42741 [st=42740]      HTTP_CACHE_WRITE_DATA  [dt=0]
t=42741 [st=42740]      HTTP_CACHE_WRITE_INFO  [dt=0]
t=42741 [st=42740]      URL_REQUEST_DELEGATE  [dt=0]
t=42741 [st=42740]   -URL_REQUEST_START_JOB
t=42741 [st=42740]    URL_REQUEST_DELEGATE  [dt=0]
t=42741 [st=42740]    HTTP_TRANSACTION_READ_BODY  [dt=0]
t=42741 [st=42740]    HTTP_CACHE_WRITE_DATA  [dt=0]
t=42741 [st=42740]    HTTP_TRANSACTION_READ_BODY  [dt=0]
t=42741 [st=42740]    HTTP_CACHE_WRITE_DATA  [dt=0]
t=42742 [st=42741] -REQUEST_ALIVE

首先,日志显示的总耗时与上面网络面板截图的总耗时是吻合的,都是42.74秒,说明我们定位正确。

以下时间均以毫秒计

日志第一列为时间线,自请求发起时算。 第二列为每步操作所逝去的时间,时间差的概念,与第三列里面的dt不同,它会积累前面的耗时。 第三列为具体的事件,以及相应事件的耗时dt,此耗时为绝对耗时。

+号对应事件开始,-号对应事件结束,也就是说他们必然成对出现。住里是展开后更加详细的子事件。直到不能再细分。

如果说一开始接触到这个日志时手足无措的话,我们来看一下正常情况下的日志是怎样的,有对比才有发现。

以下随便摘取一次正常请求的日志,如下:

384462: URL_REQUEST
http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593
Start Time: 2015-01-03 20:23:54.698

t=1556 [st=  0] +REQUEST_ALIVE  [dt=172]
t=1556 [st=  0]    URL_REQUEST_DELEGATE  [dt=0]
t=1556 [st=  0]   +URL_REQUEST_START_JOB  [dt=171]
                   --> load_flags = 335609856 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | VERIFY_EV_CERT)
                   --> method = "GET"
                   --> priority = "LOW"
                   --> url = "http://qa.tieba.baidu.com/release/getReleaseHistory?projectId=fum1.0.593"
t=1557 [st=  1]     +URL_REQUEST_DELEGATE  [dt=4]
t=1557 [st=  1]        DELEGATE_INFO  [dt=4]
                       --> delegate_info = "extension Tampermonkey"
t=1561 [st=  5]     -URL_REQUEST_DELEGATE
t=1561 [st=  5]      HTTP_CACHE_GET_BACKEND  [dt=0]
t=1561 [st=  5]      HTTP_CACHE_OPEN_ENTRY  [dt=1]
                     --> net_error = -2 (ERR_FAILED)
t=1562 [st=  6]      HTTP_CACHE_CREATE_ENTRY  [dt=0]
t=1562 [st=  6]      HTTP_CACHE_ADD_TO_ENTRY  [dt=0]
t=1562 [st=  6]      URL_REQUEST_DELEGATE  [dt=0]
t=1562 [st=  6]     +HTTP_STREAM_REQUEST  [dt=2]
t=1564 [st=  8]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                       --> source_dependency = 384467 (HTTP_STREAM_JOB)
t=1564 [st=  8]     -HTTP_STREAM_REQUEST
t=1564 [st=  8]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=1]
t=1564 [st=  8]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS
                       --> GET /release/getReleaseHistory?projectId=fum1.0.593 HTTP/1.1
                           Host: qa.tieba.baidu.com
                           Connection: keep-alive
                           Accept: application/json, text/plain, */*
                           User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
                           Referer: http://qa.tieba.baidu.com/project/
                           Accept-Encoding: gzip, deflate, sdch
                           Accept-Language: en-US,en;q=0.8
                           Cookie: [2642 bytes were stripped]
t=1565 [st=  9]     -HTTP_TRANSACTION_SEND_REQUEST
t=1565 [st=  9]     +HTTP_TRANSACTION_READ_HEADERS  [dt=161]
t=1565 [st=  9]        HTTP_STREAM_PARSER_READ_HEADERS  [dt=160]
t=1725 [st=169]        HTTP_TRANSACTION_READ_RESPONSE_HEADERS
                       --> HTTP/1.1 200 OK
                           Date: Sat, 03 Jan 2015 12:23:54 GMT
                           Content-Type: application/json; charset=UTF-8
                           Transfer-Encoding: chunked
                           Connection: keep-alive
                           Cache-Control: no-cache
                           tracecode: 14346880480340800522010320
                           tracecode: 14346880480253893130010320
                           Server: Apache
t=1726 [st=170]     -HTTP_TRANSACTION_READ_HEADERS
t=1726 [st=170]      HTTP_CACHE_WRITE_INFO  [dt=0]
t=1726 [st=170]      HTTP_CACHE_WRITE_DATA  [dt=0]
t=1726 [st=170]      HTTP_CACHE_WRITE_INFO  [dt=0]
t=1726 [st=170]     +URL_REQUEST_DELEGATE  [dt=1]
t=1726 [st=170]        DELEGATE_INFO  [dt=1]
                       --> delegate_info = "extension Tampermonkey"
t=1727 [st=171]     -URL_REQUEST_DELEGATE
t=1727 [st=171]   -URL_REQUEST_START_JOB
t=1727 [st=171]    URL_REQUEST_DELEGATE  [dt=0]
t=1727 [st=171]    HTTP_TRANSACTION_READ_BODY  [dt=0]
t=1727 [st=171]    HTTP_CACHE_WRITE_DATA  [dt=1]
t=1728 [st=172]    HTTP_TRANSACTION_READ_BODY  [dt=0]
t=1728 [st=172]    HTTP_CACHE_WRITE_DATA  [dt=0]
t=1728 [st=172] -REQUEST_ALIVE

针对上面正常的请求,我们主要关注两部分,如下面的截图:

  • 发送请求头 ` +HTTP_TRANSACTION_SEND_REQUEST [dt=1]`
  • 读取响应头 ` +HTTP_TRANSACTION_READ_HEADERS [dt=161]`

这是正常的情况下,没有什么问题。并且日志里可以清晰地看到发送的请求头是什么,然后解析出来的响应头是什么。这跟在网络面板看到的是一致的。

再回到出问题的请求日志上来,同样我们只关注这两部分。如下面的截图:

与正常相比,最后一次发送请求和读取响应头无异常,时间就多在了前面还有再次发送和请求的过程,细看时间都花在了以下两个事件中:

  • HTTP_STREAM_PARSER_READ_HEADERS [dt=21301]
  • HTTP_STREAM_PARSER_READ_HEADERS [dt=21304]

该事件的名称已经自我解读,意思是解析读取的响应头。但问题是紧接着下面报错了,

--> net_error = -101 (ERR_CONNECTION_RESET)

读取响应头时发生了链接重置的错误,有理由认为本次链接是不成功的,没拿到正确的响应头,于是解析不成功。时间都花在了这里,足足21秒之久,两个21秒造就了上面看到的Stalled了42秒之久。

问题似乎已经很明朗了。链接被重置。

在第三次尝试的时候正常了,于是正确返回,我们才看到了被解析的响应头被展示在了下面。也就是说在出问题的时候要么响应头未拿到,要么响应头非法导致解析不成功。而原因就是链接被重置。

那么接下来的工作就是对ERR_CONNECTION_RESET这个错误的追查了。

官方关于 ERR_CONNECTION_RESET 错误的解释

未找到官方相应的资料,Chrome官网上唯一关于此错误的描述是在安装Chrome时出现Error 101。我估计文档的撰写人员没想到谁会这么蛋疼想要看这些生涩的东西,除了开发者。既然你都是开发者了,那为什么不去看Chromium的源码。

好吧,唯一的途径似乎只能从源码中寻找了。作为只精JS的前端人员,现在要从C,C++代码中找答案了。估计追完这个问题,我会尝试为Chromium贡献代码。

慢着,在这之前,还是搜到一些关于这个错误的信息的。但似乎都不怎么靠谱。

比如这里提到,是因为ISP网络问题,实在无太可能。还有这是神马居然一个硬件网站但提到了这个错误,并且怀疑是杀软导致Chrome出问题,但杀软已经在上文被我们排除了。

Chromium 源码

那么这个错误究竟是什么。能不能找到点靠谱的解释。当然能,让我们进入到Chromium的源码中去。

ERR_CONNECTION_RESET被唤起的地方

在Chromium的源码中搜索该常量名,确实出现很多结果。联系到我们查看日志发现问题的上下文,是在解析响应头报的。所以我们定位到http_stream_parser.cc文件,同时注意到有一个文件叫net_errors_win.cc,所以猜测他是定义所有错误常量用的,也顺便打开之。

经过观察src/net/base/net_errors_win.cc 其路径和代码得知其中多为系统级别的错误,似乎跟我们的问题不是很关联,忽略该文件。

http_stream_parser.cc文件中,ERR_CONNECTION_RESET仅出现一次。这给我们定位带来了极大的便利。

[chromium]//src/net/base/net_errors_win.cc:

// Returns true if |error_code| is an error for which we give the server a
// chance to send a body containing error information, if the error was received
// while trying to upload a request body.
bool ShouldTryReadingOnUploadError(int error_code) {
  return (error_code == ERR_CONNECTION_RESET);
}

这里定义了一个ShouldTryReadingOnUploadError 的方法,注释耐人寻味,这个时候,这样的情景,能否正确解读注释成为了比读懂代码更重要(这是我在看JS代码时永远无法体味到的感觉),下面尽可能对它进行理解:

在尝试发送一个请求体的时候,让服务器尝试发送一个带错误的响应体,如果我们接收到了该错误则返回true

我承认被上面的复杂从句打败!

那么我们来看这个方法被调用的场景。

现在我们点击上面的ShouldTryReadingOnUploadError方法,代码下方出现调用了该方法的地方,一共有两处。

分别点击进行查看。

459行DoSendHeadersComplete方法里进行了调用:

int HttpStreamParser::DoSendHeadersComplete(int result) {
  if (result < 0) {
    // In the unlikely case that the headers and body were merged, all the
    // the headers were sent, but not all of the body way, and |result| is
    // an error that this should try reading after, stash the error for now and
    // act like the request was successfully sent.
    if (request_headers_->BytesConsumed() >= request_headers_length_ &&
        ShouldTryReadingOnUploadError(result)) {
      upload_error_ = result;
      return OK;
    }
    return result;
  }

虽然不太可能,但也不排除头部和请求体合并的情况,当所有头部发送完毕,请求体不一定,此时result便是需要稍后处理的一种错误,这里暂且先返回OK

516行另一个DoSendBodyComplete方法里进行了调用:

int HttpStreamParser::DoSendBodyComplete(int result) {
  if (result < 0) {
    // If |result| is an error that this should try reading after, stash the
    // error for now and act like the request was successfully sent.
    if (ShouldTryReadingOnUploadError(result)) {
      upload_error_ = result;
      return OK;
    }
    return result;
  }

跟上面类似,如果result出错,稍后处理,先返回正常

这也与我们在日志中看到的情况相符,在前面再次错误后,这次请求并没有终止结束,而是尝试到了第三次并且以成功结束的。

但不管怎样,从这两个方法,一个DoSendHeadersComplete, 另一个DoSendBodyComplete,身上能体现出请求确实已经发出去。

TCP RST

另外,在net_error_list.h这个文件的109行,可以准确找到我们在日志中得到的101号错误。它的定义如下:

// A connection was reset (corresponding to a TCP RST).
NET_ERROR(CONNECTION_RESET, -101)

从括号中的进一步解释可以知道,它代表TCP连接重置。

TCP

那么问题来了,什么是TCP连接重置?什么会引发TCP连接重置。从这篇文章中有比较详细的解答。

想要完全解释,本文似乎是不可能的了。但根据上面的文章,这里可以简单转述一下。

什么是TCP连接

它是一种协议。当网络上一个节点想与另一个节点通信时,双方需要选建立连接。而这个连接过程需要大家都懂的一种约定,TCP就是事先定好的一种约定,于是我们采用它吧,于是其中一个节点按照这个约定发起一建立连接的请求,另一节点收到后,根据该约定,便能读懂这个请求里各字段的意思:哦,丫这是想约我呢。

三次握手

继续上面的例子。A想与B通信,并且使用TCP。

首先A发起一个报文,其中包含自己的地址,想要连接的目标地址,自己用来连接的端口及目标机器的端口,etc.

B收到邀约,并且愿意付约。此刻B需要回传一个报文,告诉A我愿意跟你连接。

A收到B的肯定应答,到此A与B经历了三次通信或者说是握手,双方都没有异议,连接建立。

而连接断开的过程也颇为类似。双方中的一方比如说A先发起一个断开连接的报文FIN,B收到并确认,然后回传一个可以断开的报文FIN给A。此刻A收到并确认。此刻双方都确认后,连接可以安全断开,但还会保持一个等待断开的状态,大概持续4分钟,用于之前连接通路上未传输完成的数据进行善后。

什么是重置

上面提到了4分钟的等待时间,而重置RESET便是立即断开连接的手段。

发生重置的情况

到此重置的作用已然明了。也就是说,重置甚至算不上一个错误,它是TCP连接中的一种正常情况。但什么时候会发生重置,如何引起的。

上文列出了三种情况。

SMB Reset

简单举例来说,服务器提供了两个端口445,139进行服务,客户端同时去请求与这两个端口连接,服务器返回了两个端口可以被连接,此刻客户端择优选择一个进行连接,而重置另一个。

Ack, Reset

报文重置发生主要有以下情况:

  • 服务器没有监听被请求的端口,无法建立连接
  • 服务器此刻无法比如没有充裕的资源用来连接连接

TCP Reset due to no response

由于没有响应而被重置。当发起连接的一方连续发送6次请求未得到回应,此刻默认他们之间已经通过三次握手建立了连接并且通信有问题,发起的一方将连接重置。

Application Reset

除了上面的情况,找不到TCP内部自己发送的重置,则归为了这一类。程序内将连接重置。此种情况包含了所有你想得到想不到将连接断开的情况。有可能是程序内部逻辑重置的,所以不能完全认为此时发生了错误。

值得注意的是,上面列出的情况服务器的不确定性导致连接重置的可能性要合理些。Chrome 主动发起URL请求不太可能自己又重置掉,并且没有理由重置掉后又去重连。

进一步解读日志文件

上面Chromium源码部分的求证多少带有猜测成分。不妥。

因为没找到关于Chrome net-internal 日志的官方文档什么的,自身去解读始终是有局限的。不如提个ISSUE让Chromium开发人员来搭一把手吧。遂向Chromium提交ISSUE,请戳此查看,虽然我不认为现在遇到的这个问题跟Chrome有关并且属于Chrome的Bug,目的仅仅是看他们能否帮忙给出合理的日志解读来定位问题。

三天后(有点热泪盈眶),有同学回复,将日志所体现的问题诊断得似乎很有道理,可信。

1) We have a bunch of connections to qa.tieba.baidu.com, all were used successfully, and are now idle. 2) They all silently die for some reason, without us ever being informed. My guess is your personal router times out the connection, but this could also be your ISP, the destination server, or ever a real network outage (A short one) that prevents us from getting the connection closed message. 3) There’s a new request to qa.tieba.baidu.com. We try to reuse a socket. After 21 seconds, we get the server’s RST message (“I don’t have a connection to you.”). Since it was a stale socket, we’re aware this sometimes happens, so we just retry…And get the next idle socket in the list, which, after 21 seconds, gives us the same reset message. We try again, for the same reason. This time we don’t have another stale socket to try, so we use a fresh one. The request succeeds.

The real problem here is something is taking 21 seconds to send us the RST messages, despite the fact that a roundtrip to the server you’re talking to only takes about 100 milliseconds.

  • 「之前有过很多成功的连接」,确实,因为出现加载缓慢的情况是偶发的,这之前有过很多正常的不卡的请求存在过。这里没有异议。
  • 「他们都以未知的原因被断掉了」,因为不是正常地断开连接,所以客户端也就是浏览器不知道当前与服务器的TCP连接已经断开,傻傻地保留着与服务器连接的socket,注意,此时已经发生信息的不对等了,这是问题的根源。至于什么原因,给出了可能的原因:路由器认为连接超时将其断掉,同时不排除ISP(互联网服务提供商)的原因,服务器暂时的停运抽风等。不管怎样,客户端浏览器没有收到连接断开的信息。
  • 在上面的基础上,我们去发起一次新的请求。此时浏览器希望重用之前的连接以节省资源,用之前的一个socket去发起连接。21秒后收到服务器返回的重置信息(意思是服务器告诉浏览器:我和你之间没有连接),没关系,上面提到,我们有很多可以重用的连接,于是浏览器重新从可用的连接里面又选择了一个去进行连接,不幸的是,同样的情况再次发生,21秒后收到服务器的重置信息。这体现在日志上就是第二次重试失败。到第三次,因为前面浏览器认为可以重用的连接现在都被正确地标为断开了,没有新的可用,于是这次浏览器发起了全新的请求,成功了!

总结出来,两个问题:

  • 为什么之前成功的连接不正常的断开了?服务器配置或者网络原因?
  • 是什么让浏览器21秒后才收到重置信息?服务器作出反应过慢还是网络原因?

Chrome Dev Tool 中时间线各阶段代表的意义

另附注一下Chrome Dev Tool 中请求的时间线各阶段代表的意义。 以下内容扒自Chrome 开发者文档页,然后我将它本地化了一下下。

Stalled/Blocking

在请求能够被发出去前的等等时间。包含了用于处理代理的时间。另外,如果有已经建立好的连接,那么这个时间还包括等待已建立连接被复用的时间,这个遵循Chrome对同一源最大6个TCP连接的规则。

「拿我们的情况来说,上面出错所有的耗时也是算在了这部分里面。网络面板中显示的其余时间比如DNS查找,连接建立等都是属于最后那次成功请求的了」

Proxy Negotiation

处理代理的时间。

DNS Lookup

查找DNS的时间。页面上每个新的域都需要一次完整的寻路来完成DNS查找。

Initial Connection / Connecting

用于建立链接的时间,包括TCP握手及多次尝试握手,还有处理SSL。

SSL

完成SSL握手的时间。

Request Sent / Sending

发起请求的时间,通常小到可以忽略。

Waiting (TTFB)

等待响应的时间,具体来说是等待返回首个字节的时间。包含了与服务器之间一个来回响应的时间和等待首个字节被返回的时间。

Content Download / Downloading

用于下载响应的时间

结论

我相信很多同学是直接跳到这里来了的。事实上我给不出什么解决方案,但能排除前端代码引起问题的可能性。

具体来说,能够得到的结论有以下几点:

  • 请求成功构造,失败情况下也可以看到正常的请求头被打印出来了的
  • 可以肯定的是在与服务器建立连接时被Shut down了,参考上面关于连接重置的部分会更有意义一些
  • 参考上面「进一步解读日志文件」部分,来自Chromium开发者的回复中对日志文件的解读更加合理些,浏览器与服务器的连接不正常断开是导致问题的根源,以至于影响了后面对连接的重用
  • 21秒的等待仍然是个未知数,不知道有何不可抗拒的外力促使浏览器21秒后才收到服务器的重置信息。此处浏览器与服务器的失联原因有待查证

最后寄希望于RD同学跟进,协助排查服务器连接及后端代码的部分。FE同学会保持持续关注。

参考及引用

#1 Chrome stalls when making multiple requests to same resource?

#2 What does “pending” mean for request in Chrome Developer Window?

#3 Evaluating network performance / Resource network timing

#4 Provisional headers are shown

#5 “CAUTION: provisional headers are shown” in Chrome debugger

#6 Chrome 里的请求报错 “CAUTION: Provisional headers are shown” 是什么意思?

#7 Issue 345643003: Http cache: Implement a timeout for the cache lock

#8 Issue 46104: Pages can get blocked in “Waiting for Cache” for a very long time

#9 Providing Network Details for bug reports

#10 从FE的角度上再看输入url后都发生了什么

#11 ERR_CONNECTION_RESET 的Chromium 源码

#12 Chromium Network Stack

#13 Where do resets come from? (No, the stork does not bring them.)

#14 Issue 447463: Chrome-network: Long delay before RST message on stale sockets results in slow page loads)

作者:wayou (http://wayou.github.io/) – IT重度症需要治疗

web页面渲染性能-概述(谷歌开发规范) - 简书

mikel阅读(917)

来源: web页面渲染性能-概述(谷歌开发规范) – 简书

60fps与设备刷新率

目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

性能图片

像素管道

web页面其实就是把html、css、js中写的逻辑,映射到屏幕上的像素,主要包括一下五个主要区域

像素合成过程
  • JavaScript。一般来说,我们会使用 JavaScript 来实现一些视觉变化的效果。比如用 JQuery 的 animate 函数做一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。当然,除了 JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions 和 Web Animation API。
  • 样式计算。此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
  • 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 <body> 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
  • 绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。
  • 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

整个绘制过程中,页面都有机会产生卡顿,因为我们必须保证代码触发链路上的那部分逻辑
在网页合成的过程中,也许听过“栅格化”,这是因为绘制过程中分为两个步骤

  1. 创建绘图调用的列表
  2. 填充像素(栅格化)

不一定每一帧都是会经过这个管道的每一个部分,实际上不管是使用js,css,还是网络动画,在实现视觉变化时,管道针对指定帧运行通常有三种方式

1. JS/CSS >样式 > 布局 > 绘制 > 合成

完整链路

如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

2. JS / CSS > 样式 > 绘制 > 合成

重绘

如果您修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。

3. JS / CSS > 样式 > 合成

直接合成

如果您更改一个既不要布局也不要绘制的属性,则浏览器将跳到只执行合成。
这个最后的版本开销最小,最适合于应用生命周期中的高压力点,例如动画或滚动。

性能在多数情况下,我们必须和浏览器配合,也不是跟他对着干,值得注意的是,上面列出的各管道工作,在计算开销上有所不同,一些任务比其他任务开销要大的多。

接下来,持续分析渲染过程中每一部分

JS执行优化

JS会经常触发视觉变化,有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序,时机不当或长时间运行的JS可能导致性能问题的常见原因。

  • 对于动画实现,避免使用setTimeout或setInterval,请使用requestAnimateFrame
  • 将长时间运行的javascript从主线程移动Web Worker
  • 请使用微任务来执行对多个帧的DOM更改
  • 使用 chrome Devtools的Timeline 和javascript分析器来评估对Javascript的影响

使用 requestAnimationFrame 来实现视觉变化

使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,但这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在未尾,而这可能经常会使我们丢失帧,导致卡顿

示例图片

requestAnimationFrame 的基本思想让页面重绘的频率与这个刷新频率保持同步,比如显示器屏幕刷新率为 60Hz,使用requestAnimationFrame API,那么回调函数就每1000ms / 60 ≈ 16.7ms执行一次;如果显示器屏幕的刷新率为 75Hz,那么回调函数就每1000ms / 75 ≈ 13.3ms执行一次。

降低复杂性或使用 Web Worker

JavaScript 在浏览器的主线程上运行,恰好与样式计算、布局以及许多情况下的绘制一起运行。如果 JavaScript 运行时间过长,就会阻塞这些其他工作,可能导致帧丢失。

因此,您要妥善处理 JavaScript 何时运行以及运行多久。例如,如果在滚动之类的动画中,最好是想办法使 JavaScript 保持在 3-4 毫秒的范围内。超过此范围,就可能要占用太多时间。如果在空闲期间,则可以不必那么斤斤计较所占的时间。

在许多情况下,可以将纯计算工作移到 Web Worker,例如,如果它不需要 DOM 访问权限。数据操作或遍历(例如排序或搜索)往往很适合这种模型,加载和模型生成也是如此。

并非所有工作都适合此模型:Web Worker 没有 DOM 访问权限。如果您的工作必须在主线程上执行,请考虑一种批量方法,将大型任务分割为微任务,每个微任务所占时间不超过几毫秒,并且在每帧的 requestAnimationFrame 处理程序内运行。

了解 JavaScript 的“帧税”

在评估一个框架、库或您自己的代码时,务必逐帧评估运行 JavaScript 代码的开销。当执行性能关键的动画工作(例如变换或滚动)时,这点尤其重要。

测量 JavaScript 开销和性能情况的最佳方法是使用 Chrome DevTools。通常,您将获得如下的简单记录:

image.png

如果发现有长时间运行的 JavaScript,则可以在 DevTools 用户界面的顶部启用 JavaScript 分析器:

image.png

以这种方式分析 JavaScript 会产生开销,因此一定只在想要更深入了解 JavaScript 运行时特性时才启用它。启用此复选框后,现在可以执行相同的操作,您将获得有关 JavaScript 中调用了哪些函数的更多信息:

image.png

有了这些信息之后,您可以评估 JavaScript 对应用性能的影响,并开始找出和修正函数运行时间过长的热点。如前所述,应当设法移除长时间运行的 JavaScript,或者若不能移除,则将其移到 Web Worker 中,腾出主线程继续执行其他任务。

避免微优化 JavaScript

知道浏览器执行一个函数版本比另一个函数要快 100 倍可能会很酷,比如请求元素的offsetTop比计算getBoundingClientRect()要快,但是,您在每帧调用这类函数的次数几乎总是很少,因此,把重点放在 JavaScript 性能的这个方面通常是白费劲。您一般只能节省零点几毫秒的时间。

作者:cendechen
链接:https://www.jianshu.com/p/1e795d5bd209
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Nginx(三)------nginx 反向代理 - YSOcean - 博客园

mikel阅读(853)

来源: Nginx(三)——nginx 反向代理 – YSOcean – 博客园

Nginx 服务器的反向代理服务是其最常用的重要功能,由反向代理服务也可以衍生出很多与此相关的 Nginx 服务器重要功能,比如后面会介绍的负载均衡。本篇博客我们会先介绍 Nginx 的反向代理,当然在了解反向代理之前,我们需要先知道什么是代理以及什么是正向代理。

1、代理

在Java设计模式中,代理模式是这样定义的:给某个对象提供一个代理对象,并由代理对象控制原对象的引用。

可能大家不太明白这句话,在举一个现实生活中的例子:比如我们要买一间二手房,虽然我们可以自己去找房源,但是这太花费时间精力了,而且房屋质量检测以及房屋过户等一系列手续也都得我们去办,再说现在这个社会,等我们找到房源,说不定房子都已经涨价了,那么怎么办呢?最简单快捷的方法就是找二手房中介公司(为什么?别人那里房源多啊),于是我们就委托中介公司来给我找合适的房子,以及后续的质量检测过户等操作,我们只需要选好自己想要的房子,然后交钱就行了。

代理简单来说,就是如果我们想做什么,但又不想直接去做,那么这时候就找另外一个人帮我们去做。那么这个例子里面的中介公司就是给我们做代理服务的,我们委托中介公司帮我们找房子。

Nginx 主要能够代理如下几种协议,其中用到的最多的就是做Http代理服务器。

2、正向代理

弄清楚什么是代理了,那么什么又是正向代理呢?

这里我再举一个例子:大家都知道,现在国内是访问不了 Google的,那么怎么才能访问 Google呢?我们又想,美国人不是能访问 Google吗(这不废话,Google就是美国的),如果我们电脑的对外公网 IP 地址能变成美国的 IP 地址,那不就可以访问 Google了。你很聪明,VPN 就是这样产生的。我们在访问 Google 时,先连上 VPN 服务器将我们的 IP 地址变成美国的 IP 地址,然后就可以顺利的访问了。

这里的 VPN 就是做正向代理的。正向代理服务器位于客户端和服务器之间,为了向服务器获取数据,客户端要向代理服务器发送一个请求,并指定目标服务器,代理服务器将目标服务器返回的数据转交给客户端。这里客户端是要进行一些正向代理的设置的。

PS:这里介绍一下什么是 VPN,VPN 通俗的讲就是一种中转服务,当我们电脑接入 VPN 后,我们对外 IP 地址就会变成 VPN 服务器的 公网 IP,我们请求或接受任何数据都会通过这个VPN 服务器然后传入到我们本机。这样做有什么好处呢?比如 VPN 游戏加速方面的原理,我们要玩网通区的 LOL,但是本机接入的是电信的宽带,玩网通区的会比较卡,这时候就利用 VPN 将电信网络变为网通网络,然后在玩网通区的LOL就不会卡了(注意:VPN 是不能增加带宽的,不要以为不卡了是因为网速提升了)。

可能听到这里大家还是很抽象,没关系,和下面的反向代理对比理解就简单了。

3、反向代理

反向代理和正向代理的区别就是:正向代理代理客户端,反向代理代理服务器。

  反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

下面我们通过两张图来对比正向代理和方向代理:

 

理解这两种代理的关键在于代理服务器所代理的对象是什么,正向代理代理的是客户端,我们需要在客户端进行一些代理的设置。而反向代理代理的是服务器,作为客户端的我们是无法感知到服务器的真实存在的。

总结起来还是一句话:正向代理代理客户端,反向代理代理服务器。

4、Nginx 反向代理

范例:使用 nginx 反向代理 www.123.com 直接跳转到127.0.0.1:8080

①、启动一个 tomcat,浏览器地址栏输入 127.0.0.1:8080,出现如下界面

 

②、通过修改本地 host 文件,将 www.123.com 映射到 127.0.0.1

127.0.0.1 www.123.com

将上面代码添加到 Windows 的host 文件中,该文件位置在:

配置完成之后,我们便可以通过 www.123.com:8080 访问到第一步出现的 Tomcat初始界面。

那么如何只需要输入 www.123.com 便可以跳转到 Tomcat初始界面呢?便用到 nginx的反向代理。

③、在 nginx.conf 配置文件中增加如下配置:

复制代码
1     server {
2         listen       80;
3         server_name  www.123.com;
4 
5         location / {
6             proxy_pass http://127.0.0.1:8080;
7             index  index.html index.htm index.jsp;
8         }
9     }
复制代码

如上配置,我们监听80端口,访问域名为www.123.com,不加端口号时默认为80端口,故访问该域名时会跳转到127.0.0.1:8080路径上。

我们在浏览器端输入 www.123.com 结果如下:

④、总结

其实这里更贴切的说是通过nginx代理端口,原先访问的是8080端口,通过nginx代理之后,通过80端口就可以访问了。

5、Nginx 反向代理相关指令介绍

①、listen

该指令用于配置网络监听。主要有如下三种配置语法结构:

一、配置监听的IP地址

listen address[:port] [default_server] [setfib=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [deferred]
    [accept_filter=filter] [bind] [ssl];

二、配置监听端口

listen port[default_server] [setfib=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] 
    [deferred] [bind] [ipv6only=on|off] [ssl];

三、配置 UNIX Domain Socket

listen unix:path [default_server]  [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] 
    [deferred] [bind] [ssl];

上面的配置看似比较复杂,其实使用起来是比较简单的:

1 listen *:80 | *:8080 #监听所有80端口和8080端口
2 listen  IP_address:port   #监听指定的地址和端口号
3 listen  IP_address     #监听指定ip地址所有端口
4 listen port     #监听该端口的所有IP连接

下面分别解释每个选项的具体含义:

1、address:IP地址,如果是 IPV6地址,需要使用中括号[] 括起来,比如[fe80::1]等。

2、port:端口号,如果只定义了IP地址,没有定义端口号,那么就使用80端口。

3、path:socket文件路径,如 var/run/nginx.sock等。

4、default_server:标识符,将此虚拟主机设置为 address:port 的默认主机。(在 nginx-0.8.21 之前使用的是 default 指令)

5、 setfib=number:Nginx-0.8.44 中使用这个变量监听 socket 关联路由表,目前只对 FreeBSD 起作用,不常用。

6、backlog=number:设置监听函数listen()最多允许多少网络连接同时处于挂起状态,在 FreeBSD 中默认为 -1,其他平台默认为511.

7、rcvbuf=size:设置监听socket接收缓存区大小。

8、sndbuf=size:设置监听socket发送缓存区大小。

9、deferred:标识符,将accept()设置为Deferred模式。

10、accept_filter=filter:设置监听端口对所有请求进行过滤,被过滤的内容不能被接收和处理,本指令只在 FreeBSD 和 NetBSD 5.0+ 平台下有效。filter 可以设置为 dataready 或 httpready 。

11、bind:标识符,使用独立的bind() 处理此address:port,一般情况下,对于端口相同而IP地址不同的多个连接,Nginx 服务器将只使用一个监听指令,并使用 bind() 处理端口相同的所有连接。

12、ssl:标识符,设置会话连接使用 SSL模式进行,此标识符和Nginx服务器提供的 HTTPS 服务有关。

②、server_name

该指令用于虚拟主机的配置。通常分为以下两种:

1、基于名称的虚拟主机配置

语法格式如下:

server_name   name ...;

一、对于name 来说,可以只有一个名称,也可以有多个名称,中间用空格隔开。而每个名字由两段或者三段组成,每段之间用“.”隔开。

server_name 123.com www.123.com

二、可以使用通配符“*”,但通配符只能用在由三段字符组成的首段或者尾端,或者由两端字符组成的尾端。

server_name *.123.com www.123.*

三、还可以使用正则表达式,用“~”作为正则表达式字符串的开始标记。

server_name ~^www\d+\.123\.com$;

该表达式“~”表示匹配正则表达式,以www开头(“^”表示开头),紧跟着一个0~9之间的数字,在紧跟“.123.co”,最后跟着“m”($表示结尾)

以上匹配的顺序优先级如下:

1 ①、准确匹配 server_name
2 ②、通配符在开始时匹配 server_name 成功
3 ③、通配符在结尾时匹配 server_name 成功
4 ④、正则表达式匹配 server_name 成功

2、基于 IP 地址的虚拟主机配置

语法结构和基于域名匹配一样,而且不需要考虑通配符和正则表达式的问题。

server_name 192.168.1.1

③、location

该指令用于匹配 URL。

语法如下:

1 location [ = | ~ | ~* | ^~] uri {
2 
3 }

1、= :用于不含正则表达式的 uri 前,要求请求字符串与 uri 严格匹配,如果匹配成功,就停止继续向下搜索并立即处理该请求。

2、~:用于表示 uri 包含正则表达式,并且区分大小写。

3、~*:用于表示 uri 包含正则表达式,并且不区分大小写。

4、^~:用于不含正则表达式的 uri 前,要求 Nginx 服务器找到标识 uri 和请求字符串匹配度最高的 location 后,立即使用此 location 处理请求,而不再使用 location 块中的正则 uri 和请求字符串做匹配。

注意:如果 uri 包含正则表达式,则必须要有 ~ 或者 ~* 标识。

④、proxy_pass

该指令用于设置被代理服务器的地址。可以是主机名称、IP地址加端口号的形式。

语法结构如下:

proxy_pass URL;

URL 为被代理服务器的地址,可以包含传输协议、主机名称或IP地址加端口号,URI等。

proxy_pass  http://www.123.com/uri;

⑤、index

该指令用于设置网站的默认首页。

语法为:

index  filename ...;

后面的文件名称可以有多个,中间用空格隔开。

index  index.html index.jsp;

通常该指令有两个作用:第一个是用户在请求访问网站时,请求地址可以不写首页名称;第二个是可以对一个请求,根据请求内容而设置不同的首页。

参考文档:苗泽老师的《Nginx高性能Web服务器详解》