【ASP.NET Core】EF Core - “影子属性” 深入浅出经典面试题:从浏览器中输入URL到页面加载发生了什么 - Part 1

【ASP.NET Core】EF Core - “影子属性”

 

有朋友说老周近来博客更新较慢,确实有些慢,因为有些 bug 要研究,另外就是老周把部分内容转到直播上面,所以写博客的内容减少了一点。

老周觉得,视频直播可能会好一些,虽然我的水平一般,不过直播时,老周可以现场演示,可能会比看博客效果要好(因为现场演示,有时候会有失误,没办法,水平有限)。还有一个,就是.NET 的资料其实很多,毕竟也发展了十几年了,有些东西如果别人都写过了,那我也不好意思重复了。.NET Core 尽管是跨平台版本,但核心依然是.net 基础,我们不需要全新去学习,只要掌握一些新的变化就可以了。目前比较期待 .NET Core 3 的正式发布,等正式上线了,老周再挑一些有意义的内容写一下。

此外,老周也可能会写一写其他方面的博客,比如 Python、GO、Ruby、Typescript 等。老周并不是只会玩.NET ,只不过老周是主攻 .NET,在接触 .NET 之前,老周就学过很多东西,比如古老的 QBasic、Pascal ,老周在上初中时就学过。后来向 VB、C、C++ 进攻,顺便把 Ruby、Python、PB 也调戏一下,后来有一段时间,Delphi 和 E 语言也挺流行的,所以顺便也玩了两把。

再后来,学过 Java 和 PHP,抛 Java 而投 .NET 是因为 Java 太复杂,效率不高,没有深度把玩的兴趣。现在所谓的 Python 很热门纯属是商业炒作,Python 又不是什么新玩意儿,很古老了,当然相对于 C 来说,是新了一点,究其特点,就是一种脚本语言(虽然有人死要说它不是脚本语言)。现在网上更有些无知小辈,以为自己会写几行 Python 代码就到处去蹭热点,告诉你,老周当年学各种编程语言时,说不定你还没出生呢。所以,如果你真心喜欢 Python 的话,你用心去学就是了(其实老周也喜欢用 Python 来做图表),不必理会商业炒作。

记得去年 C 语言也被商业炒作了几个月,再往前几年,Javascript 和 Web 前端也被拼命炒作,说得好像 js 是万能的似的,吓得老周都不敢写前端了。最近几年,IT 界开始怀旧了,各种远古生物都被挖出来了,可能是现在计算机行业已经没什么可以创新的原因吧。现在说得较多的是人工智障,这个可以用,也可以不用,反正不痛不痒,算不上生产力革命(至少其震动效果比不上当年 Office 问世时对企业生产的影响大)。不过,人工智障在某些辅助领域还是有用的,比如现在有些小区的智能门,应用效果还可以。但是,漏洞也是百出的,总之,人类可以用它来进行辅助,但不能过于依赖它。它不能解决所有问题。

许多科幻小说都会说人类会被机器人消灭。机器人也是人创造出来的,机器不可能比人强,也不可能灭掉人类(除非机器人比奥特曼里面的超兽还牛逼)。如果人类真的智力在衰减,那么根源还是在人类自己。说白了就是,只有可能是人类自己灭掉自己。你也不用觉得太恐怖,其实只要你不要太依赖机器就好,不要失去你的本能和思考方式就行。

就像我们码农,老周也一样,天天跟计算机打交道,但老周一直坚持:用电脑,但不依赖电脑,多做些机器不能做的事。再加一句:科学只能解决数学和工程问题,而人的问题,需要哲学和美学来解决

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

好了,以上的都是 F 话,下面咱们聊正题。今天咱们耍一下 EF Core 中的影子属性。这个词翻译版本 TMD 多,有翻译为“卷影属性”的,现在的文档又改为“阴影属性”,这不好听,太有心理阴影了,故而,老周觉得,叫“影子属性”好一些。

不管叫什么,你只要知道它是个啥就行。老周喜欢一句话总结,所以,来一句话:

