【前端】vue 报错:The template root requires exactly one element 写在最前面Prettier - Code formatter插件解决 Vue 报错:The template root requires exactly one element错误原因示例 解决方法更复杂的示例使用 Fragment 解决问题 小结 🌈你好呀!我是 是Yu欸 🌌 2024每日百字篆刻时光,感谢你的陪伴与支持 ~ 🚀 欢迎一起踏上探险之旅,挖掘无限可能,共同成长! 写在最前面 封面图内含彩蛋 ~
原因:
Vue2要求模板中的<template>中只能有一个根元素,也就是说最外层不能同时包含其他元素或者文本。如果模板中出现多个根元素,就会触发这个错误。
解决:
vue2:所以,删除多的根元素,or在最外层再加一个div包裹其他根元素
vue3中,则去掉了这个限制。引入了一个新的特性叫做 Fragment,这允许我们在不需要额外的 DOM 元素的情况下使用多个根元素。
可以参考
https://blog.csdn.net/Y_soybean_milk/article/details/124338835
附言:如果你是用vue3,建议用Volar来替换vetur
参考:https://blog.csdn.net/a417125111/article/details/100996750
Prettier - Code formatter插件 此外,建议安装vue代码格式化插件,提升vue编程幸福感
在扩展商店中搜索( Prettier - Code formatter )点击安装
参考:https://blog.csdn.net/qq_38188228/article/details/118295851
解决 Vue 报错:The template root requires exactly one element 在使用 Vue.js 开发前端应用时,可能会遇到这样的错误提示:“The template root requires exactly one element”(模板根需要一个且仅一个元素)。这个错误是由于 Vue 的模板系统的一个基本要求造成的,即每个组件的模板必须有且仅有一个根元素。下面我们来深入探讨这个问题并提供解决方案。
恐怕没有哪一家本土医疗装备企业能像东软医疗一样,每一段成长的升维都发生在中国医疗科技跃迁史最重要的节点上。
在工业制造领域,医疗装备产业由于涉及数十个学科领域,其技术复合程度毫不逊于今天公众所熟知的EUV光刻机,是一门技术、资本密集型的产业,能够代表一国的工业制造和创新水平。
其中,医学影像设备被认为是医疗科技中的最具含金量也是保障人类健康的“重器”。自1971年全球第一台x射线CT机诞生以来,欧美日等发达国家企业凭借压倒性的技术和资本优势,牢牢把持全球市场。
而在14亿人口的中国,东软医疗的名字则注定与中国医学影像设备行业的成长,紧密连接。
第一国产CT、第一台国产超导磁共振、第一台国产X射线机、第一台国产三维彩超、第一台无轨悬吊智能血管机……
无数个中国“第一台”是中国医疗科技企业的崛起之路,也意味着我们可以在跨国巨头之外,有更加自主的市场选择权。
从诞生之日,东软医疗就以一己之力肩负起国产影像设备的突破,但对于寻求持续发展和壮大的企业来说,融入外部资本是实现目标的必经之路。
于是,东软董事长刘积仁早早便将东软医疗置于独立发展的航道上。
先是在发展初期积极与外资合作,以快速提升自身管理和技术水平;后在中高端设备的突飞猛进和关键零部件的攻坚战上,通过引入社会资本成功突围……
如今,在东软医疗携光子CT等革命性技术开始“弯道超车”时,中国通用技术集团等“国家队”以产业投资人的角色成为东软医疗的资本后盾,一切又变得不同了。
拥有自主技术和相对完整产业链能力的东软医疗,终于可以将“成为全球医疗服务的最佳价值创造者”的目标变成全力释出的新质生产力,在国家资本意志和本土创新力的双重加持下,狂飙突进。
从无到有
“引外资”立市场
1988年秋,一句“科学技术是第一生产力”打开了我们“以高科技领域的一个突破带动一批产业发展”的格局。
9年后,沈阳近郊刚刚落成的高新技术产业开发区东大软件园里,中国第一台临床应用CT机下线,中国成为美、德、日、荷之后,第五个能够自主制造CT机的国家。
可以说,这就是中国医疗装备产业本土化发展的发端。以自主之精神,填补医疗装备产业之空白,开创属于中国的品牌,东软医疗唤起的是整个行业的血脉觉醒。
从“0”到“1”的突破,或许在今天看来就是游标卡尺的复刻和核心技术的“另辟蹊径”,但在那个一穷二白的年代,“零突破”就是天大的事。
毕竟在没有国产CT的年代,国人看病检查需要担负沉重负担,我们没有一点点议价的权力。
东软医疗“咬碎后槽牙”推出一系列“创世纪”的产品,不仅打下来了国内医疗检查的价格,也赢得了跨国公司的资本青睐。
△1997年中国第一台国产CT诞生于东软
2004年,飞利浦(中国)投资有限公司及飞利浦电子中国集团向东软医疗抛出橄榄枝,与其共同出资设立了“东软飞利浦”。
在那个以吸引、利用外资为主和“以市场换技术”的年代,“东软飞利浦”的诞生为中国医疗装备行业带来了CT、X线、MRI以及超声系列产品等的研发、生产的全面突破。
飞利浦的资金注入为东软医疗在医疗设备技术领域的初步积累夯实了基础,也帮助其正式迈入国际合作的历史快车道,为东软医疗日后的全球化布局提供了更广阔的视野和战略资源。
需要特书一笔的是,当初“东软飞利浦”是以东软医疗知识产权入股,飞利浦现金投资的合作形式落地的。这与当时几乎都是外方投技术,中方投资金的合作模式完全不同。
能够让全球顶级的科技公司认同东软医疗的技术创新能力,从而投资中国本土企业,在那个时期绝对是一件很“炸裂”的事。
而从一开始,就将“拥有100%的自主知识产权”奉为践行中国医疗科技发展的必然路径,东软医疗凭此牢牢将命运把握在自己的手中,不仅迅速在本土市场上打开局面,不断提升市场占有率,在国际化舞台上“借船出海”的东软医疗同样疾风速进。
2013年时,东软医疗收购飞利浦所持的原东软飞利浦的51%股权,十年合作完美收官。
双方资源协同、优势互补,使飞利浦不仅获得了丰厚回报,同时也拓展了其临床使用型产品线;东软医疗则在供应链管理、质量体系建设上完成了“与国际接轨”的重要一步。
从有到强
“引社资”冲高端
新世纪第二个十年,中国经济迎来发展最快的一个历史阶段。而在这个阶段上,大规模的医疗基础设施建设催生了本土医疗设备厂商的集体爆发。
作为本土医疗装备的领军级企业,东软医疗已渐渐成为产品线覆盖最为全面,研发制造能力相对均衡,综合竞争力优势明显的头部企业。
毫无疑问,东软医疗这样优质的投资标的,又怎会被资本方轻视。
而这个阶段,也正是国内社会资本PE/VC机构最为活跃的阶段,他们在互联网领域带动的投资热潮,又迅速向壁垒更高的工业制造领域扩散。
本土医疗装备制造企业在2015年左右的那轮资本热潮中,各个估值水涨船高。这一次,东软医疗则被高盛、弘毅等大牌机构所垂青。
2014年12月间,东软集团与弘毅投资、高盛、CPPIB(加拿大养老基金投资公司)、通和资本等机构达成战略投资协议,旗下东软医疗与东软熙康共计获得37亿元人民币的投资。
这一规模体量的投资,刷新了彼时中国医疗器械领域单笔最大融资纪录,也再一次验证了市场方、资本方对于东软医疗雄心勃勃投入技术创新的超高信心。
随着资金的注入,在沈阳之外,东软医疗北京、上海、广州、南京以及韩国京畿道的研发中心相继落成,研发人员数量迫近800人,占员工总数三成;公司核心专利申请和软件著作权数量近1700件。
此时东软医疗“向上再向上”的雄心壮志,已然昭然若揭。
有了头部社会资本的强力加持,对于开始攻坚高端产品和关键技术的东软医疗而言,自然是如虎添翼。
在这个阶段上,东软医疗的技术和产品出现了“井喷”之势。
在CT这样的优势项目上,东软医疗一路做到了行业技术顶层的512层CT,同时也在探测器、高压发生器以及数据采集系统等核心部件、关键技术上实现了自主可控。
在另一个打破垄断,填补空白的项目——数字减影血管造影系统DSA上,2016年立项到2019年NeuAngio 30C、30F、30F Flex陆续上市,再到首创DSA/CT一站式综合诊疗产品,东软医疗出手就是“高端”。
而随着最能显现东软医疗技术突围的“重器”——全球首台0.235秒超宽体CT、业界首款双能3.0T磁共振、全球首创一站式智慧导管室、中国首款180皮秒级PET/CT以及全球首创螺旋容积调强放疗平台等创新产品如“下饺子”一般问世,“全线高端”的东软医疗已然成了行业强者。
△东软医疗自主研发的首款3.0T磁共振
“全线高端”之外,还有“全栈智能”的加持。
依托强大的技术优势和丰富的软件经验,东软医疗将人工智能技术嵌入全线影像产品和全域工作流,从医技科室拓展到临床科室乃至科研教学场景,实现了从设备端到应用端的全栈智能。
东软医疗顺应时代要求,将新质生产力,转化为产品竞争力。其全面超越、引领行业的伏笔,已跃然纸上。
从强到超
“引国资”打开新局
2024年7月,东软医疗再一次以创纪录的融资事件,刷屏了整个医疗装备产业。
这一次,是中央直属的国有重要骨干企业中国通用技术集团旗下的通用技术资本成为东软医疗的第一大股东,此次跟投的还有中国国有企业混合所有制改革基金,同样是央企中国诚通集团旗下的基金。
实际上,疫情前后几年医疗健康领域的投融资事件虽然依旧保有一定活跃度和数量级,但以2023年统计数据看,大部分投融资案例金额都在5000万元以下,并没有出现影响市场格局的资产交易。
虽然双方均未披露此次投融资的规模,但以东软医疗此前的估值和中国通用技术集团实际成为前者的最大股东,这意味此次交易规模放在整个医疗健康投融资领域,也都是极其罕有的。
据悉,从初次交换意向到双方战略合作协议落笔,一共不过173天。这在国家资本投资民营企业的案例中,同样罕有。
为何产业资本“国家队”对投资东软医疗如此坚定?我们或许能从通用技术集团董事长、党组书记于旭波的话里找到答案。
“战略投资东软医疗,不仅完善了通用技术集团在医药医疗健康产业的布局和生态,形成良好的业务协同效应,更是通用技术集团立足国家战略,承担国家使命,保证国家医疗健康安全的关键行动。”
能担纲国家使命的企业,必然要在行业和市场上有举足轻重的分量。大体量投资东软医疗,通用技术集团必然也是对其有着极高的价值认同的。
正如前述,东软医疗可以说是中国医学影像设备领域的开创者,不仅填补了无数国产空白,更通过不懈坚持实现了自主知识产权上的全面突围。
但对于东软医疗等中国医疗设备企业而言,比肩全球优秀企业绝不是终极目标,随着越来越多新兴技术在本土市场的大规模应用,本土医疗设备企业迎来了弯道超车的最好机会。
所谓新质生产力就是以高科技、高效能、高质量特征的新发展理念的先进生产力质态,而医疗装备产业本身就是需要不断进行科学技术跃迁来实现高效能、高质量的典型产业。
今年5月,东软医疗自主研发的光子计数CT获得首幅人体图像,这意味着在光子CT这种能够引发行业技术革命的新赛道上,东软医疗已经成为本土企业的创新引领者。
从追赶到超越,已经在中国很多产业领域发生,亦如早些年的家电产业,亦如今天的新能源产业,如今睥睨全球市场,引领和主导行业发展,其实都经历过技术跃迁的阶段。
可以预见,获得通用技术集团资源加持的东软医疗,将再一次实现技术跃迁,向“领跑”的新阶段加速迈进。
1. 前言 如果您想观看完整的系列教程,不妨移步到我的《HarmonyOS Next 应用开发教程》专栏中,进行仔细查阅 : https://blog.csdn.net/qq_35163541/category_12723932.html
前面,我们已经学习了预览器的使用,可以帮我们在开发鸿蒙应用的时候,实时的查看效果,并且针对不用设备进行预览。
不过,如果想彻底体验鸿蒙应用的所有功能,仅靠预览器还远远不够。所以,DevEco Studio提供了模拟器(Emulator),为开发者提供了运行和调试 HarmonyOS 应用/服务的便捷途径。
模拟器还原了真实设备的基本功能,如屏幕旋转、音量调节、模拟的硬件传感器和指定设备的位置等。这使得您无需拥有不同类型的物理设备,就可以在各种虚拟环境中轻松测试您的应用程序。
在某些情况下,在模拟器上进行应用测试,相比于在实际物理设备上的测试,有着更快速、更高效的体验。例如,模拟器提供了摇一摇的操作模拟,让您能够轻松触发摇一摇功能。
总的来说,无论是快速原型验证还是功能测试,模拟器都是满足您测试需求的最佳选择。几乎能满足大部分需求,不过对于一些特定功能,模拟器也不能表现得十分完美,例如,获取相机、指纹、人脸识别等依赖硬件的功能。这些都会与真机存在差异性,这个将在后面介绍。
2. 模拟器介绍 下面我们来了解一下如何利用 HarmonyOS 模拟器调试应用。首先给大家介绍一下模拟器的版本以及模拟器的获取方式。
当前模拟器支持手机、折叠屏、平板、二合一四种设备,支持的pc平台,包括 Mac arm、Mac X86、Windows X86 三种,支持的 API 版本包含 api11 和 api12。在缺少真机的情况下,您可以使用模拟器进行HarmonyOS 应用的开发与调试。
3. 模拟器获取 如何在DevEco Studio内完成模拟器镜像的下载与安装?
首先打开 IDE,点击 Tools 下的 Device Manager,进入到设备管理页面。
如果您是第一次使用,打开的界面会提示您需要登录华为账号,界面如下:
点击sign in登录,若提示该帐号没有权限,弹出截图如下:
这是由于当前下载模拟器镜像需先申请参加模拟器Beta活动。若提示该帐号没有权限,请先点击“Submit the application form”完成权限申请。
Submit the application form 跳转地址 :
https://developer.huawei.com/consumer/cn/activity/201714466699051861/signup
申请界面截图 :
若是账号已获取权限(已获取界面如下),在设备管理页面的底部栏中有一个设置模拟器的存储路径,单击edit按钮。接下来单击右下角的 New Emulator 按钮创建模拟器。
在虚拟设备配置界面,您可以选择不同设备的模拟器镜像进行下载、更新与删除。在这里,我们以phone设备为例,点击下载,并在下载完成后点击右下角的next。
接下来配置模拟器信息,可根据电脑的内存情况配置模拟器的运行内存和存储大小。配置完成后,点击Finish,完成模拟器的创建,模拟器就可以运行起来了。
模拟器创建完毕后的界面如下,并可以通过三角形按钮进行启动模拟器。
4. 模拟器的常规使用 将模拟器下载安装完成后,来了解一下模拟器使用的方式与特性。
模拟器提供了一系列基础交互,如下表 :
目录
项目创建
前端代码实现
约定前后端交互接口
需求分析
接口定义
Hutool工具
实现服务器端代码
引入依赖
获取验证码
验证码校验
调整前端代码
随着安全性的要求越来越高,目前许多项目中都使用了验证码,验证码也有各种类型,如 图形验证码、短信验证码、邮件验证码、人脸识别等,这些不同类型的验证码可以根据实际需求和安全性要求进行选择和应用,保护网站和用户免受恶意攻击
在本篇文章中,我们来学习图形验证码的实现
验证码的实现方式有很多,可以由前端实现,也可以由后端进行实现,也有很多的插件和工具包可以使用,在这里,我们使用 Hutool 提供的小工具实现
验证码需要实现功能:
1. 页面生成验证码,点击图片可进行刷新
2. 输入验证码,点击提交,验证用户输入验证码是否正确,正确则进行页面跳转
项目创建 我们首先创建项目,并引入相关依赖
前端代码实现 接下来,我们实现两个简单的前端界面:
1. 验证码界面
2. 验证成功后跳转的界面
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>验证码</title> <style> #inputCaptcha { height: 30px; vertical-align: middle; } #verificationCodeImg{ vertical-align: middle; } #checkCaptcha{ height: 40px; width: 100px; } </style> </head> <body> <h1>输入验证码</h1> <div id="confirm"> <input type="text" name="inputCaptcha" id="inputCaptcha"> <img id="
前言:
小编在近日学习了单链表的知识,为了加强记忆,于是诞生了这一篇文章,单链表是数据结构比较重要的知识,读者朋友们一定要去好好的学习!这个可以说是比顺序表更好用的线性表,下面废话不多说,开始进入单链表的学习喽!
目录:
1.单链表 1.1.链表的概念与单链表 1.2.单链表的结构 1.3.单链表的性质 2.单链表功能的代码实现 2.1.单链表的打印 2.2.单链表的尾插 2.3.单链表的头插 2.4.单链表的尾删 2.5.单链表的头删 3.还有一些函数没写,由于小编不想让篇幅太大,所以分成了两部分 正文:
1.单链表
1.1.链表的概念
链表是一种物理结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表也有很多的种类,比如单链表,双链表等等,小编今天讲述的就是单链表,下面小编来讲述一下单链表的结构!
1.2.单链表的结构
引例:
对于单链表的结构,小编先给出一个引例:火车我相信各位读者朋友都坐过,没坐过也应该见过,火车是由车头和一节一节的车厢组成,如下图所示:
在人们乘车少的时候,火车可以减少车厢,具体减少哪个车厢,这是可以随机指出的,并不一定就是去掉最后一个车厢,我们也可以把中间的给删除,在旅游旺季的时候,我们同样也可以加上几节车厢来应对人多车少的情况。
1.2.1.结点的概念
单链表就类似火车车厢一样,它也可以随时的减少,随时的增加,我们把单链表中的每一表称之为结点,所以可以这么说,单链表是由一个一个结点构成的,单链表和顺序表最大的不同,就是单链表当中的结点是一个一个动态内存申请出来的,不是和顺序表一样基于数组来进行书写的,下面小编将会给各位讲述一下单链表的结构:
·1.2.2.单链表的结构
小编在上图给各位读者朋友了单链表的结构图,细细一看,我们可以看出每一个结点,里面都存储着数据类型和一个地址,不难发现,结点里面的地址都对应着下一个结点,所以单链表就是靠地址来进行连接的,那么肯定有读者朋友会感到疑惑,这么一看,单链表的物理结构不还是连续的?其实这个说法是错误的,可能每一个单链表都隔着很远才进行的链接,所以单链表是不连续的,不和顺序表的一样,我们可以看到,单链表中最后一个结点指向的是NULL,这里展示了单链表也是有始有终的,下面我们来简单介绍一下单链表的性质
1.3.单链表的性质
单链表的性质可以分为三个:
1.单链表在逻辑上是连续的(线性表统一的性质)在物理结构是不连续的。
2.结点一般都是从堆上申请的。
3.从堆上申请的空间,是按照一定策略分配出来的,每次申请的空间可能连续,也可能不连续
其实性质我们用多了,自然也会记住了,对于编程的学习,我们不是靠死记硬背下来的,而是不断的去应用,应用多了自然就熟悉了! 我们已经讲述了单链表的概念和性质,我们可不能纸上谈兵,下面,小编将带着大家手动的一步一步的去实现单链表!
2.单链表功能的代码实现
在讲述那些单链表的增删查改之前,我们现在肯定要先创建一个单链表,我们已经学习了顺序表和单链表的知识,单链表的结构图也在上面进行展示了,那么下面我们就可以实现单链表的创建了,对于数据部分,我们不一定是存储整型,浮点型等等,所以我们可以类似顺序表用typedef关键字来对类型改名,后面我们可以统一进行替换。下面是代码的实现:
typedef int SLdate; //方便后面整体类型的改变 //创立一个单链表 typedef struct Slist { //先设置一个类型 SLdate date; struct Slist* next; //存放下一个节点的地址 }SLTNode; 此外,对于单链表代码的书写,小编同样是分成了三个文件,与顺序表一样,这三个文件分别是头文件(用于单链表的创建,各种函数的声明),源文件(函数的实现),源文件(代码的测试),我们每写完一个函数,一定一定记着先测试,免得到后面在慢慢改,这样显得太麻烦了,下面,我们开始实现一一函数的功能!
2.1.单链表的打印
虽然我们还没有开始放置数据,但我们一定要先学会单链表的打印,这个与我们正常的打印是不同的! 首先,我们肯定要创建一个函数,下面是代码呈现:
void SLTprintf(SLTNode* phead);//此时phead代表的是头节点,就是第一个节点 正如代码解释所说,phead属于头结点,我们想要打印每一个结点的数据,肯定要先知道它的头节点,之后我们可以通过它的头结点来开始对下一个节点进行遍历直到遇到NULL,这样我们便可以停止打印,所以不难想到,这里我们用到了循环的知识,通过每一次循环来打印数据,之后再让下一个结点代替当前结点,不过在这之前,我们应该做到对于头结点不让它做出改变,因为我们倘若任由头节点进行改变,那么之后我们在这个函数中会再也不会找到链表的头,这样就得不偿失了,所以我们用一个新的结点来存放头节点,让它做出对应的的改变,下面是代码的呈现:
void SLTprintf(SLTNode* phead) { SLTNode* pour = phead; //这么做是为了保证头节点不会发生改变 while (pour) { printf("
在SQL Server中,WHILE语句用于重复执行一个代码块,直到指定的条件变为假为止。语法如下:
-- 初始化变量 DECLARE @variable_name int -- 设置初始值 SET @variable_name = initial_value -- WHILE循环 WHILE condition BEGIN -- 循环体代码 -- 更新变量值 SET @variable_name = new_value END 在以上语法中:
@variable_name是一个用户定义的变量,用于控制循环。initial_value是变量的初始值。condition是一个布尔表达式,用于决定是否继续循环。new_value是一个表达式,用于更新变量的值。 下面是一个使用WHILE循环的示例,计算1到10的和:
DECLARE @i int DECLARE @sum int SET @i = 1 SET @sum = 0 WHILE @i <= 10 BEGIN SET @sum = @sum + @i SET @i = @i + 1 END SELECT @sum 变量@i用于计数,变量@sum用于累加和。WHILE循环会在@i小于等于10时重复执行循环体,每次循环都会更新@sum的值。最终,SELECT语句会输出结果为55,即1到10的和。在SQL Server中,WHILE语句用于重复执行一个代码块,直到指定的条件变为假为止。语法如下:
-- 初始化变量 DECLARE @variable_name int -- 设置初始值 SET @variable_name = initial_value -- WHILE循环 WHILE condition BEGIN -- 循环体代码 -- 更新变量值 SET @variable_name = new_value END 在以上语法中:
文章目录 强烈推荐引言解决问题1. 配置管理的集中化2. 配置的版本控制3. 环境特定配置4. 配置的动态刷新5. 安全管理敏感数据6. 配置的一致性 组件1. **配置服务器(Config Server)**2. **配置客户端(Config Client)** 配置示例配置服务器(application.yml)配置客户端(bootstrap.yml) 工作原理示例场景场景 1:多个环境配置管理示例Git 仓库中的配置文件结构:application.yml (通用配置):application-dev.yml (开发环境配置):application-test.yml (测试环境配置):application-prod.yml (生产环境配置):配置客户端(bootstrap.yml): 场景 2:配置变更的自动传播示例更新前的 application-prod.yml:更新后的 application-prod.yml:触发配置刷新 场景 3:安全管理示例Git 仓库中的配置文件:application.yml(使用加密值):配置服务器的 application.yml:配置客户端(bootstrap.yml):应用程序代码示例: 常用的参数说明配置服务器(Config Server)Git 仓库配置本地文件系统配置 配置客户端(Config Client)示例配置文件配置服务器(application.yml)配置客户端(bootstrap.yml) 总结强烈推荐专栏集锦写在最后 强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能
引言 Spring Cloud Config 是 Spring Cloud 套件中的一个工具,提供了在分布式系统中对外部化配置的服务器端和客户端支持。
解决问题 Spring Cloud Config 主要解决以下几个问题:
1. 配置管理的集中化 在一个分布式系统中,管理多个微服务的配置是一个挑战。Spring Cloud Config 提供了一个集中化的配置管理解决方案,将所有微服务的配置存储在一个中央仓库(例如 Git 仓库)中,从而简化了配置的管理和维护。
2. 配置的版本控制 通过将配置文件存储在 Git 等版本控制系统中,Spring Cloud Config 支持配置的版本控制。每次更改配置都会被记录,允许回滚到以前的版本。这对于追踪配置变更和恢复到稳定状态非常有用。
3. 环境特定配置 不同环境(如开发、测试、生产)可能需要不同的配置。Spring Cloud Config 支持根据环境动态加载配置文件,使得在不同环境中部署和运行应用程序更加方便和可靠。
🌈个人主页: 鑫宝Code
🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 Metrics: 衡量算法性能的关键指标引言1. 分类任务的评估指标1.1 准确率 (Accuracy)1.2 精确率与召回率 (Precision & Recall)1.3 F1分数 (F1 Score)1.4 ROC曲线与AUC (Receiver Operating Characteristic & Area Under Curve) 2. 回归任务的评估指标2.1 均方误差 (Mean Squared Error, MSE)2.2 均方根误差 (Root Mean Squared Error, RMSE)2.3 平均绝对误差 (Mean Absolute Error, MAE) 3. 集群分析的评估指标3.1 轮廓系数 (Silhouette Coefficient) 4. 结论 Metrics: 衡量算法性能的关键指标 引言 在算法开发和机器学习项目中,选择正确的评估指标(metrics)至关重要。它们不仅帮助我们理解模型的性能,还指导我们如何优化模型以达到预期的目标。本文将探讨几种常用的评估指标,并解释它们在不同场景下的应用。
1. 分类任务的评估指标 1.1 准确率 (Accuracy) 准确率是最直观的指标,计算的是预测正确的样本数占总样本数的比例。然而,在类别不平衡的数据集上,准确率可能会产生误导。
Accuracy = TP + TN TP + TN + FP + FN \text{Accuracy} = \frac{\text{TP + TN}}{\text{TP + TN + FP + FN}} Accuracy=TP + TN + FP + FNTP + TN
欢迎来到 CILMY23的博客
🏆本篇主题为:双指针算法之11. 盛最多水的容器
🏆个人主页:CILMY23-CSDN博客
🏆系列专栏:Python | C++ | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算法专题 | 代码训练营
🏆感谢观看,支持的可以给个一键三连,点赞关注+收藏。
题目: 一、题目解析 第一眼的时候看到题就是看题给我们什么信息
求盛水最大水量线的高度是height[i]整个数组的长度是n也就是height[size] 通过信息,发现突破口要从盛水的最大水量下手,那我们画个图看看,如何求最大水量。 假设我们有个i1 和 i9,这两条线的水量是 h[1] *(i1 - i9),i2和i9这两条线的水量是h[2] *(i2 - i9)
所以我们可以看出要求最大水量,其实也就是最短高度乘两个i之间的距离。
二、算法原理 通过题目分析可以看出我们需要求出面积,就需要有两个变量,用left 代替i1,用right 代替i2
这样两个不断靠近的时候就能求出面积了。所以暴力破解的情况下,就是让i1 - i9一个个尝试过去,求出一堆的面积,然后在一堆的面积中拿出最大的值。
兴高采烈的写完后发现,超时了。
哇,它居然超时,于是只能再想想有没有什么地方可以减少时间的,分析了一下暴力破解的空间复杂度,这种双循环的时间复杂度是O(n^2)。
经过分析发现以下三种情况,w始终是在变小的啊,那我们想求出最大的area,只能通过第三种情况了。 因此每次移动只需要移动h小的那个就够了。
三、代码编写 1.暴力破解
class Solution { public: int maxArea(vector<int>& height) { int i = 0; int j = height.size()-1; int area = 0; while(i<height.
可以在可视化的工具通过点击来操作kafka完成主题的创建,分区等操作
注意: 安装完后桌面不会有快捷方式,需要去电脑上搜索,或者去自己选的安装位置找到发送快捷方式到桌面!
连接配置 创建主题 删除主题 主题下的数据查看 数据显示问题说明 修改工具的数据显示类型
发送消息数据到kafka Kafka的Python API的操作 模块安装 纯Python的方式操作Kafka。
准备工作:在node1的节点上安装一个python用于操作Kafka的库
安装kafka-python 模模块 ,模块中提供了操作kafka的方法
在线安装
在node1上安装就可以,需要保证服务器能够连接网络
安装命令: python -m pip install kafka-python -i https://pypi.tuna.tsinghua.edu.cn/simple 离线安装
将kafka_python-2.0.2-py2.py3-none-any.whl安装包上传服务器software目录下进行安装
安装命令: pip install kafka_python-2.0.2-py2.py3-none-any.whl 模块使用 API使用的参考文档: Usage — kafka-python 2.0.2-dev documentation
模块中封装了两个类,
一个是生成者类KafkaProducer,提供了向kafka写数据的方法
另一个是消费者类KafkaConsumer,提供了读取kafka数据的方法
完成生产者代码 生成者类KafkaProducer,提供了向kafka写数据的方法
send(topic,valu)方法: 发送消息 topic参数:指定向哪个主题发送消息 value参数:指定发送的消息数据 ,数据类型要求是bytes类型 示例:
# 导包 from kafka import KafkaProducer # 编写代码 if __name__ == '__main__': # 创建生产者对象并指定对应服务器 producer = KafkaProducer(bootstrap_servers=['node1:9092']) # 发送消息 for i in range(1,101): future = producer.
目录 1.Retrieve1.基本语法2.SELECT列1.全列查询2.查询字段为表达式3.为查询结果指定别名4.结果去重 3.WHERE条件1.比较运算符2.逻辑运算符3.示例 4.结果排序1.基本语法2.示例 5.筛选分页结果 1.Retrieve 1.基本语法 SELECT [DISTINCT] * | {column [, column] ...} [FROM table_name] [WHERE ...] [ORDER BY column [ASC | DESC], ...] LIMIT ... WHERE后面没有办法使用别名 此时数据还没有被筛选出来,别名还没有生效 ORDER BY后面可以使用别名 先要有合适的数据,才能排序 LIMIT只有数据准备好了,才能显示,LIMIT的本质功能是"显示"示例数据:// 创建表结构 CREATE TABLE exam_result ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL COMMENT '同学姓名', chinese float DEFAULT 0.0 COMMENT '语文成绩', math float DEFAULT 0.0 COMMENT '数学成绩', english float DEFAULT 0.0 COMMENT '英语成绩' ); // 插入测试数据 INSERT INTO exam_result (name, chinese, math, english) VALUES ('唐三藏', 67, 98, 56), ('孙悟空', 87, 78, 77), ('猪悟能', 88, 98, 90), ('曹孟德', 82, 84, 67), ('刘玄德', 55, 85, 45), ('孙权', 70, 73, 78), ('宋公明', 75, 65, 30); Query OK, 7 rows affected (0.
目录
1.类的默认成员函数
2.构造函数 3.析构函数 4.拷贝构造函数
5.运算符重载 5.1 赋值运算符重载 5.2 使用运算符重载等特性实现日期类
6.取地址运算符重载
6.1 const成员函数 6.2 取地址运算符重载
1.类的默认成员函数 默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:
第一:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。 第二:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
2.构造函数 构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init。
构造函数的特点:
(1)函数名与类名相同。
(2)⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
(3)对象实例化时系统会⾃动调⽤对应的构造函数。
(4)构造函数可以重载。
(5)如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤户显式定义编译器将不再⽣成。
(6)⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
(7)我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Date { public: // 1.⽆参构造函数 Date() { _year = 1; _month = 1; _day = 1; } // 2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } // 3.
文章目录 基于香橙派 AIpro搭建二维码分类模型及其Flask服务—探索OPi AIpro新一代AI开发板出色性能🌴一.引言🌰1.1 Orange Pi AI Pro 开发板🌰1.2 开发板的硬件规格🌰1.3 开发板顶层视图和底层视图的接口详情图 🌴二.硬件准备与环境搭建🌴三.基于香橙派 AIpro 上手初调试🌾3.1 安装 jupyter🌾3.2 准备项目源码🌾3.3 安装 git🌾3.4 下载项目源码到本地 🌴四.基于香橙派 AIpro 搭建二维码分类模型🌝4.1 数据收集与预处理🌝4.2 canny边缘检测及Hough变换🌝4.2 光照不均二值化处理 🌴五.基于香橙派 AIpro 部署Flask服务🌴六.心得总结🌻6.1 心得🍃6.1.1 开发板性能评价🍃6.1.2 部署过程体验 🌻6.2 总结 🌴附录 基于香橙派 AIpro搭建二维码分类模型及其Flask服务—探索OPi AIpro新一代AI开发板出色性能 🌴一.引言 随着物联网(IoT)和人工智能(AI)技术的飞速发展,边缘计算设备在数据处理和实时分析方面展现出巨大潜力。香橙派(Orange Pi)作为一款性价比极高的嵌入式开发板,其最新推出的AIpro型号集成了强大的AI处理能力,为开发者提供了在边缘设备上实现复杂AI应用的可能。本文将详细介绍如何利用香橙派AIpro搭建一个二维码分类模型,并通过Flask框架创建一个Web服务,实现二维码的实时识别与分类。
🌰1.1 Orange Pi AI Pro 开发板 Orange Pi AI Pro 开发板是香橙派联合华为精心打造的高性能 AI 开发板,其搭载了昇腾 AI 处理器,可提供 8TOPS INT8 的计算能力,内存提供了 8GB 和 16GB两种版本。可以实现图像、视频等多种数据分析与推理计算,可广泛用于教育、机器人、无人机等场景。
🌰1.2 开发板的硬件规格 🌰1.3 开发板顶层视图和底层视图的接口详情图 顶层视图:
底层视图:
除此之外,还搭载了丰富的外设接口,比如:
一、Electron是什么 简单的一句话,就是用html+css+js+nodejs+(Native Api)做兼容多个系统(Windows、Linux、Mac)的软件。
官网解释如下(有点像绕口令):
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。
二、Elemtron流程模型 三、Electron搭建工程,若成功则输出123 3.1 准备 可参考Electron官网地址
需要node和npm,先检测是否安装。
node -v npm -v 3.2 项目初始化 命令行创建
mkdir my-electron-app && cd my-electron-app npm init 或者,手动快速创建
package.json文件
{ "name": "my-electron-app", "version": "1.0.0", "description": "test Electron", "main": "main.js", "author": "Bin9153", "license": "MIT" } 有几条规则需要遵循:
entry point 应为 main.js.
author 与 description 可为任意值,但对于应用打包是必填项。
3.3 安装 Electron npm install --save-dev electron //或者 npm install electron -D 3.
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
简介 虽然许多用户需要像 MySQL 这样的数据库管理系统的功能,但他们可能不太习惯仅通过 MySQL 命令行客户端与系统进行交互。
phpMyAdmin 是为了让用户能够通过 Web 界面与 MySQL 进行交互而创建的。在本指南中,我们将讨论如何安装和保护 phpMyAdmin,以便您可以安全地在 Ubuntu 18.04 系统上使用它来管理您的数据库。
先决条件 要完成本教程,您需要以下内容:
运行 Ubuntu 18.04 的服务器。该服务器应该有一个具有 sudo 权限的非 root 管理用户,并且已配置了 UFW 防火墙。要设置这些内容,请按照我们的 Ubuntu 18.04 初始服务器设置指南进行操作。在您的服务器上安装了 LAMP 堆栈。您可以按照本指南在 Ubuntu 18.04 上安装 LAMP 堆栈来设置这些内容。 最后,在使用诸如 phpMyAdmin 这样的软件时有重要的安全考虑,因为它:
直接与您的 MySQL 安装进行通信使用 MySQL 凭据进行身份验证执行并返回任意 SQL 查询的结果 出于这些原因,以及因为它是一个广泛部署的 PHP 应用程序,经常成为攻击目标,您不应该在普通的 HTTP 连接上在远程系统上运行 phpMyAdmin。如果您没有配置带有 SSL/TLS 证书的现有域名,您可以按照本指南在 Ubuntu 18.04 上使用 Let’s Encrypt 安全配置 Apache。这将需要您注册一个域名,为您的服务器创建 DNS 记录,并设置 Apache 虚拟主机。
完成这些步骤后,您就可以开始本指南了。
消息队列(Message Queue),是分布式系统中重要的组件,其通用的使用场景可以简单地描述为:当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。
一、消息队列 1.什么是消息队列 消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到MQ 中而不用管谁来取,消息使用者只管从MQ中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
2.消息队列的特征 (1)存储 与依赖于使用套接字的基本TCP和UDP 协议的传统请求和响应系统不同,消息队列通常将消息存储在某种类型的缓冲区中,直到目标进程读取这些消息或将其从消息队列中显式移除为止。
(2)异步 与请求和响应系统不同,消息队列通过缓冲消息可以在应用程序中公开一定程度的异步性,允许源进程发送消息并在队列中累积消息,而目标进程则可以挑选消息进行处理。 这样,应用程序就可以在某些故障情况下运行,例如连接断断续续或源进程或目标进程故障。
路由:消息队列还可以提供路由功能,其中多个进程可以在同一队列中读取或写入消息,从而实现广播或单播通信模式。
3.为什么需要消息队列 (1)解耦 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
(2)冗余 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
(3)扩展性 因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。
(4)灵活性&峰值处理能力 在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
(5)可恢复性 系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
(6)顺序保证 在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)
(7)缓冲 有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
(8)异步通信 很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
二、kafka基础与入门 1.kafka基本概念 Kafka 是一种高吞吐量的分布式发布/订阅消息系统,这是官方对 kafka 的定义,这样说起来,可能不太好理解,这里简单举个例子:现在是个大数据时代,各种商业、社交、搜索、浏览都会产生大量的数据。那么如何快速收集这些数据,如何实时的分析这些数据,是一个必须要解决的问题。同时,这也形成了一个业务需求模型,即生产者生产(produce)各种数据,消费者(consume)消费(分析、处理)这些数据。那么面对这些需求,如何高效、稳定的完成数据的生产和消费呢?这就需要在生产者与消费者之间,建立一个通信的桥梁,这个桥梁就是消息系统。从微观层面来说,这种业务需求也可理解为不同的系统之间如何传递消息。
Kafka 是 Apache 组织下的一个开源系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于 hadoop 平台的数据分析、低时延的实时系统、storm/spark 流式处理引擎等。kafka现在已被多家大型公司作为多种类型的数据管道和消息系统使用。
2:kafka角色术语 kafka 的一些核心概念和角色
Broker:Kafka集群包含一个或多个服务器,每个服务器被称为broker(经纪人)
Topic:每条发布到 Kafka 集群的消息都有一个分类,这个类别被称为Topic(主题)
Producer:指消息的生产者,负责发布消息到 kafka broker。
Consumer:指消息的消费者,从 kafka broker 拉取数据,并消费这些已发布的消息。 Partition:Partition 是物理上的概念,每个Topic 包含一个或多个Partition,每个partition 都是一个有序的队列。partition 中的每条消息都会被分配一个有序的 id(offset)
一、概述 在分布式系统中,消息队列(Message Queue)扮演着至关重要的角色,它能够有效控制并发量,确保消息的可靠传递,并提供异步通信机制。ZooKeeper 和 Kafka 是两个非常流行的开源项目,它们分别用于提供分布式协调服务和高性能的分布式消息队列。本文将详细介绍如何部署 ZooKeeper + Kafka 消息队列群集。
二、ZooKeeper 简介 2.1 定义与特点 ZooKeeper 是一个开源的分布式协调服务,为分布式应用提供一致性服务。它的主要特点包括:
领导者-跟随者模式:集群中有一个领导者(Leader)和多个跟随者(Follower)。全局数据一致:每个 Server 保存一份相同的数据副本,客户端连接到任何 Server 都能获得一致的数据。更新请求顺序执行:来自同一个客户端的更新请求按发送顺序执行,即先进先出。数据更新原子性:一次数据更新要么成功,要么失败。实时性:在一定时间范围内,客户端能读到最新数据。 2.2 工作机制 ZooKeeper 的工作模式基于文件系统+通知机制。每个服务端上线时需要到 ZooKeeper 集群注册信息,客户端从 ZooKeeper 集群获取在线服务端信息列表并监听。服务端上线下线时,ZooKeeper 更新列表信息并通知客户端。
2.3 应用场景 ZooKeeper 广泛应用于以下场景:
统一命名服务统一配置管理统一集群管理服务器动态上下线软负载均衡 2.4 选举机制 ZooKeeper 的选举机制包括第一次启动选举和非第一次启动选举。选举过程中会考虑 Epoch(任期代号)、ZXID(事务ID)和 SID(服务器ID)等因素。
三、Kafka 简介 3.1 定义与特点 Kafka 是一个高吞吐量的分布式发布/订阅消息系统,由 Apache 组织开发。其主要特点包括:
高吞吐量:能够实时处理大量数据。持久化存储:消息被存储在日志文件中,确保数据安全。分区和副本:通过配置多个 Partition 和副本提高系统的可靠性和可扩展性。消费者组:支持多个消费者组,每个消费者组可以独立消费消息。 3.2 核心概念 Broker:Kafka 集群中的一个服务器。Topic:消息的分类,每条消息都属于一个 Topic。Producer:消息的生产者,负责发布消息到 Broker。Consumer:消息的消费者,从 Broker 拉取并消费消息。Partition:Topic 的物理分区,每个 Partition 是一个有序的队列。Consumer Group:消费者组,允许将多个消费者组织在一起,共同消费同一个 Topic 的不同 Partition。Offset:消息在 Partition 中的唯一标识,用于追踪消息的读取位置。 四、部署步骤 4.
TCP协议是传输控制协议,所谓的传输控制指的是,所有的发送本质上是拷贝,我们应用层调用的一切的网络IO接口,其实是把数据拷贝给了传输层,说白了就是操作系统,后续的数据什么时候发,发多少?出错了怎么办,完全是由TCP协议自主控制。就好比我们对文件进行读写时,使用write将数据写到操作系统内,而不是直接写道磁盘上,至于后续的动作操作系统有自己的刷新策略。
1、TCP协议的报头(了解相关字段)
上面TCP报文的格式中的数据指的上层拷贝下来的应用层的报文,包含应用层的报头和有效载荷。报头部分中,16位源端口和目的端口含义与UDP中一样,分别标识报文从远端客户端的哪一个进程来,到服务器上面的哪一个进程去,是传输层报头中不可或缺的部分。
这里我们不考虑报头中选项部分的数据,因此可以默认TCP报头的标准长度为20个字节,报头中的4位首部长度代表TCP报头的总长度,这个总长度包括标准报头的20字节加上选项长度。由于是是4个比特位,取值范围为0~15,表征报头总长度时要乘以4(字节),根据标准报头长度为20字节,因此TCP报头的长度范围为20~60字节。
关于TCP报文的解包和分用问题也就一目了然了,传输层拿到一个TCP数据,系统首先读取前20个字节,一定可以将它转化成一个结构化的数据,通过4位首部长度可以进一步读取选项数据,再与有效载荷分离,通过16位目的端口号交付到指定的上层协议中去。
不同于UDP,TCP的报头中没有用于标注有效载荷长度的字段(在UDP中有16位报文总长度,减去报头的8个字节可以得到有效载荷长度),原因是TCP协议是面向字节流的,传输层收到报文后,直接将报头和有效载荷分离再将有效载荷推送到接收缓冲区中,它只需要考虑数据按发送顺序可靠地到达,而不需要通过传输层报头对有效载荷做任何地解释,至于缓冲区中的数据如何解析,怎么保证报文完整性,这些完全由应用层决定。
通过解答一个问题进一步理解网络协议栈和文件是什么关系。服务端的传输层接受到一个报文,是如何找到目的端口的进程的?系统是有许多的场景需要快速定位一个进程的,在操作系统内,要为进程维护各种数据结构,事实上在内核中,将所有的PCB整体以双链表的形式组织起来,但并不仅限于此,双链表形式可以保证我们的进程不丢失,可以很好地对它做管理,但有时为了快速地寻找一个进程,我们还需要将每一个进程的PCB添加到其他的数据结构里,操作系统还会以端口号为key值以进程的PCB指针为value来维护一张哈希表,通过端口号和哈希表,就可以找到指定进程的所有内容,所以我们在写代码时调用bind给进程绑定端口号,系统会将我们的进程添加到这个端口号映射进程的哈希表中。通过端口号定位到特定进程之后,数据又是怎么发送给进程的呢?定位到PCB之后,PCB中的文件描述符表(files_struct)中会新打开一个文件描述符,也就是我们上层自己维护的socket,同时系统也会为传输层创建struct_file结构体,结构体中包含一系列的读写方法,也就是我们上层调用的网络IO接口,struct_file结构体中是维护了读写缓冲区的,这也是我们TCP协议向上交付时把数据放入的缓冲区,上层调用read、write接口也是对相应的缓冲区进行操作,以文件的方式读取网络数据。
传输层所谓的报头其实是一个结构体,里面结构化地包含了一系列数据,报头是一个类型,是类型就可以定义变量,添加报头就是定义一个报头对象,使用源端口、目的端口等数据对报头对象进行填充,再将其添加到有效载荷前面就形成传输层的完整报文了。
2、TCP的可靠性(确认应答机制)以及提高传输效率的策略(也会穿插讲报头中的相关字段)
为什么数据在网络传输过程中会存在不可靠问题呢?在传输过程中信号可能会发生衰减,中间经过某些出现异常的设备发生了数据丢失,可能是某些设备因为信号的问题无法识别01了,我们的数据经过多次的数据包转发,可能会出现各种奇奇怪怪的问题,一切的问题说到底仅仅是因为传输距离长了。
不可靠问题有许多种,常见的有丢包,乱序(早发的报文阻塞在了某一个路由器上,晚发的可能走的路径比较短,反而更早地到达了),校验错误(数据包经过长距离传输地时候,发生了比特位反转,导致最后做校验和比对地时候匹配不上了),重复(发送方认为自己发送的数据包丢失了,重新又发了一份)。
关于网络通信,存在绝对的可靠性吗?可以将网络通信类比为人的对话,对话时,我们可以通过对方的回复或者点头示意等行为确认自己的消息被接收到了,同样网络通信也是通过对方的应答响应来确认自己端的消息被接收到了,在通信时,数据是有先后顺序的,那么就一定存在最后一条数据,这个最后一条消息是没有应答的,简单来说,我们通过确认应答来保证可靠性,但是最后一条消息是无法保证其可靠性的。因此绝对的可靠性是不存在的,只存在相对的可靠性。TCP的可靠性也是相对的可靠性,是基于确认应答的机制的。
基于上面讲的确认应答机制,我们可以知道,数据的类型是有不同的。在数据链路层双方在进行通信的时候,除了正常的数据段(其中包括来自上层的应用层报文),还会有确认数据段,这种数据段的目的是做确认或者响应,可惜不携带任何的有效数据。这是最基本的TCP工作模式,实际上,A给B发送数据后,B可以将自己的应用层报文与确认响应压缩到一条TCP报文中,提高传输的效率,这种方式叫做捎带应答,也是更常见的工作模式。
进一步扩展真实的工作场景,TCP实际通信过程中,发送端发送一条报文,在接收端的应答到来之前,发送端不会一直等待,它可能又连续发送了多条报文,然后接收端再统一对之前接收的报文做应答,形成发送和接收的并行工作,提高效率,发送每一条报文,不一定会立刻应答,但原则上都必须有应答。确认应答机制是通过报头中的序号和确认序号还有六位特殊字段中的ACK来实现的。通信双方,发送端发送数据是要在带上序号的,这样就算接收方收到数据的顺序与发送方发送数据不符,也可以根据序号判断数据顺序,接收方接收数据之后,在给发送方的确认应答报文中要带上确认序号,确认序号代表此序号之前所有序号的报文都已经接收,接下来请发这个序号的报文,比方说接受方给发送方发送的报文中确认序号是14,要表达的意思是,14号之前所有的报文我都已经全部接收,下一次请发送14号报文。假设发送方给接收方发送11~14号报文,13号报文在网络传输过程中丢包了,接收方只收到了11、12、14号报文,那么接收方最后一次确认应答时确认序号只会填13而不是15,代表他接收到了13号之前的所有报文。也就是说,确认序号不仅仅是对最近一次收到报文的确认,也是对历史报文的确认。这样报头中维护两组序号,类似于使用TCP时,通信双方各自在内核中也会维护两块缓冲区一样,来支持我们的全双工通信,保证双方在都能进行收发,保持数据的有序。
使用TCP作为传输层协议,我们知道通信流程开始是应用层把数据向下交付到系统维护的发送缓冲区,直到最后接收方应用层从系统维护的接收缓冲区中拿到应用层,是一个不断地从缓冲区到缓冲区的拷贝过程,如果发送方发送数据过快,远远快于接收方上层处理数据的速度,接收缓冲区保存数据的能力是有限的,满了之后,接收方只能丢弃新发来的数据,虽然后续会讲TCP拥有一系列可靠性机制,但这造成了时间和网络资源的浪费。如果发送端发送数据过慢,会影响接收方上层正常的业务处理速度。所以TCP发送数据不能过快也不能过慢。那么发送方是如何得知自己发送的数据量是合适的?发送方需要得到对方接收缓冲区的剩余空间大小。在TCP报头中的16位窗口大小表示的就是接收缓冲区剩余空间的大小,不论是发送方还是接收方,不论发送的是有效数据还是确认响应数据,发送时都会在16位窗口大小中填充自己的接收缓冲区的剩余空间大小,以供对方确定自己后续发送报文的数据量,这也是所谓的流量控制的概念。那通信双方怎么做到还没有发送数据就得知对方的接收缓冲去接收能力呢?在三次握手期间,双方就已经交换了各自的16位窗口大小,已经得知了对方的接收能力,可以做到一开始就向对方发送最合适的数据量。假如接收方的接收缓冲区满了,发送方的上层IO接口会处于阻塞状态,系统也会轮询式地进行接收方的窗口探测,假如接收方的窗口大小更新了,会向发送方发送通知,保证数据收发的效率。
TCP报文是有类型的,网络通信过程中,接收方会受到各种各样的TCP报文,接受方要根据报文的类型,进行不同的动作。如果报文是一个常规的数据报文,那么接收方要做的是把数据读到,并把它放到对应的接收缓冲区中,再把数据拷贝到struct_file的文件缓冲区中,如果是一个连接请求的报文,那么接收方要做的不是读数据,而是要进入和发送方三次握手的流程,如果是一个断开连接的请求,那么也不应该读数据,而是进入和发送方四次挥手的流程。
TCP报文的类型是根据报头中六个标记位确定的,下面讲讲这六个标记位。SYN标志位是连接请求标志位,传输常规的数据报文时,这个标志位一般是置0,如果置1代表这一个报文是连接请求报文,接收方收到之后会会与发送方进入三次握手的流程。对应地,还有FIN标志位,如果置1代表这个报文是请求断开连接的报文,接收方收到这个报文之后,会与发送方进入四次挥手的流程。ACK标志位,接收方给发送方进行确认时,发送的报头里面不光要填写确认序号,还要将ACK标志位置1,在通信过程中,不论报文是一个独立的确认报文(不携带任何应用层数据),还是一个常规的数据报文,只要这个报文具有对对方报文确认的能力,这个标志位都应该被置1,通信双方的三次握手建立好之后,基本上发送的所有报文中这个ACK标志位都会被置1,报文基本上都承担着对历史报文的确认工作。PSH标志位,服务器端由于上层处理数据过慢,导致接收缓冲区的数据越来越多,最后满了,就会导致发送端发送不了数据,我们调用的write或者send接口会阻塞,发送端会轮询式地申请接收方的接收缓冲区大小,如果接收方上层迟迟没有拿取接收缓冲区的数据,那么发送端会向接收方发送PSH位被置1的报文,意思是催促接收方尽快拿去接收缓冲区的数据(当然如果接收方不执行任何动作发送方也没办法)。URG标志位,因为TCP报文中有序号,在接收缓冲区中可以保证数据按照发送顺序排列,那如果有特殊的数据想要插队呢?这就是URG标志位的用途,如果一段报文中涵盖了需要被尽快读取的数据,可以将URG标志位置1,注意,将URG标志位置1的报文中的有效载荷并不全是紧急数据,仅代表其中包括了紧急数据,那么这段数据的位置在哪里?查看报头中的16位紧急指针,有效载荷开头往后偏移16位紧急指针字节数就是这段有效载荷的位置,并且长度只有1字节。
这里了解一下URG的使用场景。极大多数情况下URG标志位和16位紧急指针都不会被用到,一般使用这两个字段都是运维场景下,通过客户端查看服务端是否正常运行,有没有挂掉,如果通过常规的报文进行访问,此时服务端正在处理之前的报文,响应时已经过了很长的时间了,因此需要通过发送将URG置1的报文进行访问,这种数据叫做带外数据,带外数据不需要经过TCP流,服务端会优先处理这条数据,客户端也能最快地知道服务端的健康状态。一般这种带外数据都不是用于很复杂的业务,因此一个字节也就够用了。
RST标志位。我们知道TCP建立连接的策略是三次握手,但是完成了三次握手,通信双方的连接不一定是建立好的,只是系统会认为连接建立好了,同样四次挥手也一样,完成了四次挥手,只是系统认为连接断开了,但是不一定真的断开了。上面这段话听起来有点抽象,举个例子,假设通信双方通过三次挥手建立好了连接,这个时候把服务器端主机的电源拔了,再次重启,打开服务器,此时服务器与客户端之间是没有连接的,而站在客户端的角度,双方是没有进行四次挥手的,客户端此时依旧认为连接还存在。在这种场景下,客户端会正常的给服务器发送报文,也不会将SYN标志位设置为1,服务器收到报文之后就很奇怪,我们没有建立过连接,你怎么就发送数据报文给我了呢?此时它会向客户端响应一个报文,这个报文中将RST标志位置1,通知对方,连接出现异常,需要重新建立连接,因此这个标志位也叫做复位标志位。
进一步理解报头中的序号与确认序号。传输层工作的整体流程,上层调用write、send接口,将应用层缓冲区中的数据拷贝到发送缓冲区,再由传输层决定发送数据的大小,从发送缓冲区中提取数据再添加报头,发送到网络中,接收方收到来自网络中的带有报头的数据,将数据去报头,发送到接收缓冲区中,上层再调用read、recv接口将接收缓冲区中的数据拷贝到应用层缓冲区中。从应用层到达了发送缓冲区,缓冲区就是一块字符数组,每一个字节都有自己的数组下标,这个数组下标天然就是每一个字节的序号,发送数据时假如发送序号为1~1000的数据,TCP会直接拷贝这部分数据,添加一个报头,将报头中的序号设置为1000,构成完整报文发送到网络中去。
这里讲一下TCP的超时重传机制。超时重传,一定是在数据丢包的场景下引发的,因此先讲讲丢包的集中常见情况。因为前面讲过TCP的流量控制策略,因此这里不考虑由于接收方接收缓冲区满了导致的丢包。这里考虑两种丢包的情况。(1)数据包在网络传输过程中真的丢失了,接收方没有收到过任何报文,所以接收方也不会主动地向发送端发送应答,发送方没有接收到应答,在经历一段特定的时间间隔之后,发送方会认为丢包了,然后进行超时重传,也就是重新发送相同的报文。(2)数据收发过程中,接收方成功收到了报文,也向接收方发送了应答,但是传输过程中,应答报文丢包了,站在发送方的角度来看是和第一种情况一样的,它会认为丢包了,继续进行超时重传。所以说,发送方进行超时重传动作其实并不在意数据是否真的丢包,站在它的角度,只要在特定时间内没有收到接收方的应答,就会进行超时重传。基于第二种情况的存在,接收方是可能重复收到同一份报文的,重复也是传输中不可靠的现象,TCP接收方也会根据报文的序号进行去重。为了支持超时重传,发送方发送了的数据,在一段时间内并不会直接移出发送缓冲区,会维持一段时间。那么发送端超时重传之前等待的时间是固定的吗?不是的,如果这段时间设置的过长,而网络情况又很好,有可能发送出去的数据早就丢包了,发送端还在一直傻等着,如果这段时间设置的太短,而网络情况又比较拥挤,数据可能还正在路上传输,发送端就进行超时重传了,这两种情况都是不合理的,因此这段时间一定是根据网络情况动态变化的。超时重传的具体策略一般是,以500ms为单位进行控制,每次判定超时重传的时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传,如果仍然得不到应答,等待4*500ms进行重传,以此类推,累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
3、TCP连接管理机制
关于三次握手的动作其实很简单,发送端发送将SYN置1的报文,接收端收到之后向发送端发送将SYN和ACK置1的报文,发送端收到之后在发送将ACK置1的报文做确认,三次握手完成。发送端和接收端在三次握手过程之中,也是由自己的状态表述的,在一开始双方都是closed状态,发送端发起第一次握手之后,更改为SYN_SENT状态,接收端收到并发起第二次握手之后,状态更改为SYN_REVD状态,发送端收到并发起第三次握手之后,状态更改为ESTABLISHED,接收端收到后状态也更改为ESTABLISHED。
三次握手并不是我们站在上帝视角看到三次握手完成,认为通信双方连接同时建立成功,而是站在各自的视角看到不同的现象,站在发送端的视角,只要它接收到来自接收端的SYN/ACK报文,向接收端发送了ACK响应,就算这个响应正在路上没有到达接收端或者是直接丢包了,它也会认为连接建立成功。同理,接收端只要收到发送端的最后一次ACK响应就会认为连接建立成功。至于连接是否成功,连接能否继续保持,这个由TCP的一系列可靠性机制来支持。前两次的握手是需要应答的,因为有超时重传机制的存在,前两次握手出现丢包情况是不用担心的,而第三次握手是没有应答的,如果发送端发送第三次握手出现丢包,经过一段时间后接收端会超时重传,重新进行第二次握手,而发送端认为连接建立成功,会向接收端发送报文,接收端认为连接没有建立成功,会触发发送RST报文,重新建立连接,因此三次握手中不论哪一次出现异常都是由解决方案的。
一款服务器,它可能会收到很多的连接请求,也一定会同时存在很多的连接,服务器端需要知道,哪些连接已经连上了,哪些正在通信,哪些正在重传,哪些正在RESET,哪些正在断开连接等等。所以操作系统对网络连接本事是要有一定的管理机制的。怎么管理?先描述,再组织,为连接设计对应的结构体,再通过特定的数据结构将其管理起来,因此维护连接是有成本的(时间和内存)。
这里探讨一下,TCP建立连接设计成一次握手行不行?答案是当然不行,在这种情况下,服务器只要收到来自客户端的SYN报文,就认为连接建立好了,随即为连接建立内核数据结构并管理起来,这注定了客户端只需要一台机器就可以频繁地向服务器不断SYN请求,不断地蚕食着服务器的内存资源,消耗着服务器的时间,意味着一台机器就可以不断地攻击服务器直到把它搞垮。那么将TCP建立连接设计成两次握手行不行呢?跟上面的情况一样,服务端收到客户端的报文请求之后,向客户端发送SYN/ACK,就认为连接建立好了,接着为连接创建内核数据结构并管理起来,并不考虑客户端有没有应答,这也是不合理的。上面是一种SYN洪水的问题。
根据上面的SYN洪水问题验证一下TCP建立连接设计为三次握手的合理性。(1)首先,三次握手是证明全双工通信信道是通常的的最小成本,用最少的握手次数,确定了通信双方都具有发送和接收消息的能力。(2)三次握手的最后一次握手是客户端向服务端发送ACK报文,服务端收到ACK报文之后才确认连接建立成功,在此之前客户端已经确认连接建立成功了,也就是说,在服务端为建立好的连接申请资源之前,客户端要先为自己建立好的连接申请资源,这样依赖,如果有不法分子通过一台主机多次向服务端发起连接建立请求,他的主机本身也要承受建立连接导致的消耗,大大提高了不法分子犯罪的成本。
TCP面向连接,连接本质上是由实体的结构体承载的,每一个连接通信双方都需要为其维护相关的连接结构体,TCP保证可靠性,哪些报文丢失,当前的连接是处于新建状态还是通信状态还是断开状态,哪些报文丢失了,哪些包围已经超时准备重传,这些信息都是要维护在TCP连接结构体里面的,包括三次握手期间双方的CLOSED、SYN_SENT、SYN_RECV状态都是由连接结构体维护的,是由位图和宏支持的。连接结构体是TCP保证可靠性的数据结构基础,而三次握手是创建连接结构体的基础。
关于四次挥手的通信流程其实很简单,先是客户端向服务器发送断开连接请求FIN,服务器收到之后发送ACK确认,之后发送FIN报文给客户端(这个动作可能与ACK合二为一),后续收到来自客户端的ACK确认之后,四次握手完成,双方断开连接。
客户端和服务器双方,任何一方都可以主动断开连接。假设客户端主动断开连接,只要它把FIN报文发出,它对应地连接结构体里的连接状态就会自动变成FIN_WAIT_1,服务器收到FIN报文并发送ACK之后,服务器的连接状态就会置为CLOSE_WAIT,服务器发出FIN后连接状态置为LAST_ACK(最后确认),客户端收到报文并且发送ACK之后状态置为TIME_WAIT状态,后续服务器收到ACK后服务器状态为CLOSED。主动断开连接的一方,最终状态是TIME_WAIT状态,被动断开连接的一方,两次挥手完成,会进入CLOSE_WAIT状态。
这里谈一下TIME_WAIT和CLOSE_WAIT两种状态。客户端发起断开连接求,即客户端代码调用close接口,而服务器不调用close接口,那么断开连接这个动作只进行了两次挥手,服务器会一直处于CLOSE_WAIT状态。如果服务器出现了大量的处于CLOSE_WAIT状态的连接,一般是一下两种原因导致的,(1)服务器有bug,没有做close文件描述符的动作。(2)服务器有压力,可能一直在向客户端推送消息,导致来不及close。主动断开连接的一方,即使已经完成了四次挥手,但会在一段时间内维持在TIME_WAIT状态下。四次挥手中的前三次挥手,不论通信双方的连接状态如何,其实连接都是存在的,如果发生报文丢包,由于确认应答和超时重传机制的存在,其实不会造成什么影响,如果客户端发起最后一次挥手之后,系统直接将连接状态更改为CLOSED,那么最后一次挥手假如丢包,站在服务器的角度会重新发送第三次挥手,此时客户端收不到来自服务器的任何报文,服务器收不到第四次挥手,也就无法断开连接,这是一种故障,所以TCP设计了客户端发起第四次挥手之后连接状态处于TIME_WAIT,这个状态下是没有断开连接的,客户端依然可以收到来自服务器的报文。TIME_WAIT维持的时间一般是2*MSL(MSL是通信时消息从一段到达另一端的最大时间)这个2*MSL设置的含义是最后一次挥手的传输时间和假如丢包了服务器重新发送的第三次挥手的传输时间,尽可能地保证连接正常关闭。还有一点,客户端发起最后一次挥手时,网络中可能还有滞留的报文,设置TIME_WAIT时间也是保证网络中报文能被正常收到。这也解释了我们自己写的服务器,有时候关闭之后再立即启动会失败,是因为,如果服务器是发起断开连接的一方,那么在四次挥手完成之后,连接会维持一段时间的TIME_WAIT状态,在这段时间里连接依旧存在,端口依旧被占用,因此无法打开绑定相同端口号的相同的服务器进程。
4、TCP滑动窗口
之前有讲,为了支持超时重传,任何一方发送数据,在收到应答之前,必须将自己已经发送的数据暂时保存到发送缓冲区中。一发一收的方式性能较低,实际发送时发送方可一次会发送多条数据(没有收到确认应答就发送),就可以大大提高性能,实际上时将多个数据段的等待时间重叠到一起了。
发送方的发送缓冲区,从使用的角度来讲,可以分为三部分,已经发送且已经收到应答的部分,已经发送但是没有收到应答的部分,数据尚未发送的部分。应用层拷贝数据到发送缓冲区,实际上时拷贝到数据尚未发送的部分后面的空白部分。其中我们将已经发送但没有收到应答的部分称为滑动窗口。发送缓冲区本质上是一个字符数组,滑动窗口只是字符数组中的一部分,它是由数组下标来维护的,假设滑动窗口的起始下标是win_start,结尾是win_end,滑动窗口的移动本质就是两个下标数值的更新。假如滑动窗口的win_start是1001,此时发送方收到接收方的确认序号2001,代表序号2001以前的所有数据已被接收,那么滑动窗口整体向右移动1000。
滑动窗口的大小和对方的接受能力有关,我们目前可以认为滑动窗口大小=对方通告给我的自己的接收能力大小,即win_end=win_start+tcp_win(tcp_win指对方发送报文中16位窗口大小)。win_start更新依据接收方发送的确认序号ACK_SEQ,收到确认序号之后,win_start=ACK_SEQ,win_end=win_start+tcp_win,在对方发送的ACK报文中,确认序号和16位窗口大小是同步更新的。如果接收方上层不处理数据而发送方又一直发送数据,最终接收方的接收缓冲区剩余空间会逐渐变为0,并且接收方一直给发送方发送确认,滑动窗口的win_start会一直右移,直到滑动窗口大小逐渐变为0。发送缓冲区通过特定的算法被内核组织成了环形结构,所以我们不用担心滑动窗口在一直向后滑动的过程中发生滑到头的问题。
5、TCP网络拥塞控制
TCP通信时,如果发送一万条报文,有一两条丢包了,这是正常现象,发送方会基于确认应答和超时重传机制重新发送报文,如果丢失报文过多,系统会判定这是路上网络的问题。TCP的可靠性不仅考虑了双方主机的问题,也考虑了路上网络的问题。如果丢包过多,TCP不会重传,此时网络状态可能就已经比较拥堵,重传会加重网络的故障问题。
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前网络的拥堵状态,再决定按照多大的速度传输数据。此处引入一个概念叫做拥塞窗口,拥塞窗口时发送端主机定的一个数字,当发送报文大小超过拥塞窗口时,可能就会发生网络拥塞的问题,拥塞窗口用于表征网络的吞吐能力。
引入了拥塞窗口的概念,这里要更正一下滑动窗口的概念,之前再滑动窗口的讲解中提到,发送端滑动窗口的大小等于接收端发来报文的16位窗口大小,这是在没有考虑到拥塞窗口的情况下的理解。现在发送端不仅要考虑接收端的接收能力,也要考虑网络的接收能力,滑动窗口大小=min(拥塞窗口,16位窗口大小)。
慢启动的策略是指拥塞窗口前期比较小,但是增长速度非常快。具体策略是拥塞窗口前期按照指数方式增长,达到某一个阈值之后按照线性方式增长,拥塞窗口不会无限增长下去,在未来某个时间点可能就会发生网络拥塞,从慢启动开始到发生网络拥塞这段时间称为一次拥塞窗口增长的周期,发生网络拥塞时的窗口大小的0.5倍会作为下一个周期内拥塞窗口增长的阈值。
6、延迟应答机制
假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K,但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区处理掉了,在这种情况下,如果接收端稍微等一会再应答,可能返回的窗口大小就是1M。窗口越大,网络吞吐量就越大,传输效率就越高,TCP的目标就是在保证网络不拥塞的情况下尽量提高传输效率。当让也不是所有的包都可以延迟应答,延迟应答是有时间和数量限制的,每隔N个包就应答一次,超过最大延迟时间就应答一次,一般N取2, 超时时间取200ms,注意延时应答的时间是不可以超过超时重传的时间,一旦超过这个时间,发送端会认为报文丢失,这本身是有bug的。
7、TCP连接队列
有些餐厅会在外面摆上许多凳子,供顾客进行排队,这要有哪一桌的人吃完了,就会安排排队的人进去吃饭。排队的本质是让我们资源有空闲的时候,可以立马使用,提高资源的利用率。但是队列不能设置的太长,没有客户会为了服务等待过久的时间。
TCP协议同样为上层维护了一个全连接队列。这个队列里全都是建立好的连接,但是没有参与上层业务,等上层处理业务的连接走了,就会有新的连接进入上层,参与相关业务。写代码时,我们调用的accept接口,就是执行将连接带入上层的动作,从侧面也说明了,即使不调用accpt,连接也早已建立好了,accept只是获取连接的接口。
建造者模式:构建复杂对象的专家 引言 建造者模式(Builder Pattern)是一种创建型设计模式,用于创建一个复杂的对象,同时允许用户只通过指定复杂对象的类型和内容就能构建它们,它将对象的构建和表示分离,使得相同的构建过程可以创建出不同的表示。
基础知识,java设计模式总体来说设计模式分为三大类:
(1)创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
(2)结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
(3)行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
第一部分:建造者模式概述 建造者模式是一种高级的创建型设计模式,旨在提供一种灵活的解决方案,用于创建复杂的对象。
1.1 定义与用途 建造者模式的基本定义 建造者模式将一个复杂对象的构建过程封装起来,同时允许按步骤构造对象。它允许用户通过相同的创建过程生成不同的表示。
为何在复杂对象的创建中需要建造者模式 分离构建和表示:将对象的构建过程从其表示中分离出来,使得构建过程不会影响对象的使用。控制复杂性:对于包含多个组成部分的复杂对象,建造者模式可以逐步构建对象,而不是一次性完成,从而简化了构建过程。增强灵活性:允许系统在构建过程中的不同阶段进行修改,而不影响其他部分。 1.2 组成元素 产品(Product) 定义:最终要创建的复杂对象。角色:通常是多个简单对象的组合体,由建造者模式逐步构建。 建造者(Builder) 定义:一个接口,定义了创建产品的方法。角色:提供了一个抽象接口,使得不同的建造者可以构建同一产品的不同表示。 导演者(Director) 定义:负责使用建造者来创建产品的类。角色:它知道如何使用建造者来得到最终的产品,通常持有一个建造者对象,并使用该对象的构建方法来创建产品。 具体建造者(Concrete Builder) 定义:实现建造者接口的具体类。角色:实现具体的构建过程,通常包含一个产品对象,并定义了如何构建该产品的步骤。 客户端(Client) 角色:使用导演者和建造者来获取最终的产品,客户端不直接与产品或具体建造者交互。 角色之间的交互 客户端:通过导演者请求产品。导演者:使用具体建造者来构建产品。具体建造者:实现建造者接口,负责构建产品的具体步骤。 建造者模式特别适合于那些需要通过多个步骤来构建的对象,且这些步骤可能因不同的使用场景而异。通过使用建造者模式,我们可以在不牺牲对象的完整性和一致性的情况下,提供灵活的对象创建过程。在下一部分中,我们将通过Java代码示例来展示建造者模式的具体实现。
第二部分:建造者模式实现 2.1 Java实现示例 以下是使用Java语言实现建造者模式的一个示例。假设我们正在构建一个复杂的汽车对象,汽车由多个部件组成,如引擎、轮胎等。
// 产品接口 interface Car { void assembleEngine(); void assembleWheels(); // 其他组装方法... } // 具体产品 class SportsCar implements Car { public void assembleEngine() { System.out.println("Assembling sports car engine."); } public void assembleWheels() { System.out.println("Assembling sports car wheels.
目录
一、缩放功能
二、旋转功能
三、镜像功能
四、QMatrix简单介绍
一、缩放功能 (1)在头文件中添加 “protected slots:" 变量:
void ShowZoomln( ); (2)在 createActionso函数的最后添力口事件关联:
connect(zoomlnAction,SIGNAL(triggered()),this,SLOT(ShowZoomln())); (3) 实现图形放大功能的函数 ShowZoomIn() 如下:
void ImgProcessor::ShowZoomln( ) { if(img.isNull()) return; QMatrix martix; martix.scale(2,2); img = img.transformed(martix); showWidget->imageLabel->setPixmap(QPixmap::fromlmage(img)); } 解析:
if(img.isNull( )) 有效性判断。QMatrix martix、martix.scale(2,2)、img = img.transformed(martix):声明一个 QMatrix 类的实例,按照两倍比例对水平和垂直方向进行放大,并将当前显示的图形按照该坐标矩阵进行转换。QMatrix & QMatrix::scale(qreal sx,qreal sy)函数返回缩放后的 matrix 对象引用,若要实现两倍比例的缩小,则参数 SX 和 sy 改为 0.5 即可。showWidget->imageLabeI->setPixmap(QPixmap::fromImage(img)):重新设置显示图形。 (4) 在头文件中添加 “protected slots:” 变量:
void ShowZoomOut( ); (5) 在 createActions() 函数的最后添加事件关联:
connect(zoomOutAction,SIGNAL(triggered()),this,SLOT(ShowZoomOut())); (6) 实现图形缩小功能的函数 ShowZoomOuto如下: