里氏替换原则Liskov substitation principle

为什么叫里氏替换原则? 里氏替换原则在SOLID这五个设计原则中是比较特殊的存在:如果违反了里氏替换原则,不只是降低软件设计的优雅性,很可能会导致Bug。 里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。 Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。 违背 LSP 原则的一个简单示例 一个非常明显地违背 LSP原则的示例就是使用 RTTI(Run Time Type Identification)来根据对象类型选择函数执行。 void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); } 正方形和长方形,违背原则的微妙之处 很多情况下对 LSP 原则的违背方式都十分微妙。设想在一个应用程序中使用了 Rectangle 类,描述如下: public class Rectangle { private double _width; private double _height; public void SetWidth(double w) { _width = w; } public void SetHeight(double w) { _height = w; } public double GetWidth() { return _width; } public double GetHeight() { return _height; } } 违反里氏替换原则的危害 当我们违反了这一原则会带来有一些危害:反直觉。期望所有子类行为是一致的,但如果不一致可能需要文档记录,或者在代码跑失败后涨此知识;不可读。如果子类行为不一致,可能需要不同的逻辑分支来适配不同的行为,徒增代码复杂度;不可用。可能出错的地方终将会出错。 ...

April 19, 2023

C#中引用类型的赋值、浅拷贝和深拷贝

赋值和深复制、浅复制并不是一样的,含义是不一样的。赋值。指的是 “ 等号= ”。它相当于是给引用对象起一个别名。浅度复制和深度复制。指的是类实现 ICloneable接口,重写该接口的唯一方法。注意:不管是深度复制还是浅度复制,都是通过ICloneable接口去实现的。 值类型变量存储的是变量的值,直接储存在栈内存中。引用类型变量存储的是变量所在的内存地址,引用类型变量的实际数据存储于托管堆,变量本身仅仅是一个指向堆中实际数据的地址,存储于栈内存中,通常是四个字节。 值类型Value存储在线程堆栈中。引用类型Reference存储在托管堆上。 全局数据区:存放全局变量,静态数据,常量。代码区:存放所有的程序代码。栈区:存放为运行而分配的局部变量,参数,返回数据,返回地址等。堆区:即自由存储区 为了理解值类型变量和引用类型变量的内存分配模型,我们应先区分两种不同的内存区域——线程堆栈Thread Stack和托管堆Managed Heap。每一个正在运行的程序都对应着一个进程Process,在一个进程内部,可以有一个或多个线程Thread,每个线程都拥有一块“自留地”,成为线程堆栈,大小为1M,用于保存自身的一些数据,如函数中定义的局部变量、函数调用时传送的参数值等。现在我们可以解释第一句话——值类型存储在线程堆栈中,也就是说所有值类型的变量都是在线程堆栈中分配的。另一块内存区域称为堆Heap,在.NET这种托管环境下,堆由CLR(Common Language Runtime)管理,所以又称托管堆Managed Heap。例如使用new关键字创建类的对象实例时,分配给对象的内存单元就位于托管堆中。 1、赋值。赋值和深度复制,浅度复制完全是不同的概念,并没有什么关系,很多文章说赋值对于值类型是深度复制,对于引用类型是浅度复制,这种说法是不正确的,它的本质是在线程栈上产生一样的副本。 2、浅度复制。值类型成员独立,但是引用类型成员共享。 3、深度复制。值类型成员和引用类型成员都是独立的,即完完全全的一个全新的副本,称之为深度复制。 (1)String字符串对象是引用对象,但是很特殊,它表现的如值对象一样,即对它进行赋值,分割,合并,并不是对原有的字符串进行操作,而是返回一个新的字符串对象。但这其实是运算符重载的结果,将string实现为语义遵循一般的、直观的字符串规则。 String对象被分配在堆上,而不是栈上。 (2)Array数组对象是引用对象,在进行赋值的时候,实际上返回的是源对象的另一份引用而已;因此如果要对数组对象进行真正的复制(深拷贝),那么需要新建一份数组对象,然后将源数组的值逐一拷贝到目的对象中。

April 7, 2023

C#实现两个顺序链表的合并