模型类中没有定义的,但数据表中存在的属性——即模型类与数据表中没有对应关系的属性。

老周就用一个简单的示例来说明一下吧。这个示例也是老周在视频直播时用的。

假设,有个模型类,叫 Student。

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

但我想要一个属性,用来记录数据记录被写进数据库的时间,即还有一个属性,叫 InsertTime,不过,这个属性在 Student 类中是没有定义的,但在数据表中是有这一字段的。

因此,在从 DbContext 类派生时,需要重写 OnModelCreating 方法,通过 Model Builder 来定义这个“隐藏”的属性。

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Student>().Property<DateTime>("InsertTime");
        }

这个 InsertTime 属性就成了影子属性了。

 

下面是 DbContext 派生类的完整代码,我放出来是方便你去抄袭的,放心吧,无版权税的,尽管抄。

复制代码
    public class MyContext : DbContext
    {
        public DbSet<Student> Students { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"server=(localdb)\MSSQLLocalDB;database=TestDb");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Student>().Property<DateTime>("InsertTime");
        }
    }
复制代码

 

现在,到 Startup 类中,注册一下服务,让自定义的数据库上下文可以进行依赖注入。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>();
        }

刚刚在 MyContext 类中已经配置连接字符串了,所以注册服务时就不用指定连接字符串了。

 

按老周前面所写的博文,接下来是创建数据迁移,不过,这一次老周使用的是直接在运行的时候创建数据库,方法也是很简单的,找到 Main 入口点,把里面的代码改一下(项目模板默认生成的代码不进行大改)。

复制代码
        public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();
            using(IServiceScope scope= host.Services.CreateScope())
            {
                MyContext c = scope.ServiceProvider.GetService<MyContext>();
                c.Database.EnsureCreated();
            }
            host.Run();
        }
复制代码

也就是在 host 运行之前,创建一个“作用域”级别的服务实例,创建数据库,这个实例是临时使用,不遵循服务容器的生命周期规则。EnsureCreated 方法会检测数据库是否存在,如果不存在,就创建,然后返回 true;如果数据库已经存在,不做任何处理并返回 false。

如果你不打算写入一些初始数据,可以不在乎方法的返回值,如果要写入初始数据,可以 if 一下,如果方法返回 true,就写一下数据进去(true 表示新数据库)。

 

接下来,咱们用一个 API 控制器来测试一下。

复制代码
    [Route("api/[action]")]
    public class TestController : Controller
    {
        readonly  MyContext context;
        public TestController(MyContext c) => context = c;

        [HttpPost]
        public ActionResult AddNew([FromBody]Student stu)
        {
            ……
        }

        [HttpGet]
        public JArray GetList()
        {
            ……
        }
    }
复制代码

数据库上下文的实例因为已经注册到服务容器中,所以通过构造函数可以得到其实例引用。这个控制器有两个 action,AddNew 方法用来提交数据,以 POST 方式访问。

复制代码
        [HttpPost]
        public ActionResult AddNew([FromBody]Student stu)
        {
            // 添加实体
            context.Students.Add(stu);
            // 设置影子属性
            context.Entry(stu).Property<DateTime>("InsertTime").CurrentValue = DateTime.Now;
            context.SaveChanges();
            return Ok("操作成功");  
        }
复制代码

记得,参数要加上 FromBody 特性,因为它要从 HTTP 消息正文中提取,上次我直播时就是忘了写这个,所以提交不到数据。

这里要注意影子属性的赋值方法,因为它没有在 Student 类中公开,你不能通过访问成员来设置它,只能先通过 CurrentValue 属性来设置。

 

然后还有一个 GetList 方法,以 GET 方式来访问。有来返回所有 Student 数据。此处我用 JArray 以 JSON 数组格式返回。

复制代码
        [HttpGet]
        public JArray GetList()
        {
            var q = from s in context.Students
                    select new
                    {
                        s.Id,
                        s.Name,
                        s.Age,
                        // 读取影子属性值
                        InsertTime = EF.Property<DateTime>(s, "InsertTime")
                    };
            JArray arr = new JArray();
            foreach (var s in q)
            {
                JObject obj = new JObject();
                obj.Add("id", new JValue(s.Id));
                obj.Add("name", new JValue(s.Name));
                obj.Add("age", new JValue(s.Age));
                obj.Add("add_time", new JValue(s.InsertTime));
                arr.Add(obj);
            }
            return arr;
        }
