-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 346 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 346 KB
1
{"pages":[],"posts":[{"title":"编译安装nginx及php","text":"安装nginx所需依赖安装pcre-devel源安装1yum install -y pcre-devel 安装包安装安装openssl-devel1yum install -y openssl-devel 安装nginx1234567891011121314./configure \\--prefix=/usr/local/nginx \\--sbin-path=/usr/local/nginx/sbin/nginx \\--error-log-path=/var/log/nginx/error.log \\ --http-log-path=/var/log/nginx/access.log \\--with-http_stub_status_module \\--with-http_gzip_static_module \\--with-http_ssl_module \\--with-http_flv_module \\--with-http_v2_modulemake && make testmake install php7.4.15依赖libxml2-devel、sqlite-devel、libcurl-devel、libpng-devel、libzip-devel、openssl-devel 安装php7.4.15安装时可能会出现 libzip 无法找到 执行以下命令 1export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig/" 安装 12345678910111213141516171819202122./configure \\--prefix=/usr/local/php@7.4 \\--with-config-file-path=/usr/local/php@7.4/etc \\--enable-fpm \\--with-libxml \\--with-openssl \\--with-zlib \\--enable-bcmath \\--enable-calendar \\--with-curl \\--enable-ftp \\--enable-gd \\--enable-pcntl \\--with-pdo-mysql \\--enable-soap \\--enable-sockets \\--with-zip \\--with-pearmake && make test make install 参数说明: 参数 描述 –prefix 安装目录 –enable-fpm 开启php-fpm –enable-ftp 开启ftp –enable-bcmath 开启bcmath –enable-calendar 开启calendar –with-libxml 安装XML扩展 –with-openssl 安装OPENSSL扩展 –with-zlib 安装ZLIB扩展 –with-curl 安装CURL扩展 –with-pdo-mysql 安装PDO-MYSQL扩展 –with-zip 安装ZIP扩展 –with-pear 安装PEAR扩展","link":"/2021/03/03/bian-yi-an-zhuang-nginx/"},{"title":"区块链从入门到放弃","text":"入门介绍与原理:一、比特币1.比特币白皮书 这是一切的开始 2.精通比特币 讲比特币很详细的一本书,看完基本对比特币的认识就清楚了。 3.TheProof-of-Work Concept PoW机制理论介绍,英文 4.比特币的原理及运作机制 这篇是新手向,适合向圈外人介绍什么是比特币什么是区块链 5.比特币pow难度调节机制 PoW的难度调节是一个要点,一篇简介 二、区块链1.区块链技术指南 这本书也很推荐,对区块链技术讲解得非常全面 2.csdn的blockchain知识库 老牌站CSDN论坛里没有区块链的板块,不过知识库还是有些值得一看的 3.区块链可应用场景 应用场景探讨,只是简述,欢迎讨论 4.汪晓明对区块链、以太坊的思考 小明说还是做得很不错的一个系列专题 5.侧链技术介绍 侧链是区块链技术里很重要的一个分支技术,这篇讲得很详细了 6.Quorum(NRW)算法机制简介 这篇其实是一个引申,更多是关于分布式存储算法的 7.PoS的设计思想 PoS也是很重要的一种共识算法,原理讲解,当中还有对共识算法的一些基础内容 8.分布式日志系统 另一篇引申,分布式日志 9.非对称加密基本概念 区块链中非对称加密是一个重要的基础概念 技术操作向:一、以太坊 1.ethereum white paper不多说,白皮书 2.以太坊黄皮书 黄皮书更多偏向技术,英文 3.以太坊常见问题 可以看做是官方FAQ了,英文 4.Solidity语言 Solidity语言的文档,英文 5.理解以太坊Serenity 以太坊第四阶段介绍,虽然还有点遥远 6.Vlad谈Casper共识协议 Casper其实就是以太坊想要转的PoS,简介 7.以太坊智能合约编程之菜鸟教程 8.通过truffle部署以太坊智能合约 9.Truffle 3.0部署智能合约至Ethereum节点 10.以太坊智能合约编写实例 11.以太坊智能合约编写实例2 12.在CentOS6.5上搭建以太坊私有链 剩下几篇是实际操作向,有些可能因为发表时间不同会有些过时,不过也有一定的参考价值 二、Hyperledger fabric1.Hyperledger Fabric V1.0– 开发者快速入门 万达大佬写的开发者入门指南 2.Hyperledger 源码分析之 Fabric 源码分析,感兴趣研究的同学可以看看 3. Hyperledgerfablic 1.0 在centos7环境下的安装与部署和动态增加节点 4.fabric源码搭建 5.Hyperledger Fabric1.0架构概览 6.fabric中文文档 7.Ubuntu中使用 Docker 部署 HyperledgerFabric 剩下几篇也是操作向,同上 三、布萌1.布萌接入指南 布萌官方的指南有些模糊,个人整理了一版出来仅供参考 2.布萌API文档 3.布萌SDK文档 两个官方文档,其实官网就有,顺手列在这了:D 4.布萌PHP SDK 今天看到有朋友放出的PHP的SDK,膜拜大神 四、其他1.Nodejs开发加密货币 基于Ebookcoin(亿书币)的开发教程,有一定参考价值 2.创建自己的私有比特币测试链 比特币私链搭建教程 3.programmingblockchain in c# 用C#开发区块链教程,英文","link":"/2019/07/27/block-chain-in-out/"},{"title":"蓝屏编码含义","text":"蓝屏含义故障检查信息 ***STOP 0x0000001E(0xC0000005,0xFDE38AF9,0x0000001,0x7E8B0EB4) MODE_EXCEPTION_NOT_HANDLED ***其中错误的第一部分是停机码(Stop Code)也就是STOP 0x0000001E, 用于识别已发生错误的类型, 错误第二部分是被括号括起来的四个数字集, 表示随机的开发人员定义的参数(这个参数对于普通用户根本无法理解, 只有驱动程序编写者或者微软操作系统的开发人员才懂). 第三部分是错误名. 信息第一行通常用来识别生产错误的驱动程序或者设备. 这种信息多数很简洁, 但停机码可以作为搜索项在微软知识库和其他技术资料中使用 推荐操作蓝屏第二部分是推荐用户进行的操作信息. 有时, 推荐的操作仅仅是一般性的建议(比如: 到销售商网站查找BIOS的更新等); 有时, 也就是显示一条与当前问题相关的提示. 一般来说, 惟一的建议就是重启. 调试端口告诉用户内存转储映像是否写到磁盘商了, 使用内存转储映像可以确定发生问题的性质, 还会告诉用户调试信息是否被传到另一台电脑商, 以及使用了什么端口完成这次通讯. 不过, 这里的信息对于普通用户来说, 没有什么意义.有时保卫科可以顺利的查到是哪个生产小组的问题, 会在第一部分明确报告是哪个文件犯的错, 但常常它也只能查个大概范围, 而无法明确指明问题所在. 由于工厂全面被迫停止, 只有重新整顿开工, 有时, 那个生产小组会意识到错误 , 不再重犯. 但有时仍然会试图哄抢零件, 于是厂领导不得不重复停工决定(不能启动并显示蓝屏信息, 或在进行相同操作时再次出现蓝屏). 蓝屏的处理方法Windows 2K/XP蓝屏信息非常多, 无法在一篇文章中全面讲解, 但他们产生的原因往往集中在不兼容的硬件和驱动程序、有问题的软件、病毒等, 因此首先为大家提供了一些常规的解决方案, 在遇到蓝屏错误时, 应先对照这些方案进行排除. 重启有时只是某个程序或驱动程序一时犯错, 重启后他们会改过自新.(注意:此时参见7.查询停机码) 新硬件首先, 应该检查新硬件是否插牢, 这个被许多人忽视的问题往往会引发许多莫名其妙的故障. 如果确认没有问题, 将其拔下, 然后换个插槽试试, 并安装最新的驱动程序. 同时还应对照微软网站的硬件兼容类别检查一下硬件是否与操作系统兼容. 如果你的硬件没有在表中, 那么就得到硬件厂商网站进行查询, 或者拨打他们的咨询电话. 新驱动和新服务如果刚安装完某个硬件的新驱动, 或安装了某个软件, 而它又在系统服务中添加了相应项目(比如:杀毒软件、CPU降温软件、防火墙软件等), 在重启或使用中出现了蓝屏故障, 请到安全模式来卸载或禁用它们. 检查病毒比如冲击波和振荡波等病毒有时会导致Windows蓝屏死机, 因此查杀病毒必不可少. 同时一些木马间谍软件也会引发蓝屏, 所以最好再用相关工具进行扫描检查. 检查BIOS和硬件兼容性对于新装的电脑经常出现蓝屏问题, 应该检查并升级BIOS到最新版本, 同时关闭其中的内存相关项, 比如:缓存和映射. 另外, 还应该对照微软的硬件兼容列表检查自己的硬件. 还有就是, 如果主板BIOS无法支持大容量硬盘也会导致蓝屏, 需要对其进行升级. 小提示:BIOS的缓存和映射项Video BIOS Shadowing (视频BIOS映射)Shadowing address ranges(映射地址列)System BIOS Cacheable(系统BIOS缓冲)Video BIOS Cacheable(视频BIOS缓冲)Video RAM Cacheable(视频内存缓冲) 检查系统日志在开始–>菜单中输入:EventVwr.msc, 回车出现”事件查看器”, 注意检查其中的”系统日志”和”应用程序日志”中表明”错误”的项. 查询停机码把蓝屏中密密麻麻的E文记下来, 接着到其他电脑中上网, 进入微软帮助与支持网站http://support.microsoft.com, 在左上角的”搜索(知识库)”中输入停机码, 如果搜索结果没有适合信息, 可以选择”英文知识库”在搜索一遍. 一般情况下, 会在这里找到有用的解决案例. 另外, 在baidu、Google等搜索引擎中使用蓝屏的停机码或者后面的说明文字为关键词搜索, 往往也会有以外的收获. 最后一次正确配置一般情况下, 蓝屏都出现于更新了硬件驱动或新加硬件并安装其驱动后, 这时Windows 2K/XP提供的”最后一次正确配置”就是解决蓝屏的快捷方式. 重启系统, 在出现启动菜单时按下F8键就会出现高级启动选项菜单, 接着选择”最后一次正确配置”. 安装最新的系统补丁和Service Pack有些蓝屏是Windows本身存在缺陷造成的, 应此可通过安装最新的系统补丁和Service Pack来解决. 蓝屏代码含义和解决方案0x0000000A:IRQL_NOT_LESS_OR_EQUAL错误分析主要是由问题的驱动程序、有缺陷或不兼容的硬件与软件造成的. 从技术角度讲. 表明在内核模式中存在以太高的进程内部请求级别(IRQL)访问其没有权限访问的内存地址. 解决方案请用前面介绍的解决方案中的2、3、5、8、9方案尝试排除. 0x00000012:TRAP_CAUSE_UNKNOWN错误分析如果遇到这个错误信息, 那么很不幸, 应为KeBudCheck分析的结果是错误原因未知. 解决方案既然微软都帮不上忙, 就得靠自己了, 请仔细回想这个错误是什么时候出现的; 第一次发生时你对系统做了哪些操作; 发生时正在进行什么操作. 从这些信息中找出可能的原因, 从而选择相应解决方案尝试排除. 0x0000001A:MEMORY_MANAGEMENT错误分析这个内存管理错误往往是由硬件引起的, 比如: 新安装的硬件、内存本身有问题等. 解决方案如果是在安装Windows时出现, 有可能是由于你的电脑达不到安装Windows的最小内存和磁盘要求. 0x0000001E:KMODE_EXCEPTION_NOT_HANDLED错误分析Windows内核检查到一个非法或者未知的进程指令, 这个停机码一般是由问题的内存或是与前面0x0000000A相似的原因造成的. 解决方案 硬件兼容有问题:请对照前面提到的最新硬件兼容性列表, 查看所有硬件是否包含在该列表中. 有问题的设备驱动、系统服务或内存冲突和中断冲突: 如果在蓝屏信息中出现了驱动程序的名字, 请试着在安装模式或者故障恢复控制台中禁用或删除驱动程序, 并禁用所有刚安装的驱动和软件. 如果错误出现在系统启动过程中, 请进入安全模式, 将蓝屏信息中所标明的文件重命名或者删除. 如果错误信息中明确指出Win32K.sys: 很有可能是第三方远程控制软件造成的, 需要从故障恢复控制台中将对该软件的服务关闭. 在安装Windows后第一次重启时出现:最大嫌疑可能时系统分区的磁盘空间不足或BIOS兼容有问题. 如果是在关闭某个软件时出现的:很有可能时软件本省存在设计缺陷, 请升级或卸载它. 0x00000023:FAT_FILE_SYSTEM、0x00000024:NTFS_FILE_SYSTEM错误分析0x00000023通常发生在读写FAT16或者FAT32文件系统的系统分区时, 而0x00000024则是由于NTFS.sys文件出现错误(这个驱动文件的作用是容许系统读写使用NTFS文件系统的磁盘). 这两个蓝屏错误很有可能是磁盘本身存在物理损坏, 或是中断要求封包(IRP)损坏而导致的. 其他原因还包括:硬盘磁盘碎片过多; 文件读写操作过于频繁, 并且数据量非常达或者是由于一些磁盘镜像软件或杀毒软件引起的. 解决方案 首先打开命令行提示符, 运行”Chkdsk /r”(注:不是CHKDISK, 感觉象这个, 但是……)命令检查并修复硬盘错误, 如果报告存在怀道(Bad Track), 请使用硬盘厂商提供的检查工具进行检查和修复. 接着禁用所有即使扫描文件的软件, 比如:杀毒软件、防火墙或备份工具. 右击C:\\winnt\\system32\\drivers\\fastfat.sys文件并选择”属性”, 查看其版本是否与当前系统所使用的Windows版本相符.(注:如果是XP, 应该是C:\\windows\\system32\\drivers\\fastfat.sys) 安装最新的主板驱动程序, 特别IDE驱动. 如果你的光驱、可移动存储器也提供有驱动程序, 最好将它们升级至最新版. 0x00000027:RDR_FILE_SYSTEM错误分析这个错误产生的原因很难判断, 不过Windows内存管理出了问题很可能会导致这个停机码的出现. 解决方案如果是内存管理的缘故, 通常增加内存会解决问题. 0x0000002EATA_BUS_ERROR错误分析系统内存存储器奇偶校验产生错误, 通常是因为有缺陷的内存(包括物理内存、二级缓存或者显卡显存)时设备驱动程序访问不存在的内存地址等原因引起的. 另外, 硬盘被病毒或者其他问题所损伤, 以出现这个停机码. 解决方案 检查病毒 使用”chkdsk /r”命令检查所有磁盘分区. 用Memtest86等内存测试软件检查内存. 检查硬件是否正确安装, 比如:是否牢固、金手指是否有污渍. 0x00000035:NO_MORE_IRP_STACK_LOCATIONS错误分析从字面上理解, 应该时驱动程序或某些软件出现堆栈问题. 其实这个故障的真正原因应该时驱动程序本省存在问题, 或是内存有质量问题. 解决方案请使用前面介绍的常规解决方案中与驱动程序和内存相关的方案进行排除. 0x0000003F:NO_MORE_SYSTEM_PTES错误分析一个与系统内存管理相关的错误, 比如:由于执行了大量的输入/输出操作, 造成内存管理出现问题: 有缺陷的驱动程序不正确地使用内存资源; 某个应用程序(比如:备份软件)被分配了大量的内核内存等. 解决方案卸载所有最新安装的软件(特别是哪些增强磁盘性能的应用程序和杀毒软件)和驱动程序. 0x00000044:MULTIPLE_IRP_COMPLIETE_REQUESTS错误分析通常是由硬件驱动程序引起的. 解决方案卸载最近安装的驱动程序. 这个故障很少出现, 目前已经知道的是, 在使用www.in-system.com这家公司的某些软件时会出现, 其中的罪魁就是Falstaff.sys文件.(作者难道不怕吃官司嘛, 把公司网址公布) 0x00000050: PAGE_FAULT_IN_NONPAGED+AREA错误分析有问题的内存(包括屋里内存、二级缓存、显存)、不兼容的软件(主要是远程控制和杀毒软件)、损坏的NTFS卷以及有问题的硬件(比如: PCI插卡本身已损坏)等都会引发这个错误. 解决方案请使用前面介绍的常规解决方案中与内存、软件、硬件、硬盘等相关的方案进行排除. 0x00000051:REGISTRY_ERROR错误分析这个停机码说明注册表或系统配置管理器出现错误, 由于硬盘本身有物理损坏或文件系统存在问题, 从而造成在读取注册文件时出现输入/输出错误. 解决方案使用”chkdsk /r”检查并修复磁盘错误. 0x00000058:FTDISK_INTERNAL_ERROR错误分析说明在容错集的主驱动发生错误. 解决方案首先尝试重启电脑看是否能解决问题, 如果不行, 则尝试”最后一次正确配置”进行解决. 0x0000005E:CRITICAL_SERVICE_FAILED错误分析某个非常重要的系统服务启动识别造成的. 解决方案如果是在安装了某个新硬件后出新的, 可以先移除该硬件, 并通过网上列表检查它是否与Windows 2K/XP兼容, 接着启动电脑, 如果蓝屏还是出现, 请使用”最后一次正确配置”来启动Windows, 如果这样还是失败, 建议进行修复安装或是重装. 0x0000006F:SESSION3_INITIALIZATION-FAILED错误分析这个错误通常出现在Windows启动时, 一般是由有问题的驱动程序或损坏的系统文件引起的. 解决方案建议使用Windows安装光盘对系统进行修复安装. 0x00000076ROCESS_HAS_LOCKED_PAGES错误分析通常是因为某个驱动程序在完成了一次输入/输出操作后, 没有正确释放所占有的内存 解决方案 点击开始–>运行:regedt32, 找到[HKLM\\SYSTEM\\Currentcontrol set\\control\\session manager\\memory management], 在右侧新建双字节值”TrackLockedPages”, 值为1. 这样Windows便会在错误再次出现时跟踪到是哪个驱动程序的问题. 如果再次出现蓝屏, 那么错误信息会变成:STOP:0x0000000CB(0xY,0xY,0xY,0xY)DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS其中第四个”0xY”会显示为问题驱动程序的名字, 接着对其进行更新或删除. 进入注册表, 删除添加的”TrackLockedPages”. 0x00000077:KERNEL_STACK_INPAGE_ERROR错误分析说明需要使用的内核数据没有在虚拟内存或物理内存中找到. 这个错误常常于是着磁盘有问题, 相应数据损坏或受到病毒侵蚀. 解决方案使用杀毒软件扫描系统; 使用”chkdsk /r”命令检查并修复磁盘错误, 如不行则使用磁盘厂商提供的工具检查修复. 0x0000007A:KERNEL_DATA_INPAGE_ERROR错误分析这个错误往往是虚拟内存中的内核数据无法读入内存造成的. 原因可能是虚拟内存页面文件中存在坏簇、病毒、磁盘控制器出错、内存有问题. 解决方案首先用升级为最新病毒库杀毒软件查杀病毒, 如果促无信息中还0xC000009C或0xC000016A代码, 那么表示是坏簇造成的, 并且系统的磁盘检测工具无法自动修复, 这时要进入”故障恢复控制台”, 用”chkdsk /r”命令进行手动修复. 0x0000007B:INACESSIBLE_BOOT_DEVICE错误分析Windows在启动过程中无法访问系统分区或启动卷. 一般发生在更换主板后第一次启动时, 主要是因为新主板和旧主板的IDE控制器使用了不同芯片组造成的. 有时也可能是病毒或硬盘损伤所引起的. 解决方案一般只要用安装光盘启动电脑, 然后执行修复安装即可解决问题. 对于病毒则可使用DOS版的杀毒软件进行查杀(主战有kv2005DOS版下载). 如果是硬盘本身存在问题, 请将其安装到其他电脑中, 然后使用”chkdsk /r”来检查并修复磁盘错误. 0x0000007E:SYSTEM_THREAD_EXCEPTION_NOT_HANDLED错误分析系统进程产生错误, 但Windows错误处理器无法捕获. 其产生原因很多, 包括:硬件兼容性、有问题的驱动程序或系统服务、 或者是某些软件. 解决方案请使用”事件查看器”来获取更多的信息, 从中发现错误根源.(发现好像不是解决哦, 看来这里大家要自力更生了!) 0x0000007F:UNEXPECTED_KERNEL_MOED_TRAP错误分析一般是由于有问题的硬件(比如:内存)或某些软件引起的. 有时超频也会产生这个错误. 解决方案用检测软件(比如:Memtest86)检查内存, 如果进行了超频, 请取消超频. 将PCI硬件插卡从主板插槽拔下来, 或更换插槽. 另外, 有些主板(比如:nForce2主板)在进行超频后, 南桥芯片过热也会导致蓝屏, 此时为该芯片单独增加散热片往往可以有效解决问题. 0x00000080:NMI_HARDWARE_FAILURE错误分析通常是有硬件引起的.(似乎蓝屏与硬件错误有不解之缘) 解决方案如果最近安装了新硬件, 请将其移除, 然后试试更换插槽和安装最新的驱动程序, 如果升级了驱动程序, 请恢复后原来的版本; 检查内存金手指是否有污染和损坏; 扫描病毒; 运行”chkdsk /r”检查并修复磁盘错误; 检查所有硬件插卡已经插牢. 如果以上尝试都无效果, 就得找专业的电脑维修公司请求帮助了. 0x0000008E:KERNEL_MODE_EXCEPTION_NOT_HANDLED错误分析内核级应用程序产生了错误, 但Windows错误处理器没有捕获. 通常是硬件兼容性错误. 解决方案升级驱动程序或升级BIOS. 0x0000009C:MACHINE_CHECK_EXCEPTION错误分析通常是硬件引起的. 一般是因为超频或是硬件存在问题(内存、CPU、总线、电源). 解决方案如果进行了超频, 请降会CPU原来频率, 检查硬件. 0x0000009FRIVER_POWER_STATE_FAILURE错误分析往往与电源有关系, 常常发生在与电源相关的操作, 比如:关机、待机或休睡. 解决方案重装系统, 如果不能解决, 请更换电源. 0x000000A5:ACPI_BIOS_ERROR错误分析通常是因为主板BIOS不能全面支持ACPI规范. 解决方案如果没有相应BIOS升级, 那么可在安装Windows 2K/XP时, 当出现”press F6 if you need to install a third-party SCSI or RAID driver”提示时, 按下F7键, 这样Windows便会自动禁止安装ACPI HAL, 而安装 Standard PC HAL. 0x000000B4:VIDEO_DRIVER_INIT_FAILURE错误分析这个停止信息表示Windows因为不能启动显卡驱动, 从而无法进入图形界面. 通常是显卡的问题, 或者是存在与显卡的硬件冲突(比如:与并行或串行端口冲突). 解决方案进入安全模式查看问题是否解决, 如果可以, 请升级最新的显卡驱动程序, 如果还不行, 则很可能是显卡与并行端口存在冲突, 需要在安全模式按下WIN+break组合键打开”系统属性”, 在硬件–>设备管理器中找到并双击连接打印的LPT1端口的项, 在”资源”选项卡中取消”使用自动配置”的构选, 然后将”输入/输出范围”的”03BC”改为”0378”. 0x000000BE:ATTEMPTED_WRITE_TO_READONLY_MEMORY错误分析某个驱动程序试图向只读内存写入数据造成的. 通常是在安装了新的驱动程序, 系统服务或升级了设备的固件程序后. 解决方案如果在错误信息中包含有驱动程序或者服务文件名称, 请根据这个信息将新安装的驱动程序或软件卸载或禁用. 0x000000C2:BAD_POOL_CALLER错误分析一个内核层的进程或驱动程序错误地试图进入内存操作. 通常是驱动程序或存在BUG的软件造成的. 解决方案请参考前面介绍的常规解决方案相关项目进行排除. 0x000000CERIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS错误分析通常是由有问题的驱动程序或系统服务造成的. 解决方案请参考前面介绍的常规解决方案相关项目进行排除. 0x000000D1RIVER_IRQL_NOT_LESS_OR_EQUAL错误分析通常是由有问题的驱动程序引起的(比如罗技鼠标的Logitech MouseWare 9.10和9.24版驱动程序会引发这个故障). 同时,有缺陷的内存、 损坏的虚拟内存文件、 某些软件(比如多媒体软件、杀毒软件、备份软件、DVD播放软件)等也会导致这个错误. 解决方案检查最新安装或升级的驱动程序(如果蓝屏中出现”acpi.sys”等类似文件名, 可以非常肯定时驱动程序问题)和软件; 测试内存是否存在问题; 进入”故障恢复控制台”, 转到虚拟内存页面文件Pagefile.sys所在分区, 执行”del pagefile.sys”命令, 将页面文件删除; 然后在页面文件所在分区执行”chkdsk /r”命令;进入Windows后重新设置虚拟内存.如果在上网时遇到这个蓝屏, 而你恰恰又在进行大量的数据下载和上传(比如:网络游戏、BT下载), 那么应该是网卡驱动的问题, 需要升级其驱动程序. 0x000000EA:THREAD_STUCK_IN_DEVICE_DRIVER错误分析通常是由显卡或显卡驱动程序引发的. 解决方案先升级最新的显卡驱动, 如果不行, 则需要更换显卡测试故障是否依然发生. 0x000000ED:UNMOUNTABLE_BOOT_VOLUME错误分析一般是由于磁盘存在错误导致的, 有时也建议检查硬盘连线是否接触不良, 或是没有使用合乎该硬盘传输规格的连接线, 例如ATA-100仍使用ATA-33的连接线, 对低速硬盘无所谓, 但告诉硬盘(支持ATA-66以上)的要求较严格, 规格不对的连线有时也会引起这类没办法开机的故障. 如果在修复后, 还是经常出现这个错误, 很可能是硬盘损坏的前兆. 解决方案一般情况下, 重启会解决问题, 不管怎么样都建议执行”chkdsk /r”命令来检查修复硬盘 0x000000F2:HARDWARE)INTERRUPT_STORM错误分析内核层检查到系统出现中断风暴, 比如:某个设备在完成操作后没有释放所占用的中断. 通常这是由缺陷的驱动程序造成的. 解决方案升级或卸载最新安装的硬件驱动程序. 0x00000135:UNABLE_TO_LOCATE_DLL错误分析通常表示某个文件丢失或已经损坏, 或者是注册表出现错误. 解决方案如果是文件丢失或损坏, 在蓝屏信息中通常会显示相应的文件名, 你可以通过网络或是其他电脑找到相应的文件, 并将其复制到系统文件夹下的SYSTEM32子文件夹中. 如果没有显示文件名, 那就很有可能是注册表损坏, 请利用系统还原或是以前的注册表备份进行恢复. 0x0000021A:STATUS_SYSTEM_PROCESS_TERMINATED错误分析用户模式子系统, 例如Winlogon或客服服务运行时子系统(CSRSS)已损坏, 所以无法再保证安全性, 导致系统无法启动. 有时, 当系统管理员错误地修改了用户帐号权限, 导致其无法访问系统文件和文件夹. 解决方案使用”最后一次正确的配置”, 如果无效, 可使用安装光盘进行修复安装. STOP 0xC0000221 or STATUS_IMAGE_CHECKSUM_MISMATCH错误分析通常是由于驱动程序或系统DLL文件损坏造成的. 一般情况下, 在蓝屏中会出现文件名称 解决方案 使用Windows安装光盘进行修复安装; 如果还能进入安全模式, 可以”开始–>运行”: sfc /scannow 还可以采用提取文件的方法来解决, 进入”故障恢复控制台”, 使用copy或expand命令从光盘中复制或解压受损的文件. 不过, 蓝屏一般都是驱动程序文件的问题, 所以expand命令会用的都一些, 比如:蓝屏中提示tdi.sys文件, 因为驱动文件一般在i386\\driver压缩包里, 所以使用: expand %CDROM:\\i386\\driver.cab \\f:tdi.sys c:\\winnt\\system\\drivers.(xp为expand %CDROM:\\i386\\driver.cab \\f:tdi.sys c:\\windowns\\system\\drivers) 如果启动时出现这些蓝屏停机码如果在Windows启动时出现蓝屏, 并出现附表一中的错误信息, 那么多半时硬件出现了问题, 请用硬件厂商提供的诊断工具来判断硬件是否存在问题, 并到其网站查看是否有最新的BIOS或固件更新程序. 如果硬件没有问题, 重装Windows 2K/XP, 若相同问题还是出现, 就只能求助专业的技术支持了. 如果遇到的时附表二中的错误信息, 也只有重装Windows了, 如果不能解决问题, 建议求救专业的技术支持。 友情链接:https://jingyan.baidu.com/article/fec4bce22b9569f2618d8b96.html 蓝屏编码表 编码 含义 0X0000000 操作完成 0X0000001 不正确的函数 0X0000002 系统找不到指定的文件 0X0000003 系统找不到指定的路径 0X0000004 系统无法打开文件 0X0000005 拒绝存取 0X0000006 无效的代码 0X0000007 内存控制模块已损坏 0X0000008 内存空间不足,无法处理这个指令 0X0000009 内存控制模块位址无效 0X000000A 环境不正确 0X000000B 尝试载入一个格式错误的程序 0X000000C 存取码错误 0X000000D 资料错误 0X000000E 内存空间不够,无法完成这项操作 0X000000F 系统找不到指定的硬盘 0X0000010 无法移除目录 0X0000011 系统无法将文件移到其他的硬盘 0X0000012 没有任何文件 0X0000019 找不到指定扇区或磁道 0X000001A 指定的磁盘或磁片无法存取 0X000001B 磁盘找不到要求的装置 0X000001C 打印机没有纸 0X000001D 系统无法将资料写入指定的磁盘 0X000001E 系统无法读取指定的装置 0X000001F 连接到系统的某个装置没有作用 0X0000021 文件的一部分被锁定,现在无法存取 0X0000024 开启的分享文件数量太多 0X0000026 到达文件结尾 0X0000027 磁盘已满 0X0000036 网络繁忙 0X000003B 网络发生意外的错误 0X0000043 网络名称找不到 0X0000050 文件已经存在 0X0000052 无法建立目录或文件 0X0000053 INT24失败(什麼意思?还请高手指点一二) 0X000006B 因为代用的磁盘尚未插入,所以程序已经停止 0X000006C 磁盘正在使用中或被锁定 0X000006F 文件名太长 0X0000070 硬盘空间不足 0X000007F 找不到指定的程序 0X000045B 系统正在关机 0X000045C 无法中止系统关机,因为没有关机的动作在进行中 0X000046A 可用服务器储存空间不足 0X0000475 系统BIOS无法变更系统电源状态 0X000047E 指定的程序需要新的windows版本 0X000047F 指定的程序不是windwos或ms-dos程序 0X0000480 指定的程序已经启动,无法再启动一次 0X0000481 指定的程序是为旧版的 windows所写的 0X0000482 执行此应用程序所需的程序库文件之一被损 0X0000483 没有应用程序与此项操作的指定文件建立关联 0X0000484 传送指令到应用程序无效 0X00005A2 指定的装置名称无效 0X00005AA 系统资源不足,无法完成所要求的服务 0X00005AB 系统资源不足,无法完成所要求的服务 0X00005AC 系统资源不足,无法完成所要求的服务 110 0x006E 系统无法开启指定的 装置或档案。 111 0x006F 档名太长。 112 0x0070 磁碟空间不足。 113 0x0071 没有可用的内部档案识别字。 114 0x0072 目标内部档案识别字不正确。 117 0x0075 由应用程式所执行的 IOCTL 呼叫 不正确。 118 0x0076 写入验证参数值不正确。 119 0x0077 系统不支援所要求的指令。 120 0x0078 此项功能仅在 Win32 模式有效。 121 0x0079 semaphore 超过逾时期间。 122 0x007A 传到系统呼叫的资料区域 太小。 123 0x007B 档名、目录名称或储存体标?***语法错?***。 124 0x007C 系统呼叫层次不正确。 125 0x007D 磁碟没有设定标?***。 126 0x007E 找不到指定的模组。 127 0x007F 找不到指定的程序。 128 0x0080 没有子行程可供等待。 129 0x0081 %1 这个应用程式无法在 Win32 模式下执行。 130 0x0082 Attempt to use a file handle to an open disk partition for an operation other than raw disk I/O. 131 0x0083 尝试将档案指标移至档案开头之前。 132 0x0084 无法在指定的装置或档案,设定档案指标。 133 0x0085 JOIN 或 SUBST 指令 无法用於 内含事先结合过的磁碟机。 134 0x0086 尝试在已经结合的磁碟机,使用 JOIN 或 SUBST 指令。 135 0x0087 尝试在已经替换的磁碟机,使 用 JOIN 或 SUBST 指令。 136 0x0088 系统尝试删除 未连结过的磁碟机的连结关系。 137 0x0089 系统尝试删除 未替换过的磁碟机的替换关系。 138 0x008A 系统尝试将磁碟机结合到已经结合过之磁碟机的目录。 139 0x008B 系统尝试将磁碟机替换成已经替换过之磁碟机的目录。 140 0x008C 系统尝试将磁碟机替换成已经替换过之磁碟机的目录。 141 0x008D 系统尝试将磁碟机 SUBST 成已结合的磁碟机 目录。 142 0x008E 系统此刻无法执行 JOIN 或 SUBST。 143 0x008F 系统无法将磁碟机结合或替换同一磁碟机下目录。 144 0x0090 这个目录不是根目录的子目录。 145 0x0091 目录仍有资料。 146 0x0092 指定的路恕w经被替换过。 147 0x0093 资源不足,无法处理这项 指令。 148 0x0094 指定的路拿o时候无法使用。 149 0x0095 尝试要结合或替换的磁碟机目录,是已经替换过的的目标。 150 0x0096 CONFIG.SYS 档未指定系统追踪资讯,或是追踪功能被取消。 151 0x0097 指定的 semaphore事件 DosMuxSemWait 数目不正确。 152 0x0098 DosMuxSemWait 没有执行;设定太多的 semaphore。 153 0x0099 DosMuxSemWait 清单不正确。 154 0x009A 您所输入的储存媒体标 元长度限制。 155 0x009B 无法建立其他的执行绪。 156 0x009C 接收行程拒绝接受信号。 157 0x009D 区段已经被舍弃,无法被锁定。 158 0x009E 区段已经解除锁定。 159 0x009F 执行绪识别码的位址不正确。 160 0x00A0 传到 DosExecPgm 的引数字串不正确。 161 0x00A1 指定的路恕ㄔ萧T。 162 0x00A2 信号等候处理。 164 0x00A4 系统无法建立执行绪。 167 0x00A7 无法锁定档案的部份范围。 170 0x00AA 所要求的资源正在使用中。 173 0x00AD 取消范围的锁定要求不明显。 174 0x00AE 档案系统不支援自动变更锁定类型。 180 0x00B4 系统发现不正确的区段号码。 182 0x00B6 作业系统无法执行 %1。 183 0x00B7 档案已存在,无法建立同一档案。 186 0x00BA 传送的旗号错?***。 187 0x00BB 指定的系统旗号找不到。 188 0x00BC 作业系统无法执行 %1。 189 0x00BD 作业系统无法执行 %1。 190 0x00BE 作业系统无法执行 %1。 191 0x00BF 无法在 Win32 模式下执行 %1。 192 0x00C0 作业系统无法执行 %1。 193 0x00C1 %1 不是正确的 Win32 应用程式。 194 0x00C2 作业系统无法执行 %1。 195 0x00C3 作业系统无法执行 %1。 196 0x00C4 作业系统无法执行 这个应用程式。 197 0x00C5 作业系统目前无法执行 这个应用程式。 198 0x00C6 作业系统无法执行 %1。 199 0x00C7 作业系统无法执行 这个应用程式。 200 0x00C8 程式码的区段不可以大於或等於 64KB。 201 0x00C9 作业系统无法执行 %1。 202 0x00CA 作业系统无法执行 %1。 203 0x00CB 系统找不到输入的环境选项。 205 0x00CD 在指令子目录下,没有任何行程有信号副处理程式。 206 0x00CE 档案名称或副档名太长。 207 0x00CF ring 2 堆衬洏峇丑C 208 0x00D0 输入的通用档名字元 * 或 ? 不正确, 或指定太多的通用档名字元。 209 0x00D1 所传送的信号不正确。 210 0x00D2 无法设定信号处理程式。 212 0x00D4 区段被锁定,而且无法重新配置。 214 0x00D6 附加到此程式或动态连结模组的动态连结模组太多。 215 0x00D7 Can’t nest calls to LoadModule. 230 0x00E6 The pipe state is inv 231 0x00E7 所有的 pipe instances 都在忙碌中。 232 0x00E8 The pipe is being closed. 233 0x00E9 No process is on the other end of the pipe. 234 0x00EA 有更多可用的资料。 240 0x00F0 作业阶段被取消。 254 0x00FE 指定的延伸属性名称无效。 255 0x00FF 延伸的属性不一致。 259 0x0103 没有可用的资料。 266 0x010A 无法使用 Copy API。 267 0x010B 目录名称错?***。 275 0x0113 延伸属性不适用於缓冲区。 276 0x0114 在外挂的档案系统上的延伸属性档案已经毁损。 277 0x0115 延伸属性表格档满。 278 0x0116 指定的延伸属性代码无效。 282 0x011A 外挂的这个档案系统不支援延伸属性。 288 0x0120 意图释放不属於叫用者的 mutex。 298 0x012A semaphore 传送次数过多。 299 0x012B 只完成 Read/WriteProcessMemory 的部份要求。 317 0x013D 系统找不到位於讯息档 %2 中编号为 0x%1 的讯息。 487 0x01E7 尝试存取无效的位址。 534 0x0216 运算结果超过 32 位元。 535 0x0217 通道的另一端有一个行程在接送资料。 536 0x0218 等候行程来开启通道的另一端。 994 0x03E2 存取延伸的属性被拒。 995 0x03E3 由於执行绪结束或应用程式要求,而异常终止 I/O 作业。 996 0x03E4 重读?I/O 事件不是设定成通知状态。 997 0x03E5 正在处理重读?I/O 作业。 998 0x03E6 对记忆体位置的无效存取。 999 0x03E7 执行 inpage 作业发生错?***。","link":"/2019/01/28/blue-screen-coding-table/"},{"title":"基于Java语言构建区块链(一)—— 基本原型","text":"引言区块链技术是一项比人工智能更具革命性的技术,人工智能只是提高了人类的生产力,而区块链则将改变人类社会的生产关系,它将会颠覆我们人类社会现有的协作方式。了解和掌握区块链相关知识和技术,是我们每位开发人员必须要去做的事情,这样我们才能把握住这波时代趋势的红利。本文将基于Java语言构建简化版的blockchain,来实现数字货币。 创建区块区块链是由包含交易信息的区块从后向前有序链接起来的数据结构。区块被从后向前有序地链接在这个链条里,每个区块都指向前一个区块。以比特币为例,每个区块主要包含如下信息字段: 区块大小:用字节表示的区块数据大小 区块头:组成区块头的几个字段 区块头hash值 父区块头hash值 时间戳:区块产生的近似时间 Merkle根:该区块中交易的merkle树根的哈希值 难度目标:该区块工作量证明算法的难度目标 Nonce:用于工作量证明算法的计数器 交易计数器:交易的数量 交易:记录在区块里的交易信息 详见:《精通比特币》(第二版)第9章——区块链 在这里,我们主要是为了实现最简单的区块链结构,仅仅包含以下几个信息字段: 12345678910111213141516171819202122232425262728293031323334353637/** * 区块 * * @author wangwei * @date 2018/02/02 */@Datapublic class Block { /** * 区块hash值 */ private String hash; /** * 前一个区块的hash值 */ private String previousHash; /** * 区块数据 */ private String data; /** * 区块创建时间(单位:秒) */ private long timeStamp; public Block() { } public Block(String hash, String previousHash, String data, long timeStamp) { this(); this.hash = hash; this.previousHash = previousHash; this.data = data; this.timeStamp = timeStamp; }} 区块Hash值计算加密Hash值,一个通过SHA256算法对区块头进行二次哈希计算而得到的数字指纹。Hash值用于确保blockchain的安全。Hash计算是计算敏感的操作,即使在高性能电脑也需要花费一段时间来完成计算(这也就是为什么人们购买高性能GPU进行比特币挖矿的原因)。blockchain架构设计有意使Hash计算变得困难,这样做是为了加大新增一个block的难度,进而防止block在增加后被随意修改。 123456789101112131415161718192021222324252627282930313233/** * <p> 创建新区块 </p> * * @param previousHash * @param data * @return */public static Block newBlock(String previousHash, String data) { Block block = new Block("", previousHash, data.getBytes(), Instant.now().getEpochSecond()); block.setHash(); return block;}/** * 计算区块Hash * <p> * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 * * @return */private void setHash() { byte[] prevBlockHashBytes = {}; if (StringUtils.isNoneBlank(this.getPrevBlockHash())) { prevBlockHashBytes = new BigInteger(this.getPrevBlockHash(), 16).toByteArray(); } byte[] headers = ByteUtils.merge( prevBlockHashBytes, this.getData().getBytes(), ByteUtils.toBytes(this.getTimeStamp())); this.setHash(DigestUtils.sha256Hex(headers));} 创建区块链区块链本质上是一种有序、反向链接链表的数据结构。这意味着,block按照插入的顺序存放,同时每个block都保存指向上一个block的链接。这种结构保证可以快速获取最新插入的block同时获取它的hash值。这种结构保证可以快速获取最新插入的block同时(高效地)获取它的hash值。 区块链数据结构123456789101112131415/** * <p> 区块链 </p> * * @author wangwei * @date 2018/02/02 */public class Blockchain { @Getter private List<Block> blockList; public Blockchain(List<Block> blockList) { this.blockList = blockList; }} 添加区块新增一个添加区块链的方法 123456789101112131415161718/** * <p> 添加区块 </p> * * @param data 数据 */public void addBlock(String data) { Block previousBlock = blockList.get(blockList.size() - 1); this.addBlock(Block.newBlock(previousBlock.getHash(), data));}/** * <p> 添加区块 </p> * * @param block 区块 */public void addBlock(Block block) { this.blockList.add(block);} 创世区块在添加区块之前,区块链必须有个创世区块,在Block中新增创世区块方法: 12345678/** * <p> 创建创世区块 </p> * * @return */public static Block newGenesisBlock() { return Block.newBlock("", "Genesis Block");} 创建区块链再在Blockchain中新增创建区块链的方法: 12345678910/** * <p> 创建区块链 </p> * * @return */public static Blockchain newBlockchain() { List<Block> blocks = new LinkedList<>(); blocks.add(Block.newGenesisBlock()); return new Blockchain(blocks);} 测试运行12345678910111213141516171819202122232425262728293031323334353637/** * 测试 * * @author wangwei * @date 2018/02/05 */public class BlockchainTest { public static void main(String[] args) { Blockchain blockchain = Blockchain.newBlockchain(); blockchain.addBlock("Send 1 BTC to Ivan"); blockchain.addBlock("Send 2 more BTC to Ivan"); for (Block block : blockchain.getBlockList()) { System.out.println("Prev. hash: " + block.getPreviousHash()); System.out.println("Data: " + block.getData()); System.out.println("Hash: " + block.getHash()); System.out.println(); } }}/** * 输出如下信息: */Prev. hash: Data: Genesis BlockHash: 4492cb9d396a9a52e7ff17ef3782f022ddcdc7b2c276bc6dd3d448b0655eb3d4Prev. hash: 4492cb9d396a9a52e7ff17ef3782f022ddcdc7b2c276bc6dd3d448b0655eb3d4Data: Send 1 BTC to IvanHash: cd716d59d98ad673035ab7035ece751718ea9842944a4743c298bebc0fe24c04Prev. hash: cd716d59d98ad673035ab7035ece751718ea9842944a4743c298bebc0fe24c04Data: Send 2 more BTC to IvanHash: 42f78d6a86f88aa9b5b10e468494dfd1b3f558a9fb74a01eb348c2cbfc5d000a 总结我们构建了一个非常简单的区块链原型:它只是一个块的数组,每个块都与前一个块有连接。 实际的区块链要复杂得多。 缺少交易信息:我们的区块链还没有任何交易信息。 缺少工作量证明:我们的生产区块非常简单快捷,实际的区块链中,生产一个区块需要进行大量的计算。 缺少共识机制:区块链是一个非单一决策者的分布式数据库。 因此,一个新的区块必须得到网络的其他参与者的确认和批 在以后的文章中,我们将介绍这些功能。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part1-Basic_Prototype https://jeiwan.cc/posts/building-blockchain-in-go-part-1/ 《精通比特币(第二版)》","link":"/2019/01/21/build-blockchain-in-java-base-prototype/"},{"title":"基于Java语言构建区块链(三)—— 持久化 & 命令行","text":"文章的主要思想和内容均来自:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/ 引言上一篇 文章我们实现了区块链的工作量证明机制(Pow),尽可能地实现了挖矿。但是距离真正的区块链应用还有很多重要的特性没有实现。今天我们来实现区块链数据的存储机制,将每次生成的区块链数据保存下来。有一点需要注意,区块链本质上是一款分布式的数据库,我们这里不实现”分布式”,只聚焦于数据存储部分。 数据库选择到目前为止,我们的实现机制中还没有区块存储这一环节,导致我们的区块每次生成之后都保存在了内存中。这样不便于我们重新使用区块链,每次都要从头开始生成区块,也不能够跟他人共享我们的区块链,因此,我们需要将其存储在磁盘上。 我们该选择哪一款数据库呢?事实上,在《比特币白皮书》中并没有明确指定使用哪一种的数据库,因此这个由开发人员自己决定。中本聪 开发的 Bitcoin Core 中使用的是LevelDB。原文 Building Blockchain in Go. Part 3: Persistence and CLI 中使用的是 BoltDB ,对Go语言支持比较好。 但是我们这里使用的是Java来实现,BoltDB不支持Java,这里我们选用 Rocksdb 。 当然也可以选择 LevelDB,非常不错的LevelDB介绍文章:https://mp.weixin.qq.com/s/rN6HX2VzsRi3_EKXYKuJAARocksDB是由Facebook数据库工程团队开发和维护的一款key-value存储引擎,比LevelDB性能更加强大,有关Rocksdb的详细介绍,请移步至官方文档:https://github.com/facebook/rocksdb ,这里不多做介绍。 数据结构在我们开始实现数据持久化之前,我们先要确定我们该如何去存储我们的数据。为此,我们先来看看比特币是怎么做的。 简单来讲,比特币使用了两个”buckets(桶)”来存储数据: blocks. 描述链上所有区块的元数据. chainstate. 存储区块链的状态,指的是当前所有的UTXO(未花费交易输出)以及一些元数据. “在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO。”详见:《精通比特币》第二版 第06章节 —— 交易的输入与输出 此外,每个区块数据都是以单独的文件形式存储在磁盘上。这样做是出于性能的考虑:当读取某一个单独的区块数据时,不需要加载所有的区块数据到内存中来。 在 blocks 这个桶中,存储的键值对: ‘b’ + 32-byte block hash -> block index record 区块的索引记录 ‘f’ + 4-byte file number -> file information record 文件信息记录 ‘l’ -> 4-byte file number: the last block file number used 最新的一个区块所使用的文件编码 ‘R’ -> 1-byte boolean: whether we’re in the process of reindexing 是否处于重建索引的进程当中 ‘F’ + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off 各种可以打开或关闭的flag标志 ‘t’ + 32-byte transaction hash -> transaction index record 交易索引记录在 chainstate 这个桶中,存储的键值对: ‘c’ + 32-byte transaction hash -> unspent transaction output record for that transaction 某笔交易的UTXO记录 ‘B’ -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs 数据库所表示的UTXO的区块Hash(抱歉,这一点我还没弄明白……) 由于我们还没有实现交易相关的特性,因此,我们这里只使用 block 桶。另外,前面提到过的,这里我们不会实现各个区块数据各自存储在独立的文件上,而是统一存放在一个文件里面。因此,我们不要存储和文件编码相关的数据,这样一来,我们所用到的键值对就简化为: 32-byte block-hash -> Block structure (serialized) 区块数据与区块hash的键值对 ‘l’ -> the hash of the last block in a chain 最新一个区块hash的键值对(查看更加详细的解释) 序列化RocksDB的Key与Value只能以byte[]的形式进行存储,这里我们需要用到序列化与反序列化库 Kryo,代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041package one.wangwei.blockchain.util;import com.esotericsoftware.kryo.Kryo;import com.esotericsoftware.kryo.io.Input;import com.esotericsoftware.kryo.io.Output;/** * 序列化工具类 * * @author wangwei * @date 2018/02/07 */public class SerializeUtils { /** * 反序列化 * * @param bytes 对象对应的字节数组 * @return */ public static Object deserialize(byte[] bytes) { Input input = new Input(bytes); Object obj = new Kryo().readClassAndObject(input); input.close(); return obj; } /** * 序列化 * * @param object 需要序列化的对象 * @return */ public static byte[] serialize(Object object) { Output output = new Output(4096, -1); new Kryo().writeClassAndObject(output, object); byte[] bytes = output.toBytes(); output.close(); return bytes; }} 持久化上面已经说过,我们这里使用RocksDB,我们先写一个相关的工具类RocksDBUtils,主要的功能如下: putLastBlockHash:保存最新一个区块的Hash值 getLastBlockHash:查询最新一个区块的Hash值 putBlock:保存区块 getBlock:查询区块 注意:BoltDB 支持 Bucket 的特性,而RocksDB 不支持,所以需要我们自己使用Map来做一个映射。 RocksDBUtils123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147package one.wangwei.blockchain.store;import com.google.common.collect.Maps;import one.wangwei.blockchain.block.Block;import one.wangwei.blockchain.util.SerializeUtils;import org.rocksdb.RocksDB;import org.rocksdb.RocksDBException;import java.util.Map;/** * 存储工具类 * * @author wangwei * @date 2018/02/27 */public class RocksDBUtils { /** * 区块链数据文件 */ private static final String DB_FILE = "blockchain.db"; /** * 区块桶前缀 */ private static final String BLOCKS_BUCKET_KEY = "blocks"; /** * 最新一个区块 */ private static final String LAST_BLOCK_KEY = "l"; private volatile static RocksDBUtils instance; public static RocksDBUtils getInstance() { if (instance == null) { synchronized (RocksDBUtils.class) { if (instance == null) { instance = new RocksDBUtils(); } } } return instance; } private RocksDB db; /** * block buckets */ private Map<String, byte[]> blocksBucket; private RocksDBUtils() { openDB(); initBlockBucket(); } /** * 打开数据库 */ private void openDB() { try { db = RocksDB.open(DB_FILE); } catch (RocksDBException e) { throw new RuntimeException("Fail to open db ! ", e); } } /** * 初始化 blocks 数据桶 */ private void initBlockBucket() { try { byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY); byte[] blockBucketBytes = db.get(blockBucketKey); if (blockBucketBytes != null) { blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes); } else { blocksBucket = Maps.newHashMap(); db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket)); } } catch (RocksDBException e) { throw new RuntimeException("Fail to init block bucket ! ", e); } } /** * 保存最新一个区块的Hash值 * * @param tipBlockHash */ public void putLastBlockHash(String tipBlockHash) { try { blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) { throw new RuntimeException("Fail to put last block hash ! ", e); } } /** * 查询最新一个区块的Hash值 * * @return */ public String getLastBlockHash() { byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY); if (lastBlockHashBytes != null) { return (String) SerializeUtils.deserialize(lastBlockHashBytes); } return ""; } /** * 保存区块 * * @param block */ public void putBlock(Block block) { try { blocksBucket.put(block.getHash(), SerializeUtils.serialize(block)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) { throw new RuntimeException("Fail to put block ! ", e); } } /** * 查询区块 * * @param blockHash * @return */ public Block getBlock(String blockHash) { return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash)); } /** * 关闭数据库 */ public void closeDB() { try { db.close(); } catch (Exception e) { throw new RuntimeException("Fail to close db ! ", e); } }} 创建区块链现在我们来优化 Blockchain.newBlockchain 接口的代码逻辑,改为如下逻辑: 代码如下: 123456789101112131415/** * <p> 创建区块链 </p> * * @return */public static Blockchain newBlockchain() throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (StringUtils.isBlank(lastBlockHash)) { Block genesisBlock = Block.newGenesisBlock(); lastBlockHash = genesisBlock.getHash(); RocksDBUtils.getInstance().putBlock(genesisBlock); RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash); } return new Blockchain(lastBlockHash);} 修改 Blockchain 的数据结构,只记录最新一个区块链的Hash值 123456789public class Blockchain { @Getter private String lastBlockHash; private Blockchain(String lastBlockHash) { this.lastBlockHash = lastBlockHash; }} 每次挖矿完成后,我们也需要将最新的区块信息保存下来,并且更新最新区块链Hash值: 1234567891011121314151617181920212223/** * <p> 添加区块 </p> * * @param data */public void addBlock(String data) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (StringUtils.isBlank(lastBlockHash)) { throw new Exception("Fail to add block into blockchain ! "); } this.addBlock(Block.newBlock(lastBlockHash, data));}/** * <p> 添加区块 </p> * * @param block */public void addBlock(Block block) throws Exception { RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); RocksDBUtils.getInstance().putBlock(block); this.lastBlockHash = block.getHash();} 到此,存储部分的功能就实现完毕,我们还缺少一个功能: 检索区块链现在,我们所有的区块都保存到了数据库,因此,我们能够重新打开已有的区块链并且向其添加新的区块。但这也导致我们再也无法打印出区块链中所有区块的信息,因为,我们没有将区块存储在数组当中。让我们来修复这个瑕疵! 我们在Blockchain中创建一个内部类 BlockchainIterator ,作为区块链的迭代器,通过区块之前的hash连接来依次迭代输出区块信息,代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class Blockchain { .... /** * 区块链迭代器 */ public class BlockchainIterator { private String currentBlockHash; public BlockchainIterator(String currentBlockHash) { this.currentBlockHash = currentBlockHash; } /** * 是否有下一个区块 * * @return */ public boolean hashNext() throws Exception { if (StringUtils.isBlank(currentBlockHash)) { return false; } Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); if (lastBlock == null) { return false; } // 创世区块直接放行 if (lastBlock.getPrevBlockHash().length() == 0) { return true; } return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null; } /** * 返回区块 * * @return */ public Block next() throws Exception { Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); if (currentBlock != null) { this.currentBlockHash = currentBlock.getPrevBlockHash(); return currentBlock; } return null; } } .... } 测试1234567891011121314151617181920212223242526272829303132333435363738/** * 测试 * * @author wangwei * @date 2018/02/05 */public class BlockchainTest { public static void main(String[] args) { try { Blockchain blockchain = Blockchain.newBlockchain(); blockchain.addBlock("Send 1.0 BTC to wangwei"); blockchain.addBlock("Send 2.5 more BTC to wangwei"); blockchain.addBlock("Send 3.5 more BTC to wangwei"); for (Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator(); iterator.hashNext(); ) { Block block = iterator.next(); if (block != null) { boolean validate = ProofOfWork.newProofOfWork(block).validate(); System.out.println(block.toString() + ", validate = " + validate); } } } catch (Exception e) { e.printStackTrace(); } }}/*输出*/Block{hash='0000012f87a0510dd0ee7048a6bd52db3002bae7d661126dc28287bd6c23189a', prevBlockHash='0000024b2c23c4fb06c2e2c1349275d415efe17a51db24cd4883da0067300ddf', data='Send 3.5 more BTC to wangwei', timeStamp=1519724875, nonce=369110}, validate = trueBlock{hash='0000024b2c23c4fb06c2e2c1349275d415efe17a51db24cd4883da0067300ddf', prevBlockHash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', data='Send 2.5 more BTC to wangwei', timeStamp=1519724872, nonce=896348}, validate = trueBlock{hash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', prevBlockHash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', data='Send 1.0 BTC to wangwei', timeStamp=1519724869, nonce=673955}, validate = trueBlock{hash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', prevBlockHash='', data='Genesis Block', timeStamp=1519724866, nonce=840247}, validate = true 命令行界面CLI 部分的内容,这里不做详细介绍,具体可以去查看文末的Github源码链接。大致步骤如下: 添加pom.xml配置 1234567891011121314151617181920212223242526272829303132333435363738394041424344<project> ... <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.4</version> </dependency> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>one.wangwei.blockchain.cli.Main</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <!-- this is used for inheritance merges --> <phase>package</phase> <!-- 指定在打包节点执行jar包合并操作 --> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> ... </project> 添加shell脚本 blockchain.sh 1234567891011#!/bin/bashset -e# Check if the jar has been built.if [ ! -e target/blockchain-java-jar-with-dependencies.jar ]; then echo "Compiling blockchain project to a JAR" mvn package -DskipTestsfijava -jar target/blockchain-java-jar-with-dependencies.jar "$@" 执行命令 12345678910111213# 进入工程根路劲$ cd blockchain-java# 打印帮助信息$ ./blockchain.sh -h # 添加区块$ ./blockchain.sh -add "Send 1.5 BTC to wangwei"$ ./blockchain.sh -add "Send 2.5 BTC to wangwei"$ ./blockchain.sh -add "Send 3.5 BTC to wangwei"# 打印区块链$ ./blockchain.sh -print 总结本篇我们实现了区块链的存储功能,接下来我们将实现地址、交易、钱包这一些列的功能。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part3-persistence https://jeiwan.cc/posts/building-blockchain-in-go-part-3/ 《精通比特币》第二版","link":"/2019/01/23/build-blockchain-in-java-data-persistence/"},{"title":"基于Java语言构建区块链(二)—— 工作量证明","text":"文章的主要思想和内容均来自 https://jeiwan.cc/posts/building-blockchain-in-go-part-2/ 引言在上一篇文章中,我们实现了区块链最基本的数据结构模型,添加区块以及和前一个区块连接在一起。但是,我们的实现方式非常简单,而真实的比特币区块链中,每一个区块的添加都是需要经过大量的计算才可以完成,这个过程就是我们熟知的挖矿。 工作量证明机制区块链最关键的一个思想就是,必须进行大量且困难的计算工作才能将交易数据存放到区块链上。这种工作机制才能保证整个区块链数据的安全性和一致性。同时,完成这个计算工作的矿工会得到相应的Token奖励。 这套机制和我们的现实生活非常相似:我们必须努力工作来赚取报酬用以维持我们的生活。在区块链中,网络中的矿工们努力工作来维持区块链网络,为其添加区块,并且获得一定的Token奖励。作为他们工作的成果,一个区块以安全的方式被组合进了区块链中,这样就保证了整个区块链数据库的稳定性。还有一个必须要注意的是,某个矿工完成了计算工作的结果,还必须得到其他所有矿工的认同(证明是正确的),这样才算完成。 这一整套的计算和证明机制,就称为Proof-of-Work(工作量证明)。计算工作是非常非常困难的,因为它需要消耗大量的计算机算力资源,即使是性能非常高的计算机都不能非常快地计算出正确的结果。此外,随着时间的推移,这项计算工作的难度也会随之增加,目的是为了保证每小时6个新区块的出块率。在比特币中,这种工作的目标是找到满足某个特定要求的区块Hash(哈希值)。这个区块哈希值就是工作结果的一个证明。因此,计算工作的目的就是为了寻找到这个证明值。 最后要注意的是,计算出这个特定的Hash(哈希值)是非常困难的,但是别人来验证这个Hash值是否正确的时候,是非常简单的,一下子就能完成。 Hashing Hash:哈希 | 散列 我们来讨论一下Hashing(哈希),对这一块非常熟悉的朋友可以直接跳过这一段内容。 哈希是一种计算机算法,该算法能够计算出任意大小数据的哈希值,并且这个哈希值的长度是固定的,256bit。这个被计算出来的哈希值能够作为这个数据的唯一代表。哈希算法有几个关键的特性: 不可逆性。不能根据一个哈希值推导出原始数据。所以,哈希不是加密。 唯一性。每个数据有且仅有一个唯一的哈希值。 迥异性。原始数据一丁点的变化都将得到完全不一样的哈希值。例如:12SHA256("wangwei1") ——> 1e898b7c9adaad86c20139a302ccd5277f81040cab68dc2aecfc684773532652SHA256("wangwei2") ——> c9cc7417c17318c8aab448cc8ace24c53b6dcf350f5c5fd8e91cbc3b011a179d 哈希算法被广泛用于验证文件的一致性上。比如软件提供商通常会在安装包上附加一个检验码(checksums),当我们下载完一个软件安装包后,可以用哈希函数计算一下这个软件安装包的哈希值,然后再和软件安装包的检验码做个对比,就可以知道下载的安装包是否完整、是否有数据丢失。 在区块链中,哈希值用于保证区块的一致性。每一个区块被用于进行哈希计算的数据,都包含前一个区块链的哈希值,因此任何人想要修改区块的数据几乎是不可能的,他必须要把整个区块链中从创世区块到最新的区块的所有哈希值全部重新计算一遍。 你可以脑补一下这个工作量有多大,按照目前计算机的算力来看,几乎不可能 Hashcash比特币的工作量证明是使用的是Hashcash算法,一种最初被用于反垃圾邮件的算法,它可以被拆解为以下几步: 获取某种公开可知的数据data(在邮件案例中,指的是收件人邮件地址;比特币案例中,指的是区块头) 添加一个计数器counter,初始值设置为0; 计算 data 与 counter拼接字符串的哈希值; 检查上一步的哈希值是否满足某个条件,满足则停止计算,不满足则 counter 加1,然后重复第3步和第4步,直到满足这个特定的条件为止。 这是一种粗暴的算法:你改变计数器,计算一个新的哈希值,检查它,增加计数器,计算一个新的哈希值,循环往复,这就是为什么它需要花费大量计算机算力资源的原因所在。 让我们来近距离看一下这个特定的条件指的是什么。在原始的Hashcash算法中,这个特殊的要求指的是计算出来的哈希值的前20bit必须全是零, 在比特币种,这种要求哈希值前面有多少个零打头的要求是随着时间的推移而不断调整的,这是出于设计的目的,尽管在计算机的算力会不断的提升和越来越多的矿工加入这个网络中的情况下,都要保证每10min生产一个区块。 我们演示一下这个算法, 1234567# 计算字符串'I like donuts'的哈希值SHA256("I like donuts") ——> f80867f6efd4484c23b0e7184e53fe4af6ab49b97f5293fcd50d5b2bfa73a4d0# 拼接一个计数器值(ca07ca),再次进行Hash计算SHA256("I like donutsca07ca") ——> 0000002f7c1fe31cb82acdc082cfec47620b7e4ab94f2bf9e096c436fc8cee06 这里的ca07ca是计数器值的十六进制,他表示的十进制值为13240266 即,从0开始,总共计算了13240266次,才计算出I like donuts这个数据的Hash值,满足前6位(3字节)全是零。 代码实现 思路: 1)每次区块被添加到区块链之前,先要进行挖矿(Pow) 2)挖矿过程中,产生的 Hash 值,如果小于难度目标值则添加进区块,否则继续挖矿,直到找到正确的Hash为止 3)最后,验证区块Hash是否有效 定义Pow类1234567891011121314151617181920212223242526272829303132333435363738394041/** * 工作量证明 * * @author wangwei * @date 2018/02/04 */@Datapublic class ProofOfWork { /** * 难度目标位 */ public static final int TARGET_BITS = 20; /** * 区块 */ private Block block; /** * 难度目标值 */ private BigInteger target; private ProofOfWork(Block block, BigInteger target) { this.block = block; this.target = target; } /** * 创建新的工作量证明,设定难度目标值 * <p> * 对1进行移位运算,将1向左移动 (256 - TARGET_BITS) 位,得到我们的难度目标值 * * @param block * @return */ public static ProofOfWork newProofOfWork(Block block) { BigInteger targetValue = BigInteger.valueOf(1).shiftLeft((256 - TARGET_BITS)); return new ProofOfWork(block, targetValue); }} 设定一个难度目标位TARGET_BITS,表示最终挖矿挖出来Hash值,转化为二进制后,与256相比,长度少了多少bit,也即二进制前面有多少bit是零. TARGET_BITS 越大,最终targetValue就越小,要求计算出来的Hash越来越小,也就是挖矿的难度越来越大。 我们这里的TARGET_BITS是固定的,但是在真实的比特币中,难度目标是随着时间的推推,会动态调整的。详见:《精通比特币 (第二版)》第10章 由于数值比较大,这里要使用BitInteger类型。 准备数据123456789101112131415161718192021/** * 准备数据 * <p> * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 * @param nonce * @return */private String prepareData(long nonce) { byte[] prevBlockHashBytes = {}; if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); } return ByteUtils.merge( prevBlockHashBytes, this.getBlock().getData().getBytes(), ByteUtils.toBytes(this.getBlock().getTimeStamp()), ByteUtils.toBytes(TARGET_BITS), ByteUtils.toBytes(nonce) );} 参与Hash运算的如下几个信息: 前一个区块(父区块)的Hash值; 区块中的交易数据; 区块生成的时间; 难度目标; 用于工作量证明算法的计数器 详见:《精通比特币 (第二版)》第09章 Pow算法123456789101112131415161718192021222324/** * 运行工作量证明,开始挖矿,找到小于难度目标值的Hash * * @return */public PowResult run() { long nonce = 0; String shaHex = ""; System.out.printf("Mining the block containing:%s \\n", this.getBlock().getData()); long startTime = System.currentTimeMillis(); while (nonce < Long.MAX_VALUE) { String data = this.prepareData(nonce); shaHex = DigestUtils.sha256Hex(data); if (new BigInteger(shaHex, 16).compareTo(this.target) == -1) { System.out.printf("Elapsed Time: %s seconds \\n", (float) (System.currentTimeMillis() - startTime) / 1000); System.out.printf("correct hash Hex: %s \\n\\n", shaHex); break; } else { nonce++; } } return new PowResult(nonce, shaHex);} 循环体里面主要以下四步: 准备数据 进行sha256运算 转化为BigInter类型 与target进行比较 最后,返回正确的Hash值以及运算计数器nonce 验证区块Hash有效性123456789/** * 验证区块是否有效 * * @return */public boolean validate() { String data = this.prepareData(this.getBlock().getNonce()); return new BigInteger(DigestUtils.sha256Hex(data), 16).compareTo(this.target) == -1;} 修改区块添加逻辑123456789101112131415/** * <p> 创建新区块 </p> * * @param previousHash * @param data * @return */public static Block newBlock(String previousHash, String data) { Block block = new Block("", previousHash, data, Instant.now().getEpochSecond(), 0); ProofOfWork pow = ProofOfWork.newProofOfWork(block); PowResult powResult = pow.run(); block.setHash(powResult.getHash()); block.setNonce(powResult.getNonce()); return block;} 创建区块 创建Pow算法对象 执行Pow算法 保存返回的Hash以及运算计数器 测试运行1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859/** * 测试 * * @author wangwei * @date 2018/02/05 */public class BlockchainTest { public static void main(String[] args) { Blockchain blockchain = Blockchain.newBlockchain(); blockchain.addBlock("Send 1 BTC to Ivan"); blockchain.addBlock("Send 2 more BTC to Ivan"); for (Block block : blockchain.getBlockList()) { System.out.println("Prev.hash: " + block.getPrevBlockHash()); System.out.println("Data: " + block.getData()); System.out.println("Hash: " + block.getHash()); System.out.println("Nonce: " + block.getNonce()); ProofOfWork pow = ProofOfWork.newProofOfWork(block); System.out.println("Pow valid: " + pow.validate() + "\\n"); } }}/** * 设定TARGET_BITS = 20,得到如下结果: */Mining the block containing:Genesis Block Elapsed Time: 2.118 seconds correct hash Hex: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442 Mining the block containing:Send 1 BTC to Ivan Elapsed Time: 1.069 seconds correct hash Hex: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5 Mining the block containing:Send 2 more BTC to Ivan Elapsed Time: 4.258 seconds correct hash Hex: 00000777f93efe91d9aabcba14ab3d8ab8e0255b89818cdb9b93cfa844ad0c7f Prev.hash: Data: Genesis BlockHash: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442Nonce: 522163Pow valid: truePrev.hash: 00000828ee8289ef6381f297585ef8c952fde93fc2b673ff7cc655f699bb2442Data: Send 1 BTC to IvanHash: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5Nonce: 474758Pow valid: truePrev.hash: 00000a38c0d7f2ebbd20773e93770298aa8bc0cc6d85fca8756fe0646ae7fea5Data: Send 2 more BTC to IvanHash: 00000777f93efe91d9aabcba14ab3d8ab8e0255b89818cdb9b93cfa844ad0c7fNonce: 1853839Pow valid: true 总结我们正在一步一步接近真实的区块链架构,本篇我们实现了挖矿机制,但是我们还有很多关键性的功能没有实现:区块链数据库的持久性、钱包、地址、交易、共识机制,这些我们后面一步一步来实现 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part2-pow https://jeiwan.cc/posts/building-blockchain-in-go-part-2/ 《精通比特币(第二版)》","link":"/2019/01/21/build-blockchain-in-java-proof-of-work/"},{"title":"基于Java语言构建区块链(六)—— 交易(Merkle Tree)","text":"文章的主要思想和内容均来自 https://jeiwan.cc/posts/building-blockchain-in-go-part-6/ 引言在这一系列文章的最开始部分,我们提到过区块链是一个分布式的数据库。那时候,我们决定跳过”分布式”这一环节,并且聚焦于”数据存储”这一环节。到目前为止,我们几乎实现了区块链的所有组成部分。在本篇文章中,我们将会涉及一些在前面的文章中所忽略的一些机制,并且在下一篇文章中我们将开始研究区块链的分布式特性。 前面各个部分内容: 基本原型 工作量证明 持久化 & 命令行 交易(UTXO) 地址(钱包) UTXO池在 持久化 & 命令行 这篇文章中,我们研究了比特币核心存储区块的方式。当中我们提到过与区块相关的数据存储在 blocks 这个数据桶中,而交易数据则存储在 chainstate 这个数据桶中,让我们来回忆一下,chainstate 数据桶的数据结构: ‘c’ + 32-byte transaction hash -> unspent transaction output record for that transaction 某笔交易的UTXO记录 ‘B’ -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs 数据库所表示的UTXO的区块Hash 从那篇文章开始,我们已经实现了比特币的交易机制,但是我们还没有用到 chainstate 数据桶去存储我们的交易输出。所以,这将是我们现在要去做的事情。 chainstate 不会去存储交易数据。相反,它存储的是 UTXO 集,也就是未被花费的交易输出集合。除此之外,它还存储了”数据库所表示的UTXO的区块Hash”,我们这里先暂且忽略这一点,因为我们还没有用到区块高度(这一点我们会在后面的文章进行实现)。 那么,我们为什么需要 UTXO 池呢? 一起来看一下我们前面实现的 findUnspentTransactions 方法: 123456789101112131415161718192021222324252627282930313233/** * 查找钱包地址对应的所有未花费的交易 * * @param pubKeyHash 钱包公钥Hash * @return */ private Transaction[] findUnspentTransactions(byte[] pubKeyHash) throws Exception { Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(pubKeyHash); Transaction[] unspentTxs = {}; // 再次遍历所有区块中的交易输出 for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId()); int[] spentOutIndexArray = allSpentTXOs.get(txId); for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) { if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) { continue; } // 保存不存在 allSpentTXOs 中的交易 if (transaction.getOutputs()[outIndex].isLockedWithKey(pubKeyHash)) { unspentTxs = ArrayUtils.add(unspentTxs, transaction); } } } } return unspentTxs; } 该方法是用来查找钱包地址对应的包含未花费交易输出的交易信息。由于交易信息是存储在区块当中,所以我们现有的做法是遍历区块链中的每个区块,然后遍历每个区块中的交易信息,再然后遍历每个交易中的交易输出,并检查交易输出是否被相应的钱包地址所锁定,效率非常低下。截止2018年3月29号,比特币中有 515698 个区块,并且这些数据占据了140+Gb 的磁盘空间。这也就意味着一个人必须运行全节点(下载所有的区块数据)才能验证交易信息。此外,验证交易信息需要遍历所有的区块。 针对这个问题的解决办法是需要有一个存储了所有UTXOs(未花费交易输出)的索引,这就是 UTXOs 池所要做的事情:UTXOs池其实是一个缓存空间,它所缓存的数据需要从构建区块链中所有的交易数据中获得(通过遍历所有的区块链,不过这个构建操作只需要执行一次即可),并且它后续还会用于钱包余额的计算以及新的交易数据的验证。截止到2017年9月,UTXOs池大约为 2.7Gb。 好了,让我们来想一下,为了实现 UTXOs 池我们需要做哪些事情。当前,有下列方法被用于查找交易信息: Blockchain.getAllSpentTXOs —— 查询所有已被花费的交易输出。它需要遍历区块链中所有区块中交易信息。 Blockchain.findUnspentTransactions —— 查询包含未被花费的交易输出的交易信息。它也需要遍历区块链中所有区块中交易信息。 Blockchain.findSpendableOutputs —— 该方法用于新的交易创建之时。它需要找到足够多的交易输出,以满足所需支付的金额。需要调用 Blockchain.findUnspentTransactions 方法。 Blockchain.findUTXO —— 查询钱包地址所对应的所有未花费交易输出,然后用于计算钱包余额。需要调用Blockchain.findUnspentTransactions 方法。 Blockchain.findTransaction —— 通过交易ID查询交易信息。它需要遍历所有的区块直到找到交易信息为止。 如你所见,上面这些方法都需要去遍历数据库中的所有区块。由于UTXOs池只存储未被花费的交易输出,而不会存储所有的交易信息,因此我们不会对有 Blockchain.findTransaction 进行优化。 那么,我们需要下列这些方法: Blockchain.findUTXO —— 通过遍历所有的区块来找到所有未被花费的交易输出. UTXOSet.reindex —— 调用上面 findUTXO 方法,然后将查询结果存储在数据库中。也即需要进行缓存的地方。 UTXOSet.findSpendableOutputs —— 与 Blockchain.findSpendableOutputs 类似,区别在于会使用 UTXO 池。 UTXOSet.findUTXO —— 与Blockchain.findUTXO 类似,区别在于会使用 UTXO 池。 Blockchain.findTransaction —— 逻辑保持不变。 这样,两个使用最频繁的方法将从现在开始使用缓存!让我们开始编码吧! 定义 UTXOSet: 123456@NoArgsConstructor@AllArgsConstructor@Slf4jpublic class UTXOSet { private Blockchain blockchain;} 重建 UTXO 池索引: 1234567891011121314151617181920public class UTXOSet { ... /** * 重建 UTXO 池索引 */ @Synchronized public void reIndex() { log.info("Start to reIndex UTXO set !"); RocksDBUtils.getInstance().cleanChainStateBucket(); Map<String, TXOutput[]> allUTXOs = blockchain.findAllUTXOs(); for (Map.Entry<String, TXOutput[]> entry : allUTXOs.entrySet()) { RocksDBUtils.getInstance().putUTXOs(entry.getKey(), entry.getValue()); } log.info("ReIndex UTXO set finished ! "); } ...} 此方法用于初始化 UTXOSet。首先,需要清空 chainstate 数据桶,然后查询所有未被花费的交易输出,并将它们保存到 chainstate 数据桶中。 实现 findSpendableOutputs 方法,供 Transation.newUTXOTransaction 调用 123456789101112131415161718192021222324252627282930313233343536373839404142public class UTXOSet { ... /** * 寻找能够花费的交易 * * @param pubKeyHash 钱包公钥Hash * @param amount 花费金额 */ public SpendableOutputResult findSpendableOutputs(byte[] pubKeyHash, int amount) { Map<String, int[]> unspentOuts = Maps.newHashMap(); int accumulated = 0; Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket(); for (Map.Entry<String, byte[]> entry : chainstateBucket.entrySet()) { String txId = entry.getKey(); TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(entry.getValue()); for (int outId = 0; outId < txOutputs.length; outId++) { TXOutput txOutput = txOutputs[outId]; if (txOutput.isLockedWithKey(pubKeyHash) && accumulated < amount) { accumulated += txOutput.getValue(); int[] outIds = unspentOuts.get(txId); if (outIds == null) { outIds = new int[]{outId}; } else { outIds = ArrayUtils.add(outIds, outId); } unspentOuts.put(txId, outIds); if (accumulated >= amount) { break; } } } } return new SpendableOutputResult(accumulated, unspentOuts); } ... } 实现 findUTXOs 接口,供 CLI.getBalance 调用: 123456789101112131415161718192021222324252627282930public class UTXOSet { ... /** * 查找钱包地址对应的所有UTXO * * @param pubKeyHash 钱包公钥Hash * @return */ public TXOutput[] findUTXOs(byte[] pubKeyHash) { TXOutput[] utxos = {}; Map<String, byte[]> chainstateBucket = RocksDBUtils.getInstance().getChainstateBucket(); if (chainstateBucket.isEmpty()) { return utxos; } for (byte[] value : chainstateBucket.values()) { TXOutput[] txOutputs = (TXOutput[]) SerializeUtils.deserialize(value); for (TXOutput txOutput : txOutputs) { if (txOutput.isLockedWithKey(pubKeyHash)) { utxos = ArrayUtils.add(utxos, txOutput); } } } return utxos; } ... } 以上这些方法都是先前 Blockchain 中相应方法的微调版,先前的方法将不再使用。 有了UTXO池之后,意味着我们的交易数据分开存储到了两个不同的数据桶中:交易数据存储到了 block 数据桶中,而UTXO存储到了 chainstate 数据桶中。这就需要一种同步机制来保证每当一个新的区块产生时,UTXO池能够及时同步最新区块中的交易数据,毕竟我们不想频地进行 reIndex 。因此,我们需要如下方法: 更新UTXO池: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859public class UTXOSet { ... /** * 更新UTXO池 * <p> * 当一个新的区块产生时,需要去做两件事情: * 1)从UTXO池中移除花费掉了的交易输出; * 2)保存新的未花费交易输出; * * @param tipBlock 最新的区块 */ @Synchronized public void update(Block tipBlock) { if (tipBlock == null) { log.error("Fail to update UTXO set ! tipBlock is null !"); throw new RuntimeException("Fail to update UTXO set ! "); } for (Transaction transaction : tipBlock.getTransactions()) { // 根据交易输入排查出剩余未被使用的交易输出 if (!transaction.isCoinbase()) { for (TXInput txInput : transaction.getInputs()) { // 余下未被使用的交易输出 TXOutput[] remainderUTXOs = {}; String txId = Hex.encodeHexString(txInput.getTxId()); TXOutput[] txOutputs = RocksDBUtils.getInstance().getUTXOs(txId); if (txOutputs == null) { continue; } for (int outIndex = 0; outIndex < txOutputs.length; outIndex++) { if (outIndex != txInput.getTxOutputIndex()) { remainderUTXOs = ArrayUtils.add(remainderUTXOs, txOutputs[outIndex]); } } // 没有剩余则删除,否则更新 if (remainderUTXOs.length == 0) { RocksDBUtils.getInstance().deleteUTXOs(txId); } else { RocksDBUtils.getInstance().putUTXOs(txId, remainderUTXOs); } } } // 新的交易输出保存到DB中 TXOutput[] txOutputs = transaction.getOutputs(); String txId = Hex.encodeHexString(transaction.getTxId()); RocksDBUtils.getInstance().putUTXOs(txId, txOutputs); } } ... } 让我们将 UTXOSet 用到它们所需之处去: 12345678910111213141516171819public class CLI { ... /** * 创建区块链 * * @param address */ private void createBlockchain(String address) { Blockchain blockchain = Blockchain.createBlockchain(address); UTXOSet utxoSet = new UTXOSet(blockchain); utxoSet.reIndex(); log.info("Done ! "); } ... } 当创建一个新的区块链是,我们需要重建 UTXO 池索引。截止目前,这是唯一一处用到 reIndex 的地方,尽管看起有些多余,因为在区块链创建之初仅仅只有一个区块和一笔交易。 修改 CLI.send 接口: 1234567891011121314151617181920212223242526public class CLI { ... /** * 转账 * * @param from * @param to * @param amount */ private void send(String from, String to, int amount) throws Exception { ... Blockchain blockchain = Blockchain.createBlockchain(from); Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain); Block newBlock = blockchain.mineBlock(new Transaction[]{transaction}); new UTXOSet(blockchain).update(newBlock); ... } ... } 当一个新的区块产生后,需要去更新 UTXO 池数据。 让我们来检查一下它们的运行情况: 123456789101112131415161718192021222324252627282930313233343536373839404142$ ./blochchain.sh createwalletwallet address : 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf$ ./blochchain.sh createwalletwallet address : 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG$ ./blochchain.sh createwalletwallet address : 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV$ ./blochchain.sh createblockchain -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5MdfElapsed Time: 164.961 seconds correct hash Hex: 00225493862611bc517cb6b3610e99d26d98a6b52484c9fa745df6ceff93f445 Done ! $ ./blochchain.sh getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5MdfBalance of '1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf': 10$ ./blochchain.sh send -from 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -to 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -amount 5java.lang.Exception: ERROR: Not enough funds$ ./blochchain.sh send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG -amount 2Elapsed Time: 54.92 seconds correct hash Hex: 0001ab21f71ff2d6d532bf3b3388db790c2b03e28d7bd27bd669c5f6380a4e5b Success!$ ./blochchain.sh send -from 1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf -to 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV -amount 2Elapsed Time: 54.92 seconds correct hash Hex: 0009b925cc94e3db8bab2958b1fc2d1764aa15531e20756d92c3a93065c920f0 Success!$ ./blochchain.sh getbalance -address 1JgppX2xMshr35wHzvNWQBejUAZ3Te5MdfBalance of '1JgppX2xMshr35wHzvNWQBejUAZ3Te5Mdf': 6$ ./blochchain.sh getbalance -address 1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoGBalance of '1HX7bWwCjvxkjq65GUgAVRFfTZy6yKWkoG': 2$ ./blochchain.sh getbalance -address 1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZVBalance of '1L1RoFgyjCrNPCPHmSEBtNiV3h2wiF9mZV': 2 奖励机制前面的章节中我们省略了矿工挖矿的奖励机制。时机已经成熟,该实现它了。 矿工奖励其实是一个 coinbase 交易(创币交易)。当一个矿工节点开始去生产一个新的区块时,他会从队列中取出一些交易数据,并且为它们预制一个 coinbase 交易。这笔 coinbase 交易中仅有的交易输出包含了矿工的公钥hash。 只需要更新 send 命令接口,我们就可以轻松实现矿工的奖励机制: 1234567891011121314151617181920212223242526272829public class CLI { ... /** * 转账 * * @param from * @param to * @param amount */ private void send(String from, String to, int amount) throws Exception { ... Blockchain blockchain = Blockchain.createBlockchain(from); // 新交易 Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain); // 奖励 Transaction rewardTx = Transaction.newCoinbaseTX(from, ""); Block newBlock = blockchain.mineBlock(new Transaction[]{transaction, rewardTx}); new UTXOSet(blockchain).update(newBlock); ... } ... } 还需要修改交易验证方法,coinbase 交易直接验证通过: 123456789101112131415161718public class Blockchain { /** * 交易签名验证 * * @param tx */ private boolean verifyTransactions(Transaction tx) { if (tx.isCoinbase()) { return true; } ... } ... } 在我们的实现逻辑中,代币的发送也是区块的生产者,因此,奖励也归他所有。 让我们来验证一下奖励机制: 12345678910111213141516171819202122232425262728293031323334353637$ ./blochchain.sh createwallet wallet address : 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD$ ./blochchain.sh createwallet wallet address : 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX$ ./blochchain.sh createwallet wallet address : 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT$ ./blochchain.sh createblockchain -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUDElapsed Time: 17.973 secondscorrect hash Hex: 0000defe83a851a5db3803d5013bbc20c6234f176b2c52ae36fdb53d28b33d93 Done ! $ ./blochchain.sh send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX -amount 6Elapsed Time: 30.887 secondscorrect hash Hex: 00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7 Success!$ ./blochchain.sh send -from 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD -to 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT -amount 3Elapsed Time: 45.267 secondscorrect hash Hex: 00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13 Success!$ ./blochchain.sh getbalance -address 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUDBalance of '1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD': 21$ ./blochchain.sh getbalance -address 17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KXBalance of '17crpQoWy7TEkY9UPjZ3Qt9Fc2rWPUt8KX': 6$ ./blochchain.sh getbalance -address 12L868QZW1ySYzf2oT5ha9py9M5JrSRhvTBalance of '12L868QZW1ySYzf2oT5ha9py9M5JrSRhvT': 3 1MpdtjTEsDvrkrLWmMswq4K3VPtevXXnUD 这个地址一共收到了三份奖励: 第一次是开采创世区块; 第二次是开采区块:00005fd36a2609b43fd940577f93b8622e88e854f5ccfd70e113f763b6df69f7 第三次是开采区块:00009fd7c59b830b60ec21ade7672921d2fb0962a1b06a42c245450e47582a13 Merkle TreeMerkle Tree(默克尔树) 是这篇文章中我们需要重点讨论的一个机制。 正如我前面提到的那样,整个比特币的数据库占到了大约140G的磁盘空间。由于比特币的分布式特性,网络中的每一个节点必须是独立且自给自足的。每个比特币节点都是路由、区块链数据库、挖矿、钱包服务的功能集合。每个节点都参与全网络的路由功能,同时也可能包含其他功能。每个节点都参与验证并传播交易及区块信息,发现并维持与对等节点的连接。一个全节点(full node)包括以下四个功能: 随着越来越多的人开始使用比特币,这条规则开始变得越来越难以遵循:让每一个人都去运行一个完整的节点不太现实。在中本聪发布的比特币白皮书中,针对这个问题提出了一个解决方案:Simplified Payment Verification (SPV)(简易支付验证)。SPV是比特币的轻量级节点,它不需要下载所有的区块链数据,也不需要验证区块和交易数据。相反,当SPV想要验证一笔交易的有效性时,它会从它所连接的全节点上检索所需要的一些数据。这种机制保证了在只有一个全节点的情况,可以运行多个SPV轻钱包节点。 更多有关SPV的介绍,请查看:《精通比特币(第二版)》第八章 为了使SPV成为可能,就需要有一种方法在没有全量下载区块数据的情况下,来检查一个区块是否包含了某笔交易。这就是 Merkle Tree 发挥作用的地方了。 比特币中所使用的Merkle Tree是为了获得交易的Hash值,随后这个已经被Pow(工作量证明)系统认可了的Hash值会被保存到区块头中。到目前为止,我们只是简单地计算了一个区块中每笔交易的Hash值,然后在准备Pow数据时,再对这些交易进行 SHA-256 计算。虽然这是一个用于获取区块交易唯一表示的一个不错的方式,但是这种方式不具备 Merkle Tree 的优点。 更多有关Merkle Tree的介绍,请查看:《精通比特币(第二版)》第九章 来看一下Merkle Tree的结构: 每一个区块都会构建一个Merkle Tree,它从最底部的叶子节点开始往上构建,每一个交易的Hash就是一个叶子节点(比特币中用的双SHA256算法)。叶子节点的数量必须是偶数个,但是并不是每一个区块都能包含偶数笔交易数据。如果存在奇数笔交易数据,那么最后一笔交易数据将会被复制一份(这仅仅发生在Merkle Tree中,而不是区块中)。 从下往上移动,叶子节点成对分组,它们的Hash值被连接到一起,并且在此基础上再次计算出新的Hash值。新的Hash 形成新的树节点。这个过程不断地被重复,直到最后仅剩一个被称为根节点的树节点。这个根节点的Hash就是区块中交易数据们的唯一代表,它会被保存到区块头中,并被用于参与POW系统的计算。 Merkle树的好处是节点可以在不下载整个块的情况下验证某笔交易的合法性。 为此,只需要交易Hash,Merkle树根Hash和Merkle路径。 Merkle Tree代码实现如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155package one.wangwei.blockchain.transaction;import com.google.common.collect.Lists;import lombok.Data;import one.wangwei.blockchain.util.ByteUtils;import org.apache.commons.codec.digest.DigestUtils;import java.util.List;/** * 默克尔树 * * @author wangwei * @date 2018/04/15 */@Datapublic class MerkleTree { /** * 根节点 */ private Node root; /** * 叶子节点Hash */ private byte[][] leafHashes; public MerkleTree(byte[][] leafHashes) { constructTree(leafHashes); } /** * 从底部叶子节点开始往上构建整个Merkle Tree * * @param leafHashes */ private void constructTree(byte[][] leafHashes) { if (leafHashes == null || leafHashes.length < 1) { throw new RuntimeException("ERROR:Fail to construct merkle tree ! leafHashes data invalid ! "); } this.leafHashes = leafHashes; List<Node> parents = bottomLevel(leafHashes); while (parents.size() > 1) { parents = internalLevel(parents); } root = parents.get(0); } /** * 构建一个层级节点 * * @param children * @return */ private List<Node> internalLevel(List<Node> children) { List<Node> parents = Lists.newArrayListWithCapacity(children.size() / 2); for (int i = 0; i < children.size() - 1; i += 2) { Node child1 = children.get(i); Node child2 = children.get(i + 1); Node parent = constructInternalNode(child1, child2); parents.add(parent); } // 内部节点奇数个,只对left节点进行计算 if (children.size() % 2 != 0) { Node child = children.get(children.size() - 1); Node parent = constructInternalNode(child, null); parents.add(parent); } return parents; } /** * 底部节点构建 * * @param hashes * @return */ private List<Node> bottomLevel(byte[][] hashes) { List<Node> parents = Lists.newArrayListWithCapacity(hashes.length / 2); for (int i = 0; i < hashes.length - 1; i += 2) { Node leaf1 = constructLeafNode(hashes[i]); Node leaf2 = constructLeafNode(hashes[i + 1]); Node parent = constructInternalNode(leaf1, leaf2); parents.add(parent); } if (hashes.length % 2 != 0) { Node leaf = constructLeafNode(hashes[hashes.length - 1]); // 奇数个节点的情况,复制最后一个节点 Node parent = constructInternalNode(leaf, leaf); parents.add(parent); } return parents; } /** * 构建叶子节点 * * @param hash * @return */ private static Node constructLeafNode(byte[] hash) { Node leaf = new Node(); leaf.hash = hash; return leaf; } /** * 构建内部节点 * * @param leftChild * @param rightChild * @return */ private Node constructInternalNode(Node leftChild, Node rightChild) { Node parent = new Node(); if (rightChild == null) { parent.hash = leftChild.hash; } else { parent.hash = internalHash(leftChild.hash, rightChild.hash); } parent.left = leftChild; parent.right = rightChild; return parent; } /** * 计算内部节点Hash * * @param leftChildHash * @param rightChildHash * @return */ private byte[] internalHash(byte[] leftChildHash, byte[] rightChildHash) { byte[] mergedBytes = ByteUtils.merge(leftChildHash, rightChildHash); return DigestUtils.sha256(mergedBytes); } /** * Merkle Tree节点 */ @Data public static class Node { private byte[] hash; private Node left; private Node right; }} 然后修改 Block.hashTransaction 接口: 1234567891011121314151617181920public class Block { ... /** * 对区块中的交易信息进行Hash计算 * * @return */ public byte[] hashTransaction() { byte[][] txIdArrays = new byte[this.getTransactions().length][]; for (int i = 0; i < this.getTransactions().length; i++) { txIdArrays[i] = this.getTransactions()[i].hash(); } return new MerkleTree(txIdArrays).getRoot().getHash(); } ... } MerkleTree的根节点的Hash值,就是区块中交易信息的唯一代表。 小结这一节我们主要是对前面的交易机制做了进一步的优化,加入UTXO池和Merkle Tree机制。 下一讲,我们来介绍一下比特币的交易脚本相关的内容。 资料 源码:https://github.com/wangweiX/blockchain-java/tree/part6-transaction2 《精通比特币(第二版)》 The UTXO Set UTXO set statistics Merkle Tree Why every Bitcoin user should understand “SPV security” Script “Ultraprune” Bitcoin Core commit Smart contracts and Bitcoin","link":"/2019/01/23/build-blockchain-in-java-transaction-merkle-tree/"},{"title":"基于Java语言构建区块链(七)—— 交易脚本(智能合约)","text":"上一篇 文章我们引入 UTXOset 和 Merkle Tree 对交易流程做了些许优化,本篇文章我们将介绍比特币另一个更加重要的机制 —— 交易脚本。 在介绍 UTXO的文章 中,我们已经了解到比特币的交易输出由锁定脚本锁定,它只能被交易输出所被指向的交易输入中的解锁脚本所解锁,今天让我们来详细讨论一下它们的实现机制。 交易详情如今,大多数比特币网络处理的交易是以“Alice付给Bob”的形式存在的。同时,它们是以一种称为“P2PKH”(Pay-to-Public-Key-Hash)脚本为基础的。然而,通过使用脚本来锁定输出和解锁输入意味着通过使用编程语言,比特币交易可以包含无限数量的条件。当然,比特币交易并不限于“Alice付给Bob” 的形式和模式。” 这只是这个脚本语言可以表达的可能性的冰山一角。稍后, 我们将会全面展示比特币交易脚本语言的各个组成部分;同时,我们也会演示如何使用它去表达复杂的使用条件以及解锁脚本如何去满足这些花费条件。 比特币交易验证并不基于一个不变的模式,而是通过运行脚本语言来实现。这种语言可以表达出多到数不尽的条件变种。这也是比特币作为一种“可编程的货币”所拥有的权力。 我们以《精通比特币(第二版)》第二章节 中 Alice向Bob购买咖啡为例,点击查看该笔交易详情 交易输入:0.1000 BTC手续费用:0.0005 BTC支付费用:0.0150 BTC找 零: 0.0845 BTC 该笔交易的数据如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647{ "hash": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2", "locktime": 0, "size": 258, "txid": "0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2", "version": 1, "vin": [ { "scriptSig": { "asm": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf", "hex": "483045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf" }, "sequence": 4294967295, "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18", "vout": 0 } ], "vout": [ { "n": 0, "scriptPubKey": { "addresses": [ "1GdK9UzpHBzqzX2A9JFP3Di4weBwqgmoQA" ], "asm": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac", "reqSigs": 1, "type": "pubkeyhash" }, "value": 0.015 }, { "n": 1, "scriptPubKey": { "addresses": [ "1Cdid9KFAaatwczBwBttQcwXYCpvK8h7FK" ], "asm": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac", "reqSigs": 1, "type": "pubkeyhash" }, "value": 0.0845 } ], "vsize": 258} 交易输入 1234567891011"vin": [ { "scriptSig": { "asm": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf", "hex": "483045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf" }, "sequence": 4294967295, "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18", "vout": 0 }] 它所包含的信息: 交易ID。包含了它所指向的UTXO的交易的Hash值。 UTXO下标。定义了它所指向的UTXO在上一笔交易中交易输出数组的位置(下标值)。 签名。用于满足它所指向的UTXO上所设定的花费条件。 交易输出 12345678910111213141516171819202122232425262728"vout": [ { "n": 0, "scriptPubKey": { "addresses": [ "1GdK9UzpHBzqzX2A9JFP3Di4weBwqgmoQA" ], "asm": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac", "reqSigs": 1, "type": "pubkeyhash" }, "value": 0.015 }, { "n": 1, "scriptPubKey": { "addresses": [ "1Cdid9KFAaatwczBwBttQcwXYCpvK8h7FK" ], "asm": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac", "reqSigs": 1, "type": "pubkeyhash" }, "value": 0.0845 }] 它所包含的信息: 比特币的数量。单位:satoshis(聪) 比特币地址。交易输出所绑定的地址。 锁定脚本。定义了花费这笔交易输出所需要满足的限制条件。其中包含了一些字符串,例如:OP_DUP、OP_HASH160、OP_EQUALVERIFY、OP_CHECKSIG,这些叫操作码,后面会做介绍。 那么,交易输入中的script 是如何满足交易输出中script_string的限制条件的呢?接下来,我们来一起看下比特币的交易脚本是如何工作的。首先,我们来了解一下比特币所用到脚本语言的特性以及它的工作原理。 比特币脚本语言特性脚本是一种类似Forth的基于堆栈的逆波兰表示法的图灵非完备语言。接下来,让我们逐个解释一下: 图灵非完备(Turing Incomplete) 什么是图灵完备 在可计算性理论里,如果一系列操作数据的规则(如指令集、编程语言、细胞自动机)可以用来模拟单带图灵机,那么它是图灵完备的。这个词源于引入图灵机概念的数学家艾倫·图灵。 虽然图灵机会受到储存能力的物理限制,图灵完全性通常指「具有无限存储能力的通用物理机器或编程语言」。 来源:维基百科 图灵非完备语言将会有有限的功能,不能进行跳转或/和循环。因此它们不能进入无线循环。图灵完备就意味着,在给定的计算资源和内存下,图灵完备程序,能够解决任何问题。Solidity 就是其中一种图灵完备语言。 为什么比特币脚本是图灵非完备的因为没有必要。比特币脚本没有必要做到像以太坊智能合约那样复杂。事实上,如果一个脚本是图灵完备的,它会给恶意的人以机会去随意创造复杂的交易,这将会吃掉比特币网络的哈希率并降低整个系统的性能。 哈希率是比特币网络的处理能力的衡量单位。为了安全,比特币网络必须进行高强度的数学运算。网络的哈希率达到10TH/s,意味着这个网络每秒能处理10亿次计算。 逆波兰表示法(Reverse Polish notation) 逆波兰表示法(Reverse Polish notation,RPN,或逆波兰记法),是一种是由波兰数学家扬·武卡谢维奇1920年引入的数学表达式方式,在逆波兰记法中,所有操作符置于操作数的后面,因此也被称为后缀表示法。逆波兰记法不需要括号来标识操作符的优先级。 基于堆栈这是一种具有 LIFO(Last In First Out)特性的数据结构,熟悉数据结构的应该非常清楚,这里不多做介绍。 想深入了解的朋友,可以查看我的另一篇文章https://wangwei.one/posts/java-data-structures-and-algorithms-stack.html 类Forth脚本语言比特币脚本恰好类似于编程语言“Forth”,它也恰好是基于堆栈的一种编程语言。 工作原理比特币的脚本语言非常简单,这种语言的代码无非就是一系列数据和操作符。脚本语言通过从左至右地处理每个项目的方式执行脚本。数字(常数)被推送至堆栈,操作符向堆栈推送(或移除)一个或多个参数,对它们进行处理,甚至可能会向堆栈推送一个结果。例如,OP_ADD将从堆栈移除两个项目,将二者相加,然后再将二者相加之和推送到堆栈。 条件操作符评估一项条件,产生一个真或假的结果。例如,OP_EQUAL从堆栈移除两个项目,假如二者相等则推送真(表示为1),假如二者不等则推送为假(表示为0)。比特币交易脚本常含条件操作符,当一笔交易有效时,就会产生True的结果。 我们以一个简单的脚本来进行演示: 12 3 OP_ADD 5 OP_EQUAL 从左至右,依次执行,过程如下: 弄明白这个过程之后,你会发现其中所蕴含的堆栈特性以及逆波兰表示法特性。接下来,我们来看下比特币脚本的锁定与解锁逻辑。 锁定与解锁逻辑比特币的交易验证引擎依赖于两类脚本来验证比特币交易:一个锁定脚本和一个解锁脚本。 锁定脚本是放置在输出上的消费条件:它指定将来要花费输出必须满足的条件。锁定脚本常被称为scriptPubKey,因为它通常包含公钥或比特币地址(公钥哈希)。 解锁脚本是通过“解决”或满足锁定脚本上的交易输出条件并允许交易输出花费的脚本。 解锁脚本是每个交易输入的一部分。大多数情况下,它包含了由用户私钥所产生的数字签名。解锁脚本常被称为 scriptSig ,因为它通常包含数字签名。 每当要验证一笔交易的有效性时,解锁脚本和锁定脚本会随着堆栈的传递被分别执行。首先,使用堆栈执行引擎执行解锁脚本。如果解锁脚本在执行过程中未报错(例如:没有“悬挂”操作码),则复制 主堆栈(而不是备用堆栈),并执行锁定脚本。如果从解锁脚本中复制而来的堆栈数据执行锁定脚本的结果 为“TRUE”,那么解锁脚本就成功地满足了锁定脚本所设置的条件,因此,该输入是一个能使用该UTXO的有效授 权。如果在合并脚本后的结果不是”TRUE“以外的任何结果,输入都是无效的,因为它不能满足UTXO中所设置的使 用该笔资金的条件。 下图所示是最为常见类型的比特币交易(向公钥哈希进行一笔支付)的解锁和锁定脚本样本,该样本展示了在脚本验证之前将解锁脚本和锁定脚本串联而成的组合脚本。 这是比特币脚本中使用最为常见的一种形式,名叫 Pay to Public Key Hash (P2PKH)。基于前面 2 + 3 = 5 的验证过程,我们可以得到 P2PKH 脚本在堆栈引擎中的验证过程如下所示: 好了, 到此为止,你已经对比特币的交易脚本以及它的工作原理已经有了一个非常清楚的理解与认识。 参考资料 《精通比特币(第二版)》第六章 https://jeiwan.cc/posts/building-blockchain-in-go-part-6 https://blockgeeks.com/guides/best-bitcoin-script-guide https://blockgeeks.com/guides/bitcoin-script-guide-part-2 https://en.bitcoin.it/wiki/Script","link":"/2019/01/23/build-blockchain-in-java-transaction-script/"},{"title":"基于Java语言构建区块链(四)—— 交易(UTXO)","text":"文章的主要思想和内容均来自 https://jeiwan.cc/posts/building-blockchain-in-go-part-4/ 引言上一篇 文章,我们实现了区块数据的持久化,本篇开始交易环节的实现。交易这一环节是整个比特币系统当中最为关键的一环,并且区块链唯一的目的就是通过安全的、可信的方式来存储交易信息,防止它们创建之后被人恶意篡改。今天我们开始实现交易这一环节,但由于这是一个很大的话题,所以我们分为两部分:第一部分我们将实现区块链交易的基本机制,到第二部分,我们再来研究它的细节。 比特币交易如果你开发过Web应用程序,为了实现支付系统,你可能会在数据库中创建一些数据库表:账户 和 交易记录。账户用于存储用户的个人信息以及账户余额等信息,交易记录用于存储资金从一个账户转移到另一个账户的记录。但是在比特币中,支付系统是以一种完全不一样的方式实现的,在这里: 没有账户 没有余额 没有地址 没有 Coins(币) 没有发送者和接受者 由于区块链是一个公开的数据库,我们不希望存储有关钱包所有者的敏感信息。Coins 不会汇总到钱包中。交易不会将资金从一个地址转移到另一个地址。没有可保存帐户余额的字段或属性。只有交易信息。那比特币的交易信息里面到底存储的是什么呢? 交易组成一笔比特币的交易由 交易输入 和 交易输出 组成,数据结构如下: 1234567891011121314151617181920212223242526/** * 交易 * * @author wangwei * @date 2017/03/04 */@Data@AllArgsConstructor@NoArgsConstructorpublic class Transaction { /** * 交易的Hash */ private byte[] txId; /** * 交易输入 */ private TXInput[] inputs; /** * 交易输出 */ private TXOutput[] outputs; } 一笔交易的 交易输入 其实是指向上一笔交易的交易输出 (这个后面详细说明)。我们钱包里面的 Coin(币)实际是存储在这些 交易输出 里面。下图表示了区块链交易系统里面各个交易相互引用的关系:注意: 有些 交易输出 并不是由 交易输入 产生,而是凭空产生的(后面会详细介绍)。 但,交易输入 必须指向某个 交易输出,它不能凭空产生。 在一笔交易里面,交易输入 可能会来自多笔交易所产生的 交易输出。 在整篇文章中,我们将使用诸如“钱”,“币”,“花费”,“发送”,“账户”等词语。但比特币中没有这样的概念,在比特币交易中,交易信息是由 锁定脚本 锁定一个数值,并且只能被所有者的 解锁脚本 解锁。(解铃还须系铃人) 交易输出让我们先从交易输出开始,他的数据结构如下: 12345678910111213141516171819202122/** * 交易输出 * * @author wangwei * @date 2017/03/04 */@Data@AllArgsConstructor@NoArgsConstructorpublic class TXOutput { /** * 数值 */ private int value; /** * 锁定脚本 */ private String scriptPubKey; } 实际上,它表示的是能够存储 “coins(币)”的交易输出(注意 value 字段)。并且这里所谓的 value 实际上是由存储在 ScriptPubKey (锁定脚本)中的一个puzzle(难题) 所锁定。在内部,比特币使用称为脚本的脚本语言,用于定义输出锁定和解锁逻辑。这个语言很原始(这是故意的,以避免可能的黑客和滥用),但我们不会详细讨论它。 你可以在这里找到它的详细解释。here 在比特币中,value 字段存储着 satoshis 的任意倍的数值,而不是BTC的数量。satoshis 是比特币的百万分之一(0.00000001 BTC),因此这是比特币中最小的货币单位(如1美分)(satoshis:聪)。锁定脚本是一个放在一个输出值上的“障碍”,同时它明确了今后花费这笔输出的条件。由于锁定脚本往往含有一个公钥(即比特币地址),在历史上它曾被称作一个脚本公钥代码。在大多数比特币应用源代码中,脚本公钥代码便是我们所说的锁定脚本。 由于我们还没有实现钱包地址的逻辑,所以这里先暂且忽略锁定脚本相关的逻辑。ScriptPubKey 将会存储任意的字符串(用户定义的钱包地址) 顺便说一句,拥有这样的脚本语言意味着比特币也可以用作智能合约平台。 关于 交易输出 的一个重要的事情是它们是不可分割的,这意味着你不能将它所存储的数值拆开来使用。当这个交易输出在新的交易中被交易输入所引用时,它将作为一个整体被花费掉。 如果其值大于所需值,那么剩余的部分则会作为零钱返回给付款方。 这与真实世界的情况类似,例如,您支付5美元的钞票用于购买1美元的东西,那么你将会得到4美元的零钱。 交易输入1234567891011121314151617181920212223242526/** * 交易输入 * * @author wangwei * @date 2017/03/04 */@Data@AllArgsConstructor@NoArgsConstructorpublic class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易输出索引 */ private int txOutputIndex; /** * 解锁脚本 */ private String scriptSig; } 前面提到过,一个交易输入指向的是某一笔交易的交易输出: txId 存储的是某笔交易的ID值 txOutputIndex 存储的是交易中这个交易输出的索引位置(因为一笔交易可能包含多个交易输出) scriptSig 主要是提供用于交易输出中 ScriptPubKey 所需的验证数据。 如果这个数据被验证正确,那么相应的交易输出将被解锁,并且其中的 value 能够生成新的交易输出; 如果不正确,那么相应的交易输出将不能被交易输入所引用; 通过锁定脚本与解锁脚本这种机制,保证了某个用户不能花费属于他人的Coins。 同样,由于我们尚未实现钱包地址功能,ScriptSig 将会存储任意的用户所定义的钱包地址。我们将会在下一章节实现公钥和数字签名验证。 说了这么多,我们来总结一下。交易输出是”Coins”实际存储的地方。每一个交易输出都带有一个锁定脚本,它决定了解锁的逻辑。每一笔新的交易必须至少有一个交易输入与交易输出。一笔交易的交易输入指向前一笔交易的交易输出,并且提供用于锁定脚本解锁需要的数据(ScriptSig 字段),然后利用交易输出中的 value 去创建新的交易输出。 注意,这段话的原文如下,但是里面有表述错误的地方,交易输出带有的是锁定脚本,而不是解锁脚本。 Let’s sum it up. Outputs are where “coins” are stored. Each output comes with an unlocking script, which determines the logic of unlocking the output. Every new transaction must have at least one input and output. An input references an output from a previous transaction and provides data (the ScriptSig field) that is used in the output’s unlocking script to unlock it and use its value to create new outputs. 鸡与蛋的问题在比特币中,鸡蛋先于鸡出现。交易输入源自于交易输出的逻辑是典型的”先有鸡还是先有蛋”的问题:交易输入产生交易输出,交易输出又会被交易输入所引用。在比特币中,交易输出先于交易输入出现。 当矿工开始开采区块时,区块中会被添加一个 coinbase 交易(创币交易)。coinbase 交易是一种特殊的交易,它不需要以前已经存在的交易输出。它会凭空创建出交易输出(i.e: Coins)。也即,鸡蛋的出现并不需要母鸡,这笔交易是作为矿工成功挖出新的区块后的一笔奖励。 正如你所知道的那样,在区块链的最前端,即第一个区块,有一个创世区块。他产生了区块链中有史以来的第一个交易输出,并且由于没有前一笔交易,也就没有相应的输出,因此不需要前一笔交易的交易输出。 让我们来创建 coinbase 交易: 123456789101112131415161718192021/** * 创建CoinBase交易 * * @param to 收账的钱包地址 * @param data 解锁脚本数据 * @return */public Transaction newCoinbaseTX(String to, String data) { if (StringUtils.isBlank(data)) { data = String.format("Reward to '%s'", to); } // 创建交易输入 TXInput txInput = new TXInput(new byte[]{}, -1, data); // 创建交易输出 TXOutput txOutput = new TXOutput(SUBSIDY, to); // 创建交易 Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput}); // 设置交易ID tx.setTxId(); return tx;} coinbase交易只有一个交易输入。在我们的代码实现中,txId 是空数组,txOutputIndex 设置为了 -1。另外,coinbase交易不会在 ScriptSig 字段上存储解锁脚本,相反,存了一个任意的数据。 在比特币中,第一个 coinbase 交易报刊了如下的信息:”The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. 点击查看 SUBSIDY 是挖矿奖励数量。在比特币中,这个奖励数量没有存储在任何地方,而是依据现有区块的总数进行计算而得到:区块总数 除以 210000。开采创世区块得到的奖励为50BTC,每过 210000 个区块,奖励会减半。在我们的实现中,我们暂且将挖矿奖励设置为常数。(至少目前是这样) 在区块链中存储交易信息从现在开始,每一个区块必须存储至少一个交易信息,并且尽可能地避免在没有交易数据的情况下进行挖矿。这意味着我们必须移除 Block 对象中的 date 字段,取而代之的是 transactions: 1234567891011121314151617181920212223242526272829/** * 区块 * * @author wangwei * @date 2018/02/02 */@Data@AllArgsConstructor@NoArgsConstructorpublic class Block { /** * 区块hash值 */ private String hash; /** * 前一个区块的hash值 */ private String previousHash; /** * 交易信息 */ private Transaction[] transactions; /** * 区块创建时间(单位:秒) */ private long timeStamp;} 相应地,newGenesisBlock 与 newBlock 也都需要做改变: 12345678910111213141516171819202122232425/** * <p> 创建创世区块 </p> * * @param coinbase * @return */public static Block newGenesisBlock(Transaction coinbase) { return Block.newBlock("", new Transaction[]{coinbase});}/** * <p> 创建新区块 </p> * * @param previousHash * @param transactions * @return */public static Block newBlock(String previousHash, Transaction[] transactions) { Block block = new Block("", previousHash, transactions, Instant.now().getEpochSecond(), 0); ProofOfWork pow = ProofOfWork.newProofOfWork(block); PowResult powResult = pow.run(); block.setHash(powResult.getHash()); block.setNonce(powResult.getNonce()); return block;} 接下来,修改 newBlockchain 方法: 123456789101112131415161718/** * <p> 创建区块链 </p> * * @param address 钱包地址 * @return */public static Blockchain newBlockchain(String address) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (StringUtils.isBlank(lastBlockHash)) { // 创建 coinBase 交易 Transaction coinbaseTX = Transaction.newCoinbaseTX(address, ""); Block genesisBlock = Block.newGenesisBlock(coinbaseTX); lastBlockHash = genesisBlock.getHash(); RocksDBUtils.getInstance().putBlock(genesisBlock); RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash); } return new Blockchain(lastBlockHash);} 现在,代码有钱包地址的接口,将会收到开采创世区块的奖励。 工作量证明(Pow)Pow算法必须将存储在区块中的交易信息考虑在内,以保存交易信息存储的一致性和可靠性。因此,我们必须修改 ProofOfWork.prepareData 接口代码逻辑: 123456789101112131415161718192021/** * 准备数据 * <p> * 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换 * @param nonce * @return */private String prepareData(long nonce) { byte[] prevBlockHashBytes = {}; if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) { prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray(); } return ByteUtils.merge( prevBlockHashBytes, this.getBlock().hashTransaction(), ByteUtils.toBytes(this.getBlock().getTimeStamp()), ByteUtils.toBytes(TARGET_BITS), ByteUtils.toBytes(nonce) );} 其中 hashTransaction 代码如下: 123456789101112/** * 对区块中的交易信息进行Hash计算 * * @return */public byte[] hashTransaction() { byte[][] txIdArrays = new byte[this.getTransactions().length][]; for (int i = 0; i < this.getTransactions().length; i++) { txIdArrays[i] = this.getTransactions()[i].getTxId(); } return DigestUtils.sha256(ByteUtils.merge(txIds));} 同样,我们使用哈希值来作为数据的唯一标识。我们希望区块中的所有交易数据都能通过一个哈希值来定义它的唯一标识。为了达到这个目的,我们计算了每一个交易的唯一哈希值,然后将他们串联起来,再对这个串联后的组合进行哈希值计算。 比特币使用更复杂的技术:它将所有包含在块中的交易表示为 Merkle树 ,并在Proof-of-Work系统中使用该树的根散列。 这种方法只需要跟节点的散列值就可以快速检查块是否包含某笔交易,而无需下载所有交易。 UTXO(未花费交易输出) UTXO:unspend transaction output.(未被花费的交易输出) 在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO. UTXO 是理解比特币交易原理的关键所在,我们先来看一段场景: 场景:假设你过去分别向A、B、C这三个比特币用户购买了BTC,从A手中购买了3.5个BTC,从B手中购买了4.5个BTC,从C手中购买了2个BTC,现在你的比特币钱包里面恰好剩余10个BTC。 问题:这个10个BTC是真正的10个BTC吗?其实不是,这句话可能听起来有点怪。(什么!我钱包里面的BTC不是真正的BTC,你不要吓我……) 解释:前面提到过在比特币的交易系统当中,并不存在账户、余额这些概念,所以,你的钱包里面的10个BTC,并不是说钱包余额为10个BTC。而是说,这10个BTC其实是由你的比特币地址(钱包地址|公钥)锁定了的散落在各个区块和各个交易里面的UTXO的总和。 UTXO 是比特币交易的基本单位,每笔交易都会产生UTXO,一个UTXO可以是一“聪”的任意倍。给某人发送比特币实际上是创造新的UTXO,绑定到那个人的钱包地址,并且能被他用于新的支付。 一般的比特币交易由 交易输入 和 交易输出 两部分组成。A向你支付3.5个BTC这笔交易,实际上产生了一个新的UTXO,这个新的UTXO 等于 3.5个BTC(3.5亿聪),并且锁定到了你的比特币钱包地址上。 假如你要给你女(男)朋友转 1.5 BTC,那么你的钱包会从可用的UTXO中选取一个或多个可用的个体来拼凑出一个大于或等于一笔交易所需的比特币量。比如在这个假设场景里面,你的钱包会选取你和C的交易中的UTXO作为 交易输入,input = 2BTC,这里会生成两个新的交易输出,一个输出(UTXO = 1.5 BTC)会被绑定到你女(男)朋友的钱包地址上,另一个输出(UTXO = 0.5 BTC)会作为找零,重新绑定到你的钱包地址上。 有关比特币交易这部分更详细的内容,请查看:《精通比特币(第二版)》第6章 —— 交易 我们需要找到所有未花费的交易输出(UTXO)。Unspent(未花费) 意味着这些交易输出从未被交易输入所指向。这前面的图片中,UTXO如下: tx0, output 1; tx1, output 0; tx3, output 0; tx4, output 0. 当然,当我们检查余额时,我不需要区块链中所有的UTXO,我只需要能被我们解锁的UTXO(当前,我们还没有实现密钥对,而是替代为用户自定义的钱包地址)。首先,我们在交易输入与交易输出上定义锁定-解锁的方法: 交易输入: 1234567891011121314public class TXInput { ... /** * 判断解锁数据是否能够解锁交易输出 * * @param unlockingData * @return */ public boolean canUnlockOutputWith(String unlockingData) { return this.getScriptSig().endsWith(unlockingData); }} 交易输出: 1234567891011121314public class TXOutput { ... /** * 判断解锁数据是否能够解锁交易输出 * * @param unlockingData * @return */ public boolean canBeUnlockedWith(String unlockingData) { return this.getScriptPubKey().endsWith(unlockingData); }} 这里我们暂时用 unlockingData 来与脚本字段进行比较。我们会在后面的文章中来对这部分内容进行优化,我们将会基于私钥来实现用户的钱包地址。 下一步,查询所有与钱包地址绑定的包含UTXO的交易信息,有点复杂(本篇先这样实现,后面我们做一个与钱包地址映射的UTXO池来进行优化): 从与钱包地址对应的交易输入中查询出所有已被花费了的交易输出 再来排除,寻找包含未被花费的交易输出的交易 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677public class Blockchain { ... /** * 查找钱包地址对应的所有未花费的交易 * * @param address 钱包地址 * @return */ private Transaction[] findUnspentTransactions(String address) throws Exception { Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(address); Transaction[] unspentTxs = {}; // 再次遍历所有区块中的交易输出 for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { String txId = Hex.encodeHexString(transaction.getTxId()); int[] spentOutIndexArray = allSpentTXOs.get(txId); for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) { if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) { continue; } // 保存不存在 allSpentTXOs 中的交易 if (transaction.getOutputs()[outIndex].canBeUnlockedWith(address)) { unspentTxs = ArrayUtils.add(unspentTxs, transaction); } } } } return unspentTxs; } /** * 从交易输入中查询区块链中所有已被花费了的交易输出 * * @param address 钱包地址 * @return 交易ID以及对应的交易输出下标地址 * @throws Exception */ private Map<String, int[]> getAllSpentTXOs(String address) throws Exception { // 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值 Map<String, int[]> spentTXOs = new HashMap<>(); for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) { Block block = blockchainIterator.next(); for (Transaction transaction : block.getTransactions()) { // 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出 if (transaction.isCoinbase()) { continue; } for (TXInput txInput : transaction.getInputs()) { if (txInput.canUnlockOutputWith(address)) { String inTxId = Hex.encodeHexString(txInput.getTxId()); int[] spentOutIndexArray = spentTXOs.get(inTxId); if (spentOutIndexArray == null) { spentTXOs.put(inTxId, new int[]{txInput.getTxOutputIndex()}); } else { spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex()); spentTXOs.put(inTxId, spentOutIndexArray); } } } } } return spentTXOs; } ...} 得到了所有包含UTXO的交易数据,接下来,我们就可以得到所有UTXO集合了: 123456789101112131415161718192021222324252627282930public class Blockchain { ... /** * 查找钱包地址对应的所有UTXO * * @param address 钱包地址 * @return */ public TXOutput[] findUTXO(String address) throws Exception { Transaction[] unspentTxs = this.findUnspentTransactions(address); TXOutput[] utxos = {}; if (unspentTxs == null || unspentTxs.length == 0) { return utxos; } for (Transaction tx : unspentTxs) { for (TXOutput txOutput : tx.getOutputs()) { if (txOutput.canBeUnlockedWith(address)) { utxos = ArrayUtils.add(utxos, txOutput); } } } return utxos; } ... } 现在,我们可以实现获取钱包地址余额的接口了: 123456789101112131415161718192021222324public class CLI { ... /** * 查询钱包余额 * * @param address 钱包地址 */ private void getBalance(String address) throws Exception { Blockchain blockchain = Blockchain.createBlockchain(address); TXOutput[] txOutputs = blockchain.findUTXO(address); int balance = 0; if (txOutputs != null && txOutputs.length > 0) { for (TXOutput txOutput : txOutputs) { balance += txOutput.getValue(); } } System.out.printf("Balance of '%s': %d\\n", address, balance); } ... } 查询 wangwei 这个钱包地址的余额: 1$ ./blockchain.sh getbalance -address wangwei 输出Balance of ‘wangwei’: 10 转账现在,我们想要给某人发送一些币。因此,我们需要创建一笔新的交易,然后放入区块中,再进行挖矿。到目前为止,我们只是实现了 coinbase 交易,现在我们需要实现常见的创建交易接口: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748public class Transaction { ... /** * 从 from 向 to 支付一定的 amount 的金额 * * @param from 支付钱包地址 * @param to 收款钱包地址 * @param amount 交易金额 * @param blockchain 区块链 * @return */ public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { SpendableOutputResult result = blockchain.findSpendableOutputs(from, amount); int accumulated = result.getAccumulated(); Map<String, int[]> unspentOuts = result.getUnspentOuts(); if (accumulated < amount) { throw new Exception("ERROR: Not enough funds"); } Iterator<Map.Entry<String, int[]>> iterator = unspentOuts.entrySet().iterator(); TXInput[] txInputs = {}; while (iterator.hasNext()) { Map.Entry<String, int[]> entry = iterator.next(); String txIdStr = entry.getKey(); int[] outIdxs = entry.getValue(); byte[] txId = Hex.decodeHex(txIdStr); for (int outIndex : outIdxs) { txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, from)); } } TXOutput[] txOutput = {}; txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to)); if (accumulated > amount) { txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from)); } Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(); return newTx; } ... } 在创建新的交易输出之前,我们需要事先找到所有的UTXO,并确保有足够的金额。这就是 findSpendableOutputs 要干的事情。之后,为每个找到的输出创建一个引用它的输入。接下来,我们创建两个交易输出: 一个 output 用于锁定到接收者的钱包地址上。这个是真正被转走的coins; 另一个 output 锁定到发送者的钱包地址上。这个就是 找零。只有当用于支付的UTXO总和大于要支付的金额时,才会创建这部分的 交易输出。记住:交易输出是不可分割的 findSpendableOutputs 需要调用我们之前创建的 findUnspentTransactions 接口: 1234567891011121314151617181920212223242526272829303132333435363738394041424344public class Blockchain { ... /** * 寻找能够花费的交易 * * @param address 钱包地址 * @param amount 花费金额 */ public SpendableOutputResult findSpendableOutputs(String address, int amount) throws Exception { Transaction[] unspentTXs = this.findUnspentTransactions(address); int accumulated = 0; Map<String, int[]> unspentOuts = new HashMap<>(); for (Transaction tx : unspentTXs) { String txId = Hex.encodeHexString(tx.getTxId()); for (int outId = 0; outId < tx.getOutputs().length; outId++) { TXOutput txOutput = tx.getOutputs()[outId]; if (txOutput.canBeUnlockedWith(address) && accumulated < amount) { accumulated += txOutput.getValue(); int[] outIds = unspentOuts.get(txId); if (outIds == null) { outIds = new int[]{outId}; } else { outIds = ArrayUtils.add(outIds, outId); } unspentOuts.put(txId, outIds); if (accumulated >= amount) { break; } } } } return new SpendableOutputResult(accumulated, unspentOuts); } ...} 这个方法会遍历所有的UTXO并统计他们的总额。当计算的总额恰好大于或者等于需要转账的金额时,方法会停止遍历,然后返回用于支付的总额以及按交易ID分组的交易输出索引值数组。我们不想要花更多的钱。 现在,我们可以修改 Block.mineBlock 接口: 123456789101112131415161718192021public class Block { ... /** * 打包交易,进行挖矿 * * @param transactions */ public void mineBlock(Transaction[] transactions) throws Exception { String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash(); if (lastBlockHash == null) { throw new Exception("ERROR: Fail to get last block hash ! "); } Block block = Block.newBlock(lastBlockHash, transactions); this.addBlock(block); } ... } 最后,我们来实现转账的接口: 12345678910111213141516171819202122public class CLI { ... /** * 转账 * * @param from * @param to * @param amount */ private void send(String from, String to, int amount) throws Exception { Blockchain blockchain = Blockchain.createBlockchain(from); Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain); blockchain.mineBlock(new Transaction[]{transaction}); RocksDBUtils.getInstance().closeDB(); System.out.println("Success!"); } ... } 转账,意味着创建一笔新的交易并且通过挖矿的方式将其存入区块中。但是,比特币不会像我们这样做,它会把新的交易记录先存到内存池中,当一个矿工准备去开采一个区块时,它会把打包内存池中的所有交易信息,并且创建一个候选区块。只有当这个包含所有交易信息的候选区块被成功开采并且被添加到区块链上时,这些交易信息才算被确认。 让我们来测试一下: 123456789101112131415161718# 先确认 wangwei 的余额$ ./blockchain.sh getbalance -address wangweiBalance of 'wangwei': 10# 转账$ ./blockchain.sh send -from wangwei -to Pedro -amount 6Elapsed Time: 0.828 seconds correct hash Hex: 00000c5f50cf72db1f375a5d454f98bc49d07335db921cbef5fa9e58ad34d462 Success!# 查询 wangwei 的余额$ ./blockchain.sh getbalance -address wangweiBalance of 'wangwei': 4# 查询 Pedro 的余额$ ./blockchain.sh getbalance -address PedroBalance of 'Pedro': 6 赞!现在让我们来创建更多的交易并且确保从多个交易输出进行转账是正常的: 123456789101112$ ./blockchain.sh send -from Pedro -to Helen -amount 2Elapsed Time: 2.533 seconds correct hash Hex: 00000c81d541ad407a3767ad633d1147602df86fe14e1962ec145ab17b633e88 Success!$ ./blockchain.sh send -from wangwei -to Helen -amount 2Elapsed Time: 1.481 seconds correct hash Hex: 00000c3f8b82c2b970438f5f1f39d56bb8a9d66341efc92a02ffcbff91acd84b Success! 现在,Helen 这个钱包地址上有了两笔从 wangwei 和 Pedro 转账中产生的UTXO,让我们将它们再转账给另外一个人: 1234567891011121314$ ./blockchain.sh send -from Helen -to Rachel -amount 3Elapsed Time: 17.136 seconds correct hash Hex: 000000b1226a947166c2b01a15d1cd3558ddf86fe99bad28a0501a2af60f6a02 Success!$ ./blochchain.sh getbalance -address wangweiBalance of 'wangwei': 2$ ./blochchain.sh getbalance -address Pedro Balance of 'Pedro': 4$ ./blochchain.sh getbalance -address HelenBalance of 'Helen': 1$ ./blochchain.sh getbalance -address RachelBalance of 'Rachel': 3 非常棒!让我们来测试一下失败的场景: 123456$ ./blochchain.sh send -from wangwei -to Ivan -amount 5 java.lang.Exception: ERROR: Not enough funds at one.wangwei.blockchain.transaction.Transaction.newUTXOTransaction(Transaction.java:104) at one.wangwei.blockchain.cli.CLI.send(CLI.java:138) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:73) at one.wangwei.blockchain.cli.Main.main(Main.java:7) 总结本篇内容有点难度,但好歹我们现在有了交易信息了。尽管,缺少像比特币这一类加密货币的一些关键特性: 钱包地址。我们还没有基于私钥的真实地址。 奖励。挖矿绝对没有利润。 UTXO池。当我们计算钱包地址的余额时,我们需要遍历所有的区块中的所有交易信息,当有许许多多的区块时,这将花费不少的时间。此外,如果我们想验证以后的交易,可能需要很长时间。 UTXO迟旨在解决这些问题并快速处理交易。 内存池。 这是交易在打包成区块之前存储的地方。 在我们当前的实现中,一个块只包含一笔交易,而且效率很低。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part4-transaction1 《精通比特币(第二版)》第6章 —— 交易","link":"/2019/01/23/build-blockchain-in-java-transaction-utxo/"},{"title":"JS常用函数","text":"JS自定义监听事件123456789101112131415161718192021222324252627282930event = { // 通过on接口监听事件eventName // 如果事件eventName被触发,则执行callback回调函数 on: function (eventName, callback) { //你的代码 if(!this.handles){ //this.handles={}; Object.defineProperty(this, "handles", { value: {}, enumerable: false, configurable: true, writable: true }) } if(!this.handles[eventName]){ this.handles[eventName]=[]; } this.handles[eventName].push(callback); }, // 触发事件 eventName emit: function (eventName) { //你的代码 if(this.handles[arguments[0]]){ for(var i=0;i<this.handles[arguments[0]].length;i++){ this.handles[arguments[0]][i](arguments[1]); } } }}","link":"/2019/01/23/common-functions-js/"},{"title":"基于Java语言构建区块链(五)—— 地址(钱包)","text":"文章的主要思想和内容均来自 https://jeiwan.cc/posts/building-blockchain-in-go-part-5/ 引言在 上一篇 文章当中,我们开始了交易机制的实现。你已经了解到交易的一些非个人特征:没有用户账户,您的个人数据(例如:姓名、护照号码以及SSN(美国社会安全卡(Social Security Card)上的9 位数字))不是必需的,并且不存储在比特币的任何地方。但仍然必须有一些东西能够识别你是这些交易输出的所有者(例如:锁定在这些输出上的币的所有者)。这就是比特币地址的作用所在。到目前为止,我们只是使用了任意的用户定义的字符串当做地址,现在是时候来实现真正的地址了,就像它们在比特币中实现的一样。 比特币地址这里有一个比特币地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是一个非常早期的比特币地址,据称是属于中本聪的比特币地址。比特币地址是公开的。如果你想要给某人发送比特币,你需要知道对方的比特币地址。但是地址(尽管它是唯一的)并不能作为你是一个钱包所有者的凭证。事实上,这样的地址是公钥的一种可读性更好的表示 。在比特币中,你的身份是存储在你计算机上(或存储在你有权访问的其他位置)的一对(或多对)私钥和公钥。比特币依靠加密算法的组合来创建这些密钥,并保证世界上没有其他人任何人可以在没有物理访问密钥的情况下访问您的比特币。 比特币地址与公钥不同。比特币地址是由公钥经过单向的哈希函数生成的 接下来,让我们来讨论一下这些加密算法。 注意:不要向本篇文章中的代码所生成的任何比特币地址发送真实的比特币来进行测试,否则后果自负…… 公钥密码学公钥加密算法(public-key cryptography)使用的是密钥对:公钥和私钥。公钥属于非敏感信息,可以向任何人透露。相比之下,私钥不能公开披露:除了所有者之外,任何人都不能拥有私钥的权限,因为它是用作所有者标识的私钥。你的私钥代表就是你(当然是在加密货币世界里的)。 本质上,比特币钱包就是一对这样的密钥。当你安装一个钱包应用程序或者使用比特币客户端去生成一个新的地址时,它们就为你创建好了一个密钥对。在比特币种,谁控制了私钥,谁就掌握了所有发往对应公钥地址上所有比特币的控制权。 私钥和公钥只是随机的字节序列,因此它们不能被打印在屏幕上供人读取。这就是为什么比特币会用一种算法将公钥的字节序列转化为人类可读的字符串形式。 如果你曾今使用过比特币钱包的应用程序,它可能会为你生成助记词密码短语。这些助记词可以用来替代私钥,并且能够生成私钥。这种机制是通过 BIP-039 来实现的。 好了,现在我们已经知道在比特币中由什么来决定用户的标识了。但是,比特币是如何校验交易输出(和它里面存储的一些币)的所有权的呢? 数字签名在数学和密码学中,有个数字签名的概念,这套算法保证了以下几点: 保证数据从发送端传递到接收端的过程中不会被篡改; 数据由某个发送者创建; 发送者不能否认发送的数据; 通过对数据应用签名算法(即签署数据),可以得到一个签名,以后可以对其进行验证。数字签名需要使用私钥,而验证则需要公钥。 为了能够签署数据我们需要: 用于被签名的数据; 私钥。 签名操作会产生一个存储在交易输入中的签名。为了能够验证一个签名,我们需要: 签名之后的数据; 签名; 公钥。 简单来讲,这个验证的过程可以被描述为:检查签名是由被签名数据加上私钥得来,并且这个公钥也是由该私钥生成。 数字签名并不是一种加密方法,你无法从签名反向构造出源数据。这个和我们 前面 提到过的Hash算法有点类似:通过对一个数据使用Hash算法,你可以得到该数据的唯一表示。它们两者的不同之处在于,签名算法多了一个密钥对:它让数字签名得以验证成为可能。 但是密钥对也能够用于去加密数据:私钥用于加密数据,公钥用于解密数据。不过比特币并没有使用加密算法。 在比特币中,每一笔交易输入都会被该笔交易的创建者进行签名。比特币中的每一笔交易在放入区块之前都必须得到验证。验证的意思就是: 检查交易输入是否拥有引用前一笔交易中交易输出的权限 检查交易的签名是否正确 数据签名以及签名验证的过程如下图所示:让我们来回顾一下交易的完整生命周期: 最开始,会有一个包含了Coinbase交易的创世区块。由于在Coinbase交易中没有真正的交易输入,所以它不需要签名。Coinbase交易的交易输出会包含一个Hashing之后的公钥(使用的算法为 RIPEMD16(SHA256(PubKey)) ) 当一个人发送比特币时,会创建一笔交易。这笔交易的交易输入会引用前一笔或多笔交易的交易输出。每一个交易输入将会存储未经Hashing处理的公钥以及整个交易的签名信息。 当比特币网络中的其他节点收到其他节点广播的交易数据之后将,将会对其进行验证。其他的事情除外,他们将会验证: 检查交易输入中公钥的Hash值是否与它所引用的交易输出的Hash值想匹配,这是确保发送方只能发送属于他们自己的比特币。 检查签名是否正确,这是为了确保这笔交易是由比特币的真正所有者创建的。 当一个矿工准备开始开采一个新的区块时,他会将交易信息放入区块中,然后开始挖矿。 当一个区块完成挖矿之后,网络中的其他节点将会收到一条区块已挖矿完毕的消息,并且他们会把这个区块添加到区块链中去。 当一个区块被添加到区块链之后,就标志着这笔交易已经完成,它所产生的交易输出将会在新的交易中被引用。 椭圆曲线密码学正如前面所提到的那样,公钥和私钥是一串随机的字符序列。由于私钥是用来识别比特币所有者身份的缘故,因此有一个必要的条件:这个随机算法必须产生真正的随机序列。我们不希望意外地生成其他人所拥有的私钥。也就是要保证随机序列的绝对唯一性。 比特币是使用的椭圆曲线来生成的私钥。椭圆曲线是一个非常复杂的数学概念,这里我们不做详细的介绍(如果你对此非常好奇,可以点击 this gentle introduction to elliptic curves 进行详细的 了解,警告:数学公式)。我们需要知道的是,这些曲线可以用来生成真正大而随机的数字。比特币所采用的曲线算法能够随机生成一个介于0到 2^2^56之间的数字(这是一个非常大的数字,用十进制表示的话,大约是10^77, 而整个可见的宇宙中,原子数在 10^78 到 10^82 之间) 。这么巨大的上限意味着产生两个一样的私钥是几乎不可能的事情。 另外,我们将会使用比特币中所使用的 ECDSA (椭圆曲线数字签名算法)去签署交易信息。 Base58和Base58Check编码现在让我们回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa . 现在我们知道这个地址其实是公钥的一种可读高的表示方式。如果我们对他进行解码,我们会看到公钥看起来是这样子的(字节序列的十六进制的表示方式): 0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93 Base58Base64使用了26个小写字母、26个大写字母、10个数字以及两个符号(例如“+”和“/”),用于在电子邮件这样的基于文本的媒介中传输二进制数据。Base64通常用于编码邮件中的附件。Base58是一种基于文本的二进制编码格式,用在比特币和其它的加密货币中。这种编码格式不仅实现了数据压缩,保持了易读性,还具有错误诊断功能。Base58是Base64编码格式的子集,同样使用大小写字母和10个数字,但舍弃了一些容易错读和在特定字体中容易混淆的字符。具体地,Base58不含Base64中的0(数字0)、O(大写字母o)、l(小写字母L)、I(大写字母i),以及“+”和“/”两个字符。简而言之,Base58就是由不包括(0,O,l,I)的大小写字母和数字组成。 比特币的Base58字母表: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz Base58CheckBase58Check是一种常用在比特币中的Base58编码格式,增加了错误校验码来检查数据在转录中出现的错误。校验码长4个字节,添加到需要编码的数据之后。校验码是从需要编码的数据的哈希值中得到的,所以可以用来检测并避免转录和输入中产生的错误。使用Base58check编码格式时,编码软件会计算原始数据的校验码并和结果数据中自带的校验码进行对比。二者不匹配则表明有错误产生,那么这个Base58Check格式的数据就是无效的。例如,一个错误比特币地址就不会被钱包认为是有效的地址,否则这种错误会造成资金的丢失。 为了使用Base58Check编码格式对数据(数字)进行编码,首先我们要对数据添加一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。例如,比特币地址的前缀是0(十六进制是0x00),而对私钥编码时前缀是128(十六进制是0x80)。 让我们以示意图的形式展示一下从公钥得到地址的过程: 因此,上述解码的公钥由三部分组成: 12Version Public key hash Checksum00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93 由于哈希函数是单向的(也就说无法逆转回去),所以不可能从一个哈希中提取公钥。不过通过执行哈希函数并进行哈希比较,我们可以检查一个公钥是否被用于哈希的生成。 OK,现在我们有了所有的东西,让我们来编写一些代码。 当一些概念被写成代码时,我们会对此理解的更加清晰和深刻。 地址实现让我们从 Wallet 的构成开始,这里我们需要先引入一个maven包: 12345<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.59</version></dependency> 钱包结构 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162/** * 钱包 * * @author wangwei * @date 2018/03/14 */@Data@AllArgsConstructorpublic class Wallet { // 校验码长度 private static final int ADDRESS_CHECKSUM_LEN = 4; /** * 私钥 */ private BCECPrivateKey privateKey; /** * 公钥 */ private byte[] publicKey; public Wallet() { initWallet(); } /** * 初始化钱包 */ private void initWallet() { try { KeyPair keyPair = newECKeyPair(); BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate(); BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic(); byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); this.setPrivateKey(privateKey); this.setPublicKey(publicKeyBytes); } catch (Exception e) { e.printStackTrace(); } } /** * 创建新的密钥对 * * @return * @throws Exception */ private KeyPair newKeyPair() throws Exception { // 注册 BC Provider Security.addProvider(new BouncyCastleProvider()); // 创建椭圆曲线算法的密钥对生成器,算法为 ECDSA KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); // 椭圆曲线(EC)域参数设定 // bitcoin 为什么会选择 secp256k1,详见:https://bitcointalk.org/index.php?topic=151120.0 ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); g.initialize(ecSpec, new SecureRandom()); return g.generateKeyPair(); } } 所谓的钱包,其实本质上就是一个密钥对。这里我们需要借助 KeyPairGenerator 生成密钥对。 接着,我们来生成比特币的钱包地址: 1234567891011121314151617181920212223242526272829303132public class Wallet { ... /** * 获取钱包地址 * * @return */ public String getAddress() throws Exception { // 1. 获取 ripemdHashedKey byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey(); // 2. 添加版本 0x00 ByteArrayOutputStream addrStream = new ByteArrayOutputStream(); addrStream.write((byte) 0); addrStream.write(ripemdHashedKey); byte[] versionedPayload = addrStream.toByteArray(); // 3. 计算校验码 byte[] checksum = BtcAddressUtils.checksum(versionedPayload); // 4. 得到 version + paylod + checksum 的组合 addrStream.write(checksum); byte[] binaryAddress = addrStream.toByteArray(); // 5. 执行Base58转换处理 return Base58Check.rawBytesToBase58(binaryAddress); } ...} 这个时候,你就可以得到 真实的比特币地址 了,并且你可以到 blockchain.info 上去检查这个地址的余额。 例如,通过 getAddress 方法,得到了一个比特币地址为:1rZ9SjXMRwnbW3Pu8itC1HtNBVHERSQhaACbL16 我敢保证,无论你生成多少次比特币地址,它的余额始终为0.这就是为什么选择适当的公钥密码算法如此重要:考虑到私钥是随机数字,产生相同数字的机会必须尽可能低。 理想情况下,它必须低至“永不”。 另外,需要注意的是你不需要连接到比特币的节点上去获取比特币的地址。有关地址生成的开源算法工具包已经有很多编程语言和库实现了。 现在,我们需要去修改交易输入与输出,让他们开始使用真实的地址: 交易输入1234567891011121314151617181920212223242526272829303132333435363738394041/** * 交易输入 * * @author wangwei * @date 2017/03/04 */@Data@AllArgsConstructor@NoArgsConstructorpublic class TXInput { /** * 交易Id的hash值 */ private byte[] txId; /** * 交易输出索引 */ private int txOutputIndex; /** * 签名 */ private byte[] signature; /** * 公钥 */ private byte[] pubKey; /** * 检查公钥hash是否用于交易输入 * * @param pubKeyHash * @return */ public boolean usesKey(byte[] pubKeyHash) { byte[] lockingHash = BtcAddressUtils.ripeMD160Hash(this.getPubKey()); return Arrays.equals(lockingHash, pubKeyHash); }} 交易输出123456789101112131415161718192021222324252627282930313233343536373839404142434445/** * 交易输出 * * @author wangwei * @date 2017/03/04 */@Data@AllArgsConstructor@NoArgsConstructorpublic class TXOutput { /** * 数值 */ private int value; /** * 公钥Hash */ private byte[] pubKeyHash; /** * 创建交易输出 * * @param value * @param address * @return */ public static TXOutput newTXOutput(int value, String address) { // 反向转化为 byte 数组 byte[] versionedPayload = Base58Check.base58ToBytes(address); byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length); return new TXOutput(value, pubKeyHash); } /** * 检查交易输出是否能够使用指定的公钥 * * @param pubKeyHash * @return */ public boolean isLockedWithKey(byte[] pubKeyHash) { return Arrays.equals(this.getPubKeyHash(), pubKeyHash); }} 代码中还有很多其他的地方需要变动,这里不一一指出,详见文末的源码连接。 注意,由于我们不会去实现脚本语言特性,所以我们不再使用 scriptPubKey 和 scriptSig 字段。取而代之的是,我们将 scriptSig 拆分为了 signature 和 pubKey 字段,scriptPubKey 重命名为了 pubKeyHash 。我们将会实现类似于比特币中的交易输出锁定/解锁逻辑和交易输入的签名逻辑,但是我们会在方法中执行此操作。 usesKey 用于检查交易输入中的公钥是否能够解锁交易输出。需要注意的是,交易输入中存储的是未经hash过的公钥,但是方法实现中对它做了一步 ripeMD160Hash 转化。 isLockedWithKey 用于检查提供的公钥Hash是否能够用于解锁交易输出,这个方法是 usesKey 的补充。usesKey 被用于 getAllSpentTXOs 方法中,isLockedWithKey 被用于 findUnspentTransactions 方法中,这样使得在前后两笔交易之间建立起了连接。 newTXOutput 方法中,将 value 锁定到了 address 上。当我们向别人发送比特币时,我们只知道他们的地址,因此函数将地址作为唯一的参数。然后解码地址,并从中提取公钥哈希并保存在PubKeyHash字段中。 现在,让我们一起来检查一下是否能够正常运行: 123456789101112131415161718192021222324252627282930313233343536$ ./blochchain.sh createwalletwallet address : 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh$ ./blochchain.sh createwalletwallet address : 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e$ ./blochchain.sh createwalletwallet address : 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1$ ./blochchain.sh createblockchain -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdhElapsed Time: 6.77 seconds correct hash Hex: 00000e44be0c94c39a4fef24c67d85c428e8bfbd227e292d75c0f4d398e2e81c Done ! $ ./blochchain.sh getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdhBalance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 10$ ./blochchain.sh send -from 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e -to 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVd -amount 5java.lang.Exception: ERROR: Not enough funds$ ./blochchain.sh send -from 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh -to 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e-amount 5Elapsed Time: 4.477 seconds correct hash Hex: 00000da41dfacc8032a553ed5b1aa5e24318d5d89ca14a16c4f70129609c8365 Success!$ ./blochchain.sh getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdhBalance of '13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh': 5$ ./blochchain.sh getbalance -address 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2eBalance of '1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e': 5$ ./blochchain.sh getbalance -address 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1Balance of '19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1': 0 Nice! 现在让我们一起来实现交易签名部分的内容。 签名实现交易数据必须被签名,因为这是比特币中能够保证不能花费属于他人比特币的唯一方法。如果一个签名是无效的,那么这笔交易也是无效的,这样的话,这笔交易就不能被添加到区块链中去。 我们已经有了实现交易签名的所有片段,还有一个事情除外:用于签名的数据。交易数据中哪一部分是真正用于签名的呢?难道是全部数据?选择用于签名的数据相当的重要。用于签名的数据必须包含以独特且唯一的方式标识数据的信息。例如,仅对交易输出签名是没有意义的,因为此签名不会考虑发送发与接收方。 考虑到交易数据要解锁前面的交易输出,重新分配交易输出中的 value 值,并且锁定新的交易输出,因此下面这些数据是必须被签名的: 存储在解锁了的交易输出中的公钥Hash。它标识了交易的发送方。 存储在新的、锁定的交易输出中的公钥Hash。它标识了交易的接收方。 新的交易输出中包含的 value 值。 在比特币中,锁定/解锁逻辑存储在脚本中,解锁脚本存储在交易输入的 ScriptSig 字段中,而锁定脚本存储在交易输出的 ScriptPubKey 的字段中。 由于比特币允许不同类型的脚本,因此它会对ScriptPubKey的全部内容进行签名。 如你所见,我们不需要去对存储在交易输入中的公钥进行签名。正因为如此,在比特币中,所签名的并不是一个交易,而是一个去除部分内容的交易输入副本,交易输入里面存储了被引用交易输出的 ScriptPubKey 。 获取修剪后的交易副本的详细过程在这里. 虽然它可能已经过时了,但是我并没有找到另一个更可靠的来源。 OK,它看起来有点复杂,因此让我们来开始coding吧。我们将从 Sign 方法开始: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354public class Transaction { ... /** * 签名 * * @param privateKey 私钥 * @param prevTxMap 前面多笔交易集合 */ public void sign(BCECPrivateKey privateKey, Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不需要签名,因为它不存在交易输入信息 if (this.isCoinbase()) { return; } // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 创建用于签名的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); ecdsaSign.initSign(privateKey); for (int i = 0; i < txCopy.getInputs().length; i++) { TXInput txInputCopy = txCopy.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 对整个交易信息仅进行签名,即对交易ID进行签名 ecdsaSign.update(txCopy.getTxId()); byte[] signature = ecdsaSign.sign(); // 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名 // 注意是将得到的签名赋值给原交易信息中的交易输入 this.getInputs()[i].setSignature(signature); } } ... } 这个方法需要私钥和前面多笔交易集合作为参数。正如前面所提到的那样,为了能够对交易信息进行签名,我们需要能够访问到被交易数据中的交易输入所引用的交易输出,因此我们需要得到存储这些交易输出的交易信息。 让我们来一步一步review这个方法: 123if (this.isCoinbase()) { return;} 由于 coinbase 交易信息不存在交易输入信息,因此它不需要签名,直接return. 1Transaction txCopy = this.trimmedCopy(); 创建交易的副本 12345678910111213141516171819202122232425262728public class Transaction { ... /** * 创建用于签名的交易数据副本 * * @return */ public Transaction trimmedCopy() { TXInput[] tmpTXInputs = new TXInput[this.getInputs().length]; for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null); } TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length]; for (int i = 0; i < this.getOutputs().length; i++) { TXOutput txOutput = this.getOutputs()[i]; tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash()); } return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs); } ... } 这个交易数据的副本包含了交易输入与交易输出,但是交易输入的 Signature 与 PubKey 需要设置为null。 使用私钥初始化 SHA256withECDSA 签名算法: 123Security.addProvider(new BouncyCastleProvider());Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME);ecdsaSign.initSign(privateKey); 接下来,我们迭代交易副本中的交易输入: 1234567for (TXInput txInput : txCopy.getInputs()) { // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()]; txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); txInputCopy.setSignature(null); 在每一个 txInput中,signature 都需要设置为null(仅仅是为了二次确认检查),并且 pubKey 设置为它所引用的交易输出的 pubKeyHash 字段。在此刻,除了当前的正在循环的交易输入(txInput)外,其他所有的交易输入都是”空的”,也就是说他们的 Signature 和 PubKey 字段被设置为 null。因此,交易输入是被分开签名的,尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入。 Hash 方法对交易进行序列化,并使用 SHA-256 算法进行哈希。哈希后的结果就是我们要签名的数据。在获取完哈希,我们应该重置 PubKey 字段,以便于它不会影响后面的迭代。 123// 得到要签名的数据,即交易IDtxCopy.setTxId(txCopy.hash());txInput.setPubKey(null); 现在,最关键的部分来了: 12345678910// 对整个交易信息仅进行签名,即对交易ID进行签名Security.addProvider(new BouncyCastleProvider());Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME);ecdsaSign.initSign(privateKey);ecdsaSign.update(txCopy.getTxId());byte[] signature = ecdsaSign.sign();// 将整个交易数据的签名赋值给交易输入,因为交易输入需要包含整个交易信息的签名// 注意是将得到的签名赋值给原交易信息中的交易输入this.getInputs()[i].setSignature(signature); 使用 SHA256withECDSA 签名算法加上私钥,来对交易ID进行签名,从而得到了交易输入所要设置的交易签名。 现在,让我们来实现交易的验证功能: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public class Transaction { ... /** * 验证交易信息 * * @param prevTxMap 前面多笔交易集合 * @return */ public boolean verify(Map<String, Transaction> prevTxMap) throws Exception { // coinbase 交易信息不需要签名,也就无需验证 if (this.isCoinbase()) { return true; } // 再次验证一下交易信息中的交易输入是否正确,也就是能否查找对应的交易数据 for (TXInput txInput : this.getInputs()) { if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) { throw new Exception("ERROR: Previous transaction is not correct"); } } // 创建用于签名验证的交易信息的副本 Transaction txCopy = this.trimmedCopy(); Security.addProvider(new BouncyCastleProvider()); ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1"); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME); Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null); // 使用椭圆曲线 x,y 点去生成公钥Key BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33)); BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65)); ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y); ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters); PublicKey publicKey = keyFactory.generatePublic(keySpec); ecdsaVerify.initVerify(publicKey); ecdsaVerify.update(txCopy.getTxId()); if (!ecdsaVerify.verify(txInput.getSignature())) { return false; } } return true; } ...} 首选,同前面签名一样,我们先获取交易的拷贝数据: 1Transaction txCopy = this.trimmedCopy(); 获取椭圆曲线参数和签名类: 1234Security.addProvider(new BouncyCastleProvider());ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); 接下来,我们来检查每一个交易输入的签名是否正确: 1234567891011121314for (int i = 0; i < this.getInputs().length; i++) { TXInput txInput = this.getInputs()[i]; // 获取交易输入TxID对应的交易数据 Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId())); // 获取交易输入所对应的上一笔交易中的交易输出 TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()]; TXInput txInputCopy = txCopy.getInputs()[i]; txInputCopy.setSignature(null); txInputCopy.setPubKey(prevTxOutput.getPubKeyHash()); // 得到要签名的数据,即交易ID txCopy.setTxId(txCopy.hash()); txInputCopy.setPubKey(null);} 这部分与Sign方法中的相同,因为在验证过程中我们需要签署相同的数据。 123456789101112// 使用椭圆曲线 x,y 点去生成公钥KeyBigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);PublicKey publicKey = keyFactory.generatePublic(keySpec);ecdsaVerify.initVerify(publicKey);ecdsaVerify.update(txCopy.getTxId());if (!ecdsaVerify.verify(txInput.getSignature())) { return false;} 由于交易输入中存储的 pubkey ,实际上是椭圆曲线上的一对 x,y 坐标,所以我们可以从 pubKey 得到公钥PublicKey,然后在用公钥去签名进行验证。如果验证成功,则返回true,否则,返回false。 现在,我们需要一个方法来获取以前的交易。 由于这需要与区块链互动,我们将使其成为 blockchain 的一种方法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354public class Blockchain { ... /** * 依据交易ID查询交易信息 * * @param txId 交易ID * @return */ private Transaction findTransaction(byte[] txId) throws Exception { for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) { Block block = iterator.next(); for (Transaction tx : block.getTransactions()) { if (Arrays.equals(tx.getTxId(), txId)) { return tx; } } } throw new Exception("ERROR: Can not found tx by txId ! "); } /** * 进行交易签名 * * @param tx 交易数据 * @param privateKey 私钥 */ public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception { // 先来找到这笔新的交易中,交易输入所引用的前面的多笔交易的数据 Map<String, Transaction> prevTxMap = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction prevTx = this.findTransaction(txInput.getTxId()); prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx); } tx.sign(privateKey, prevTxMap); } /** * 交易签名验证 * * @param tx */ private boolean verifyTransactions(Transaction tx) throws Exception { Map<String, Transaction> prevTx = new HashMap<>(); for (TXInput txInput : tx.getInputs()) { Transaction transaction = this.findTransaction(txInput.getTxId()); prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction); } return tx.verify(prevTx); }} 现在,我们需要对我们的交易进行真正的签名和验证了,交易的签名发生在 newUTXOTransaction 中: 123456789101112public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception { ... Transaction newTx = new Transaction(null, txInputs, txOutput); newTx.setTxId(newTx.hash()); // 进行交易签名 blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); return newTx;} 交易的验证发生在一笔交易被放入区块之前: 12345678910public void mineBlock(Transaction[] transactions) throws Exception { // 挖矿前,先验证交易记录 for (Transaction tx : transactions) { if (!this.verifyTransactions(tx)) { throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! "); } } ...} OK,让我们再一次对整个工程的代码做一个测试,测试结果: 123456789101112131415161718192021222324252627282930313233343536$ ./blochchain.sh createwalletwallet address : 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6$ ./blochchain.sh createwalletwallet address : 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB$ ./blochchain.sh createwalletwallet address : 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f$ ./blochchain.sh createblockchain -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6Elapsed Time: 164.961 seconds correct hash Hex: 00000524231ae1832c49957848d2d1871cc35ff4d113c23be1937c6dff5cdf2a Done ! $ ./blochchain.sh getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 10$ ./blochchain.sh send -from 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -to 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f -amount 5java.lang.Exception: ERROR: Not enough funds$ ./blochchain.sh send -from 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 -to 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -amount 5Elapsed Time: 54.92 seconds correct hash Hex: 00000354f86cde369d4c39d2b3016ac9a74956425f1348b4c26b2cddb98c100b Success!$ ./blochchain.sh getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6Balance of '1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6': 5$ ./blochchain.sh getbalance -address 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGBBalance of '1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB': 5$ ./blochchain.sh getbalance -address 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13fBalance of '13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f': 0 Good!没有任何错误! 让我们注释掉 NewUTXOTransaction 方法中的一行代码,确保未被签名的交易不能被添加到区块中: 12345... // blockchain.signTransaction(newTx, senderWallet.getPrivateKey()); ... 测试结果: 123456java.lang.Exception: Fail to verify transaction ! transaction invalid ! at one.wangwei.blockchain.block.Blockchain.verifyTransactions(Blockchain.java:334) at one.wangwei.blockchain.block.Blockchain.mineBlock(Blockchain.java:76) at one.wangwei.blockchain.cli.CLI.send(CLI.java:202) at one.wangwei.blockchain.cli.CLI.parse(CLI.java:79) at one.wangwei.blockchain.BlockchainTest.main(BlockchainTest.java:23) 说明 WalletUtils 如若抛出异常:Illegal key size or default parameters,请按以下方法进行解决: https://stackoverflow.com/questions/6481627/java-security-illegal-key-size-or-default-parameters 总结这一节,我们学到了: 使用椭圆曲线加密算法,如何去创建钱包; 了解到了如何去生成比特币地址; 如何去对交易信息进行签名并对签名进行验证; 到目前为止,我们已经实现了比特币的许多关键特性! 我们已经实现了除外网络外的几乎所有功能,并且在下一篇文章中,我们将继续完善交易这一环节机制。 资料 源代码:https://github.com/wangweiX/blockchain-java/tree/part5-wallet 《精通比特币(第二版)》—— 第四章 Elliptic Curve Key Pair Generation and Key Factories How to create public key objects with x and y coordinates? Public-key cryptography Digital signatures Elliptic curve Elliptic curve cryptography ECDSA Technical background of Bitcoin addresses Address Base58 A gentle introduction to elliptic curve cryptography","link":"/2019/01/23/build-blockchain-in-java-wallet-address/"},{"title":"PHP踩坑记录","text":"windows 下 php curl error 60下载cacert.pem修改配置文件:php.ini curl.cainfo = “cacert.pem文件路径”。","link":"/2019/01/23/common-questions-php/"},{"title":"Fatal Error Base address marks unusable memory region.","text":"Windows平台php7.2 Opcache 引起的 Fatal Error Base address marks unusable memory region. 异常 修改php.ini 配置 1opcache.mmap_base = 0x20000000 原文地址:https://php.upupw.net/apache/6/660.html","link":"/2020/11/30/fatal-error-base-address-marks-unusable-memory-region/"},{"title":"PHP常用函数","text":"常用排序算法冒泡排序1234567891011121314151617<?phpfunction bubble_sort($arr){ $length = count($arr); if($length<=1){ return $arr; } for($i=0;$i<$length;$i++){ for($j=$length-1;$j>$i;$j--){ if($arr[$j]<$arr[$j-1]){ $tmp = $arr[$j]; $arr[$j] = $arr[$j-1]; $arr[$j-1] = $tmp; } } } return $arr;} 快速排序1234567891011121314151617181920<?phpfunction quick_sort($arr){ $length = count($arr); if($length <=1){ return $arr; } $pivot = $arr[0];//枢轴 $left_arr = array(); $right_arr = array(); for($i=1;$i<$length;$i++){//注意$i从1开始0是枢轴 if($arr[$i]<=$pivot){ $left_arr[] = $arr[$i]; }else{ $right_arr[] = $arr[$i]; } } $left_arr = quick_sort($left_arr);//递归排序左半部分 $right_arr = quick_sort($right_arr);//递归排序右半部份 return array_merge($left_arr,array($pivot),$right_arr);//合并左半部分、枢轴、右半部分} 选择排序12345678910111213141516171819202122<?php//(不稳定)function select_sort($arr){ $length = count($arr); if($length<=1){ return $arr; } for($i=0;$i<$length;$i++){ $min = $i; for($j=$i+1;$j<$length;$j++){ if($arr[$j]<$arr[$min]){ $min = $j; } } if($i != $min){ $tmp = $arr[$i]; $arr[$i] = $arr[$min]; $arr[$min] = $tmp; } } return $arr;} 插入排序1234567891011121314151617<?phpfunction insert_sort($arr){ $length = count($arr); if($length <=1){ return $arr; } for($i=1;$i<$length;$i++){ $x = $arr[$i]; $j = $i-1; while($x<$arr[$j] && $j>=0){ $arr[$j+1] = $arr[$j]; $j--; } $arr[$j+1] = $x; } return $arr;} 常用查找算法二分查找1234567891011121314<?phpfunction binary_search($arr,$low,$high,$key){ while($low<=$high){ $mid = intval(($low+$high)/2); if($key == $arr[$mid]){ return $mid+1; }elseif($key<$arr[$mid]){ $high = $mid-1; }elseif($key>$arr[$mid]){ $low = $mid+1; } } return -1;} 顺序查找12345678910<?phpfunction sq_search($arr,$key){ $length = count($arr); for($i=0;$i<$length;$i++){ if($key == $arr[$i]){ return $i+1; } } return -1;} 常用数据结构线性表的删除123456789101112<?phpfunction delete_array_element($arr,$pos){ $length = count($arr); if($pos<1 || $pos>$length){ return "删除位置出错!"; } for($i=$pos-1;$i<$length-1;$i++){ $arr[$i] = $arr[$i+1]; } array_pop($arr); return $arr;} 约瑟夫环问题12345678910111213141516171819202122<?php//方法一function joseph_ring($n,$m){ $arr = range(1,$n); $i = 0; while(count($arr)>1){ $i=$i+1; $head = array_shift($arr); if($i%$m != 0){ //如果不是则重新压入数组 array_push($arr,$head); } } return $arr[0];}//方法二function joseph_ring2($n,$m){ $r = 0; for($i=2;$i<=$n;$i++){ $r = ($r+$m)%$i; } return $r + 1;} 计算笛卡尔乘积123456789101112131415161718<?phpfunction dikaer(){ $arr = func_get_args(); $arr1 = array(); $result = array_shift($arr); while ($arr2 = array_shift($arr)) { $arr1 = $result; $result = array(); foreach ($arr1 as $v1) { foreach ($arr2 as $v2) { if (!is_array($v1)) $v1 = array($v1); if (!is_array($v2)) $v2 = array($v2); $result[] = array_merge_recursive($v1, $v2); } } } return $result;} 驼峰下划线相互转换12345678910111213141516171819202122232425<?php/** * 下划线转驼峰* 思路:* step1.原字符串转小写,原字符串中的分隔符用空格替换,在字符串开头加上分隔符* step2.将字符串中每个单词的首字母转换为大写,再去空格,去字符串首部附加的分隔符.* @param string $uncamelized_words* @param string $separator* @return string*/function camelize($uncamelized_words,$separator='_'){ $uncamelized_words = $separator. str_replace($separator, " ", strtolower($uncamelized_words)); return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator );}/** * 驼峰命名转下划线命名 * 思路:小写和大写紧挨一起的地方,加上分隔符,然后全部转小写 * @param string $camel_caps * @param string $separator * @return string */function uncamelize($camel_caps,$separator='_'){ return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camel_caps));} 斐波那契算法12345678910<?php/*** 斐波那契算法* @param int $i* @return int*/function fibonacci(int $i){ if ($i < 2) return $i; return fibonacci($i - 2) + fibonacci($i - 1);} 生成随机信用卡号12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455<?php/** * 生成随机信用卡号 * @param string $org * @return mixed|string */function create_creditcard($org = ''){ $prefixs = [ 'bankcard' => [5610, 560221, 560222, 560223, 560224, 560225], 'china_union_pay' => [62], 'diners' => [36, 54, 55, 300, 301, 302, 303, 304, 305], 'discover' => [6011, 644, 645, 646, 647, 648, 649, 65], 'jcb' => [3528, 3589], 'mastercard' => [51, 52, 53, 54, 55], 'visa' => [4485, 4532, 4539, 4556, 4716, 4916, 4929], ]; if ($org && isset($prefixs[$org])) { $ccnumber = array_rand($prefixs[$org]); } else { $prefixs_tmp = []; foreach ($prefixs as $prefix) { $prefixs_tmp = array_merge($prefixs_tmp, $prefix); } $ccnumber = $prefixs_tmp[array_rand($prefixs_tmp)]; echo '<pre>'; print_r($ccnumber); } $length = 16; // generate digits while (strlen($ccnumber) < ($length - 1)) { $ccnumber .= rand(0, 9); } // Calculate sum $sum = 0; $pos = 0; $reversedCCnumber = strrev($ccnumber); while ($pos < $length - 1) { $odd = $reversedCCnumber[$pos] * 2; if ($odd > 9) { $odd -= 9; } $sum += $odd; if ($pos != ($length - 2)) { $sum += $reversedCCnumber[$pos + 1]; } $pos += 2; } // Calculate check digit $checkdigit = ((floor($sum / 10) + 1) * 10 - $sum) % 10; $ccnumber .= $checkdigit; return $ccnumber;} 生成随机身份证号123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333<?php/** * 生成随机身份证号码 * @param $base * @return string */function create_idcard($base){ $content = ''; //身份证起止年月 eg:1990年12月31日 mktime(0,0,0,12,31,1990) $Year_start = mktime(0, 0, 0, 1, 1, 1950); $Year_end = mktime(0, 0, 0, 12, 31, 1992); //全国区域代码 共3131 $Region = array( 110101, 110102, 110105, 110106, 110107, 110108, 110109, 110111, 110112, 110113, 110114, 110115, 110116, 110117, 110228, 110229, 120101, 120102, 120103, 120104, 120105, 120106, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120221, 120223, 120225, 130101, 130102, 130103, 130104, 130105, 130107, 130108, 130121, 130123, 130124, 130125, 130126, 130127, 130128, 130129, 130130, 130131, 130132, 130133, 130181, 130182, 130183, 130184, 130185, 130201, 130202, 130203, 130204, 130205, 130207, 130208, 130209, 130223, 130224, 130225, 130227, 130229, 130281, 130283, 130301, 130302, 130303, 130304, 130321, 130322, 130323, 130324, 130401, 130402, 130403, 130404, 130406, 130421, 130423, 130424, 130425, 130426, 130427, 130428, 130429, 130430, 130431, 130432, 130433, 130434, 130435, 130481, 130501, 130502, 130503, 130521, 130522, 130523, 130524, 130525, 130526, 130527, 130528, 130529, 130530, 130531, 130532, 130533, 130534, 130535, 130581, 130582, 130601, 130602, 130603, 130604, 130621, 130622, 130623, 130624, 130625, 130626, 130627, 130628, 130629, 130630, 130631, 130632, 130633, 130634, 130635, 130636, 130637, 130638, 130681, 130682, 130683, 130684, 130701, 130702, 130703, 130705, 130706, 130721, 130722, 130723, 130724, 130725, 130726, 130727, 130728, 130729, 130730, 130731, 130732, 130733, 130801, 130802, 130803, 130804, 130821, 130822, 130823, 130824, 130825, 130826, 130827, 130828, 130901, 130902, 130903, 130921, 130922, 130923, 130924, 130925, 130926, 130927, 130928, 130929, 130930, 130981, 130982, 130983, 130984, 131001, 131002, 131003, 131022, 131023, 131024, 131025, 131026, 131028, 131081, 131082, 131101, 131102, 131121, 131122, 131123, 131124, 131125, 131126, 131127, 131128, 131181, 131182, 140101, 140105, 140106, 140107, 140108, 140109, 140110, 140121, 140122, 140123, 140181, 140201, 140202, 140203, 140211, 140212, 140221, 140222, 140223, 140224, 140225, 140226, 140227, 140301, 140302, 140303, 140311, 140321, 140322, 140401, 140402, 140411, 140421, 140423, 140424, 140425, 140426, 140427, 140428, 140429, 140430, 140431, 140481, 140501, 140502, 140521, 140522, 140524, 140525, 140581, 140601, 140602, 140603, 140621, 140622, 140623, 140624, 140701, 140702, 140721, 140722, 140723, 140724, 140725, 140726, 140727, 140728, 140729, 140781, 140801, 140802, 140821, 140822, 140823, 140824, 140825, 140826, 140827, 140828, 140829, 140830, 140881, 140882, 140901, 140902, 140921, 140922, 140923, 140924, 140925, 140926, 140927, 140928, 140929, 140930, 140931, 140932, 140981, 141001, 141002, 141021, 141022, 141023, 141024, 141025, 141026, 141027, 141028, 141029, 141030, 141031, 141032, 141033, 141034, 141081, 141082, 141101, 141102, 141121, 141122, 141123, 141124, 141125, 141126, 141127, 141128, 141129, 141130, 141181, 141182, 150101, 150102, 150103, 150104, 150105, 150121, 150122, 150123, 150124, 150125, 150201, 150202, 150203, 150204, 150205, 150206, 150207, 150221, 150222, 150223, 150301, 150302, 150303, 150304, 150401, 150402, 150403, 150404, 150421, 150422, 150423, 150424, 150425, 150426, 150428, 150429, 150430, 150501, 150502, 150521, 150522, 150523, 150524, 150525, 150526, 150581, 150601, 150602, 150621, 150622, 150623, 150624, 150625, 150626, 150627, 150701, 150702, 150721, 150722, 150723, 150724, 150725, 150726, 150727, 150781, 150782, 150783, 150784, 150785, 150801, 150802, 150821, 150822, 150823, 150824, 150825, 150826, 150901, 150902, 150921, 150922, 150923, 150924, 150925, 150926, 150927, 150928, 150929, 150981, 152201, 152202, 152221, 152222, 152223, 152224, 152501, 152502, 152522, 152523, 152524, 152525, 152526, 152527, 152528, 152529, 152530, 152531, 152921, 152922, 152923, 210101, 210102, 210103, 210104, 210105, 210106, 210111, 210112, 210113, 210114, 210122, 210123, 210124, 210181, 210201, 210202, 210203, 210204, 210211, 210212, 210213, 210224, 210281, 210282, 210283, 210301, 210302, 210303, 210304, 210311, 210321, 210323, 210381, 210401, 210402, 210403, 210404, 210411, 210421, 210422, 210423, 210501, 210502, 210503, 210504, 210505, 210521, 210522, 210601, 210602, 210603, 210604, 210624, 210681, 210682, 210701, 210702, 210703, 210711, 210726, 210727, 210781, 210782, 210801, 210802, 210803, 210804, 210811, 210881, 210882, 210901, 210902, 210903, 210904, 210905, 210911, 210921, 210922, 211001, 211002, 211003, 211004, 211005, 211011, 211021, 211081, 211101, 211102, 211103, 211121, 211122, 211201, 211202, 211204, 211221, 211223, 211224, 211281, 211282, 211301, 211302, 211303, 211321, 211322, 211324, 211381, 211382, 211401, 211402, 211403, 211404, 211421, 211422, 211481, 220101, 220102, 220103, 220104, 220105, 220106, 220112, 220122, 220181, 220182, 220183, 220201, 220202, 220203, 220204, 220211, 220221, 220281, 220282, 220283, 220284, 220301, 220302, 220303, 220322, 220323, 220381, 220382, 220401, 220402, 220403, 220421, 220422, 220501, 220502, 220503, 220521, 220523, 220524, 220581, 220582, 220601, 220602, 220605, 220621, 220622, 220623, 220681, 220701, 220702, 220721, 220722, 220723, 220724, 220801, 220802, 220821, 220822, 220881, 220882, 222401, 222402, 222403, 222404, 222405, 222406, 222424, 222426, 230101, 230102, 230103, 230104, 230108, 230109, 230110, 230111, 230112, 230123, 230124, 230125, 230126, 230127, 230128, 230129, 230182, 230183, 230184, 230201, 230202, 230203, 230204, 230205, 230206, 230207, 230208, 230221, 230223, 230224, 230225, 230227, 230229, 230230, 230231, 230281, 230301, 230302, 230303, 230304, 230305, 230306, 230307, 230321, 230381, 230382, 230401, 230402, 230403, 230404, 230405, 230406, 230407, 230421, 230422, 230501, 230502, 230503, 230505, 230506, 230521, 230522, 230523, 230524, 230601, 230602, 230603, 230604, 230605, 230606, 230621, 230622, 230623, 230624, 230701, 230702, 230703, 230704, 230705, 230706, 230707, 230708, 230709, 230710, 230711, 230712, 230713, 230714, 230715, 230716, 230722, 230781, 230801, 230803, 230804, 230805, 230811, 230822, 230826, 230828, 230833, 230881, 230882, 230901, 230902, 230903, 230904, 230921, 231001, 231002, 231003, 231004, 231005, 231024, 231025, 231081, 231083, 231084, 231085, 231101, 231102, 231121, 231123, 231124, 231181, 231182, 231201, 231202, 231221, 231222, 231223, 231224, 231225, 231226, 231281, 231282, 231283, 232721, 232722, 232723, 310101, 310104, 310105, 310106, 310107, 310108, 310109, 310110, 310112, 310113, 310114, 310115, 310116, 310117, 310118, 310120, 310230, 320101, 320102, 320103, 320104, 320105, 320106, 320107, 320111, 320113, 320114, 320115, 320116, 320124, 320125, 320201, 320202, 320203, 320204, 320205, 320206, 320211, 320281, 320282, 320301, 320302, 320303, 320305, 320311, 320312, 320321, 320322, 320324, 320381, 320382, 320401, 320402, 320404, 320405, 320411, 320412, 320481, 320482, 320501, 320505, 320506, 320507, 320508, 320509, 320581, 320582, 320583, 320585, 320601, 320602, 320611, 320612, 320621, 320623, 320681, 320682, 320684, 320701, 320703, 320705, 320706, 320721, 320722, 320723, 320724, 320801, 320802, 320803, 320804, 320811, 320826, 320829, 320830, 320831, 320901, 320902, 320903, 320921, 320922, 320923, 320924, 320925, 320981, 320982, 321001, 321002, 321003, 321012, 321023, 321081, 321084, 321101, 321102, 321111, 321112, 321181, 321182, 321183, 321201, 321202, 321203, 321281, 321282, 321283, 321284, 321301, 321302, 321311, 321322, 321323, 321324, 330101, 330102, 330103, 330104, 330105, 330106, 330108, 330109, 330110, 330122, 330127, 330182, 330183, 330185, 330201, 330203, 330204, 330205, 330206, 330211, 330212, 330225, 330226, 330281, 330282, 330283, 330301, 330302, 330303, 330304, 330322, 330324, 330326, 330327, 330328, 330329, 330381, 330382, 330401, 330402, 330411, 330421, 330424, 330481, 330482, 330483, 330501, 330502, 330503, 330521, 330522, 330523, 330601, 330602, 330621, 330624, 330681, 330682, 330683, 330701, 330702, 330703, 330723, 330726, 330727, 330781, 330782, 330783, 330784, 330801, 330802, 330803, 330822, 330824, 330825, 330881, 330901, 330902, 330903, 330921, 330922, 331001, 331002, 331003, 331004, 331021, 331022, 331023, 331024, 331081, 331082, 331101, 331102, 331121, 331122, 331123, 331124, 331125, 331126, 331127, 331181, 340101, 340102, 340103, 340104, 340111, 340121, 340122, 340123, 340124, 340181, 340201, 340202, 340203, 340207, 340208, 340221, 340222, 340223, 340225, 340301, 340302, 340303, 340304, 340311, 340321, 340322, 340323, 340401, 340402, 340403, 340404, 340405, 340406, 340421, 340501, 340503, 340504, 340506, 340521, 340522, 340523, 340601, 340602, 340603, 340604, 340621, 340701, 340702, 340703, 340711, 340721, 340801, 340802, 340803, 340811, 340822, 340823, 340824, 340825, 340826, 340827, 340828, 340881, 341001, 341002, 341003, 341004, 341021, 341022, 341023, 341024, 341101, 341102, 341103, 341122, 341124, 341125, 341126, 341181, 341182, 341201, 341202, 341203, 341204, 341221, 341222, 341225, 341226, 341282, 341301, 341302, 341321, 341322, 341323, 341324, 341501, 341502, 341503, 341521, 341522, 341523, 341524, 341525, 341601, 341602, 341621, 341622, 341623, 341701, 341702, 341721, 341722, 341723, 341801, 341802, 341821, 341822, 341823, 341824, 341825, 341881, 350101, 350102, 350103, 350104, 350105, 350111, 350121, 350122, 350123, 350124, 350125, 350128, 350181, 350182, 350201, 350203, 350205, 350206, 350211, 350212, 350213, 350301, 350302, 350303, 350304, 350305, 350322, 350401, 350402, 350403, 350421, 350423, 350424, 350425, 350426, 350427, 350428, 350429, 350430, 350481, 350501, 350502, 350503, 350504, 350505, 350521, 350524, 350525, 350526, 350527, 350581, 350582, 350583, 350601, 350602, 350603, 350622, 350623, 350624, 350625, 350626, 350627, 350628, 350629, 350681, 350701, 350702, 350721, 350722, 350723, 350724, 350725, 350781, 350782, 350783, 350784, 350801, 350802, 350821, 350822, 350823, 350824, 350825, 350881, 350901, 350902, 350921, 350922, 350923, 350924, 350925, 350926, 350981, 350982, 360101, 360102, 360103, 360104, 360105, 360111, 360121, 360122, 360123, 360124, 360201, 360202, 360203, 360222, 360281, 360301, 360302, 360313, 360321, 360322, 360323, 360401, 360402, 360403, 360421, 360423, 360424, 360425, 360426, 360427, 360428, 360429, 360430, 360481, 360482, 360501, 360502, 360521, 360601, 360602, 360622, 360681, 360701, 360702, 360721, 360722, 360723, 360724, 360725, 360726, 360727, 360728, 360729, 360730, 360731, 360732, 360733, 360734, 360735, 360781, 360782, 360801, 360802, 360803, 360821, 360822, 360823, 360824, 360825, 360826, 360827, 360828, 360829, 360830, 360881, 360901, 360902, 360921, 360922, 360923, 360924, 360925, 360926, 360981, 360982, 360983, 361001, 361002, 361021, 361022, 361023, 361024, 361025, 361026, 361027, 361028, 361029, 361030, 361101, 361102, 361121, 361122, 361123, 361124, 361125, 361126, 361127, 361128, 361129, 361130, 361181, 370101, 370102, 370103, 370104, 370105, 370112, 370113, 370124, 370125, 370126, 370181, 370201, 370202, 370203, 370205, 370211, 370212, 370213, 370214, 370281, 370282, 370283, 370284, 370285, 370301, 370302, 370303, 370304, 370305, 370306, 370321, 370322, 370323, 370401, 370402, 370403, 370404, 370405, 370406, 370481, 370501, 370502, 370503, 370521, 370522, 370523, 370601, 370602, 370611, 370612, 370613, 370634, 370681, 370682, 370683, 370684, 370685, 370686, 370687, 370701, 370702, 370703, 370704, 370705, 370724, 370725, 370781, 370782, 370783, 370784, 370785, 370786, 370801, 370802, 370811, 370826, 370827, 370828, 370829, 370830, 370831, 370832, 370881, 370882, 370883, 370901, 370902, 370911, 370921, 370923, 370982, 370983, 371001, 371002, 371081, 371082, 371083, 371101, 371102, 371103, 371121, 371122, 371201, 371202, 371203, 371301, 371302, 371311, 371312, 371321, 371322, 371323, 371324, 371325, 371326, 371327, 371328, 371329, 371401, 371402, 371421, 371422, 371423, 371424, 371425, 371426, 371427, 371428, 371481, 371482, 371501, 371502, 371521, 371522, 371523, 371524, 371525, 371526, 371581, 371601, 371602, 371621, 371622, 371623, 371624, 371625, 371626, 371701, 371702, 371721, 371722, 371723, 371724, 371725, 371726, 371727, 371728, 410101, 410102, 410103, 410104, 410105, 410106, 410108, 410122, 410181, 410182, 410183, 410184, 410185, 410201, 410202, 410203, 410204, 410205, 410211, 410221, 410222, 410223, 410224, 410225, 410301, 410302, 410303, 410304, 410305, 410306, 410311, 410322, 410323, 410324, 410325, 410326, 410327, 410328, 410329, 410381, 410401, 410402, 410403, 410404, 410411, 410421, 410422, 410423, 410425, 410481, 410482, 410501, 410502, 410503, 410505, 410506, 410522, 410523, 410526, 410527, 410581, 410601, 410602, 410603, 410611, 410621, 410622, 410701, 410702, 410703, 410704, 410711, 410721, 410724, 410725, 410726, 410727, 410728, 410781, 410782, 410801, 410802, 410803, 410804, 410811, 410821, 410822, 410823, 410825, 410882, 410883, 410901, 410902, 410922, 410923, 410926, 410927, 410928, 411001, 411002, 411023, 411024, 411025, 411081, 411082, 411101, 411102, 411103, 411104, 411121, 411122, 411201, 411202, 411221, 411222, 411224, 411281, 411282, 411301, 411302, 411303, 411321, 411322, 411323, 411324, 411325, 411326, 411327, 411328, 411329, 411330, 411381, 411401, 411402, 411403, 411421, 411422, 411423, 411424, 411425, 411426, 411481, 411501, 411502, 411503, 411521, 411522, 411523, 411524, 411525, 411526, 411527, 411528, 411601, 411602, 411621, 411622, 411623, 411624, 411625, 411626, 411627, 411628, 411681, 411701, 411702, 411721, 411722, 411723, 411724, 411725, 411726, 411727, 411728, 411729, 419001, 420101, 420102, 420103, 420104, 420105, 420106, 420107, 420111, 420112, 420113, 420114, 420115, 420116, 420117, 420201, 420202, 420203, 420204, 420205, 420222, 420281, 420301, 420302, 420303, 420321, 420322, 420323, 420324, 420325, 420381, 420501, 420502, 420503, 420504, 420505, 420506, 420525, 420526, 420527, 420528, 420529, 420581, 420582, 420583, 420601, 420602, 420606, 420607, 420624, 420625, 420626, 420682, 420683, 420684, 420701, 420702, 420703, 420704, 420801, 420802, 420804, 420821, 420822, 420881, 420901, 420902, 420921, 420922, 420923, 420981, 420982, 420984, 421001, 421002, 421003, 421022, 421023, 421024, 421081, 421083, 421087, 421101, 421102, 421121, 421122, 421123, 421124, 421125, 421126, 421127, 421181, 421182, 421201, 421202, 421221, 421222, 421223, 421224, 421281, 421301, 421303, 421321, 421381, 422801, 422802, 422822, 422823, 422825, 422826, 422827, 422828, 429004, 429005, 429006, 429021, 430101, 430102, 430103, 430104, 430105, 430111, 430112, 430121, 430124, 430181, 430201, 430202, 430203, 430204, 430211, 430221, 430223, 430224, 430225, 430281, 430301, 430302, 430304, 430321, 430381, 430382, 430401, 430405, 430406, 430407, 430408, 430412, 430421, 430422, 430423, 430424, 430426, 430481, 430482, 430501, 430502, 430503, 430511, 430521, 430522, 430523, 430524, 430525, 430527, 430528, 430529, 430581, 430601, 430602, 430603, 430611, 430621, 430623, 430624, 430626, 430681, 430682, 430701, 430702, 430703, 430721, 430722, 430723, 430724, 430725, 430726, 430781, 430801, 430802, 430811, 430821, 430822, 430901, 430902, 430903, 430921, 430922, 430923, 430981, 431001, 431002, 431003, 431021, 431022, 431023, 431024, 431025, 431026, 431027, 431028, 431081, 431101, 431102, 431103, 431121, 431122, 431123, 431124, 431125, 431126, 431127, 431128, 431129, 431201, 431202, 431221, 431222, 431223, 431224, 431225, 431226, 431227, 431228, 431229, 431230, 431281, 431301, 431302, 431321, 431322, 431381, 431382, 433101, 433122, 433123, 433124, 433125, 433126, 433127, 433130, 440101, 440103, 440104, 440105, 440106, 440111, 440112, 440113, 440114, 440115, 440116, 440183, 440184, 440201, 440203, 440204, 440205, 440222, 440224, 440229, 440232, 440233, 440281, 440282, 440301, 440303, 440304, 440305, 440306, 440307, 440308, 440401, 440402, 440403, 440404, 440501, 440507, 440511, 440512, 440513, 440514, 440515, 440523, 440601, 440604, 440605, 440606, 440607, 440608, 440701, 440703, 440704, 440705, 440781, 440783, 440784, 440785, 440801, 440802, 440803, 440804, 440811, 440823, 440825, 440881, 440882, 440883, 440901, 440902, 440903, 440923, 440981, 440982, 440983, 441201, 441202, 441203, 441223, 441224, 441225, 441226, 441283, 441284, 441301, 441302, 441303, 441322, 441323, 441324, 441401, 441402, 441421, 441422, 441423, 441424, 441426, 441427, 441481, 441501, 441502, 441521, 441523, 441581, 441601, 441602, 441621, 441622, 441623, 441624, 441625, 441701, 441702, 441721, 441723, 441781, 441801, 441802, 441821, 441823, 441825, 441826, 441827, 441881, 441882, 445101, 445102, 445121, 445122, 445201, 445202, 445221, 445222, 445224, 445281, 445301, 445302, 445321, 445322, 445323, 445381, 450101, 450102, 450103, 450105, 450107, 450108, 450109, 450122, 450123, 450124, 450125, 450126, 450127, 450201, 450202, 450203, 450204, 450205, 450221, 450222, 450223, 450224, 450225, 450226, 450301, 450302, 450303, 450304, 450305, 450311, 450321, 450322, 450323, 450324, 450325, 450326, 450327, 450328, 450329, 450330, 450331, 450332, 450401, 450403, 450404, 450405, 450421, 450422, 450423, 450481, 450501, 450502, 450503, 450512, 450521, 450601, 450602, 450603, 450621, 450681, 450701, 450702, 450703, 450721, 450722, 450801, 450802, 450803, 450804, 450821, 450881, 450901, 450902, 450921, 450922, 450923, 450924, 450981, 451001, 451002, 451021, 451022, 451023, 451024, 451025, 451026, 451027, 451028, 451029, 451030, 451031, 451101, 451102, 451121, 451122, 451123, 451201, 451202, 451221, 451222, 451223, 451224, 451225, 451226, 451227, 451228, 451229, 451281, 451301, 451302, 451321, 451322, 451323, 451324, 451381, 451401, 451402, 451421, 451422, 451423, 451424, 451425, 451481, 460101, 460105, 460106, 460107, 460108, 460201, 460321, 460322, 460323, 469001, 469002, 469003, 469005, 469006, 469007, 469021, 469022, 469023, 469024, 469025, 469026, 469027, 469028, 469029, 469030, 500101, 500102, 500103, 500104, 500105, 500106, 500107, 500108, 500109, 500110, 500111, 500112, 500113, 500114, 500115, 500116, 500117, 500118, 500119, 500223, 500224, 500226, 500227, 500228, 500229, 500230, 500231, 500232, 500233, 500234, 500235, 500236, 500237, 500238, 500240, 500241, 500242, 500243, 510101, 510104, 510105, 510106, 510107, 510108, 510112, 510113, 510114, 510115, 510121, 510122, 510124, 510129, 510131, 510132, 510181, 510182, 510183, 510184, 510301, 510302, 510303, 510304, 510311, 510321, 510322, 510401, 510402, 510403, 510411, 510421, 510422, 510501, 510502, 510503, 510504, 510521, 510522, 510524, 510525, 510601, 510603, 510623, 510626, 510681, 510682, 510683, 510701, 510703, 510704, 510722, 510723, 510724, 510725, 510726, 510727, 510781, 510801, 510802, 510811, 510812, 510821, 510822, 510823, 510824, 510901, 510903, 510904, 510921, 510922, 510923, 511001, 511002, 511011, 511024, 511025, 511028, 511101, 511102, 511111, 511112, 511113, 511123, 511124, 511126, 511129, 511132, 511133, 511181, 511301, 511302, 511303, 511304, 511321, 511322, 511323, 511324, 511325, 511381, 511401, 511402, 511421, 511422, 511423, 511424, 511425, 511501, 511502, 511503, 511521, 511523, 511524, 511525, 511526, 511527, 511528, 511529, 511601, 511602, 511621, 511622, 511623, 511681, 511701, 511702, 511721, 511722, 511723, 511724, 511725, 511781, 511801, 511802, 511803, 511822, 511823, 511824, 511825, 511826, 511827, 511901, 511902, 511921, 511922, 511923, 512001, 512002, 512021, 512022, 512081, 513221, 513222, 513223, 513224, 513225, 513226, 513227, 513228, 513229, 513230, 513231, 513232, 513233, 513321, 513322, 513323, 513324, 513325, 513326, 513327, 513328, 513329, 513330, 513331, 513332, 513333, 513334, 513335, 513336, 513337, 513338, 513401, 513422, 513423, 513424, 513425, 513426, 513427, 513428, 513429, 513430, 513431, 513432, 513433, 513434, 513435, 513436, 513437, 520101, 520102, 520103, 520111, 520112, 520113, 520114, 520121, 520122, 520123, 520181, 520201, 520203, 520221, 520222, 520301, 520302, 520303, 520321, 520322, 520323, 520324, 520325, 520326, 520327, 520328, 520329, 520330, 520381, 520382, 520401, 520402, 520421, 520422, 520423, 520424, 520425, 520502, 520521, 520522, 520523, 520524, 520525, 520526, 520527, 520602, 520603, 520621, 520622, 520623, 520624, 520625, 520626, 520627, 520628, 522301, 522322, 522323, 522324, 522325, 522326, 522327, 522328, 522601, 522622, 522623, 522624, 522625, 522626, 522627, 522628, 522629, 522630, 522631, 522632, 522633, 522634, 522635, 522636, 522701, 522702, 522722, 522723, 522725, 522726, 522727, 522728, 522729, 522730, 522731, 522732, 530101, 530102, 530103, 530111, 530112, 530113, 530114, 530122, 530124, 530125, 530126, 530127, 530128, 530129, 530181, 530301, 530302, 530321, 530322, 530323, 530324, 530325, 530326, 530328, 530381, 530402, 530421, 530422, 530423, 530424, 530425, 530426, 530427, 530428, 530501, 530502, 530521, 530522, 530523, 530524, 530601, 530602, 530621, 530622, 530623, 530624, 530625, 530626, 530627, 530628, 530629, 530630, 530701, 530702, 530721, 530722, 530723, 530724, 530801, 530802, 530821, 530822, 530823, 530824, 530825, 530826, 530827, 530828, 530829, 530901, 530902, 530921, 530922, 530923, 530924, 530925, 530926, 530927, 532301, 532322, 532323, 532324, 532325, 532326, 532327, 532328, 532329, 532331, 532501, 532502, 532503, 532523, 532524, 532525, 532526, 532527, 532528, 532529, 532530, 532531, 532532, 532601, 532622, 532623, 532624, 532625, 532626, 532627, 532628, 532801, 532822, 532823, 532901, 532922, 532923, 532924, 532925, 532926, 532927, 532928, 532929, 532930, 532931, 532932, 533102, 533103, 533122, 533123, 533124, 533321, 533323, 533324, 533325, 533421, 533422, 533423, 540101, 540102, 540121, 540122, 540123, 540124, 540125, 540126, 540127, 542121, 542122, 542123, 542124, 542125, 542126, 542127, 542128, 542129, 542132, 542133, 542221, 542222, 542223, 542224, 542225, 542226, 542227, 542228, 542229, 542231, 542232, 542233, 542301, 542322, 542323, 542324, 542325, 542326, 542327, 542328, 542329, 542330, 542331, 542332, 542333, 542334, 542335, 542336, 542337, 542338, 542421, 542422, 542423, 542424, 542425, 542426, 542427, 542428, 542429, 542430, 542521, 542522, 542523, 542524, 542525, 542526, 542527, 542621, 542622, 542623, 542624, 542625, 542626, 542627, 610101, 610102, 610103, 610104, 610111, 610112, 610113, 610114, 610115, 610116, 610122, 610124, 610125, 610126, 610201, 610202, 610203, 610204, 610222, 610301, 610302, 610303, 610304, 610322, 610323, 610324, 610326, 610327, 610328, 610329, 610330, 610331, 610401, 610402, 610403, 610404, 610422, 610423, 610424, 610425, 610426, 610427, 610428, 610429, 610430, 610431, 610481, 610501, 610502, 610521, 610522, 610523, 610524, 610525, 610526, 610527, 610528, 610581, 610582, 610601, 610602, 610621, 610622, 610623, 610624, 610625, 610626, 610627, 610628, 610629, 610630, 610631, 610632, 610701, 610702, 610721, 610722, 610723, 610724, 610725, 610726, 610727, 610728, 610729, 610730, 610801, 610802, 610821, 610822, 610823, 610824, 610825, 610826, 610827, 610828, 610829, 610830, 610831, 610901, 610902, 610921, 610922, 610923, 610924, 610925, 610926, 610927, 610928, 610929, 611001, 611002, 611021, 611022, 611023, 611024, 611025, 611026, 620101, 620102, 620103, 620104, 620105, 620111, 620121, 620122, 620123, 620201, 620301, 620302, 620321, 620401, 620402, 620403, 620421, 620422, 620423, 620501, 620502, 620503, 620521, 620522, 620523, 620524, 620525, 620601, 620602, 620621, 620622, 620623, 620701, 620702, 620721, 620722, 620723, 620724, 620725, 620801, 620802, 620821, 620822, 620823, 620824, 620825, 620826, 620901, 620902, 620921, 620922, 620923, 620924, 620981, 620982, 621001, 621002, 621021, 621022, 621023, 621024, 621025, 621026, 621027, 621101, 621102, 621121, 621122, 621123, 621124, 621125, 621126, 621201, 621202, 621221, 621222, 621223, 621224, 621225, 621226, 621227, 621228, 622901, 622921, 622922, 622923, 622924, 622925, 622926, 622927, 623001, 623021, 623022, 623023, 623024, 623025, 623026, 623027, 630101, 630102, 630103, 630104, 630105, 630121, 630122, 630123, 632121, 632122, 632123, 632126, 632127, 632128, 632221, 632222, 632223, 632224, 632321, 632322, 632323, 632324, 632521, 632522, 632523, 632524, 632525, 632621, 632622, 632623, 632624, 632625, 632626, 632721, 632722, 632723, 632724, 632725, 632726, 632801, 632802, 632821, 632822, 632823, 640101, 640104, 640105, 640106, 640121, 640122, 640181, 640201, 640202, 640205, 640221, 640301, 640302, 640303, 640323, 640324, 640381, 640401, 640402, 640422, 640423, 640424, 640425, 640501, 640502, 640521, 640522, 650101, 650102, 650103, 650104, 650105, 650106, 650107, 650109, 650121, 650201, 650202, 650203, 650204, 650205, 652101, 652122, 652123, 652201, 652222, 652223, 652301, 652302, 652323, 652324, 652325, 652327, 652328, 652701, 652722, 652723, 652801, 652822, 652823, 652824, 652825, 652826, 652827, 652828, 652829, 652901, 652922, 652923, 652924, 652925, 652926, 652927, 652928, 652929, 653001, 653022, 653023, 653024, 653101, 653121, 653122, 653123, 653124, 653125, 653126, 653127, 653128, 653129, 653130, 653131, 653201, 653221, 653222, 653223, 653224, 653225, 653226, 653227, 654002, 654003, 654021, 654022, 654023, 654024, 654025, 654026, 654027, 654028, 654201, 654202, 654221, 654223, 654224, 654225, 654226, 654301, 654321, 654322, 654323, 654324, 654325, 654326, 659001, 659002, 659003, 659004); $seed = mt_rand(0, 3130);//total of region code $Birth = mt_rand($Year_start, $Year_end); $Birth_format = date('Ymd', $Birth); $suffix_a = mt_rand(0, 9); $suffix_b = mt_rand(0, 9); $suffix_c = mt_rand(0, 9);//male or female if (!$base or mb_strlen($base) <> 6) $base = $Region[$seed] . $Birth_format . $suffix_a . $suffix_b . $suffix_c; $factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; $sums = 0; $suffix_d = ''; for ($i = 0; $i < 17; $i++) { $sums += substr($base, $i, 1) * $factor[$i]; } $mods = $sums % 11;//10X98765432 switch ($mods) { case 0: $suffix_d = '1'; break; case 1: $suffix_d = '0'; break; case 2: $suffix_d = 'x'; break; case 3: $suffix_d = '9'; break; case 4: $suffix_d = '8'; break; case 5: $suffix_d = '7'; break; case 6: $suffix_d = '6'; break; case 7: $suffix_d = '5'; break; case 8: $suffix_d = '4'; break; case 9: $suffix_d = '3'; break; case 10: $suffix_d = '2'; break; } $content .= $base . $suffix_d; return $content;} 判断身份证号格式是否正确1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556<?php/*** 判断身份证号是否正确* @param string $id* @return boolean **/function is_idcard($id){ $id = strtoupper($id); $regx = "/(^\\d{15}$)|(^\\d{17}([0-9]|X)$)/"; $arr_split = array(); if (!preg_match($regx, $id)) { return false; } if (15 == strlen($id)) //检查15位 { $regx = "/^(\\d{6})+(\\d{2})+(\\d{2})+(\\d{2})+(\\d{3})$/"; @preg_match($regx, $id, $arr_split); //检查生日日期是否正确 $dtm_birth = "19" . $arr_split[2] . '/' . $arr_split[3] . '/' . $arr_split[4]; if (!strtotime($dtm_birth)) { return false; } else { return false; } } else //检查18位 { $regx = "/^(\\d{6})+(\\d{4})+(\\d{2})+(\\d{2})+(\\d{3})([0-9]|X)$/"; @preg_match($regx, $id, $arr_split); $dtm_birth = $arr_split[2] . '/' . $arr_split[3] . '/' . $arr_split[4]; if (!strtotime($dtm_birth)) //检查生日日期是否正确 { return false; } else { //检验18位身份证的校验码是否正确。 //校验位按照ISO 7064:1983.MOD 11-2的规定生成,X可以认为是数字10。 $arr_int = array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2); $arr_ch = array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'); $sign = 0; for ($i = 0; $i < 17; $i++) { $b = (int)$id{$i}; $w = $arr_int[$i]; $sign += $b * $w; } $n = $sign % 11; $val_num = $arr_ch[$n]; if ($val_num != substr($id, 17, 1)) { return false; } else { return true; } } }} 生成随机手机号1234567891011121314151617181920212223242526272829<?php/** * 生成随机手机号 * * @return mixed|string */function create_mobile(){ // 中国移动前缀 $mobile_prefixes = [ 1340, 1341, 1342, 1343, 1344, 1345, 1346, 1347, 1348, 135, 136, 137, 138, 139, 147, 150, 151, 152, 157, 158, 159, 172, 178, 182, 183, 184, 187, 188, 198 ]; // 中国联通前缀 $unicom_prefixes = [130, 131, 132, 145, 155, 156, 166, 171, 175, 176, 185, 186]; // 中国电信前缀 $telecom_prefixes = [133, 149, 153, 173, 177, 180, 181, 189, 199]; // 所有前缀 $all_prefixes = $mobile_prefixes + $unicom_prefixes + $telecom_prefixes; $mobile = $all_prefixes[array_rand($all_prefixes)]; while (strlen($mobile) < 11) { $mobile .= mt_rand(0, 9); } return $mobile;} 判断手机号是否正确12345678910111213141516171819<?php/** * @param $mobile * @return false|int * * 移动号段:134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、172、178、182、183、184、187、188、198 * 联通号段:130、131、132、145、155、156、166、171、175、176、185、186 * 电信号段:133、149、153、173、177、180、181、189、199 * 虚拟运营商 * 电信:1700、1701、1702 * 移动:1703、1705、1706 * 联通:1704、1707、1708、1709、171 * 卫星通信:1349 */function is_mobile($mobile){ return preg_match('/^((13[0-9])|(14[5,7,9])|(15[^4])|(18[0-9])|(17[0,1,3,5,6,7,8]))\\d{8}$/', $mobile);} 微信头像合并123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123<?php /** * 微信头像合并 * @param array $pic_list * @param string $save_dir * @return string */function image_merge($pic_list,$save_dir){ $pic_list = array_slice($pic_list, 0, 9); // 只操作前9个图片 $bg_w = 150; // 背景图片宽度 $bg_h = 150; // 背景图片高度 $background = imagecreatetruecolor($bg_w, $bg_h); // 背景图片 $color = imagecolorallocate($background, 202, 201, 201); // 为真彩色画布创建白色背景,再设置为透明 imagefill($background, 0, 0, $color); imageColorTransparent($background, $color); $pic_count = count($pic_list); $lineArr = array(); // 需要换行的位置 $space_x = 3; $space_y = 3; $line_x = 0; $start_x = 0; $start_y = 0; $pic_w = 0; $pic_h = 0; switch ($pic_count) { case 1: // 正中间 $start_x = intval($bg_w / 4); // 开始位置X $start_y = intval($bg_h / 4); // 开始位置Y $pic_w = intval($bg_w / 2); // 宽度 $pic_h = intval($bg_h / 2); // 高度 break; case 2: // 中间位置并排 $start_x = 2; $start_y = intval($bg_h / 4) + 3; $pic_w = intval($bg_w / 2) - 5; $pic_h = intval($bg_h / 2) - 5; $space_x = 5; break; case 3: $start_x = 40; // 开始位置X $start_y = 5; // 开始位置Y $pic_w = intval($bg_w / 2) - 5; // 宽度 $pic_h = intval($bg_h / 2) - 5; // 高度 $lineArr = array(2); $line_x = 4; break; case 4: $start_x = 4; // 开始位置X $start_y = 5; // 开始位置Y $pic_w = intval($bg_w / 2) - 5; // 宽度 $pic_h = intval($bg_h / 2) - 5; // 高度 $lineArr = array(3); $line_x = 4; break; case 5: $start_x = 30; // 开始位置X $start_y = 30; // 开始位置Y $pic_w = intval($bg_w / 3) - 5; // 宽度 $pic_h = intval($bg_h / 3) - 5; // 高度 $lineArr = array(3); $line_x = 5; break; case 6: $start_x = 5; // 开始位置X $start_y = 30; // 开始位置Y $pic_w = intval($bg_w / 3) - 5; // 宽度 $pic_h = intval($bg_h / 3) - 5; // 高度 $lineArr = array(4); $line_x = 5; break; case 7: $start_x = 53; // 开始位置X $start_y = 5; // 开始位置Y $pic_w = intval($bg_w / 3) - 5; // 宽度 $pic_h = intval($bg_h / 3) - 5; // 高度 $lineArr = array(2, 5); $line_x = 5; break; case 8: $start_x = 30; // 开始位置X $start_y = 5; // 开始位置Y $pic_w = intval($bg_w / 3) - 5; // 宽度 $pic_h = intval($bg_h / 3) - 5; // 高度 $lineArr = array(3, 6); $line_x = 5; break; case 9: $start_x = 5; // 开始位置X $start_y = 5; // 开始位置Y $pic_w = intval($bg_w / 3) - 5; // 宽度 $pic_h = intval($bg_h / 3) - 5; // 高度 $lineArr = array(4, 7); $line_x = 5; break; } foreach ($pic_list as $k => $pic_path) { $kk = $k + 1; if (in_array($kk, $lineArr)) { $start_x = $line_x; $start_y = $start_y + $pic_h + $space_y; } /** @var string $image */ $image = file_get_contents($pic_path); $resource = imagecreatefromstring($image); // $start_x,$start_y copy图片在背景中的位置 // 0,0 被copy图片的位置 // $pic_w,$pic_h copy后的高度和宽度,最后两个参数为原始图片宽度和高度,倒数两个参数为copy时的图片宽度和高度 imagecopyresized($background, $resource, $start_x, $start_y, 0, 0, $pic_w, $pic_h, imagesx($resource), imagesy($resource)); $start_x = $start_x + $pic_w + $space_x; } $filename = md5(uniqid()).'.png'; $filepath = $save_dir.$filename; //header("Content-type: image/jpg"); imagegif($background, $filepath); return $filepath;}","link":"/2019/01/23/common-functions-php/"},{"title":"分库分表","text":"问题:为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的? 分析:其实这块肯定是扯到高并发了,因为分库分表一定是为了支撑高并发、数据量大两个问题的。而且现在说实话,尤其是互联网类的公司面试,基本上都会来这么一下,分库分表如此普遍的技术问题,不问实在是不行,而如果你不知道那也实在是说不过去! 剖析:(1)为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?) 说白了,分库分表是两回事儿,大家可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。我先给大家抛出来一个场景。 假如我们现在是一个小创业公司(或者是一个BAT公司刚兴起的一个新部门),现在注册用户就20万,每天活跃用户就1万,每天单表数据量就1000,然后高峰期每秒钟并发请求最多就10。。。天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。 结果没想到我们运气居然这么好,碰上个CEO带着我们走上了康庄大道,业务发展迅猛,过了几个月,注册用户数达到了2000万!每天活跃用户数100万!每天单表数据量10万条!高峰期每秒最大请求达到1000!同时公司还顺带着融资了两轮,紧张了几个亿人民币啊!公司估值达到了惊人的几亿美金!这是小独角兽的节奏! 好吧,没事,现在大家感觉压力已经有点大了,为啥呢?因为每天多10万条数据,一个月就多300万条数据,现在咱们单表已经几百万数据了,马上就破千万了。但是勉强还能撑着。高峰期请求现在是1000,咱们线上部署了几台机器,负载均衡搞了一下,数据库撑1000 QPS也还凑合。但是大家现在开始感觉有点担心了,接下来咋整呢。。。。。。 再接下来几个月,我的天,CEO太牛逼了,公司用户数已经达到1亿,公司继续融资几十亿人民币啊!公司估值达到了惊人的几十亿美金,成为了国内今年最牛逼的明星创业公司!天,我们太幸运了。 但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达50万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的5000~8000!别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了! 好吧,所以看到你这里你差不多就理解分库分表是怎么回事儿了,实际上这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库一定扛不住。 比如你单表都几千万数据了,你确定你能抗住么?绝对不行,单表数据量太大,会极大影响你的sql执行的性能,到了后面你的sql可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。 分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户id来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在200万以内。 分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒1000左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 这就是所谓的分库分表,为啥要分库分表?你明白了吧 (2)用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点? 这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。 比较常见的包括:cobar、TDDL、atlas、sharding-jdbc、mycat cobar:阿里b2b团队开发和开源的,属于proxy层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库join和分页等操作。 TDDL:淘宝团队开发的,属于client层方案。不支持join、多表查询等语法,就是基本的crud语法是ok,但是支持读写分离。目前使用的也不多,因为还依赖淘宝的diamond配置管理系统。 atlas:360开源的,属于proxy层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在5年前了。所以,现在用的公司基本也很少了。 sharding-jdbc:当当开源的,属于client层方案。确实之前用的还比较多一些,因为SQL语法支持也比较多,没有太多限制,而且目前推出到了2.0版本,支持分库分表、读写分离、分布式id生成、柔性事务(最大努力送达型事务、TCC事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从2017年一直到现在,是不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。 mycat:基于cobar改造的,属于proxy层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于sharding jdbc来说,年轻一些,经历的锤炼少一些。 所以综上所述,现在其实建议考量的,就是sharding-jdbc和mycat,这两个都可以去考虑使用。 sharding-jdbc这种client层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc的依赖; mycat这种proxy层方案的缺点在于需要部署,自己及运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。 通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用sharding-jdbc,client层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多; 但是中大型公司最好还是选用mycat这类proxy层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护mycat,然后大量项目直接透明使用即可。 我们,数据库中间件都是自研的,也用过proxy层,后来也用过client层 (3)你们具体是如何对数据库如何进行垂直拆分或水平拆分的? 水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。 垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。 还有表层面的拆分,就是分表,将一个表变成N个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。否则单表数据量越大,SQL性能就越差。一般是200万行左右,不要太多,但是也得看具体你怎么操作,也可能是500万,或者是100万。你的SQL越复杂,就最好让单表行数越少。 好了,无论是分库了还是分表了,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。 你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。 而且这儿还有两种分库分表的方式,一种是按照range来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;或者是按照某个字段hash一下均匀分散,这个较为常用。 range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据 hash分法,好处在于说,可以平均分配没给库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程","link":"/2019/07/23/fenku-fenbiao/"},{"title":"【转】如何提升你的能力?给年轻程序员的几条建议","text":"一转眼工作已有8年,前两天公司一位初入职场的同事希望我给一些建议与经验。我觉得这个话题很有价值,这里以个人的想法与经历写成此文,希望给年轻的开发者们一些启发。 我工作过的公司有4家,NVIDIA, Google, Slide和Glow。其中两家是知名的大公司,Slide我是D轮过后加入的,那时约150人。Glow则是从它第一天创立,一直走到现在。个人的工作也从Developer,Tech Lead,Engineering Manager到CTO。这些经历使我对程序员的个人发展之路有比较全面的看法。 如果你问一个年轻的前端开发人员,你在今后的3年内如何提升自己的能力?他可能会说“我现在对Web前端比较熟悉,但我想深入了解AngularJS,另外React现在发展的很快我也想看一下。之后,我会花时间去学习iOS和Android开发。”看上去不错,但缺乏系统性的目标。或者说,他制定了学什么,但对为什么要学这些并没有仔细的思考。 在技术领域,有太多的东西会迅速的过时,如何利用有限时间,最大化你的长期收益?这里我可以给出几条建议 打造你的工具箱工欲善其事,必先利其器。每个开发者都应该有一把自己的瑞士军刀,在将来漫长的职业生涯中,这些工具可以为你省下宝贵的时间,并帮助你更好的组织个人知识库。举两个例子 一套高效的开发环境 一个信息采集器和一本笔记本 高效的开发环境我们可以从编缉器谈起,这里有IDE vs Text Editor,有Vim vs Emacs,有Sublime vs Atom,那该如何选择呢?在做选择之前,我们先想想自己的目标。我们希望这是一个长期的投资,这款编缉器能被长期使用,在这个过程不断的打磨,使其能完全适合自己的习惯,最大化编缉效率。如果程序员是侠客,编缉器则是他手中的剑。 虽然我是Vim的重度用户,但我觉得当年选择Vim时有欠考虑。如果让我重选一次,我的第一选择会是Emacs,第二选择会是Atom。Emacs已存在30年,社区仍然活跃,其可扩展性在编缉器中无人能出其右。Emacs的脚本语言elisp又是lisp的一种dialect,我觉得对lisp的学习可以提升程序员对编程核心思想的理解。另一个加分点是Emacs由于其本身的高门槛及lisp特质,吸引了大批高质素的程序员,其社区可谓藏龙卧虎,更诞生了像Org-mode这样神级的插件。反观Vim,Vim的精髓在于Mode editing,这是值得学习的,可以极大提高文本编缉的效率。但当你熟悉了这一理念后,我觉得可以转投其他编缉器,因为Vim的架构与Vimscript限制了其扩展性。Emacs通过Evil插件非常完整的支持了Mode editing,其他主流的编辑器也有类似插件,所以你一旦掌握了这个理念,在别的编辑器中也可以发挥作用。可能有人会说没有一个Vim emulator能做到Vim 100%的功能,但重点不在于某条指令是否被移植,而是mode editing思想的精髓能否被移植,我觉得答案是肯定的。 再看Atom vs Sublime,Atom的可扩展性非常好,它的大部分核心功能也是以插件的方式实现,这点与Emacs有异曲同工之妙。并且其开源的特性,使我相信它有比Sublime更持久的生命力。 关于IDE,我的看法是,我不排斥IDE,但每个IDE都是为了某个特定的任务或是编程语言服务的。做为一个有追求的程序员,可以用IDE,但依然需要精通一个强大的通用编缉器。 类似编缉器,高效的开发环境还包括Shell,Launcher,窗口管理器,文档阅读器等等。其中有一部分只需要你化很少的时间就可以完成配置,它们的投资回报率是非常高。 信息采集器和笔记本前者是用来收集别人产生的信息,后者则是收集自己产生的信息。前者一个简单的例子就是浏览器的Bookmark。你需要能随时将一组有用的信息归档,并在未来的某个时刻快速找到它。后者最直观的例子则是Mac OS或是iOS自带的笔记本,这里的目的是能随时随地记录你自己的想法。从本质上讲,就是你需要有一套好用的工具来做你的知识库管理(Knowledge management),也可以说是你知识和思想的外部备份。我个人现在是用Evernote同时来做信息采集与笔记的。如果有一个好的流程,你也完全可以用两个工具来分别把这两件事做好。但我建议你花足够多的时间来思考如何组织你的个人知识库。 以上只是两个典型的例子,你需要做的是发现那些你要长期从事的任务(往往不随技术而改变,也不随公司而改变),将完成这些任务所需的工具调整至最优。再举一个例子,我会留意身边的程序员所用的键盘。只有少部分的程序员会买高端的静电容键盘,比如HHKB。而在我看来,这明显是一笔很划得来的投资,程序员在工作的大部分时间里都需要和键盘打交道,一个舒适的打字体验是非常有收益的,更何况这类高品质的键盘都非常的耐用。 开阔你的视野,构建你的技术体系首先你要给自己设定一个目标,就如同一个公司会设定它的Vision。 目标要够大,这样你才能看到更多的风景。 目标应该设定在解决哪一类问题,而不是精通哪一类技术。技术只是手段,不是目的。 例如,“我要成为iOS developer中的达人”这个目标,就远不如“我要成为前端应用开发的专家”来得有意义。前者学到深处你可能会去钻研iOS framework里各种奇技淫巧,而后者你会开始关注视觉与交互设计,研究各平台间的差异与共同趋势。显然,后者更有助于你的个人发展。 不过即便有了明确的目标,选择哪一类技术学习,如何学习,在信息过载的今天依然是一个难题。常有的观点是应该学习最新的技术,因为老的已经过时,而反对的观点则是新技术还不成熟。我个人的观点是,当初入一个领域时,选择主流技术框架;当你有一定经验后,选择技术时更应该关注背后的推动者,我相信优秀的人和团队总能打造优秀的产品,无论是商业公司还是开源社区。不必太在意技术的新旧,因为可能很快都会成为过去时。你真正要学习的是技术背后的思想。有不少语言与开源项目会写它的Coding philosophy,这是很有意思的,你可以从它们的源代码中去验证这些编程理念。以Python为例,如果你执行import this就会看到它的理念,再如Python中一个著名的开源库Celery,在它的文档有专门一节讲述它的编程理念。它们对你的影响会比这些技术本身来得更深远,这是我给初学者们的一个忠告。同理,我非常推荐读一些优秀开源库或是语言的源代码,例如Python的标准库绝大部分都是用Python实现的,而且可读性非常好。如果学习一门技术仅仅停留在用的层面上,你就还没有完全吸取其中的精华,而且学习的收益会随着技术的过时而消失。 我的另一个学习原则是,在选择学习一门新技术时,最大化它与你现有知识库的差异性。读起来可能有拗口,例如你会Django,接下去你应该去学习Ruby on Rails还是NodeJS? 依据这个原则,你应该学NodeJS,因为它的异步IO模型在理念上与Django的同步模型差异很大,而RoR则与Django更多相似之处。但更好的选择是不要去学另一个Web framework,去学习ZeroMQ或是Redis,这两者对于Web development也非常有帮助,这样就做到了最大化差异。从构建一个程序员的技术理念角度,我会推荐每一个程序至少去了解Lisp或是一门Functional programming language,不管你是否会在可见的未来用到,它们能让你从一个不同的角度看待编程。 最后我建议每个程序员都应该经营一款自己的产品,它可以是一款app,一个网站或是一个开源软件。除非你是一个创业公司的早期员工,不然你可能没有机会将所有学到的技术或是理念都付之实践,有很多人想成为全栈工程师,最快的捷径就是打造一款自己的产品。任何一个设计师都会精心打造自己的Portfolio,但大部分程序员却不会。当评估一个程序员的Coding能力时,我会去看他的Github上是否有出彩的项目,可惜国内绝大部分程序员的Github空空如也,或者只有一些非常简单的程序。我建议大家好好经营自己Github上项目,这不但可以提高你的声誉,对你将来的求职也非常有帮助。当你报怨求职面试时又被问到各种无厘头的程序题时,有没有想过面试官也很无奈,因为他没有任何其他方法得知你的Coding能力究竟如何。如果每一个程序员都有自己的作品,我想程序员的面试会简单许多。 重视沟通能力的培养当被问到“你觉得Junior Developer和Senior Developer最大的差别是什么”时,我最自然的反应是沟通与文档。沟通包括程序员团队内部的沟通,与其他团队的沟通,与Manager的沟通等等。我不认为自己有能力把这些问题非常概括地说清楚,不过我可以给一条建议,那就是先学会和你的Manager沟通,让他来教你其余的部分。许多公司都会设置Manager与组员的1:1,一个有效率的1:1应该大部分时间有组员来主导。这需要你在1:1之前花足够多的时间来考虑要问的问题,并且最好提前1天发给Manager,让他有机会思考答案。许多人对此不太重视,或者只问非常具体的问题而不是一些开放性问题,这样你很难在你的Manager身上学到东西。如果你渐渐懂得如何利用1:1的时间,它很会成为你在工作中单位时间投资回报率最高的活动。 累积你的人脉每个人都明白人脉的重要性,但实际做起来却不容易。参加一些线下的会议或是活动,可能是最直接的扩展人脉的方式之一。可惜大部分人似乎只是去听了一场技术讲座就回家了。当然,这和不少活动的时间安排也有关系,讲座时间排得太满,茶歇时间短,加上有时嘉宾迟到或是没控制好时间,干脆就把茶歇取消了。而实际上,结识一两个同道中人远比听技术讲座有价值。下次去参加这类会议,不妨给你自己设个目标,比如至少加两个同行的微信。之后维系你的人脉可能需要花更多的时间,下了班或是周末找你的朋友们喝个咖啡吧! 另外我觉得每个人都需要一个职场导师,他可以是你第一份工作的导师或是Manager,也可以是你认识的其他前辈。你们需要维系一个非常长期的关系,不止于一家公司,最好贯穿你的整个职业生涯。每当你遇到疑惑时,都可以询求他的建议,我觉得这将是你最宝贵的一笔人脉财富。 寻找发挥你才华的平台最后也是最重要的一步,找到适合你的公司。做为求职者评估一家公司可以看三个方面 公司的发展前景(大公司的话,看所在部门的发展前景) 你将要加入的团队 薪资福利 所以在面试一家公司的时候,你要意识到面试是双向的,公司在面试你的同时,你也在面试这家公司。面试前你应该对这家公司做足功课,准备好一些有质量的问题,比如指出产品中的问题,询问开发流程或是如何做绩效评估。到时你也可以检验一下你的面试官是否合格。 每次选择公司对以上三个方面都应兼顾,但在职业生涯的不同阶段,侧重点不同。比如,在刚刚工作时,加入一个优秀的团队最为重要,他们可以教会你很多东西,提升你的能力。工作5年之后,你需要一个平台施展你的才华,体现个人价值,公司发展前景的重要性迅速提升。当你做出一番成绩,证明了自己的价值之后,逐渐进入收获期,就有了与公司要价的资格。另一方面,团队实力对公司的前景也有很大的影响。 对一个刚毕业,初入职场的同学,一个近几年被问了无数次的问题“我的第一份工作是去创业公司还是大公司?”我的回答仍旧是“加入一个优秀的团队最为重要”。一些知名的大公司,团队的素质是有一定保证,但创业公司则不然,团队素质参差不齐,所以如我前面所说你需要面试这个团队,做出自己的判断。不过除了团队因素之外,我想提一下毕业生去创业公司的几个好处。首先,在刚毕业的一段时间内,经济压力小,是最自由最能承受风险的时期,而这段时间往往不长,所以应把握好这个去创业公司的黄金时段。其次,所有的学生进入大公司后,都会担任初级职位,某种程度上来讲是学校学习的延续,规范有条理,但缺乏独立性和创新性,而这正是中国大部分学生所欠缺的。这方面的能力在一家创业公司可以得到快速锻炼,而在大公司可能要等升到中级职位后才有这方面的机会。个人观点,仅供参考。 小结我觉得步入职场的前3年对今后的发展尤其重要,希望此文能对年轻的程序员们有所帮助。欢迎评论! 原文地址:https://tech.glowing.com/cn/advices-to-junior-developers/","link":"/2019/01/26/how-to-improve-your-ability/"},{"title":"使用git webhook实现代码自动部署","text":"前言每次项目修改完代码后,需要通过一些文件传输工具上传到服务器,显得有些繁琐。希望git push的时候,服务器上的代码也能自动更新,这样也能节省不少时间。 实现原理利用git的hook机制,当每次用户提交了代码后触发一个动作去请求我们自己的服务器,服务器收到通知后将代码重新拉取一下,实现代码的自动部署。 具体实现服务端设置SSH秘钥免密登录 在客户端生成公钥和私钥 1ssh-keygen [-t rsa] [-C "comment"] 将公钥拷贝到需要免密登录的服务端,路径/path/to/.ssh 注意: /path/to/.ssh 文件权限必须为700 /path/to/.ssh/authorized_keys 权限必须为600 公钥文件权限必须为600 在服务端创建一个站点,用来响应hook动作。nginx.conf123456789101112131415161718192021222324server { listen 80; server_name localhost; root /path/to/hook.php; location / { try_files $uri $uri/ /index.php?$query_string; index index.php index.html index.htm; } location ~* ^\\/(upload|plugins|themes)\\/.+\\.(html|php)$ { return 404; } location ~ \\.php(.*)$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_split_path_info ^((?U).+\\.php)(/?.+)$; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; include fastcgi_params; }} hook.php123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145<?php/** * Git钩子 * @author chen */$_GET && safe_filter($_GET);$_POST && safe_filter($_POST);$_REQUEST && safe_filter($_REQUEST);$_COOKIE && safe_filter($_COOKIE);$gitMaps = [ 'xxxx' => [ 'name' => 'xxx', 'email' => 'xxx@qq.com', 'domain' => 'xxx', 'savePath' => '/www/wwwroot/', 'gitPath' => 'git@code.aliyun.com:xxx/xxx.git', ]];$logPath = 'git-webhook.log';$projectName = $_GET['name'] ?? null;if (is_null($projectName) || !array_key_exists($projectName, $gitMaps)){ return false;}extract($gitMaps[$projectName]);/** * @var $name * @var $email * @var $domain * @var $savePath * @var $gitPath */// 判断是否存在.git文件夹$isCloned = is_dir($savePath . $domain . '/.git');if ($isCloned) { $requestBody = file_get_contents("php://input"); if (empty($requestBody)) { return false; } // 解析Git服务器通知过来的JSON信息 $content = json_decode($requestBody, true); // 若是主分支且提交数大于0 if ($content['ref'] == 'refs/heads/master' && $content['total_commits_count'] > 0) { // 拉取更新 shell_exec("cd {$savePath}{$domain} && git pull"); // 修改web路径的权限 shell_exec("chown -R www.www {$savePath}{$domain}"); //记录日志 $log = sprintf('%s在%s向%s项目的%s分支推送了%s个提交', $content['user_name'], date('Y-m-d H:i:s'), $content['repository']['name'], $content['ref'], $content['total_commits_count'] ); file_put_contents($logPath, $log, FILE_APPEND); }} else { //如果本地项目不存在,直接克隆项目 //注:在这里需要设置用户邮箱和用户名,不然后面无法拉去代码 //shell_exec 需要root的权限 shell_exec("git config --global user.email {$email}}"); shell_exec("git config --global user.name {$name}}"); shell_exec("git clone {$gitPath} {$savePath}{$domain}"); shell_exec("chown -R www.www {$savePath}{$domain}"); $log = sprintf('克隆项目'); file_put_contents($logPath, $log, FILE_APPEND);}/** * php防注入和XSS攻击过滤 * @param $arr */function safe_filter(&$arr){ $ra =[ '/([\\x00-\\x08,\\x0b-\\x0c,\\x0e-\\x19])/', '/script/', '/javascript/', '/vbscript/', '/expression/', '/applet/', '/meta/', '/xml/', '/blink/', '/link/', '/style/', '/embed/', '/object/', '/frame/', '/layer/', '/title/', '/bgsound/', '/base/', '/onload/', '/onunload/', '/onchange/', '/onsubmit/', '/onreset/', '/onselect/', '/onblur/', '/onfocus/', '/onabort/', '/onkeydown/', '/onkeypress/', '/onkeyup/', '/onclick/', '/ondblclick/', '/onmousedown/', '/onmousemove/', '/onmouseout/', '/onmouseover/', '/onmouseup/', '/onunload/' ]; if (is_array($arr)) { foreach ($arr as $key => $value) { if (!is_array($value)) { //不对magic_quotes_gpc转义过的字符使用addslashes() //避免双重转义 if (!get_magic_quotes_gpc()) { // 给单引号(')、双引号(")、反斜线(\\) //与 NUL(NULL 字符)加上反斜线转义 $value = addslashes($value); } //删除非打印字符,粗暴式过滤xss可疑字符串 $value = preg_replace($ra, '', $value); //去除 HTML 和 PHP 标记并转换为 HTML 实体 $arr[$key] = htmlentities(strip_tags($value)); } else { safe_filter($arr[$key]); } } }}","link":"/2019/08/19/git-webhook/"},{"title":"IP概览","text":"IP地址的概念IP 地址是我们进行TCP/IP通讯的基础,每个连接到网络上的计算机都必须有一个IP地址。我们目前使用的IP地址是32位的,通常以点分十进制表示。例如: 192.168.0.181。IP地址的格式为: IP地址 = 网络地址 + 主机地址 或者 IP地址=主机地址 + 子网地址 + 主机地址。一个简单的IP地址其实包含了网络地址和主机地址两部分重要的信息。 IP地址基本概念Internet依靠TCP/IP协议,在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的互联。在Internet上,每一个节点都依靠唯一的IP地址互相区分和相互联系。IP地址是一个32位二进制数的地址, 由4个8位字段组成,每个字段之间用点号隔开,用于标识TCP/IP宿主机。 每个IP地址都包含两部分:网络ID和主机ID。网络ID标识在同一个物理网络上的所有宿主机,主机ID 标识该物理网络上的每一个宿主机,于是整个Internet上的每个计算机都依靠各自唯一的IP地址来标识。 IP地址构成了整个Internet的基础,它是如此重要,每一台联网的计算机无权自行设定IP地址,有一个统一的机构—IANA负责对申请的组织分配唯一的网络ID,而该组织可以对自己的网络中的每一个主机分配一个唯一的主机ID,正如一个单位无权决定自己在所属城市的街道名称和门牌号,但可以自主决定本单位内部的各个办公室编号一样。 静态IP与动态IPIP地址是一个32位二进制数的地址,理论上讲,有大约40亿(2的32次方)个可能的地址组合,这似乎是一个很大的地址空间。实际上,根据网络ID和主机ID的不同位数规则,可以将IP地址分为A(7位网络ID和24位主机ID)、B(14位网络ID和16位主机ID)、C(21位网络ID和8位主机ID)三类,由于历史原因和技术发展的差异,A类地址和B类地址几乎分配殆尽,目前能够供全球各国各组织分配的只有C类地址。所以说IP地址是一种非常重要的网络资源。 对于一个设立了因特网服务的组织机构,由于其主机对外开放了诸如WWW、FTP、E-mail等访问服务,通常要对外公布一个固定的IP地址,以方便用户访问。当然,数字IP不便记忆和识别,人们更习惯于通过域名来访问主机,而域名实际上仍然需要被域名服务器(DNS)翻译为IP地址。例如,你的主页地址是www.myhost.com ,用户可以方便地记忆和使用,而域名服务器会将这个域名翻译为101.12.123.234,这才是你在网上的真正地址。 而对于大多数拨号上网的用户,由于其上网时间和空间的离散性,为每个用户分配一个固定的IP地址(静态I P)是非常不可取的,这将造成IP地址资源的极大浪费。因此这些用户通常会在每次拨通ISP的主机后,自动获得一个动态的IP地址,该地址当然不是任意的,而是该ISP申请的网络ID和主机ID的合法区间中的某个地址。拨号用户任意两次连接时的IP地址很可能不同,但是在每次连接时间内IP地址不变。 IP地址类型最初设计者,为了便于网络寻址以及层次化构造网络,每个IP地址包括两个标识(ID),即网络ID和主机ID。同一个物理网络上的所有机器都用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机ID与其对应。 IP地址根据网络ID的不同分为5种类型: A/B/C/D/E。 A class IP:结构: 网络地址(1字节) + 主机地址(3字节)保留: 网络地址的最高位(二进制)必须是0, 值0和127不能使用。范围: 1.x.x.x ~ 126.x.x.x数量: ( ( 2 ** ( 8 - 1 ) ) - 2 ) * ( ( 2 ** 24 ) - 2 ) = 126 * 16,777,214 = 2,113,928,964使用: 国家级 B class IP:结构: 网络地址(2字节) + 主机地址(2字节)保留: 网络地址的最高两位(二进制)必须是10范围: 128.x.x.x ~ 191.x.x.x主机: ( ( 2 ** ( 16 - 2 ) ) ) * ( ( 2 ** 16 ) - 2 ) = 16384 * 65534 = 1,073,709,056使用: 跨国的组织 C class IP:结构: 网络地址(3字节) + 主机地址(1字节)保留: 网络地址的最高三位(二进制)必须是110范围: 192.x.x.x ~ 223.x.x.x主机: ( ( 2 ** ( 24 - 3 ) ) ) * ( ( 2 ** 8 ) - 2 ) = 2097152 * 254 = 532,676,608使用: 企业组织 D class for Multicast:保留: 网络地址的最高四位(二进制)必须是1110作用: 它是一个专门保留的地址, 它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。 E class for Reservation:保留: 网络地址的最高五位(二进制)必须是11110用作: 暂时无用,保留待用。 备注: 全零 0.0.0.0 地址对应于当前主机。全”1”的 255.255.255.255 是当前子网的广播地址。 网络掩码(Net Mask)是做什么用的IP地址必须和一个网络掩码(Net Mask)对应使用, 缺一不可。网络掩码的主要作用是告诉计算机如何从IP地址中析取网络标识和主机标识。A/B/C class 都有缺省的网络掩码, A -> 255.0.0.0, B -> 255.255.0.0, C -> 255.255.255.0 子网掩码(Sub-Net Mask)是做什么用的子网掩码的作用是将一个主机量超过了物理设备的限制,过大的IP网络划分为更多的子网络,而每个子网络的主机数量相对而言维持在一个较少的量上。起到物理设备上的负载均衡以及提高网络的可靠性。其实现是通过设置掩码来将原本属于主机ID的位(bit)借用给网络ID, 从而起到减少主机数量的作用。当通过设置掩码从主机ID来借用位(bit)时, 至少要留下2个位(bit)来做主机ID。因为只留一个位的情况下,全0和全1都没有意义(见前边)。 什么是私有IP地址和保留IP地址私有IP地址和保留IP地址是两个常见的翻译用法。概念是一样的,都是对英文中的Private IP的翻译。我们以下通称为私有IP地址。 实践中证明,并不是每一台联网的计算都需要一个全球唯一的IP地址,同时为了减少对于有限的IP地址资源的消耗,最初设计者在A/B /C class 中各自划分了一些地址范围作为私有地址来使用。 A class: 10.0.0.0 ~ 10.255.255.255 B class: 172.16.0.0 ~ 172.31.255.255 C class: 192.168.0.0 ~ 192.168.255.255 私有IP地址的主要特点: 在全球范围内不具有唯一性,因此不能唯一标识一台联网的计算机。无需担心私有IP地址在全球范围内的冲突问题。 私有IP地址的路由信息不能对外发布,外部的IP数据包无法路由到私有IP地址的计算机上。 IP数据包中的源地址和目的地址是私有IP地址的, 不能在Internet上的路由器间进行存储转发的操作。 IP地址间传输TCP/IP数据包的流程在TCP/IP协议栈的实现中设定了许多的规则,其中有一条就是, 两台联网的主机想直接通讯的话,必须有相同的网络标识和不同的主机标识。具有不同的网络标识的两台主机要想通讯的话必须通过一台中间设备 - 路由器的转发才能实现。 IP地址在我们身边的不同的应用先看看我们目前的主要连接互联网的方式, Dial, ISDN, ADSL, ethernet 等等。其实,只要我们有一个非私有的IP地址,那么我们就可以在 Internet上冲浪了。呜呼,线路那里来呢?所以我们必须每月向网络接入提供商支付Money来获取到线路的使用权,同时会给我们分配一个非私有的IP地址。那么网络接入提供商的非私有IP地址那里来的呢?当然是申请得来的了。 根据中国互联网络信息中心(CNNIC)公布的最新数据显示, 截至2004年9月30日, 我国网民数已居世界第二, 而所拥有的IPv4和 IPv6资源均仅占世界的3%, 不仅远远低于美国, 而且也无法与亚太地区日本的7%和16%相比。而另一方面,在亚太地区已分配IP地址中,我国IP 地址总量只占25%, 居于日本29%和韩国21%之间。看到了吗?这就是发达国家的垄断和霸权,我国分配的IPv4的地址资源甚至不如美国的两个大学分配的IPv4的地址资源多。因此,我们时刻要记住打到美帝国主义… IP地址的分配管理机构: (I CANN)[全球] -> (APNIC)[亚太] -> (CNNIC)[中国] APNIC规定, 亚太地区需要IP地址资源的企业、单位或团体, 均可申请成为其会员。会员单位使用IP地址, 除了每年每个地址要交纳一定的资源占用费外, 每个会员还要根据等级的不同交纳不等的地址使用费。但是在目前IPv4的地址紧张的情况下企业要想申请到B class 的地址的机会很少,看看长宽,使用的都是C class 的IP地址,增加了路由器的设备和维护费用而已。","link":"/2019/01/28/ip-overview/"},{"title":"解决Centos7在Vmware中无法全屏的问题","text":"安装Vmware-Tools启动系统,点击安装VMware Tools装载VMware Tool镜像1mkdir -t auto /dev/cdrom /mnt/cdrom 复制 /mnt/cdrom 目录下的 VMwareTools-*.tar.gz文件到用户目录下,并解压123cp /mnt/cdrom/VMwareTools-10.3.10-12406962.tar.gz ~/cd ~/ tar -zxvf VMwareTools-10.3.10-12406962.tar.gz 进入目录后安装12cd ~/vmware-tools-distrib./vmware-install.pl 安装时可能会出现错误,提示 ipconfig命令 不存在, 需要安装 net-tools1yum -y install net-tools 更改Centos7分辨率进入系统更改分辨率1vim /etc/default/grub 将GRUB_TERMINAL_OUTPUT值由默认的”console”改为”gfxterm”,并添加GRUB_GFXMODEubuntu自带grub2默认是gfxterm, 而centos默认是console,所以只设置GRUB_GFXMODE不生效! 12345678GRUB_TIMEOUT=2GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"GRUB_DEFAULT=savedGRUB_DISABLE_SUBMENU=trueGRUB_TERMINAL_OUTPUT="gfxterm"GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet"GRUB_DISABLE_RECOVERY="true"GRUB_GFXMODE=1440x900,1024x768,640x480 更新grub.cfg新版grub2已经不再使用update-grub这个命令了,改用grub2-mkconfig 1grub2-mkconfig -o /boot/grub2/grub.cfg 重启1reboot","link":"/2020/11/30/jie-jue-centos7-zai-vwmare-zhong-wu-fa-quan-pin-de-wen-ti/"},{"title":"MySql数据库备份与恢复","text":"MySql数据库备份与恢复——使用mysqldump 导入与导出方法总结mysqldump客户端可用来转储数据库或搜集数据库进行备份或将数据转移到另一个sql服务器(不一定是一个mysql服务器)。转储包含创建表和/或装载表的sql语句。如果在服务器上进行备份,并且表均为myisam表,应考虑使用mysqlhotcopy,因为可以更快地进行备份和恢复。本文从四部分介绍了mysql数据备份与恢复:第一,mysql基本常识;第二,数据备份与恢复示例;第三,mysqldump具体参数说明。 一、MySql基本常识(1)连接mysql:(格式: mysql -h主机地址 -u用户名 -p用户密码) 连接到本机上的MYSQL键入命令mysql-uroot -p,回车后提示你输密码,如果刚安装好MYSQL,超级用户root是没有密码的,故直接回车即可进入到MYSQL中了,MYSQL的提示符是:mysql> 连接到远程主机上的MYSQL假设远程主机的IP为:10.0.0.1,用户名为root,密码为12356。则键入以下命令:mysql -h10.0.0.1 -uroot -p12356(注:u与root可以不用加空格,其它也一样) 退出MYSQL命令exit (回车) (2)常见命令: 显示数据库列表:show databases;刚开始时才两个数据库:mysql和test。mysql库很重要它里面有MYSQL的系统信息,我们改密码和新增用户,实际上就是用这个库进行操作。 显示库中的数据表:use mysql; //打开库show tables; 显示数据表的结构:describe 表名;(简写:desc 表名) 建库:create database 库名; 建表:use 库名;create table 表名 (字段设定列表); 删库和删表:drop database 库名;drop table 表名; 将表中记录删除:delete from 表名; 将表中记录清空truncate 表名; 显示表中的记录:select * from 表名; 二、数据备份与恢复(1)备份:从数据库导出数据: 格式:mysqldump -h链接ip -P(大写)端口 -u用户名 -p密码数据库名>d:XX.sql(路劲) 示例:mysqldump -h132.72.192.432 -P3307 -uroot -p8888 htgl>d:\\htgl.sql; (2)备份导出示例: 导出数据和表结构——将特定数据库特定表中的数据和表格结构和数据全部返回mysqldump --u b_user -h 101.3.20.33 -p’H_password’ -P3306 database_di up_subjects > 0101_0630_up_subjects.sql 导出表结构却不导出表数据——只返回特定数据库特定表格的表格结构,不返回数据,添加“-d”命令参数mysqldump --u b_user -h 101.3.20.33 -p’H_password’ -P3306 -d database_di up_subjects > 0101_0630_up_subjects.sql 导出表结构和满足挑顶条件的表数据——只返回特定数据库中特定表的表格结构和满足特定条件的数据mysqldump --u b_user -h 101.3.20.33 -p’H_password’ -P3306 database_di up_subjects --where=” ctime>’2017-01-01’ and ctime<’2017-06-30’” > 0101_0630_up_subjects.sql 导出数据却不导出表结构——只返回特定数据库中特定表格的数据,不返回表格结构,添加“-t”命令参数mysqldump --u b_user -h 101.3.20.33 -p’H_password’ -t -P3306 database_di up_subjects >0101_0630_up_subjects.sql 导出特定数据库的所有表格的表结构及其数据,添加“--databases ”命令参数mysqldump --u b_user -h 101.3.20.33 -p’H_password’ -P3306 --databases test > all_database.sql (3)恢复,导入数据库数据:将导出的本地文件导入到指定数据库 系统命令行格式:mysql -h链接ip -P(大写)端口 -u用户名 -p密码 数据库名 < d:XX.sql(路劲)mysql -uusername -ppassword db1 <tb1tb2.sql 或mysql命令行mysql>user db1;source tb1_tb2.sql; 恢复整个数据库的方法:mysql -u b_user -h 101.3.20.33 -p’H_password’ -P3306 < all_database.sql (4)具体恢复示例: 先登录该MySQL服务器,切换要导入数据的数据具体命令如下:mysql> use test;mysql> source /home/test/0101_0630_up_subjects.sqlQuery OK, 0 rows affected (0.01 sec)……Query OK, 0 rows affected (0.00 sec)Query OK, 9 rows affected (0.00 sec)Records: 9 Duplicates: 0 Warnings: 0注释:表示影响的记录为9行,重复的记录有0行,警告的记录有0个 直接使用系统命令行mysql -u b_user -h 101.3.20.33 -p’H_password’ -P3306 test </home/test/0101_0630_up_subjects.sql 三、mysqldump参数说明 --default-character-set字符集设置mysqldump -uusername -ppassword --default-character-set=gb2312 db1 table1 > tb1.sql --all-databases, -A导出全部数据库mysqldump -uroot -p --all-databases --all-tablespaces,-Y导出全部表空间mysqldump -uroot -p --all-databases --all-tablespaces --no-tablespaces,-y不导出任何表空间信息mysqldump -uroot -p --all-databases --no-tablespaces --add-drop-database每个数据库创建之前添加drop数据库语句mysqldump -uroot -p --all-databases --add-drop-database --add-drop-table每个数据表创建之前添加drop数据表语句(默认为打开状态,使用--skip-add-drop-table取消选项)mysqldump -uroot -p --all-databases (默认添加drop语句)mysqldump -uroot -p --all-databases –skip-add-drop-table (取消drop语句) --add-locks在每个表导出之前增加LOCK TABLES并且之后UNLOCK TABLE。(默认为打开状态,使用--skip-add-locks取消选项)mysqldump -uroot -p --all-databases(默认添加LOCK语句)mysqldump -uroot -p --all-databases –skip-add-locks(取消LOCK语句) --allow-keywords允许创建是关键词的列名字。这由表名前缀于每个列名做到mysqldump -uroot -p --all-databases --allow-keywords --apply-slave-statements在’CHANGE MASTER’前添加’STOP SLAVE’,并且在导出的最后添加’START SLAVE’mysqldump -uroot -p --all-databases --apply-slave-statements --character-sets-dir字符集文件的目录mysqldump -uroot -p --all-databases --character-sets-dir=/usr/local/mysql/share/mysql/charsets --comments附加注释信息。默认为打开,可以用--skip-comments取消mysqldump -uroot -p --all-databases (默认记录注释)mysqldump -uroot -p --all-databases --skip-comments (取消注释) --compatible导出的数据将和其它数据库或旧版本的MySQL 相兼容。值可以为ansi、mysql323、mysql40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_tables_options、no_field_options等,要使用几个值,用逗号将它们隔开。它并不保证能完全兼容,而是尽量兼容。mysqldump -uroot -p --all-databases --compatible=ansi --compact导出更少的输出信息(用于调试)。去掉注释和头尾等结构。可以使用选项:--skip-add-drop-table --skip-add-locks --skip-comments --skip-disable-keysmysqldump -uroot -p --all-databases --compact --complete-insert, -c使用完整的insert语句(包含列名称)。这么做能提高插入效率,但是可能会受到max_allowed_packet参数的影响而导致插入失败。mysqldump -uroot -p --all-databases --complete-insert --compress, -C在客户端和服务器之间启用压缩传递所有信息mysqldump -uroot -p --all-databases --compress --create-options, -a在CREATE TABLE语句中包括所有MySQL特性选项。(默认为打开状态)mysqldump -uroot -p --all-databases --databases, -B导出几个数据库。参数后面所有名字参量都被看作数据库名。mysqldump -uroot -p --databases test mysql --debug输出debug信息,用于调试。默认值为:d:t,/tmp/mysqldump.tracemysqldump -uroot -p --all-databases --debugmysqldump -uroot -p --all-databases --debug=” d:t,/tmp/debug.trace” --debug-check检查内存和打开文件使用说明并退出。mysqldump -uroot -p --all-databases --debug-check --debug-info输出调试信息并退出mysqldump -uroot -p --all-databases --debug-info --default-character-set设置默认字符集,默认值为utf8mysqldump -uroot -p --all-databases --default-character-set=utf8 --delayed-insert采用延时插入方式(INSERT DELAYED)导出数据mysqldump -uroot -p --all-databases --delayed-insert --delete-master-logsmaster备份后删除日志. 这个参数将自动激活--master-data。mysqldump -uroot -p --all-databases --delete-master-logs --disable-keys对于每个表,用/*!40000 ALTER TABLE tbl_name DISABLE KEYS /;和/!40000ALTER TABLE tbl_name ENABLE KEYS */;语句引用INSERT语句。这样可以更快地导入dump出来的文件,因为它是在插入所有行后创建索引的。该选项只适合MyISAM表,默认为打开状态。mysqldump -uroot -p --all-databases --dump-slave该选项将主的binlog位置和文件名追加到导出数据的文件中(show slave status)。设置为1时,将会以CHANGE MASTER命令输出到数据文件;设置为2时,会在change前加上注释。该选项将会打开--lock-all-tables,除非--single-transaction被指定。该选项会自动关闭--lock-tables选项。默认值为0。mysqldump -uroot -p --all-databases --dump-slave=1mysqldump -uroot -p --all-databases --dump-slave=2 --master-data该选项将当前服务器的binlog的位置和文件名追加到输出文件中(show master status)。如果为1,将会输出CHANGE MASTER 命令;如果为2,输出的CHANGE MASTER命令前添加注释信息。该选项将打开--lock-all-tables 选项,除非--single-transaction也被指定(在这种情况下,全局读锁在开始导出时获得很短的时间;其他内容参考下面的--single-transaction选项)。该选项自动关闭--lock-tables选项。mysqldump -uroot -p --host=localhost --all-databases --master-data=1;mysqldump -uroot -p --host=localhost --all-databases --master-data=2; --events, -E导出事件。mysqldump -uroot -p --all-databases --events --extended-insert, -e使用具有多个VALUES列的INSERT语法。这样使导出文件更小,并加速导入时的速度。默认为打开状态,使用 --skip-extended-insert取消选项。mysqldump -uroot -p --all-databasesmysqldump -uroot -p --all-databases--skip-extended-insert (取消选项) --fields-terminated-by导出文件中忽略给定字段。与--tab选项一起使用,不能用于--databases和--all-databases选项mysqldump -uroot -p test test --tab=”/home/mysql” --fields-terminated-by=”#”--fields-enclosed-by输出文件中的各个字段用给定字符包裹。与--tab选项一起使用,不能用于--databases和--all-databases选项mysqldump -uroot -p test test --tab=”/home/mysql” --fields-enclosed-by=”#”--fields-optionally-enclosed-by输出文件中的各个字段用给定字符选择性包裹。与--tab选项一起使用,不能用于--databases和--all-databases选项mysqldump -uroot -p test test --tab=”/home/mysql” --fields-enclosed-by=”#” --fields-optionally-enclosed-by =”#”--fields-escaped-by输出文件中的各个字段忽略给定字符。与--tab选项一起使用,不能用于--databases和--all-databases选项mysqldump -uroot -p mysql user --tab=”/home/mysql” --fields-escaped-by=”#” --flush-logs开始导出之前刷新日志。请注意:假如一次导出多个数据库(使用选项--databases或者--all-databases),将会逐个数据库刷新日志。除使用--lock-all-tables或者--master-data外。在这种情况下,日志将会被刷新一次,相应的所以表同时被锁定。因此,如果打算同时导出和刷新日志应该使用--lock-all-tables 或者--master-data 和--flush-logs。mysqldump -uroot -p --all-databases --flush-logs --flush-privileges在导出mysql数据库之后,发出一条FLUSH PRIVILEGES 语句。为了正确恢复,该选项应该用于导出mysql数据库和依赖mysql数据库数据的任何时候。mysqldump -uroot -p --all-databases --flush-privileges --force在导出过程中忽略出现的SQL错误。mysqldump -uroot -p --all-databases --force --help显示帮助信息并退出。mysqldump --help --hex-blob使用十六进制格式导出二进制字符串字段。如果有二进制数据就必须使用该选项。影响到的字段类型有BINARY、VARBINARY、BLOB。mysqldump -uroot -p --all-databases --hex-blob --host, -h需要导出的主机信息mysqldump -uroot -p --host=localhost --all-databases --ignore-table不导出指定表。指定忽略多个表时,需要重复多次,每次一个表。每个表必须同时指定数据库和表名。例如:--ignore-table=database.table1 --ignore-table=database.table2 ……mysqldump -uroot -p --host=localhost --all-databases --ignore-table=mysql.user--include-master-host-port在--dump-slave产生的’CHANGE MASTER TO..’语句中增加’MASTER_HOST=,MASTER_PORT=‘mysqldump -uroot -p --host=localhost --all-databases --include-master-host-port --insert-ignore在插入行时使用INSERT IGNORE语句.mysqldump -uroot -p --host=localhost --all-databases --insert-ignore --lines-terminated-by输出文件的每行用给定字符串划分。与--tab选项一起使用,不能用于--databases和--all-databases选项。mysqldump -uroot -p --host=localhost test test --tab=”/tmp/mysql” --lines-terminated-by=”##” --lock-all-tables, -x提交请求锁定所有数据库中的所有表,以保证数据的一致性。这是一个全局读锁,并且自动关闭--single-transaction 和--lock-tables 选项。mysqldump -uroot -p --host=localhost --all-databases --lock-all-tables --lock-tables, -l开始导出前,锁定所有表。用READ LOCAL锁定表以允许MyISAM表并行插入。对于支持事务的表例如InnoDB和BDB,--single-transaction是一个更好的选择,因为它根本不需要锁定表。请注意当导出多个数据库时,--lock-tables分别为每个数据库锁定表。因此,该选项不能保证导出文件中的表在数据库之间的逻辑一致性。不同数据库表的导出状态可以完全不同。mysqldump -uroot -p --host=localhost --all-databases --lock-tables --log-error附加警告和错误信息到给定文件mysqldump -uroot -p --host=localhost --all-databases --log-error=/tmp/mysqldump_error_log.err --max_allowed_packet服务器发送和接受的最大包长度。mysqldump -uroot -p --host=localhost --all-databases --max_allowed_packet=10240 --net_buffer_lengthTCP/IP和socket连接的缓存大小。mysqldump -uroot -p --host=localhost --all-databases --net_buffer_length=1024 --no-autocommit使用autocommit/commit 语句包裹表。mysqldump -uroot -p --host=localhost --all-databases --no-autocommit --no-create-db, -n只导出数据,而不添加CREATE DATABASE 语句。mysqldump -uroot -p --host=localhost --all-databases --no-create-db --no-create-info, -t只导出数据,而不添加CREATE TABLE 语句。mysqldump -uroot -p --host=localhost --all-databases --no-create-info --no-data, -d不导出任何数据,只导出数据库表结构。mysqldump -uroot -p --host=localhost --all-databases --no-data --no-set-names, -N等同于--skip-set-charsetmysqldump -uroot -p --host=localhost --all-databases --no-set-names --opt等同于--add-drop-table, --add-locks, --create-options, --quick, --extended-insert,--lock-tables, --set-charset,--disable-keys 该选项默认开启, 可以用--skip-opt禁用.mysqldump -uroot -p --host=localhost --all-databases --opt --order-by-primary如果存在主键,或者第一个唯一键,对每个表的记录进行排序。在导出MyISAM表到InnoDB表时有效,但会使得导出工作花费很长时间。mysqldump -uroot -p --host=localhost --all-databases --order-by-primary --password, -p连接数据库密码 --pipe(windows系统可用)使用命名管道连接mysqlmysqldump -uroot -p --host=localhost --all-databases --pipe --port, -P连接数据库端口号 --protocol使用的连接协议,包括:tcp, socket, pipe, memory.mysqldump -uroot -p --host=localhost --all-databases --protocol=tcp --quick, -q不缓冲查询,直接导出到标准输出。默认为打开状态,使用--skip-quick取消该选项。mysqldump -uroot -p --host=localhost --all-databasesmysqldump -uroot -p --host=localhost --all-databases --skip-quick --quote-names,-Q使用(`)引起表和列名。默认为打开状态,使用--skip-quote-names取消该选项。mysqldump -uroot -p --host=localhost --all-databasesmysqldump -uroot -p --host=localhost --all-databases --skip-quote-names --replace使用REPLACE INTO 取代INSERT INTO.mysqldump -uroot -p --host=localhost --all-databases --replace --result-file, -r直接输出到指定文件中。该选项应该用在使用回车换行对(\\r\\n)换行的系统上(例如:DOS,Windows)。该选项确保只有一行被使用。mysqldump -uroot -p --host=localhost --all-databases--result-file=/tmp/mysqldump_result_file.txt --routines, -R导出存储过程以及自定义函数。mysqldump -uroot -p --host=localhost --all-databases --routines --set-charset添加’SET NAMES default_character_set’到输出文件。默认为打开状态,使用--skip-set-charset关闭选项。mysqldump -uroot -p --host=localhost --all-databasesmysqldump -uroot -p --host=localhost --all-databases --skip-set-charset --single-transaction该选项在导出数据之前提交一个BEGIN SQL语句,BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态。它只适用于多版本存储引擎,仅InnoDB。本选项和--lock-tables 选项是互斥的,因为LOCK TABLES 会使任何挂起的事务隐含提交。要想导出大表的话,应结合使用--quick 选项。mysqldump -uroot -p --host=localhost --all-databases --single-transaction --dump-date将导出时间添加到输出文件中。默认为打开状态,使用--skip-dump-date关闭选项。mysqldump -uroot -p --host=localhost --all-databasesmysqldump -uroot -p --host=localhost --all-databases --skip-dump-date --skip-opt禁用–opt选项.mysqldump -uroot -p --host=localhost --all-databases --skip-opt --socket,-S指定连接mysql的socket文件位置,默认路径/tmp/mysql.sockmysqldump -uroot -p --host=localhost --all-databases --socket=/tmp/mysqld.sock --tab,-T为每个表在给定路径创建tab分割的文本文件。注意:仅仅用于mysqldump和mysqld服务器运行在相同机器上。注意使用--tab不能指定--databases参数mysqldump -uroot -p --host=localhost test test --tab=”/home/mysql” --tables覆盖--databases (-B)参数,指定需要导出的表名,在后面的版本会使用table取代tables。mysqldump -uroot -p --host=localhost --databases test --tables test --triggers导出触发器。该选项默认启用,用--skip-triggers禁用它。mysqldump -uroot -p --host=localhost --all-databases --triggers --tz-utc在导出顶部设置时区TIME_ZONE=’+00:00’ ,以保证在不同时区导出的TIMESTAMP 数据或者数据被移动其他时区时的正确性。mysqldump -uroot -p --host=localhost --all-databases --tz-utc --user, -u指定连接的用户名。 --verbose, --v输出多种平台信息。 --version, -V输出mysqldump版本信息并退出 --where, -w只转储给定的WHERE条件选择的记录。请注意如果条件包含命令解释符专用空格或字符,一定要将条件引用起来。mysqldump -uroot -p --host=localhost --all-databases --where=” user=’root’” --xml, -X导出XML格式.mysqldump -uroot -p --host=localhost --all-databases --xml --plugin_dir客户端插件的目录,用于兼容不同的插件版本。mysqldump -uroot -p --host=localhost --all-databases--plugin_dir=”/usr/local/lib/plugin” --default_auth客户端插件默认使用权限。mysqldump -uroot -p --host=localhost --all-databases--default-auth=”/usr/local/lib/plugin/”","link":"/2018/12/07/mysql-mysqldump/"},{"title":"如何做到自律","text":"原文链接地址:如何逼自己做到真正的自律? - 黛西巫巫的回答 - 知乎 这篇回答会颠覆你的人生观哦。 如果你总是喜欢拖延,晚上躺床上玩手机不想闭眼,做事三分钟热度,偶尔想坚持努力,过不了几天就被打回原形,不要怕,你不是真的懒,而是没有找到方法。今天方法来了,刷到这篇回答的你很幸运。我帮你找到适合你的自律方法,像玩手机和玩游戏一起轻松地实现自律。 我自己就是这套方法的受益者。曾经我也是个死肥宅,又胖又丑,每次照镜子都觉得自己恶心,连男朋友都找不到。后来我下定决心,一定要瘦下来,一日三餐只吃果蔬沙拉,坚持每天去健身房跑步30分钟做20分钟Hit燃脂训练。不到半年时间,我的身材发生了明显的变化: 腰瘦了,胸型也好看了很多~接下来开始进入正题,文章有点长,4000多字,但请答应我,咬牙读完,腰挺直认真看,一定可以帮你改变糟糕的生活。 先认识什么是真正的自律6点半起床到图书馆,低头看书学习到午饭时间,吃饱后在桌子上趴一会,中午两点开始继续学习到晚上10点,除了晚饭外都在看书。周一到周六都机械般重复上面的事。你觉得这是自律吗?不是,这跟流水线生产东西一样,只是一种流程。 这种生活让人难以忍受,枯燥无味,大多数人坚持一段时间后都会放弃,不具备普适性。 那么,什么是真正的自律?首先,你得想明白自己要什么。有些人的自律之所以是假的,就是因为他们没有明确的目标,他们之所以给自己制定了很多计划,只是因为感觉人就应该努力,不努力就是堕落。然后偶尔努力一下就会有一段时间感觉到踏实,但过几天又会因为坚持不下去堕落了。所谓的间歇性努力持续性混吃等死,基本就是这样产生的。所以,你要给自己定一个目标,让欲望和动力去驱动你,而不是焦虑和自责,当每天唤醒你的是梦想而不是闹钟,你就开始明白什么是真正的自律了。 说完什么是自律,我们再来讲如何让自己变得自律起来。 “清华北大只是过程,不是目的。” “人生就像射箭,梦想就像箭靶子,如果没有箭靶子,你每天的拉弓就毫无意义。” 《银河补习班》里的邓超希望儿子能想明白自己要什么,说了这两句话。 要想自律,你先要找到自己的箭靶子。而这箭靶子,也就是我们的自律驱动器。 如何找到你的自律驱动器? 首先要理性地面对自己的欲望。 一个经常健身,不吃油炸食品,八块腹肌的帅哥,他之所以自律,是因为喜欢别人看他时羡慕的眼神,是因为他爬10楼楼梯都不累时的那种从容,这些欲望让他战胜了大汗淋漓时想放弃的心。一个清华北大的学子每天依旧刻苦学习,他之所以自律,是因为想要改命的决心,是因为他希望父母看到自己功成名就时能开心地笑,上清华和北大不是他的目的,通过努力而改变自己的命运,让家人过上更好的生活,这些才是他的目的,也正是因为这些欲望让他战胜了懒惰和不想学习的心。那么,你有什么欲望呢?比如,我想发财让家人过上更好的生活、我想拥有一个好的身体能在床上更持久让伴侣开心等等。 接着,直面你的欲望,然后把你的欲望具体化。 最好精确到能用数字表述出来。 比如,我想3年赚100万,给家人买1套房,我想坚持30分钟。然后,把你足够具体的欲望写下来,可以放在床头,也可以放在手机备忘录里,只要你能经常看到就行。 把目标细分化。 比如说,你想3年赚100万,那么你每个月要存2.8万,然后,你需要掌握什么技能,升到什么职位,完成这些需要做哪些事,细分到每一天去,只有当你足够想要,你才能得到。然后,在执行的过程中,你还要不断提醒自己最初的计划,因为如果你一个星期没思考的话很容易就忘,千万不要对你的大脑过度自信,重复提醒自己才能让你最终收获到果实。接着,像我最开始说的那样,不要把计划做得太机械,而且也不要有过度完美主义,当出现问题时,平静沉稳地解决就行,不要指责自己,继续下去。 能读到这里的人都是愿意深度思考的优秀者,我想请你帮个忙,如果内容对你有启发,你可以花1秒钟点个赞后再继续阅读。 下面我再分享一些私货,你可以提起精神来,重点内容到啦! 一些让你像玩手机一样轻松的自律方法 用「方式替换」帮你喜欢上自律的生活。 我之前因为工作一直需要对着电脑,身体虚弱了很多,所以就想要运动锻炼下身体。一开始选择了到户外跑步,但坚持了几天我就放弃了,因为跑步是一种没有太多惊喜的运动,很枯燥无味。但那时我还是把原因归为自己不自律。 以前尝试跑步,但没坚持多久 后来偶然一次,朋友拉我去健身房,我被好几个运动吸引了,瑜伽、搏击操、有氧舞蹈,到现在两年多,我每天都会锻炼1小时左右。跑步让我想死,瑜伽、搏击操、有氧舞蹈等运动使我快乐,但这些运动都是能实现我锻炼身体的目标,所以,在不影响终极目的情况下,可以去寻找适合自己的方式。比如你定了一个运动目标,可以去试一试跑步、篮球、台球、舞蹈等等,找到自己喜欢的再去行动。比如你喜欢摄影却在做着文案的工作,那么就辞职改成去影楼工作吧,这样产生的自律才是健康且长期的。 每一小步都值得干杯有什么样的方法,能让没耐心的小孩把一件繁琐的事做好? 当他完成一小步,就给他一颗他喜欢的糖,当他完成第二步,再给他一颗,第三步,再给他一颗,十步、十颗,他完成了。大人也一样,当你完成一个分期任务之后,我们需要为自己取得的成就庆祝,哪怕这成就很小。对你付出的努力作出肯定,这种庆祝的仪式感会产生正向的反馈,让你的自律生活处于正向的循环。 让你的身体充满能量。 身体虚弱的人,真的很难谈什么享受生活,更别说坚持自律的生活,一个人生活中80%的不开心,基本也是来源于没睡够或者吃不好。所以,用1.的方法去找到一个你喜欢的运动方式,锻炼好身体,用下面这张图,把自己喂成一个营养健康的人,而在这过程中,你不仅能享受到身体逐渐变好的红利,还能借着这个过程培养自己的自律习惯。中国营养学会根据设计的中国居民膳食指南,靠谱,也很有普适性。 找到优质的环境和志同道合的人。 在宿舍学习的效果不如在自习室和图书馆、在家里工作的效果不如在办公室。一个坏的环境让好的人变差,一个好的环境能让一个坏的人变好。所以,找到一个能让你心仪的环境很重要,比如说你想健身,可以尝试在家里买一个跑步机、也可以去户外跑步、还可以去健身房里运动,但千万不要去空气很差的地方跑,在经济条件能接受的情况下,找到最优质的环境,对你的人生有很大的帮助。另外,要远离那些打击你成长的人。这种打击分为「被动打击」和「主动打击」。被动打击如:你身边那些好吃懒做、喜欢打游戏刷抖音、不思进取的人,他们会同化你的思维,让你提不起劲去努力。再比如说:你想戒烟,而当你身边的人都在吸烟的时候,你想戒都借不了,因为不仅你看别人吸的时候会有瘾,而且总有人会递烟给你。更可恨的,是那些知道你戒烟后还跟你说:“戒什么烟,男人不抽烟不喝酒,算什么男人。”这种就是最可怕的主动打击了,像学生时代也有一些人喜欢刺几句去图书馆学习的人。如果你身边有类似的人,请远离,哪怕需要付出代价,长痛不如短痛。真正好的社交关系,是1+1>2甚至大于3的,大家一起学习、一起看书、一起研究赚钱方法、一起成长,互相帮助对方解决困难,在这种氛围和环境里,自律是顺其自然且幸福的。 再分享一些小技巧,让你提升工作效率,时刻保持饱满的精神状态。 规律的作息。 一个在白天睡觉、晚上工作的人身体好?还是一个在晚上睡觉,白天工作的人身体好?答案是一样的。真正会伤害人身体的,是作息的不规律,比如倒班,比如你今天晚上12点睡,明天10点睡,后天晚上3点睡,这才会伤害你的身体。有规律的作息会让身体各部位能够知道在该休息的时候休息。如果无规律,身体就无法协调一致。时间久了就如该保养的车没保养,会出事故的。所以,早睡早起不是重点,规律的作息才是。保持充足且规律的睡眠,人的思维活跃,精力旺盛。 番茄工作法 “番茄工作法是由西里洛创立的一种相对于GTD更微观的时间管理方法。使用番茄工作法,选择一个待完成的任务,将番茄时间设为25分钟,专注工作,中途不允许做任何与该任务无关的事,直到番茄时钟响起,然后在纸上画一个X短暂休息一下(5分钟就行),每4个番茄时段多休息一会儿。“学会使用这个方法,能让你每天都处于高效的工作状态。 曼特拉冥想 “在所有的瑜伽冥想体系中,没有哪一种比得上曼特拉冥想的功效那么直接。”,一些已经被现代医学证实的冥想功效:改善大脑,保持脑细胞的年轻活力,提升人的专注力,而且能使人产生心情愉快的感觉,使免疫功能增强、延缓衰老等。冥想的方法:平躺着,做深呼吸,注意力放在呼吸上面,可以跟着呼吸的节奏在心里默念“呼”“吸”,如果注意力分散了,就重新来过,每天5分钟的冥想,会让你受用无穷。也可以运用想象,推荐一段曼特拉冥想词:想象自己躺在一片绿色的草地上,软软的,绵绵的,阵阵清香扑面而来。蓝蓝的天空没有一丝云彩。潺潺的小溪,从身边缓缓流过,叫不出名的野花,争相开放。远处一只母牛带着它的崽崽在散步,身边孩子们尽情地嬉戏玩耍着。一只蛐蛐在地里蹦来蹦去,还有那树上的鸟儿不停地在歌唱。(还可以下载一些帮助冥想的音乐) 杀死时间黑洞,除掉干扰源。 卸载一些消耗时间的APP:游戏、抖音、快手、垃圾小说和脑残剧。工作时关掉干扰源:朋友圈的红点、手机信号等整洁的桌面:不要放太多东西,漫画书、零食、美女海报等会让你分心的东西都清理掉。 最后,再送你四句话,一定要看完: “真的想做成某件事,如果可以的话,就从现在开始,不是明天,不是下个月,就此刻,然后坚持。即使今天跟明天之间,也隔着很多意外,所以不要等。” “看100篇、1000篇回答,不如看精通一篇回答,然后深度思考并且结合自己的感悟,去把事情做好。” “赠人一赞,手留余香。我获得了你的鼓励,会写出更好的东西来回馈给你。” “乾坤未定,你我皆是黑马。”","link":"/2017/01/26/ru-he-zi-lv/"},{"title":"SSL证书生成","text":"前言要做这件事情的起因在于,代码的升级包放在一个https的服务器上,我们的设备要实现升级,则是通过wget 获取https上的升级包,并且要实现验证证书的功能,这样可以防止设备被恶意篡改升级成其他文件包。 起初,https的服务器都已经被搭建好了,可是验证证书的过程一直不顺利,现在把网络上自己试验成功方法总结如下,日后出现类似问题方便参考了。 环境:https服务器,Ubuntu12.04+apache2+openssl 首先,理解一下证书的类型。SSL证书包括: CA证书,也叫根证书或者中间级证书。如果是单向https认证的话,该证书是可选的。不安装CA证书的话,浏览器默认是不安全的。 服务器证书,必选项。通过key,证书请求文件csr,再通过CA证书签名,生成服务器证书。 客户端证书,可选项。若有客户端证书则是双向https验证。 以上所有证书都可以自己生成。 文件后缀linux系统是不以后缀名来判断文件类型的,但是为了我们能够更好地判断文件用途,所以添加各种后缀。以下是约定成俗的后缀。 *.key:密钥文件,一般是SSL中的私钥; *.csr:证书请求文件,里面包含公钥和其他信息,通过签名后就可以生成证书; *.crt, *.cert:证书文件,包含公钥,签名和其他需要认证的信息,比如主机名称(IP)等。 *.pem:里面一般包含私钥和证书的信息。 服务器证书的生成生成服务器私钥 输入加密密码,用 128 位 rsa 算法生成密钥,得到 server.key 文件。 1openssl genrsa -des3 -out server.key 1024 生成服务器证书请求( CSR ) CSR( Certificate Signing Request)是一个证书签名请求,在申请证书之前,首先要在服务器上生成 CSR ,并将其提交给 CA 认证中心, CA 才能签发 SSL 服务器证书。也可以认为, CSR 就是一个在服务器上生成的证书。在生成这个文件的过程中,有一点需要特别注意,Common Name 填入主机名(或者服务器IP)。 1openssl req -new -key server.key -out server.csr 自己生成服务器证书 如果不使用 CA 证书签名的话,用如下方式生成: 1openssl req -x509 -days 1024 -key server.key -in server.csr > server.crt 用服务器密钥和证书请求生成证书 server.crt , -days 参数指明证书有效期,单位为天。商业上来说,服务器证书是由通过第三方机构颁发的,该证书由第三方认证机构颁发的。 如果使用 CA 证书签名,用 openssl 提供的工具 CA.sh 生成服务器证书: 123mv server.csr newreq.pem./CA.sh -signmv newcert.pem server.crt 签名证书后,可通过如下命令可查看服务器证书的内容: 1openssl x509 -noout -text -in server.crt 可通过如下命令验证服务器证书: 1openssl verify -CAfile ca.crt server.crt 客户证书的生成客户证书是可选的。如果有客户证书,就是双向认证 HTTPS ,否则就是单向认证 HTTPS 。生成客户私钥 1openssl genrsa -des3 -out client.key 1024 生成客户证书签名请求 1openssl req -new -key client.key -out client.csr 生成客户证书(使用 CA 证书签名) 1openssl ca -in client.csr -out client.crt 证书转换成浏览器认识的格式 1openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.pfx 证书验证证书列表 如果使用双向认证,就会有三个私钥和三个证书。分别是 ca.key, ca.crt, server.key, server.crt, client.key, client.crt ,以及给浏览器的 client.pfx 。 如果使用有 CA 证书的单向认证,证书和私钥就是 ca.key, ca.crt, server.key, server.crt 。 如果使用无 CA 证书的单向认证,证书和私钥就是 server.key, server.crt 。 最后在fedora作为客户端,wget 1.14通过如下命令成功获取文件,证书验证通过。 1wget --ca-certificate=server.crt https://+ip+file","link":"/2019/01/21/ssl-certificate-generation/"},{"title":"Laravel学习(一)","text":"原文地址:https://blog.csdn.net/zdw19861127/article/details/75394365 介绍Laravel 是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)。它可以让你从面条一样杂乱的代码中解脱出来;它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁、富于表达力。 请求周期Laravel 采用了单一入口模式,应用的所有请求入口都是 public/index.php 文件。 注册类文件自动加载器:Laravel通过composer进行依赖管理,并在bootstrap/autoload.php中注册了Composer Auto Loader (PSR-4),应用中类的命名空间将被映射到类文件实际路径,不再需要开发者手动导入各种类文件,而由自动加载器自行导入。因此,Laravel允许你在应用中定义的类可以自由放置在Composer Auto Loader能自动加载的任何目录下,但大多数时候还是建议放置在app目录下或app的某个子目录下 创建服务容器:从 bootstrap/app.php 文件中取得 Laravel 应用实例 $app (服务容器) 创建 HTTP / Console 内核:传入的请求会被发送给 HTTP 内核或者 console 内核进行处理,HTTP 内核继承自 Illuminate\\Foundation\\Http\\Kernel 类。它定义了一个 bootstrappers 数组,数组中的类在请求真正执行前进行前置执行,这些引导程序配置了错误处理,日志记录,检测应用程序环境,以及其他在请求被处理前需要完成的工作;HTTP 内核同时定义了一个 HTTP 中间件列表,所有的请求必须在处理前通过这些中间件处理 HTTP session 的读写,判断应用是否在维护模式, 验证 CSRF token 等等 载入服务提供者至容器:在内核引导启动的过程中最重要的动作之一就是载入服务提供者到你的应用,服务提供者负责引导启动框架的全部各种组件,例如数据库、队列、验证器以及路由组件。因为这些组件引导和配置了框架的各种功能,所以服务提供者是整个 Laravel 启动过程中最为重要的部分,所有的服务提供者都配置在 config/app.php 文件中的 providers 数组中。首先,所有提供者的 register 方法会被调用;一旦所有提供者注册完成,接下来,boot 方法将会被调用 分发请求:一旦应用完成引导和所有服务提供者都注册完成,Request 将会移交给路由进行分发。路由将分发请求给一个路由或控制器,同时运行路由指定的中间件 服务容器和服务提供者服务容器是 Laravel 管理类依赖和运行依赖注入的有力工具,在类中可通过 $this->app 来访问容器,在类之外通过 $app 来访问容器;服务提供者是 Laravel 应用程序引导启动的中心,关系到服务提供者自身、事件监听器、路由以及中间件的启动运行。应用程序中注册的路由通过RouteServiceProvider实例来加载;事件监听器在EventServiceProvider类中进行注册;中间件又称路由中间件,在app/Http/Kernel.php类文件中注册,调用时与路由进行绑定。在新创建的应用中,AppServiceProvider 文件中方法实现都是空的,这个提供者是你添加应用专属的引导和服务的最佳位置,当然,对于大型应用你可能希望创建几个服务提供者,每个都具有粒度更精细的引导。服务提供者在 config/app.php 配置文件中的providers数组中进行注册 123456789101112131415161718192021<?phpnamespace App\\Providers;use Foo\\Bar;use Illuminate\\Support\\ServiceProvider;class RiakServiceProvider extends ServiceProvider{ /** * 在容器中注册绑定 * * @return void */ public function register() { $this->app->singleton(Bar::class, function ($app) { return new Bar(config('riak')); }); }} 依赖注入Laravel 实现依赖注入方式有两种:自动注入和主动注册。自动注入通过参数类型提示由服务容器自动注入实现;主动注册则需开发人员通过绑定机制来实现,即绑定服务提供者或类(参考: https://laravel.com/docs/5.5/container )。 绑定服务提供者或类:这种方式对依赖注入的实现可以非常灵活多样 123456789101112131415161718<?phpuse Illuminate\\Support\\Facades\\Storage;use App\\Http\\Controllers\\PhotoController;use App\\Http\\Controllers\\VideoController;use Illuminate\\Contracts\\Filesystem\\Filesystem;$this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); });$this->app->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); }); 参数类型声明:通过对类的构造器参数类型、类的方法参数类型、闭包的参数类型给出提示来实现 12345678910111213141516171819202122232425262728293031323334353637<?phpnamespace App\\Http\\Controllers;use App\\Users\\Repository as UserRepository;class UserController extends Controller{ /** * user repository 实例。 */ protected $users; /** * 控制器构造方法。 * * @param UserRepository $users * @return void */ public function __construct(UserRepository $users) { $this->users = $users; } /** * 储存一个新用户。 * * @param Request $request * @return Response */ public function store(Request $request) { $name = $request->input('name'); // }} 路由参数依赖:下边的示例使用 Illuminate\\Http\\Request 类型提示的同时还获取到路由参数id 1234<?php// routes/web.phpRoute::put('user/{id}', 'UserController@update'); 123456789101112131415161718192021<?php// app/Http/Controllers/UserControllernamespace App\\Http\\Controllers;use Illuminate\\Http\\Request;class UserController extends Controller{ /** * 更新指定的用户。 * * @param Request $request * @param string $id * @return Response */ public function update(Request $request, $id) { // }} Artisan ConsoleLaravel利用PHP的CLI构建了强大的Console工具artisan,artisan几乎能够创建任何你想要的模板类以及管理配置你的应用,在开发和运维管理中扮演着极其重要的角色,artisan是Laravel开发不可或缺的工具。在Laravel根目录下运行:PHP artisan list可查看所有命令列表。用好artisan能极大地简化开发工作,并减少错误发生的可能;另外,还可以编写自己的命令。下面列举部分比较常用的命令: 启用维护模式:php artisan down –message=’Upgrading Database’ –retry=60 关闭维护模式:php artisan up 生成路由缓存:php artisan route:cache 清除路由缓存:php artisan route:clear 数据库迁移 Migrations:php artisan make:migration create_users_table –create=users 创建资源控制器:php artisan make:controller PhotoController –resource –model=Photo 创建模型及迁移:php artisan make:model User -m 表单验证机制表单验证在web开发中是不可或缺的,其重要性也不言而喻,也算是每个web框架的标配部件了。Laravel表单验证拥有标准且庞大的规则集,通过规则调用来完成数据验证,多个规则组合调用须以“|”符号连接,一旦验证失败将自动回退并可自动绑定视图。 下例中,附加bail规则至title属性,在第一次验证required失败后将立即停止验证;“.”语法符号在Laravel中通常表示嵌套包含关系,这个在其他语言或框架语法中也比较常见 123456789<?phpuse Illuminate\\Http\\Request;$this->validate(Request::all(), [ 'title' => 'bail|required|unique:posts|max:255', 'author.name' => 'required', 'author.description' => 'required',]); Laravel验证规则参考 https://laravel.com/docs/5.5/validation ;另外,在Laravel开发中还可采用如下扩展规则: 自定义FormRequest (须继承自 Illuminate\\Foundation\\Http\\FormRequest ) Validator::make()手动创建validator实例 创建validator实例验证后钩子 按条件增加规则 数组验证 自定义验证规则 事件机制Laravel事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。事件类 (Event) 类通常保存在 app/Events 目录下,而它们的监听类 (Listener) 类被保存在 app/Listeners 目录下,使用 Artisan 命令来生成事件和监听器时他们会被自动创建。 注册事件和监听器:EventServiceProvider的 listen 属性数组用于事件(键)到对应的监听器(值)的注册,然后运行 php artisan event:generate将自动生成EventServiceProvider中所注册的事件(类)模板和监听器模板,然后在此基础之上进行修改来实现完整事件和监听器定义;另外,你也可以在 EventServiceProvider 类的 boot 方法中通过注册闭包事件来实现 定义事件(类):事件(类)就是一个包含与事件相关信息数据的容器,不包含其它逻辑 定义监听器:事件监听器在 handle 方法中接受了事件实例作为参数 停止事件传播:在监听器的 handle 方法中返回 false 来停止事件传播到其他的监听器 触发事件:调用 event 辅助函数可触发事件,事件将被分发到它所有已经注册的监听器上 队列化事件监听器:如果监听器中需要实现一些耗时的任务,比如发送邮件或者进行 HTTP 请求,那把它放到队列中处理是非常有用的。在使用队列化监听器,须在服务器或者本地环境中配置队列并开启一个队列监听器,还要增加 ShouldQueue 接口到你的监听器类;如果你想要自定义队列的连接和名称,你可以在监听器类中定义 $connection 和 $queue 属性;如果队列监听器任务执行次数超过在工作队列中定义的最大尝试次数,监听器的 failed 方法将会被自动调用 事件订阅者:事件订阅者允许在单个类中定义多个事件处理器,还应该定义一个 subscribe 方法,这个方法接受一个事件分发器的实例,通过调用事件分发器的 listen 方法来注册事件监听器,然后在 EventServiceProvider 类的 $subscribe 属性中注册订阅者 Eloquent 模型Eloquent ORM 以ActiveRecord形式来和数据库进行交互,拥有全部的数据表操作定义,单个模型实例对应数据表中的一行 123456<?php$flights = App\\Flight::where('active', 1) ->orderBy('name', 'desc') ->take(10) ->get(); config/database.php中包含了模型的相关配置项。Eloquent 模型约定: 数据表名:模型以单数形式命名(CamelCase),对应的数据表为蛇形复数名(snake_cases),模型的$table属性也可用来指定自定义的数据表名称 主键:模型默认以id为主键且假定id是一个递增的整数值,也可以通过incrementing = false 时间戳:模型会默认在你的数据库表有 created_at 和 updated_at 字段,设置dateFormat 属性用于在模型中设置自己的时间戳格式 数据库连接:模型默认会使用应用程序中配置的数据库连接,如果你想为模型指定不同的连接,可以使用 $connection 属性自定义 批量赋值:当用户通过 HTTP 请求传入了非预期的参数,并借助这些参数 create 方法更改了数据库中你并不打算要更改的字段,这时就会出现批量赋值(Mass-Assignment)漏洞,所以你需要先在模型上定义一个 guarded(黑名单,禁止批量赋值字段名数组) 模型软删除:如果模型有一个非空值 deleted_at,代表模型已经被软删除了。要在模型上启动软删除,则必须在模型上使用Illuminate\\Database\\Eloquent\\SoftDeletes trait 并添加 deleted_at 字段到你的模型 $dates 属性上和数据表中,通过调用trashed方法可查询模型是否被软删除 查询作用域:Laravel允许对模型设定全局作用域和本地作用域(包括动态范围),全局作用域允许我们为模型的所有查询添加条件约束(定义一个实现 Illuminate\\Database\\Eloquent\\Scope 接口的类),而本地作用域允许我们在模型中定义通用的约束集合(模型方法前加上一个 scope 前缀)。作用域总是返回查询构建器 隐藏和显示属性:模型visible 属性用于显示属性和关联的输出,另外makeVisible()还可用来临时修改可见性。当你要对关联进行隐藏时,需使用关联的方法名称,而不是它的动态属性名称 访问器和修改器:访问器(getFooAttribute)和修改器(setFooAttribute)可以让你修改 Eloquent 模型中的属性或者设置它们的值,比如你想要使用 Laravel 加密器来加密一个被保存在数据库中的值,当你从 Eloquent 模型访问该属性时该值将被自动解密。访问器和修改器要遵循cameCase命名规范,修改器会设置值到 Eloquent 模型内部的 $attributes 属性上 追加属性:在转换模型到数组或JSON时,你希望添加一个在数据库中没有对应字段的属性,首先你需要为这个值定义一个 访问器,然后添加该属性到改模型的 appends 属性中 属性类型转换:$casts 属性数组在模型中提供了将属性转换为常见的数据类型的方法,且键是那些需要被转换的属性名称,值则是代表字段要转换的类型。支持的转换的类型有:integer、real、float、double、string、boolean、object、array、collection、date、datetime、timestamp 序列化: Laravel模型及关联可递归序列化成数组或JSON 关联(方法)与动态属性:在 Eloquent 模型中,关联被定义成方法(methods),也可以作为强大的查询语句构造器。Eloquent 模型支持多种类型的关联:一对一、一对多、多对多、远层一对多、多态关联、多态多对多关联 模型事件: Laravel为模型定义的事件包括creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。 模型上定义一个 $events 属性 Laravel的Restful风格一般认为Restful风格的资源定义不包含操作,但是在Laravel中操作(动词)也可作为一种资源来定义。下图是对Laravel中资源控制器操作原理的描述,可以看到,create、edit就直接出现在了URI中,它们是一种合法的资源。对于create和edit这两种资源的访问都采用GET方法来实现,第一眼看到顿感奇怪,后来尝试通过artisan console生成资源控制器,并注意到其对create、edit给出注释“ Show the form for ”字样,方知它们只是用来展现表单而非提交表单的。 扩展开发我们知道,Laravel本身是基于Composer管理的一个包,遵循Composer的相关规范,可以通过Composer来添加所依赖的其他Composer包,因此在做应用的扩展开发时,可以开发Composer包然后引入项目中即可;另外也可开发基于Laravel的专属扩展包。下面所讲的就是Laravel的专属扩展开发,最好的方式是使用 contracts ,而不是 facades,因为你开发的包并不能访问所有 Laravel 提供的测试辅助函数,模拟 contracts 要比模拟 facade 简单很多。","link":"/2019/08/01/laravel-learn-1/"},{"title":"使用tcpdump查看HTTP请求响应","text":"tcpdump安装在Ubuntu/Debian系统上,执行如下命令安装tcpdump工具: 1sudo apt-get install tcpdump 在CentOS系统上,执行如下命令安装tcpdump工具: 1sudo yum install tcpdump 安装完tcpdump后,就可以使用man命令查看tcpdump的文档了。如果想直接看看tcpdump的一些使用例子,执行: 1man tcpdump | less -Ip examples tcpdump查看HTTP流量查看HTTP GET请求 1sudo tcpdump -s 0 -A 'tcp dst port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420' 查看HTTP POST请求 1sudo tcpdump -s 0 -A 'tcp dst port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)' 查看HTTP请求响应头以及数据 12sudo tcpdump -A -s 0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'sudo tcpdump -X -s 0 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)' 后语要理解上述tcpdump过滤器中的位操作,需要了解TCP数据包的构造。后面的参考资料中给出了一个分析例子。 笔者有过这样的经历,接手一个遗留的软件项目,发现各个API接口参数没有文档记录,而代码中的注释说明是过时的!当接手这种项目开始重构的时候,需要理解代码逻辑,如果能知道线上实际运行中的API请求参数是什么样子的,将有助于理解。笔者曾尝试修改Nginx配置文件来记录HTTP POST请求信息,却没有发现一个简单有效的方案。使用上述tcpdump命令来捕获HTTP POST请求就十分简单了。 参考资料 Can I use tcpdump to get HTTP requests, response header and response body? Use TCPDUMP to Monitor HTTP Traffic String-Matching Capture Filter Generator","link":"/2019/01/25/use-tcpdump-to-view-http-request-response-details/"},{"title":"十大经典排序算法","text":"算法概述算法分类十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 算法复杂度|排序方法|时间复杂度(平均)|时间复杂度(最坏)|时间复杂度(最好)|空间复杂度|稳定性|占用额外内存||-|-|-|-|-|-||插入排序|O(n^2)|O(n^2)|O(n)|O(1)|稳定|否||希尔排序|O(n^1.3)|O(n^2)|O(n)|O(1)|不稳定|否||选择排序|O(n^2)|O(n^2)|O(n^2)|O(1)|不稳定|否||堆排序|O(nlog₂n)|O(nlog₂n)|O(nlog₂n)|O(1)|不稳定|否||冒泡排序|O(n^2)|O(n^2)|O(n)|O(1)|稳定|否||快速排序|O(nlog₂n)|O(n^2)|O(nlog₂n)|O(nlog₂n)|不稳定|否||归并排序|O(nlog₂n)|O(nlog₂n)|O(nlog₂n)|O(n)|稳定|是||计数排序|O(n+k)|O(n+k)|O(n+k)|O(n+k)|稳定|是||桶排序|O(n+k)|O(n^2)|O(n)|O(n+k)|稳定|是||基数排序|O(nk)|O(nk)|O(n*k)|O(n+k)|稳定|是| 相关概念 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。 排序算法冒泡排序(Bubble Sort)冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 算法描述 比较相邻的元素。如果第一个比第二个大,就交换它们两个; 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数; 针对所有的元素重复以上的步骤,除了最后一个; 重复步骤1~3,直到排序完成。 动图演示 代码实现12345678910111213function bubbleSort(arr) { var len = arr.length; for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j+1]) { // 相邻元素两两对比 var temp = arr[j+1]; // 元素交换 arr[j+1] = arr[j]; arr[j] = temp; } } } return arr;} 选择排序(Selection Sort)选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 算法描述n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下: 初始状态:无序区为R[1..n],有序区为空; 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区; n-1趟结束,数组有序化了。 动图演示 代码实现12345678910111213141516function selectionSort(arr) { var len = arr.length; var minIndex, temp; for (var i = 0; i < len - 1; i++) { minIndex = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { // 寻找最小的数 minIndex = j; // 将最小数的索引保存 } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } return arr;} 算法分析表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。 插入排序(Insertion Sort)插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 算法描述一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下: 从第一个元素开始,该元素可以认为已经被排序; 取出下一个元素,在已经排序的元素序列中从后向前扫描; 如果该元素(已排序)大于新元素,将该元素移到下一位置; 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置; 将新元素插入到该位置后; 重复步骤2~5。 动图演示 代码实现1234567891011121314function insertionSort(arr) { var len = arr.length; var preIndex, current; for (var i = 1; i < len; i++) { preIndex = i - 1; current = arr[i]; while (preIndex >= 0 && arr[preIndex] > current) { arr[preIndex + 1] = arr[preIndex]; preIndex--; } arr[preIndex + 1] = current; } return arr;} 算法分析插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 希尔排序(Shell Sort)1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。 算法描述先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; 按增量序列个数k,对序列进行k 趟排序; 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 动图演示 代码实现123456789101112131415161718function shellSort(arr) { var len = arr.length, temp, gap = 1; while (gap < len / 3) { // 动态定义间隔序列 gap = gap * 3 + 1; } for (gap; gap > 0; gap = Math.floor(gap / 3)) { for (var i = gap; i < len; i++) { temp = arr[i]; for (var j = i-gap; j > 0 && arr[j]> temp; j-=gap) { arr[j + gap] = arr[j]; } arr[j + gap] = temp; } } return arr;} 算法分析希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。 归并排序(Merge Sort)归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 算法描述 把长度为n的输入序列分成两个长度为n/2的子序列; 对这两个子序列分别采用归并排序; 将两个排序好的子序列合并成一个最终的排序序列。 动图描述 代码实现123456789101112131415161718192021222324252627282930function mergeSort(arr) { // 采用自上而下的递归方法 var len = arr.length; if (len < 2) { return arr; } var middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right));} function merge(left, right) { var result = []; while (left.length>0 && right.length>0) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result;} 算法分析归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。 快速排序(Quick Sort)快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。 算法描述快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下: 从数列中挑出一个元素,称为 “基准”(pivot); 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。 动图演示 代码实现1234567891011121314151617181920212223242526272829303132function quickSort(arr, left, right) { var len = arr.length, partitionIndex, left = typeof left != 'number' ? 0 : left, right = typeof right != 'number' ? len - 1 : right; if (left < right) { partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex-1); quickSort(arr, partitionIndex+1, right); } return arr;} function partition(arr, left ,right) { // 分区操作 var pivot = left, // 设定基准值(pivot) index = pivot + 1; for (var i = index; i <= right; i++) { if (arr[i] < arr[pivot]) { swap(arr, i, index); index++; } } swap(arr, pivot, index - 1); return index-1;} function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;} 堆排序(Heap Sort)堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。 算法描述 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区; 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n]; 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。 动图演示 代码实现1234567891011121314151617181920212223242526272829303132333435363738394041424344var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量 function buildMaxHeap(arr) { // 建立大顶堆 len = arr.length; for (var i = Math.floor(len/2); i >= 0; i--) { heapify(arr, i); }} function heapify(arr, i) { // 堆调整 var left = 2 * i + 1, right = 2 * i + 2, largest = i; if (left < len && arr[left] > arr[largest]) { largest = left; } if (right < len && arr[right] > arr[largest]) { largest = right; } if (largest != i) { swap(arr, i, largest); heapify(arr, largest); }} function swap(arr, i, j) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;} function heapSort(arr) { buildMaxHeap(arr); for (var i = arr.length - 1; i > 0; i--) { swap(arr, 0, i); len--; heapify(arr, 0); } return arr;} 计数排序(Counting Sort)计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。 算法描述 找出待排序的数组中最大和最小的元素; 统计数组中每个值为i的元素出现的次数,存入数组C的第i项; 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加); 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。 动图演示 代码实现12345678910111213141516171819202122function countingSort(arr, maxValue) { var bucket = new Array(maxValue + 1), sortedIndex = 0; arrLen = arr.length, bucketLen = maxValue + 1; for (var i = 0; i < arrLen; i++) { if (!bucket[arr[i]]) { bucket[arr[i]] = 0; } bucket[arr[i]]++; } for (var j = 0; j < bucketLen; j++) { while(bucket[j] > 0) { arr[sortedIndex++] = j; bucket[j]--; } } return arr;} 算法分析计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。 桶排序(Bucket Sort)桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。 算法描述 设置一个定量的数组当作空桶; 遍历输入数据,并且把数据一个一个放到对应的桶里去; 对每个不是空的桶进行排序; 从不是空的桶里把排好序的数据拼接起来。 图片演示 代码实现12345678910111213141516171819202122232425262728293031323334353637383940function bucketSort(arr, bucketSize) { if (arr.length === 0) { return arr; } var i; var minValue = arr[0]; var maxValue = arr[0]; for (i = 1; i < arr.length; i++) { if (arr[i] < minValue) { minValue = arr[i]; // 输入数据的最小值 } else if (arr[i] > maxValue) { maxValue = arr[i]; // 输入数据的最大值 } } // 桶的初始化 var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5 bucketSize = bucketSize || DEFAULT_BUCKET_SIZE; var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1; var buckets = new Array(bucketCount); for (i = 0; i < buckets.length; i++) { buckets[i] = []; } // 利用映射函数将数据分配到各个桶中 for (i = 0; i < arr.length; i++) { buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]); } arr.length = 0; for (i = 0; i < buckets.length; i++) { insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序 for (var j = 0; j < buckets[i].length; j++) { arr.push(buckets[i][j]); } } return arr;} 算法分析桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 基数排序(Radix Sort)基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。 算法描述 取得数组中的最大数,并取得位数; arr为原始数组,从最低位开始取每个位组成radix数组; 对radix进行计数排序(利用计数排序适用于小范围数的特点); 动图演示 代码实现12345678910111213141516171819202122232425// LSD Radix Sortvar counter = [];function radixSort(arr, maxDigit) { var mod = 10; var dev = 1; for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { for(var j = 0; j < arr.length; j++) { var bucket = parseInt((arr[j] % mod) / dev); if(counter[bucket]==null) { counter[bucket] = []; } counter[bucket].push(arr[j]); } var pos = 0; for(var j = 0; j < counter.length; j++) { var value = null; if(counter[j]!=null) { while ((value = counter[j].shift()) != null) { arr[pos++] = value; } } } } return arr;} 算法分析基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。 基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。","link":"/2019/01/25/top-ten-classical-sorting-algorithms/"},{"title":"正则入门","text":"本文目标30分钟内让你明白正则表达式是什么,并对它有一些基本的了解,让你可以在自己的程序或网页里使用它。 如何使用本教程别被下面那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有想像中的那么困难。当然,如果你看完了这篇教程之后,发现自己明白了很多,却又几乎什么都记不得,那也是很正常的——我认为,没接触过正则表达式的人在看完这篇教程后,能把提到过的语法记住80%以上的可能性为零。这里只是让你明白基本的原理,以后你还需要多练习,多使用,才能熟练掌握正则表达式。 除了作为入门教程之外,本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。就作者本人的经历来说,这个目标还是完成得不错的——你看,我自己也没能把所有的东西记下来,不是吗? 清除格式 文本格式约定:专业术语 元字符/语法格式 正则表达式 正则表达式中的一部分(用于分析) 对其进行匹配的源字符串 对正则表达式或其中一部分的说明。 隐藏边注 本文右边有一些注释,主要是用来提供一些相关信息,或者给没有程序员背景的读者解释一些基本概念,通常可以忽略。 正则表达式到底是什么东西?字符是计算机软件处理文字时最基本的单位,可能是字母,数字, 标点符号,空格,换行符,汉字等等。字符串是0个或更多个字符的序列。文本也就是文字,字符串。说某个字符串匹配某个正则表达 式,通常是指这个字符串里有一部分(或几部分分别)能满足表达式给出的条件。在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就 是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。 很可能你使用过Windows/Dos下用于文件查找的通配符(wildcard), 也就是*和?。如果你想查找某个目录下的所 有的Word文档的话,你会搜索*.doc。在这里,*会被解释成任意的字符串。和通配符类似,正则表达式也是用来进行文本匹配的工具,只不过比起通配符,它能更精 确地描述你的需求——当然,代价就是更复杂——比如你可以编写一个正则表达式,用来查找所有以0开头,后面跟着 2-3个数字,然后是一个连字号“-”,最后是7或8位数字的字符串(像010-12345678或0376-7654321)。 入门学习正则表达式的最好方法是从例子开始,理解例子之后再自己对例子进行修改,实验。下面给出了不少简单的例子,并对它们作了详细的说明。 假设你在一篇英文小说里查找hi,你可以使用正则表达式hi。 这几乎是最简单的正则表达式了,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是h,后一个是i。通常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中了这个选项,它可以匹配hi,HI,Hi,hI这四种情况中的任意一种。 不幸的是,很多单词里包含hi这两个连续的字符,比如him,history,high等等。用hi来查找的话,这里边的hi也会被找出来。如果要精确地查找hi这个单词的话,我们应该使用\\bhi\\b。 \\b是正则表达式规定的一个特殊代码(好吧,某些人叫它元字符,metacharacter),代表着单词的开头或结尾,也就是单词的分界处。虽然通常英文的单词是由空格,标点符号或者换行来分隔的,但是\\b并不匹配这些单词分隔字符中的任何一个,它只匹配一个位置。 如果需要更精确的说法,\\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\\w。假如你要找的是hi后面不远处跟着一个Lucy,你应该用\\bhi\\b.*\\bLucy\\b。 这里,.是另一个元字符,匹配除了换行符以外的任意字符。*同样是元字符,不过它代表的不是字符,也不是位置,而是数量——它指定*前边的内容可以连续重复使用任意次以使整个表达式得到匹配。因此,.*连在一起就意味着任意数量的不包含换行的字符。现在\\bhi\\b.*\\bLucy\\b的意思就很明显了:先是一个单词hi,然后是任意个任意字符(但不能是换行),最后是Lucy这个单词。 换行符就是’\\n’,ASCII编码为10(十六进制0x0A)的字符。如果同时使用其它元字符,我们就能构造出功能更强大的正则表达式。比如下面这个例子: 0\\d\\d-\\d\\d\\d\\d\\d\\d\\d\\d匹配这样的字符串:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字(也就是中国的电话号码。当然,这个例子只能匹配区号为3位的情形)。 这里的\\d是个新的元字符,匹配一位数字(0,或1,或2,或……)。-不是元字符,只匹配它本身——连字符(或者减号,或者中横线,或者随你怎么称呼它)。 为了避免那么多烦人的重复,我们也可以这样写这个表达式:0\\d{2}-\\d{8}。这里\\d后面的{2}({8})的意思是前面\\d必须连续重复匹配2次(8次)。 测试正则表达式如果你不觉得正则表达式很难读写的话,要么你是一个天才,要么,你不是地球人。正则表达式的语法很令人头疼,即使对经常使用它的人来说也是如此。由于难于读写,容易出错,所以找一种工具对正则表达式进行测试是很有必要的。 不同的环境下正则表达式的一些细节是不相同的,本教程介绍的是微软 .Net Framework 4.5 下正则表达式的行为,所以,我向你推荐我编写的.Net下的工具 Regester。请参考该页面的说明来安装和运行该软件。 元字符现在你已经知道几个很有用的元字符了,如\\b,.,*,还有\\d. 正则表达式里还有更多的元字符,比如\\s匹配任意的空白 符,包括空格,制表符(Tab),换行符,中文全角空格等。\\w匹配字母或数字或下划线或汉字等。 对中文/汉字的特殊处理是由.Net提供的正则表达式引擎支持的,其它环境下的具体情况请查看 相关文档。 下面来看看更多的例子:\\ba\\w\\b匹配以字母a开头的单词——先是某个单词开始处(\\b),然后是字 母a,然后是任意数量的字母或数字(\\w), 最后是单词结束处(\\b)。 好吧,现在我们说说正则表达式里的单词是什么意思吧:就是不少于一个的连续的\\w。不错,这与学习英文时要背的成千上万个同名的东西的确关系不大 :) \\d+匹配1个或更多连续的数字。 这里的+是和类似的元字符,不同的是匹配重复任意次(可能是0次),而+则匹配重复1次或更多次。 \\b\\w{6}\\b 匹配刚好6个字符的 单词。 代码 说明 . 匹配除换行符以外的任意字符 \\w 匹配字母或数字或下划线或汉字 \\s 匹配任意的空白符 \\d 匹配数字 \\b 匹配单词的开始或结束 ^ 匹配字符串的开始 $ 匹配字符串的结束 正则表达式引擎通常会提供一个“测试指定的字符串是否匹配一个正则表达式”的方法,如JavaScript里的 RegExp.test()方法或.NET里的Regex.IsMatch()方法。这里的匹配是指是字符串里有没有符合表达式规则的部分。如果不使用^和$的话,对于\\d{5,12}而言,使用这样的方法就只能保证字符串里包含5到 12连续位数字,而不是整个字符串就是5到12位数字。 元字符^(和数字6在同一个键位上的符号)和$都 匹配一个位置,这和\\b有点类似。^匹配你 要用来查找的字符串的开头,$匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果 要求你填写的QQ号必须为5位到12位数字时,可以使用:^\\d{5,12}$。 这里的{5,12}和前面介绍过的{2}是 类似的,只不过{2}匹配只能不多不少重复2次,{5,12}则是重复的次数不能少于5次,不能多于12次, 否则都不匹配。 因为使用了^和$,所以输入 的整个字符串都要用来和\\d{5,12}来匹配,也就是说整个输入必须是5到12个数字,因此如果输入的QQ号能匹配这个正则表达式的话,那就符合要求了。 和忽略大小写的选项类似,有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项,^和$的意义就变成了匹配行的开始处和结束处。 字符转义如果你想查找元字符本身的话,比如你查找.,或者*,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\\来取消这些字符的特殊意义。因此,你应该使用.和*。当然,要查找\\本身,你也得用\\. 例如:deerchao.net匹配deerchao.net,C:\\Windows匹配C:\\Windows。 重复你已经看过了前面的*,+,{2},{5,12}这几个匹配重复的方式了。下面是 正则表达式中所有的限定符(指定数量的代码,例如*,{5,12}等): 代码/语法 说明 * 重复零次或更多次 + 重复一次或更多次 ? 重复零次或一次 {n} 重复n次 {n,} 重复n次或更多次 {n,m} 重复n到m次 下面是一些使用重复的例子: Windows\\d+匹配Windows 后面跟1个或更多数字 ^\\w+匹配一行的第一个单词(或整个字 符串的第一个单词,具体匹配哪个意思得看选项设置) 字符类要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元 音字母a,e,i,o,u),应该怎么办? 很简单,你只需要在方括号里列出它们就行了,像[aeiou]就匹配任何一个英文元音字母,[.?!]匹配标点符号(.或?或!)。 我们也可以轻松地指定一个字符范围,像[0-9]代 表的含意与\\d就是完全一致的:一位数字; 同理[a-z0-9A-Z_]也完全等同于\\w(如 果只考虑英文的话)。 下面是一个更复杂的表达式:(?0\\d{2}[) -]?\\d{8}。 “(”和“)”也是元字符,后面的分组节里会提 到,所以在这里需要使用转义。这个表达式可以匹配几种格式的电话号码,像(010)88886666,或022-22334455, 或02912345678等。我们对它进行一些分析吧:首先是一个转义字符(,它能出现0次或1次(?),然后是一个0,后面跟着2个数字(\\d{2}),然后是)或-或空格中 的一个,它出现1次或不出现(?),最后是8个数字(\\d{8})。 分组我们已经提到了怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达 式的重复次数了,你也可以对子表达式进行其它一些操作(后面会有介绍)。 (\\d{1,3}.){3}\\d{1,3}是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\\d{1,3}匹 配1到3位的数字,(\\d{1,3}.){3}匹 配三位数字加上一个英文句号(这个整体也就是这个分组)重 复3次,最后再加上一个一到三位的数字(\\d{1,3})。 IP地址中每个数字都不能大于255,大家千万不要被《24》第三季的编剧给忽悠了……不幸的是,它也将匹配256.300.888.999这种不可能存在的IP地 址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个 正确的IP地址:((2[0-4]\\d|25[0-5]|[01]?\\d\\d?).){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)。 理解这个表达式的关键是理解2[0-4]\\d|25[0-5]|[01]?\\d\\d?, 这里我就不细说了,你自己应该能分析得出来它的意义。 反义有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义: 代码/语法 说明 \\W 匹配任意不是字母,数字,下划线,汉字的字符 \\S 匹配任意不是空白符的字符 \\D 匹配任意非数字的字符 \\B 匹配不是单词开头或结束的位置 [^x] 匹配除了x以外的任意字符 [^aeiou] 匹配除了aeiou这几个字母以外的任意字符 例子:\\S+匹配不包含空白符的字符串。 <a[^>]+>匹配用 尖括号括起来的以a开头的字符串。 后向引用使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它 程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括 号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。 呃……其实,组号分配还不像我刚说得那么简单: 分组0对应整个正则表达式 实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组 号 你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权. 后向引用用于重复搜索前面某个分组匹配的文本。例如,\\1代表分组1匹配的文本。难以理解?请看示例: \\b(\\w+)\\b\\s+\\1\\b可以用来匹配重复的单词,像go go, 或者kitty kitty。这个表达式首先是一个单词, 也就是单词开始处和结束处之间的多于一个的字母或数字(\\b(\\w+)\\b), 这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\\1)。 你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?\\w+)(或者把尖括号换成’也 行:(?’Word’\\w+)),这样就把\\w+的 组名指定为Word了。要反向引用这个分组捕获的 内容,你可以使用\\k,所以上一个例子也可以写成这样:\\b(?\\w+)\\b\\s+\\k\\b。 使用小括号的时候,还有很多特定用途的语法。下面列出了最常用的一些: 分类 代码/语法 说明 捕获 (exp) 匹配exp,并捕获文本到自动命名的组里 捕获 (?exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成 (?’name’exp) 捕获 (?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号 零宽断言 (?=exp) 匹配exp前面的位置 零宽断言 (?<=exp) 匹配exp后面的位置 零宽断言 (?!exp) 匹配后面跟的不是exp的位置 零宽断言 (?<!exp) 匹配前面不是exp的位置 注释 (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读 我们已经讨论了前两种语法。第三个(?:exp)不会改变正则表达式的处理方式,只 是这样的组匹配的内容不会像前两种那样被捕获到某个组里面,也不会拥有组号。“我为什么会想要这样 做?”——好问题,你觉得为什么呢? 零宽断言地球人,是不是觉得这些术语名称太复杂,太难记了?我也有同感。知道有这么一种东西就行了,它叫什么,随它去 吧!人若无名,便可专心练剑;物若无名,便可随意取舍……接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\\b,^,$那样用于指定一个位置,这个位置应该满足一定的 条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧: 断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。(?=exp)也叫零宽度正预测先行断言, 它断言自身出现的位置的后面能匹配表达式exp。比如\\b\\w+(?=ing\\b), 匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I’m singing while you’re dancing.时,它会匹配sing和danc。 (?<=exp)也叫零宽度正回顾后 发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\\bre)\\w+\\b会匹配以re开头的单词的后半部 分(除了re以外的部分),例如在查找reading a book时,它匹配ading。 假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=\\d)\\d{3})+\\b,用它对1234567890进 行查找时结果是234567890。下面这个例子同时使用了这两种断言:(?<=\\s)\\d+(?=\\s)匹配以空白符间隔的数字(再次强调,不包括这些空白符)。 负向零宽断言前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确 保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词–它里面出现了字母q,但是q后面跟的不是字母u, 我们可以尝试这样: \\b\\wq[^u]\\w\\b匹配包含后 面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词 的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]将 会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\\w\\b将会匹配下一 个单词,于是\\b\\wq[^u]\\w\\b就能匹配整个Iraq fighting。负向零宽断言能解决这 样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\\b\\wq(?!u)\\w*\\b。 零宽度负预测先行断言(?!exp),断言此位置的后面不能匹配表达式exp。例如:\\d{3}(?!\\d)匹 配三位数字,而且这三位数字的后面不能是数字;\\b((?!abc)\\w)+\\b匹 配不包含连续字符串abc的单词。同理,我们可以用(?<!exp),零 宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:(?<![a-z])\\d{7}匹配前面不是小写字母的七位数 字。 请详细分析表达式(?<=<(\\w+)>).*(?=</\\1>), 这个表达式最能表现零宽断言的真正用途。 一个更复杂的例子:(?<=<(\\w+)>).*(?=</\\1>)匹 配不包含属性的简单HTML标签内里的内容。(<?(\\w+)>)指 定了这样的前缀:被尖括号括起来的单词(比 如可能是),然后是.*(任意的字符串),最后是一个后缀(?=</\\1>)。注意后缀里的/,它用到了前面提过的字符转义;\\1则是一个反向 引用,引用的正是捕获的第一组,前面的(\\w+)匹 配的内容,这样如果前缀实际上是的话,后缀就是了。整个表达式匹配的是和 之间的内容(再次提醒,不包括前缀和后缀本身)。 注释小括号的另一种用途是通过语法(?#comment)来包含注释。例如:2[0-4]\\d(?#200-249)|250-5|[01]?\\d\\d?(?#0-199)。要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽 略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样: 1234567(?<= # 断言要匹配的文本的前缀<(\\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)) # 前缀结束.* # 匹配任意文本(?= # 断言要匹配的文本的后缀<\\/\\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签) # 后缀结束 贪婪与懒惰当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的 字符。以这个表达式为例:a.*b,它将会匹配最长的以 a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。 有时,我们更需要懒惰匹配,也就是匹配尽可能少的 字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提 下使用最少的重复。现在看看懒惰版的例子吧: a.*?b匹配最短的,以a开始,以b结 束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。 为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另 一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。 代码/语法 说明 *? 重复任意次,但尽可能少重复 +? 重复1次或更多次,但尽可能少重复 ?? 重复0次或1次,但尽可能少重复 {n,m}? 重复n到m次,但尽可能少重复 {n,}? 重复n次以上,但尽可能少重复 处理选项在C#中,你可以使用Regex(String, RegexOptions)构造函数来设置正则表达式的处理选项。 如:Regex regex = new Regex(@”\\ba\\w{6}\\b”, RegexOptions.IgnoreCase);上面介绍了几个选项如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项: 名称 说明 IgnoreCase(忽略大小写) 匹配时不区分大小写。 Multiline(多行模式) 更改^和$的 含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的 精确含意是:匹配\\n之前的位置以及字符串结束前的位置.) Singleline(单行模式) 更改.的含义,使它与每一个字符匹配(包括换行 符\\n)。 IgnorePatternWhitespace(忽略空白) 忽略表达式中的非转义空白并启用由#标记的注释。 ExplicitCapture(显式捕获) 仅捕获已被显式命名的组。 一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较 相似(以至于让人感到疑惑)以外。 平衡组/递归匹配这里介绍的平衡组语法是由.Net Framework支持的;其它语言/库不一定支持这种功能,或者支持此功能但需要使用不同的语法。 有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构, 这时简单地使用(.+)则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论 的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如( 5 / ( 3 + 2 ) ) ),那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的 内容呢? 为了避免(和(把你的大脑 彻底搞糊涂,我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把xx <aa aa> yy这样的字符串里,最长的配对的尖括号内的内容捕获出来? 这里需要用到以下的语法构造: (?’group’) 把捕获的内容命名为group,并压入堆栈(Stack) (?’-group’) 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败 (?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分 (?!) 零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败 如果你不是一个程序员(或者你自称程序员但是不知道堆栈是什么东西),你就这样理解上面的三种语法吧:第一个就 是在黑板上写一个”group”,第二个就是从黑板上擦掉一个”group”,第三个就是看黑板上写的还有没有”group”,如果有就继续匹配yes部 分,否则就匹配no部分。 我们需要做的是每碰到了左括号,就在压入一个”Open”,每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就 证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。 123456789101112131415< #最外层的左括号 [^<>]* #最外层的左括号后面的不是括号的内容 ( ( (?'Open'<) #碰到了左括号,在黑板上写一个"Open" [^<>]* #匹配左括号后面的不是括号的内容 )+ ( (?'-Open'>) #碰到了右括号,擦掉一个"Open" [^<>]* #匹配右括号后面不是括号的内容 )+ )* (?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败> #最外层的右括号 平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的标签:<div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div>。 还有些什么东西没提到上边已经描述了构造正则表达式的大量元素,但是还有很多没有提到的东西。下面是一些未提到的元素的列表,包含语法和简单的说明。你可以在网 上找到更详细的参考资料来学习它们–当你需要用到它们的时候。如果你安装了MSDN Library,你也可以在里面找到.net下正则表达式详细的文档。 这里的介绍很简略,如果你需要更详细的信息,而又没有在电脑上安装MSDN Library,可以查看关于正则表达式语言元素 的MSDN在线文档。 代码/语法 说明 \\a 报警字符(打印它的效果是电脑嘀一声) \\b 通常是单词分界位置,但如果在字符类里使用代表退格 \\t 制表符,Tab \\r 回车 \\v 竖向制表符 \\f 换页符 \\n 换行符 \\e Escape \\0nn ASCII代码中八进制代码为nn的字符 \\xnn ASCII代码中十六进制代码为nn的字符 \\unnnn Unicode代码中十六进制代码为nnnn的字符 \\cN ASCII控制字符。比如\\cC代表Ctrl+C \\A 字符串开头(类似^,但不受处理多行选项的影响) \\Z 字符串结尾或行尾(不受处理多行选项的影响) \\z 字符串结尾(类似$,但不受处理多行选项的影响) \\G 当前搜索的开头 \\p{name} Unicode中命名为name的字符类,例如\\p{IsGreek} (?>exp) 贪婪子表达式 (?-exp) 平衡组 (?im-nsx:exp) 在子表达式exp中改变处理选项 (?im-nsx) 为表达式后面的部分改变处理选项 (?(exp)yes|no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为 此组的表达式;否则使用no (?(exp)yes) 同上,只是使用空表达式作为no (?(name)yes|no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用 no (?(name)yes) 同上,只是使用空表达式作为no","link":"/2019/08/19/zheng-ze-ru-meng/"}],"tags":[{"name":"nginx","slug":"nginx","link":"/tags/nginx/"},{"name":"php","slug":"php","link":"/tags/php/"},{"name":"蓝屏","slug":"蓝屏","link":"/tags/%E8%93%9D%E5%B1%8F/"},{"name":"blockchain","slug":"blockchain","link":"/tags/blockchain/"},{"name":"java","slug":"java","link":"/tags/java/"},{"name":"javascript","slug":"javascript","link":"/tags/javascript/"},{"name":"curl","slug":"curl","link":"/tags/curl/"},{"name":"mysql","slug":"mysql","link":"/tags/mysql/"},{"name":"git","slug":"git","link":"/tags/git/"},{"name":"webhook","slug":"webhook","link":"/tags/webhook/"},{"name":"ssh","slug":"ssh","link":"/tags/ssh/"},{"name":"centos7","slug":"centos7","link":"/tags/centos7/"},{"name":"vmware","slug":"vmware","link":"/tags/vmware/"},{"name":"mysqldump","slug":"mysqldump","link":"/tags/mysqldump/"},{"name":"backup","slug":"backup","link":"/tags/backup/"},{"name":"ssl","slug":"ssl","link":"/tags/ssl/"},{"name":"Laravel","slug":"Laravel","link":"/tags/Laravel/"},{"name":"Laravel学习","slug":"Laravel学习","link":"/tags/Laravel%E5%AD%A6%E4%B9%A0/"},{"name":"tcpdump","slug":"tcpdump","link":"/tags/tcpdump/"},{"name":"http","slug":"http","link":"/tags/http/"},{"name":"sort","slug":"sort","link":"/tags/sort/"},{"name":"regex","slug":"regex","link":"/tags/regex/"}],"categories":[{"name":"java","slug":"java","link":"/categories/java/"},{"name":"javascript","slug":"javascript","link":"/categories/javascript/"},{"name":"php","slug":"php","link":"/categories/php/"},{"name":"mysql","slug":"mysql","link":"/categories/mysql/"},{"name":"regex","slug":"regex","link":"/categories/regex/"}]}