最近开始刷leetcode,刷了一周时间,确实会上瘾,不过能巩固很多基础知识,包括算法和语言。我现在是架构与编码并行。今天遇到的是两个列表的合并,刚开始我自己写了一个笨方法来合并,虽然通过了测试,但是代码质量不高,于是研究了其他人写的代码。 //传入的两个链表本身是从小到大排序好 public ListNode MergeTwoLists(ListNode list1, ListNode list2) { ListNode head = new ListNode(); //定义新的链表头 ListNode current = head;//定义链表最新的next地址 //遍历两个链表,任何一个完毕后就终止循环 while (list1 != null && list2 != null) { if (list1.val < list2.val) { //如果list1的元素值小,则把当前list1赋予current.next,注意这里相当于head.next current.next = list1; list1 = list1.next; } else { current.next = list2; list2 = list2.next; } //将current变量指向current.next,相当于head.next,当进入下一次循环给current.next赋值的时候,相当于是给head.next.next赋值了,一直这么循环到最后 current = current.next; } //head链表的next指向未循环完成的部分 current.next = list1 == null ? list2 : list1; //返回合并后的链表 return head.next; } 我刚开始发懵了,居然没理解这部分代码,因为忽略了“每次循环之后current的引用指向已经发生变化”。

April 7, 2023

整理和总结我自己的软件架构知识体系

最近开始整理自己实际项目中用到的和可能用到的软件架构体系,我打算每种架构都好好写文章,并且根据自己的实际情况定制出相应的架构,我还希望把他编写成一个codesmith模板,可以实现自动化代码。前面7种是具体落地的软件架构,最后1种是架构设计思想。 1、极简数据库访问架构,这种架构非常适合编写一些小软件,里面只有一个SQLHelper文件,并且模仿三层架构,拥有DAL类和BLL类,最后加上一个程序入口Program。 2、简单三层架构,这种架构非常适合开发小型项目,web层就是MVC或者API,并且拥有BLL层和DAL层。 3、N层架构,这是一种适合大型项目的开发架构,但是更多是基于数据库构建的系统,没有基于DDD或者其他好的软件设计模式。 4、基于AutoFac、依赖注入和模板模式的多层架构。 5、基于ABP框架的架构,完全按照DDD设计模式开发,可能有些臃肿,但是非常适合大型软件和系统。 6、基于整洁架构的框架,完全按照DDD并且结合各种软件技术。 7、垂直切片架构,这是一种和传统分成架构完全不一样的架构,适合已经熟悉DDD和各种架构模式的成熟团队使用,并且适合开发微服务架构下的子系统。 8、基于清晰架构的微服务软件架构设计思想,这不是具体的实现,而只是一种架构大局观,用于指导大型的软件架构设计。

April 3, 2023

关于软件的架构:微服务架构、清晰架构、整洁架构、洋葱架构、六边形架构、垂直切片架构、基于DDD分层架构、三层架构