复制代码

读取影子属性的方法是用 EF.Property 静态方法,第一个参数是实体模型类的实例,第二个参数是影子属性的名字。这个方法只能在 LINQ 树中使用,如果不在 LINQ 中用会发生异常。这里还有一个问题,就是在 select 子句中用 new 返回的匿名类型无法创建 JObject 对象,所以只好手动去构建。

 

现在可以测一下了,首先提交一下数据。

{
    "name":"李三跳",
    "age":52
}

接着获取一下数据列表。

复制代码
[
    {
        "id": 1,
        "name": "什么鬼",
        "age": 29,
        "add_time": "2018-11-26T11:15:38.2012809"
    },
    {
        "id": 2,
        "name": "陈大扣",
        "age": 83,
        "add_time": "2018-11-26T11:21:07.5374308"
    },
    {
        "id": 3,
        "name": "李三跳",
        "age": 52,
        "add_time": "2018-11-26T12:41:51.758849"
    }
]
复制代码

 

好了,示例就完成了。

影子属性的典型用法就像刚刚这个例子这样,可以用来记录数据的添加时间或者更新时间,但这种数据,一般不需要在实体模型中公开。

 

 

深入浅出经典面试题:从浏览器中输入URL到页面加载发生了什么 - Part 1

 

背景

“从浏览器中输入URL到页面加载的发生了什么“,这是一道经典的面试题,涉及到的知识面非常多,但作为一个自认为对网络知识掌握的比较好的老码农来说,回答这个问题自然不在话下。如果这道题目如果在面试出现,对我来说就是送分题啊。尽管如此,我还是愿意花一些时间根据我自己的理解回答一下这个题目,看我自己到底掌握的有多深,同时也把自己的知识梳理一下。

这让我想起另外一件往事,这道题有点类似于“在手机上浏览器上输入一个URL,手机做了一些什么”,我当时学习通信里的核心网时就给自己提出过这个问题。

我非常愿意将这个面试题的答案共享出来,一是希望得到大家的意见,二是也希望对那些不是特别熟的人起到一些帮助。

因为文章较长,我将它们分为三大部份,后面会提供该文章的完整PDF版本,供大家下载。

从本文里学到什么?

正如前面所说,这篇文章涉及到的知识面非常丰富,我相信您绝对可以从本文里学到很多基础知识,还有一些高级话题。

  1. DNS的解析原理,常用命令,端口等
  2. TCP/IP模型,三次握手,四次挥手。
  3. HTTP/HTTPS的原理和解析。
  4. 浏览器render一个页面
  5. Web安全性问题
  6. 抓包,分析TCP模型,三次握手,SSL/TLS,让学起来不再枯燥。
  7. 其他一些高级话题。

自认为是目前写的最详细的一篇文章了,因为里面有理论,有实战,应该会起到一个比较好的效果。

总概: 几大步骤

总的来说,当你输入在浏览器里输入一个URL到页面加载,发生的顺序如下:

  1. DNS查询
  2. TCP连接
  3. 发送HTTP请求
  4. Server处理HTTP请求并返回HTTP报文
  5. 浏览器解析并render页面
  6. HTTP连接断开
后面将对以上步骤详细介绍。

DNS查询

DNS解析流程

假设输入的URL是包含域名的,那肯定会涉及到DNS解析。当然,如果URL仅仅是IP,那就不会涉及到DNS的。域名的出现是为了方便记忆,因为域名比IP好记。我们这里假设URL包含域名。

解析的步骤大致如下图:

 
首先,在本地域名服务器中根据域名查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求。
如果根域名服务器也不存在该域名时,本地域名会向com顶级域名服务器(TLD)发送一个请求,依次类推下去。
直到最后本地域名服务器得到google的IP地址并把它缓存到本地,供下次查询使用。
 
