Hessian源码分析(java)
个人博客: 戳我,戳我
先扯一扯
前一篇博文Hessian通信案例(java)简单实现了Java版的Hessian客户端和服务端的通信,总体看来,实现起来比较简单,整个基于Hessian的远程调用过程也显得很方便。但是知其然还要知其所以然,Hessian的底层是怎么实现远程调用的?是怎么序列化的?又是怎么反序列化的?又是如何通信的?
还记得吗
下面这段代码你还记得吗?
String url = "http://localhost:8080/hessian_server/ServerMachineTest";
HessianProxyFactory factory = new HessianProxyFactory();
IBasic basic = (IBasic) factory.create(IBasic.class, url);
String helloreturn = basic.hello();
上面这段代码就是前一篇博文中实现的客户端的代码,实现远程过程调用就短短的四行代码,如此简单。但是new HessianProxyFactory()是做什么的?factory.create()又是怎么实现的?
一层层的剥去外衣
项目姿势微调
为了探究new HessianProyFactory()具体实现了什么,需要对之前博文中实现的案例进行一点调整。案例中是直接导入了hessian-4.0.7.jar作为lib库的方式,为了在Eclipse中进行单步调试,需要用源码(hessian-4.0.7-src.jar)来替代这个jar包。这里需要注意的是版本,可能会出现兼容性的问题,具体情况可以试错。
导入源码包替换了jar包之后的效果:
client包主要是client端使用的功能,里面就是动态代理和http连接等操作,io包则是处理序列化。
这里需要注意的是,可能会出现一些错误,可能需要添加commons-io-2.4.jar这个包以及Tomcat的runtime环境。具体的话自行解决。
调整成这样后,启动Tomcat,启动Hessian服务端就可以对客户端进行单步调试了!O(∩_∩)O哈哈~
启动Hessian服务端
启动Hessian服务端后先进行下测试,运行刚刚调整过的Hessian客户端,看看有没有出错?正常情况会出现下面的结果:
接下来就可以对客户端进行单步调试了。
单步调试
在Eclipse中进行代码调试很方便,要在哪一行设置断点,只需要在行首进行双击,就可以看到一个圆点。F11(Debug),F5(step into),F6(step over)。
HessianProxyFactory factory = new HessianProxyFactory();
IBasic basic = (IBasic) factory.create(IBasic.class, url);
String helloreturn = basic.hello();
前两句没什么特殊的,new了一个动态代理工厂,这个工厂负责调用底层的序列化方法进行序列化;creat()函数根据定义好的接口函数以及设置好的服务端的地址进行一些处理。真正实现远程调用的是第三句代码,如下图,我在String helloreturn = basic.hello()这句代码设置断点。,启动调试,可以看到进入到了一个函数invoke()。
public Object invoke(Object proxy, Method method, Object []args)throws Throwable{String mangleName;synchronized (_mangleMap) {mangleName = _mangleMap.get(method);}if (mangleName == null) {String methodName = method.getName();Class<?> []params = method.getParameterTypes();// equals and hashCode are special casedif (methodName.equals("equals")&& params.length == 1 && params[0].equals(Object.class)) {
Object value = args[0];
if (value == null || ! Proxy.isProxyClass(value.getClass()))return Boolean.FALSE;Object proxyHandler = Proxy.getInvocationHandler(value);if (! (proxyHandler instanceof HessianProxy))return Boolean.FALSE;HessianProxy handler = (HessianProxy) proxyHandler;return new Boolean(_url.equals(handler.getURL()));}else if (methodName.equals("hashCode") && params.length == 0)
return new Integer(_url.hashCode());else if (methodName.equals("getHessianType"))
return proxy.getClass().getInterfaces()[0].getName();else if (methodName.equals("getHessianURL"))
return _url.toString();else if (methodName.equals("toString") && params.length == 0)
return "HessianProxy[" + _url + "]";if (! _factory.isOverloadEnabled())
mangleName = method.getName();elsemangleName = mangleName(method);synchronized (_mangleMap) {
_mangleMap.put(method, mangleName);}}InputStream is = null;HessianConnection conn = null;try {if (log.isLoggable(Level.FINER))
log.finer("Hessian[" + _url + "] calling " + mangleName);conn = sendRequest(mangleName, args);is = conn.getInputStream();if (log.isLoggable(Level.FINEST)) {
PrintWriter dbg = new PrintWriter(new LogWriter(log));
HessianDebugInputStream dIs= new HessianDebugInputStream(is, dbg);dIs.startTop2();is = dIs;}AbstractHessianInput in;int code = is.read();if (code == 'H') {
int major = is.read();
int minor = is.read();in = _factory.getHessian2Input(is);Object value = in.readReply(method.getReturnType());return value;}else if (code == 'r') {
int major = is.read();
int minor = is.read();in = _factory.getHessianInput(is);in.startReplyBody();Object value = in.readObject(method.getReturnType());if (value instanceof InputStream) {value = new ResultInputStream(conn, is, in, (InputStream) value);is = null;conn = null;
}
elsein.completeReply();return value;}else
throw new HessianProtocolException("'" + (char) code + "' is an unknown code");} catch (HessianProtocolException e) {throw new HessianRuntimeException(e);} finally {try {
if (is != null)is.close();} catch (Exception e) {
log.log(Level.FINE, e.toString(), e);}try {
if (conn != null)conn.destroy();} catch (Exception e) {
log.log(Level.FINE, e.toString(), e);}}
}
客户端任何远程调用函数都会经由invoke函数实现,其中关键的几句代码如下:
log.finer("Hessian[" + _url + "] calling " + mangleName);conn = sendRequest(mangleName, args);
mangleName可能就是定义好的接口,比如我的hello函数名,args就是接口函数的参数,最后通过sendRequest函数和服务端通信。下面着重看下这个函数的实现:
protected HessianConnection sendRequest(String methodName, Object []args)throws IOException{HessianConnection conn = null;conn = _factory.getConnectionFactory().open(_url);boolean isValid = false;try {addRequestHeaders(conn);OutputStream os = null;try {
os = conn.getOutputStream();} catch (Exception e) {
throw new HessianRuntimeException(e);}if (log.isLoggable(Level.FINEST)) {
PrintWriter dbg = new PrintWriter(new LogWriter(log));
HessianDebugOutputStream dOs = new HessianDebugOutputStream(os, dbg);
dOs.startTop2();
os = dOs;}AbstractHessianOutput out = _factory.getHessianOutput(os);out.call(methodName, args);out.flush();conn.sendRequest();isValid = true;return conn;} finally {if (! isValid && conn != null)
conn.destroy();}}
可以看到这个函数的实现方法是,构造http的协议头,通过call()函数后再通过sendRequest()函数发送出去。调试到conn.sendRequest()函数的时候阻塞了(如果你没有启动服务端的话),可见最后走http协议发送的任务就是由conn.sendRequest()完成的。那么out.call(methodName,args)又做了什么呢?可以告诉你,这个函数真正实现了报文内容的序列化:
public void call(String method, Object []args)throws IOException
{int length = args != null ? args.length : 0;startCall(method, length);for (int i = 0; i < length; i++)writeObject(args[i]);completeCall();}
代码很明了,可以猜测startCall(method,length)实现对方法名(也即接口函数名)的序列化;然后对接口函数的每一个参数调用writeObject()进行序列化,这是重头戏。最后completeCall()进行了序列化的收尾工作。
具体序列化的过程我就不跟进去了。call()函数完成了hessian的序列化,下面是对hello()这个函数序列化后的hessian报文:
可以看到序列化后包含了一些不可见的字符,下面这个是用十六进制查看的。由于我是回过头来写这篇博文的,所以对于hessian的序列化机制是知道的。上面序列的方式是字符’c’后面追加hessian的版本,然后字符’m’代表method,然后是接口函数名hello,然后是函数的参数(此处由于hello函数参数为空,故没有),最后追加序列化结束的标志,字符’z’。
这是比较简单的函数,简单的参数,如果碰到比较复杂的函数和参数,序列化的过程会更复杂。具体请看hessian协议2.0序列化规则。
——————————————–我是分割线—————————————————————-
——————————————–后来追加的—————————————————————-
初写这篇博文的时候没打算跟进writeObject(args[i])函数,后来打算加进去这部分的分析过程,比较重头戏就是序列化和反序列化。由于上面的案例用到的接口函数string hello()比较简单,没有参数,故此处重新换一个函数String hello_2(int arg1,String arg2)进行调试分析。同样,调试过程进入到call()函数,由于参数有两个,故writeObject()函数将执行两次。代码如下:
public void writeObject(Object object)
throws IOException
{
if (object == null) {writeNull();return;
}Serializer serializer;serializer = _serializerFactory.getSerializer(object.getClass());serializer.writeObject(object, this);
}
其中关键代码:
serializer = _serializerFactory.getSerializer(object.getClass());
这句代码就是根据参数对象的类型寻找相匹配的序列化器,进行序列化。正如getSerializer的代码注释一样:
/*** Returns the serializer for a class.** @param cl the class of the object that needs to be serialized.*
* @return a serializer object for the serialization.
*/
找到序列化器之后真正根据hessian协议执行序列化的是serializer.writeObject(object,this)函数:
public void writeObject(Object obj, AbstractHessianOutput out)
throws IOException
{
switch (_code) {
case BOOLEAN:out.writeBoolean(((Boolean) obj).booleanValue());break;case BYTE:
case SHORT:
case INTEGER:out.writeInt(((Number) obj).intValue());break;case LONG:out.writeLong(((Number) obj).longValue());break;case FLOAT:
case DOUBLE:out.writeDouble(((Number) obj).doubleValue());break;case CHARACTER:
case CHARACTER_OBJECT:out.writeString(String.valueOf(obj));break;case STRING:out.writeString((String) obj);break;case DATE:out.writeUTCDate(((Date) obj).getTime());break;case BOOLEAN_ARRAY:
{if (out.addRef(obj))return;boolean []data = (boolean []) obj;boolean hasEnd = out.writeListBegin(data.length, "[boolean");for (int i = 0; i < data.length; i++)out.writeBoolean(data[i]);if (hasEnd)
out.writeListEnd();break;
}case BYTE_ARRAY:
{byte []data = (byte []) obj;out.writeBytes(data, 0, data.length);break;
}case SHORT_ARRAY:
{if (out.addRef(obj))return;short []data = (short []) obj;boolean hasEnd = out.writeListBegin(data.length, "[short");for (int i = 0; i < data.length; i++)out.writeInt(data[i]);if (hasEnd)
out.writeListEnd();break;
}case INTEGER_ARRAY:
{if (out.addRef(obj))return;int []data = (int []) obj;boolean hasEnd = out.writeListBegin(data.length, "[int");for (int i = 0; i < data.length; i++)out.writeInt(data[i]);if (hasEnd)
out.writeListEnd();break;
}case LONG_ARRAY:
{if (out.addRef(obj))return;long []data = (long []) obj;boolean hasEnd = out.writeListBegin(data.length, "[long");for (int i = 0; i < data.length; i++)out.writeLong(data[i]);if (hasEnd)
out.writeListEnd();break;
}case FLOAT_ARRAY:
{if (out.addRef(obj))return;float []data = (float []) obj;boolean hasEnd = out.writeListBegin(data.length, "[float");for (int i = 0; i < data.length; i++)out.writeDouble(data[i]);if (hasEnd)
out.writeListEnd();break;
}case DOUBLE_ARRAY:
{if (out.addRef(obj))return;double []data = (double []) obj;boolean hasEnd = out.writeListBegin(data.length, "[double");for (int i = 0; i < data.length; i++)out.writeDouble(data[i]);if (hasEnd)
out.writeListEnd();break;
}case STRING_ARRAY:
{if (out.addRef(obj))return;String []data = (String []) obj;boolean hasEnd = out.writeListBegin(data.length, "[string");for (int i = 0; i < data.length; i++) {out.writeString(data[i]);}if (hasEnd)
out.writeListEnd();break;
}case CHARACTER_ARRAY:
{char []data = (char []) obj;out.writeString(data, 0, data.length);break;
}case OBJECT_ARRAY:
{if (out.addRef(obj))return;Object []data = (Object []) obj;boolean hasEnd = out.writeListBegin(data.length, "[object");for (int i = 0; i < data.length; i++) {out.writeObject(data[i]);}if (hasEnd)
out.writeListEnd();break;
}case NULL:out.writeNull();break;case OBJECT:ObjectHandleSerializer.SER.writeObject(obj, out);break;case BYTE_HANDLE:out.writeObject(new ByteHandle((Byte) obj));break;case SHORT_HANDLE:out.writeObject(new ShortHandle((Short) obj));break;case FLOAT_HANDLE:out.writeObject(new FloatHandle((Float) obj));break;default:throw new RuntimeException(_code + " unknown code for " + obj.getClass());
}
}
}
可以看到根据不同的参数类型,调用相关的基础序列化函数执行。
到此,就很好理解了。hessian的序列化支持基本的类型,int,double,long,date,string等。序列化的方式是把接口函数名和参数根据一定的规则进行序列化,然后走http信道发送到服务端。
——————————————我是分割线END———————————————————–
回到sendRequest函数发送完hessian报文后,回到invoke函数,接下来就是对服务端返回的内容进行反序列化:
if (code == 'H') {
int major = is.read();
int minor = is.read();in = _factory.getHessian2Input(is);Object value = in.readReply(method.getReturnType());return value;}else if (code == 'r') {
int major = is.read();
int minor = is.read();in = _factory.getHessianInput(is);in.startReplyBody();Object value = in.readObject(method.getReturnType());
两个if判断只是为了确定服务端的序列化版本,’H’代表服务端是用2.0,’r’代表服务端是采用1.0的序列化方法。真正进行反序列的函数分别是readReply()和readObject()函数。
具体实现细节此处就不赘述了。反序列化对应序列化,是一个相反的过程。最终反序列化得到服务端返回的hessian报文。
调试完了
至此,客户端的底层实现细节就披露完了,简单讲,调用接口函数后进入invoke函数,invoke函数构造http头,调用call函数进行序列化,调用sendRequest函数进行发送,然后调用readReply或者readObject函数进行反序列化,得到服务端返回的应答。
服务端呢?
服务端的序列化和反序列化方式和客户端大同小异,差别只是一些头部和尾部的构造等。此处就略去不分析了。
关于服务端的调试也是一样的方式,设置断点,然后Debug。
复杂的类型呢?
上面讲的都是比较简单的函数,序列化过程比较简单,如果碰到比较复杂的函数呢?例如下面的函数:
CTrade hello(string arg1,int arg2,list<int> arg3,map<string,string> arg4...);
这里就自己去探索了,研究的方式也是一样,单步调试加日志记录。当时我的项目里做的是hessian与xml之间的转换,hessain报文比较复杂,层次结构比较多,涉及到的类型也很多,后来对这些做了一些研究,参照Java版的hessian采用c++实现了GXP(公司里的一个c++平台)上的一些组包解包。当然这也涉及到c++版的hessian的使用等。情况比较复杂,如果有时间,后面的博文会简要记录下关于这部分的内容。
完事了
可能由于我现在是回过头来记录这些内容,关于hessian的序列化和反序列,我的感觉是比较简单。但当时由于刚刚接触到hessian,而且能找到的资料里基本都是java版的,对于一个从事c++开发的人来说,当然也不是什么难事,所以花时间研究了java版的hessian的案例实现,源码实现等。当然,其实还是有点复杂的。我现在属于站着说话不腰疼,好了伤疤忘了疼。哈哈!
Blog:
rebootcat.com (默认)
email: linuxcode2niki@gmail.com
2016-11-18 于杭州
By 史矛革
相关文章:
必读!53个Python经典面试题详解
作者 | Chris翻译 | 苏本如,编辑 | 夕颜题图 | 视觉中国出品 | AI科技大本营(ID:rgznai100)本文列出53个Python面试问题,并且提供了答案,供数科学家和软件工程师们参考。不久前,我作为“数据科学家”开始担…

Microsoft Web 平台安装程序 (Web PI) Microsoft Web Platform Installer
Microsoft Web 平台安装程序 3.0 (Web PI) 是一款免费的工具,使用它可以获得 Microsoft Web 平台的最新组件(包括 Internet Information Services (IIS)、SQL Server Express、.NET Framework 和 Visual Web Developer)。Web PI 的内置Window…

Linux Shell 脚本限制ssh最大用户登录数
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://dgd2010.blog.51cto.com/1539422/1670233 我撰写本文原来的意图是想把“复制SSH渠道”和"copy SSH Session"这样的功能从远程s…

hessiancpp编译和使用(C++版)
个人博客:戳我,戳我 许下的承诺 前两篇博客Hessian通信案例(java)和Hessian源码分析(java)介绍了Java版的hessian的使用以及源码分析。当时也说过打算写一下C版的hessian的使用和源码分析,现在就是兑现承诺的时候了。其实我项目中实际用到的…
美国AI博士一针见血:Python这样学最容易成为高手!
我见过市面上很多的 Python 讲解教程和书籍,他们大都这样讲 Python 的:先从 Python 的发展历史开始,介绍 Python 的基本语法规则,Python 的 list, dict, tuple 等数据结构,然后再介绍字符串处理和正则表达式࿰…

win7操作系统在哪显示隐藏文件夹
win7操作系统在哪显示隐藏文件夹 打开计算机--组织--文件夹和搜索选项--查看--把 “隐藏受保护的操作系统文件”前面的钩去掉,选中“显示隐藏的文件、文件夹和驱动器”--确定

ASP.NET MVC4中调用WEB API的四个方法
当今的软件开发中,设计软件的服务并将其通过网络对外发布,让各种客户端去使用服务已经是十分普遍的做法。就.NET而言,目前提供了Remoting,WebService和WCF服务,这都能开发出功能十分强大的服务。然而,越来越多的互联网…

使用docker制作hexo镜像
个人博客:戳我,戳我 背景 这段时间一直在折腾我的博客,由于之前出现过一次电脑硬盘完全挂掉的情况,为了避免重新搭建博客系统,一直打算搞一个方便点的环境,能进行多机迁移之类的。正好,Docker完…
3D目标检测深度学习方法数据预处理综述
作者 | 蒋天元来源 | 3D视觉工坊(ID: QYong_2014)这一篇的内容主要要讲一点在深度学习的3D目标检测网络中,我们都采用了哪些数据预处理的方法,主要讲两个方面的知识,第一个是representation,第二个数据预处…

NTLM协议认证
第一篇blog,发现这是个记录学习过程的好地方。从基础的开始吧。 NTLM: 基本知识telnet的一种验证身份方式,即Windows NT LAN Manager (NTLM); NTLM 是为没有加入到域中的计算机(如独立服务器和工作组)提供的…

新盒模型移动端的排版
这里采用的是新盒模型来进行排版: <div class"mytest"> <header></header> <section></section> <footer></footer> </div> 在CSS样式里添加如下样式 html,body{ height: 100%; } .mytest{ …

微信跳一跳高分辅助踩坑
旧博文,搬到 csdn 原文:http://rebootcat.com/2018/01/08/wechat_jump_hack/ 最近挺火的微信跳一跳 最近新版微信的『跳一跳』小程序着实火了一把,也把小程序这个概念再次推波助澜了一波,看来以后小程序这个入口会有大作为。 张小…
“编程能力差,90%的人会输在这点上!”谷歌开发:其实都是在瞎努力
这是一个很难让人心平气和的年代。疫情之下,很多人的都在面临着:失业、降薪、找不到工作、随时被裁等风险。但是:有心的人早已上路超车,做个人能力的升级——提高自己的不可替代性。李开复曾提出过“五秒钟准则”:一项…

64位win7安装IIS7时不能浏览asp的问题
64位win7高级家庭版安装IIS7,安装完成后只能浏览静态页,找了很多的教程都没有解决,最后在一个博客里看到说64位系统下ASP是不支持的ODB读取ACC的数据库的,因此需要开启32位应用程序的支持。 方法是: Internet 信息服务…

0525 项目回顾7.0
一、sprint总结 当谈到团队,我开始真的不知道团队是怎么样的,怎么样进行工作的,要该怎么出力团队的关系,有时候会涉及到个人问题,是不是该考虑进来,但是很多时候是不能的,每一个人作为团队的一份…

辩证看待 iostat
旧博文,搬到 csdn 原文:http://rebootcat.com/2018/01/16/using-iostat-dialectically/ 前言 经常做系统分析会接触到很多有用的工具,比如 iostat,它是用来分析磁盘性能、系统 I/O 的利器。 本文将重点介绍 iostat 命令的使用,并…
搞机器学习,Python和R哪个更合适?
【编者按】如果你正想构建一个机器学习项目,但却纠结于如何选择编程语言,这篇文章将是你所需要的。这篇文章不仅帮助你理解Python和R这两种语言的区别,还有助于你了解各个语言多方面的优势。作者 | Manav Jain译者 | Joe,编辑 | 夕…

Java安装方法
第1章 Java简介及开发环境搭建 实验1 JDK的下载、安装与配置 【实验目的】 (1)熟悉JDK工具包的下载及安装过程。 (2)掌握JAVA_HOME、CLASSPATH及Path的设置内容。 (3)掌握Java程序运行原理及Javac、Java命…

Hash函数的安全性
我们为了保证消息的完整性,引进了散列函数,那么散列函数会对安全正造成什么影响呢?这是需要好好研究一番的问题。 三个概念: 1.如果y<>x,且h(x)h(y),则…

一键安装python3环境
旧博文,搬到 csdn 原文:http://rebootcat.com/2018/04/15/python3_in_a_box/ 一键安装python3环境 由于现在逐步转移到 python3 进行开发,但是很多机器并没有预装 python3 环境,所以需要安装。 所以分享一个我常用的,…
认知智能再突破,阿里 18 篇论文入选 AI 顶会 KDD
作者 | 马超责编 | 屠敏头图 | CSDN 下载自东方 IC出品 | CSDN(ID:CSDNnews)近日,国际知识发现与数据挖掘协会KDD在官网(https://www.kdd.org/kdd2020)公布其2020年度的论文收录结果,笔者看到阿里共有18篇论文入选&…

python采集cpu信息
旧博文,搬到 csdn 原文:http://rebootcat.com/2018/05/20/analyze_cpu/ python脚本采集cpu 经常要做一些 linux 系统上的性能分析或者采集 cpu/mem/bandwidth 上报到监控系统。 分享一个我平常常用到的 cpu 采集脚本,原理是分析 /proc/stat…

Pretty Login便携版:Windows 7登录界面修改器
Pretty Login是由chnable开发的一个美化小工具,用来辅助修改Widnows 7登陆界面的背景图片,除此之外,它也能定制欢迎界面上的文本、按钮样式,如设置阴影、半透明效果。 由于Windows 7限制登录背景图片的大小不超过255KB,…
来了来了!趋势预测算法大PK!
作者 | 王哲责编 | Carol头图 | CSDN 付费下载自视觉中国趋势预测在很多应用场景中都会起到至关重要的作用,比如淘宝商家会考虑库存量应该保持在多少才能够满足客户需求,商场希望得知假期会迎来多大的客流量以安排系列活动,机场想要预测五一黄…

hdu 5713(状态压缩DP)
要进行两次dp, 第一个,dp[i],1<i<(1<<n) 其中用i的二进制形式表示已选择的点。 dp[i] 用来保存i中的点构成一个连通块,边集多少种可能。 转移方程: save[0] 1;//这里用save[i]表示dp[i]for(int i1;i<(1<<n)…

nginx特定的 404页面利于seo
要求:访问http://www.qq.com/123 url保持不变 显示的结果为指定的404页面curl -I http://www.qq.com/123 返回的状态码为404 准备一 404.php页面在最底部加上:<?phpheader(HTTP/1.1 404 Not Found);header(Status: 404 Not Found);?>然后ngin…

python采集bandwidth信息
旧博文,搬到 csdn 原文:http://rebootcat.com/2018/05/21/analyze_bandwidth/ python脚本采集bandwidth 经常要做一些 linux 系统上的性能分析或者采集 cpu/mem/bandwidth 上报到监控系统。 分享一个我平常常用到的 bandwidth 采集脚本,原理…
零基础搭建个性化精准营销 AI 应用,这次手把手教你!
百万学AI系列AI 应用开发大师课已经直播两期了,在前两期的内容中,大家在入门级任务《猫狗分类器》中上手 TensorFlow 开发,通过离线 SDK 在 Android 手机中完成人脸识别应用的部署。在这两个任务中,能成功安装开发环境,…

C++数据类型简析
C语言的基本数据类型有如下四种: 整型,说明符为int;字符型,说明符为char;浮点型(又称实型),说明符为float(单精度),double(双精度&…
浅谈几种区块链网络攻击以及防御方案之51#37攻击
旧博文,搬到 csdn 原文:http://rebootcat.com/2020/04/11/network_attack_of_blockchain_51_attack/ 写在前面的话 自比特币诞生到现在,比特币(网络)经历过大大小小非常多次的攻击,尤其在比特币诞生之初的…