这段时间一直在重构公司的项目代码,所以非常关注软件架构的设计,回顾我的架构接触史:第一次接触C#和.NET的时候就知道了三层架构和多层架构,当时被DAL和BLL等分层震惊了,原来软件设计还有那么多门道;毕业之后工作一直在使用多层架构,接着是MVC架构的出现,这种快速开发web站点的架构横扫整个行业;后来进入大厂工作,接触到了DDD;最近几年微服务架构又风声水起。 整体下来我最开始用了三层架构、然后是多层架构,接着是MVC架构配合多层架构,最后到了微服务架构,现在深刻理解那句话:没有最好的架构,只有最合适的架构。 网上看到有个人总结传统的架构演变之路: 三层架构:这是最简单、同时也是最成熟的软件应用程序架构,它将应用程序组织到三个逻辑和物理计算层中,包括表示层或用户界面、用于处理数据的应用(业务逻辑)程序层和用于存储和管理应用程序关联数据的数据层。大型项目中很少用,但是我还是会在一些小工具和小应用里运用。 多层架构:这种架构是三层架构的升级版,就是增加更多层对系统进行隔离,提高可扩展性和复用性,当然也增加了系统的复杂度。这是我刚毕业那家年用得最多的架构。 基于DDD的分层架构:采用领域驱动设计的思想设计的多层架构,以领域模型为核心、使用依赖注入和控制反转等技术来实现软件,对系统进行解耦,获得最大限度地可维护性和可扩展性。这是我现在在项目中用得最多的架构。 领域模型准确反映了业务语言,而传统数据对象除了简单setter/getter方法外,没有任何业务方法,即失血模型,那么DDD领域模型就是充血模型(业务方法定义在实体对象中)。首次清晰描述了领域驱动的分层实现并统一了业务语言。单一职责、低耦合、高内聚、业务内核沉淀。 六边形架构:让用户、程序、自动化测试和批处理脚本可以平等地驱动应用,让应用的开发和测试可以独立于其最终运行的设备和数据库。 左侧: 代表 UI 的适配器被称为主适配器,它们发起了对应用的一些操作,端口(应用层API)和它的具体实现(controller实现)都在应用内部。右侧: 表示和后端工具链接的适配器,被称为从适配器,它们只会对主适配器的操作作出响应,端口在应用内部(业务接口),具体实现(impl)在应用之外。 **洋葱架构:**在端口和适配器架构的基础上贯彻了将领域放在应用中心,将传达机制(UI)和系统使用的基础设施(ORM、搜索引擎、第三方 API…)放在外围的思路。洋葱架构在业务逻辑中加入了一些在“领域驱动设计”中被识别出来的层次。 围绕独立的对象模型构建应用。内层定义接口,外层实现接口。依赖的方向指向圆心。所有的应用代码可以独立于基础设施编译和运行。职责分离更彻底,高内聚低耦合。更好的可测试性和可维护性。 **整洁架构:**这套架构是站在巨人的肩膀上,把MVC、EBI、端口适配器、洋葱架构、DDD融会贯通,形成了一套落地实践方案。 清晰架构:融合 DDD、洋葱架构、整洁架构、CQRS等一系列架构的信息,这种架构很复杂,你可以根据实际情况来选择。 微服务架构:微服务架构的诞生是因为docker的兴起,因为可以更好地管理各种部署,所以可以将一个大型系统拆分部署到docker之中,每个小服务都能实现集群部署。我个人认为并非一开始就要上微服务架构,架构是不断演化的过程,哪怕是一开始就定位很大系统,可以先划分出基本的服务就行(比如登录系统、支付中心、内容服务、接口开放平台和后台系统),更细的划分可以随着业务的发展不断演化——当然前提是要一直保证代码的整洁和简洁、遵循基本的面向对象代码原则。 垂直切片架构:我是最近才接触到了这种架构,我们的项目最近用到了MediatR和AutoMapper,这两个开源项目的作者Bogard JIMMY BOGARD在自己的项目中实现了这种架构,通过反思洋葱架构和整洁架构等各种分层和抽象得出来的一种架构,作者不建议做太多的分层,因为各种以来会导致大量的问题,作者认为在微服务架构里面特别实用,当然代码也会准从DDD的设计原则。作者认为在所谓传统正常的“N层”或六边形或任何架构中,通过垂直切片移除这些层障碍,并沿着变化轴聚合在一起:

March 24, 2023

垂直切片架构 - Jimmy Bogard