可以参考页面https://www.verisign.com/en_US/website-presence/online/how-dns-works/index.xhtml ,该页面诠释了DNS的过程。
需要说明的是Root DNS Server一般有13个,后面有个点(.),别忘了。
a.root-servers.net.
c.root-servers.net.
j.root-servers.net.
b.root-servers.net.
i.root-servers.net.
d.root-servers.net.
k.root-servers.net.
f.root-servers.net.
l.root-servers.net.
h.root-servers.net.
m.root-servers.net.
g.root-servers.net.
e.root-servers.net.

DNS的优化

我们发现,一个DNS查询在没有缓存的情况下会有6步,这将是一个耗时的过程,如果DNS 查询时间过长,甚至会影响到用户体验。
 
那么现阶段是怎么优化的呢?缓存。DNS是存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
 
我们以Chrome为例子,输入chrome://net-internals/#dns,我们会看到如下界面:
这里的Capacity: 1000,代表缓存1K条,那么每条记录缓存多久呢?参看
timeout 1
这里的1代表1分钟。
如果是系统缓存,一般分2种情况:
 
Linux 操作系统
见下图,一般在/etc/hosts下。
 

 

Windows操作系统
一般在C:\Windows\System32\Drivers\etc\hosts
 
谈到这里,我给大家分享一个特别有用的技巧
我们在做开发和测试时,会有一种情况,经常会去访问某个(测试)URL,例如http://192.168.1.8:8080/admin/login, 如果这样的话,我们极有可能存在一个问题,觉得每次输入ip真的很麻烦。怎么解决?那么我们可以这么做,利用DNS的原理,将某个伪hostname(abc-test)加入到hosts里,只需加入一条记录:
192.168.1.8. abc-test
这样就可以用http://abc-test:8080/admin/login去访问了,简单易用,对吧。

DNS负载均衡

不知道大家有没有注意或思考过一个问题,Google在全球都有服务器,不同的国家或区域访Google,是不是都很快,这就是Google GSLB的作用。也就是说不同的区域访问Google返回的IP是不一样的,甚至在同一个办公室访问Google,返回回来的IP也有可能是不一样的。
为什么呢?这里用的就是DNS负载均衡,不然Google怎么去支持全球几十亿客户的请求呢。总之,DNS会根据你的位置或IP返回一个合适的IP给你用。除了Google,一些CDN的SP例如Akamai、AWS、Azure、阿里云都有类似的服务。

DNS Record(记录)

DNS记录是一个非常重好的概念。DNS记录类型如下表。
记录类型
含义简介
A(Address)
指定域名对应的IPv4地址
AAAA
指定域名对应的IPv6地址
NS(Name Server)
指定该域名由哪个DNS服务器来进行解析
MX(Mail Exchanger)
邮件交换记录,用于电子邮件系统发邮件时根据收信人的地址后缀来定位邮件服务器
CNAME
别名记录,多个域名映射到同一台计算机(如同一主机提供mail和www服务)
TXT
主机名或域名的说明
TTL(Time-To-Live)
DNS服务器中保存的时间
PTR
将一个主机地址映射到对应的域名
HINFO
说明映射到特定 DNS 主机名的 CPU 类型和操作系统类型
 
在这里只介绍常用的A Record, CNAME,MX,NS。
 
A记录
A记录是用的最多的一种类型。
A (Address) 记录是用来指定主机名(或域名)对应的IP地址记录。用户可以将该域名下的网站服务器指向到自己的Web Server上。
同时也可以设置该域名的子域名。通俗来说A记录就是服务器的IP,域名绑定A记录就是告诉DNS,当你输入域名的时候给你引导向设置在DNS的A记录所对应的服务器。
后面的抓包分析会对A记录进行分析,让您有直观认识。
 
CNAME记录
CNAME记录是另一种用的比较多的记录,可以将一个域名或者子域名指向另外一个主机名。
 
