暂时未有相关云产品技术能力~
2022年了,来点不一样的体验。过来用一下云上桌面吧这是开机进入的桌面,除了一个小小修改,眼尖的朋友可以找找茬(看看那个被学委修改了)。学委偷偷把浏览器(Chrome)改了个文件名。什么是云桌面,无影云又是啥?首先,云桌面,其实就是云上的PC主机。既然是在云上的电脑,我们怎么开机关机啊,怎么进行日常操作啊?我们需要使用一个无影客户端来进行链接,登陆到这个云PC,然后进行操作。过往学委使用过CitrixDesktop,这个也是类似的。他们本质上就是,对计算机的虚拟化。有些人可能会问:这么麻烦,我不得在一个PC终端去链接这个无影云吗?这个就涉及到这个产品的作用/意义了。为啥需要虚拟化的PC终端?相信大伙依然记得19年疫情开始爆发那段时间,很多企业单位停工停产了。但是依旧有不少单位依然正常运转,除了这些单位有一个强大的协调管理能力之外,我想,数字化和虚拟化技术是绝对发挥了超大作用的。自动化可以让人类解脱枯燥乏味的重复性工作。虚拟化保证数字型企业提高安全可靠的工作环境(这个依靠云上安全技术,这块不是学委的专长)的情况下,支持各地办公。只要你有一个电脑,装上云桌面的客户端。那么就可以像在办公室使用电脑一样,在云上使用公司内部的服务/软件,达到正常的工作交流。同时,在云PC内的数据不至于泄漏。为什么这么说?有些公司可能会发放电脑给员工使用,最后收回(比如离职的时候,或者报废的时候)。 这样无形中对企业来说,是增加了资产积累,当然也是会折旧损耗,换个说法,也是成本。如果是让员工使用个人电脑访问公司服务,进行工作,这个难免会出现数据泄漏。所以在云PC上,我们用完PC,关掉链接,是没有任何数据泄漏的。因为我们不需要从内部服务拷贝数据到个人电脑。数据安全,肯定是一个非常重要的,任何企业都需要关注和解决的议题。所以,走向虚拟化,必然是一个企业成长为中大型企业的一个更优的选择。好处是多方面的,减少了资产折旧,更安全更稳定的环境支持协作。好了,说了那么多的虚拟化,云主机的好处,下面分享一下个人实操吧。分享一下,整个无影云的使用过程第一步就是去申请选择合适的云上PC学委选择了4核8G的主机,非常快速,感觉就一会功夫,就创建好了。选好机器配置支付之后,就可以在用户管理看到,新建的云桌面了(非常快速)。接着,生成用户,下载安装‘无影云电脑‘(支持MacBook的)。下图就是我在电脑上面链接云桌面的截图。这里学委推荐一个官方的短视频:,只要看完它,很快就能上手了(不过也可以像我一样不看视频直接操作的,也很方便)上面的操作仅仅支持个人电脑跟云桌面数据的传输交互,默认情况下我们是访问不到互联网的(在云桌面内)。(默认没有网络的)但是,我们可以让云桌面访问到个人电脑的文件。这样,我们开发者是可以用这个云桌面来安装一些编程软件(直接从本机器拷贝一些合适的软件或者资料,用云主机跑一跑程序的)不过,我这里主要还是像说说关键的,给云主机通网。第二步 给云桌面通网这个其实也非常简单,老用户知道可以找控制台(无影云桌面)上的互联网访问的菜单。点击开通互联网访问:选择合适的宽带,支付完成,就能让主机通网了。第三步 可以撒开搞了,装个程序运行一下。这里推荐一篇学委自己的文章:https://developer.aliyun.com/article/830364照着下载安装Python,非常顺利!推荐使用无影云桌面内部的应用软件:小手点一点,三大热门开发工具一次GET!不关机退出客户端,直接连接继续上一次操作当然还有一个,也不用关机了,断开客户端链接,下回直接登陆继续操作。 这个跟我使用MacBook体验很像所以进入打开python安装博客,点击下载,关闭,过一会再登陆,python还是会继续在无影云桌面上继续下载直到下载完成。(不过这里提示一下,如果文件比较大,可能中途这个下载会断开,这个建议在云上使用云盘软件做持续下载)。这个就是学委本次使用无影云的体验,桌面用起来非常流畅。当然上面更多是从开发者角度出发,分享的使用经历。当然还可以创建多个用户,给他人临时使用权限,比如做好了一个作品,时间太短了,直接使用这个来展示给他人,或者直接让他人等使用。这里有一个阿里专家过往的分享:https://developer.aliyun.com/live/43828?spm=a2c6h.14164896.0.0.1997573db84Mby,包括镜像创建,安全策略等,半个小时左右的分享,读者朋友也可以去看看。如果是企业管理的云桌面,肯定会在防火墙和内网等涉及数据方面的设置做更多优化。这块,阿里云明显也是做的很专业,配套周边都给到了。如果我是企业老板,可以这样用云桌面其实个人用,跟工作室,企业使用,我认为是无缝契合的。个人创建一台云桌面,多台云桌面+多个伙伴,管理起来就是一个小微公司了。当然,如果我们去看云桌面控制台,可以看到这个产品对企业型用户使用上的支持。首先,我们可以创建多个云桌面,提供给多个用户使用。这个,学委这里贴上了用户创建的截图,这个可以给一个云桌面分配多个用户的,这样就重分的使用到了云上PC。对于小团队,我们可以申请一部分,少量的主机,进行工作使用的,也不会出现资源浪费。另外还可以先体验,在加入企业网。实现内部桌面通信。同时阿里云桌面还有广泛的应用桌面配套的应用市场,提供了各种软件(办公的,开发的,交流的等等)除了收费项目,还是有不少免费的软件可以使用。当然还有很多功能,由于时间限制,还没有探索到的。后续会继续分享,我看到阿里云也有官方的最佳实践,也可以看看:https://developer.aliyun.com/live/247816?spm=a2c6h.14164896.0.0.7ddb1f6bOIax3A保持好奇心,多使用新产品,有益于拓展技术视野!学委这么多年一直专注在技术这块,接触了很多技术栈,也深度学习了一下东西,其实这个过程就是好奇心驱使!作为技术人,我觉得好奇心是非常重要的!好奇心对于一个公司更加的重要,这决定了一个公司能否作出更加突破的产品。有时候不是因为你的产品不够努力,而是精力没有使在刀刃上。如果你挖掘了越多别人看不到的东西,未来不远就在脚下。非常高兴能够使用这样一款产品,后续更多深度使用再发表一些操作心得吧。最后,这是无影云的官方页面 https://developer.aliyun.com/topic/wuying?spm=a2c6h.12873639.0.0.56a86466T9MM8t
前篇学委展示分享了类的继承和重写,面向对象还有一些概念,我们看看一些object的通用函数,继续跟上吧!Python Override重写就重新定义,在程序中就是覆盖父类的函数的这种行为。Override还能重写object类的一些通用函数,它们是:__init____str____eq__这里手动写几个,也无须全部记忆。因为我们双击object这个base class可以看到一系例的object类的函数:学委准备了下面的代码:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/15 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : __init__.py.py # @Project : hello """ 下面是一个程序员类定义 """ class Programmer(object): def __init__(self, name): self.name = name def code(self): print(f"{self.name}: life is short, why not python?") p = Programmer("学委粉丝") # p.code() #TypeError: code() missing 1 required positional argument: 'lang' p.code() print("p:", p) print("namespace:", Programmer.__dict__) class JavaProgrammer(object): def __init__(self, name): self.name = name def code(self): print(f"{self.name}: like if like a box of chocolate?") def __str__(self): return f"JavaProgrammer(name:{self.name})" p = JavaProgrammer("学委粉丝2号") # p.code() #TypeError: code() missing 1 required positional argument: 'lang' p.code() print("p:", p) 这是运行结果:我们下面在上面的代码基础上重写__eq__ 函数。重写 == 操作:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/15 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : __init__.py.py # @Project : hello """ 下面是一个程序员类定义 """ class JavaProgrammer(object): def __init__(self, name): self.name = name def code(self): print(f"{self.name}: like if like a box of chocolate?") def __str__(self): return f"JavaProgrammer(name:{self.name})" def __eq__(self, other): if isinstance(other, self.__class__): return self.name == other.name return False p1 = JavaProgrammer("学委粉丝2号") # p.code() #TypeError: code() missing 1 required positional argument: 'lang' p1.code() print("p1:", p1) p2 = JavaProgrammer("学委粉丝2号") # p.code() #TypeError: code() missing 1 required positional argument: 'lang' p2.code() print("p2:", p2) print("same ? ", p1 == p2) 重写之后两个对象果然相等了!(可能初学者会觉得有点奇怪,name不是一样吗)在Python中两个对象属性都相同,但是它们不一定相等的。这是注释了eq函数后到运行结果:Python 默认不支持方法重载!什么是重载?重载这种行为就是一个类出现多个同名函数,必然的函数接收的参数不一样(一样不就重复定义了,在Java代码里直接就报错了!)这在Python中默认不支持的。我们看看下面的代码,学委写了两个同名函数code但是参数数量稍微区别开了:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/15 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : __init__.py.py # @Project : hello """ 下面是一个程序员类定义 """ class Programmer(object): def __init__(self, name): self.name = name def code(self): print(f"{self.name}: life is short, why not python?") def code(self, lang): print(f"{self.name}: life is short, why not {lang} ?") p = Programmer("学委粉丝") #下面的代码取消注释会报错 #p.code() #TypeError: code() missing 1 required positional argument: 'lang' p.code("java") print("namespace:", Programmer.__dict__) 运行结果如下:这里我把运行结果的namespace复制出来了:namespace: {’__module__’: ‘__main__’, ‘__init__’: <function Programmer.__init__ at 0x1042ad280>, ‘code’: <function Programmer.code at 0x104494d30>, ‘__dict__’: <attribute ‘__dict__’ of ‘Programmer’ objects>, ‘__weakref__’: <attribute ‘_weakref_’ of ‘Programmer’ objects>, ‘__doc__’: None}我们看到namespace里面只有一个code,这告诉我们在内存中,python这个类值映射到一个code函数,明显是后者(第二个code函数)。但是有库可以做到重载,后面继续说。总结读者还可以选取一些object的函数进行重写试试。类继承带来便利的同时,也带来了复杂度。因为有时候子类调用父类完成一部分工作,父类调用其他,这样反反复复,整个函数处理逻辑就非常难以一目了然看明白,只能通过看局部代码跳来跳去的拼凑成一个接近全貌的认识。好处就是重复做的代码少了,组件更加简洁。学委写了十几年的Java了,但是写的这套Python教程非常务实,对基础编程有任何问题请查看相关文章。
前面写了文件的读取,按行读写等,这篇我们把文件处理等其他函数也过一过吧。文件属性前面我们更多集中学习了文件的读写(open)函数打开文件然后使用read/write函数。文件还有很多属性,常见的有下面的几个:name 文件名字encoding 编码,有时候编码搞错了就容易出现乱码!mode 打开文件的模式(r=读文件,w=写文件)closed 是否关闭状态buffer 缓冲IO对象(根据mode来创建读缓冲/写缓冲)复制运行下面的代码看看:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/12 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : filedemo.py # @Project : hello afile = open("sample.txt","w") print("file name :", afile.name) print("file mode :", afile.mode) print("buffer:", afile.buffer) print("encoding :", afile.encoding) print("closed ? ", afile.closed) afile.close() print("closed ? ", afile.closed) 下面是运行结果:前面文章分享了读取文章的代码,读者可以改改,观察一下读取文件的状态有哪些不同。学委运行了一下:还有其他哪些打开文件的模式吗?它们是:横向分为三大类,读取数据,写入数据,文件末尾追加数据,非常直观。这里不一一进行代码展示,掌握两种读写方式其他可以后期查表。其他文件操作:增删查改学委先展示文件夹的基本操作,所以:这里的增是 创建文件夹这里的删是 删除文件夹这里的查是 查找文件夹这里的改是 改文件夹名为此我准备了下面的代码:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/12 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : filedemo.py # @Project : hello import os current_folder = os.getcwd() # 获取当前目录 print("当前目录:" + current_folder) os.mkdir("./xuewei_testingfiles") os.rename("./xuewei_testingfiles", "./xuewei_demo") files = os.listdir(current_folder) print("files:" + str(files)) os.removedirs("./xuewei_demo") print("删除了文件夹:xuewei_demo") files = os.listdir(current_folder) print("files:" + str(files)) 这个就是运行效果。但是学委并没有把查文件夹做完全,留一个思考空间给读者。稍微解释一下:os.listdir 是一个查看目录下面文件包括文件夹的函数os.removedirs 是一个移除目录的函数os.rename 是用来修改文件名的函数上面的操作是list类型的,学过学委发表的list文章肯定知道怎么遍历查找了。自己动手吧。进入主题 文件的增删查改请读者自己想想,你会怎么写?给你一个提示:os.remove 函数可以用来移除文件。其他操作类比文件夹上面的代码进行编写。你能想到吗?这是代码,直接贴了。#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/12 11:58 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : filedemo.py # @Project : hello import os current_folder = os.getcwd() # 获取当前目录 print("当前目录:" + current_folder) new_file = "./xuewei_file" with open(new_file, "w") as f: f.write("持续学习持续开发,我雷学委") os.rename(new_file, "./xuewei_demo_file") files = os.listdir(current_folder) print("files:" + str(files)) os.remove("./xuewei_demo_file") print("删除了文件:xuewei_demo_file") files = os.listdir(current_folder) print("files:" + str(files)) 总结Python 文件的属性不少,我们需要把握重点,特别是mode属性和encoding属性。以及文件的增删查改操作,基本是每个程序员必须掌握的知识点,刷一下吧。
学委前面展示dict的概念和常用操作,谈到了一个shadow copy产生的新dict对象受到原dict对象影响。这篇我们继续看看深拷贝,深拷贝是什么?我们拿字典的复制可以通过copy函数来实现,但是它是浅拷贝,也就是拷贝了但不完全拷贝简单粗暴来说浅拷贝就是拷贝key,但是新字典的值还是沿用旧字典的值的引用(内存地址一样)。与浅拷贝对比,深拷贝就是除了拷贝key,新字典的值不会沿用旧字典值的引用,而是创建类似的数据,引用分开!也就是内存分配两个空间内串联起来的数据块,是可以做到毫无关联!我们看看字典的深浅拷贝对比代码示例:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/11/6 11:25 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : thecopy.py # @Project : hello import copy # 引入了copy模块,3.8自带的模块 info_dict = dict() info_dict['name'] = 'LEI_XUE_WEI' # 或者改为 'levin' info_dict['blogs'] = '95' # 目前发表了93个博客文字。 info_dict['gongzhong_hao'] = '【雷学委】' # 欢迎过来关注支持 info_dict['meta-data'] = dict() print('info_dict:', info_dict) print("*" * 16) # 浅拷贝//Shadow Copy: 全拷贝但不完全拷贝 shadow_copied_dict = info_dict.copy() # 也可以使用: copy.copy(info_dict) print("shadow_copied_dict :", shadow_copied_dict) deepcopied_dict = copy.deepcopy(info_dict) print("deepcopied_dict :", deepcopied_dict) # 我们修改原始的dict info_dict['meta-data']['date'] = '06 Nov 2021' print('info_dict:', info_dict) print('shadow_copied_dict:', shadow_copied_dict) # 这里输出看到变化也传递给了 shadow_copied_dict print('deepcopied_dict:', deepcopied_dict) # 这里输出看到变化也传递给了 deepcopied_dict 效果如下:从这里看,我们发现两个拷贝的字典,浅拷贝因为被拷贝的字典产生变更而更新了。而深拷贝产生的deepcopied_dict对象则纹丝不动!这里还得说一下copy模块。稍微说说copy.deepcopy函数的原理考虑到基础系列专栏,这一段会比较难以理解,我会再以后的文章更加细致讲。这里只针对dict对象的拷贝做带过:deepcopy内部通过_deepcopy_dispatch对象查找字典(dict)的拷贝器拷贝器定义的拷贝方法遍历原字典对象依次对key 和 value 进行深度拷贝就这么简单,如果key和value还不是最简单的类型(比如int/float/str等),继续查找拷贝器,遍历进行拷贝,直到基础类型直接返回,然后回溯。小白可以跳过这一段继续学习,先把基础熟练了。总结深拷贝和浅拷贝涉及的技术很多。本篇只是介绍了怎么进行深拷贝(借用copy模块),也侧重于展示和使用介绍如果不用怎么实现呢?当然copy模块也给了我们一个参考答案,但这个背后涉及了pickle序列化反序列化还有内存管理等,不难但篇幅更长以后再说。
学委之前发布了ppc工具和renxianqi万能清点工具,展示了开源项目的开发过程。刚好上周五学委发布了 一篇文末抽奖的文章,所以花点时间构思编写了一个抽奖小工具:prize(名字非常直接)另外,抽奖结果11月10号晚上10点公布。请确保您的电脑安装了python和pip工具,下面使用pip安装并使用prize工具。第一步 安装与使用 prize打开任意终端或者CMDpip install prize #或者这个:pip install choujiang 或者是安装这个:pip install prize 或者使用下面命令pip install prize -i https://pypi.tuna.tsinghua.edu.cn/simple 安装完成可以使用下面的命令,即可运行【人贤齐】清点工具:prize #或者输入:choujiang 输入之后我们可以看到弹出一个界面:这里是macbook安装工具后的效果图,windows系统会有小小差别。点击【使用介绍】菜单可以查看操作帮助。第二步 使用 prize来抽奖场景一 诗人斗诗大会抽奖这里是唐朝诗人以前举办了一个斗诗大会。但是清点人数困难。有了【人贤齐】工具之后,这个工作就非常简便,左边为预期全部诗人,右边为实际参加斗诗大会的诗人们。预期全部诗人:杜甫:茅屋为秋风所破歌 李白:静夜思 王勃:送杜少府之任蜀州 李贺:雁门太守行 然后点击按钮:【卡片生成】,下面生成了4个诗人卡片!\接着点击‘重新抽奖’,我们看效果:我们看到这里暂时支持冒号(英文冒号)隔开,截取冒号前面为卡片ID,生成个体卡片。然后再抽奖。其他场景这个工具不限定于以上一个场景,也适用于以下的:所有评论区(用户id加上冒号(:)格式的评论分析截取,生成卡片,抽奖。类似的,我们可以找到一些活动名单,复制全部人员评论到上方文本区域,点击‘生成卡片’和‘重新抽奖’原理和数据解析这个工具今天写完今天发布(0.0.2),突发奇想来的小作,后面学委还会继续送书抽奖(C站那边的),后面不断更新完善。UI设计的过程参考renxianqi工具和前篇文章 => 保姆级别指导开发UI应用【实战指导建议收藏】总结延伸这个只是一个小工具,但是可以应对任意的抽奖的场景。比较好玩,是一个通用的抽奖小工具。另外提供建议的命令行入口:prize (choujiang),只要打开任意终端即可运行人抽奖小工具。打开任意终端或者CMD, 赶紧安装用起来吧。pip install prize #或者下面这个: pip install choujiang
今天我们说了字符串的基础,格式化,这次我们讲解字符串的常用函数,不要错过! (文本送书,评论区抽取一位送书)前两篇都在本文同个专栏,欢迎关注。下面开始讲解。字符串都有哪些操作?实际开发都有这些需求:第一大类:判断识别字符串判断字符串属于那种字面类型(数字,全字母,其他)判断字符串包含某些结构(数字大写,局部子串,子串出现频次等)第二类:字符串编辑的操作(生成新字符串)字符串的替换/合并/填充等字典替换,填充0值,清空操作等第三类:字符串跟字节串的互转。这类操作通常发生在数据跨程序/跨服务器传输,我们传输bytes,然后获取转string类型。第一类 判断识别字符串学委准备了下面的代码:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/30 10:13 上午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : string_funs_cat1.py # @Project : hello import sys slogan = "keep studying, keep coding, I am Levin" # 判断结构 print("算某个子串出现数次: slogan.count('keep') = ", slogan.count('keep')) print("找某个子串首次出现的下标: slogan.find('keep') = ", slogan.find('keep')) print("找某个子串最后出现的下标: slogan.rfind('keep') = ", slogan.rfind('keep')) print("找某个子串下标: slogan.index('keep') = ", slogan.index('keep')) print("找某个子串下标: slogan.rindex('keep') = ", slogan.rindex('keep')) print("是否'keep'开头的字符串: slogan.startswith('keep') = ", slogan.startswith('keep')) print("是否'keep'结束的字符串: slogan.endswith('keep') = ", slogan.endswith('keep')) # 字符串属性相关 print("字符串长度: len(slogan) = ", len(slogan)) print("字符串是否都是空格: slogan.isspace() = ", slogan.isspace()) print("字符串是否大写: slogan.isupper() = ", slogan.isupper()) print("字符串是否小写: slogan.islower() = ", slogan.islower()) print("字符串是否为每个词首字母都大写: slogan.istitle() = ", slogan.istitle()) # 判断字符串数据类型 print("字符串是否全为字母: slogan.isalpha() = ", slogan.isalpha()) print("字符串是否全为数字: slogan.isalnum() = ", slogan.isalnum()) print("字符串是否数字: slogan.isnumeric() = ", slogan.isnumeric()) print("字符串是否浮点数: slogan.isdecimal() = ", slogan.isdecimal()) print("字符串是否为空格串: slogan.isspace() = ", slogan.isspace()) 读者可以直接复制运行代码,学委补充了运行效果图:第二类 字符串编辑的操作下面学委准备了一些代码展示:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/30 10:13 上午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : string_funs_cat2.py # @Project : hello import sys slogan = "keep studying, keep coding, I am Levin" print("首字母大写: slogan.capitalize() = ", slogan.capitalize()) print("全部字母大写: slogan.upper() = ", slogan.upper()) print("全部字母小写: slogan.lower() = ", slogan.lower()) print("转为首字母都大写(标题风格): slogan.title() = ", slogan.title()) print("大小写逆转: slogan.swapcase() = ", slogan.swapcase()) table = slogan.maketrans({"e": "5"}) print("字符串替换表: slogan.translate(table) = ", slogan.translate(table)) # 字符串替换,合并,填充等 print("替换tabs为n个空格: 'hello\t学委'.expandtabs(4) = '", "hello\t学委".expandtabs(4)) print("左子串来串联传入的列表: ' '.join(slogan) = '", " ".join(slogan)) print("替换子串: ' '.replace(first, second) = '", slogan.replace("e", "11")) print("填充0值: slogan.rzfill(2)= '", slogan.zfill(50)) print("填充#值: slogan.rjust(50,"#")= '", slogan.rjust(50,"#")) print("填充#值: slogan.ljust(50,"#")= '", slogan.ljust(50,"#")) print("移除首尾空格: slogan.strip()= '", slogan.strip()) data = slogan.split("e") print("split slogan into data= ", data) 运行效果如下:第三类:字符串跟字节串的互转下面学委准备了一些代码展示:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/30 10:13 上午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : string_funs_cat3.py # @Project : hello import sys slogan = "keep studying, keep coding, I am Levin" bytes = slogan.encode("utf-8") print("type of encoded string = ", type(bytes)) # 注意python的string类型没有decode函数,该函数属于bytes类型对象特有!!! print("type of decoded byte = ", type(bytes.decode("utf-8"))) print("type of decoded byte = ", bytes.decode("utf-8")) 运行效果如下:书籍赠送 - 《大数据分析实用教程——基于Python实现》【简介】本书对大数据分析的原理与Python程序实现进行了系统的介绍,每种算法都采用sklearn和Matplotlib分别进行程序实现及数据可视化。本书共8章,内容包括大数据分析概述、Python数据分析与可视化基础、关联规则与推荐算法、聚类算法及其应用、分类算法及其应用、回归与逻辑回归、人工神经网络、支持向量机等。本书在理论上突出可读性,在实践上强调可操作性,实践案例丰富,实用性强。随书提供微课视频(正文对应处扫码可观看)、教学课件、习题答案、教学大纲等教学资源。本书可作为高等院校相关专业大数据分析或大数据概论等课程的教材。也可供从事大数据分析、机器学习的人员作为参考书。【作者】这本书由三位作者联合主编:唐四薪, 赵辉煌, 唐琼。唐四薪,男,1980年11月出生,湖南湘潭人。硕士,毕业于中南大学,现为衡阳师范学院计算机学院讲师,主要从事计算机相关专业的教学与研究。资深计算机图书作者,清华大学出版社畅销书作者查看百度百科可以发现这是一位长期写作的作者,值得期待。【编辑推荐】书籍内扫码有配套微课,注重操作性大量操作实验,参考答案足够适量的大数据基础补充大数据分析、机器学习是当下热门,值得学习目录和更多书籍信息https://item.jd.com/10035055672444.htmlhttps://product.suning.com/0071151794/12304785309.html作为大数据从业人员,学委看到了很多契合的技术,同时书内讲解的算法(K-mean,贝叶斯分类等),相关库(Numpy,SciPy,sklearn,Tensorflow等),软件(Anaconda)这些都是日常高频出现的。就跟字符串处理的脉络一样,这些技术构成了数据分析的脉络,也丰富发展了数据分析数据科学这个领域,可以看看。总结学委并未把str字符串的所有函数都罗列了,编程不是刻板的,必须抓住重点。学习编程不是去记忆,但是也并非啥都不看都靠感觉。我喜欢下面这句话:读书破万卷,下笔如有神!精心准备的代码,读者运行一下,自己感悟!(注释写的很清楚了)
很多学习编程的都多多少少学习了一些数学知识。前篇我们讲讲那些常用的数学处理函数, 我们接着过一过三角函数!数学三角函数除了前篇说的数的取值,指数对数等,还有我们初中数学的一些函数。如下:正弦,余弦,正切反正弦,反余弦,反正切等等的,下面会使用math.sin/ math.cos/ math.tan 等等来进行三角函数运算。他们都有一个共同点,参数都是弧度制(而非角度制)。而asin/acos/atan 这类接收数字,输出的结果也并非角度,也是弧度值。举个例子之计算 30度正弦值如下,我们都知道3 0 ∘ 30^\circ30 ∘ 正弦值等于0.5,但是上面提到的函数都只接受弧度的sin \sinsin(3 0 ∘ 30^\circ30 ∘ ) = 1/2也就是说:我们需要使用 30度 对应的弧度值,传给sin函数,才能算出sin(30度)至此,我们需要引入两个重要的函数:math.degrees(传入参数为弧度值)# 比如math.degrees(math.PI) 结果为180(度) math.radians(传入参数为角度值)# 比如math.radians(180) 结果为PI #角度转弧度 rad = math.radians(角度值) #或者把知道角度的弧度值给进来:比如30度 #计算sin 给进弧度值对应的某个角度 math.sin(rad) # 结果为1/2 代码展示鉴于很多函数都需要接收一个产生或者是多个参数,学委把函数调用进行包装。编写了exec函数,动态的打印执行了哪个函数,参数是啥。如果你是小白,请简单理解exec函数是一个帮你运行数学函数的工具即可,它的用法是:exec(参数,被调用的数学函数) 或者 exec(参数列表,被调用的数学函数) #!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/26 10:02 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : number_demo.py # @Project : hello # @Blog: https://blog.csdn.net/geeklevin/article/details/121024945 import math #这个函数为了方便输出展示,前篇文章有更多解释,需要请查看前篇。 def exec(obj, func, label=""): if type(obj) == list: print("%s : apply func %s on obj %s = %s" % (label,func, obj, func(*obj))) elif type(obj) == tuple: print("%s : apply func %s on obj %s = %s" % (label,func, obj, func(*obj))) else: #print("type of obj is %s " % type(obj)) print("%s : apply func %s on obj %s = %s" % (label,func, obj, func(obj))) exec(math.pi, math.degrees,"把角度转换为弧度") # pi 弧度 为 180度 exec(180, math.radians,"把弧度换转为角度") # 180度 为 1 pi 弧度 exec(math.pi/6, math.sin, "求sin(30度)") # sin(30度)= 1/2 exec(0.5, math.asin, "求asin(30度)") # asin(1/2) = 30度 print(math.degrees(math.asin(0.5))) # 30度 exec(math.pi/3, math.cos, "求cos(60度)") # cos(60度)= 1/2 exec(0.5, math.acos, "求acos(60度)") # acos(1/2) = 60度 print(math.degrees(math.acos(0.5))) # 60度 exec(0, math.tan, "求tan(0度)") exec(0, math.atan, "求atan(0度)") exec(math.pi/2, math.tan, "求tan(90度)") #tan (90度)没有任何意义,但是在计算机中math.pi/2 弧度没有办法严格表示为90读 print(math.tan(math.radians(45)))#tan(45度)= 1 print(math.tan(math.radians(90)))#tan (90度) 没有任何意义 #print(math.radians(90))# 因为角度转弧度并非精准表示的,计算机只能算一个无限逼近的数字弧度值来表示90,所以上面的90可以计算的 exec(1, math.atan, "求atan(0度)") print(math.degrees(math.atan(1))) # 45度 下面是运行效果:可以看到每个输出都跟我们实际数学学习到的公式结果一直(有些值只是稍微接近)。注意! 但是tan(9 0 ∘ 90^\circ90 ∘ )居然被算出来了!数学课本都说tan(9 0 ∘ 90^\circ90 ∘ )毫无意义!这里我们也看到上面的三角函数都接受弧度值,这类值在计算机中只能表现为一个不断逼近的数字,所以尽管我们通过radians函数把90翻译为弧度了,但这个值不是绝对的90度。不会出现无意义的结果!题外话:关于exec函数,请查看前篇文章说明。
本文使用pymouse库,控制鼠标的移动。下面分享一个自用的鼠标自动随机区域内点击。代码如下:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/20 10:10 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo import os import time from pymouse import PyMouse import random def rand(end=8): r = random.randint(0,end) print("rand %s " % r) return r m = PyMouse() print(m.screen_size()) print(m.position()) m.move(200, 200) #m.click(200, 200, 1,2) # 或者是这个可以双击左边鼠标按钮。 x=792 y=257 i=0 #设置一个大的数字,也可以设置While True一直移动鼠标不点击 while i < 10000: i+=1 time.sleep(30) print("move mouse...") m.move(200+rand(10), 200+rand(10)) print("exit the auto-click program") 为什么发布这个程序呢?因为有些企业内的服务器系统定制的,如果没有获得管理员权限,是没有办法关闭锁屏设置的,所以可以借鉴这个代码使用pymouse来控制鼠标一直移动。上面的程序循环运行鼠标随机点击,可以保持电脑不会被锁屏, 这是方法之一。
之前学委分享了renxianqi(人贤齐)aka 万能清点小工具。这次给大家分享下如何在Tkinter开发的UI应用中 添加菜单项目,并对菜单绑定操作还没有安装的朋友打开终端或者cmd(命令行), 可以运行下面的命令安装用起来吧。pip install renxianqi #或者下面这个: pip install qingdian 本文要制作的菜单如下:第一步 添加菜单到根部这里实用的是Tkinter库,我们需要导入这个库,然后实用Menu类,并添加到Tk根对象。下面看一点点代码:root = Tk() menubar = Menu(root) #并添加到Tk根对象 about_menu = Menu(menubar) #创建并将关于菜单项 添加到菜单栏 setting_menu = Menu(menubar) #创建并将配置菜单项 添加到菜单栏 这里有4个对象,它们直接一一联结:root <-- menubar <-- about_menuroot <-- menubar <-- setting_menu就这么简单,创建了两个一级菜单, 下面是创建菜单部分的代码,包含了指定菜单名称(通过label设置)from tkinter import * def app_start(): root = Tk() menubar = Menu(root) about_menu = Menu(menubar) setting_menu = Menu(menubar) # 一级菜单 menubar.add_cascade(label="使用介绍", menu=about_menu) menubar.add_cascade(label="更多配置", menu=setting_menu) root.config(menu=menubar) 第二步 创建二级菜单二级菜单,我们绑定了 一些函数。从简入难,下面摘取3个添加二级菜单的代码。添加二级菜单通过menu对象的add_command函数来实现, 其中:label: 为菜单名称command: 为绑定事件函数(点击菜单后触发)about_menu.add_command(label='版权信息', command=show_copyright) about_menu.add_command(label='操作说明', command=show_about) setting_menu.add_command(label='创建桌面快捷方式', command=make_shortcut) 直接看下面是更完整的代码:from tkinter import * def app_start(): root = Tk() menubar = Menu(root) about_menu = Menu(menubar) setting_menu = Menu(menubar) about_menu.add_command(label='版权信息', command=show_copyright) about_menu.add_command(label='操作说明', command=show_about) setting_menu.add_command(label='创建桌面快捷方式', command=make_shortcut) setting_menu.add_command(label='数据文件信息', command=show_datafiles) menubar.add_cascade(label="使用介绍", menu=about_menu) menubar.add_cascade(label="更多配置", menu=setting_menu) root.config(menu=menubar) leixuewei_ui = LXW_NAME_LISTING_GUI(root) leixuewei_ui.setup_root_win() 第三步实现菜单事件比如我们要实现点击【版权信息】弹出下面提示框应该怎么做?弹出信息面板如下:我们已经在第二步定义二级菜单,并绑定了show_copyright事件函数了。这里只需要实现弹出窗口即可,也就是下面的messagebox模块import tkinter.messagebox as mb 这样版权弹出窗口可以这样写:def show_copyright(): message = """ 工具采用Apache License,请放心免费使用! 开发者:雷学委 作者网站:https://blog.csdn.net/geeklevin 社区信息:https://py4ever.gitee.io/ 欢迎关注公众号【雷学委】,加入Python开发者阵营! """ #调用messagebox的showinfo函数展示提示信息 mb.showinfo("[人贤齐-万能清点工具]", message) 读者可以复用并修改为自己应用的提示。支持整个UI应用的菜单开发涉及的技术点全部涵盖了。下面是完整可执行代码:# -*- coding: utf-8 -*- # @Time : 2021/8/29 10:30 上午 # @Author : LeiXueWei # @CSDN/Zhihu: 雷学委 # @XueWeiTag: CodingDemo # @File : name_checker.py # @Project : renxianqi from tkinter import * import tkinter.messagebox as mb TITLE = '[人贤齐]万能清点工具' BG_COLOR = 'skyblue' LOG_LINE_NUM = 0 SHOW_DEBUG = True def show_copyright(): message = """ 工具采用Apache License,请放心免费使用! 开发者:雷学委 作者网站:https://blog.csdn.net/geeklevin 社区信息:https://py4ever.gitee.io/ 欢迎关注公众号【雷学委】,加入Python开发者阵营! """ mb.showinfo("[人贤齐-万能清点工具]", message) def show_about(): pass def show_datafiles(): pass def make_shortcut(): pass class LXW_NAME_LISTING_GUI(): def __init__(self, root): self.root = root self.log_line_no = 0 def setup_root_win(self): # 窗口标题,大小,颜色设置。 self.root.title(TITLE) self.root.geometry('604x600') self.root.configure(bg=BG_COLOR) self.root.resizable(0, 0) # 阻止Python GUI的大小调整 # 组件标签 self.data_label = Label(self.root, background="tomato", text="预期全部人员") self.banner_label = Label(self.root, width=2, height=25, background="black", text="") self.result_label = Label(self.root, background="tomato", text="实际出席人数") # 处理数据按钮 self.process_btn = Button(self.root, text="开始校验", fg="red", width=10, command=self.compare_data) # 处理数据按钮 self.reset_btn = Button(self.root, text="清空重置", fg="red", width=10, command=self.clear_data) self.log_label = Label(self.root, width=10, background="tomato", text="缺席人员") # 文本展示框 self.all_member_text = Text(self.root, width=40, height=25) self.attended_text = Text(self.root, width=40, height=25) self.log_text = Text(self.root, width=85, height=9) # 布局 self.data_label.grid(row=0, column=0, sticky=W + E + N + S) self.banner_label.grid(row=0, column=1, rowspan=2, sticky=N + S) self.result_label.grid(row=0, column=2, sticky=W + E + N + S) self.all_member_text.grid(row=1, column=0, sticky=N + S) self.attended_text.grid(row=1, column=2, sticky=N + S) self.process_btn.grid(row=2, column=0, sticky=W + E) self.reset_btn.grid(row=2, column=2, sticky=W + E) self.log_label.grid(row=3, column=0, columnspan=3, sticky=W + E) self.log_text.grid(row=4, column=0, columnspan=3, sticky=W + E) self.preload() def preload(self): pass def clear_data(self): pass def compare_data(self): pass def log_on_text(self, message): pass def app_start(): root = Tk() menubar = Menu(root) about_menu = Menu(menubar) setting_menu = Menu(menubar) about_menu.add_command(label='版权信息', command=show_copyright) about_menu.add_command(label='操作说明', command=show_about) setting_menu.add_command(label='创建桌面快捷方式', command=make_shortcut) setting_menu.add_command(label='数据文件信息', command=show_datafiles) menubar.add_cascade(label="使用介绍", menu=about_menu) menubar.add_cascade(label="更多配置", menu=setting_menu) root.config(menu=menubar) leixuewei_ui = LXW_NAME_LISTING_GUI(root) leixuewei_ui.setup_root_win() # 进入事件循环,保持窗口运行 root.mainloop() def about(): pass if __name__ == "__main__": # 启动程序 app_start() 最后,赶紧安装使用renxianqi清点工具吧打开任意终端或者CMD, 赶紧安装用起来吧。pip install renxianqi #或者下面这个: pip install qingdian (ps:推荐Windows系统安装,Mac推荐1.0.8版本)
本篇给大家分享一个超级使用的编程技巧,快速给应用创建快捷方式,双击运行还没有安装的朋友可以运行下面的命令:打开任意终端或者cmd(命令行), 赶紧安装用起来吧。pip install renxianqi #或者下面这个: pip install qingdian 这篇文章只分享一件事情,给这个renxianqi清点工具创建快捷方式。先看效果:只需要进行一次操作上面安装完成,打开cmd输入下面命令,即可运行【人贤齐】清点工具:rxq #或者输入:renxianqi 打开应用,点击菜单【更多配置】然后点击【创建桌面快捷方式】,运行效果如下:桌面也多了一个图标(如下),下次运行RenXianQi万能清点就可以直接双击使用了,非常方便。好,针对RXQ万能清点工具的介绍到这里。下面是快捷方式的实现源码。实现原理:分享一下创建快捷方式的代码创建快捷方式,目前仅适用于Windows系统。所以需要先安装Windows系统的Python库,可以运行下面的代码安装:pip install winshell pip install pypiwin32 下面代码使用了上面的库,复制代码可以直接运行使用:#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/10/10 8:30 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : shortcut.py.py # @Project : absentee import os “”“ 这里学委调用了winshell的CreateShortcut函数。 传入4个参数,分别为:快捷方式的路径,exe文件的路径,图标路径,还有描述信息。 ”“” def create_shortcut(bin_path: str, name: str, desc: str): try: import winshell shortcut = os.path.join(winshell.desktop(), name + ".lnk") winshell.CreateShortcut( Path=shortcut, Target=bin_path, Icon=(bin_path, 0), Description=desc ) return True except ImportError as err: print("Well, do nothing as 'winshell' lib may not available on current os") print("error detail %s" % str(err)) return False if __name__ == "__main__": #这里的程序exe路径,请修改为个人库的路径,第二个参数为快捷方式的文件名,第三个为描述信息。 create_shortcut("C:/LeiXueWei/Python.framework/Versions/3.8/bin/rxq.exe", "RenXianQi", "学委特制清点小程序") 更多说明创建桌面快捷方式,这里bin_path(应用路径)非常重要。所以,读者自己开发的应用需要解决的是定位到exe的路径。这个也很好做可以参考renxianqi源码来实现。最后,赶紧安装使用renxianqi清点工具吧打开任意终端或者CMD, 赶紧安装用起来吧。pip install renxianqi #或者下面这个: pip install qingdian
学委之前发布了ppc工具,帮助了很多初学者快速建立项目。本篇分享一个通过ppc工具创建开发的完整库:renxianqi(人贤齐)名字比较像明星,这个工具的作用就是,清点人数,比对是否齐全,所以叫做 人贤齐 renxianqi。请确保您的电脑安装了python和pip工具,下面使用pip安装并使用rxq工具。第一步 安装与使用 renxianqi打开任意终端或者CMDpip install renxianqi 或者是安装这个:pip install qingdian 或者使用下面命令pip install renxianqi -i https://pypi.tuna.tsinghua.edu.cn/simple 安装完成可以使用下面的命令,即可运行【人贤齐】清点工具:rxq #或者输入:renxianqi 输入之后我们可以看到弹出一个界面:这里是macbook安装工具后的效果图,windows系统会有小小差别。点击【使用介绍】菜单可以查看操作帮助。第二步 使用 renxianqi人贤齐多场景帮助我们快速清点人员场景一 诗人斗诗大会清点这里是唐朝诗人以前举办了一个斗诗大会。但是清点人数困难。有了【人贤齐】工具之后,这个工作就非常简便,左边为预期全部诗人,右边为实际参加斗诗大会的诗人们。预期全部诗人:杜甫 李白,王维 白居易 王勃 李贺 然后点击按钮:【开始校验】,清点出4位诗人缺席了!场景二 上课点名全部同学:雷小花 雷学委 小白 小明、小红 实际到场:雷学委,小白 小红 小明 直接复制上面的数据,打开终端输入rxq打开万能清点工具:其他场景这个工具不限定于以上两个场景,也适用于以下的:技能树对比活动签到比对单词两边比对类似的,我们可以找到一些活动名单,复制全部人员到左边栏目, 然后把实际出行的人数复制到右边栏目,点击‘开始校验’。下方的缺席人员就可以展示了。原理和数据解析这个工具几个月前就发布0.0.2 版本了,后面不断更新完善。最近发布1.0.5版本,更多是支持本地数据文件存储和友好的操作。UI设计的过程参考前篇文章 => 保姆级别指导开发UI应用【实战指导建议收藏】数据读取加载过程rxq(人贤齐)工具启动使用应用,加载界面之前会读取用户home目录下的.renxianqi目录。我们看看下图,这个工具会缓存当前用户录入的数据在本地home目录下面。我们下次运行点名/签到/清点之类的活动,直接运行”rxq“或者”renxianqi“即可。校对的原理点击按钮的时候,获取了左边内容和右边内容。两者进行整理比对,按照左边数据为基准,查找左边没有出现在右边的一行记录,发现则记录缺席。扫描右边整个列表,知道整理出全部缺席人员。总结延伸这个只是一个小工具,但是可以应对任意的清点人数的场景。比较好玩,核心是友好的获取(本地存储录入数据),方便以后多次重用。另外提供建议的命令行入口:renxianqi (rxq),只要打开任意终端即可运行人贤齐清点小工具。打开任意终端或者CMD, 赶紧安装用起来吧。pip install renxianqi #或者下面这个: pip install qingdian
它就是基于hexo来生成的,当然做了一些定制。这次,学委会一步一步的指导大家建立一个博客站。什么是hexohexo 是一款博客生成工具,它基于Markdown语法的博客生成。hexo它也发行了基于NodeJS的cli库(命令行工具),在npm上面可以查找到,下面进入安装。可以简单理解,它就是一个博客网站工厂,专门生成博客网站的, 但是需要安照它的方式。下面学习hexo生成博客的步骤。安装全局安装,把hexo命令添加到运行环境的PATH变量上,这样随处可用。npm install -g hexo-cli 通常我们安装完一个库,马上就查看版本/或者查看帮助,确保安装正常hexo -v 第一步 生成站点项目运行下面的命令创建博客项目:hexo init 该命令会快速的生成博客模版文件。我们这里来看这个站点就是一个标准的NodeJS项目。如下图,文件很多,重点是package.json,它复制管理项目的其他依赖工具库。然后就是themes,这是博客的主题风格,也就是整体样式,布局方面的。就像音乐的中国风,R&B之类的,它限制了整首歌的曲调,对我们博客来说就是整个观感,整体的排版。然后就是scaffolds:比较简单,就是创建分类/文章等的模板,可以修改的。关于这个NodeJS项目更多信息可以补充学习:雷学委之NodeJS爱好系列, 我们继续博客页面的编辑。修改站点信息如下,我们把title先修改了:还有其他细节操作,我们先生成站点。第二步 生成站点网页预览站点之前需要先生成网站的页面,这里使用下面的命令hexo generate #或者hexo g 该命令帮我们把网站从md文件模版生成为html文件,可以看到项目目录内有一个public目录:这个就是最终博客网站会显示的html文件了,懂html的朋友都知道,这里所见即所得,你在html放什么文字颜色块,浏览器就相应的显示出来。第三步 预览网站我们继续使用默认主题,运行下面的命令,先看修改后的网站效果。hexo server 运行效果如下:我们可以打开浏览器查看:hexo 工具启动了一个服务器,渲染了我们刚刚生成的博客站点,非常方便。同时,博客自带了一篇博客文章“Hello World”, 这是hexo默认生成的。编辑博客页面我们打开博客文件夹内的source目录。它放置了站点的文章。这里就很好弄了,它是一个markdown(MD)格式的文件。小白可以简单理解,它一种文档风格规范。比如‘# ’ 开头表示一个大标题,一级标题。‘## ’ 开头表示二级标题。下面修改一下:修改了title为:雷学委博客教程修改了二级标题为:Quick Start / 快速开始添加了一行文本: “第一行文本(from 学委)”我们可以根据需要修改文件内容,重新运行下面命令就能查看效果了hexo generate #或者hexo g hexo server #或者hexo s 刷新刚刚的页面,我们看到博客内容变化了。总结与延伸本文展示了一个博客站点的快速生成。添加新博客,可以复制helloworld.md或者使用‘hexo new 博客名称’。发布站点到网站简单来说可以复制public目录到服务器,渲染即可。但是Hexo提供了更高效的做法,我们下篇继续讲解。下一篇,学委会具体介绍py4ever网站的技术搭建细节。
站点如下图,特此学委写了本篇。不花一分钱建立个人站点,分享一些设置开源社区站的技术选型思考过程。文末有特制表格总结,建议收藏下面一一罗列。CodeChina PagesCSDN 的CodeChina算是比较新的开源代码管理站(csdn子站),它也能够支持Pages 站点,不过目前只支持官方授权的企业或者特定开源团队。这里是关于CodeChina 的Pages的说明,读者可以阅读活得更详细的要求:https://codechina.csdn.net/codechina/help-docs/-/wikis/docs/user/pageshttps://codechina.csdn.net/codechina/help-docs/-/wikis/docs/user/org/star-community-join-planGithub Pages最广泛使用的Pages 应用,做的非常成熟。开通站点应用非常简单,只需要配置一个账户名的特定repo,比如学委组织的 py4ever 代码仓库对应的Pages 仓库名如下:py4ever.github.io只需要配置这个仓库,然后打开Pages应用开关即可渲染md或者html站点。更多pages的信息参考链接:https://pages.github.com/好处:比较成熟,还有自动化workflow基于commit自动构建开源站!特别是使用hexo生成站点的开发者,我们可以在项目根目录发现一个.github目录,内放置一个workflow.yml文件。然后github pages 的repo能够识别到action,自动构建开源站点。如下图,github的Actions服务,支持自动化网站动态生成Gitee PagesGitee 也支持Pages 应用,跟Github 应用的区别是,需要加多一步实名认证。比如py4ever组织的Pages仓库的名称为 :py4ever然后配置service那里,进行实名认证。对比选型学委还是推荐Gitee + Github 组合的,应对国内外开发者发行开源库/技术。学委制作了下表,作为一个参考表格,读者自行定夺。Gitee 对国内用户最友好的,缺点是没有免费的站点自动构建的机制。Github 对于开源团队也非常友好,毕竟我们开源了一些库/框架,不希望只限制于国内开发者使用。下一篇,学委会具体介绍py4ever网站的技术搭建细节。
这次介绍一下getopt这个库。这个库学委在pypi-seed开源项目中使用了,本文介绍如何使用优雅的应对不同的参数,让cli命令行程序更加弹性!python内置库getoptgetopt是一个简化命令行工具参数处理的库,可以定义短参数和长参数。这个说的比较官方。下面看看相对友好的介绍:小白可能不太懂,它像媒人配对一样,自动的把参数进行配对。你告诉他参数‘-a’ 后面跟一个名字,getopt可以在程序内解析出(a,名字)这样的映射如果参数是这样:–author=雷学委 --project=hellogetopt可以帮我们处理为:(author, “雷学委”),(project,“hello”)getopt的调用#getopt.getopt传入三个参数 import getopt getopt.getopt(args, options, [long_options]) 比如pypiseed生成项目,接收了3个主要参数name: 项目名dir: 项目路径author: 作者名字我们从pypiseed调用命令来看如何编写getopt。第一种 短参数类型短参数类型,即是使用‘-‘ 加上一个单字母。比如大家非常常见的’-h’ (查看帮助信息) 或者 ‘-v’ (查看版本信息)我们下面来看pypiseed CLI如何使用getopt模块的。pypiseed -p demo_project -a testuser -d /tmp 我们看到这个命令行pypiseed接受了-p , -a, -d 三个。我们使用getopt 如何声明处理这三个参数。#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/9/9 10:45 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : getopstest.py # @Project : hello import sys import getopt def parse_args(): project = author = dir = None print("argv:%s" % sys.argv) argv = sys.argv[1:] opts, args = getopt.getopt(argv, "p:a:d:", []) for opt, arg in opts: if opt in ['-p']: project = arg elif opt in ['-a']: author = arg elif opt in ['-d']: dir = arg print(" project:%s, author:%s, dir:%s " % (project, author, dir)) parse_args() 保存上面代码为getopstest.py,,输入三个参数并运行:-p sample -a "雷学委" -d /tmp 这里要注意‘-’ 后面接的必须是短参数,也就是一个字母。像‘-dir’ 这种参数会导致getopt处理解析异常!第二种 长参数类型使用pypiseed生成项目的长参数类型pypiseed --project demo_project --author testuser --dir=/tmp 上面的代码不能支持长参数,这里单独做一个支持长参数的。#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2021/9/9 10:45 下午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : getopstest.py # @Project : hello import sys import getopt def parse_long_args(): project = author = dir = None print("argv:%s" % sys.argv) argv = sys.argv[1:] opts, args = getopt.getopt(argv, "", ["project=", "author=", "dir="]) for opt, arg in opts: if opt in ['--project']: project = arg elif opt in ['--author']: author = arg elif opt in ['--dir']: dir = arg print(" project:%s, author:%s, dir:%s " % (project, author, dir)) parse_long_args() 保存上面代码为getopstest.py,输入三个参数并运行:-p sample -a "雷学委" -d /tmp 这样就会出错:getopt.GetoptError: option -p not recognized我们不能还是传递短参数,参数应该改为:--project sample --author "雷学委" --dir "/tmp" 或者这样也支持--project=sample --author="雷学委" --dir="/tmp" 第三种 长短参数合并使用这里学委带大家回顾上面的两个代码,里面其实很像。比较关键的区别的是对:getopt.getopt这个函数的调用。#短参数解析调用opts, args = getopt.getopt(argv, "p:a:d:", []) #长参数解析调用opts, args = getopt.getopt(argv, "", ["project=", "author=", "dir="]) 先说说,getopt函数的参数,第一个是参数列表,它是从系统参数第二个开始到最后一个参数构成的列表。然后第二个参数我们在第一段代码中看到设置的是短参数。这里定义一个字母或者多个字母即可。如果是带参数值的,后面跟上一个冒号(:, 比如a:, 表示-a '参数值‘)然后第三个参数在第二段代码中是一个单词(可以跟上‘=’符合)的列表。带上等于符合表示,该参数后面可以跟上一个参数值。好了,那同时处理短参数和长参数的代码就是把第二,第三的参数位置都设置对于短参数字母串和长参数的单词串。参考下面代码: opts, args = getopt.getopt(argv, "p:a:d:", ["project=", "author=", "dir="]) 总结本文展示了getopt模块的使用,以及实际命令行工具开发的代码剖析。这里有个福利,现在读者可以通过pypiseed,就能够一键生成命令行工具项目了,运行下面代码即可pip install pypi-seed && pypiseed -p demo_project -a testuser -d /tmp --cli
问题不大,找几个开源项目来参加就可以了,需要开源项目磨练的可以联系学委。本文重点讲解如何快速的给开源项目贡献代码。首先你必要要有一个GitHub账号(或者Gitee/CSDN也行),然后找到开源项目,申请加入。或者fork模式参与开发。对了,还没有安装ppc的直接运行下面命令:pip install pypi-seed # pip install --upgrade pypi-seed 或者这个命令升级最近ppc ppc # 运行pypiseed命令行查看使用帮助 本文基于pypi-seed演示,下面展示申请加入后的必备操作。第一步 创建个人开发分支比如参与到pypi-seed项目,先熟悉项目结构代码,然后进行贡献。创建分支:dev-code-fix-0923 (分支名字需要自己记住,跟提交的功能有关即可)第二步检出自己的开发分支打开pycharm,检出整个pypi-seed代码:git clone git@github.com:py4ever/pypi_seed.git 然后打开git视图:第三步 本地开发并提交代码在自己的分支上继续开发,然后commit 并push代码。比如找到自己需要修改的代码,修改并commit(提交高效的修改描述信息),然后push上传代码到Github。当然这里有一个非常重要的点:运行测试,保证一切现有测试都运行通过!并为自己新增功能添加测试用例我们打开Github网站能看到自己的分支第四步 提交一个Pull Request申请开源项目的负责人进行审核合并创建一个PR如下图:提交完之后,可以找到项目的工作组或者负责人给他发送邮件,简单说一下本次提交的内容。(如果提交很多修改,请务必解释清楚)本文展示的PR:https://github.com/py4ever/pypi_seed/pull/1/files打开可以看到提交的修改的详细信息,这次提交修改了公众号信息,截图如下。总结贡献代码给开源项目这个过程 = 验证自己的开源项目理解的一个过程,能够被合并发布,也是得到了项目方的认可的。更多是一个前期准备,你需要理解熟悉这个项目,而不是胡乱修改就提交了(一般被否决或者无视了)当然学委这里并没有展示一个需要花比较长时间开发才能合并的branch的操作,本文只是展示了一个开发的关键环节,算是最基本的提交了。更复杂的提交方式,这个以后会说。
这次学委讲讲开源项目的分支管理,帮助读者了解开源项目是怎么管理代码的。多数开源项目都是main(以前是master/trunk)分支管理代码的。开发版本或者中间修订版本走feature 分支发布,然后再定期合并到master 分支。分支管理是什么分支管理,简单理解,就是多条生产线管理,比如一款手机的多个版本的生产。比如某个手机的三条生产线:现售的Mate30手机,在装订生产。同时还有Mate20旧版本还有部分销售订单还在继续装订。新版本Mate40还在研发这样就好理解,我们的项目代码就是制作手机的输入,通过管理多个分支,让不同产品线团队做到最大程度的独立开发,发布不同款式产品。下面介绍一下著名开源项目和我们做开源项目应该怎么进行分支管理的第一种 基于主干分支管理(Trunk Based)比如LINUX内核我们打开Linux内核的主线repo,看到如下的分支,只有master主干分支。非常简单,也比较典型的主干为中心的分支管理。因为linux内核主线只有一个,新的特性代码会被不断提交到主线,新内核也没有必要支持向后移植。如果需要得用指定的tag来检出特定版本分支,进行修订发行。就像下图一样,以trunk分支为中心,每一个弯都是一个PR(Pull Request),合并到trunk分支。学委补充一下PR,PR代表每次有效开发工作的提交与合并。主干分支管理的好坏:好处:简化管理,所有特性只要审核通过马上合并到主干分支。这样随时可以发布,也避免了出现大规模集成的风险。缺点:一个个单一的提交造成问题不大,不过当出现颠覆式的提交或者多个提交,这些提交造成的问题会一直影响主干分支,也影响到每次发布,直到问题解决。小伙伴可能有疑问了,那么稳定发行版本,如何进行打补丁(比如遇到bug或者安全漏洞需要提交修改)稳定版本仓库:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/linux还有一个独立的stable 仓库,它基于主线分支的一次镜像(或者定期同步,主要是发行大版本的时候)。确定了发行的主版本后,做一个分支发布为stable仓库,后续比较急的补丁都打在stable分支上,然后再回顾这些提交,发布到内核仓库的主干分支。第二种 Gitflow模式,即多特性分支管理, 比如hadoop大数据的底层hadoop框架是怎么发行的?我们看看下图,这就一目了然了。一个trunk主干分支,加上现行的主流版本的特性分支(branch-2.10, branch-3.2等)。别看有trunk分支就是咬定了是trunk-based开发模式,我们要看这个产品活跃分支。hadoop框架诞生很早,应用比较广泛,属于应用型上层框架。很多企业用的版本都不是统一个的3.X新版本,不少企业还是2.X版本。所以hadoop社区采用这种分支管理模式,长期维护了多个主流版本的开发。这点学委觉得是非常合适的。就像下图一样,每一个弯都是一个PR(Pull Request):学委想指出,这里出现了多个长分支,像feature1.x, feature2.x(当然hadoop现在不维护1.x版本了),这个图只是展示作用。这样的好处:各个分支的开发独立运作,hadoop 2.X项目主要还是基于Java7运行环境执行。而hadoop 3.X 跟Java8运行。这样提高了不同分支的独立行,兼顾发行效率。毕竟java7跟java8有不少区别的。坏处最关键的就是:合并的风险!随着时间发展,这些大分支积累的patch和主干分支积累的新开发特性,总需要一个时间合并起来,这时候需要很多工作进行合并校对应对一个相对大的工程。这些大量代码合并往往需要配套不止集成测试,还有回归测试。(当然主干分支需要这套,但是主干分支可以保存每天测完,这个是多分支管理无法比拟的)延伸 - 自己的项目该如何选择?非常简单,如果是个人开发者,直接走TBD(Trunk Based Development)。比较简单,随时可以打一个tag,发布一个版本。如果是一个百人团队,那么学委建议根据实际考虑了。统一业务平台(二开或者传统大型平台)多团队多项目开发,建议走Git Flow(feature branch模式)多分支模式开发,保证各个项目团队的独立性,同时缩短定期合并的周期。微服务多组件的单个平台,可以走TBD,划分业务的服务单元保证多个业务单元独立开发,同时分化一个核心平台组管理开发平级别需求。思考的核心,是把平台规划好,让它支持TBD模式,避免出现大规模合并,除了开发测试成本,这个需要很大量管理介入协调!
本文旨在提供一个通用解决方案,适应所有场景的结局RST文件渲染错误!特别是新手在发布开源项目的时候容易遇到这个问题:#比如这个错误,学委也会遇到。The description failed to render in the default format of reStructuredText. See https://pypi.org/help/#description-content-type for more information.当然,作为开发老兵有时候也会遇到,因为一不小心rst格式的文件内容非常敏感,有时候不经常敲这个,敲错了就报错了。上传pypi库发生错误如下:第一步 认识reStructuredTextreStructuredText 是一种文本,常用在python项目中,用于对项目或者是程序模块进行解释说明。很多项目也配套了rst文件,然后用rst文件生成项目文档。简单理解就是一种文本,里面可以用标记符号来表示段落,代码,超链接,也可以加上样式。第二步 认识错误错误的根本原因是rst的内容不符合标准。很多工具支持rst文件可视化,看起来好好的,但是穿到pypi上面就报错了。下面细讲。标题格式错误了大标题格式错误=================== pypi_seed =================== 和这种格式的章节标题---------------- Further discussion / 更多使用问题 ---------------- Please raise PR or find the tencent group chat : https://jq.qq.com/?_wv=1027&k=ISjeG32x 第三步 动手解决上面两个错误都是PyCharm上面RST可视化工具不会识别的。大标题的格式换成下面的可以正常通过解析pypi_seed ========= 章节标题参考下面的格式:Further discussion / 更多使用问题 -------------------------- Please raise PR or find the tencent group chat : https://jq.qq.com/?_wv=1027&k=ISjeG32x 其他错误比如下面的内容也会报错:因为章节标题过长了, 改短一点。重新提交。bash Further discussion / 更多 -------------------------- rst 文件支持很多格式,也不可能一个一个罗列。下面是通用方法。上面都解决不了:直接找个开源项目参考有时候自己的rst文件改半天都不行。这个时候只能化整为零了。找一个开源的项目的README.rst 文件来参考:https://raw.githubusercontent.com/py4ever/pypi_seed/main/README.rst或者gitee上面的:https://gitee.com/py4ever/pypi_seed/raw/main/README.rst安装里面的内容修改自己的rst文件。小技巧01分段修改,起个新文件贴进去一段一段追加,直到最终生成自己想要的rst文件。小技巧02先写成markdown,在用工具转: https://cloudconvert.com/md-to-rst总结很多开源的python项目中都配套了README.rst, 都可以去参考,举一反三!编程问题没办法都写文档解释透,代码在变化,技术在变化,唯一需要我们的就是多动动脑,找对的例子参考。本文参考的开源项目地址在这里:Pypi-Seed on Github> 持续学习持续开发,我是雷学委!> 编程很有趣,关键是把技术搞透彻讲明白。> 创作不易,请关注收藏点赞,或者留下评论打打气!参考链接:https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
前面,学委写了一个保姆级别UI教程,展示了 【一个窗口放两个按钮,点击来提示不同消息】,挺受欢迎的。但那布局比较简单,本来打算安排开发签到程序的,但有必要教会一下布局(打好基础之后,干啥都快,还能胜任复杂的UI应用开发!)什么是布局?布局就是谋划部署干一件大事,跑偏了。这里的布局描述的是UI应用中的组件摆放。简单理解布局,就像搞室内设计定制一样,在大厅定制个桌椅,搞个茶具,通过精心布置设计让住户心情愉悦!搞UI类/界面应用开发,也是如此,再界面放置几个按钮,表格,给用户直接操作反馈数据(这个也是开发需要掌握学习的基础,就加进来分享了)好了,Python内置的Tk UI套件有三种布局,但是下面主要谈谈Grid(网格布局)。谈谈Tk的网格布局网格布局,很直观,就是网格化,像表格一样第一行第一列 第一行第二列第二行第一列 第二行第二列这就是网格布局的原型了,没错就这么简单。我们将要开发的UI应用如下:2x2 的表格布局,上面放置4个按钮,每行两个。这个怎么做出来呢?直接看下面代码这个代码在前一篇的基础上修改,使用grid方法,传入了一些方位的参数。保存下面代码为tkui_lxw_demo.py并运行。# -*- coding: utf-8 -*- # @Time : 2021/8/29 11:58 上午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : tkui_layout.py # @Project : hello from tkinter import * import tkinter.messagebox as mb top = Tk() top.title("雷学委的Tkinter布局Demo") top.geometry('600x400') def open_msg_box(): mb.showinfo("[学委温馨提示]", "简单的消息弹出窗口!") def open_err_box(): mb.showerror("[学委温馨提示]", "演示错误消息窗口!") #这里加上01和02在按钮显示文本上。 first = Button(top, text="点击01->查看消息", fg="tomato", command=open_msg_box) second = Button(top, text="点击02->查看错误", fg="tomato", command=open_err_box) #学委前一篇文章这里注释掉 # first.pack() # second.pack() #--- 雷学委CSDN代码展示 ---# #然后添加两个按钮放在第二行 button03 = Button(top, text="点击03->查看消息", fg="tomato", padx=8, pady=8, background="blue", highlightbackground="blue") button04 = Button(top, text="点击04->查看错误", fg="tomato", padx=8, pady=8, background="green", highlightbackground="green") #好,下面学委展示用grid布局(也就是网格布局) first.grid(row=0, column=0, sticky=W) second.grid(row=0, column=1, sticky=W) button03.grid(row=1, column=0, sticky=W) button04.grid(row=1, column=1, sticky=W) #老套路,调用事件循环保持UI程序不退出。 top.mainloop() 代码讲解通过观察,我们主要是使用了grid函数,它接受了3个参数。row 把ui组件放在第几排,从0开始column 把ui组件放在第几列,从0开始sticky 放置的方位W是表示朝向西边。上面的grid放置组件的代码就是告诉程序:学会了吗?运行程序就能看到上面的布局了。in或in_ 这个选项,可以将该组件放到该选项指定的父组件中, 指定的组件必须是该组件的父组件。这个默认即可。我们下面试试把,上面的表格可以多看无需记忆!代码如下:这里学委不贴全部了,直接在上面代码的按钮3(就是这一行开始*#— 雷学委CSDN代码展示 —#*)的位置开始覆盖为下面的代码。保存文件,重新运行即可。#--- 雷学委CSDN代码展示 ---# # 然后添加两个按钮放在第二行 button03 = Button(top, text="点击03->查看消息", fg="tomato", padx=8, pady=8, background="blue", highlightbackground="blue") button04 = Button(top, text="点击04->查看错误", fg="tomato", padx=8, pady=8, background="green", highlightbackground="green") button05 = Button(top, text="点击05 放满第三行", fg="tomato", padx=8, pady=8, background="green", highlightbackground="green") # 好,下面学委展示用grid布局(也就是网格布局) first.grid(row=0, column=0, sticky=W) second.grid(row=0, column=1, sticky=W) button03.grid(row=1, column=0, sticky=W) button04.grid(row=1, column=1, sticky=W) button05.grid(row=2, column=0, columnspan=2, sticky=W+E) #放在在第三排,列合并,同时设置W+E铺满第三列。 # 老套路,调用事件循环保持UI程序不退出。 top.mainloop() 延伸很多大型复杂的UI其实就是一个一个网格布局不断套用迭代起来的。掌握上面的核心布局可以推演,开发出更多层次的界面。Tk还有其他两种布局,学委主要并非UI开发,我认为另外的要么过于粗放要么过于细致,不便于快速开发(喜欢细抠的可以看看place布局)。布局思想不仅限于Tk,或者Python,很多UI方案都有类似的概念(流式布局,网格布局,精准位置摆放),掌握一种其实可以通吃,因为雷学委十年前就玩Swing应用开发很熟练了。。。后面再分享更多应用,没事多把上面的参数试试。如果读到这,会想一下上面第一排的按钮都有很大缝隙的?请问怎么铺满?欢迎评论区作答。
之前 你不知道Python多能干 ,秒懂精通pip并快速体验深度学习应用和 多图展示学会Python基础上篇 等,这次我们来看看使用Python 开发一个简单的UI应用吧。我们要做一个窗口,然后放两个按钮,提示不同消息。就做这个简单功能。先介绍一个UI库The tkinter package (“Tk interface”) 是一个基于Tcl/Tk GUI工具标准的Python接口。集合在大多数操作系统都有Tk和tkinter 库,包括MacOS,Window还有一些Unix类的操作系统!小白简单理解,这个就是一个标准的图形化用户界面开发的库!还是Python内置无需再次安装的库。好下面开始学委带大伙做App窗口。第一步,先做窗口,跑起来这里学委使用的是tkinter这个库,python内置的UI库。# -*- coding: utf-8 -*- # @CSDN/Juejin/Wechat: 雷学委 from tkinter import * #创建主窗口 top = Tk() top.title("雷学委的TkinterDemo") #标题设置 top.geometry('300x100') #设置窗口大小为300x100 横纵尺寸 #调用主事件循环,让窗口程序保持运行。 top.mainloop() 就几行代码,保存为first_ui.py直接运行。好,直接看效果:很简单吧。第二步,难度升级加按钮效果如下,尝试加两个按钮。就在前面的代码的基础上添加按钮组件。# -*- coding: utf-8 -*- # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # -*- coding: UTF-8 -*- from tkinter import * top = Tk() top.title("雷学委的TkinterDemo") top.geometry('300x100') def open_msg_box(): pass def open_err_box(): pass #添加按钮1 first = Button(top, text="点击->查看消息", fg="tomato", command=open_msg_box) first.pack() #添加按钮2 second = Button(top, text="点击->查看错误", fg="tomato", command=open_err_box) second.pack() top.mainloop() 好像也不难,但是运行上面的代码只会展示看到没有,这里有个command参数赋值是一个函数(但是学委还没有实现细节)。第三步,实现事件弹出消息窗口,错误窗口效果如下,点击不同按钮展示不同类型窗口和消息。# -*- coding: utf-8 -*- # @Time : 2021/8/29 11:58 上午 # @Author : LeiXueWei # @CSDN/Juejin/Wechat: 雷学委 # @XueWeiTag: CodingDemo # @File : first_ui.py.py # @Project : hello from tkinter import * import tkinter.messagebox as mb top = Tk() top.title("雷学委的TkinterDemo") top.geometry('600x400') def open_msg_box(): mb.showinfo("[学委温馨提示]", "简单的消息弹出窗口!") def open_err_box(): mb.showerror("[学委温馨提示]", "演示错误消息窗口!") first = Button(top, text="点击->查看消息", fg="tomato", command=open_msg_box) first.pack() second = Button(top, text="点击->查看错误", fg="tomato", command=open_err_box) second.pack() top.mainloop() 到这了展示完毕,学会了吗?学会欢迎在评论区打卡。请务必学会,下一篇我们讲学习开发这个工具:喜欢Python的朋友,请关注学习 Python基础专栏 or Python入门到精通大专栏
最近Nginx很火(这玩意很多年了),学委16年就开始用了,这里总结准备了一键安装的方法,送给大家,学会不用花冤枉钱买更多强劲服务器了!跟着一步一步学习一下,文末有一键安装的技巧(自动帮你源码编译!)Nginx 是什么它是一个轻量级的高性能的 Web 服务器 / 反向代理服务器,同时也是一个IMAP/POP3/SMTP 代理服务器。反正是一个好东西来着,“服务器中的超跑”值得耐心学习一下!主要是性能强,做个简单站点不用花太多钱买更高配置。小白不太清楚作用,可以把它当做一个很牛的服务器软件!我们可以在这个服务器软件内放置自己制作的漂亮页面。这里展示安装重点,然后学委最后会修改一下页面,一起看看吧。安装 - 源码编译安装Nginx下面是亲测可用的脚本(早在几年前就放在学委的Github上了,现在运行还是完美的!)保存代码为leixuewei.sh, 并在CentOS7运行。systemctl stop firewalld.service #use alibaba yum repo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup`date '+%Y%m%d_%H%M%S'` curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache #get netstat yum -y install net-tools yum -y vim #get deps to compile python modules yum -y install gcc automake autoconf libtool make yum -y install gcc yum -y install gcc-c++ mkdir /toolings/xuewei cp /tmp/*.tar.gz /toolings/xuewei cd /toolings/xuewei nginx=/toolings/xuewei/nginx/nginx #for url rewrite if [ ! -e pcre.tar.gz ] ; then curl -k https://jaist.dl.sourceforge.net/project/pcre/pcre/8.34/pcre-8.34.tar.gz -o pcre.tar.gz ; fi tar -zxvf pcre.tar.gz if [ ! -e $nginx ] ; then cd pcre-8.34 && ./configure && make && make install ; fi cd /toolings/xuewei if [ ! -e zlib.tar.gz ] ; then curl -k https://nchc.dl.sourceforge.net/project/libpng/zlib/1.2.11/zlib-1.2.11.tar.gz -o zlib.tar.gz ; fi tar -zxvf zlib.tar.gz if [ ! -e $nginx ] ; then cd zlib-1.2.11/ && ./configure && make && make install ; fi cd /toolings/xuewei if [ ! -e openssl.tar.gz ] ; then curl -k https://www.openssl.org/source/openssl-1.0.1t.tar.gz -o openssl.tar.gz ; fi tar -zxvf openssl.tar.gz cd openssl-1.0.1t/ ./config shared zlib --prefix=/toolings/xuewei/openssl if [ ! -e $nginx ] ; then make && make install ; fi cd /toolings/xuewei if [ ! -e nginx.tar.gz ] ; then curl http://nginx.org/download/nginx-1.4.2.tar.gz -o nginx.tar.gz ; fi tar -zxvf nginx.tar.gz cd nginx-1.4.2/ ./configure --sbin-path=/toolings/xuewei/nginx/nginx \ --conf-path=/toolings/xuewei/nginx/nginx.conf \ --pid-path=/toolings/xuewei/nginx/nginx.pid \ --with-http_ssl_module \ --with-pcre=/toolings/xuewei/pcre-8.34 \ --with-zlib=/toolings/xuewei/zlib-1.2.11 \ --with-openssl=/toolings/xuewei/openssl-1.0.1t if [ ! -e $nginx ] ; then make && make install ; fi ln -s $nginx /usr/bin/nginx cd /toolings/xuewei/nginx && ls -rtla #netstat -ano|grep 80 netstat -tulnp|grep 80 cd / && nohup /toolings/xuewei/nginx/nginx & netstat -tulnp|grep 80|egrep -v '80[0-9]+' echo "[雷学委温馨提示] nginx is started" 代码解析:上面的代码主要分为3步检查是否需要,否则下载依赖软件一个一个编译依赖软件编译安装nginx然后启动这就启动了一个Web服务器了,上面的脚本跑完效果如下:一键安装上的代码需要配置一台Linux虚拟机或者有一台Linux服务器,比较麻烦。所以创建一个目录放置Vagrantfile,然后跑vagrant up启动完事。具体操作如下,完成一次操作以后都能反复用。创建一个目录里面放置Vagrantfile所以学委准备了下面的Vagrantfie。Vagrant.configure("2") do |config| config.vm.box = "centos/7" config.vm.box_check_update = false config.vm.provider "virtualbox" $num_vms = 1 (1..$num_vms).each do |id| config.vm.define "nginxbox#{id}" do |node| node.vm.hostname = "nginxbox#{id}" #node.vm.network :private_network, ip: "10.64.4.#{id}", auto_config: true config.vm.network "forwarded_port", guest: 80, host: 80, auto_correct: true # nginx node.vm.provider :virtualbox do |vb, override| vb.name = "雷学委Demo的Nginxbox#{id}" vb.gui = false vb.memory = 2048 vb.cpus = 1 end config.vm.provision "shell", path: "leixuewei.sh" end end end 然后打开终端运行vagrant up这样就一键启动安装Nginx了, 做成的虚拟机还能分享给其他人呢。其他操作(非必要操作)如果想要加快安装的朋友,需要学习更多Vagrant知识-> Vagrant使用笔记,然后把下面的内容加到Vagrantfile。并跟着下载软件包。 config.vm.provision "file", source: "lib/nginx-1.4.2.tar.gz", destination: "/tmp/nginx.tar.gz" config.vm.provision "file", source: "lib/openssl-1.0.1t.tar.gz", destination: "/tmp/openssl.tar.gz" config.vm.provision "file", source: "lib/pcre-8.34.tar.gz", destination: "/tmp/pcre.tar.gz" config.vm.provision "file", source: "lib/zlib-1.2.11.tar.gz", destination: "/tmp/zlib.tar.gz" config.vm.provision "file", source: "nginx.conf", destination: "/tmp/nginx.conf" 读者可以先去下载相关依赖https://nchc.dl.sourceforge.net/project/libpng/zlib/1.2.11/zlib-1.2.11.tar.gzhttps://www.openssl.org/source/openssl-1.0.1t.tar.gzhttps://jaist.dl.sourceforge.net/project/pcre/pcre/8.34/pcre-8.34.tar.gzhttp://nginx.org/download/nginx-1.4.2.tar.gz非必选,下载上面的包放到当前目录的lib下面。学点原理,然后定制页面试试默认安装nginx内部如何运作?nginx通过读取/toolings/xuewei/nginx/nginx.conf文件,加载内部的server模块。server模块内监听了80端口,同时影射了网站到当前目录的html文件夹(也就是/toolings/xuewei/nginx/html目录),加载里面的静态文件。好了,就知道这么多就够了。我们进入虚拟机(Linux主机)内的/toolings/xuewei/nginx/html目录,找到下面的index.html. 改一下标题和欢迎内容为中文,如下:改掉主要是:修改title标签块,标题加了’[雷学委]’修改body内的h1标签块的文本改为:欢迎使用Nginx保存刷新浏览器页面,改动马上生效哦!因为是Nginx是直接加载html目录的静态资源的。重点来了:怎么把默认网站成个人站点很好办,把个人网页打包解压到上面的html目录,覆盖里面的文件包括index.html。Nginx很轻量还支持大并发(单台万级并发),网站加载速度直接起飞!当然,除非别人也是Nginx。就分享这么多,先用起来。如果是有动态网站的,把静态内容直接nginx处理,动态的交给像tomcat这种,性能至少翻倍提高,懂了吧!这个后面会细讲。
之前 你不知道Python多能干 ,秒懂精通pip并快速体验深度学习应用和 总结Python运行代码的几种姿势 等文章更多是给读者打开认知,找找感觉建立兴趣。本篇我们直接过一遍基础(上篇),代码和运行图片都贴好了。表达式下面表达可以直接执行1+1 2-2 5*5 9/3 "希望都333" print("希望都333") 几行代码分别展示加减乘除。我把结果贴出来了,方便学习:后面两行为字符串表达式,和函数调用表达式。变量 & 常量下面的第一行代码就是把右边的表达式的值,传递给变量text,或者说是赋值给text。第一行定义了变量text,同时把左边的值赋给它。第二行直接进行输出。text = "Hello, 雷学委的读者们!" print(text) text = "Demo" x, y = 1, 1 a = b = c = 333 第三行为变量重新赋值(设置新值);第四行为多个变量赋值。python没有常量!但是我们都会默契的把常量的名字命名为大写字母PI = 3.1415926 print(PI) PI = 3 我们看到第三行,PI变量被重新赋值了!所以都是默契大写字母命名常量,碰到这种常量不要去修改它的值!注释注释就是代码中标注,就像写PPT在旁边加一下备注。或者制作excel在单元格加备注一样。程序不会运行注释,但是备注的注释可以自己以后看,亦能给他人更方便阅读你的代码。python的注释使用#符号开头或者多行注释,如下:#这是学委添加注释:算出5的平方 a=5*5 #这里也可以注释 """这个也算注释 多行注释 """ 函数还是上面的代码,print就是一个函数。但是Python中函数出了调用,还能自己定义。类似数学中f(x) = 2x + 5 表示一条直线一样。"""雷学委现编函数:求直线上面任意一点的纵轴值""" def f(x): return 2*x + 5 #函数调用 print(f(2)) 定义了函数f(x)之后,我们就可以直接调用了。比如查看x=2时,f(x)的值,那么就调用f(2)。这里特别说明定义函数稍微有要求的。如下:(为了看清楚一点,学委用了“····”代表4个空格)def 函数名(参数1,参数2,…, 参数N):····第一行代码····第二行代码····省略N行代码····最后一行代码所以这里看到函数定义内部是需要锁进的,这样的代码为一个整体块。上面的函数内部用了4个空格缩进,那么内部每一行至少得4个空格缩进。如果碰到分支还需要再次缩进。python解析器会安装缩进规则扫描识别函数,如果缩进不一致,那么会报错!所以要么4个空格缩进,要么2个空格(学委看过的开源python框架都是4个空格的)。保留关键字保留是什么意思?看看下图,用and命名变量,程序直接报错!Python内不允许使用下面的单词作为变量名这些词就像汉字拆解出来的笔画一样,不允许被用来重新定义!反正大把命名的词汇,别硬杠上面的!标准数据类型Numbers (多种数字类型)String(字符串)List (数组)Tuple(元组)Dictionary(字典)数字类型整型数(不像其他语言,python只有一个整数类型,包括了长整型)浮点数( 其实这个就是带小数点的数)复数(这个还是数学课本中的复数)我们把形如a+bi(a,b均为实数)的数称为复数,其中a称为实部,b称为虚部,i称为虚数单位。不用都说,直接展示一下:zhengshu = 123 zhengshu = -12345 print(zhengshu) fudianshu = 123.1323 fudianshu = -123.34234 print(fudianshu) fushu = 1234j fushu = -1 + 143.3j print(fushu) 看完后,直接复制贴到终端运行:这里需要注意的是:python3中只有一个整型类型int,不像Java,当然python2是区分了int和long类型。这样在python3中,int类型的整数可以认为范围无限大(如果内存也跟着无限制的话)。字符串类型字符串就是多个字符串在一起被引号(单引号/双引号/三引号)括起来。比如text=“hello” 那就是把多给字符“h",“e”,“l”,“l”,"o"一个接一个串起来。我们看看下面的代码,直接运行比较直观。text1='hello' text2="Hello" text3="""学委展示多行文本! 第二行 """ text1 text2 text3 字符串其他操作text="abcde12345" text[0] text[2:5] text[2:] print(text * 2) #打印2两次 print("Hello" + text) text由于10个字符串起来。简单理解,第二行‘[‘和‘]’,这一对中括号,是python操作字符串获取子串的。效果如下:基本上比较简单,唯一注意的就是字符串的下标是从0开始的。数组如果我们把上面的字符串‘粗暴地’看作一个字符数组的话,那么我们这里说的数组更加宽松。它的元素可以是字符串,整数,浮点数等。比如下面的代码:list1 = ['1','2','3','4','5'] list2 = ['雷学委代码', 12345, 2.333, 5-4j] list3 = [] list1 list2 list3 截取获得局部数组类似于字符串获取子串,数组也可以通过‘[:]'符号来获取:list1 = ['1','2','3','4','5'] list2 = ['abc', 'cde'] list1[0] list1[1:3] #获取第二到第四个位置之前的元素为新的数组 print(list1 * 2) #元素复制一份追加后面排成一个新的数组 print(list1 + list2) #数组的加法 运行效果如下:元组Tuple元组Tuple是另一种系列数据,跟数组很像。但是外部用圆括号包围,内部元素用逗号隔开。而且元组是不允许修改,也就是可以当做只读数组。tp1 = (1,2,3) tp2 = ('abc', 123) tp1 tp2 print(tp2 *2) print(tp1 + tp2) 运行效果如下:没骗你,tuple类型只读的。这里补充一下数组的一个追加数据的方法append。(删除怎么做,读者可以自己想想看)。但是tuple类型明显没有支持这个操作。如下图:字典Dictionary字典类型,比较形象,就像字典一样,可以通过索引快速找到内容。比如某个字典只有三个词索引映射情况如下:a -> 吖b -> 呗c -> 吃所有它具有(索引-> 目标值) 这种结构。(玩过Scala的朋友就知道,scala写一个类似索引的结构很直观)dict = {} dict['a'] = '吖' #设置字典索引‘a'的值为’吖‘ dict['b'] = '呗' dict['c'] = '吃' dict print(dict['a']) #查找索引'a'的值 print(dict['b']) print(dict['c']) """学委这里展示了一个毫无意义的字典,只有三个词""" 执行效果如下:最后思考一下上面是比较轻松的学习,都是直接贴代码到终端就可以运行看效果了。现在来个小问题:def f(x): return x*x #请问下面是几次函数调用? #调用函数就输入:f(2), 调用了f函数,参数为2. #下面调用了print函数,参数为f(2) print(f(2)) 下篇链接: 多图速成Python基础语法下篇 。
文章为了让新朋友快速写Python代码,直接推荐了绿色内置版安装。很多时候比如在正式服务器环境,我们是使用rpm或者指定版本源码编译打包基础服务器镜像的。从简到难,先从安装包来安装python,后面在Linux安装并使用。最后是源码安装的方式,如下图Linux服务器多个Python版本兼容。好下面一步一步来。开发者电脑安装Window电脑,这个比较简单,我们进入Python官网,找到Installer下载安装。如下图,一般都是64位的选择圈中的64-bit下载。这个是exe文件,所以就一步一步直接安装即可。或者直接这个链接下载:https://www.python.org/ftp/python/3.9.6/python-3.9.6-amd64.exe本机安装,我们的目标是使用Python开发,一步一步直接设置默认的就最好!(也就是环境变量也不用去定制了)MacOS如何安装Python下载pkg文件,一步一步安装,同上,基本不用自己配置环境变量!https://www.python.org/ftp/python/3.9.6/python-3.9.6-macos11.pkg最难的来了。源码安装Python?这种安装就比较多步骤了,tar包编译安装,然后配置环境。这种过程多,能够学到很多。源码安装python可能遇到的多个问题python版本过新,导致安装过程缺少一些c代码的方法或者.h文件缺失安装过程缺少一些python开发相关的库,比如想zlib或者ffi库等ZipImportError: cannot’ decompress data; zilib not available比如下面的代码致命错误ffi.h(当然下面的错误是因为libffi这库没有预装了)准确点说,主要是下面两个问题:代码要求高版本的gcc,当前系统的过于陈旧!python开发依赖缺失了,当前系统没有安装!但是问题不大,迎难而上,还好不是迎男而上。升级gcc,需要root切换为root用户安装yum install -y centos-release-scl yum install -y devtoolset-7-gcc* scl enable devtoolset-7 bash gcc -v #学委的代码分享 安装缺失的开发依赖库切换为root用户安装#这里距离 yum install -y libffi-devel.x86_64 yum -y install zlib* 升级完gcc和依赖后可以安装了直接报存下面的代码为install.sh,然后运行安装吧。#从这里下载安装 curl -k https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tar.xz -o leixuewei-python.tar.xz #解压 tarball #xz -dk leixuewei-python.tar.xz tar xf leixuewei-python.tar.xz #配置安装 cd Python-3.9.6 ./configure --prefix=/opt/python3.9.6 make sudo make install 安装后使用打开任意终端/CMD输入:python安装顺利,能够查到版本号码。可以这样设置(不影响Python2)这种可以对某个特定用户设置python3,而且不影响其他用户使用python2首先需要root用户在python3安装路径创建软连接cd /opt/python3.9.6/bin ln -s python3 python ln -s pip3 pip 对某个用户的.bash_profile中添加设置PYTHONHOME和PATH,把bin目录放在path的最前面(至少要在搜索python2,也就是/usr/bin目录之前)#比如xuewei这个用户 PYTHONHOME=/opt/python3.9.6 PATH=$PYTHONHOME/bin:$PATH 成功设置:也可以这样设置上面源码编译后二进制文件为python3,我们希望在任意目录输入python都能调用python3。可以切换到root用户,通过下面的命令设置:cd /usr/bin unlink python ln -s /opt/python3.9.6/bin/python3 python ln -s /opt/python3.9.6/bin/pip3 pip 执行后,我们打开一个终端输入python就用上python3了。再回来说环境变量使用Python安装虚拟环境,并创建一个项目pip install virtualenv mkdir py-selenium cd py-selenium && virtualenv . 比如在使用viritualenv创建虚拟环境遇到下面的问题:yum install openssl-devel cd ~/Python-3.9.6 ./configure --prefix=/opt/python3.9.6 make sudo make install activate 虚拟环境:到这里证明我们整个安装是顺利,而且可用的。题外话不过我们在服务器端,主要是以Linux服务器安装Python为主。同时在大公司,服务器成百上千不在话下,语言和软件都是有统筹管理的。就算是自动化安装也是把这个手动过程编写为自动化脚本或者配置比如Ansible。或者用Docker镜像,但这些方式本质也是走服务器安装的方式,只是被打包到镜像了。
很多时间我们会在登录或者比如一些抽奖报名页面放一个验证码。然后在后台进行校验。通过放置图片验证码,这样可以防止机器人暴力扫描重试系统接口。当然也有些网站验证码设计的像是防止正常用户使用似的(像之前某车票网站一样。。。)我们直接来看效果如上图,登录表单提交一个用户名字和验证码。后台获取输入验证码,并进行校验。如果校验失败需要用户点击图片生成新验证码图片,然后继续提交表单校验。输入错误图片验证码如下图,这个例子就禁止用户继续登录,打印提示信息。原理某用户小白刷新登录页面,后台根据一个给定的文本来生成验证码,并把这个文本记录起来。(存储在服务器会话session)当接受这个数字对应的图片验证码的用户,提交表单的时候。(小白需要提交验证码图片一致的文字)。后台从会话获取对应小白的文本,进行校验。校验成功,则认为是小白的人工操作,继续放行后续操作。npm install -S svg-captcha 核心代码如下:const express = require('express'); const session = require('express-session'); const serveStatic = require('serve-static'); const bodyParser = require('body-parser'); const app = express(); // parse application/x-www-form-urlencoded so that req will has a body attribute app.use(bodyParser.urlencoded({ extended: false })) app.use(serveStatic('./public')); const port = 12024; app.use(session({ secret: '雷学委mySecret2021', resave: false, saveUninitialized: true, cookie: { secure: false} })) const svgCaptcha = require('svg-captcha'); // a function to generate captcha and display on user screen const captChaHandler = function(req, res){ var captcha = svgCaptcha.create(); req.session.captcha = captcha.text; res.type('svg'); res.status(200).send(captcha.data); } app.get('/captcha',captChaHandler); //user will submit form with code in request body and login handler will get captcha from session and check it with given code app.post('/login', function(req, res){ console.log('[雷学委] try login'); console.log('[雷学委] body:', req.body); var captchaCode = req.session.captcha console.log('[雷学委] captchaCode:', captchaCode) if(req.body && req.body.code == captchaCode){ res.status(200).send(req.body.user + " 登录验证成功!"); }else{ console.log('[雷学委] 验证码校验失败'); res.status(400).send("BadRequest, 验证码不对!"); } }); console.log('listening port ' + port); app.listen(port); 对应的我们在页面可以编写类似下面的代码(非完整,仅重点展示核心实现)<form> 姓名: <input type="text" id="username" value="雷学委" /><br/> 验证码:<input type="text" id="code" value="" />&nbsp;<img class="captcha" src="/captcha" οnclick={$(event.target).attr('src','/captcha?'+Math.random())} /> <br/> <input type="submit" value="Submit" /> </form> <br/><div id="result"></div> <script src="https://cdn.staticfile.org/jquery/3.4.0/jquery.min.js"></script> <script> $(document).ready(function() { $("form").submit(function(e){ e.preventDefault(); var userName = $("#username").val(); var code = $("#code").val(); $.ajax({ type: 'post', data: {user:userName, code: code}, url: "http://localhost:12024/login/", success:function(data){$('#result').html(JSON.stringify(data));}, error:function(error){$("#result").html(JSON.stringify(error));} }) }); } 主要是为了实现下面的form表单:
刚好我手上有一个Unity安装包, 就发给他了。不知道啥原因,他说传输后打不开?怎么可能呢?接着,再传了一次还是打不开。这让学委一下子想到了:得cksum一下1) 先cksum工具本地算一次,得到校验码2)然后在接受的系统中又计算一次,得到文件校验码3)当两个数字校验相等,文件被认为是被正确传输了使用就像下面一样:cksum 文件名 第一个数字:2283207869 //为校验码第二个数字: 844948304 // 为文件字节数然后我让小白在本机跑一边cksum,发结果给我,一看他那边的校验数字居然是 33303330333 ,这个数字,稍微思考一下就很离谱!很明显Unity包传输出错了。这次我直接拷贝U盘给他了,并且进入U盘对应目录进行cksum了,万无一失!好了,小白可以先走了。亲爱的读者我们继续学习一下cksum吧,很多使用的。cksum官方补充Linux cksum命令用于检查文件的CRC是否正确。确保文件从一个系统传输到另一个系统的过程中不被损坏。CRC是一种排错检查方式,该演算法的标准由CCITT所指定,至少可检测到99.998%的已知错误。指定文件交由cksum演算,它会回报计算结果,供用户核对文件是否正确无误。https://www.man7.org/linux/man-pages/man1/cksum.1.html这个算法不继续介绍,本文谈谈应用。更多应用 - sha512cksumsha512cksum 比cksum(32位 cksum)更加可靠,因为是512位哈希cksum。比如我们常见的maven(Java项目管理工具):下图的表格第二列为下载链接,第三列为每一个包的sha512cksum的签名。上面页面的链接可以点击【maven下载页面】我们可以通过点击上面的链接下载,比如这个:maven tar gz包通过这个链接下载然后跑shasum在本地校验一次。shasum -a 512 apache-maven-3.8.1-bin.tar.gz #得到这个签名:0ec48eb515d93f8515d4abe465570dfded6fa13a3ceb9aab8031428442d9912ec20f066b2afbf56964ffe1ceb56f80321b50db73cf77a0e2445ad0211fb8e38d 这个值跟第三列链接的文件【点这里下载sha512】内容必须一致.操作复杂,学委准备了下面的脚本。#!/bin/sh #雷学委的demo代码 #仅支持macbook url=https://mirror-hk.koddos.net/apache/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz #url_sha512=${url}.sha512 url_sha512=https://downloads.apache.org/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz.sha512 curl ${url_sha512} -o maven.tar.gz.sha512 curl ${url} -o maven.tar.gz shasum -a 512 maven.tar.gz 读者可以运行这个脚本试试,最好的验证效果如下图:更多场景多文件迁移校验通常是用在大量的打包数据迁移,生成每个文件的数字签名。部署制品的校验做Java的同学知道一个叫做Nexus的依赖仓库,不止可以放jar,还能放tgz包,我们通常会生成tgz包的同时,进行sha512把签名结果存到文件一并传到nexus上面。当我们拿tgz文件部署的时候,同时下载tgz和sha512,本地校验,保证了部署安装的包跟实际交付的一致。
小白最近没有来问学委问题,不过前几天,有朋友问到如何进行访问控制,资源控制的,学委特地写了一篇。这其实就是权限认证,理解并掌握其核心思想很重要,而且每个系统都避不开!下面一起看吧。故事讲述 - 极简权限认证【代码+原理+建议收藏】(这里附上了一个视频,过去看一下来个三连呗)通常情况下,我们访问所有API都需要进行用户访问权限鉴定。比如判断当前访问的用户是什么人,什么角色。下面基于商业办公楼安保系统来陈述。今天去上班但是忘记带员工卡了做为一个上班族,你去一个大型办公楼某一层楼办公/上班,想要进入不同楼层,商场物业会进行鉴定。然后进入电梯打开按某一楼层。然后进入办公室之前还需要打开,这个滴卡的时候还会联网校验。有些公司还会对员工进行更多的识别,但是本质都是说类似的。当然滴多少次卡验证多少次,取决于你要去哪里,对应设置多少道安全关卡。这其实跟我们做网站/平台的权限认证一个道理。我们去一个地方滴卡,在技术中对等什么行为呢?其实我们做系统为了安全,很多时候不会直接发送用户名和密码,这是不安全的。这就像,多数办公楼小区不会每次都让你拿出身份证,然后让你背诵身份证号,地址。或者每次你进入一个场所,都让你回答一堆私密问题,当答案和系统预先录入的数据完全配置,才让你进去。这很麻烦!所以我们做权限认证的时候基于一个令牌(token)机制,这就像我们拿一个员工卡,门禁卡一样。这里我们简单理解一个概念,token就像一个门禁卡一样,我们访问平台的资源的时候都带上这个token,滴一下,资源可以访问。没滴卡/拿错别的卡,禁止进入办公楼!先看效果,这就像下面的展示效果一样:打了卡,我能进电梯/办公楼了吗?先看效果:这个是登录用户访问受限资源的让我们对比一下两次请求第一次访问受限资源"/restrictData",通过Inspector查看,这个请求的请求头并没有带上token。就像忘记拿卡一样,网站直接拒绝访问了。第二个请求细节如下,细看下图,这个请求携带了token令牌,访问受限资源,并且是有效果token,所以下面输出了“访问受限资源,当前用户雷学委”。看完效果,你懂了权限认证的核心了吗?你是否有几个问题在脑海中这个网站是怎么认证每个请求是否有效合法?(针对每个访客,检验访客是否可进去访问资料)这个网站的令牌token怎么生成的?(类似卡是怎么做出来,发卡的)说完了上面一些直观感受,下面雷学委要带大家解读技术实现了,这次用javascript来实现,基于NodeJS引擎运行。原理解析第一个问题,如何验证每个请求是否合法我们这里使用了express的拦截器(Interceptor)。代码如下://针对每一个请求 app.use((req, res, next)=>{ var url = req.originalUrl console.log('filter - url:',url) // 第一步,判断是否白名单,比如登录,退出,其他不限制的资源访问。 if(url.startsWith('/login/')||url==='/logout'||url==='/checkToken'){ next(); return; } // 第二步,如果不是上面的白名单资源,那么获取令牌token,没有代令牌直接拒绝 var t = req.header('token') console.log('filter - request header token:', t) if(!t){ res.status(400).send("禁止越权访问!!!") return } // console.log('filter - current session:', req.session) //第三步,查询网站的用户令牌注册表,是否保护当前令牌对应的用户,否则显示令牌失效,类似拿错卡了。 var user = getUserByToken(req) if(!user){ res.status(400).send("您的token失效或者未登录,请登录!") return } //最后,上面的校验都通过了,直接进行后续资源处理。 req.session.user = user console.log('filter - check user is ' + user) next(); }); // token令牌注册表 const tokenRegisterTable = {} // 定义一个根据令牌查询并获取用户信息的方法 const getUserByToken=(req)=>{ var t = req.header('token') if(!t) return false return tokenRegisterTable[t] } 所以,通过一个拦截器,类似Spring的AOP机制(或者Servlet中的Filter)针对每一个请求查看令牌。这个拦截器/过滤器,就像一个坚守在阵地的安全卫士:小兄弟站住,请出示有效证件?否则,尽快离开!(图片转自:https://www.sohu.com/a/293107575_579043,如果有版权问题请告知,感谢)令牌如何生成,卡片怎么造出来发行的?注册卡片,就像每个学生/员工去相关单位报到一样,你报到当天,管理给了你一个卡。在我们代码中,通常都会有一个注册过程,但是不会这个时候发卡给你,因为网站数据发一个永久可以用的卡,会有很大的安全隐患。所以更多的做法是,每次登录都发卡(token令牌)!而且这个卡是有时限性的。下面是登录发放token令牌的实现://这个是登录的接口,这里省去了校验密码的过程 app.get('/login/:user', function(req, res){ const user = req.params.user console.log('user is ',user) //从express 的session会话中查看用户是否已经登录 if(req.session.name){ res.send('您已登录,用户'+req.session.name) return; } //如果没有登录,记录用户和在相应中设置两个cookie,记录用户名和token令牌 req.session.user = user res.cookie('name', user) res.cookie('token', getTokenFromSession(user,req.session)) //token令牌用于客户端存储。看看下面的浏览器token的截图 res.send("登录用户=" + user) }); // 一个生成随机令牌的简单实现 const getTokenFromSession = (user, session)=>{ var token = 'token-' + rand() tokenRegisterTable[token] = user console.log('tokenRegisterTable=',tokenRegisterTable) return token } const rand = () => new Date().getTime()+'-'+Math.floor((Math.random()+1)*1000) 我们看看用户本地的令牌,这里还存了一个用户名。难度升级,我们看看大致过程下面是这个极简权限认证的核心代码,学委这里只展示重点环节。展示了登录和令牌生成,但是没有从数据库查加密密码密文或者分布式缓存校验用户回会话。只是简单的对cookie和session进行加密只是简单做了一个tokenRegisterTable,令牌注册表实现了登录/退出/一个受限资源进行核心原理展示。const express = require('express') const serveStatic = require('serve-static') const session = require('express-session') const cookieParser = require('cookie-parser') const app = express() const tokenRegisterTable = {} //价值静态页面,上图中展示登录/验证访问受限资源的页面 app.use(serveStatic('./public')); app.use(cookieParser('雷学委Key')) app.use(session({ secret: '雷学委Key', resave: true, saveUninitialized: true, cookie: { secure: true } })) const getUserByToken=(req)=>{ var t = req.header('token') if(!t) return false return tokenRegisterTable[t] } //请求拦截器 app.use((req, res, next)=>{ var url = req.originalUrl console.log('filter - url:',url) if(url.startsWith('/login/')||url==='/logout'||url==='/checkToken'){ next(); return; } var t = req.header('token') console.log('filter - request header token:', t) if(!t){ res.status(400).send("禁止越权访问!!!") return } // console.log('filter - current session:', req.session) var user = getUserByToken(req) if(!user){ res.status(400).send("您的token失效或者未登录,请登录!") return } req.session.user = user console.log('filter - check user is ' + user) next(); }); //退出用户,清楚本地的访问令牌 app.get('/logout',(req,res)=>{ var t = req.cookies['token'] var u = req.session.user req.session.destroy(); tokenRegisterTable[t] = null res.clearCookie('name') res.clearCookie('token') console.log('logout - session:',req.session) res.send(u + ' 退出登录成功!'); }); //一个简单的受限资源,当用户合法访问时,返回“访问受限资源,当前用户某某某“ app.get('/restrictData',function(req,res){ var user = req.session.user res.send('访问受限资源,当前用户'+user) }); const rand = () => new Date().getTime()+'-'+Math.floor((Math.random()+1)*1000) const getTokenFromSession = (user, session)=>{ var token = 'token-' + rand() tokenRegisterTable[token] = user console.log('tokenRegisterTable=',tokenRegisterTable) return token } //单独校验token的接口 app.get('/checkToken', function(req, res){ res.send(getUserByToken(req)&&true) }); //登录用户的校验 app.get('/login/:user', function(req, res){ const user = req.params.user console.log('user is ',user) if(req.session.user){ token = req.cookies['token'] let data = '您已登录,用户'+req.session.user res.send(data) return; } req.session.user = user res.cookie('name', user) res.cookie('token', getTokenFromSession(user,req.session)) console.log('user login :' , user) res.send("登录用户=" + user) }); PORT=12024 app.listen(PORT, function(){ console.log('app running at port ' + PORT); }); 上面代码登录,拦截器解释过了。这里补充一下登出/退出这个接口很有必要,每次用户访问一个logout或者点击登出的时候,从服务端的令牌注册表清楚对应的令牌映射为空,同时清除用户浏览器端的token(当然这不是标准做法,特别像App,一般是会保留一个refreshToken,本文不会涉及)。总结访问每个资源都需要带卡,而且合适的卡,才能访问相应的服务!或者有些读者能联想到健康码,本质上是一种信任授权机制!再次说下,为啥每次校验不直接用密码,每次请求都带密码,造成泄漏密码的风险更大!至于令牌就像一个卡一样,随时可以作废取消访问,更好管理。本文重点展示了web平台权限认证的思想,涵盖了权限认证的令牌生成,令牌管理,请求校验的过程。其他像令牌定制,比如有JWT令牌,令牌时效性,用户角色管理这些没有涉及。这些实现有很大细节定制。如果开发者不理解本文的核心思想,更别谈实现这些细节了。像微服务平台会有一个统一鉴权服务,像大数据平台还有TMS,引入Kerberos,那个实现机制更加复杂。后面有机会继续讲。
学委之前做视频感觉就挺吃力的,听说能用编程来做,一下子就起劲了,估计能更好呈现趣味故事学编程这个专栏! 这就搞起来。不过我从未使用过Unity,但是作为一个写了十几年Java的多语言爱好者,我觉得不应该被难倒!先看看效果:帮你省了3个小时,来直接入门开发Unity的第一个HelloWorld把学习新知识,当做一个挑战,下面我就展开了。安装Unity我用的是MacBook Pro: 学委下载的是2017的UNITY LTS版为什么选择这个?因为通过UnityHub安装(我是先安装了hub的),安装的时候提示要14G左右的空间下载Unity相关的。 另一个朋友展示安装Unity 2020的开发组件也有2G多。新手体验其实可以先挑个简单好弄的,干嘛不来个轻便快速的!怎么快怎么来,下载上面的2017LTS版本。安装过程很简单,这里唯一要注意的就是选择License,Unity支持个人学习使用的,请记得选择‘Unity Personal’,其他都是下一步下一步很简便安装的。开发新建一个2D项目如下操作:建好项目之后的主界面如下:这个界面左中右分布。左边为场景,镜头,和组件管理。中间为可视化区域,包括游戏模拟窗口。右边为Inspector/Services栏目,主要是更加精细化控制Unity组件的。然后下方默认是:Project和ConsoleProject就是管理项目文件和资源。Console就是运行C#程序展示调试日志信息的。如下图(学委自己加了一个AudioMixer)初步感觉学委进入初学者模式才一会,下面是个人感觉,但请勿全信!一开始进入Unity,我是懵的。这玩意跟AutoCAD/SolidWorks/3DMax这些建模的好像啊。摸索了一下,也感觉好像,可能Unity多了绑定脚本的和游戏渲染这一块吧。Unity跟其他开发语言相比就像是:初学Unity跟开飞机一样,上面给你很多按钮组件,也能编程但是这玩意你需要操作熟悉,熟悉就快一点。不然你花再久也找不到!!!(崩溃)学习Java/Python这些就像骑个单车一样,让你上车了,再在上面加东西,对于开发者更多是语言上层框架的探索拓展,不断堆外设,最后变得功能丰富,赶超飞机得看实力了!(C++做的游戏引擎很可以的)好,先放下感觉,直接摸索看了一部分文档。先看下面操作。操作重点在主界面加入方块,圆形,和文字快等。添加Sphere(圆形) 和 Quad(方块)这两个组件。下图为方块组件:绑定代码到方块上面选择方形组件(Quad)进入Inspector,接着点击右下角的Add Component添加脚本(绑定我们的程序代码)输入Player,然后点击“Create And Add”:成功后我们看到Inspector栏目中多了下面这个:懒人复制下面的代码到Player这个C#脚本内并保存:using System.Collections; using System.Collections.Generic; using UnityEngine; //雷学委Unity小白初学demo public class Player : MonoBehaviour { // Use this for initialization void Start () { Cursor.visible = false; } // Update is called once per frame void Update () { //获取鼠标纵轴 float y = Camera.main.ScreenToWorldPoint(Input.mousePosition).y; //让当前组件的坐标x轴保持不变,y轴跟者鼠标移动,也就是原地上下动。 this.transform.position = new Vector3(transform.position.x, y, 0); } } 如上图位置,点击中间播放箭头➡️按钮,再看看效果截图:重点代码解析:下面截图的知识是核心接口MonoBehavior,这个类很重要(链接在文末)!这个类就是用来更新组件状态的:也就是我们想编程让组件往左往右,上串下跳都得来看这个。修改方块颜色这个操作小白可以不做!具体屏幕右方点击Inspector->Materials -> Element 0 点击配置按钮,选择方块的材质。最后的效果:总结本文只是一个从未学习过Unity, C#的开发者(说的是雷学委自己了)从安装到开发的第一个体验,算是一个好的开始!而且之间从这个文章开始,可以节约一写探索时间,直接复制可运行代码!建议新手都看看,记得收藏后面写程序的时候再多回顾!而且读者们请多花点耐心,下图是官方一个LEGO(乐高)的游戏,付出努力必能拿下这个水晶,下次再试试!还有更多的酷炫的效果,这里建议你先简单学习一下!(PS:这官方的版本跟学委Unity版本不兼容,所以本文没有选择之间demo这个,因为需要安装14.9G的相关软件估计3个小时都下不完)不管你编程能力如何,在新知识面前我们永远都是小白。所以请时刻保持虚心学习,这样才能进步。后面再把安装和具体操作视频更新。
或者对方电脑没装python,直接编译成一个exe文件,就很方便。学委这里做了一个短视频,可以看看整个过程。C站配套食用:python代码编译发布为可执行文件【保姆级别操作指导,建议收藏】(喜欢的过去给个三连鸭,别下次一定了!B站萌新UP需要很多鼓励呢,感谢!)好,让我们看看怎么做。雷学委找到了这个神器:PyInstaller这玩意就相当于,一个免费翻译,懂了吧,负责把python代码直接翻译成exe文件了!这是官网的说明,好像挺6的!多方便啊:安装,然后就可以编译python程序成为exe可执行文件了。确认安装好pip参考 快速安装并掌握pip使用pip安装pyinstaller执行下面代码:pip install pyinstaller 效果如下:比如学委想要编译打包demo.py这个python程序demo.py程序代码如下:import time #很油条的死循环,没干啥就是睡3秒 #然后打印! #雷学委奉劝各位朋友做技术千万别学这个程序,哈哈哈 while True: time.sleep(3) print("waiting ...")``` 开始运行下面代码翻译python代码为exe ```bash pyinstaller demo.py 上图为整个运行过程,13462行信息提示编译了exe文件,并存到了当前目录的build/demo目录,exe文件名为demo。进入build/demo目录,双击demo文件运行看看(因为博主用的是苹果电脑,也没有装那个运行window的软件,运行不了。)不过官网文档说编译后的最后结果放在dist文件夹了,我们看看。不错,执行正确!补充一下:最后是文件大小如下图,原来的demo.py脚本才4k,编译为可执行文件整个dist文件夹为16M(整个打包发送给他人执行即可)(毕竟PyInstaller并非直接翻译为机器码,而是通过cpython解析器来解析的,这样不要求对方电脑安装Python,而且也比安装python要小得多了)下面是dist/demo目录的部分cpython库文件展示:
回去狂写代码,写了很多代码,面向对象,继承,多态,入门,初级的统统都写了。雷学委既想表扬,又不能! 毕竟,老人这样猛敲代码,对身体不好! 不过老人代码写的很不错,小白看了想要整理汇编成一个新手填坑展示项目(挺有头脑啊!不愧是遗传!)跑过来找学委,你看我奶奶这代码,好多个文件夹,请问怎么样把这些组织好,方便代码分享啊?哇,项目工程化这一块很重要啊,很多小白写很多代码最后还是零散分布的,没有形成自己的积累,其实很亏!效果如下,可惜端午节过去太久,雷学委只能顺着鞋厂大学问再深入讲解,不然给大伙讲如何工程化包粽子也不是不行呢。雷学委趣味编程配套食用:一个Java项目的简单工程化来,我们一起看看,王总的大型鞋厂一条龙大企业家王总,虽是半路出家做鞋子,可做事一点也不迷糊。他经历过一块块砖头,一车车水泥沙石钢筋堆砌成一层层楼房的时候,也经历过一栋栋一片片的房屋出售的冲锋时刻,啥场面没见过。自然想的已非个人亲手做一只只鞋起家,而是直接联系了很多大鞋厂老板,说要给他们投资,结果参观学习了3个月,探访了不少不同规模鞋厂。王总:想要搞大规模,就必须把体系做好。所以转型做鞋子,第一步,得把整个工程生产线搭建好,不然别的都是白干费力!雷学委认为做软件也是如此,工程化是很重要的。高效的组织代码,构建高质量代码产品,对个人对企业都是很有帮助的!小白直呼,商人特么精明!这手段玩的是一套一套的,要是王总写代码不知道得多6!鞋子是怎么来的?原料: 鞋底,鞋带,皮革,缝合线等等的。这些组合而成就是一只鞋子,然后再成对匹配。这些肯定都标准化,每双鞋都是同一系列的步骤照做的,不然最好做出来的鞋子形状千奇百怪,怎么做好销量?(又不是搞个人定制鞋款的)说到这,小白好像突然开窍了,插嘴说:您是不是想说,这些玩意就跟我们的源代码一样?小白激动地说:做好的鞋子,那就是相对于发布的一个jar包啊?!哈哈哈,小白说出这个理解,学委很开心。这样讲解工程化就好理解了!这里补充一下jar包,这是我们java同学有时候写了一些代码直接打个jar文件,包含程序发给别人直接运行。有点类似python的whl包,但是有区别,whl包只能用pip安装后在代码使用。不错哟! 小白悟性是越来越好了。小白又问,那继续说说具体工程化吧?手动打包搞多了挺累的,有时候漏了被同学嘲笑还得再来!好的,让我们继续看看王总怎么做的呗。(下图为鞋底机械自动成型)(图片截图自:如何做一双适足的鞋子 — 自动制鞋生产线 )王总这个工厂做鞋车间基本上都是机械化自动的,加上最后少量的人工校准。整个制鞋大车间,有个机器负责把鞋底膜摆放好,传送带运到一个密闭小盒内,一个气垫放上,高温压印。鞋底成型了。(图片截图自:如何做一双适足的鞋子 — 自动制鞋生产线 )传送到下一个链条,皮革准备好机器自动缝合,基本误差很低。差不多了,最后上一个自动的拉伸鞋子看看生产的这个鞋子质量如何。这时候鞋子基本上好了,后期销售再配点鞋带就可以了。工程化/项目组织 是什么鬼呢? 就是通过一条纽带把项目的代码,文档,测试,脚本等组织管理起来,目标是提供一个简单的使用,比如mvn clean deploy直接发布jar包。或者npm run start去启动一个前端项目。就只有这个好处吗?在这里做Java的朋友,可以理解为一个流水线很方便的按个按钮就将代码生成jar包,接着可以自动发送jar包,也允许少量人工比如把jar包发给一些个人使用。回到实际项目中,这里拿一个Java项目来说在外面项目中,我们会把代码,放在一些文件夹(package)中。然后也有一些启动脚本(不同配件)放在特定目录,测试代码放在不同的目录。然后用maven/gradle,里面使用了不同的插件(plugin)去运行代码的编译,代码测试,编译产物拷贝移动到特定目录。然后用另一些插件去组织代码。下面是一个真实的Shoe项目的“车间视图”,下面再解释。(IntelliJ开发工具视角,左边文件目录结构,中间为pom.xml, 右边为构建生命周期)让我们来看这个工作车间的一个产品作业线(一个子模块leixuewei-classobject)。谈谈这个制鞋项目,编译一个鞋子类,写一个测试类,打包代码为一个jar包。类似摆放鞋子原料,如上图,我们开发需要把代码相关文件放置好。如下图,整个项目把代码分为了两个大目录src/main(核心代码) 和src/test (测试)src/main/java 负责放置项目的核心代码src/main/resources 负责放置项目运行相关配置src/test/java 负责放置项目的测试代码src/test/resources 负责放置项目测试相关配置做Java的同学经常会使用一个叫Maven的软件工具,用它来管理项目的代码,依赖,同时执行测试,跟打包。就像上面一个真正的制鞋子的车间图一样,Maven就像整个流水线系统一样。制鞋车间分步骤的组装鞋子,Maven分步骤地执行各个阶段的插件编译源码(填充鞋底),执行测试(机械臂拉伸缝好的鞋子),打包代码(自动配件分类包装)。具体工作如下:Maven这个工具根据pom.xml读取项目组织,依赖,插件plugin(类似制鞋车间内的一些辅助维修工具)。maven-compiler-plugin : 这个插件负责编译java代码成class文件maven-surefire-plugin : 这个插件负责执行测试代码maven-jar-plugin: 这个插件负责整理class和相关配置达成jar包maven软件把这些插件绑定到了不同的软件构建生命周期,这就像一条龙车间流水线一样按照顺序执行,把源码转化为最终产品。Maven 还能做的事情更多,这里只是展示了编译打包了一个jar,它还能把jar包上传到公共仓库给其他开发下载使用呢。通过把整个项目使用Maven管理,以后需要打包项目,只需要运行mvn clean package就可以打包代码了。这个命令就像,我们在制鞋车间,输入指令一样,车间根据指令执行对应的操作,保证了整个过程都是机械无人工参与,减少人为操作失误导致的质量问题。小白听完,满脸高兴,这个过程还能自动化的,原来编程这么有意思的!(可不,软件世界只是现实世界的体现,对现实问题的抽象罢了!)总结好的项目工程化,可以把开发过程整规范管理。分好了不同车间,不同成员开发不同的模块,大大减少互相干扰。而且前端后端工程化技术棧不一样,大数据SparkJob项目也不一样,但是整个过程是很类似的,规范的把开发原材料(源码Java/Html/Js等等)转化为高质量的输出制品(jar包,zip包),也有构建好后直接把包分享和部署到测试环境运行的,甚至动态测试提升部署到线上的!这里有一个点很重要,工程化需要适度原则,雷学委建议读者去参考对应语言的优秀开源项目的代码工程化方式!学委见过把代码一股脑打包的,几个G,你就是上去改个标点符号也要打包半天,这不是好的工程化。也看过把十几个类拆成6个项目,改一个类速度是快了,但是搞一个聚合功能,那你得跨组件修改代码,多个重新运行打包,得不偿失!
像记日记一样,日志是通过程序打印出来的,记录程序内部何时何处发生了什么事件。本文只为使用学习为主,想对日志管理和分析有更深认识的可以看 => 日志原理与开发分析这篇完全指南就够了!适应各种规模我们也用过console.log来打印一些调试信息,有什么区别呢?请读者带着这个问题往下读。先安装我们这里先安装一个日志的模快。打开终端执行下面命令:npm install log4js npm install log4jsconst log4js = require('log4js') var logger = log4js.getLogger('雷学委开发日常') logger.level = 'info' logger.info('早上起床') logger.info('拥抱太阳') logger.info('吃完早餐') logger.info('充满希望') logger.info('开始了代码的一天') 保存上面代码为demo-fun.js ,然后运行:node demo-fun.js可以得到跟上面日志图片的效果,读完可以试试。效果如下:项目配置使用日志log4js 可以基于代码配置日志规则,但是我们推荐使用json来配置。这是一个好习惯,虽然修改代码和配置都需要重启应用,但是可以通过程序实现让代码不重启也加载日志,这一点本文不作深入展示,后续补上。代码配置日志读者可以保存为demo2.js, 自行运行。//demo2.js const log4js = require("log4js"); //这里配置log4js使用fileAppender来输出“error”级别的日志。 //然后fileAppender是一个文件类型的日志累加器,输出日志到文件demo2.log log4js.configure({ appenders: { fileAppender: { type: "file", filename: "demo2.log" } }, categories: { default: { appenders: ["fileAppender"], level: "error" } } }); const logger = log4js.getLogger("demo"); //下面调用logger对象来打印一些日志。 logger.info("普通日志输出在这里!!!"); logger.error("雷学委,程序发现错误了,报警信息!"); logger.fatal("雷学委,这里通常是服务端/引擎吃不消了,打印严重错误日志。"); 使用JSON配置log4js把下面的内容保存为log4js.json定义了两个appender:fileAppender, stdout,分别写日志到文件和标准输出流。然后设置了default(默认)加载的appenders列表。{ "appenders": { "fileAppender": { "type": "file", "filename": "leiXueWei.log" }, "stdout": { "type": "stdout", "layout": { "type": "pattern", "pattern": "%d [%p] [%c] - %m%n" }} }, "categories": { "default": { "appenders": ["fileAppender","stdout"], "level": "info" } } } 雷学委的 simpleweb.js 代码const log4js = require("log4js"); const logCfg = require("./log4js.json") console.log('logCfg:',logCfg) log4js.configure(logCfg) const logger = log4js.getLogger('雷学委Logger') const pid = require('process').pid logger.info("process id:",pid) const server = require('http').createServer((req,res)=>{ console.log(new Date() + ' - visiting app:', req.url) logger.log('visiting app:', req.url) res.write("Levin - Log4js DEMO - ProcessId: "+pid) res.end() }) server.listen(8000,()=>{console.log('listening at 8000, pid:',pid)}) 使用log4js打印日志效果图当前项目路径中,可以找到下面日志文件leixuewei.log,我们查看发现console.log的信息都没有被记录。log4js的logger 跟console.log的区别console.log通常是我们用来输出程序中间状态/事件等信息,也能像打印日志一样把日期其他数据都配套上,但是它更加轻便,而且不能写到文件中,打印方式也比较单一。log4js提供logger给我们使用,这样我们可以log4js.getLogger(不同logger的名字),来区别的打印程序中间状态/事件等信息。关键是能够写到文件中,这样方便我们以后回溯程序状态。log数据很有用,必须让程序打印准确的信息比如程序运营一个月后某段时间发现程序渲染页面特别慢。有了文件日志,我们可以轻松的进行统计分析,发现程序一些懒惰的bug,那些随着时间才能被发现的bug。特别是一些多人协作的项目,一个开发可能不知道其他开发代码的逻辑,经过多次包装后,有可能出现一些诡异的循环,或者特定条件才执行的代码(比如根据节假日来展示活动的代码)。很多问题在程序不被运行时,很难被发现,所以通过日志,可以记录并跟踪发现潜在问题,当然前提是,开发者得往程序中输出日志。另外一个,就是不能盲目输出过多日志,访问主页,你打印1G的log数据,不光是占有硬盘还容易导致主页加载不出来。说的稍微夸张,但是品一品。生产环境可用的log4js配置好,刚好端午,附送一个适合生存环境使用的log4js.prod.json配置。 它定义了每天日志回滚,并保持30天的log数据,基本符合生产使用。{ "appenders": { "fileAppender": { "type": "dateFile", "daysToKeep": 30, "pattern": ".yyyyMMdd", "filename": "leixuewei.log", "layout": { "type": "pattern", "pattern": "%d [%p] [%c] - %m%n" } } }, "categories": { "default": { "appenders": ["fileAppender"], "level": "info" } } } 下面是pattern为“.yyyyMMddhhmm’ 是的日志滚动效果,读者自行下载代码修改尝试吧。总结博主给个小建议一定要打log,除非这个应用只有你维护不然出故障就是坑人。可能我们写了100行代码可以打印个2到3行,如果是一些特别复杂的业务可以多打点。同时使用不同级别的日志来打印。这不是硬性标准,关键是反复使用中学会高效的使用日志系统。
为啥公司业务上不去?要么程序没有输出日志到位,缺少价值数据。要么打印日志到位了,没有高效的分析工具!为啥程序会崩溃?日志处理没到位呗,线上故障就得加班,浪费了时间还找不到问题根源!别想为啥了,跟上来学习吧,本篇解决此类问题。什么是日志(log)想想,我们为什么要做笔记,翻看笔记?或者说,每天写日记,我们做了笔记是为了加强记忆,如果忘记了后面还能翻看。日志就是类似笔记一样的东西。小白问:那么日志能帮助开发者提高记忆,轻松学会写代码了?这倒不是的。日志就想日记一样可以随时翻看,查看过去发生的事情。好,下面来看雷学委的开发日常:没错,像记日记一样,用程序记录了每个时刻做的事情,输出到文件,还能经常查看。这就是日志。当我们把这些零散的日志归集起来,数据量大了,就能发现日志数据的规律和价值。本文会使用log4js来进行日志使用的展示。Java开发是使用Log4J或者Slf4J,但是核心思想是一样的。本文适用于任何语言,而且覆盖了单应用到大规模平台的处理。前面已经掌握了基本概念,下面继续看, 日志的简单应用 多种日志格式的使用和含义 实际项目如何管理和配置日志 大规模平台的日志管理与数据价值分析学习的第一步都是先知道啥,怎么用,下面先使用。先安装我们这里先安装一个日志的模快。打开终端执行下面命令:npm install log4js 使用看看雷学委的日记const log4js = require('log4js') var logger = log4js.getLogger('雷学委开发日常') logger.level = 'info' logger.info('早上起床') logger.info('拥抱太阳') logger.info('吃完早餐') logger.info('充满希望') logger.info('开始了代码的一天') 保存上面代码为demo-fun.js ,然后运行:node demo-fun.js可以得到跟上面日志图片的效果,读完可以试试。下面正式开发这里会有三个例子,展示日志打印和日志输出控制。最简单的例子复制下面代码为demo.jsvar log4js = require("log4js"); // 加载log4js库 var logger = log4js.getLogger();//获取默认的logger logger.level = "debug"; logger.debug("一些调试信息"); 直接运行:node demo.js,效果如下:很像console.log,不过多了一些东西,看起来像下面的格式:[日期] [DEBUG] default - 日志消息详情。这里的DEBUG为,一个日志级别,就像文件级别一样(有公开,内部可读,保密,绝密档案)这只是一条日志记录。我们再看下面的,找找规律再看一个例子://demo1.js var log4js = require("log4js"); var logger = log4js.getLogger(); logger.level = "debug"; logger.debug("一些调试信息"); logger.info(" 普通日志"); logger.debug("又是调试信息了"); 复制上面的6行代码保存为demo1.js,运行打印如下信息:这里打印了3行日志,看到规律了吗?每一行都是:[日期] [日志级别] default - 具体的日志内容好,这里稍微做一下解释。logger对象提供了不同方法,像debug,info等表示不同的日志级别!不同的日志级别又代表什么呢?看下面一个稍微复杂的例子,就能明白。避免新人写代码出错,雷学委这里又贴心的为小白/懒人,提供了直接可以复制运行的代码。先花30秒耐心看一下,保存为demo2.js//demo2.js const log4js = require("log4js"); //这里配置log4js使用fileAppender来输出“error”级别的日志。 //然后fileAppender是一个文件类型的日志累加器,输出日志到文件demo2.log log4js.configure({ appenders: { fileAppender: { type: "file", filename: "demo2.log" } }, categories: { default: { appenders: ["fileAppender"], level: "error" } } }); const logger = log4js.getLogger("demo"); //下面调用logger对象来打印一些日志。 logger.trace("一些程序跟踪信息"); logger.debug("这里输出调试信息."); logger.info("普通日志输出在这里!!!"); logger.warn("程序发现可疑问题,打印警告信息"); logger.error("雷学委,程序发现错误了,报警信息!"); logger.fatal("雷学委,这里通常是服务端/引擎吃不消了,打印严重错误日志。"); 看懂了吗?上面的代码分为两个部分第一段加载并配置log4js对象,日志输出保存到哪里,输出的日志级别是什么,这里是‘error’级别第二段为创建logger对象,并用它的不同方法来打印不同级别的日志。运行效果如下:node demo2.js, 如上所说,日志打印到文件demo2.log里面。另外demo2.log内容只有 [ERROR] 和[FATAL]这两个级别的日志,也就是,只有logger.error和logger.fatal两个函数的日志详情被输出到文件中。如果把ERROR比喻为公开级别,那么像DEBUG/TRACE等就是保密级别了,指定了输出公开信息等话,保密数据是绝对不能打印出来的。理解一下日志分级先说级别,不同级别日志会有级别的关键词,像日志中的‘ERROR’, ’DEBUG’ 这些。既然是级别了就有高低级别之分。可以用下面的表达式进行级别低到高排列:TRACE < DEBUG < INFO < WARN < ERROR < FATAL所以这解释了demo2.js代码中指定了‘error‘级别之后, 只有logger.error 和 logger.fatal 两行日志被打印了。看懂了吗?就像日常的信息保护级别,指定级别越高越不能暴露更多信息,在程序中级别指定越高,输出的信息越少,同一个道理。信息量慢慢加大 - 全部日志级别下图是log4js模块提供的方法和日志级别。log4js 提供了getLogger方法,开发者可以调用这个获取一个logger对象,用来输出不同级别日志。然后,这里是最全面的log4js的提供的日志级别。重新整理整个日志级别的排列变更如下:ALL <TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF上面这个公式务必背诵默写!这里读者可自行运行文末实例代码。这里ALL级别相当于整个日志信息都打印,暴露的信息量最多,OFF暴露的信息量是极少的。雷学委也准备了代码,读者稍后可从文末下载执行。到这里还不懂,雷学委精心准备了下图,看到这个图已经值了!至此,整个日志框架的核心,您已掌握了。喝杯水,下面继续看实际应用。实际项目中如何应用日志配置化日志管理上面的demo2的代码配置,应该提取日志模块的配置到文件。配置放在文件中,可以修改配置不改代码切换日志。但是应用需要重启,可以代理logger对象实现不重启动态加载。日志级别的使用建议通常我们打印‘info’级别日志,记得常规用户行为,如登录/查看更新资料/其他业务访问的log等。info级别以下的使用在开发过程中可以使用logger.debug函数去打印程序内部信息,比如打印一些中间变量的值,打印中间处理状态等等。对于trace级别的日志,这个比较低级别,通常用来本地跟踪代码的执行的详细信息!在服务端,服务器上运行的程序通常不会开启,因为日志量会很大。对于all级别很少使用,可以根据情况自己定义,很多时候用到trace级别就很够信息量了。对于info以上的高日志级别warn级别:打印警告信息,就是可能是错误,或者是忽略的已知错误。比如系统在调用一个不稳定的接口,有时候别人不稳定只能多次重试,这时候会使用logger.warn方法来打印日志内容”忽略第三方程序调用错误,准备重试“error级别:顾名思义就是,程序错误,这个可以用来打印错误数据,错误请求的事件信息,比如java中抛出一些CheckedException的时候经常会在catch代码快中调用logger.error来记录此类异常。这类成为错误记录,更多是程序内部处理,业务方面的错误记录。fatal级别更多是在NodeJS引擎出现故障,或者JVM(java虚拟机)这种引擎级别的故障可以用这个级别来打印。针对web应用,像访问底层数据库的失败的时候,也可以用这个fatal级别来记录。出现这种问题一般是本应用的故障,或者因为网络,操作系统的故障导致的程序故障。日志格式化问题如下面的log4js配置一样,加了一个标准输出来展示。这里的patern: “%d [%p] [%c] - %m%n” 就是我们打印日志的格式。%d是一个占位符,最简单可以理解,就是表示一个时间戳,代表打印日志的时间。%p %c这些就是占位符,也就是代表了日志级别,日志对象(特定的logger)等。%m 代表当前如logger.info方法传入的消息。%n 为换行。效果如下:所有格式定义参考:https://github.com/log4js-node/log4js-node/blob/master/docs/layouts.mdlog4js还可以支持将日志写到多个文件中参考:https://github.com/log4js-node/log4js-node/blob/master/docs/multiFile.md重点说一下,日志格式其实不用过度纠结,但必须在输出准确日志信息的情况下,同时保证整个系统或者多个服务的配置达到统一标准。统一标准,比如说格式上的输出标准,代码日志输出标准,路径的标准等等。不然容易失控,这需要在架构上达成一致。日志文件滚动策略开发的应用通常都是长期运行的,会不停的产生日志。随着时间推移,把日志写在一个文件也不现实。而且不利于复查问题,占用大量存储资源,另外,在大文件中搜索关键字速度也降低。所以需要滚动(Rotate/Rolling)日志,也就是把日志设置一个固定规模,比如写日志到 LeixueweiDemoApp.log 文件,设置最大尺寸,当文件超过最大尺寸自动的创建新文件,同时可以设置最大的日志文件备份,比如保留一个月,半年,甚至更长期的日志。这是log4js官网的:JS的日志配置使用了JSON来呈现是非常直观的。Java则更多使用logback或者SLF4J + logback/log4j ,配置的内容很像,但是呈现格式不一样,很多是xml文件或者properties文件。这里可以拿一个XML文件(类似html标签化语义)来看看,如下:<?xml version="1.0" encoding="UTF-8"?> <configuration> <property resource="application.properties" /> <!-- application.name在application.properties中定义 --> <contextName>${application.name}</contextName> <!-- 日志级别 --> <property name="logLevel" value="INFO" /> <!-- 日志文件名 --> <property name="logFileName" value="LeixueweiDemoApp"/> <!-- 保存60天 --> <property name="maxHistory" value="60"/> <!-- application.logHome在application.properties中定义 --> <property name="logPath" value="${application.logHome}/logs/${projectName}"/> <!--格式化输出:%d{yyyy-MM-dd HH:mm:ss.SSS} 表示特定时间格式日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符--> <property name = "LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n"></property> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- 日志记录器,日期滚动记录 --> <appender name="FILEAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--日志文件路径及文件名 --> <file>${logPath}/${logFileName}.log</file> <!-- 日志滚动策略,按日期记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 归档日志文件路径--> <fileNamePattern>${logPath}/${logFileName}.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>${maxHistory}</maxHistory> </rollingPolicy> <!-- 追加记录日志 --> <append>true</append> <!-- 日志文件格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- 记录info及其以上级别 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>info</level> </filter> </appender> <!--适用上面两个appender--> <root level="${logLevel}"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILEAppender"/> </root> </configuration> 上面定义了两个log appender 和 root根logger来配置全局logLevel。其实配置的属性两个技术很相似。到这里已经变得非常复杂了,但是一个完整的应用日志也就是上面的,文末的代码仓库链接有实例配置。读到这里,基本可以解决单个应用的日志管理,而且能够做的成熟专业的应用日志处理了。大规模分布式场景的日志管理与分析高性能的处理这里稍微提一下,在应用中打印日志不宜过多,会影响程序性能。比如访问了一个静态页面,服务器直接打印1G的log出来,这就很离谱。但有时候比如一些重点信息很多,比如在很大请求量的情况下,要保证性能,又想要留住重要的信息跟踪,怎么处理?这时候可以使用日志队列的方式来解决。在使用NodeJS情况下,这里又得提到pm2了,使用集群模式运行,把多个worker节点日志发布到master进行统一化处理,减轻写同个日志或者轮转日志的负担。Java中也有类似的方法,异步处理大量日志,可以查看logback的配置,AsyncAppender。<appender name="ASYNC_LOG_INFO" class="ch.qos.logback.classic.AsyncAppender"> <!-- 设置为:不丢失日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>${queueSize}</queueSize> <appender-ref ref="FILE_INFO"/> </appender> 这个配置的queueSize变量,得跟据应用的实际QPS日志流量来分析,其他参考:http://logback.qos.ch/manual/appenders.html#AsyncAppender统一日志处理与分析上面说的很多直接用来做一个应用的日志处理都可以。统一日志处理说的是把一个大型系统的日志,统一的集中化管理。重点是:分布式日志集中化管理。这一点很重要,在运维大型平台的时候,有几百上千台服务器,上面很多服务,很多日志不可能一个一个登录上去看。所以我们需要使用工具进行集中化,而且做一个大规模的统一分析。思想是把本服务日志传输到一个中央分析主机,使用高效的分析工具,分析数据的规律下面是一个简化的日志集中收集处理的架构,应用服务器多,日志收集终端可以把日志放到消息队列,缓冲处理日志。这一块可以拓展的解决方案很多,是一个开放问题,后面可以写一篇专门Elastic技术棧的。快速过一下elastic stack技术组件:FileBeat: 一个收集日志的程序Logstash: 接收网络端数据,有Pipeline进行日志解构与重组,格式化为结构化数据,方便分析ElasticSearch: 一个高效的存储框架,支持快速数据存储查询Kibana:一个数据可视化工具,支持快速对接elasticsearch的数据,快速的图表面板制作。映射到上面的架构图,我们使用elastic stack的组件流程如下:终端应用服务器日志收集(FileBeat) -> 统一日志处理(Logstash) -> 统一日志存储(ElasticSearch) -> 统一的数据展示面板(Kibana)这里简单提一下,使用这些组件更多是继续配置整合,参考官网配置和文本的代码REPO即可。小团队或者小公司可以使用/开发日志收集器(功能类似logstash+filebeat来传输处理日志),然后存储到elastic search来统一分析,最后使用Kibana做很多分析面板,这里就要求所有应用日志的格式统一,打印足够有效信息。日志数据分析与价值揭示不给有效数据,再怎么分析也是徒劳。数据采集的量也得足够,不能只来一两条,那是很零散的分布。下面来看看Kibanan制作的一些数据分析面板比如监控Nginx 的访问请求情况然后还能细致分析到每一个用户请求的响应情况:这两个图表的数据来源是Nginx组件产生的log(日志)文件,一天的用户请求访问日志的分析。或者像MITRE 公司展示的各种黑客攻击的活动分析面板它是一个数据安全领域的组织,下面面板数据来自:https://attackevals.mitre.org/evaluations.html?round=APT29下面是一周的网络攻击活动统计,有高到低,左边为攻击手段排名,中间为具体技术总如上图,把零散的数据导入作出面板,作出图表显现了数据的规律。上面揭示了各种安全攻击的分布,以及使用的主要技术。这里可以感受到日志数据分析的价值了吧。自研发平台也可以参考Kibana或者Grafana技术棧,实施日志收集分析方案。读者可以去elastic网站看demo深度了解一下。再回顾一下日志处理与分析跟我们写程序思考是一样的,先把思路捋清楚,找去定标准,找合适的工具,还能挖掘出很多业务规律发现价值,创造营收本文讲述了下面这些 日志的概念 日志的作用 日志的简单应用 多种日志格式的使用和含义 实际项目如何管理和配置日志 大平台/高并发场景的日志处理与价值分析
这是一个极简的代码展示,使用koa web服务渲染网站静态页面。系统组件组成前端服务的文件列表:读者可自行准备package.json, 本文使用以下版本:"koa": "^2.13.1", "koa-router": "^10.0.0", "koa-static": "^5.0.0" 直接懒人复制 package.json{ "name": "uiapp", "version": "1.0.0", "description": "uiapp by levin", "main": "app.js", "scripts": { "start": "node app.js" }, "keywords": [], "author": "levin", "dependencies": { "koa": "^2.13.1", "koa-router": "^10.0.0", "koa-static": "^5.0.0" } } app.js 应用入口代码const koa = require('koa'); const serve = require('koa-static'); const path=require('path'); // 启动koa web服务 const app = new koa() // 加载静态页面 var staticPath = path.join(__dirname + '/static'); console.log('static-path:', staticPath) app.use(serve(staticPath)) console.log(new Date()) const PORT = 8080 console.log('start app at port %s', PORT) app.listen(PORT); 思路:启动一个koa 服务,并使用koa-static中间件渲染当前应用下的static文件夹的静态文件,会默认加载index.html首页。好,接下来。开发我们的UI站点。UI站点先写index.html下面代码主要使用了axios和一个getProduct.js。当首页加载的时候,getProduct.js 会获取后台产品服务展示产品。<html> <head> <title>雷学委-UI FrontendSerivce</title> </head> <body> <h1>这是一个前后段分离的应用!</h1> <h2>后端数据展示:"http://localhost:8081"</h2> <div id="result" ></div> </body> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="/getProducts.js" ></script> </html> 接下来是getProducts.js这个内部使用了axios调用了产品服务(第三方服务)的接口(http://localhost:8081/products) ,然后分别针对请求成功和失败把状态写到id为“result” 的div上。调用第三方服务成功显示绿色背景,失败显示红色背景。重点来了:复杂的web 应用中会有很多getProducts这样的JS更后端多个接口进行交互,然后再把交互完的数据更新反馈到UI层,这个不管是Angular/React/Vue都是如此。function $(id){ return document.getElementById(id) } function handleOnData(data){ $('result').innerHTML = '<div style="background:green">' + JSON.stringify(data) + '</div>' } function handleOnError(msg){ $('result').innerHTML = '<div style="background:red">' + JSON.stringify(msg) + '</div>' } // 第三方接口(产品服务接口) // 代码实现:https://blog.csdn.net/geeklevin/article/details/109403172 var api = 'http://localhost:8081/products' console.log('will get products from api:' , api); axios.get(api) .then(function (response) { handleOnData(response.data); }) .catch(function (error) { console.log(error); handleOnError('后端服务已下线!'); }); 后端的产品服务实现代码请复制: NodeJS 后端开发 03 使用Restify开发API 一个完整的CRUD, 改一下端口为8081,并参考该文章启动。效果演示UI启动命令如下,下面两个图左边为UI服务主页,右边为产品服务接口浏览器打开的状态。node app.js #启动UI服务。 启动后台服务(右边已不可访问),左边的UI站点显示产品的JSON数据!UI读者可以自行绘制更漂亮的,请尽情发挥想象。停止后台服务(右边已不可访问),左边的UI站点显示产品后台服务已下线!思想总结前后端分离更多是解耦合大原则指导下在web应用上的呈现。像过去有Swing, Qt或者C#UI,也能应用这个思想。把界面/页面跟后端代码通过使用API(web API)来实现无缝整合。这样还能实现技术上异构(web服务NodeJS,后端用Springboot/PythonDjango等),弹性很大又不会导致架构过度离散。前后端分离是行业的标准做法!看完本文,您学会了吗?不妨试试使用熟悉的技术自己做一次,或者找一两个朋友按着本文分别开发一个服务并整合起来,互相讨论,这样技术进步很明显。希望读者们都完全掌握理解,把握重点。
前面发了一篇多种姿势后台启动进程提到pm2,它就像一个大管家一样,高效管理协调多个微服务,神奇吧。本文会从简单使用,切入到微服务管理。所以下面,我们先试一个web服务来熟悉它。接着,围绕一个极简微服务平台,运用pm2来管理,举一反三从而熟悉甚至掌握微服务管理运维的精髓!文末有好书推荐!安装与使用全局安装npm install -g pm2查看应用程序进程pm2 ls 当前pm2管家没有启动任何服务,所以上面展示了一个空列表。第一个简单web应用效果如下,左边浏览器为web主页,右边为nodejs启动终端上图展示的应用代码如下,可以复制保存为: simpleweb.js直接运行启动命令:node simpleweb.jsconst pid = require('process').pid const server = require('http').createServer((req,res)=>{ console.log(new Date() + ' - visiting app') res.write("Levin - PM2 DEMO - ProcessId: "+pid) res.end() }) server.listen(8000,()=>{console.log('listening at 8000, pid:',pid)}) 以上代码使用NodeJS内置http模块来创建一个服务器监听8000端口,打开浏览器访问能看到进程号。接下来都交给pm2关闭上面进程后使用命令:pm2 start simpleweb.js查看应用进程,日志一旦讲应用交由pm2管理,我们可以用使用下面命令pm2 ls #查看进程 pm2 logs simpleweb #或者pm2 logs 应用序号,pm2 ls返回的第一列 pm2 monit #监控查看当前应用的日志 效果如下,看到simpleweb这个应用了把,pm2帮我们管理了。不过我们这里使用: pm2 start simpleweb.js --watch。后面加了–watch,这个能够监听默认当前工作目录。试着修改文件比如下图中间终端移动文件,pm2 检测到变化重启web应用,左边浏览器web页面打印新的进程id,右边日志也更新了。pm2 保证应用程序进程常驻读者可以试着kill -9 命令杀进程(加上面浏览器显示的进程ID),刷新web页面之后打印了新的进程id。这时候 pm2发现程序挂了,自动恢复了。这里简单带过一下pm2的原理我们看到pm2命令被执行后,系统中多了一个守护进程“PM2 v4.5.6: God Daemon", 下图。它维护了一些进程信息在当前用户主目录下 .pm2目录。当我们把这个进程杀掉之后,连带被pm2使用的服务都被停掉了。必须启动pm2 daemon之后再restart感受到pm2的魅力了吧,其他命令可使用pm2 -h查看。这里先使用pm2 delete simpleweb,清理一下应用。下面进入多个服务管理场景,稍微复杂一点,加一点耐心看完下去就能学会一个微服务并掌握它的运维管理。管理一个迷你的微服务平台简单带过一下微服务把人体看作一个微服务平台,那眼睛就是一个专门接入图像数据的微服务,大脑就是一个执行数据计算的微服务,然后有些还是成对出现的(双热实例)。商品展示微服务平台上图有两个微服务LevinUIApp前台服务:用来展示产品库存数据。BackendApp后端产品服务:提供产品库存数据 给前台服务但是有两个worker子进程。启动UI服务#这里加上--name指定了应用名为uiapp pm2 start ui-app/app.js --name uiapp 启动后端进程后端服务启动两个worker线程(-i) :#雷学委demo代码,指定进程名字为levinbapp并启动两个worker线程,用更多CPU来提高效率。 pm2 start backend-app/app.js --name levinbapp -i 2 效果如下,目前有两个应用进程了。这里通过-i轻易的横向拓展更多个worker。 想要玩更多的服务,读者可以看完学会后自行尝试。看效果并微服务日志:#雷学委demo代码 pm2 monit 运行上面代码,同时打开两浏览器页面。刷新前端应用(最左边窗口),会调用后端接口(中间的窗口),monit控制台切换服务查看日志。如果刷新后端接口,monit控制台实时打印日志。使用下面命令杀掉中间的后端服务, 一次刷新最左边uiapp应用,和中间的后端服务窗口。显示服务下线了。而且在最右边monit窗口,能方便实时的切换服务日志,和查看服务状态,这是很不错的。用起来还是很简单的,pm2管家覆盖支持了程序查日志,起停服务恢复等等。那么下面的配置化管理,让我们更加简单专业的管理微服务平台。配置化管理启动/停止服务群使用 pm2 ecosystem 生成类似下面的ecosystem.config.js文件,这个可以启动的。读者可以不使用博主的代码,自己做一个前台app.js和后台app.js试试看。module.exports = { apps : [{ name: 'uiapp', script: './ui-app/app.js', watch: ['./ui-app'] }, { name: 'levinbapp', script: './backend-app/app.js', watch: ['./backend-app'] }], deploy : {//ssh方式把微服务整个附属到生产服务器! production : { user : 'root', host : 'alicloudtx',//阿里云主机 ref : 'origin/master', repo : 'GIT_REPOSITORY', path : '/demo/levin-demo-msa', 'pre-deploy-local': '', 'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production', 'pre-setup': '' } } }; 简单介绍一下:apps属性配置了需要pm2管理的服务群。然后deploy属性支持用户使用pm2直接把多个服务群部署到阿里云。然后使用: pm2 start ecosystem.config.js部署到云上,这里不展示了,只要配置好ssh,即可部署到任意云平台。总结微服务平台,总是表现为多个服务多个机器分布式运行,资源和算力拓展了,管理等复杂度提升了。可能两个服务登录不同机器,查看日志还容易,当服务到达成百上千的时候问题就很明显。所以,我们需要(孵化)像pm2这样的工具,提供下面的便利:开发和运维上的便利无缝接入服务管理这个对NodeJS应用来说,几乎是神器,pm2 原生地支持了应用程序管理,提供了命令管理查看用户应用。举Springboot开发的微服务平台为例,开发应用的同学需要引入SpringCloud等组件进行服务发现,注册到注册中心。本人也使用过春天全家桶来制作微服务平台,再简化还得定制一个通用SpringBoot Starter,理念也是类似的!统一管理日志工作台特别是在微服务环境下,多个服务,使用pm2 monit,可以很方便的一个monit工作台切换微服务日志。大型平台那就需要做日志搜索了,比较成百上千个服务在pm2 monit窗口切换也不现实,这也是pm2缺少的地方!不过,pm2还有一个在线版的Keymetics 做专业微服务平台监控管理的工作台(收费)。更容易的应用弹性伸缩上面启动后台服务的时候,加了一个-i参数,指定数量就能启动多worker服务。虽然在NodeJS中还是单进程多线程,但是这个参数化实例拓展,这个设计是很有指导意义的!服务启动/恢复操作的封装,原子性我们使用pm2 start/stop appname即可,而非开发进入多个应用目录手动打node app.js。再者pm2会常驻应用保证应用不掉线,这个设计也是值得参考。比如下面的命令:#记得加 -i 2否则启动一个worker单线程处理ui的请求。 pm2 start levinbapp -i 2 整体编排和部署配置化管理一个配置管理整个微服务的多个服务和实例,还能快速部署到云平台!说到这,读者会不会觉得有些地方跟k8s(kubernetes)有相似的地方,无缝契合,配置化编排,弹性伸缩等等。 在NodeJS应用这一块pm2几乎是轻量接入,不需要像k8s那样去进行很多配置才能用起来。本文主要是通过pm2使用展示了微服务应用的管理运维,指出一个更好的方向,以此引导读者把握并朝着这些角度开发优化微服务平台的运维与管理!看看你周边的微服务可以想想怎么更好更方便的定位日志,服务部署,起停/横向拓展等等?至于书籍想做微服务的务必把这本书《微服务设计》看完。这对架构实施和运营微服务很有帮助,算是微服务领域内比较经典的书籍,看了不下两遍!还有参考链接中的MicroServicePattern 网站,上面有很多例子。题外话,对NodeJS比较熟的同学可以去阅读一下它的源码,像服务常驻/热加载更新/无缝接入管理,这些优秀的实现值得挖一挖,可以开发出优秀的运维监控平台。代码和参考链接上文的微服务代码: https://blog.csdn.net/geeklevin/article/details/117458297?spm=1001.2014.3001.5501pm2官网: https://pm2.keymetrics.io/docs/usage/quick-start/书籍:https://book.douban.com/subject/26772677/微服务网站: https://microservices.io/patterns/microservices.html
前言先说一下上一篇NodeJS文章NodeJS 后端开发 06 连接MySQL,这一篇展示了一个数据库链接的db.js 工具库。该工具库提供了一个runSql函数,它的运行方式是先提交SQL,然后再异步把查询结果回调传递给callback函数的。这个函数只适用于提前加载数据的情况,比如预先缓存批量的查库结果。然后用户请求WebAPI的时候,直接读查询缓存。这样会有下面的问题:新数据入库了,本地缓存需要更新。而且无法支持动态查询。说这么多,直接用Koa做个接口演示一下查询效果吧。代码展示,KOA做接口查询数据库const koa = require('Koa'); const app = new koa() var router = require('koa-router')(); var db = require('./db-lite'); //做个简单接口 router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'}); //做个接口查询数据库 router.get('/query', function(ctx,next){ db.runSql("SELECT * FROM COMPANY; ", function(result,fields){ console.log('查询结果:', result); ctx.body = JSON.stringify(result); console.log('设置响应body成功!'); }); console.log('processing'); }); //启动服务器 app.use(router.routes()); const PORT = 8081 console.log('start app at port %s', PORT) app.listen(PORT); 先查看简单接口展示效果:再看看/query接口日志看到设置了响应体ctx.body成功了,可这时候请求已经结束了。因为db.runSql函数不是同步执行了,/query这个接口执行这个函数,碰到函数结束了,当前请求处理完毕,没有设置请求体,所以响应Not Found。看看Promise吧var data = new Promise(function(resolve, reject){ asynFunction(function(error, result){ if(error){ reject(error); }else{ resolve(result); } }); }); async getData(){ var result = await data(); console.log('result is:', result); } 就两段代码,第一段是构造Promise对象;第二段为调用Promise对象处理异步请求。先说Promise。data是一个Promise对象,它内部调用了一个异步函数asynFunction(这个函数可以是查库,请求其他网站接口,消息队列获取数据等),但是它能够保证两个结果:处理异步请求成功的时候调用resolve函数,传递result对象;出错的时候调用reject函数,传递error对象。就这么简单。第二段代码就是一个声明了异步响应的函数,虽然内部调用的Promise对象,又套娃调用了异步函数,但是await语法糖,能够保证result变量赋值这一行同步执行,也就是getData会等待result赋值这一行异步调用执行完毕,再执行console.log。这里稍微有点绕了,请读者停下来,好好思考整理一下。简单来说就是,在async声明的异步函数中,同步等待Promise内部执行结果,就是,同步等待异步请求。这样,异步转同步,很神奇吧!像查询数据库这样的异步操作就能够在API中被等待,并响应给用户了,下面来代码了。好,用koa接口得怎么做呢?需要在数据库调用端怎么调整呢?下面是runSql的简化实现代码,我们要改造它const runSql = function(sql, callback){ console.log('will run sql:', sql); //这里是异步调用的,但是不是Promise实现,没有办法使用asyn,await来实现同步等待异步请求。 db.query(sql, function(error, result, fields){ try{ if(error) throw error; console.log('query result:', result); }finally{ if(callback) callback(result, fields); } }); }
前言先说一下上一篇NodeJS文章NodeJS 后端开发 06 连接MySQL,这一篇展示了一个数据库链接的db.js 工具库。该工具库提供了一个runSql函数,它的运行方式是先提交SQL,然后再异步把查询结果回调传递给callback函数的。这个函数只适用于提前加载数据的情况,比如预先缓存批量的查库结果。然后用户请求WebAPI的时候,直接读查询缓存。这样会有下面的问题:新数据入库了,本地缓存需要更新。而且无法支持动态查询。说这么多,直接用Koa做个接口演示一下查询效果吧。代码展示,KOA做接口查询数据库const koa = require('Koa'); const app = new koa() var router = require('koa-router')(); var db = require('./db-lite'); //做个简单接口 router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'}); //做个接口查询数据库 router.get('/query', function(ctx,next){ db.runSql("SELECT * FROM COMPANY; ", function(result,fields){ console.log('查询结果:', result); ctx.body = JSON.stringify(result); console.log('设置响应body成功!'); }); console.log('processing'); }); //启动服务器 app.use(router.routes()); const PORT = 8081 console.log('start app at port %s', PORT) app.listen(PORT); 先查看简单接口展示效果:再看看/query接口日志看到设置了响应体ctx.body成功了,可这时候请求已经结束了。因为db.runSql函数不是同步执行了,/query这个接口执行这个函数,碰到函数结束了,当前请求处理完毕,没有设置请求体,所以响应Not Found。看看Promise吧var data = new Promise(function(resolve, reject){ asynFunction(function(error, result){ if(error){ reject(error); }else{ resolve(result); } }); }); async getData(){ var result = await data(); console.log('result is:', result); } 就两段代码,第一段是构造Promise对象;第二段为调用Promise对象处理异步请求。先说Promise。data是一个Promise对象,它内部调用了一个异步函数asynFunction(这个函数可以是查库,请求其他网站接口,消息队列获取数据等),但是它能够保证两个结果:处理异步请求成功的时候调用resolve函数,传递result对象;出错的时候调用reject函数,传递error对象。就这么简单。第二段代码就是一个声明了异步响应的函数,虽然内部调用的Promise对象,又套娃调用了异步函数,但是await语法糖,能够保证result变量赋值这一行同步执行,也就是getData会等待result赋值这一行异步调用执行完毕,再执行console.log。这里稍微有点绕了,请读者停下来,好好思考整理一下。简单来说就是,在async声明的异步函数中,同步等待Promise内部执行结果,就是,同步等待异步请求。这样,异步转同步,很神奇吧!像查询数据库这样的异步操作就能够在API中被等待,并响应给用户了,下面来代码了。好,用koa接口得怎么做呢?需要在数据库调用端怎么调整呢?下面是runSql的简化实现代码,我们要改造它const runSql = function(sql, callback){ console.log('will run sql:', sql); //这里是异步调用的,但是不是Promise实现,没有办法使用asyn,await来实现同步等待异步请求。 db.query(sql, function(error, result, fields){ try{ if(error) throw error; console.log('query result:', result); }finally{ if(callback) callback(result, fields); } }); } 先看koa怎么搞?这个好办, 下面是一个简单的router(路由web API)。我们看到在router.get 第二个参数加了async关键字修饰的方法。根据ES6,我们知道async函数内可以使用await强制等待异步Promise函数的相应结果。很明显,上面的runSql函数返回不是一个Promise对象的,所以这里先调用一个未实现的函数:prunSql。router.get('/asynQuery', async function(ctx, next){ //参考上面Promise使用,声明同步等待一个返回Promise对象的函数。 var data = await prunSql('SELECT * FROM COMPANY;'); ctx.body = JSON.stringify(data) }); 这是一个新的接口, 跟其他接口不一样的就是,它处理请求的方法是用async声明的异步函数。我们需要改造runSql函数const prunSql = function(sql){ console.log('will run sql:', sql); return new Promise(function(resolve, reject){ console.log('asyn run sql: ', sql); db.query(sql, function(error, result, fields){ if(error){ console.log('[%s] asyn error:', error); reject(error); }else{ console.log('asyn result:', result); resolve(result); } }); }); }; 这就是改造好的prunSql,这里用了resolve函数把数据库查询结果传递出去。这样在使用Promise的then可以获取到异步查询结果。或者在一个异步响应的方法内await Promise对象,等待查询返回值。
前言先说一下上一篇NodeJS文章NodeJS 后端开发 06 连接MySQL,这一篇展示了一个数据库链接的db.js 工具库。该工具库提供了一个runSql函数,它的运行方式是先提交SQL,然后再异步把查询结果回调传递给callback函数的。这个函数只适用于提前加载数据的情况,比如预先缓存批量的查库结果。然后用户请求WebAPI的时候,直接读查询缓存。这样会有下面的问题:新数据入库了,本地缓存需要更新。而且无法支持动态查询。说这么多,直接用Koa做个接口演示一下查询效果吧。代码展示,KOA做接口查询数据库const koa = require('Koa'); const app = new koa() var router = require('koa-router')(); var db = require('./db-lite'); //做个简单接口 router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'}); //做个接口查询数据库 router.get('/query', function(ctx,next){ db.runSql("SELECT * FROM COMPANY; ", function(result,fields){ console.log('查询结果:', result); ctx.body = JSON.stringify(result); console.log('设置响应body成功!'); }); console.log('processing'); }); //启动服务器 app.use(router.routes()); const PORT = 8081 console.log('start app at port %s', PORT) app.listen(PORT); 先查看简单接口展示效果:再看看/query接口日志看到设置了响应体ctx.body成功了,可这时候请求已经结束了。因为db.runSql函数不是同步执行了,/query这个接口执行这个函数,碰到函数结束了,当前请求处理完毕,没有设置请求体,所以响应Not Found。看看Promise吧var data = new Promise(function(resolve, reject){ asynFunction(function(error, result){ if(error){ reject(error); }else{ resolve(result); } }); }); async getData(){ var result = await data(); console.log('result is:', result); } 就两段代码,第一段是构造Promise对象;第二段为调用Promise对象处理异步请求。先说Promise。data是一个Promise对象,它内部调用了一个异步函数asynFunction(这个函数可以是查库,请求其他网站接口,消息队列获取数据等),但是它能够保证两个结果:处理异步请求成功的时候调用resolve函数,传递result对象;出错的时候调用reject函数,传递error对象。就这么简单。第二段代码就是一个声明了异步响应的函数,虽然内部调用的Promise对象,又套娃调用了异步函数,但是await语法糖,能够保证result变量赋值这一行同步执行,也就是getData会等待result赋值这一行异步调用执行完毕,再执行console.log。这里稍微有点绕了,请读者停下来,好好思考整理一下。简单来说就是,在async声明的异步函数中,同步等待Promise内部执行结果,就是,同步等待异步请求。这样,异步转同步,很神奇吧!像查询数据库这样的异步操作就能够在API中被等待,并响应给用户了,下面来代码了。好,用koa接口得怎么做呢?需要在数据库调用端怎么调整呢?下面是runSql的简化实现代码,我们要改造它const runSql = function(sql, callback){ console.log('will run sql:', sql); //这里是异步调用的,但是不是Promise实现,没有办法使用asyn,await来实现同步等待异步请求。 db.query(sql, function(error, result, fields){ try{ if(error) throw error; console.log('query result:', result); }finally{ if(callback) callback(result, fields); } }); } 先看koa怎么搞?这个好办, 下面是一个简单的router(路由web API)。我们看到在router.get 第二个参数加了async关键字修饰的方法。根据ES6,我们知道async函数内可以使用await强制等待异步Promise函数的相应结果。很明显,上面的runSql函数返回不是一个Promise对象的,所以这里先调用一个未实现的函数:prunSql。router.get('/asynQuery', async function(ctx, next){ //参考上面Promise使用,声明同步等待一个返回Promise对象的函数。 var data = await prunSql('SELECT * FROM COMPANY;'); ctx.body = JSON.stringify(data) }); 这是一个新的接口, 跟其他接口不一样的就是,它处理请求的方法是用async声明的异步函数。我们需要改造runSql函数const prunSql = function(sql){ console.log('will run sql:', sql); return new Promise(function(resolve, reject){ console.log('asyn run sql: ', sql); db.query(sql, function(error, result, fields){ if(error){ console.log('[%s] asyn error:', error); reject(error); }else{ console.log('asyn result:', result); resolve(result); } }); }); }; 这就是改造好的prunSql,这里用了resolve函数把数据库查询结果传递出去。这样在使用Promise的then可以获取到异步查询结果。或者在一个异步响应的方法内await Promise对象,等待查询返回值。看看使用Promise处理后的效果:问题至此,迎刃而解!具体用什么框架做接口不重要,理解整个思想才是重点。贴一下代码, 下面的数据库查询库(db-lite.js),读者可自行更换成其他异步查询的库,进行改造,不必过多纠结一定要使用本文的runSql函数。const koa = require('Koa'); const app = new koa() var router = require('koa-router')(); var db = require('./db-lite'); router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'}); router.get('/query', function(ctx,next){ db.runSql("SELECT * FROM COMPANY; ", function(result,fields){ console.log('查询结果:', result); ctx.body = JSON.stringify(result); console.log('设置响应body成功!'); }); console.log('processing'); }); router.get('/asynQuery', async function(ctx, next){ var data = await db.prunSql('SELECT * FROM COMPANY;'); ctx.body = JSON.stringify(data) }); app.use(router.routes()); const PORT = 8081 console.log('start app at port %s', PORT) app.listen(PORT); db-lite.js 代码:const mysql = require('mysql'); const db = mysql.createConnection({ host: "localhost", port: 3306, user: "root", password: "12345678" }); const logDbStat = function(){ console.log("db state %s and threadID %s", db.state, db.threadId); // console.log("db detail:", db); } logDbStat(); console.log('start to connect mysql'); db.connect(function(err){ if(err){ console.log('fail to connect db',err.stack); throw err; } logDbStat(); }); const close = function(){ db.destroy(); console.log('db disconnected'); logDbStat(); }; var counter = 0; const runSql = function(sql, callback){ counter++; var cv = counter; console.log('run sql[%s] :[%s]',cv, sql); db.query(sql, function(error, result, fields){ try{ if(error) throw error; }finally{ if(callback) callback(result, fields); } }); } const prunSql = function(sql){ console.log('will sql:', sql); return new Promise(function(resolve, reject){ counter++; var cv = counter; console.log('asyn run sql[%s] :[%s]', cv, sql); db.query(sql, function(error, result, fields){ if(error){ console.log('[%s] asyn error:', cv, error); reject(error); }else{ // console.log('[%s] asyn result:', cv, result); resolve(result); } }); }); }; const myDb = 'demo20210330'; runSql("USE " + myDb); module.exports.runSql = runSql; module.exports.prunSql = prunSql; 总结Promise结合async和await结合能够产生神奇的效果,也是很多新手过不去的坎,希望读者都能够熟练运用。上面展示代码,读者可以自行思考,改造工作中使用的异步请求转化为同步调用。篇幅有限,当然这本质上还是异步执行的,希望读者自行鞭策,思考理解。
Ambari是ASF(Apache Software Foundation)中的一个项目,并且是一个顶级项目,致力于让Hadoop集群管理更加简单。它开发了丰富的RESTful APIs,以及一套直观易用的WebUI管理界面。大数据集群除了我们常见的Hadoop,集群内还有Hive,Hbase,Sqoop,Zookeeper等。因为大数据这个坑里,组件特别的多,一个一个安装配置很麻烦,所以社区孵化了Ambari。就 Ambari的作用来说,就是创建、管理、监视大数据集群,让组件更容易整合进来。它主要是由Cloudera公司参与贡献开发的(可以查看https://ambari.apache.org/team-list.html)。功能它提供了:provision 必要组件供配(安装)的功能一步接一步的安装面板进行组件安装帮助大数据运维工程师管理集群上所有服务的配置,配置是版本化的。managing 管理集群上的组件启动关闭更新配置monitoring 监控集群上的组件提供了一个Dashboard(面板)监控并可用来展示集群监控和集群状态运用了AMS(Ambari Metrics System)进行监控指标收集运用了AAF(Ambari Alert Framework)进行系统告警,同时通知运维人员集群状态。使用目前主要是Cloudera公司使用了Ambari打包封装提供了一些企业版本的产品,如HDP(Hortonworks Data Platform),CDP(Cloudera Data Platform),CDF(Cloudera DataFlo)。CDF前身为HDF(Hortonworks DataFlow)。HDP vs HDFHDP可以简单理解为一个HadoopCluster Platform,就是一个大数据的存储和计算平台,关注在HDFS, Yarn以及一些计算引擎的(比如Spark/Tez)管理。HDF(DataFlow)这个包含Nifi组件(Nifi是一个数据迁移搬运的流式处理框架),更多关注点在于如何把大量的,多种格式的数据,以高效可控的方式导入到大数据存储层。所以,我们经常会看HDP + HDF 组合在一起的集成大数据平台。后续学委会展示一个使用Ambari搭建的集群,可以先关注,第一时间查看后续更新
start all没有报错,但是发现这NameNode的webUI上面DataNode没有挂上。进入DataNode查看日志发现下面问题。datanode 进程没有起来NodeManager启动过一段时间退出了。错误 java.io.IOException: No services to connect, missing NameNode address.2021-05-15 16:31:40,824 WARN org.apache.hadoop.hdfs.server.datanode.DataNode: Unable to get NameNode addresses.2021-05-15 16:31:40,907 INFO org.eclipse.jetty.server.handler.ContextHandler: Stopped o.e.j.w.WebAppContext@33308786{/,null,UNAVAILABLE}{/datanode}2021-05-15 16:31:40,921 INFO org.eclipse.jetty.server.AbstractConnector: Stopped ServerConnector@4b6e2263{HTTP/1.1,[http/1.1]}{localhost:0}2021-05-15 16:31:40,921 INFO org.eclipse.jetty.server.handler.ContextHandler: Stopped o.e.j.s.ServletContextHandler@46b61c56{/static,file:///usr/local/hadoop/hadoop-3.2.1/share/hadoop/hdfs/webapps/static/,UNAVAILABLE}2021-05-15 16:31:40,922 INFO org.eclipse.jetty.server.handler.ContextHandler: Stopped o.e.j.s.ServletContextHandler@36060e{/logs,file:///usr/local/hadoop/hadoop-3.2.1/logs/,UNAVAILABLE}2021-05-15 16:31:40,950 INFO org.apache.hadoop.ipc.Server: Stopping server on 98672021-05-15 16:31:40,951 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: Stopping DataNode metrics system…2021-05-15 16:31:40,951 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: DataNode metrics system stopped.2021-05-15 16:31:40,952 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: DataNode metrics system shutdown complete.2021-05-15 16:31:40,963 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: Shutdown complete.2021-05-15 16:31:40,964 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Exception in secureMainjava.io.IOException: No services to connect, missing NameNode address.at org.apache.hadoop.hdfs.server.datanode.BlockPoolManager.refreshNamenodes(BlockPoolManager.java:165)at org.apache.hadoop.hdfs.server.datanode.DataNode.startDataNode(DataNode.java:1441)at org.apache.hadoop.hdfs.server.datanode.DataNode.(DataNode.java:501)at org.apache.hadoop.hdfs.server.datanode.DataNode.makeInstance(DataNode.java:2806)at org.apache.hadoop.hdfs.server.datanode.DataNode.instantiateDataNode(DataNode.java:2714)at org.apache.hadoop.hdfs.server.datanode.DataNode.createDataNode(DataNode.java:2756)at org.apache.hadoop.hdfs.server.datanode.DataNode.secureMain(DataNode.java:2900)at org.apache.hadoop.hdfs.server.datanode.DataNode.main(DataNode.java:2924)2021-05-15 16:31:40,967 INFO org.apache.hadoop.util.ExitUtil: Exiting with status 1: java.io.IOException: No services to connect, missing NameNode address.2021-05-15 16:31:40,988 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: SHUTDOWN_MSG:*这个错误前面还有一个警告日志:Unable to get NameNode addressesNodeManager 过了一会也挂了详细错误棧2021-05-15 16:47:25,535 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 9 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:47:49,580 INFO org.apache.hadoop.yarn.server.nodemanager.containermanager.localizer.ResourceLocalizationService: Cache Size Before Clean: 0, Total Deleted: 0, Public Deleted: 0, Private Deleted: 02021-05-15 16:47:56,541 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 0 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:47:57,542 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 1 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:47:58,546 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 2 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:47:59,547 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 3 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:00,549 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 4 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:01,552 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 5 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:02,556 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 6 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:03,558 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 7 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:04,559 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 8 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:05,563 INFO org.apache.hadoop.ipc.Client: Retrying connect to server: 0.0.0.0/0.0.0.0:8031. Already tried 9 time(s); retry policyis RetryUpToMaximumCountWithFixedSleep(maxRetries=10, sleepTime=1000 MILLISECONDS)2021-05-15 16:48:05,564 ERROR org.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl: Unexpected error starting NodeStatusUpdaterjava.net.ConnectException: Your endpoint configuration is wrong; For more details see: http://wiki.apache.org/hadoop/UnsetHostnameOrPortat sun.reflect.GeneratedConstructorAccessor21.newInstance(Unknown Source)at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)at java.lang.reflect.Constructor.newInstance(Constructor.java:423)at org.apache.hadoop.net.NetUtils.wrapWithMessage(NetUtils.java:833)at org.apache.hadoop.net.NetUtils.wrapException(NetUtils.java:753)at org.apache.hadoop.ipc.Client.getRpcResponse(Client.java:1549)at org.apache.hadoop.ipc.Client.call(Client.java:1491)at org.apache.hadoop.ipc.Client.call(Client.java:1388)at org.apache.hadoop.ipc.ProtobufRpcEngineI n v o k e r . i n v o k e ( P r o t o b u f R p c E n g i n e . j a v a : 233 ) a t o r g . a p a c h e . h a d o o p . i p c . P r o t o b u f R p c E n g i n e Invoker.invoke(ProtobufRpcEngine.java:233) at org.apache.hadoop.ipc.ProtobufRpcEngineInvoker.invoke(ProtobufRpcEngine.java:233)atorg.apache.hadoop.ipc.ProtobufRpcEngineInvoker.invoke(ProtobufRpcEngine.java:118)at com.sun.proxy.P r o x y 73. r e g i s t e r N o d e M a n a g e r ( U n k n o w n S o u r c e ) a t o r g . a p a c h e . h a d o o p . y a r n . s e r v e r . a p i . i m p l . p b . c l i e n t . R e s o u r c e T r a c k e r P B C l i e n t I m p l . r e g i s t e r N o d e M a n a g e r ( R e s o u r c e T r a c k e r P B C l i e n t I m p l . j a v a : 73 ) a t s u n . r e f l e c t . G e n e r a t e d M e t h o d A c c e s s o r 11. i n v o k e ( U n k n o w n S o u r c e ) a t s u n . r e f l e c t . D e l e g a t i n g M e t h o d A c c e s s o r I m p l . i n v o k e ( D e l e g a t i n g M e t h o d A c c e s s o r I m p l . j a v a : 43 ) a t j a v a . l a n g . r e f l e c t . M e t h o d . i n v o k e ( M e t h o d . j a v a : 498 ) a t o r g . a p a c h e . h a d o o p . i o . r e t r y . R e t r y I n v o c a t i o n H a n d l e r . i n v o k e M e t h o d ( R e t r y I n v o c a t i o n H a n d l e r . j a v a : 422 ) a t o r g . a p a c h e . h a d o o p . i o . r e t r y . R e t r y I n v o c a t i o n H a n d l e r Proxy73.registerNodeManager(Unknown Source) at org.apache.hadoop.yarn.server.api.impl.pb.client.ResourceTrackerPBClientImpl.registerNodeManager(ResourceTrackerPBClientImpl.java:73) at sun.reflect.GeneratedMethodAccessor11.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:422) at org.apache.hadoop.io.retry.RetryInvocationHandlerProxy73.registerNodeManager(UnknownSource)atorg.apache.hadoop.yarn.server.api.impl.pb.client.ResourceTrackerPBClientImpl.registerNodeManager(ResourceTrackerPBClientImpl.java:73)atsun.reflect.GeneratedMethodAccessor11.invoke(UnknownSource)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)atjava.lang.reflect.Method.invoke(Method.java:498)atorg.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:422)atorg.apache.hadoop.io.retry.RetryInvocationHandlerCall.invokeMethod(RetryInvocationHandler.java:165):2021-05-15 16:48:05,602 INFO org.apache.hadoop.yarn.server.nodemanager.containermanager.localizer.ResourceLocalizationService: Public cache exiting2021-05-15 16:48:05,602 WARN org.apache.hadoop.yarn.server.nodemanager.NodeResourceMonitorImpl: org.apache.hadoop.yarn.server.nodemanager.NodeResourceMonitorImpl is interrupted. Exiting.2021-05-15 16:48:05,609 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: Stopping NodeManager metrics system…2021-05-15 16:48:05,610 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: NodeManager metrics system stopped.2021-05-15 16:48:05,610 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: NodeManager metrics system shutdown complete.2021-05-15 16:48:05,610 ERROR org.apache.hadoop.yarn.server.nodemanager.NodeManager: Error starting NodeManagerorg.apache.hadoop.yarn.exceptions.YarnRuntimeException: java.net.ConnectException: Your endpoint configuration is wrong; For more details see: http://wiki.apache.org/hadoop/UnsetHostnameOrPortat org.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl.serviceStart(NodeStatusUpdaterImpl.java:278)at org.apache.hadoop.service.AbstractService.start(AbstractService.java:194)at org.apache.hadoop.service.CompositeService.serviceStart(CompositeService.java:121)at org.apache.hadoop.service.AbstractService.start(AbstractService.java:194)at org.apache.hadoop.yarn.server.nodemanager.NodeManager.initAndStartNodeManager(NodeManager.java:975)at org.apache.hadoop.yarn.server.nodemanager.NodeManager.main(NodeManager.java:1054)Caused by: java.net.ConnectException: Your endpoint configuration is wrong; For more details see: http://wiki.apache.org/hadoop/UnsetHostnameOrPortat sun.reflect.GeneratedConstructorAccessor21.newInstance(Unknown Source)at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)at java.lang.reflect.Constructor.newInstance(Constructor.java:423)at org.apache.hadoop.net.NetUtils.wrapWithMessage(NetUtils.java:833)at org.apache.hadoop.net.NetUtils.wrapException(NetUtils.java:753)at org.apache.hadoop.ipc.Client.getRpcResponse(Client.java:1549)at org.apache.hadoop.ipc.Client.call(Client.java:1491)at org.apache.hadoop.ipc.Client.call(Client.java:1388)at org.apache.hadoop.ipc.ProtobufRpcEngineI n v o k e r . i n v o k e ( P r o t o b u f R p c E n g i n e . j a v a : 233 ) a t o r g . a p a c h e . h a d o o p . i p c . P r o t o b u f R p c E n g i n e Invoker.invoke(ProtobufRpcEngine.java:233) at org.apache.hadoop.ipc.ProtobufRpcEngineInvoker.invoke(ProtobufRpcEngine.java:233)atorg.apache.hadoop.ipc.ProtobufRpcEngineInvoker.invoke(ProtobufRpcEngine.java:118)at com.sun.proxy.P r o x y 73. r e g i s t e r N o d e M a n a g e r ( U n k n o w n S o u r c e ) a t o r g . a p a c h e . h a d o o p . y a r n . s e r v e r . a p i . i m p l . p b . c l i e n t . R e s o u r c e T r a c k e r P B C l i e n t I m p l . r e g i s t e r N o d e M a n a g e r ( R e s o u r c e T r a c k e r P B C l i e n t I m p l . j a v a : 73 ) a t s u n . r e f l e c t . G e n e r a t e d M e t h o d A c c e s s o r 11. i n v o k e ( U n k n o w n S o u r c e ) a t s u n . r e f l e c t . D e l e g a t i n g M e t h o d A c c e s s o r I m p l . i n v o k e ( D e l e g a t i n g M e t h o d A c c e s s o r I m p l . j a v a : 43 ) a t j a v a . l a n g . r e f l e c t . M e t h o d . i n v o k e ( M e t h o d . j a v a : 498 ) a t o r g . a p a c h e . h a d o o p . i o . r e t r y . R e t r y I n v o c a t i o n H a n d l e r . i n v o k e M e t h o d ( R e t r y I n v o c a t i o n H a n d l e r . j a v a : 422 ) a t o r g . a p a c h e . h a d o o p . i o . r e t r y . R e t r y I n v o c a t i o n H a n d l e r Proxy73.registerNodeManager(Unknown Source) at org.apache.hadoop.yarn.server.api.impl.pb.client.ResourceTrackerPBClientImpl.registerNodeManager(ResourceTrackerPBClientImpl.java:73) at sun.reflect.GeneratedMethodAccessor11.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:422) at org.apache.hadoop.io.retry.RetryInvocationHandlerProxy73.registerNodeManager(UnknownSource)atorg.apache.hadoop.yarn.server.api.impl.pb.client.ResourceTrackerPBClientImpl.registerNodeManager(ResourceTrackerPBClientImpl.java:73)atsun.reflect.GeneratedMethodAccessor11.invoke(UnknownSource)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)atjava.lang.reflect.Method.invoke(Method.java:498)atorg.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:422)atorg.apache.hadoop.io.retry.RetryInvocationHandlerCall.invokeMethod(RetryInvocationHandler.java:165)at org.apache.hadoop.io.retry.RetryInvocationHandlerC a l l . i n v o k e ( R e t r y I n v o c a t i o n H a n d l e r . j a v a : 157 ) a t o r g . a p a c h e . h a d o o p . i o . r e t r y . R e t r y I n v o c a t i o n H a n d l e r Call.invoke(RetryInvocationHandler.java:157) at org.apache.hadoop.io.retry.RetryInvocationHandlerCall.invoke(RetryInvocationHandler.java:157)atorg.apache.hadoop.io.retry.RetryInvocationHandlerCall.invokeOnce(RetryInvocationHandler.java:95)at org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:359)at com.sun.proxy.P r o x y 74. r e g i s t e r N o d e M a n a g e r ( U n k n o w n S o u r c e ) a t o r g . a p a c h e . h a d o o p . y a r n . s e r v e r . n o d e m a n a g e r . N o d e S t a t u s U p d a t e r I m p l . r e g i s t e r W i t h R M ( N o d e S t a t u s U p d a t e r I m p l . j a v a : 416 ) a t o r g . a p a c h e . h a d o o p . y a r n . s e r v e r . n o d e m a n a g e r . N o d e S t a t u s U p d a t e r I m p l . s e r v i c e S t a r t ( N o d e S t a t u s U p d a t e r I m p l . j a v a : 272 ) . . . 5 m o r e C a u s e d b y : j a v a . n e t . C o n n e c t E x c e p t i o n : C o n n e c t i o n r e f u s e d a t s u n . n i o . c h . S o c k e t C h a n n e l I m p l . c h e c k C o n n e c t ( N a t i v e M e t h o d ) a t s u n . n i o . c h . S o c k e t C h a n n e l I m p l . f i n i s h C o n n e c t ( S o c k e t C h a n n e l I m p l . j a v a : 715 ) a t o r g . a p a c h e . h a d o o p . n e t . S o c k e t I O W i t h T i m e o u t . c o n n e c t ( S o c k e t I O W i t h T i m e o u t . j a v a : 206 ) a t o r g . a p a c h e . h a d o o p . n e t . N e t U t i l s . c o n n e c t ( N e t U t i l s . j a v a : 533 ) a t o r g . a p a c h e . h a d o o p . i p c . C l i e n t Proxy74.registerNodeManager(Unknown Source) at org.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl.registerWithRM(NodeStatusUpdaterImpl.java:416) at org.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl.serviceStart(NodeStatusUpdaterImpl.java:272) ... 5 more Caused by: java.net.ConnectException: Connection refused at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:715) at org.apache.hadoop.net.SocketIOWithTimeout.connect(SocketIOWithTimeout.java:206) at org.apache.hadoop.net.NetUtils.connect(NetUtils.java:533) at org.apache.hadoop.ipc.ClientProxy74.registerNodeManager(UnknownSource)atorg.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl.registerWithRM(NodeStatusUpdaterImpl.java:416)atorg.apache.hadoop.yarn.server.nodemanager.NodeStatusUpdaterImpl.serviceStart(NodeStatusUpdaterImpl.java:272)...5moreCausedby:java.net.ConnectException:Connectionrefusedatsun.nio.ch.SocketChannelImpl.checkConnect(NativeMethod)atsun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:715)atorg.apache.hadoop.net.SocketIOWithTimeout.connect(SocketIOWithTimeout.java:206)atorg.apache.hadoop.net.NetUtils.connect(NetUtils.java:533)atorg.apache.hadoop.ipc.ClientConnection.setupConnection(Client.java:700)分析关键信息:org.apache.hadoop.yarn.exceptions.YarnRuntimeException: java.net.ConnectException: Your endpoint configuration is wrong; For more details see: http://wiki.apache.org/hadoop/UnsetHostnameOrPortCaused by: java.net.ConnectException: Connection refused可能是DataNode忘记配置配置常规文件了。core-site.xml - 导致DataNode进程连接不到master 9000端口yarn-site.xml - 导致NodeManager多次重试连接本机8031端口(yarn resourcetracker),没有配置的默认值。这属于两个分开的问题,最后在进行了如下改动。core-site.xml 设置fs.defaultFS指定为集群namenode url。yarn-site.xml 设置yarn.resourcemanager.hostname指定值为resourcemanager的主机,当然也可以直接修改yarn.resourcemanager.resource-tracker.address为RM主机:端口号。重跑一下NodeManager进程(不建议跑stop-all和start-all.sh,会把整个集群重启)。检查查看NameNode WebUI没启动的DataNode重新连接集群了。问题彻底解决。
之前集群能够启动各个节点的。因为最近集群改名了,启动NameNode可以的。启动DataNode没有任何提示。进入hadoop logs目录查找hadoop-root-datanode-bd-cluster1.log文件发现下面的错误。错误关键信息: org.apache.hadoop.hdfs.server.common.Storage: Failed to add storage directory [DISK]file:/tmp/hadoop-root/dfs/datajava.io.IOException: Incompatible clusterIDs in /tmp/hadoop-root/dfs/data: namenode clusterID = CID-17d9d128-d0f2-4b73-9460-0074536fa7a7; datanode clusterID = CID-29c3283b-0f02-4814-ae01-50065e15c0042021-05-15 15:34:23,573 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: Acknowledging ACTIVE Namenode during handshakeBlock pool (Datanode Uuid unassigned) service to /10.231.33.101:90002021-05-15 15:34:23,582 INFO org.apache.hadoop.hdfs.server.common.Storage: Using 1 threads to upgrade data directories (dfs.datanode.parallel.volumes.load.threads.num=1, dataDirs=1)2021-05-15 15:34:23,597 INFO org.apache.hadoop.hdfs.server.common.Storage: Lock on /tmp/hadoop-root/dfs/data/in_use.lock acquired by nodename 5526@bd-cluster12021-05-15 15:34:23,600 WARN org.apache.hadoop.hdfs.server.common.Storage: Failed to add storage directory [DISK]file:/tmp/hadoop-root/dfs/datajava.io.IOException: Incompatible clusterIDs in /tmp/hadoop-root/dfs/data: namenode clusterID = CID-17d9d128-d0f2-4b73-9460-0074536fa7a7; datanode clusterID = CID-29c3283b-0f02-4814-ae01-50065e15c004at org.apache.hadoop.hdfs.server.datanode.DataStorage.doTransition(DataStorage.java:744)at org.apache.hadoop.hdfs.server.datanode.DataStorage.loadStorageDirectory(DataStorage.java:294)at org.apache.hadoop.hdfs.server.datanode.DataStorage.loadDataStorage(DataStorage.java:407)at org.apache.hadoop.hdfs.server.datanode.DataStorage.addStorageLocations(DataStorage.java:387)at org.apache.hadoop.hdfs.server.datanode.DataStorage.recoverTransitionRead(DataStorage.java:559)at org.apache.hadoop.hdfs.server.datanode.DataNode.initStorage(DataNode.java:1743)at org.apache.hadoop.hdfs.server.datanode.DataNode.initBlockPool(DataNode.java:1679)at org.apache.hadoop.hdfs.server.datanode.BPOfferService.verifyAndSetNamespaceInfo(BPOfferService.java:390)at org.apache.hadoop.hdfs.server.datanode.BPServiceActor.connectToNNAndHandshake(BPServiceActor.java:282)at org.apache.hadoop.hdfs.server.datanode.BPServiceActor.run(BPServiceActor.java:824)at java.lang.Thread.run(Thread.java:748)2021-05-15 15:34:23,602 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: Initialization failed for Block pool (Datanode Uuid f4005613-8a0d-4177-a59a-5a999f41ccd6) service to /10.231.33.101:9000. Exiting.java.io.IOException: All specified directories have failed to load.at org.apache.hadoop.hdfs.server.datanode.DataStorage.recoverTransitionRead(DataStorage.java:560)at org.apache.hadoop.hdfs.server.datanode.DataNode.initStorage(DataNode.java:1743)at org.apache.hadoop.hdfs.server.datanode.DataNode.initBlockPool(DataNode.java:1679)at org.apache.hadoop.hdfs.server.datanode.BPOfferService.verifyAndSetNamespaceInfo(BPOfferService.java:390)at org.apache.hadoop.hdfs.server.datanode.BPServiceActor.connectToNNAndHandshake(BPServiceActor.java:282)at org.apache.hadoop.hdfs.server.datanode.BPServiceActor.run(BPServiceActor.java:824)at java.lang.Thread.run(Thread.java:748)2021-05-15 15:34:23,603 WARN org.apache.hadoop.hdfs.server.datanode.DataNode: Ending block pool service for: Block pool (Datanode Uuid f4005613-8a0d-4177-a59a-5a999f41ccd6) service to /10.231.33.101:9000分析上面的错误就是因为本地的DataNode之前存储过旧集群数据,写新数据的时候发现版本不一样。直接删除旧的DataNode的数据目录, 默认为:/tmp/hadoop-root/dfs/data.重新运行:hdfs --daemon start datanode启动成功了:datanode连接成功。好了,如果有多个DataNode必须重复删除所有DataNode的数据文件,再运行上述命令。总结根本原因:不同集群数据问题校验问题,错误为Incompatible clusterIDs错误
补充知识:OpenSSH 是 SSH (Secure SHell) 协议的免费开源实现。SSH协议族可以用来进行远程控制, 或在计算机之间传送文件(scp)。而实现此功能的传统方式,如telnet(终端仿真协议)、 rcp ftp、 rlogin、rsh都是极为不安全的,并且会使用明文传送密码。OpenSSH提供了服务端后台程序和客户端工具,用来加密远程控件和文件传输过程中的数据,并由此来代替原来的类似服务。OpenSSH的分为客户端和服务端两部分客户端的配置文件:/etc/ssh/ssh_configSSH服务端配置文件:/etc/ssh/sshd_config服务脚本:/etc/rc.d/init.d/sshd其他移步 openssh 官网:https://www.openssh.com/
直接使用命令 brew upgrade python 进行升级这个命令会安装过程会下载一下依赖包,也会下载稳定版的python3。Operation time out 问题如果出现问题:Failed to connect to www.python.org port 443: Operation timed out类似的错误:ERROR: Could not install packages due to an EnvironmentError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443)这个主要是网络问题,建议重试几次之后重试brew升级命令遇到xcode-select问题单独运行 xcode-select --install .安装即可,Apple国内下载xcode相关包速度还够给力。主要是brew安装新版python过程需要一些xcode的依赖。升级Python3之后发现terminal这边输入python -V,输出还是2.7出现下面的情况,明明brew安装好了python3,可是重新开一个terminal输入python -V 仍然显示2.7因为系统之前安装过anaconda,查看.bash_profile文件发现最后将conda.sh提前source了,path变量被动了。这里需要将bash profile中conda 设置PATH这一段注释掉。或者直接在bash profile,3.8版本的路径可以参考下面内容,直接添加到~/.bash_profile最后一行:export PATH=/Library/Frameworks/Python.framework/Versions/3.8/bin:$PATH旧的pip也需要进行升级否则系统会一直显示2.7 Python 不支持(DEPRECATION: Python 2.7 reached the end of its life)相关的信息。强烈建议升级, 否则有些项目使用pip命令会出现“socket.timeout: The read operation timed out” 这样的错误。python -m pip install --upgrade pip或者全路径:/usr/local/bin/python -m pip install --upgrade pip旧版的virtualenv需要升级安装virtualenv命令可以使用但是就没有报错,不兼容,直接升级。升级virtualenv需要使用pip命令来做:pip install --upgrade virtualenvvirtualenv 创建一个虚拟环境至此,pip,virtualenv都是可以有效使用的,这次分享到这里。
该文章分享的js代码不适用于生成环境使用。在生产环境中,后端服务对数据库连接的管理都是通过一个 数据库连接池。在接口需要获取数据库数据的时候,从池中取一个活跃连接,到数据库取完数据/执行数据变更操作后,释放连接并放回连接池。上图所示,当业务线程处理数据时,使用连接;右边为业务处理结束,释放连接,放回连接池中。这里分享一个连接池的JS代码const mysql = require("mysql"); const port = 3306 //创建数据库连接池,连接到指定IP const pool = mysql.createPool({ host: 'localhost', port: port, user: 'root', password: '12345678', database: 'demo20210330', multipleStatements: true }); // 定义sql执行函数 // 并允许用户输入连接执行成功的回调函数onSuccess,以及错误回调onError const runSql = function(sql, onSuccess, onError) { pool.getConnection(function(err, conn) { if (err) { onError(err); return; } conn.query(sql, function(err, data) { try { if (err) { onError(err); } else { onSuccess(data); } } catch(err) { onError(err); } finally { console.log('release connection'); conn.release(); } }); }); }; //查看池的状态 const showPool = function() { console.log('pool: ', pool); }; module.exports.show = showPool; module.exports.runSql = runSql;这个工具JS库runSql函数中每次执行SQL,最后不管结果如何,总会在finally内释放链接,达到了连接自动回收的作用。PS:上面的数据库配置密码过于简单,实际环境建议改复杂点(推荐:通过环境配置来解密数据库密码)。保存代码为dbpool.js, 进入Node终端;然后输入以下代码并查看结果:var pool = require('./dbpool')pool.runSql("select * from company;", (e)=>console.log(e)PS:下面的数据库在前一篇文章中提前创建好了输入pool.show() 查看连接池状态, 比如连接信息,空闲的数据库连接等。注意事项:连接池的连接不要手动去调用destroy(销毁连接)。连接池的作用因为销毁连接,当业务需要的时候调用数据库获取数据的时候,需要重新建立连接(TCP连接),这个过程开销是很大的。相比之下,维持一个长连接省去很多建立重连TCP的时间。起到了缓冲作用,而且所有数据库连接都会在缓存池里被统一管理。上文是MySQL JS库自带的连接池,普通应用开发上面的足够,在Github上还有一个通用连接池项目:https://github.com/coopernurse/node-pool
首先,启动MySQL服务器并准备数据库Profile(下图为MacBook 单机版MySQL Server)数据库Profile信息包含:目标数据库服务器IP,端口,用户账户和密码,还有目标数据库名称。编写连接数据库的JS函数将下面内容复制到db.js文件const mysql = require('mysql'); //创建一个数据库连接 const db = mysql.createConnection({ host: "localhost", port: 3306, user: "root", password: "12345678" }); const logDbStat = function(){ console.log("db state %s and threadID %s", db.state, db.threadId); // console.log("db detail:", db); } //在此时,仍旧未有实际连接到数据库 logDbStat(); console.log('start to connect mysql'); db.connect(function(err){ if(err){ console.log('fail to connect db',err.stack); throw err; } // 这里正真连接上数据库了。 logDbStat(); }); const close = function(){ db.destroy(); console.log('db disconnected'); logDbStat(); }; //定义一个计数器和runSql函数 //作用是跟踪输入的SQL与输出结果,也对数据库操作做一个简单包装。 var counter = 0; const runSql = function(sql, callback){ counter++; var cv = counter; console.log('run sql[%s] :[%s]',cv, sql); db.query(sql, function(error, result, fields){ try{ if(error) throw error; console.log('[%s] result:', cv, result); // use cv instead of counter, as counter is part of 'global', it will be polluted }finally{ if(callback) callback(result, fields); } }); } // 执行SQL语句 const myDb = 'demo20210330'; runSql("CREATE DATABASE IF NOT EXISTS " + myDb + " DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ; "); runSql("USE " + myDb); //创建公司数据表 runSql("CREATE TABLE IF NOT EXISTS COMPANY ( " + " ID int(11) NOT NULL AUTO_INCREMENT, " + " NAME varchar(64) NOT NULL UNIQUE, " + " AGE int(8) NOT NULL, " + " ADDRESS text DEFAULT NULL, " + " STOCK_PRICE decimal(10,3) DEFAULT NULL, " + " PRIMARY KEY (ID) " + " ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8; "); runSql("TRUNCATE TABLE COMPANY ;"); //插入三条公司股价信息 runSql(" INSERT INTO COMPANY " + "(NAME, AGE, ADDRESS, STOCK_PRICE ) VALUES " + "('Alibaba', 21, 'Hangzhou', 230.35), " + "('Tencent', 31, 'Shenzhen', 610.00), " + "('XiaoMi', 10, 'Beijing', 25.05) ; "); // 查询所有公司股票信息和地址等 const querySql = "SELECT * FROM COMPANY ;"; runSql(querySql, function(result,fields){ console.log('query result:', result); }); /** db.query(querySql, function(err, rows){ if(err){ console.log('error ' + err); throw err; } console.log('query output='); console.log(rows); }); */ // close(); // don't close here as the query function may not completed. runSql("SELECT 1 ; ", function(result, fields){ close(); }); // runSql(" DELETE FROM COMPANY ;");或者从我的代码库下载:https://codechina.csdn.net/geeklevin/nodejs-api-006-db重点函数解析runSql(上面代码中对SQL语句执行的包装),使用了全局counter变量,并允许用户调用端提供一个回调函数。比如代码最后执行‘SELECT 1; ', 执行回调关闭数据库连接。数据库连接用完,必须进行管理,如果不适当关闭回导致数据库服务器端连接过多而拒绝新的数据库连接,影响其他实例,这里不做细讲。执行node db.js 运行效果第六条语句:查找到3条记录,并输出Alibaba,Tencent,XiaoMi 三个公司信息及股票价格等。结果分析从上面截图看,runSql函数顺序执行,可是SQL执行结果(result)对象是不同时输出(异步)的。所以在设计runSql函数的时候,加入了counter计数器,这样可以根据输出,对应找到相应的下标(比如第六条SELECT SQL)结果。本文的db.js不作为生产环境使用,仅供普通开发者参考,学习NodeJS连接数据库,具体的数据库SQL语句,读者可自行查找学习。引用:https://www.npmjs.com/package/mysql
在NodeJS项目开发过程中,我们经常使用公共JS库。比较常用的做法就是通过npm去install目标js库,然后这个库会被放在node_modules目录下。接着,我们自己写的JS文件中,使用require("目标js")库来使用他人共享的代码,这样很容易事半功倍。下面谈谈require函数本文会使用Node REPL:这是一个交互式的NodeJS代码执行终端,更多参考: http://nodejs.cn/learn/how-to-use-the-nodejs-repl先搞清楚是什么首先,直接说require的函数功能:用来加载目标js库,并返回当前库公开的属性成员函数/变量。我们打开terminal终端/Command,输入: node //打开Node REPL然后输入下面内容:requirethis.require === require所以这里得到结论:require是Node引擎上下文(context)的内置对象属性,也就是全局对象的require属性,可调用或者使用this.require也行。require能用来干什么?初学NodeJS会了解到它有内置模块,比如fs,http 等。好,这里我们试着require('fs') //加载文件系统模块require('fs')非内置的模块,也想用require来加载怎么做?在当前目录下,我们编写一个product.js(内容如下)。然后,试着用require来加载看看。const products = {data:[]} function getData(){ return products.data; }Node REPL终端输入:require('./product.js')require函数能够加载这个product.js,不过不像内置模块一样,需要通过给路径来定位到js文件,如:require('./product') 或者 require('./product.js')这里我们看到打印的对象没有任何属性,require返回值为 :{} //没有任何属性。require函数加载原理由于NodeJS模块都遵循了CommonJS规范,根据CommonJS规范,JS库的开发者如果需要开发某些函数对外部模块使用,需要使用module.exports或者exports具体如下:module.exports.属性名 = 函数引用 //这里将当前JS内的某个函数赋给module.exports或者:exports.属性名 = 函数引用 像前面一个版本的product.js相当于const products = {data:[]} function getData(){ return products.data; } //默认情况,module.exports 这个对象没有任何属性,如下代码。 module.exports = {}好,修改product.js文件,继续在终端跑require('./product')看看(输出仍旧为:{} )。这里需要退出当前node终端,重新进入(请读者带着一个思考题:为什么需要重新打开一个node REPL终端)。那么在npm registry上的库,怎么进行加载?比如输入require('lodash') 马上发现错误了,“Cannot find module 'lodash'", 这个错误经常容易见到(有时候拿到一个NodeJS项目忘记跑npm install了)。也可以输入require('restify'), require加载一些常见的模块试试。通常我们需要使用命令安装JS库:npm install 目标JS库名,再来使用它共享的功能。我们试试看,安装完lodash库之后,继续在Node终端输入require('lodash') 可以使用了,这里不需要重启(想想为什么)。好了,前面提了几个围绕了是否重新开一个NodeREPL终端来require JS库的问题为何node终端能够加载到product.js, export内部函数之后又需要重启,引入外部JS库又不需要重开一个Node REPL终端。。。这里需要讲讲require的另一个伙伴,module函数。它跟require函数一样都挂载在上下文中,也是全局对象的一个属性,它的作用是管理整个项目的模块。上面所示,在一个有product.js 和node_modules/lodash这个模块,进入node终端,打印module显示的了模块加载的细节,这里稍微留意一下children(目前是一个空的数组)。解答”Cannot find module"问题但是paths非空,我们使用require加载函数的时候,node引擎会从内置模块和paths对应的路径去查找模块,找不到才会抛出类似异常:“Cannot find module 'lodash'"当我们跑了npm install 库名, 对应模块被下载到node_module目录,加载的时候才能定位到库,正常使用该库功能。在含有package.json的目录中,执行npm install命令,可以一次性下载dependencies属性声明的全部依赖库,在我们写的js文件中能够正常使用。解答是否需要重启Node REPL 或者修改代码是否需要重启正在的NodeJS进程的问题继续在终端输入require('./product') ,然后输入 module, 再次输出module对象,它的children已经多了一个Module对象(id对应到了product.js)。当我们修改了product.js的时候,node引擎发现module对象已经记录加载过product.js了,不会重新进行加载。所以,虽然最新代码导出了getData函数,可是我们加载到的仍旧是:{}//无任何函数导出。解答为何npm install lodash之后为何能够直接在node终端直接require这个很简单,因为node启动,默认会查找到当前目录下的node_modules目录(不管目录存在不存在)。当我们require一个不存在的js模块的时候,module对象找不到模块,它的children属性并不会有任何变动。所以只需要安装了,就可以require加载。构建代码共享,开源文化这算是个题外话,前面有文章写过关于实现商品的增删查改,https://blog.csdn.net/geeklevin/article/details/109403172。读者可以下载这个JS:https://codechina.csdn.net/geeklevin/nodejs-api-002-crud/-/blob/master/product.js,使用require调用就能获得对商品对增删查改的功能。通过对代码进行封装,对外开放几个函数,隐藏了细节,也简化了对功能的使用。var pm = require('./product') //调用库开放函数 pm.getData() //其他调用 pm.其他函数()好用的代码像诗,越简洁,越好用,那些垃圾代码只会慢慢沉寂下来无人问津。npm registry (https://www.npmjs.com/)在npm registry上面有很多开发者发布的库,当我们想要使用一个功能,或者实现一个功能之前,不妨先上去找找有没有别人写好的库。如果没有,那么遵循CommonJS(前面说的module.exports)来组织我们的代码发布共享,解决问题同时帮助他人,微薄之力能够推动社区进步呢。npm社区就这样通过一个个普通开发者,分享一个个的小的库,培养了一个丰富多彩的技术生态。Atwood’s Law是Jeff Atwood在2007年提出的:“any application that can be written in JavaScript, will eventually be written in JavaScript.”这里我们在简单大胆的总结,require和module互相协作产生的模块加载机制,是整个NodeJS开源文化的基石之一;而CommonJS就是一个脱离了框架的协议。这也在很多语言中反复出现,像python/java的import包,CommonJS就像一个包协议约定了库的共享的标准格式,npm对标maven central/python libs。这套协议加上加载模式相关的接口模式,很值得借鉴。总结本文简单的介绍了require和module函数,Node引擎内使用module来管理模块,使用require加载模块。基于这个技术和协议延伸了:如何做代码分享,构建JS模块分享的生态。本篇从使用函数反向思考简单涉猎,更多细节可以自行阅读NodeJS源码。篇幅有限,后续会发表更多技术补充。细心的读者可以发现,本文对于加载lodash这个模块没有更多深入解析,这种发布在npm registry上的包的解析,可以自行阅读它的代码,这一块还是比较好玩的。参考:https://nodejs.org/en/knowledge/getting-started/what-is-require/https://nodejs.org/docs/v0.4.2/api/modules.html (这里的模块加载方式已经过时,不过刚好没有找到module的解析,可以看看,原理类似)
在我们开发后端服务的过程中,我们除了开发当前服务的数据接口之外,避免不了需要调用外部服务接口(一个或者多个服务的API)前篇我写了一个基于Restify实现的CRUD的商品管理服务:https://blog.csdn.net/geeklevin/article/details/109403172,Restify可以帮我们快速的编写服务接口。当我们需要访问其他服务的接口有什么JS库可以使用呢?回看Restify文档,它本身有Restify Client这个组件,可以帮助开发者实现对站外服务的调用。http://restify.com/docs/client-guide/不过这里我们会使用axios这个库来进行外部服务的调用,这个库很火,很多前端同学在开发React/Vue前端模块的时候经常会用到。先看文档,直接打开Axios Github直接看:https://github.com/axios/axios 或者 (中文友好 http://www.axios-js.com/zh-cn/docs/ )编写Axios HelloWorld创建一个新的NodeJS项目,引入axios包:#跟当前文章同个版本npm install axios@0.20.0发送请求前需要启动商品服务https://blog.csdn.net/geeklevin/article/details/109403172发送一个GET请求const axios = require('axios'); //外站接口 const api = 'http://localhost:8080/products'; //axios通过提供对应HTTP请求方法,实现GET/POST/PUT 等对应的请求发送 // 这里调用对/products接口的GET方法,获取产品 axios.get(api) .then(function (response) { //这里获得整个请求响应对象 console.log(response); //获取商品数据只需要调用: response.data }) .catch(function (error) { console.log(error); }) .then(function () { });上面代码保存为 getProducts.js, 执行下面命令运行,效果如下:node getProducts通过查看console输出的整个response对象,接口数据可以通过response.data来获取,进一步简化可以改成下面代码:const axios = require('axios'); //外站接口 const api = 'http://localhost:8080/products'; // 编写进行处理产品数据的业务代码 const handleOnData = (data) => { console.log('get data', data); } // 这里调用对/products接口的GET方法,获取产品 axios .get(api) .then(function (response) { handleOnData(response.data); }) .catch(function (error) { console.log(error); }); 发送一个POST请求,模拟添加商品数据其他请求类似,这里只是快速展示了axios这个组件的使用。对了,学委还有这个可以关注长期阅读 =>雷学委趣味编程故事汇编或者=> 雷学委NodeJS系列项目代码参考:https://codechina.csdn.net/geeklevin/nodejs-api-004-call-other-service
本文基于CentOS7,安装使用Jenkins需要用户提前安装JDK或者jre环境。下载安装下载新版Jenkins WAR包可以去Jenkins官网或者国内搜索最新稳定版本:https://www.jenkins.io/download/本文使用的Jenkins: https://download.csdn.net/download/geeklevin/15022043 (可以从这里下载,上面的网站在境外访问很慢)启动Jenkinsexport JENKINS_HOME=/devops/jenkins/home #这个目录存放Jenkins的所有配置 nohup java -jar ./jenkins.war > jenkins.log & 使用Jenkins配置&运行第一个Job这里创建一个类型为FreeStyle的Job,内容如下,只是简单的展示了maven版本和Java版本。mvn -version java -version配置为国内的Jenkins插件中心拷贝: https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json点击“Submit"(PS: 这里配置的时候卡了,点击了多次导致后面出现一个意想不到的错误)这里会出现问题,因为插件升级中心会进行签名检查。我们修改了默认的,需要在启动脚本添加下面的参数,设置为忽略。-Dhudson.model.DownloadService.noSignatureCheck=true 更多资源移步清华的开源镜像源:https://mirrors.tuna.tsinghua.edu.cn/jenkins/尝试安装插件安装一个插件把build状态改成绿色球Caused: java.io.IOException: Failed to load https://updates.jenkins.io/download/plugins/greenballs/1.15.1/greenballs.hpi to /usr/local/src/jenkins/home/plugins/greenballs.jpi.tmp at hudson.model.UpdateCenter$UpdateCenterConfiguration.download(UpdateCenter.java:1287) Caused: java.io.IOException: Failed to download from https://updates.jenkins.io/download/plugins/greenballs/1.15.1/greenballs.hpi (redirected to: https://get.jenkins.io/plugins/greenballs/1.15.1/greenballs.hpi)哎,居然遇到错误,算了,后面再看。继续尝试一个国内的插件。安装汉化包这个汉化包,本身是一个插件(hpi/jpi)后缀。进入服务器,查看jenkins home plugins目录,可以进一步验证。勾选 Restart Jenkins 这一栏,可以重启并加载下载好的插件。这个插件安装完,可以在system configuration那里看到Locale,结果没有。点击插件,跳转到官网,也没有看到任何配置说明 => https://plugins.jenkins.io/localization-zh-cn/插件已经装上了,不过预期出现的配置位没有出现,估计是版本兼容问题,先不处理。升级其他插件新装的Jenkins会有很多警告信息,也可以跑去Update Center,选择升级,消除警告。警报数量将为1了,但是还有很多插件缺少,先写到这里。前面插件下载遇到问题明明设置了国内Plugin Center,日子还显示从默认的plugin repo下载。查看Jenkins服务器上面的插件配置文件出现了多个site,而且是重复的。进行删除留下一个重试下载。很多插件都能正确下载了。安装成功重启后,打开原来的test job发现Console Output旁边的圆形图标变成绿色的了。参考链接:https://www.jenkins.io/https://plugins.jenkins.io/https://jenkins-zh.cn/wechat/中文插件问题 - https://cloud.tencent.com/developer/article/1631632
概述CRUD 就是我们常说的创建Create,读取Read,更新Update,删除Delete。这几个操作是后端开发中最常见的几个,举个例子,比如说一个商品销售的网站。它涉及的商品添加上架,提供商品给用户浏览,以及后续商品属性更新,下架到最终撤走,都是围绕一个商品进行CRUD的操作。当然实际操作会更加复杂。从简入手前篇写了一个更加参数name打印到响应返回用户端,在浏览器展示。这篇我们将创建一个商品product,然后我们围绕商品做下面4个接口:添加商品信息读取商品信息更新商品信息删除商品先看效果图读取所有商品,打开浏览器访问:http://localhost:8080/products(下图为FireFox 右键->查看元素,这里选择火狐因为结果自动格式化了。 如果是Chrome在上面网络这一栏,我们可以进去看到,浏览器访问这个接口链接本质上,是进行了一个GET 请求。这里有必要引入一个常用的命令行工具“curl”, 先记住简单使用命令:curl -X GET 目标链接 #上面的命令是使用curl工具对 目标链接 发起 GET 请求。上图左边为接口测试窗口,右边为接口服务的web进程。由于浏览器默认没有安装测试web api的工具,下面其他增加,删除,更新操作都使用curl工具进行添加商品信息curl -X POST -H "Content-type: application/json" -d '{"id":5,"name":"Cannon","type":"Camera"}' http://localhost:8080/produc删除商品信息,效果如下执行删除前,我们先查询一下id为4的产品信息,再执行删除,效果如下右边web进程输出了最新产品信息没有id=4的记录了,删除成功。查询商品信息,效果如下#查询id=4的产品,没有返回,已删除curl -X GET http://localhost:8080/product/4 #查询id=5的产品,返回id为5的产品信息。curl -X GET http://localhost:8080/product/5 修改商品信息#我们打算把id=5的产品名改为CannonX2curl -X PUT -H "Content-type: application/json" -d '{"id":5,"name":"CannonX2","type":"Camera"}' http://localhost:8080/product 再回来讲原理快速过一下,curl工具的介绍curl 是Linux类操作系统常见的Http请求客户端,linux可以通过终端输入:yum install -y curl 进行快速安装。简单重复一下curl 的使用:curl -X 动作(http 方法) 链接(目标接口网址)更多信息看:https://curl.se/download.html本篇重点下面代码重点解析,上图多个接口的代码实现。关于server(http 服务创建) 没有再次注释,可以回顾前篇: https://blog.csdn.net/geeklevin/article/details/109013368。const restify = require('restify'); //加载了产品信息的js模块赋值给pm常量 const pm = require('./product.js'); //这里回顾上面演示第一个图,访问链接前打印所有产品信息 pm.show(); console.log(pm.query(1)); function read(req, res, next) { ....//省略获取请求中id的代码段 // 调用pm对象的查询产品方法 res.send(pm.query(id)) } function dele(req, res, next) { ....//省略获取请求中id的代码段 //调用pm对象的删除产品方法 res.send({status:'deleted',msg: pm.delete(id)}) } function readAll(req, res, next){ //调用pm对象的读取所有产品方法 res.send(pm.getData()) } function create(req, res, next){ ....//省略获取请求中product的代码段 //调用pm对象的增加产品方法 pm.add(product); } function update(req, res, next){ ....//省略获取请求中product的代码段 //调用pm对象的修改产品方法 pm.update(product) } //这里是前篇讲过 var server = restify.createServer(); server.use(restify.plugins.bodyParser({ mapParams: true })); //这里重点,把readAll方法绑定到接口/products的GET动作 server.get('/products',readAll); //这里依次把 查/增/改/删 分别绑定到/product的GET / POST / PUT / DELETE 动作。 server.get('/product/:id', read); server.post('/product', create); server.put('/product', update); server.del('/product/:id', dele); //服务器监听8080端口 server.listen(8080, function() { console.log('%s listening at %s', server.name, server.url); });上面通过restify 对应的server对象绑定了4个常用的HTTP请求方法,实现了对同个链接不同请求方法的不同响应。在此,我们再拿一个更新产品操作解析:curl -X PUT -H "Content-type: application/json" -d '{"id":5,"name":"CannonX2","type":"Camera"}' http://localhost:8080/product 用户端:使用curl 针对/product接口,发起PUT请求服务端:发现/product接口有效,并且绑定了更新产品方法到PUT方法,执行更新产品方法。(最近想到了一个RESTFly工具在弄,拖了很久写的这篇文章。欢迎读者评论解析其他增加,删除,查询的原理)PS:上图为重点代码解析,省略了很多代码(不作为可执行最终版)项目完整代码:https://codechina.csdn.net/geeklevin/nodejs-api-002-crud
REST API是什么?REST中文意思是REpresentational(代表性的)State(状态)Transfer(传输),比较拗口,这个概念是Roy fielding提出的一种应用在分布式系统的架构风格。很多时候不小心写错了,我们会看到Restful API,碰到这样写法,严格说是错误的。简单理解,RESTful API就是尽量满足以下六个设计原则来进行实现的接口,它们是:数据跟界面解耦无状态可缓存统一接口/标准化接口分层次系统风格按需编码(可选)这么多个原则,换个简化的说法,即是,更清晰更标准化更易扩展的接口。还是很抽象的感觉?下面举几个例子一一阐述这些原则。No.1 数据跟界面解耦合这个很好理解,接口要朝着提供视图需要的数据的方向来设计,定制。比如我们在做查询用户接口数据/api/user/001的响应时,应该仅仅返回一个用户的json数据{ "name":"levin", "photo":"baidu.com/ll/png234u1o343.png" }视图代码如下(以web页面为例子,界面代码通常是一些标签包围起来的代码块)<div> <div>Name:<span>{{name}}</span></div> <div>Photo:<img src="{{photo}}"/></div> </div>而不是设计一个接口直接生成(当然这个原则并不能阻止部分程序员直接生成带数据的视图,最好不要这样做,让数据和界面分开可以有更高复用性)<div> <div>Name:<span>levin</div> <div>Photo:<img src="baidu.com/ll/png234u1o343.png"/></div> </div> No.2 无状态首先,无状态就是请求这个接口的时候,处理该请求的服务在未获取数据时,该服务是没有地方存储待获取的数据的。举个例子,还是/api/user/001 这个接口,背后的伪代码可以如下:let dataService = new DistributedDataService() @Get("/api/user/{id}") function getUser(id){ return dataService.queryUser(id) }这个接口的实现调用了dataService去查询用户编号是001的信息。服务本身是没有存储任何状态的,数据可以从数据库或者缓存中获取,这就是保证服务的无状态。什么情况是有状态呢?举个例子,伪代码如下://local data cache let dataService = LocalDataService() @Get("/api/user/{id}") function getUser(id){ return dataService.queryUser(id) } @Delete("/api/user/{id}") function getUser(id){ return dataService.deleteUser(id) }这里使用的dataService是LocalDataService(本地的数据服务),通常单进程运行是没有任何问题的。因为无论如何操作,只要LocalDataService支持线程安全(这个需要开新的一篇来讲),它的状态严格上说不会有问题。但在分布式系统中,普遍做法是部署多个服务器多实例,而且他们同时提供/api/user/001接口的服务。这时候问题就来了,每个服务进程都拥有一个localDataService。当调用来DELETE /api/user/001,只有一个服务的localDataService状态更改了(少了001用户)。其他实例的本地数据仍有001用户,下一次查询的结果会有两种,一种结果是返回用户没找到,另一个结果是返回001用户,这就跟我们预期的不一致,导致了业务错误。No.3 可缓存应用缓存是一种提高性能的手段。这里官方也没有特别说明,其实符合普遍对资源的缓存的理解。针对某个接口,可以设置请求头Cache-Control, 可以是no-cache 或者max-age= 60 (某个数值)去告诉接口请求方可以缓存该接口数据多久。举个例子,我们进入百度搜索RFC 7234(http协议关于缓存的标准)可以看到“消息头”里面关于缓存的设置,百度把一些不经常变化的资源标记了很大值,告诉火狐浏览器,下次不需要重新下载该资源。https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/soutu/js/tu_68114f1.js刷新页面,我们可以拿上面缓存的url进行搜索,这里显示已经缓存了。No.4 统一接口/标准化接口官网提了,接口定义需要满足4个约束:资源标示、通过呈现方式来操作资源、资源自描述、以及使用超媒体作为应用状态。举个例子,像设计 /api/user/ 这个接口,如何符合第四原则满足统一标准化接口?首先,/api/user 这个uri定位了用户资源相关的信息操作,然后可以用GET来查询用户信息,DELETE来删除用户信息。再说资源自描述,我们查询 /api/user/1001, 获得一下响应消息:{ "id":"1001", "name":"levin", "country": "China" } 像这样的就是资源自描述,这个响应告诉了我们用户信息的属性,结构。我们查询其他人像小明,也是获取类似的反馈。至于超媒体作为应用状态,这个不需要多说,简单理解为多种格式的响应消息作为状态即可。这个设计原则,更多约束目标系统的REST API更加容易操作,可见可读性。No.5 分层次系统风格这里就是架构的分层,每层内的组件不允许看到与其不直接交互的层,也就是说,非直接交互的层对当前层内的组件不可访问。比如上图,A <->B<->C,三层架构中,AB紧邻,BC紧邻,这样分层避免了AC直接交流,A层的变动不会对C层造成影响。这样设计可以更好的让层与层之间互相解耦,让每一层专注做单层的功能(当然分层数过多也有坏处)。No.6 按需编码(可选)服务器可以提供一些代码或者脚本并在客户的运行环境中执行, 比如一些JS脚本供客户端下载调用。像过去Java服务器端可以生成Applet脚本供客户端执行。说到这里跟按需编码(CodeOnDemand), 好像表达的意思不太贴切,我们只需要明白这个设计原则是为了提高客户端的扩展性即可。上面说那么多,其实RESTful API本质是通过走HTTP协议来呈现接口。通过uri为系统交互的接口,比直接代码调用代码更加简化,而且脱离了语言约束。更多细节参考:https://restfulapi.net/https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Controlhttps://www.rfc-editor.org/info/rfc7234https://www.rfc-editor.org/rfc/inline-errata/rfc2616.html
关于NodeJSNodeJS是一个基于Chrome V8引擎的JavaScript运行环境,一个让JavaScript运行在服务端的开发平台;它用于方便地搭建响应速度快、易于扩展的网络应用。其他概念不重要,作为一个新手,这里只要记住NodeJS基于一个引擎来运行的环境,这引擎就是下文的node程序来启动的。小白可能看不懂了,那就先把NodeJS就是一个代码执行器,我们后面写代码都得用这个来运行。代码就是NodeJS软件能读懂的语言,跟我们说中文,英文一样的。但是NodeJS是外国来的,它天生就不懂中文,所以新手写的代码的时候,尽量不要出现中文也不要用中文输入法(这有可能让你的代码出现中文符合,程序错误)!前端的同学经常回看到命令:npm run start 或npm run dev等等。然后自动打开了一个浏览器窗口,加载了当前项目编译好的前端UI代码。这个进程运行的就是NodeJS引擎。下面先安装Node软件吧,初步使用一下node程序。下载与安装https://nodejs.org/zh-cn/download/ or https://nodejs.org/zh-cn/download/releases/版本一般选择最新即可,window操作系统选择exe安装。推荐使用zip或者tar.gz方式,然后参考下面设置到环境变量PATH的方式。set PATH=%NODEJS_HOME%:%PATH%通过手动设置这种方式,可以让你会更熟悉node/npm配置。以后玩vue cli或者angular,react-script都很有帮助。安装成功可以通过下面命令查看:本文使用的版本从这里下载:https://nodejs.org/download/release/v12.18.0/,苹果电脑 macbook选择pkg包安装好后,打开cmd(命令行)或者苹果电脑打开terminal(终端),输入node这直接就进入了对接NodeJS引擎的交互终端了。给小白的提示:交互终端就是在线写代码运行代码的窗口特别注意:下面输入程序的时候,不要有中文符号,因为NodeJS毕竟出生在国外,肯定不懂中文,运行就会出错!开始写NodeJS代码编写第一行代码直接复制下面一行代码:console.log("hello,雷学委!") 效果如下,也可以参考下面继续试着输入一个表达式:1+1NodeJS来做一道应用题在数学中定义变量就是如下:已知:x=4, y=20,求 y由几个x相加而得到?这道应用题大家都见过吧,下面用编程来表达。我们继续在上面的node窗口中输入下面三行:x=4 y=20 u=y/x是不是很方便变量u的值5,就是我们要求解问题的答案!这些只是简单的使用。NodeJS来做一个函数数学中,我们都学习过函数这个概念,比如y=2x + 5 代表一个直线。知道随意一个x值就能够求出y的值。那这个用程序怎么做呢?好了自己试试吧。创建一个新项目什么是项目?项目简单理解就是一个文件夹,它包含了一些代码文件(如js结尾的文件)。我们把整个目录的文件看成一个整体就是项目。做的好的项目还能分享给程序员使用!通常分享给别人的项目我们称作库(代码库),后面统一用JS库代指。稍微看一下概念,打开命令终端输入:npm init如下图,中间过程会有提示很多输入选项,重点关注package(包) 和version(版本号)。这两个就像身份证好一样的东西,可以让其他开发者找到你的项目。为啥项目需要给定package和版本号?如果很多作业考试题目都有一个package和version来定位?那么我想要找到一道题目的答案就能通过pacakge + version来定位到了。在NodeJS的开发社区中,有一个网站叫做npm registry: https://www.npmjs.com/。这个网站登记了所有NodeJS对外分享开源的JS库,有空多去搜搜看。如果我们想要复用别人项目做好的功能,那么通过package和版本号就能定位到。通过这两个定位找到别人的代码来使用,相当于抄作业了,别人写一个正确的版本给你哦(千万要记得去点赞转发支持,这会鼓励更多人发布分享更多的JS库)当我们项目需要引用其它JS库的时候,可以明确给定某个”库@版本“进行定位。比如下图package.json dependencies引用了koa这个库。开发第一个NodeJS项目编写app.js或者index.js刚才我们创建项目时,通过设置main属性,指定了app.js文件作为入口。所以这里我们编写一个app.js, 具体如下图的代码,解释一下:console.log 是NodeJS内置提供的一个JS函数,我们可以使用它来打印内容输出到终端。然后可以使用node app.js 执行。记住,执行一个js文件使用命令:node 文件名.js这就是一个最简单的JS库了,只是用来打印内容,我们没有引用别人的JS库。进一步工程化很多nodejs项目通常可以使用npm start或者npm run start.这里我们可以在package.json 中进行修改,如下图第八行所示。”npm run start” 或者更早版本“npm start”,nodejs会解析package.json查找scripts属性下的“start”属性对应的命令,等价于执行node app.js。通常可以看的很多项目中有“build”,“test", "build-prod"等等应对nodejs项目构建测试的一些scripts,可以自己尝试添加更多scripts字属性验证。先到这里,上图为第一个nodejs项目的执行效果。这个示例很简单:app.js写了代码调用console.log输出"hello nodejs 001"。回顾这个项目我们打开package.json, 它定义了这个项目的name,version和描述。这文件也定义了程序入口(main),同时其它包括作者(author)项目地址等信息。package.json还能定义项目的一些引用的js库(依赖的JS项目)。这个在下篇会继续展示,同时使用nodejs制作API也就是提供数据接口的示例。对了,学委还有这个可以关注长期阅读 =>雷学委趣味编程故事汇编或者=> 雷学委NodeJS系列当前示例项目仓库:https://codechina.csdn.net/geeklevin/nodejs-001。
2022年01月
2021年12月