许多年前,我们开始了一个新的长期项目,首先,我们基于洋葱架构构建了它的架构。在几个月内,这种风格开始显示出裂缝,我们从这种架构转向CQRS。随着转向CQRS,我们开始围绕垂直切片而不是层(无论是平面还是同心,它仍然是层)构建我们的架构。从那以后,在过去7到8年左右的时间里,围绕垂直切片架构构建应用程序和系统的所有方式一直是我们独有的方法,我无法想象回到分层架构方法的限制。 传统的分层/洋葱/清洁架构在其目标是单体的: 这种架构方式问题是实际上只适用于系统中的少数典型请求。此外,我倾向于看到这些架构严重拟合,严格遵守依赖关系管理规则。在实践中,我发现这些规则很少有用,并且你开始得到很多关于真正不应该被抽象的抽象(控制器必须与必须使用存储库的服务进行对话)。 相反,我想对我的系统采用量身定制的方法,我将每个请求视为如何处理其代码的独特用例。因为我的系统整齐地分解为“命令”请求和“查询”请求(HTTP-land中的GET与POST / PUT / DELETE),所以向垂直切片架构的移动使我使用了CQRS。 什么是“垂直切片架构”?在这种风格中,我的架构是围绕不同的具体请求功能而构建的,通过这种方法,我们的每个垂直切片都可以自行决定如何最好地满足请求: (对于获取订单,直接使用ORM转换为DTO,对于订单细节使用原生SQL转换为DTO,对于发票,使用基于聚合根的事件溯源,取消订单使用存储过程,这是一种微服务风格) 在所谓正常的“n层”或六边形或任何架构中,通过垂直切片移除这些层障碍,并沿着变化轴聚合在一起: 在应用程序中添加或更改功能时,通常会在应用程序中涉及到许多不同的“层”。现在改为沿着切片垂直将这些功能聚合在一起。 最小化切片之间的耦合,并最大化切片内的聚合。 通过这种方法,大多数抽象都消失了,我们不需要任何类型的“共享”层抽象,如存储库,服务,控制器。有时我们仍需要这些工具,但我们将交叉切片逻辑共享保持在最低限度。 通过这种方法,我们的每个垂直切片都可以自行决定如何最好地满足请求。 “企业架构模式”一书中的旧域逻辑模式不再需要成为应用程序范围内的选择。相反,我们可以从简单的(事务脚本)开始,并简单地重构从我们在业务逻辑中看到的代码气味中出现的模式。新功能只添加代码,您不会更改共享代码并担心副作用。非常自由! 但是,这种方法有一些缺点,因为它确实假设您的团队了解代码气味和重构。如果您的团队不理解“服务”在将逻辑推送到领域时自己却做得太多相关业务逻辑事情,那么这种模式可能不适合您。(服务类似餐厅服务员,服务员不应该做决定,只是协调者,当然如果你想退菜,服务员会决定说:不能退,菜已经烧了。) 如果您的团队确实理解了重构,并且能够识别何时将复杂的逻辑推入域,进入DDD服务应该是什么,并且熟悉其他Fowler/Kerievsky重构技术,那么您会发现这种架构风格能够远远超过传统的分层/同心架构。 来源:https://www.jdon.com/53095.html

March 24, 2023

关于android手机刷机的各类问题总结

这两周来频繁刷机,更新系统又还原系统,弄懂了很多事情,写一篇日志总结,以便以后查询使用,也希望能够看到的人能避免踩坑,也许第三部分更有价值,可以直接跳过去看。 一、关于刷机 刷机有风险,可能会变砖。不过因为很多手机都留了最后一手:FASTBOOT,所以总能把手机就回来 。 刷机的有不少的方法:卡刷、线刷、OTA。卡刷:一般来说卡刷简单的能够理解为在内存卡中刷机,就是进入Recovery中刷入系统;线刷:线刷事实上能够简单的理解为用USB线刷机;OTA:仅仅要在系统中在线升级就能够成功刷机。 刷机之前需要解锁,一般开放点的厂商都会给相应的解锁工具和权限,只要愿意折腾都可以完成解锁。其实解锁和刷机就是为了获得root权限,这样可以对系统做更大的定制,当然也降低手机的安全性。 刷机要根据自己的手机型号找到相应的刷机包(也就是ROM),要注意是卡刷还是线刷,卡刷就是拷贝到手机卡里刷机,线刷就是用电脑的工具通过手机数据线刷机。 小米的MIUI系统有大量的刷机包,我一直都喜欢用欧版,地址是:https://xiaomi.eu 内网地址是:https://sourceforge.net/projects/xiaomi-eu-multilang-miui-roms/ 二、获得root权限 我用的是小米手机,有两种方法,这里从别的地方搬过来,我只把重要的步骤留下,并且指出可能的坑点。 1、通过替换boot.img方式安装Magisk获取Root权限指南 来源原文:https://miuiver.com/install-magisk-for-xiaomi/ 现在获取手机 Root 主要通过安装 Magisk 实现,这篇文章将以新手视角介绍如何安装使用 Magisk。 准备工作:安装 Magisk 过程中需要用到 fastboot 命令,需要刷写手机 boot 或 Recovery 分区,请确保手机已完成 BL 解锁,不然无法进行 。正常情况下不会丢数据,但是建议都先外置备份手机数据再操作。 下载Magisk 地址 https://github.com/topjohnwu/Magisk/releases (选择下载最新版,然后安装到手机)。 提取相应文件:查看手机上运行的系统版本是多少,下载对应系统版本刷机包,从里面提取相应文件(请见下面说明)。如果之前 Ramdisk 查询结果为“是”,请提取 boot.img 文件如果之前 Ramdisk 查询结果为“否”,请提取 recovery.img 文件。 文件提取方法:如果系统版本有线刷包,可以直接解压提取;如果系统版本只有卡刷包,需要从解压的 payload.bin 文件里提取(老机型卡刷包可以直接提取),将提取到的文件复制到手机上。 生成修补文件:手机打开 Magisk 软件,点击 Magisk 卡片中的“安装”按钮。点击“选择并修补一个文件”,选择之前提取到的 boot.img 或 recovery.img 文件,点击“开始”,然后等待生成修补文件。据 Magisk 文档指出,小米有个别机型 Ramdisk 结果可能不能准确检测。如果修补 recovery.img 文件失败,可以尝试用 boot.img 修补,后面安装也遵循 Ramdisk 结果为“是”的做法。将生成的修补文件复制到电脑上(修补文件默认保存在手机内部存储 Download 目录)。 刷写修补文件:将手机关机,长按音量下键+电源键进入 FASTBOOT 模式,用数据线连接到电脑。电脑打开存放修补文件的文件夹,按住键盘 Shift 键,同时鼠标右键点击文件夹空白处,在右键菜单点击“在此处打开 Powershell 窗口”,然后运行下面刷写命令(命令中的文件名请先自行修改)。如果之前修补 boot.img 文件请用这个命令:.\fastboot flash boot magisk_patched-25200_pU6ZV.img;如果之前修补 recovery.img。文件请用这个命令:.\fastboot flash recovery magisk_patched-25200_pU6ZV.img。刷写完成后用下面命令重启手机:.\fastboot reboot ...