最常用的使用场景是什么呢?没错,CDN就是这种方式。举个例子,例如公司A想把自己的图片放在Akamai的CDN上,A的子域名是img.abc.com, 而 Akamai的CDN服务域名是img.akaimacdn.com. 但是A公司期望用自己的域名吗,而不是Akamai的域名。为了实现这个目标,怎么办?是的,使用CNAME,只需要将子域名img.abc.com指向到img.akaimacdn.com。问题又来了,在哪里设置呢?肯定是在公司A这边的DNS server上,而不是Akamai那边。
后面的抓包分析会对MX进行分析,让您有直观认识。
 
MX记录
MX(Mail Exchanger)记录,字面意思很直观,知道它用来做邮件路由,用户可以将域名下的邮件服务器指向到自己的邮件服务器上,然后可以自己操控所有的邮箱设置。所以只需在线填写服务器的IP地址,即可以将域名下的邮件全部转到您自己设定相应的邮件服务器上。
 
NS记录 
NS记录用来解析服务器记录,表明由哪台服务器对该域名进行解析,这里的NS记录只对子域名生效。
例如用户希望由12.34.56.78这台服务器解析sub1.mydomain.com,则需要设置 sub1.mydomain.com的NS记录。
这里涉及到一个问题,细心的我们会发现A记录也有该功能,这里就涉及到优先级的问题了。NS记录优先于A记录。如果一个主机地址同时存在NS记录和A记录,则A记录不生效。
 

DNS常用命令和工具

只介绍2个常用的命令dig和nslookup。
 
dig
dig是一个DNS查询工具。
 
[warren]$ dig
 
; <<>> DiG 9.9.5-3ubuntu0.16-Ubuntu <<>>
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57775
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 13, AUTHORITY: 0, ADDITIONAL: 1
 
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;.                              IN      NS
 
;; ANSWER SECTION:
.                       16265   IN      NS      a.root-servers.net.
.                       16265   IN      NS      c.root-servers.net.
.                       16265   IN      NS      j.root-servers.net.
.                       16265   IN      NS      b.root-servers.net.
.                       16265   IN      NS      i.root-servers.net.
.                       16265   IN      NS      d.root-servers.net.
.                       16265   IN      NS      k.root-servers.net.
.                       16265   IN      NS      f.root-servers.net.
.                       16265   IN      NS      l.root-servers.net.
.                       16265   IN      NS      h.root-servers.net.
.                       16265   IN      NS      m.root-servers.net.
.                       16265   IN      NS      g.root-servers.net.
.                       16265   IN      NS      e.root-servers.net.
 
;; Query time: 0 msec
;; SERVER: 208.113.157.202#53(208.113.157.202)
;; WHEN: Thu Nov 29 18:04:06 PST 2018
;; MSG SIZE  rcvd: 239
 
再看看dig 京东的域名www.jd.com 会有什么效果。
 
; <<>> DiG 9.10.6 <<>> www.jd.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2675
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
 
;; QUESTION SECTION:
;www.jd.com.                   IN    A
 
;; ANSWER SECTION:
www.jd.com.            300  IN    CNAME    www.jd.com.gslb.qianxun.com.
www.jd.com.gslb.qianxun.com. 300 IN CNAME    www.jdcdn.com.
www.jdcdn.com.        300  IN    A     61.174.55.1
 
;; Query time: 3 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Fri Nov 30 10:07:37 CST 2018
;; MSG SIZE  rcvd: 106
红色部分为有CNAME记录和A记录。
 
nslookup
nslookup适用于Windows,Linux,macOS等操作系统。
nslookup www.jd.com
Server:            192.168.1.1
Address:   192.168.1.1#53
 
Non-authoritative answer:
www.jd.com      canonical name = www.jd.com.gslb.qianxun.com.
www.jd.com.gslb.qianxun.com    canonical name = www.jdcdn.com.
Name:      www.jdcdn.com
Address: 61.174.55.1
 

 

DNS抓包分析

说了这么多,有些同学可能还是觉得只有概念,没法完全理解。所以,我们还是来点实战更为实际,这也是我更进一步了解DNS的工作方式,因为在我看来想了解根本,最有效的方法是抓包,然后深入查探DNS的packet。
 
本文都会使用Wireshark,相信大家都用过这个工具吧。如果没用过,可以下载一个,入门很简单的,别担心。
 
A记录
下面是抓包图,过滤条件是dns,即protocol为DNS。
 
请参看第89和90包,这是查询www.jd.com 的request和response。
 
第89包细节图如下:
我们可以看到上图,Name是www.jd.com, Type是A记录。
 
 
另外,我们可以看到其他信息,DNS是通过UDP传送。其实,有时候也是通过TCP来传送。那么什么时候用TCP,什么时候用UDP?很简单,当response的packet大于512字节时,就用TCP,反之,则用UDP。再回头看着89包,长度为70,所以用UDP了。那多问一个问题,什么情况下DNS查询包超过512?CNAME就有可能。可以参考下面的CNAME抓包。
 
端口是多少呢?53,是的,DNS用的是53端口,非常重要,一般防火墙要打开,否则DNS解析不了,也意味着无法访问域名的website。
 
那么返回什么呢?继续看第90包。
我们可以看到www.jd.com 的DNS A记录,IP地址是61.174.55.1。
 
CNAME
下图是查询www.taobao.com 的抓包。
同时我们也可以看到www.taobao.com.danuoyi.tbcache.com 有多条A记录,这里就是传说的DNS 负载均衡。

DNS标准和协议

DNS是有标准和协议的,就跟HTTP类似的。DNS标准和协议在IETF里。大家可以参考https://en.wikipedia.org/wiki/Domain_Name_System 的 RFC documents部分。
我认为学习协议,框架,或技术,最佳的方法就是阅读官方资料。例如,这里学习DNS可以直接看RFC文档,学习Angular,可以访问其官网。

DNS 10问

如果面试,下面10问基本可以覆盖全了,答案在上面已经说过了:
1.     为什么要用域名?
2.    DNS解析的基本流程?
3.    DNS的根域名是什么,有几个Server?TLD DNS是什么?
4.    DNS的优化策略是什么?在各个环节怎么做的?Chrome和各个操作系统怎么做的?
5.    DNS负载均衡是什么,为什么要用?
6.    DNS的记录类型有哪些?CNAME一般用在哪些场合?举例子说明一下。
7.    DNS的常用工具和命令有哪些?
8.    DNS查询是用TCP还是UDP?一般用哪个端口?
9.    DNS抓包抓过吗?Wireshark有用过吗?
10.  请说明一下www.google.com 和google.com的区别,如何设置它们的DNS?
 
本章介绍完DNS,下一章节介绍TCP/IP,三次握手,四次挥手,以及SSL/TLS,内容绝对不能错过。
 
 
 

TCP连接

DNS解析返回域名的IP之后,接下来就是浏览器要和该IP建立TCP连接了。为什么是TCP而不是UDP?那是因为HTTP是基于TCP上的。这里涉及到另外一个话题:TCP/IP 模型。这个已经在大学的课本上学过了,我们再复习一下。
 

TCP/IP模型

TCP/IP模型一般分为4层,下面是我用PPT画的。
在这里不得不说OSI七层参考模型,它和TCP/IP模型的区别和联系,继续用PPT画,见下图:

不多解释,OSI的7/6/5层和TCP/TP的应用层对应,2/1层和链路层对应。在实际的应用中,主要还是TCP/IP概念模型,后面的内容主要讲它。

这些都是课本上的,也许忘了(毕竟不是天天用到这些嘛),没关系,我们以最简单的方式来讲解。为了便于理解每层的含义和作用,先看每层有哪些协议,看看有没有自己熟悉的协议。有熟悉的协议,先体会一下。