February 15, 2023

.NET7下string的改进

string是开发过程中,使用频度最高的类型之一,所以在构建类型时作了很多处理,如“不可变性”,“保留性”等特点。string的常量是在"“引号中进行赋值的。 var str1 = "这是一段文字"; Console.WriteLine(str1); 为了字符串的格式化,引入了$““定义方式,这样就可以在字符串中用{}来标注格式化的内容了。 var str2 = $"时间:{DateTime.Now}"; Console.WriteLine(str2); //输出结果是:时间:1/6/2023 15:37:13 var str2_1 = $"时间:{DateTime.Now:yyyy-MM-dd}"; Console.WriteLine(str2_1); //输出结果是:时间:2023-01-06 为了解决字符串内容的换行,引定入@"",来定义有换行的字符串,比如下面的一条SQL查询,可以按格式化后的样式来定义。$和@可以混用,不分先后。 var str3 = @"SELECT ID ,Question ,Score ,QuestionTypeID ,SubjectTypeID FROM Questions"; Console.WriteLine(str3); var str3_1 = @$"SELECT ID ,Question ,Score ,QuestionTypeID ,SubjectTypeID FROM Questions WHERE Score>{10}"; Console.WriteLine(str3_1); 其实原始字符串还解决了一个问题,就是字符串中有"的问题,以前需要有转义字符来实现,现在原始字符串都搞定了。 Console.WriteLine("\"a\" 是小写的");//通过\来转义 Console.WriteLine(@"""a"" 是小写的");//前缀是@时,通过"转义 最佳demo是json字符串的定义,用原始字符串的方式定义json字符串,最合适不过了。 var jsonString = """ { "irstName": "John", "astName": "Smith", "ex": "male", "ge": 25, "ddress": { "treetAddress": "21 2nd Street", "ity": "New York", "tate": "NY", "ostalCode": "10021" } } """; Console.WriteLine(jsonString) 通过下图,看到的json原始字符串,一目了然: ...

January 12, 2023

武鸣要通高铁了:南宁北站封顶