应用层
我们可以看到,有常用的HTTP/HTTPS/IMAP/SSH/Telnet等都在应用层上(题外话,这一层你用的协议越多,说明你知识越开阔)。相信每个人都用过HTTP/HTTPS,所以我上面说HTTP/HTTP是基于TCP上的。
Wikipedia 这么解释:
The application layer is the scope within which applications create user data and communicate this data to other applications on another or the same host. The applications, or processes, make use of the services provided by the underlying, lower layers, especially the Transport Layer which provides reliable or unreliable pipesto other processes. The communications partners are characterized by the application architecture, such as the client-server model and peer-to-peer networking. This is the layer in which all higher level protocols, such as SMTP, FTP, SSH, HTTP, operate. Processes are addressed via ports which essentially represent services.
 
传输层
没错,最常见的TCP和UDP就在这里,TCP三次握手也在这里。
Wikipedia 这么解释:
The transport layer performs host-to-host communications on either the same or different hosts and on either the local network or remote networks separated by routers.[22] It provides a channel for the communication needs of applications. UDP is the basic transport layer protocol, providing an unreliable datagram service. The Transmission Control Protocol provides flow-control, connection establishment, and reliable transmission of data.
 
IP层
IP层非常重要,可能这么说还不太懂,看看其他协议。大家知道ICMP吗?估计很多人还是说不上来ICMP是什么东西。大家肯定用过ping命令吧,它就是用的ICMP。说到这里,应该有感性的认识了吧。
Wikipedia 这么解释:
The internet layer exchanges datagrams across network boundaries. It provides a uniform networking interface that hides the actual topology (layout) of the underlying network connections. It is therefore also referred to as the layer that establishes internetworking. Indeed, it defines and establishes the Internet. This layer defines the addressing and routing structures used for the TCP/IP protocol suite. The primary protocol in this scope is the Internet Protocol, which defines IP addresses. Its function in routing is to transport datagrams to the next IP router that has the connectivity to a network closer to the final data destination.
 
链路层
这个非常底层了,ARP,NDP,Ethernet都很常见,如果认真看过HTTP抓包,熟悉LVS,Ngnix等提供的负载均衡,应该对ARP不陌生,是的ARP用来查找设备的MAC地址,在LVS做负载均衡时会用到,因为进来的stream的包MAC地址要和出去的Stream包的MAC地址保持一致,因为做负载均衡,有可能会变化,如何解决这个问题,则不在本文的讨论范围内,如有兴趣可以参看LVS的文档。
Wikipedia 这么解释:
The link layer defines the networking methods within the scope of the local network link on which hosts communicate without intervening routers. This layer includes the protocols used to describe the local network topology and the interfaces needed to effect transmission of Internet layer datagrams to next-neighbor hosts.

 

TCP/IP抓包分析

看了前面的内容,还是觉得抽象吗?如果是,不要紧,也在预期内。让我们抓个包,分析认识一下就清楚了。
 
先访问www.qq.com 这个主页,抓到的包如下:
 
看到这里,应该开始有感觉了吧。
HTTP,即应用层,正在访问js.aq.qq.com.
TCP层,src port是62957, dst port是80端口,因为js.aq.qq.com的端口是80。
IP层,用的是IPV4,我的计算机IP地址是192.168.1.2,目标IP是180.153.105.248.
链路层, 我使用的是Apple电脑,我的MAC地址我隐藏了,对端是ZTE设备。
 
再截几个图大家仔细看一下。
链路层
 
IP层

TCP层
 
应用层(HTTP)

 

在这里不加以详解,后面会对TCP以及HTTP层详详细说明。

TCP三次握手与四次挥手

 
TCP三次握手
 
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。
·       第一次握手(SYN=1, seq=x):
客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
发送完毕后,客户端进入 SYN_SEND 状态。
·       第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
·       第三次握手(ACK=1,ACKnum=y+1)
客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1
发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED状态,TCP 握手结束。
三次握手的过程的示意图如下:

 

确实比较抽象,让我们继续通过抓包来分析(访问baidu.com)
这里有三个包。
192.168.1.2是我机器的IP,115.239.211.112是baidu的IP,是不是和上图一致。
具体看270包,192.168.1.2 发送SYN到115.239.211.112,seq=0;
如下图:

 

274包图,115.239.211.112返回SYN 和ACK给192.168.1.2, seq =0, 但是ACK等于SYN里的seq(为0)+1,所以为1