这两天桂南高铁终点站南宁北站封顶,大概率武鸣今年要通高铁了,很难想象这是县城通的第一条铁路,之前一直没有任何列车通过武鸣。看新闻说以后从南宁北站始发的有:南宁北到广州、南宁北到深圳,南宁北到福州、厦门,南宁北到凭祥,那么无论以后我们在北边还是去了南边,回家都会方便很多。 [caption id=“attachment_1824” align=“aligncenter” width=“1417”] 南宁北站:汉语标题的左边是壮语发音,右边是英语。[/caption] 这条贵阳到南宁的高铁是广西第一条时速350公里的高铁,广西的交通从来都不是优先出海,而是往山区延伸。想起小时候经常听说轰轰烈烈的南昆铁路,长大了才知道这是为他人做嫁衣,这次也差不多。 改开几十年,西南几个“难兄难弟”,一直都只能给珠三角和东部输送廉价劳力,现在经济下行后才开始说要共同富裕,但是只有把社会资源扩容,把饼做得更大,才有可能解决发展不均衡。

January 12, 2023

当你在浏览器地址栏输入一个网址的时候,会发生什么?

作为一个软件开发者,你一定会对网络应用如何工作有一个完整的层次化的认知,同样这里也包括这些应用所用到的技术:像浏览器,HTTP,HTML,网络服务器,需求处理等等。 本文将更深入的研究当你输入一个网址的时候,后台到底发生了一件件什么样的事 1 首先,你得在浏览器里输入要网址: 2 浏览器查找域名的IP地址 导航的第一步是通过访问的域名找出其IP地址。DNS查找过程如下: 浏览器缓存 – 浏览器会缓存DNS记录一段时间。 有趣的是,操作系统没有告诉浏览器储存DNS记录的时间,这样不同浏览器会储存个自固定的一个时间(2分钟到30分钟不等)。 系统缓存 – 如果在浏览器缓存里没有找到需要的记录,浏览器会做一个系统调用(windows里是gethostbyname)。这样便可获得系统缓存中的记录。 路由器缓存 – 接着,前面的查询请求发向路由器,它一般会有自己的DNS缓存。 ISP DNS 缓存 – 接下来要check的就是ISP缓存DNS的服务器。在这一般都能找到相应的缓存记录。 递归搜索 – 你的ISP的DNS服务器从跟域名服务器开始进行递归搜索,从.com顶级域名服务器到Facebook的域名服务器。一般DNS服务器的缓存中会有.com域名服务器中的域名,所以到顶级服务器的匹配过程不是那么必要了。DNS递归查找如下图所示: DNS有一点令人担忧,这就是像wikipedia.org 或者 facebook.com这样的整个域名看上去只是对应一个单独的IP地址。还好,有几种方法可以消除这个瓶颈: 循环 DNS 是DNS查找时返回多个IP时的解决方案。举例来说,Facebook.com实际上就对应了四个IP地址。 负载平衡器 是以一个特定IP地址进行侦听并将网络请求转发到集群服务器上的硬件设备。 一些大型的站点一般都会使用这种昂贵的高性能负载平衡器。 地理 DNS 根据用户所处的地理位置,通过把域名映射到多个不同的IP地址提高可扩展性。这样不同的服务器不能够更新同步状态,但映射静态内容的话非常好。 Anycast 是一个IP地址映射多个物理主机的路由技术。 美中不足,Anycast与TCP协议适应的不是很好,所以很少应用在那些方案中。大多数DNS服务器使用Anycast来获得高效低延迟的DNS查找。 3 浏览器给web服务器发送一个HTTP请求 因为像Facebook主页这样的动态页面,打开后在浏览器缓存中很快甚至马上就会过期,毫无疑问他们不能从中读取。 所以,浏览器将把一下请求发送到Facebook所在的服务器: GET http://facebook.com/ HTTP/1.1 Accept: application/x-ms-application, image/jpeg, application/xaml+xml, [...] User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; [...] Accept-Encoding: gzip, deflate Connection: Keep-Alive Host: facebook.com Cookie: datr=1265876274-[...]; locale=en_US; lsd=WW[...]; c_user=2101[...] GET 这个请求定义了要读取的URL: “http://facebook.com/”。 浏览器自身定义 (User-Agent 头), 和它希望接受什么类型的相应 (Accept and Accept-Encoding 头). Connection头要求服务器为了后边的请求不要关闭TCP连接。 ...

January 10, 2023