275包图,192.168.1.2收到ACK包后,给115.239.211.112再回一个ACK,ACK#为1:

对于协议的理解,还是多观察,多比对,就知道是怎么回事了。

 

TCP四次挥手
TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。
·       第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
发送完毕后,客户端进入 FIN_WAIT_1 状态。
·       第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
·       第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
·       第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。
服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
 
四次挥手的示意图如下:

这里就不抓包了,可以自行抓包对比看看。

HTTPS证书

越来越多的网站开始使用HTTPS(Apple要求App都须用HTTPS)。对于HTTPS,需要有一个SSL/TLS的鉴权/认证,才能建立TCP链接。
 
下图描述了HTTP和HTTPS的区别。

 

关于SSL/TLS如何交互等,可以参看阮一峰老师的文章http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html 和http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html,写非常清楚的。
借用一张阮一峰老师的图:

 

 

 

我们还是抓包体会一下。

 

对着阮一峰老师图片的流程看:
273包: Client 发送Client Hello给Server。
278包: Server回Server Hello给Client。
283包: Server继续回Server Certificate给Client,要交换证书。
294包:Server继续回Server Key Exchange,交换key。
295包:Server返回Server Hello Done。
截止这里,Server已经做了不少事,接下来轮到Client了。
298包:Client Key Exchange,Change Cipher Spec,Encrypted Handshake Message。到这里就结束了。
 
我们再仔细看看Client Hello的273包,注意标记部分.
接下来再看看Extension,有很多对不对。这里专门说一下 SNI(Server Name Indication), server_name, 我们可以看见它的值是www.baidu.com. 也就是说浏览器发送过来的证书是给baidu这个域名签发的。SNI用来校验该证书是不是为server name提供的域名签发的。如果不是,就会报错。
 
有一种情况特别需要注意,在早期版本的浏览器或者HTTP客户端,SNI可能不包含在该包里的,那么怎么处理呢?如果Server端只有一个证书部署,那简单,就是按照部署的那个证书去判断。如果有多个证书部署呢?比如部署了aaa.com bbb.com ccc.com 三个域名的证书,那么就会按照缺省的去匹配,取决于软件(Apache,Tomcat等)和硬件(F5,Netscaler)怎么配置了。
但是如果已经商用,去改默认配置,会对商用服务有影响,那么可不可以在Client有一些改进呢?如果您正在使用某个HTTP library,可以考虑升级版本是否支持。
 

 

278包的Server Hello

继续看看Server Certificate,即283包
我们看看baidu的证书,打开Chrome就可以看到了,对比一下两图的基本信息,这时是不是觉得更容易理解?我相信答案是肯定的。
294包,Server key Exchange

 

 
295包,Server Hello Done。

 

298包,Client Key Exchange/Change Cipher Spec/Encrypted Handshake Message。

TCP/IP其他

上面都是最基本的东西,在实际的过程中还有包重试,包拼装等,太底层了,大家有兴趣可以找资料看看。

TCP/IP 10问

以下几个问题大部分都可以找到答案。
  1. TCP/IP的4层模型了解吗?每层有哪些常见协议?
  2. TCP/IP的三次握手了解吗?四次挥手是什么,了解多少?
  3. HTTP和HTTPS在TCP握手上有什么不同?SSL/TLS握手流程了解吗?
  4. SSL/TLS的版本有哪些?当前浏览器支持哪些版本?
  5. SNI了解多少?如果SNI没有,该如何校验证书?
  6. TCP与UDP区别在哪里?
  7. 为什么TCP经常会组装包?如何保证包的完整性?
  8. TCP滑动窗口原理是什么?TCP有哪些状态?
  9. MAC地址的是如何定义的?(这个问题太Edge了)
  10. SSL/TLS证书和端口有关系吗?为什么?

今天把TCP/IP, SSL/TLS介绍完了,下一部分是最后一部份了,左右介绍HTTP和Broswer的机制。

posted @ 2018-12-04 18:40  ~雨落忧伤~  阅读(837)  评论(2编辑  收藏  举报