Blog

科普百科-经济与政治-美国建国后政党的历史由来

在当代美国,在地方政府层次,很多地区并不分党派;在州一级,目前仅有内布拉斯加州议会仍保持不分党派性质。在全国层次,现在当然是驴象泾渭分明。但是如果我们溯美国历史之源,会发现原来美国建国之初并没有政党。

美国建国史上,有四位“国父”与美国政党发展历史息息相关:第一任总统华盛顿,第一任财政部长汉密尔顿,《独立宣言》起草人、第一任国务卿、第三位总统杰斐逊,《宪法》起草者,后来成为美国第四位总统的麦迪逊。麦迪逊与汉密尔顿都是美国建国早期经典文献《联邦党人文集》的作者。《文集》第十篇是麦迪逊所著,麦迪逊警告了政党政治的危害。他说,党派“不利于其他公民的权利,也不利于共同体的永久集体利益”。

华盛顿最著名的演讲之一是他卸任总统时的《告别演说》。这篇演讲稿的撰稿人是汉密尔顿。演说最重要的主旨之一就是警告美国人不要陷入党争。华盛顿(实际上也是汉密尔顿)表示,要“最郑重地反对政党精神所带来的有害效果”。

可是,在华盛顿的任期里,虽然没有政党政治之名,却开始有了党争之实。领头的,就是以上四位对政党政治严词警告的革命同志。

起因主要还是在于对国家前景的不同理念:究竟是要把美国建设成一个类似当时欧洲列强那样的、联邦权力集中、工商业发达的国家,还是一个充分保障州权民权,以农场、种植园为主的农业经济

汉密尔顿是前一派的领导人,虽然并没有正式结党,但是人们把他们称为联邦党。当时在国会的麦迪逊非常担心这一趋势,于是暗中拉拢与他理念相近的人士,并让杰斐逊担任领导人,结成“民主共和党”。

华盛顿虽然超脱于外,但是从政治理念上,他还是更接近于汉密尔顿的联邦党。这样,美国早期历史上的“两党制”就形成了。需要指出,这两党仍属于松散的政治联盟,并非现代政党。但是美国两党主导政治的格局就此开始。

1796年,华盛顿退休。在总统选举中,联邦党人亚当斯击败了民主共和党领导人杰斐逊。根据当时的选举制度,杰斐逊同意担任亚当斯的副总统。1800年,杰斐逊再度挑战亚当斯并且成功,民主共和党由此开始长期执政,联邦党从此式微,到1816年之后几乎不复存在。

这是美国政党历史的第一阶段。1796年和1800年的总统选举为美国政党政治与选举政治开创了良好的先例:杰斐逊1796年选举失败接受担任副总统的妥协结果;1800年政权在两党之间和平轮替。

此后的历史笔者在前文曾有所涉及。联邦党消失后,民主共和党内部出现了不同派系,反映在总统选举上,1824年选举成为美国历史上一次肮脏的争议选举。1828年,上一次选举中的受害者杰克逊卷土重来,当选总统。他身边和他的支持者很快形成了民主党。

这个杰克逊的民主党就是我们今天看到的民主党,日后产生了威尔逊、罗斯福、肯尼迪、克林顿的政党。这是美国甚至也是世界上第一个真正意义上的政党组织。1833年,杰克逊的反对者们组织了辉格党,联邦党人的一些残余势力也加入其中。从1833年到1856年,民主党与辉格党的对峙形成美国第二个两党制阶段。辉格党的主要主张是国会权力要高于总统,对外主张贸易保护。在这个时期,奴隶制的存废已经成为美国政治当中最重要的问题。两党内部却都无法对此达成一致。到1850年代后,辉格党因奴隶制问题而发生分裂,影响逐渐缩小,到1856年选举时正式崩盘。

此前的1854年,美国国内主张废除奴隶制的力量组成共和党。从辉格党中脱离出来的林肯代表共和党赢得1856年大选。共和党由此成为全国性政党,这是林肯的党,也是其后老罗斯福、里根和布什的政党。美国的两党政治由此进入第三阶段并持续至今。此后美国所有总统均出自民主、共和两党。


民主党是”左派””自由派”, 代表中产阶层和贫民阶层, 城市力量较强,主要支持者是工会和知识分子, 及社会边缘化势力如移民, 女权主义, 少数族群, 同性恋团体,等. 政治上, 民主党重视内政, 环境保护, 九 健康保险与社会福利, 教育等领域.
共和党是”右派””保守派”, 代表资产阶层和社会保守势力, 在乡村特别是南方力量强大. 主要支持者有宗教组织, 大企业, 退伍军人, 白人特别是男性白人是共和党最重要资源. 政策上, 共和党支持商界, 削减政府规模开支和福利计划, 但保证军力, 与民主党对立, 后者强调政府投资同时削减军队把钱用于国内. 共和党比较偏重外交, 特别是动用军力干涉国际事物, 是所谓”鹰派”, 与民主党”鸽派”相对.
共和党也称为老大党(GOP), 象征是大象,现任领袖是美国总统乔治.布什. 民主党象征是驴子,现任领袖是其全国委员会主席霍华德.丁.
当代的民主党与共和党在政治理念上是对立的.
民主党是”左派”, “自由派”. 共和党是”右派”, “保守派”.
对外政策上, 民主党强调外交, 共和党主张武力.
政府管理上, 民主党强调机构扩大, 共和党主张机构精简.
宗教文化问题上, 民主党强调多元化, 共和党主张保持基督教精神.
社会问题上, 民主党强调多元化, 共和党主张传统责任道德.
经济政策上, 民主党强调保护主义, 支持工会, 共和党主张自由贸易, 支持大公司.
人权问题上, 民主党支持坠胎和人体基因研究, 废除死刑, 共和党反对坠胎和人体基金研究, 支持死刑.


民主党是左翼政党,是改革性质浓厚点的政党,政策比较偏向中产阶级,重视福利和公平,推动改革。另外民主党是比较主张减少对外战争的,从伊拉克撤军这一条很受军人欢迎。

共和党是右翼政党,代表的是大资本家的利益,政策比较容易被利益集团所左右,是保守性政党。格外重视军力的发展,认为美国要去维护世界政治秩序和贸易秩序,讨伐全世界的疯子。所以共和党也会受到跨国利益集团、军事科技集团的拥护。

实际上民主、共和两党的左、右之分是被媒体夸大了。大多数政客,不论那个党,都会在基本盘稳定的情况下向中间靠。 在外交上,不论哪个党都会坚持一件事,那就是美国利益高于一切。这就是说,大家的对外政策其实没什么区别,比如反恐、打击朝鲜、颠覆非亲美政权、遏制中国……等等,手段虽略有差别,实质其实一样。

另外,无论哪个政党,其根本利益总是受到各个大的利益集团操控,比如军火集团想做大生意,那么,美国政府就要在各地“整事”,有战乱,军事集团才有生意做!美国在世界各地的各种活动,其实都是打着国家利益的旗号,体现着各个利益集团的利益。从本质上讲,美国的两党的根本利益是一致的。

科普百科-经济与政治-美前财长保尔森揭秘金融危机

芮成钢:尊敬的保尔森先生,很高兴您接受我们的采访。
保尔森先生:很高兴来这里。

 

芮成钢:我们知道,您将去参加博鳌亚洲论坛,可能这对您来说是首次。您对这次论坛有什么期待?您会如何参与到这次论坛中去?
保尔森:我很开心去参加博鳌亚洲论坛。我是博鳌亚洲论坛的理事,这是我在卸任财长之后接受的唯一的理事会职务。我对金融问题、经济改革问题、亚洲、中国等都非常感兴趣。当我看了参会代表名单后,我觉得这将是一次非常令人兴奋的论坛。

芮成钢:您曾说过,您会永远与应对处理金融危机联系在一起,与金融世界联系在一起,如果我们退回去,可能这个问题您已经回答过很多次了,如果您或您的同事当时做出了不同的决定,危机就不会发生或者程度会更轻些?
保尔森:你指的是我在美国政府的同事。有趣的是,我刚写完一本书《峭壁边缘》,这本书就谈到了这样的事情。尽管我认为我们犯了很大的错误,我们也做出了很多重大决定,这些决定大多数都是正确的。我可能是事后诸葛亮 。大家认为的那些错误实际上是不可避免的,因为在那时我们没有所需的权力。我确信我们是避免了金融崩溃,也就是说我们避免了一场灾难。

芮成钢:您认为这次危机更多地是政府造成的还是市场自身的原因?
保尔森:我想一定会有很多批判的声音。首先,关于政府的政策带来的不平衡,美国人储蓄太少,个人和国家都借债太多,住房政策过分刺激了房产市场,我们拥有房产的人数比例达到69%,这是不可持续的。我们的监管制度过时了,监管部门过时了,他们没有随着金融制度的成长而成长。当然就金融体系而言,金融产品过于复杂,不够透明,投资者、评级机构犯了很多错误。这些都是长期积累的结果,需要被纠正。

芮成钢:您提到了您犯的错误,在您的书《峭壁边缘》中提过的。大家对不救助雷曼兄弟有很大的争议,您认为不救雷曼个错误吗?
保尔森:不,因为我们没有从政府获得救雷曼的权力。我曾非常努力地去帮助雷曼,想阻止它的倒闭,有趣的是,在我们国家,在我给出这样的答案时,很多人都不相信,尽管伯南克、盖特纳和我都说了同样的话。我们没有法律权力和紧急救助权力来处理非银行机构,在破产程序之外清算他们。如果美联储或财政部有权保证偿还债务,或者注入资金,我们并不愿意去向国会要资产救助资金,所以当贝尔斯登需要救助时我们是幸运的,因为有摩根大通这样的买家,它可以和美联储一起,在困难时期保证交易进行。但是对于雷曼来说,我们没有这样的权力。

芮成钢:显然,当政府决定救助AIG时,情况发生了很大的变化。有些理论,甚至是阴谋论说,救AIG的原因是因为高盛。你怎么看这样的说法?
保尔森:我知道有这样的说法,但是我认为这些非常荒谬。当时的体系实际上已经处在崩溃边缘。如果AIG倒下的话,整个金融体系会随之崩溃,我们会经历经济灾难。幸运的是,雷曼是个征兆,当我们努力救助雷曼的那个周末,我们也在同时考虑救助AIG和美林。如果雷曼有买家的话,也许美林就没有买家了。对于AIG来说,控股公司有很严重的流动性的问题,但旗下的保险公司却有不同的信用评级,那些被认为是稳定的,而且资金充裕。所以美联储可以合法地借钱给这些机构,来阻止AIG的倒下。

芮成钢:所以可以说是一种阴谋论的论调?
保尔森:对我来说,当时作为美国的财长,我结束了和高盛的关系,卖掉了高盛的股票,我的责任是对美国人负责。我认为任何一个理性的、有知识的美国人和其它国家的人都不会希望AIG破产。事实上,如果当时我们再让任何一家主要的金融机构破产,我们将面临巨大的灾难。在雷曼倒下之后,如我所说,雷曼是个征兆,因为在雷曼倒下的两周内,有6个欧洲国家介入拯救他们的银行,所以我们为华盛顿互惠银行和美林表示担心,因为要在同一个周末被收购。但就在雷曼倒下、AIG被救之后,资本市场冻结了。当那些蓝筹公司遇到资金困难时,我知道我们到了崩溃边缘。因为如果整个体系崩溃了,会有更多的公司破产,全国范围内的工业公司将不会有能力获得发展所需要的短期资金,将无法支付他们的供应商、客户、员工,这将对经济造成重大影响,失业率将很容易达到大萧条时期的25%,将会有几千万人失去他们的积蓄,百万人失去工作,很多房屋被收回,这些都是我们已经避免了的。在美国我们有3.8万亿的货币市场基金。3000万美国人,一些中国投资者,还有其它国家的投资者,都在这个账户有投资,当他们要缩减投资时,我们有储备金,我们可以支撑保证他们的账户。这些货币市场基金在持续变化,所以我们必须介入保护这些基金。那是我们当时面临的状况,所以我们想的绝对不是说为某个银行好,我们是在努力阻止一场经济灾难的出现。

芮成钢:回过头看,也许在您的书里也提到了,我知道用事后诸葛亮说很容易,如果有一件或两件事情,让您重新做,您会做的不同吗?
保尔森:有各种各样的说法。其中有一点我在书中提到了,是这次危机来的速度,我们要在信息不完全的情况下快速做出决定。还有政府和市场之间的摩擦,因为危机很可能在最坏的时候出现,在距总统选举仅有几周的时间里,所以信息交流是个大挑战。比如说,我说的我们是在边缘的那一周,我们作出决定,认为需要去找国会,去获得从未有过的处理危机的权力。那是个危险的时期,因为我们一旦去国会,我们就需要得到相应的权力,如果说我们需要却没有得到,那是真的不幸。所以我们给国会递上去三页的提纲,我们需要政府的帮助。我当时应该开个媒体见面会,告诉大家这是谈判的起点。我没有这么做。另一个例子是,我始终没有机会让美国人知道,这不是和华尔街、银行有关,这是和所有美国人有关。如果说体系崩溃了,付出最大代价的将是美国人。那是非常困难和具有挑战性的,像我跟你说的那样,当我们去国会时,市场冻结了。伯南克和我知道,我们很肯定,再过几周,几个月,美国经济将放缓,失业率将上升,而且事实上已经发生了。国会的人还不了解情况。所以我们必须让他们相信我们,他们所作的将是在避免一场灾难。他们给了我们需要的权力,我们行使了这些权力,我们阻止了一场灾难的发生。当经济变糟时,国会和美国人说,我们赋予了你们权力,你们救助了银行,看看发生了什么,失业率上升了,但他们没有看到更没有感谢我们阻止了一场他们从未见过的灾难。

芮成钢:说到去国会,你是不是在议长佩洛西下跪,促成了财政救助方案的通过?
保尔森:我在书中提到的一段内容是,我们去国会之后,麦凯恩说他要去华盛顿帮忙。那是非常紧张的时期,如果当时任何一个总统候选人,无论是奥巴马还是麦凯恩反对银行救助资金,我们将不会得到那些钱,我们将会处于无保护状态。麦凯恩曾让我失眠了好多次,我和他进行过多次困难的、氛围紧张的谈话,但令我高兴的是,他最终没有把问题政治化,他支持救助资金。他回来之后我们在内阁室开了一次非比寻常的会议,与总统、副总统和两边的领导,结果是大家惊人的一致。我们当时迫切地需要那些权力,在国会取得了进展,最后我们取得了一致。之后,人们聚集在内阁室之外,民主党把奥巴马留在了罗斯福室,在西翼,就在内阁室旁边,我担心他们出去会说些有煽动性的话,我就突然出现在罗斯福室,当他们见到我时,他们不太高兴。所以我想带来些快乐,让大家笑,我跪了下来。演讲者说不知道我原来是个天主教徒。我说请不要毁了这个协议,她说他们不会。那确实是紧张的时刻。

芮成钢:这也解释了要把正确的经济学观点向政客讲清楚,并防止他们干预不是件容易的事情?
保尔森:恩,你说的对,会有冲突。有两次难忘的经历,一次是获得无限权力,从政治上讲,我这样说是不对的,所以我换个说法,没有经过界定的权力,来处理房地美房利美的事情;另一次是让国会在体系崩溃之前通过救助资金的方案。但是,国会的人还是必须一边考虑选举人,因为那是他们的工作,一边去做那些该做的事情。任何一个投票通过救助资金的人,知道他们在做一个从政治角度来说不受欢迎的投票。

芮成钢:提到高盛还有林林总总的阴谋论调,不仅是媒体,还有一些知名经济学家,例如几周前刚刚接受我采访的斯蒂格利茨就提到,普通人都认为用纳税人的钱去拯救银行是不合道德的,特别是这些银行还用救市资金去支付奖金和高薪。他说也许银行家不会觉得这不值得大惊小怪。
保尔森:我必须说,或许我是有偏见的,在美国有不同的观点。我还必须说,在危机中,我没发现太多专家或经济学家有什么帮助,他们有他们的模型和理论,但我认为让我们成功的是,我在书中提到的一个主题,我们是一个团队,我们有伯南克,世界级的经济学家,明白经济的历史规律。我们有盖特纳,终身投入在公共事业当中,在财政部工作。我个人呢,有在金融机构的工作经验,知道如何做决定,如何快速做决定,我本身是个有决断力的人。我认为这样的合作,还是那句话,我们需要做的是,阻止可能出现的系统崩溃。我相信历史会证明,在我在职时我们给银行的钱,会带来收益的,系统并没有崩溃。我相信不会有经济学家认为,应该让系统崩溃,或者说我们不该给银行注资。还有一件没有被充分理解但应该被理解的事情是,当我们快速行动给银行注资时,我认为系统是严重资本不足,所以我设计了一个方案,会很有吸引力,会让大多数银行都愿意接受,将有3000个银行接受注资。当银行申请救助时,很快有一些批判开始影响这项计划,说你们应该有所限制,应该限制给银行家的钱,应该让银行开始放贷,但并没有说我们怎么能使得银行放贷。当国会开始处理这件事情时,国会立刻改变了计划。所以有很多申请了救助的银行,撤回了申请,其它一些已经接受了救助的,急于偿还这些钱。所以说,这项方案很有效,确实阻止了系统崩溃,钱回到了纳税人手中,而且带来了利润。但如果有几千家银行接受了注资,这项计划将会更成功,而且这将是银行愿意放贷的强大基础。所以与其说我们需要让银行开始放贷,不如让银行接受注资。这样的话,银行将会有更多的放贷,会让经济好转的更快。所以,我们做了该做的决定,我认为是正确的决定,起了效果,许多经济学家今天都支持我们当时的政策,当然也有一些不支持。

芮成钢:这让我想起黑格将军曾经对我说的话,在一次与基辛格先生的争论中,基辛格先生说,如果你知道我所知道的,你就会完全同意我的观点。所以你一定认为有一些经济学家,像斯蒂格利茨,你知道很多他们不知道的内容,他们不了解的信息,如果他们知道的话……
保尔森:他是一位卓越的经济学家。实际上今天绝大多数的经济学家都认为应该让银行资本金充足,这是我们需要做的。同时,我认为任何人站在我的立场上,发现这些大型金融机构资金如此集中,都会意识到危险。1990年,美国排名前十的金融机构所占金融资产为总量的10%,今天已经达到60%。例如AIG为数百亿美元的养老金提供担保,还有数以亿计的保单。一旦你注意到这个公司的规模和其千丝万缕的联系,并理解到一个金融机构的崩溃,会对修补整个金融系统造成何困难,那么我觉得任何理性的人如果了解这些因素都不会反对我们的做法。所以,对我的批评主要是“救市”、“挽救行动”或是不作为。比如为何不救雷曼兄弟,为何不救其他机构。不过我认为都是缺乏理由的,我去华盛顿不是为了去救银行而是因为深谙不救的后果。伯南克他精通经济学史,算是美国大萧条时期经济学相关理论的权威。所以他知道在这个资金以光速流动的时代,金融系统互相依存,欧洲美国发生了种种状况。他了解一旦系统崩溃,想重新构建将异常艰难。而我从实践的角度理解其后果的灾难性。所以我们没有花很多时间争论如何行动,而是讨论采取何种有效的手段。

芮成钢:您卸任之后那些银行高管们的奖金和高薪到底是怎么回事?
保尔森:我还在华尔街工作的时候就觉得(高管)薪酬水平有些荒唐。所以想让我去捍卫什么奖金、高薪是不可能的。经济不景气的时候,例如我个人过去三年就没拿过现金薪酬,而且从未出于私利抛售个人持有的股份。我持有股票是觉得有信心。拯救金融系统,我们面临两个选择,其一是延续过去的做法,或是英国的做法,也就是当某机构濒临崩溃,将其国有化;其二是未雨绸缪,从整个系统入手,防范于未然,避免重大倒闭事件。至于薪酬的问题,我认为从政府政策的角度来看,关键在与薪酬的结构必须符合公司和股东的利益、管理层的利益、银行的利益。这方面已经有了长足的进步,也是我在担任财长的时候反复强调的问题。实际上我在书中也写到这一点。11月的时候,我和四大监管机构合作出台了薪酬指南,要求薪酬结构不得诱发过度承担风险。

芮成钢:所以您认为华尔街的高管们薪酬一直过高?而且用纳税人的钱去支付奖金是不道德的?
保尔森:是的。我想说两点。首先,银行家的高薪结构让我解释起来,我看即使我做教师的妻子都不一定比某位火星来客理解的更清楚。至于救市的行动是否不受欢迎,当然,我想在美国我们认为承担风险的人也要为损失负责,私营部门也不例外。所以任何救市措施都会遭到非议,特别是涉及到银行业的时候。因为银行业的问题是他们自己造成的。由于利率降低,银行业的利润恢复高于总体经济水平。那么我对这个问题开什么处方呢,例如在监管改革方面?雷曼兄弟倒台前两个月,我在一次演讲中就呼吁,美国需要清算机构和紧急权力,这样就不存在某个机构大到“不能倒台”的问题。任何类型的机构濒临崩溃,政府应动用紧急权力将其清算而不走破产程序,以防影响其他经济部门。这样就不会动用纳税人的钱来支撑这些机构。美国人气愤的不仅仅是高管薪酬,而是这些金融机构令政府不得不去救助,去挽救。

芮成钢:所以一方面,您认为华尔街高管们的薪酬结构的确有问题;另一方面,您也认为向普通人去解释他们的薪酬结构是很难的事情,所以有时高管的薪酬就成了替罪羊。
保尔森:我想说两点。银行高管的薪酬涉及两个问题。其一是薪酬的形式,其二是薪酬的数额。从公共政策的角度看,其形式和背后结构的是问题的关键。如果交易员的薪酬和奖金与其创造的利润按照一定公式挂钩,那么风险就不可避免。所以我认为从公共政策的角度看,合理的薪酬结构是关键。这应是政府要下大力气的工作。我认为,公众很难理解薪酬的高昂数额,特别是在大规模的非常规救助行动之后,整个金融体系,包括那些濒临倒闭的机构,都从纳税人采取的行动中受益。

芮成钢:所以应该像影星、运动员一样。影星、运动员薪水也很高,但是不会制造灾难,无论他们是否和政府合作?
保尔森:您说得对。毫无疑问,电影明星和演员中至少一部分收入很高,还有运动员等。但是针对金融高管,必须有稳健的监管。监管当局必须关注的一个问题就是薪酬的结构,一定要防止其引发如过度承担风险在内的各种问题。我认为监管当局还应加强资本金、流动性等要求。这方面有很多工作要做。我认为美国应设立风险监管机构,享有搜集信息、审查所有类型的金融机构、干预和限制威胁整个金融系统的业务行为的权力。目前的监管体制只能照顾到一棵棵树木,我们需要的是能覆盖整片森林的监管体制。我想应该能通过相应的立法。

芮成钢:再谈谈高盛。现在一条重大新闻就是欧洲主权债务危机,特别是希腊。媒体称高盛也参与其中。有人认为其中有利害冲突。一方面,高盛给政府提供建议,另一方面,高盛通过抛售政府债券获益。您对这些报道有何回应?
保尔森:我对这个问题无法发表评论。希腊政府有权对其发表意见。我想他们已经做出回应。

芮成钢:如果您仍是高盛的董事长,是否会有不同做法?
保尔森:我已经不是主席的身份,无法对任何一个金融机构的事务发表意见。

芮成钢:高盛有员工服务社会的传统吗?从鲁宾到佐利克再到您本人?
保尔森:我认为政府工作是崇高的事业,可以为国家服务,也是难得的机会。我在书中写到,当小布什政府聘请我出任财长的时候,我开始没有答应,家人也不支持。所以我拒绝了两次。但后来想了想,虽然这些年事业有成,但我不想等到退休之后才后悔当初没有抓住为国家服务的机会。我在高盛供职时值得自豪的不仅是履新财长一职,还骄傲于之前高盛的员工所从事的各种高尚事业,比如对大学、慈善机构、各种事业的支持。我认为这是很多杰出的企业希望员工做到的。

芮成钢:曾有人说您是美国历史上最强势的财长吗?就您的影响而言?
保尔森:历史就交给史学家们去撰写吧!我和历史缺乏距离感。我非常赞赏和小布什总统的良好关系。我书中专门提到他,但某些对总统有偏见的美国读者也许不赞同的是,我认为小布什总统对市场有丰富的知识。他也不希望采取救市措施去挽救诸如房地美和房利美等华尔街机构。但是他支持我做出所有那些艰难的政治抉择。他曾对我说“这些决定不会容易,政治方面会导致批评,肯定看上去糟糕。但是我们必须采取必要行动保护美国人民和世界人民的工作。”

芮成钢:我们知道您在中国有位老朋友,就是中国人民银行的周小川先生。二位过去商讨过的一个问题就是人民币汇率问题。现在好像中国离被贴上“汇率操纵国”标签越来越近了。您认为中国是汇率操纵国吗?
保尔森:首先我想说,货币问题在中国和美国媒体都十分关注,它仅是中美关系中的一部分,特别是经济关系。所以每次我都会向中美双方人士解释,危急中或危机后,美国人民可能遇到的最糟糕情况就是中国经济停止增长。中国经济增长对美国有益,倘若停止,美国会受害。反之亦然,美国经济景气中国受益,美国经济滑坡中国遭殃。就货币问题,当我还是财长时,我在公开和私人场合都表示货币(汇率)灵活性是关键。人民币(汇率)发展和改革对中国和世界有益,有益于中国向更多依赖消费的经济模式转变,更加和谐、更加均衡、普遍繁荣,给予决策者更多控制通胀的工具。这点很重要。您必须了解,在美国,货币是一种符号,是不断改革的符号。所以美国的政治家和其他人士期望看到货币(汇率)的变化。我总是强调,中国必须按照最符合本国利益的方式采取行动。我认为(汇率改革)符合中国的利益。有时我把中美的分歧描述为时机问题,而不是方向问题。我还担任财长时,双方有了进展我都会提及,例如很多重要经济领域都有进展,除了货币(汇率)灵活性,还有食品、产品安全,双边合作等。危机当中,中国是美国的良好伙伴,给予大力支持。这些进展和努力避免了美国国会的敌对情绪,特别是在大选年将近时,促成我成功阻止国会通过一些列惩罚性的立法。

芮成钢:找替罪羊于事无补。但中国人会把目前的中美关系与上世纪八十年代的美日关系做比较,比如《广场协定》的签署。日元大幅升值,导致日本经济数十年停滞不前。日本经济衰退和目前情况有可比性吗?
保尔森:我认为完全不同。不同的国家,不同的经济状况。这种论调我有耳闻。我不是说中国今天已经准备好完全让市场确定货币(汇率)。我的意思是,中国是个不错的特例。但是历史上从未有哪个国家如此庞大、经济地位如此重要、如此紧密地融入全球商品服务贸易体系,却同时没有市场主导的货币(汇率),而其他主要经济体都是市场主导。所以我认为关键的问题,这点中国朋友应该赞同,就是不断内部改革。中国“奇迹”,包括改革、资本市场、银行业等,是全系统的。这种势头必须保持下去,而其重要指标之一就是货币(汇率)的水平。灵活的货币(汇率)符合中国的国家利益。这是我的观点。这个方面中日没有可比性。

来自:芮成钢的博客

科普百科-经济与政治-美联储救两房和AIG但不救雷曼的原因

拯救贝尔斯登(3月16日被收购)


经常以“国际成熟资本市场”为自己学习榜样、经常口出“绝不政策救市”的国内资本市场管理层,很可能忽略掉一个极其重要的事实是:从20世纪以来,全球最大最先进的资本市场所在地美国,拥有一个越来越积极主动救市的“最后拯救者”———美联储。

贝尔斯登危机爆发后,美联储紧急通过一向有“救市传统”的摩根大通银行之手,将一笔为期28天的抵押担保贷款注入贝尔斯登,使后者的流动性不至于枯竭。美国财长鲍尔森也在接受电视采访时表达了要维护华尔街“第一块多米诺骨牌”的意思。对此,人们不禁要问两个问题:第一,为什么美联储必须拯救贝尔斯登?第二,这种拯救对中国资本市场有何启示?

贝尔斯登是一家什么机构?简单地说,它是一家中介类型的交易商,为不计其数的对冲基金以及小券商提供担保和清算服务。我们最近频频听到的“次级抵押贷款支持证券”及其衍生品,就必须由贝尔斯登这样的交易商提供服务。

在华尔街,类似贝尔斯登这样的角色,被认为是“秘密的最后守护者”(贝尔斯登是其中较大的),因为数以千计的次级债衍生品在市场上交易,甚至美联储都不确切知道总交易规模有多大,但贝尔斯登却很可能知道,因为不论你怎样交易,最后总要经过清算服务这一环节。1998年发生危机的长期资本管理公司(LTCM)的指定清算交易商就是贝尔斯登,当时,LTCM以40亿美元的自有资金,运作着超过1000亿美元的债券和证券交易,更通过金融衍生品合约进行着超过12000亿美元的全球金融交易,华尔街上的每家机构几乎都有资金卷入,但最终损失最小的就是贝尔斯登,因为它知道“底牌”。

今天,贝尔斯登终于自尝不顾风险放大交易的恶果,导致资金链断裂,不过,贝尔斯登却“不能”破产清算,甚至美联储都直接拿出现金为它输血。这其中的奥妙不在于有些人士评论的“太大而不能倒闭”,而在于一旦贝尔斯登这个清算商的资产负债表曝光,天文数字的交易数据将被华尔街上各神通广大的机构获取,一个本能的反应就是大家赶紧抛空所有这些与贝尔斯登相关的岌岌可危的交易合约,其情景类似于银行发生了“挤兑”。更恐怖的是,一旦这个趋势形成,将发生多米诺骨牌般的连锁反应,那些原本就价格暴跌的抵押品将跌无可跌,而相关的参与机构的信用评级也将节节败退,因为“交易对手”不复存在而让大量本来经过对冲避险的交易全部成为“裸露交易”,到时候恐怕华尔街上的投行倒掉一半都有可能。

近100年来,美联储越来越走向前台,甚至在很多“溺爱华尔街坏孩子”的质疑声中直接出手拯救资本市场危机,其深刻的背景是全球金融衍生品交易的数量已经是一个天文数字,远远不是一个金融机构甚至是整条华尔街可以承受的。

在当今美国面临通胀、经济停滞、美元持续下跌的情况下,依然把市场流动性、把维护“第一块多米诺骨牌”为所有任务的第一环节,固然是会留下很多后遗症的,但也是无奈中的最佳选择。当今的全球资本市场已经陷入了事实上的流动性危机之中,华尔街正源源不断地将原先散布在世界各地的现金流回收去补洞,这个洞如果能在美联储的帮助下补掉,市场也要至少几个月休养生息,如果补不掉,那么全球金融危机已无可避免。

拯救两房(9月7日宣布拯救)


美国的次贷危机在肆虐全球的同时,也给美国的两大按揭机构房利美(联邦国民抵押贷款协会)和房地美(联邦住宅抵押贷款公司)带来重创。过去一年里,两家公司股价惨跌,融资成本不断上升,一度濒临破产。由于担心两家公司破产对美国金融市场和整体经济造成严重冲击,9月7日,美国财政部长保尔森宣布:政府将接管美国这两家最大的房贷机构,这意味着,上述两家目前由私人控股的上市公司将被“国有化”。这也将是美国有史以来规模最大的金融救助案例。

但是格林斯潘以及一些经济学家对此持完全相反的意见,其理由是应按市场化的原则处理“两房”危机,任其自生自灭,破产倒闭或者采取收购兼并等市场化运作方案。那么,作为世界上最强大的自由市场经济国家,为什么美国当局非要力保“两房”而置市场化原则于不顾呢?笔者分析主要有以下三个原因:

第一,维护美国政府的信誉和形象。房利美和房地美作为收购银行等抵押贷款的主要金融机构,其业务量占全美5万多亿美元抵押贷款的一半左右,而且“两房”用于集资所发行的债券对美国十分重要,几乎与美国国债的信誉相当,甚至全球各国央行也大量持有“两房”发行的债券。如果美国任由“两房”坍塌,会使世界对美国的信誉彻底失去信心。

第二,“两房”的盈利能力能为美国赚取巨额利润。除了其全球融资功能支持政策的正常利益之外,通过为“两房”收拾市场残局,将以极低的价格购入大量抵押贷款证券,特别是次级抵押贷款证券最终会流入“两房”及其相关企业,未来房产市场回暖之后获利将十分丰厚。美国任何当局都不会放任“两房”被市场力量兼并重组,这样政府将失去实施隐形政策的渠道和能力,对美国赤字政策将形成巨大挑战,而且也会危及“两房”传统的优势地位和筹资功能。

第三,维护市场稳定,防范金融危机。“两房”的规模是如此之庞大,它们与金融系统的关系又如此之紧密和错综复杂,以致其中任何一家一旦失败,都将导致美国以及全球金融市场的大骚动和混乱,美国和世界很多地方的消费者的住房贷款、汽车贷款以及消费等能力将会受到明显影响。挽救“两房”是为了稳定金融市场、保证提供房屋贷款、保护纳税人。

在权衡利弊之后,美国政府终于出手托管“两房”。这充分说明,世界上没有完全自由的市场经济。一般来说,当一个市场需要运用政府的行政资源来强行扭转市场运行轨迹的时候,总是这个市场处于严重的危机、靠市场自身力量已经很难疗救的时候,因此政府必须及时有效地进行干预。在当前全球金融动荡和自身金融市场制度不健全的内外环境条件下,新兴市场经济国家尤其要重视对金融市场安全稳定运行监控、干预和救助。

(作者为本报特约评论员、同济大学经济与管理学院教授)

来自人民日报 海外版

不救雷曼(9月15号破产)


雷曼的财务报表显示,其财务杠杆率(总资产/总股东权益) 仍然在20倍以上,2008年2月底达到了31.7倍的高峰。以30倍杠杆为例。只要资产价格上涨1%,就相当于赚到30%的收益,而一旦价格下跌导致亏损3.3%。次贷危机中,美国住房市场价格已经下降了20%,持有大量相关债券的雷曼,最终倒下了!

雷曼公司是私营企业。美国政府动用纳税人的钱对股市、对贝尔斯登公司实施救助,对“两房”托底等,已经引发了国会中不少议员的强烈不满,更是招致民众的普遍抨击。另外,从目前的实际情况来看,雷曼兄弟绝不是最后一家需要救助的金融机构。如果再出手,很有可能把美国政府财政也“拉”下水。如果要防止金融危机进一步扩大,挽救已经陷入困境的金融企业,政府就不能不考虑市场化之路。美林证券刘二飞说:“现在看来,美国政府已经不愿自己出大量资金(救市场),美国政府现在政策的主要导向,第一是鼓励企业联合起来自自救援。

雷曼伸手太过,势头咄咄逼人,导致华尔街老大老二们的愤怒,联手做了雷曼。……大资本,一样江湖险恶,一山不容二虎,尤其不容想篡位富有野心的雷曼——这是逻辑使然,不需要特别的细节证明。

雷曼兄弟公司虽然名列美国五大投资银行的“五虎将”之列,但与高盛公司和摩根斯坦利公司比起来,它与美联储和美国政府的渊源的关系,一个是嫡系部队,一个是杂牌军。大家知道,控制美国经济命脉的是美联储,不是美国政府。

高盛次贷产品有在AIG买保险,美林,摩根斯坦利有政府房利美,房贷美担保,而雷曼没有买任何保险。

当保尔森跪求国会用纳税人的钱救助美国金融机构,危胁不救明天将没美国时,国会为了做给纳税人看,只能放弃雷曼,才能平民愤。

令美国政府没有想到的是,雷曼兄弟公司的倒闭的消息,会引起全世界资本市场的这么大的震动,世界各大股市出现暴跌。引发美联储和美国政府对资本市场的脆弱性的恐惧,失去了美国一贯崇尚的对自由市场经济的自我调节能力的信心,立即对其他三家也处于危机之中的高盛公司、美林公司、摩根斯坦利公司,进行全面的大手术,以避免更大的资本市场的大动荡的出现。

雷曼就只有被破产了,造成国外投资者损失惨重,引发股市血流成河,从一万五千点直下六千多点,无人幸免为难。

——————————-
雷曼垮台一直以来被视为引发了多米诺效应,导致很多其他公司纷纷崩溃:美林公司(Merrill Lynch)被仓促出售给美国银行(Bank of America);美国国际集团(American International Group,简称AIG)获得救援;货币市场基金的资产净值跌破一美元;高盛(Goldman Sachs)和摩根士丹利(Morgan Stanley)几近垮台,变为银行控股公司;政府决定执行7000亿美元的问题资产救助计划(Troubled Asset Relief Program,简称TARP)以帮助整个银行业脱离困境。
不救雷曼的决定曾被认为是错误的,而且是相当糟糕的错误。时任法国财政部长的克里斯蒂娜·拉加德(Christine Lagarde)称,这是一个“可怕的”决定。
没人说过雷曼值得挽救。但提出的观点却是,如果雷曼获救,也许危机就不会那么严重。雷曼的破产为市场带来了严重的不安情绪,投资者对于政府的作用感到迷茫,不知政府是否在插手干预,决定不同机构的存亡。政府此前救助了贝尔斯登(Bear Stearns),然后对房利美(Fannie Mae)和房地美(Freddie Mac)进行了国有化,仅留下雷曼等死,却回过头又救助了AIG。
当时美国财长小亨利·M·保尔森(Henry M. Paulson Jr.)曾暗示,政府别无选择。由于没有买家愿意出手相助,政府缺乏合适手段接管雷曼。英国政府在最后一刻叫停了巴克莱银行的收购计划。回头来看,这确实是个英明的决定。
保尔森可能是正确的:他没有手段。但这并未阻止政府巧妙布局,救助AIG,然后再使用强力手段——如强制美国银行完成对美林的收购,同时避免向持股人透露美林问题的严重性——若非国家身陷危机,如此下策会被视为良心的缺失。
当然,政府虽然放言进行帮助,但却不太情愿参与救助雷曼。同一时期的证据非常有力:“看起来保尔森将亲临纽约来解决雷曼这个乱摊子……想象不出什么样的局面需要政府进行注资……拭目以待吧,”保尔森的幕僚长吉姆·威尔金森(Jim Wilkinson)在 9月12日给一个招聘人员的邮件中写到。
两天后,他向摩根大通(JPMorgan Chase)全球资产管理部门的负责人杰斯·斯塔利(Jes Staley)又发了一封邮件:“政府不可能出钱……我正在写政府关于有序退出的表态口径……刚和白宫通话,政府部门统一意见了,不出钱。保尔森是打死也不会让步了。”
如果当时政府挺身而出,接下来又会发生什么呢?
这就是假想游戏开始的地方。但我们完全可以做出一些非常合理的设想。救助雷曼会一石激起千层浪。我们经常忘记,在雷曼破产之后,主流观点认为这是件好事。《纽约时报》(The New York Times)刊登社论:“看到财政部和美联储(Federal Reserve)会坐视雷曼兄弟崩溃,让人有种奇怪的欣慰感。”(《华尔街日报》[The Wall Street Journal]也持类似观点。)
同样值得注意的是,华尔街的多数人相信雷曼兄弟的失败不会危及系统。“我认为市场能承受雷曼的解散,但在本周内需要有人给美林出个价,”斯塔利在周末给威尔金森的邮件里写到。
真相是,在童话版的救助雷曼故事里,作为多米诺牌阵的下一位,AIG会摔得更惨。如果说围绕着救助雷曼的政治斗争是激烈的,那么围绕着救助AIG的政治斗争只会更激烈。而且和雷曼解体相比,AIG垮台对系统造成的风险远超雷曼几个数量级。
如果美联储不顾政治考量,在那一刻出手帮助AIG,那么财政部就益发难以取得足够的国会支持以通过TARP。至少,该计划的规模和覆盖面会大打折扣。请记住,尽管当时股市和经济都开始了自由落体运动,国会起初仍驳回了TARP,后来市场似乎坠入死亡漩涡,TARP才时来运转,得以通过。
正如拉姆·伊曼纽尔(Rahm Emanuel)2008年所言:“你绝不想浪费一个严重的危机。我的意思是,这是个机会,去做你以前无法做到的事。”
雷曼的垮台使得政府能够给予经济更多支持。历史学家尼尔·弗格森(Niall Ferguson)在危机结束后第二年在《金融时报》的一篇评论文章中这样写道:“雷曼就像是伏尔泰名著中那位被处死的英国海军上将,它必须牺牲才能使其它银行认识到注入公共资本的必要性,也才能说服立法机构批准注资计划。”
乔治·W·布什总统(George W. Bush)的经济顾问艾德·拉齐尔(Ed Lazear)却告诉芝加哥大学布斯商学院(the University of Chicago Booth School of Business)的学生们,把危机比为一系列多米诺骨牌效应可能是错误的比喻。
“根据多米诺理论里的传染性,一个多米诺牌倒下,并碰倒其他多米诺牌,它们全部倒下,你手里就是一个烂摊子,”他说道。“在我们救助贝尔斯登的时候,就是这么想的。而且看起来局势就会那样发展下去。不幸的是,那并不是一个多米诺模式,而是爆米花模式。”
“做爆米花的时候,你把锅加热,玉米粒受热后爆开了。把第一个爆开的玉米粒从锅里拿走没有任何用。其他玉米粒仍在受热,只要热量在,无论怎样它们都会爆开,”他解释道。
当然,如果专栏还有足够的地方的话,还有一个问题就是,如果政府没有救助贝尔斯登,又会怎样呢……
—————————

雷曼的悲剧在于它的按揭产品与不动产投资太过冒进,而且其进行投资的资金使用了大量杠杆工具,使其背负的负债是自身价值的四十余倍(导致破产清算时累计上千亿美元天量负债)。也就是说雷曼的财务状况是最差的,也是最先被媒体与投资者发现的。

综上所述,百年雷曼的逝去,是多方面因素共同作用的结果。如果从外部找原因,可以追溯到克林顿签署的《金融服务现代化法案》(这个法案的签署是大型金融并购和危险性极大的杠杆交易高危玩法的源头),但主要就是乔治布什总统的心腹——美国财长保尔森的见死不救。如果从自身找原因那就是福尔德高压独裁 疯狂揽入不动产按揭工具的愚蠢大跃进领航方式最终撞向冰山,但 但 但 当时所有投行都在玩高收益按揭产品,你不玩就赚不到钱! 纵使雷曼交易团队与高管中像是加德沃德、亚历克斯等最聪明的头脑早在灾难发生两年前向雷曼总裁福尔德进言危险并悲壮的以辞职相逼但依然没有改变福尔德的顽固(毕竟美国房地产市场以及其衍生金融产品在大灾前表现太优异,而且让穷人有房子住降低首付款放宽按揭条件甚至是自克林顿开始推动的类似于国策的趋势,历史成绩说话加国家政策撑腰的好生意,傻瓜才不玩)

作者:张數
链接:https://www.zhihu.com/question/22102220/answer/149323658

—————————

拯救AIG(9月16日放贷款)

本文刊于2008年9月18日《21世纪经济报道》3版

雷曼的客户都是机构投资者,而AIG除了与大量金融机构交易外,与普通老百姓的生活关系也非常密切,美联储会让雷曼倒,而不会允许AIG倒。

特派记者 吴晓鹏 纽约报道

在拒绝救助投资银行雷曼兄弟两天之后,美国政府在当地时间9月16日向全美最大的保险公司美国国际集团(AIG)伸出了援助之手,提供850亿美元的紧急贷款,防止AIG破产给美国经济带来系统性风险。

美联储当地时间周二晚间宣布,将向AIG提供最高达850亿美元的两年期贷款,并获得该公司79.9%的股权。AIG由此成为有史以来美国政府救助的最大一家私人企业。

美联储在其网站的一份声明中说,AIG“无序倒闭”将会加剧金融市场已经“相当严重”的脆弱程度,还将导致借款成本大幅上升,家庭财富减少并“严重削弱”美国经济的表现。

美国财政部长保尔森随即发表声明支持该项交易,他认为这将“减轻更广泛市场混乱的产生,同时保护纳税人的利益”。

保尔森说,财政部和美联储以及美国证券交易委员会等监管部门一直在紧密合作,试图稳定金融市场,并极力减少金融市场的动荡对经济的破坏。

纽约一家对冲基金的衍生品交易主管Chang Jinpeng周一在接受记者采访时就表示,雷曼的客户都是机构投资者,而AIG除了与大量金融机构交易外,与普通老百姓的生活关系也非常密切,美联储会让雷曼倒,而不会允许AIG倒。

AIG在全球130多个国家开展寿险、财险、事故险以及飞机租赁等业务,资产超过1万亿美元,拥有雇员11万余名。

苏格兰皇家银行在一份报告中表示,AIG若走向破产,将危及全球更多的金融机构,造成约1800亿美元损失。

周一晚些时候,美国三大信用评级机构一致下调了AIG的债务信用级别,使得该公司的财务状况进一步恶化。

面临巨额亏损的AIG原本急需大量的资本金注入。市场对于该公司可能破产的担忧导致其周一股价下跌61%,同时拖累道琼斯指数下跌超过500点。周二再跌31%,在连续三个交易日中跌幅达79%。

美联储称,这笔贷款“将帮助AIG按期履行债务义务”,同时,AIG将以有序的方式出售一部分业务资产,以尽可能减少影响。

AIG将向联储支付的利率为较三个月伦敦银行间拆放款利率(LIBOR)高出8.5个百分点,这让当前利率达到约11.4%。这将会鼓励AIG展开大规模资产出售,以快速偿还贷款。

与此同时,为避免让股东从中获利,美国政府还将有权否决支付给优先股和普通股股东的分红。

美联储公开市场委员会(FOMC)在当天的会议上决定,将联邦基金利率保持在2%的原有水平

高盛在周二晚间发送给记者的一份研究报告中说,如果考虑到当天晚上AIG的行动,理解FOMC为什么不降息就容易很多了。

“美联储的主要官员都知道这项交易正在进行之中,他们或许感觉到如果能让AIG不倒闭,既能化解市场的系统性风险,同时还避免了降息。”

高盛执行董事苏晖对本报记者说,AIG从事的传统保险业务一直都是中等的成长型业务,但最近几年公司为了追求成长性,为大量的金融产品提供保险,包括风险极高的CDO和次级债产品。这些金融产品造成AIG在过去三个季度共亏损了180亿美元。

她说,AIG接下来可能会出售一些传统的保险业务给其他保险公司,如房屋保险或者飞机租赁等。

websocket协议-细说WebSocket – java篇

tcp/udp 是协议,而 socket 是实现 该协议的 接口。所以 网络编程 就是 socket 编程。因为 http 和 websocket 都是 http 80 端口 tcp 协议。所以 web 服务器 肯定要实现 ,socket 区分 http 字节流还是  websocket 字节流。 这个 暂时 没有深入,有机会 去研究  web 开源 服务器 ,看看 如何 做到 区分 。


文章待整理

http://www.111cn.net/wy/html5/69508.htm (手动解析 websocket   )

https://www.cnblogs.com/jingmoxukong/p/7755643.html (框架解析websocket)


关于网络编程 :

百度搜 tcp ip 网络编程

【Java TCP/IP Socket】Socket编程大合集
https://blog.csdn.net/ns_code/article/details/17526127

怎样算得上熟悉 TCP/IP 协议编程?
链接:https://www.zhihu.com/question/20795067/answer/16233370

常见的三个网络协议:NetBEUI、IPX/SPX、TCP/IP
http://network.51cto.com/art/200701/38792.htm

三大协议:NetBEUI、IPX/SPX 和TCP/IP
https://searchnetworking.techtarget.com.cn/12-15241/

NetBIOS
https://zh.wikipedia.org/wiki/NetBIOS

Internet协议
https://baike.baidu.com/item/Internet%E5%8D%8F%E8%AE%AE/11049108

网络拓扑 锁定
https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%8B%93%E6%89%91/4804125?fr=aladdin

链路层包括 物理链路层 和 数据链路层

websocket协议-细说WebSocket – php篇

下面我画了一个图演示 client 和 server 之间建立 websocket 连接时握手部分,这个部分在 node 中可以十分轻松的完成,因为 node 提供的 net 模块已经对 socket 套接字做了封装处理,开发者使用的时候只需要考虑数据的交互而不用处理连接的建立。而 php 没有,从 socket 的连接、建立、绑定、监听等,这些都需要我们自己去操作,所以有必要拿出来再说一说。

   +--------+    1.发送Sec-WebSocket-Key        +---------+
    |        | --------------------------------> |        |
    |        |    2.加密返回Sec-WebSocket-Accept  |        |
    | client | <-------------------------------- | server |
    |        |    3.本地校验                      |        |
    |        | --------------------------------> |        |
    +--------+                                   +--------+

看了我写的上一篇文章的同学应该是对上图有了比较全面的理解。① 和 ② 实际上就是一个 HTTP 的请求和响应,只不过我们在处理的过程中我们拿到的是没有经过解析的字符串。如:

GET /chat HTTP/1.1
Host: server.example.com
Origin: http://example.com

我们往常看到的请求是这个样子,当这东西到了服务器端,我们可以通过一些代码库直接拿到这些信息。

一、php 中处理 websocket

WebSocket 连接是由客户端主动发起的,所以一切要从客户端出发。第一步是要解析拿到客户端发过来的 Sec-WebSocket-Key 字符串。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

前文中也提到了 client 请求的格式(如上),首先 php 建立一个 socket 连接,监听端口的信息。

1. socket 连接的建立

关于 socket 套接字的建立,相信很多大学修过计算机网络的人都知道了,下面是一张连接建立的过程:

// 建立一个 socket 套接字
$master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($master, $address, $port);
socket_listen($master);

相比 node,这个地方的处理实在是太麻烦了,上面几行代码并未建立连接,只不过这些代码是建立一个 socket 套接字必须要写的东西。由于处理过程稍微有复杂,所以我把各种处理写进了一个类中,方便管理和调用。

//demo.php
Class WS {
    var $master;  // 连接 server 的 client
    var $sockets = array(); // 不同状态的 socket 管理
    var $handshake = false; // 判断是否握手

    function __construct($address, $port){
        // 建立一个 socket 套接字
        $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)   
            or die("socket_create() failed");
        socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)  
            or die("socket_option() failed");
        socket_bind($this->master, $address, $port)                    
            or die("socket_bind() failed");
        socket_listen($this->master, 2)                               
            or die("socket_listen() failed");

        $this->sockets[] = $this->master;

        // debug
        echo("Master socket  : ".$this->master."\n");

        while(true) {
            //自动选择来消息的 socket 如果是握手 自动选择主机
            $write = NULL;
            $except = NULL;
            socket_select($this->sockets, $write, $except, NULL);

            foreach ($this->sockets as $socket) {
                //连接主机的 client 
                if ($socket == $this->master){
                    $client = socket_accept($this->master);
                    if ($client < 0) {
                        // debug
                        echo "socket_accept() failed";
                        continue;
                    } else {
                        //connect($client);
                        array_push($this->sockets, $client);
                        echo "connect client\n";
                    }
                } else {
                    $bytes = @socket_recv($socket,$buffer,2048,0);
                    if($bytes == 0) return;
                    if (!$this->handshake) {
                        // 如果没有握手,先握手回应
                        //doHandShake($socket, $buffer);
                        echo "shakeHands\n";
                    } else {
                        // 如果已经握手,直接接受数据,并处理
                        $buffer = decode($buffer);
                        //process($socket, $buffer); 
                        echo "send file\n";
                    }
                }
            }
        }
    }
}

上面这段代码是经过我调试了的,没太大的问题,如果想测试的话,可以在 cmd 命令行中键入 php /path/to/demo.php;当然,上面只是一个类,如果要测试的话,还得新建一个实例。

$ws = new WS('localhost', 4000);

客户端代码可以稍微简单点:

var ws = new WebSocket("ws://localhost:4000");
ws.onopen = function(){
    console.log("握手成功");
};
ws.onerror = function(){
    console.log("error");
};

运行服务器代码,当客户端连接的时候,我们可以看到:

通过上面的代码可以清晰的看到整个交流的过程。首先是建立连接,node 中这一步已经封装到了 net 和 http 模块,然后判断是否握手,如果没有的话,就 shakeHands。这里的握手我直接就 echo 了一个单词,表示进行了这个东西,前文我们提到过握手算法,这里就直接写了。

2. 提取 Sec-WebSocket-Key 信息

function getKey($req) {
    $key = null;
    if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) { 
        $key = $match[1]; 
    }
    return $key;
}

这里比较简单,直接正则匹配,websocket 信息头一定包含 Sec-WebSocket-Key,所以我们匹配起来也比较快捷~

3. 加密 Sec-WebSocket-Key

function encry($req){
    $key = $this->getKey($req);
    $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}

将 SHA-1 加密后的字符串再进行一次 base64 加密。如果加密算法错误,客户端在进行校检的时候会直接报错:

4. 应答 Sec-WebSocket-Accept

function dohandshake($socket, $req){
    // 获取加密key
    $acceptKey = $this->encry($req);
    $upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
               "Upgrade: websocket\r\n" .
               "Connection: Upgrade\r\n" .
               "Sec-WebSocket-Accept: " . $acceptKey . "\r\n" .
               "\r\n";

    // 写入socket
    socket_write(socket,$upgrade.chr(0), strlen($upgrade.chr(0)));
    // 标记握手已经成功,下次接受数据采用数据帧格式
    $this->handshake = true;
}

这里千万要注意,每一个请求和相应的格式,最后有一个空行,也就是 \r\n,开始测试的时候把这东西给弄丢了,纠结了半天。

当客户端成功校检key后,会触发 onopen 函数:

5. 数据帧处理

// 解析数据帧
function decode($buffer)  {
    $len = $masks = $data = $decoded = null;
    $len = ord($buffer[1]) & 127;

    if ($len === 126)  {
        $masks = substr($buffer, 4, 4);
        $data = substr($buffer, 8);
    } else if ($len === 127)  {
        $masks = substr($buffer, 10, 4);
        $data = substr($buffer, 14);
    } else  {
        $masks = substr($buffer, 2, 4);
        $data = substr($buffer, 6);
    }
    for ($index = 0; $index < strlen($data); $index++) {
        $decoded .= $data[$index] ^ $masks[$index % 4];
    }
    return $decoded;
}

这里涉及的编码问题在前文中已经提到过了,这里就不赘述,php 对字符处理的函数太多了,也记得不是特别清楚,这里就没有详细的介绍解码程序,直接把客户端发送的数据原样返回,可以算是一个聊天室的模式吧。

// 返回帧信息处理
function frame($s) {
    $a = str_split($s, 125);
    if (count($a) == 1) {
        return "\x81" . chr(strlen($a[0])) . $a[0];
    }
    $ns = "";
    foreach ($a as $o) {
        $ns .= "\x81" . chr(strlen($o)) . $o;
    }
    return $ns;
}

// 返回数据
function send($client, $msg){
    $msg = $this->frame($msg);
    socket_write($client, $msg, strlen($msg));
}

客户端代码:

var ws = new WebSocket("ws://localhost:4000");
ws.onopen = function(){
    console.log("握手成功");
};
ws.onmessage = function(e){
    console.log("message:" + e.data);
};
ws.onerror = function(){
    console.log("error");
};
ws.send("李靖");

在连通之后发送数据,服务器原样返回:

二、注意问题

1. websocket 版本问题

客户端在握手时的请求中有Sec-WebSocket-Version: 13,这样的版本标识,这个是一个升级版本,现在的浏览器都是使用的这个版本。而以前的版本在数据加密的部分更加麻烦,它会发送两个key:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Key1: xxxx
Sec-WebSocket-Key2: xxxx

如果是这种版本(比较老,已经没在使用了),需要通过下面的方式获取

function encry($key1,$key2,$l8b){ //Get the numbers preg_match_all('/([\d]+)/', $key1, $key1_num); preg_match_all('/([\d]+)/', $key2, $key2_num);

    $key1_num = implode($key1_num[0]);
    $key2_num = implode($key2_num[0]);
    //Count spaces
    preg_match_all('/([ ]+)/', $key1, $key1_spc);
    preg_match_all('/([ ]+)/', $key2, $key2_spc);

    if($key1_spc==0|$key2_spc==0){ $this->log("Invalid key");return; }
    //Some math
    $key1_sec = pack("N",$key1_num / $key1_spc);
    $key2_sec = pack("N",$key2_num / $key2_spc);

    return md5($key1_sec.$key2_sec.$l8b,1);
}

只能无限吐槽这种验证方式!相比 nodeJs 的 websocket 操作方式:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            console.log(e);
        };
    });
}).listen(8000);

多么简洁,多么方便!有谁还愿意使用 php 呢。。。。

2. 数据帧解析代码

本文没有给出 decodeFrame 这样数据帧解析代码,前文中给出了数据帧的格式,解析纯属体力活。

3. 代码展示

对这部分感兴趣的同学可以再去深究。代码用到了demo.php和ws.html两个文件。

<?php
class WS {
	var $master;
	var $sockets = array();
	var $debug = false;
	var $handshake = false;

	function __construct($address, $port){
		$this->master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
		socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");
		socket_bind($this->master, $address, $port)                    or die("socket_bind() failed");
		socket_listen($this->master,20)                                or die("socket_listen() failed");
		
		$this->sockets[] = $this->master;
		$this->say("Server Started : ".date('Y-m-d H:i:s'));
		$this->say("Listening on   : ".$address." port ".$port);
		$this->say("Master socket  : ".$this->master."\n");
		
		while(true){
			$socketArr = $this->sockets;
			$write = NULL;
			$except = NULL;
			socket_select($socketArr, $write, $except, NULL);  //自动选择来消息的socket 如果是握手 自动选择主机
			foreach ($socketArr as $socket){
				if ($socket == $this->master){  //主机
					$client = socket_accept($this->master);
					if ($client < 0){
						$this->log("socket_accept() failed");
						continue;
					} else{
						$this->connect($client);
					}
				} else {
					$this->log("^^^^");
					$bytes = @socket_recv($socket,$buffer,2048,0);
					$this->log("^^^^");
					if ($bytes == 0){
						$this->disConnect($socket);
					}
					else{
						if (!$this->handshake){
							$this->doHandShake($socket, $buffer);
						}
						else{
							$buffer = $this->decode($buffer);
							$this->send($socket, $buffer); 
						}
					}
				}
			}
		}
	}
	
	function send($client, $msg){
		$this->log("> " . $msg);
		$msg = $this->frame($msg);
		socket_write($client, $msg, strlen($msg));
		$this->log("! " . strlen($msg));
	}
	function connect($socket){
		array_push($this->sockets, $socket);
		$this->say("\n" . $socket . " CONNECTED!");
		$this->say(date("Y-n-d H:i:s"));
	}
	function disConnect($socket){
		$index = array_search($socket, $this->sockets);
		socket_close($socket);
		$this->say($socket . " DISCONNECTED!");
		if ($index >= 0){
			array_splice($this->sockets, $index, 1); 
		}
	}
	function doHandShake($socket, $buffer){
		$this->log("\nRequesting handshake...");
		$this->log($buffer);
		list($resource, $host, $origin, $key) = $this->getHeaders($buffer);
		$this->log("Handshaking...");
		$upgrade  = "HTTP/1.1 101 Switching Protocol\r\n" .
					"Upgrade: websocket\r\n" .
					"Connection: Upgrade\r\n" .
					"Sec-WebSocket-Accept: " . $this->calcKey($key) . "\r\n\r\n";  //必须以两个回车结尾
		$this->log($upgrade);
		$sent = socket_write($socket, $upgrade, strlen($upgrade));
		$this->handshake=true;
		$this->log("Done handshaking...");
		return true;
	}

	function getHeaders($req){
		$r = $h = $o = $key = null;
		if (preg_match("/GET (.*) HTTP/"              ,$req,$match)) { $r = $match[1]; }
		if (preg_match("/Host: (.*)\r\n/"             ,$req,$match)) { $h = $match[1]; }
		if (preg_match("/Origin: (.*)\r\n/"           ,$req,$match)) { $o = $match[1]; }
		if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$req,$match)) { $key = $match[1]; }
		return array($r, $h, $o, $key);
	}

	function calcKey($key){
		//基于websocket version 13
		$accept = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
		return $accept;
	}

	function decode($buffer) {
		$len = $masks = $data = $decoded = null;
		$len = ord($buffer[1]) & 127;

		if ($len === 126) {
			$masks = substr($buffer, 4, 4);
			$data = substr($buffer, 8);
		} 
		else if ($len === 127) {
			$masks = substr($buffer, 10, 4);
			$data = substr($buffer, 14);
		} 
		else {
			$masks = substr($buffer, 2, 4);
			$data = substr($buffer, 6);
		}
		for ($index = 0; $index < strlen($data); $index++) {
			$decoded .= $data[$index] ^ $masks[$index % 4];
		}
		return $decoded;
	}

	function frame($s){
		$a = str_split($s, 125);
		if (count($a) == 1){
			return "\x81" . chr(strlen($a[0])) . $a[0];
		}
		$ns = "";
		foreach ($a as $o){
			$ns .= "\x81" . chr(strlen($o)) . $o;
		}
		return $ns;
	}

	
	function say($msg = ""){
		echo $msg . "\n";
	}
	function log($msg = ""){
		if ($this->debug){
			echo $msg . "\n";
		} 
	}
}
	

new WS('localhost', 4000);
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:4000");
ws.onopen = function(){
	console.log("握手成功");
}
ws.onmessage = function(e){
	console.log("message:" + e.data);
}
ws.onerror = function(){
	console.log("error");
}
</script>

 

4. 相关开源库参考

http://socketo.me Ratchet 为 php 封装的一个 WebSockets 库。

Google 上搜索 php+websoket+class,也能找到不少相关的资料。

三、参考资料

来自:https://www.barretlee.com/blog/2013/12/25/cb-websocket-with-php/     可以关注微信公众号 (小胡子哥)


文章错误纠正:

1、握手函数中不应该加chr(0),只应该发送$upgrade,否则后续消息发到浏览器中都会提示A server must not mask any frames that it sends to the client.

———————————————————

2、我想问下 在demo.php 中 您用了一个全局变量‘$handshake’ 来标记是否握手 好像这边会导致只有第一次连接的client能够成功连上,第二个client,第三个client的就连接不上了

3、我试过了可以改成 ,在把new socket变量加到全局变量$socket之后 ,就立即进行握手动作。 可以解决我之前遇到的只能第一个client连接上的问题。

websocket协议-细说WebSocket – Node篇

在上一篇提高到了 web 通信的各种方式,包括 轮询、长连接 以及各种 HTML5 中提到的手段。本文将详细描述 WebSocket协议 在 web通讯 中的实现。

一、WebSocket 协议

1. 概述

websocket协议允许不受信用的客户端代码在可控的网络环境中控制远程主机。该协议包含一个握手和一个基本消息分帧、分层通过TCP。简单点说,通过握手应答之后,建立安全的信息管道,这种方式明显优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协议包括两个方面,握手链接(handshake)和数据传输(data transfer)。

2. 握手连接

这部分比较简单,就像路上遇到熟人问好。

Client:嘿,大哥,有火没?(烟递了过去)
Server:哈,有啊,来~ (点上)
Client:火柴啊,也行!(烟点上,验证完毕)

握手连接中,client 先主动伸手:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。 Server 看到 Client 打招呼之后,悄悄地告诉 Client 他已经知道了,顺便也打个招呼。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Server 返回了 Sec-WebSocket-Accept 这个应答,这个应答内容是通过一定的方式生成的。生成算法是:

mask  = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // 这是算法中要用到的固定字符串
accept = base64( sha1( key + mask ) );

key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。分解动作:

1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
   -> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2. s = sha1(t) 
   -> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 
      0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
3. base64(s) 
   -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

上面 Server 端返回的 HTTP 状态码是 101,如果不是 101 ,那就说明握手一开始就失败了~

下面就来个 demo,跟服务器握个手:

var crypto = require('crypto');

require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            // 握手
            // 应答部分,代码先省略
            console.log(e.toString());
        }else{

        };
    });
}).listen(8000);

客户端代码:

var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
  console.log(e);
};

上面当然是一串不完整的代码,目的是演示握手过程中,客户端给服务端打招呼。在控制台我们可以看到:

看起来很熟悉吧,其实就是发送了一个 HTTP 请求,这个我们在浏览器的 Network 中也可以看到:

但是 WebSocket协议 并不是 HTTP 协议,刚开始验证的时候借用了 HTTP 的头,连接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包试试,肯定是拿不到的,后面的通信部分是基于 TCP 的连接。

服务器要成功的进行通信,必须有应答,往下看:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            console.log(e);
        };
    });
}).listen(8000);

关于crypto模块,可以看看官方文档,上面的代码应该是很好理解的,服务器应答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。

//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onopen=function(e){
    console.log("握手成功");
};

可以看到

3. 数据帧格式

官方文档提供了一个结构图

 0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

第一眼瞟到这张图恐怕是要吐血,如果大学修改计算机网络这门课应该不会对这东西陌生,数据传输协议嘛,是需要定义字节长度及相关含义的。

FIN      1bit 表示信息的最后一帧,flag,也就是标记符
RSV 1-3  1bit each 以后备用的 默认都为 0
Opcode   4bit 帧类型,稍后细说
Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
Payload  7bit 数据的长度
Masking-key      0 or 4 bit 掩码
Payload data     (x + y) bytes 数据
Extension data   x bytes  扩展数据
Application data y bytes  程序数据
https://tools.ietf.org/html/rfc6455#section-5.2

 Masking-key:  0 or 4 bytes

      All frames sent from the client to the server are masked by a
      32-bit value that is contained within the frame.  This field is
      present if the mask bit is set to 1 and is absent if the mask bit
      is set to 0.  See Section 5.3 for further information on client-
      to-server masking.

每一帧的传输都是遵从这个协议规则的,知道了这个协议,那么解析就不会太难了,下面我就直接拿了次碳酸钴同学的代码。

4. 数据帧的解析和编码

数据帧的解析代码:

function decodeDataFrame(e){
  var i=0,j,s,frame={
    //解析前两个字节的基本数据
    FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
    PayloadLength:e[i++]&0x7F
  };
  //处理特殊长度126和127
  if(frame.PayloadLength==126)
    frame.length=(e[i++]<<8)+e[i++];
  if(frame.PayloadLength==127)
    i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
    frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
  //判断是否使用掩码
  if(frame.Mask){
    //获取掩码实体
    frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
    //对数据和掩码做异或运算
    for(j=0,s=[];j<frame.PayloadLength;j++)
      s.push(e[i+j]^frame.MaskingKey[j%4]);
  }else s=e.slice(i,frame.PayloadLength); //否则直接使用数据
  //数组转换成缓冲区来使用
  s=new Buffer(s);
  //如果有必要则把缓冲区转换成字符串来使用
  if(frame.Opcode==1)s=s.toString();
  //设置上数据部分
  frame.PayloadData=s;
  //返回数据帧
  return frame;
}

数据帧的编码:

//NodeJS
function encodeDataFrame(e){
  var s=[],o=new Buffer(e.PayloadData),l=o.length;
  //输入第一个字节
  s.push((e.FIN<<7)+e.Opcode);
  //输入第二个字节,判断它的长度并放入相应的后续长度消息
  //永远不使用掩码
  if(l<126)s.push(l);
  else if(l<0x10000)s.push(126,(l&0xFF00)>>2,l&0xFF);
  else s.push(
    127, 0,0,0,0, //8字节数据,前4字节一般没用留空
    (l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF
  );
  //返回头部分和数据部分的合并缓冲区
  return Buffer.concat([new Buffer(s),o]);
}

有些童鞋可能没有明白,应该解析哪些数据。这的解析任务主要是服务端处理,客户端送过去的数据是二进制流形式的,比如:

var ws = new WebSocket("ws://127.0.0.1:8000/"); 
ws.onopen = function(){ 
  ws.send("握手成功"); 
};

Server 收到的信息是这样的:

一个放在Buffer格式的二进制流。而当我们输出的时候解析这个二进制流:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            // 输出之前解析帧
            console.log(decodeDataFrame(e));
        };
    });
}).listen(8000);

那输出的就是一个帧信息十分清晰的对象了:

5. 连接的控制

上面我买了个关子,提到的Opcode,没有详细说明,官方文档也给了一张表:

 |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
 | 0      | Continuation Frame                  | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 1      | Text Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 2      | Binary Frame                        | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 8      | Connection Close Frame              | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 9      | Ping Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 10     | Pong Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|

decodeDataFrame 解析数据,得到的数据格式是:

{
    FIN: 1,
    Opcode: 1,
    Mask: 1,
    PayloadLength: 4,
    MaskingKey: [ 159, 18, 207, 93 ],
    PayLoadData: '握手成功'
}

那么可以对应上面查看,此帧的作用就是发送文本,为文本帧。因为连接是基于 TCP 的,直接关闭 TCP 连接,这个通道就关闭了,不过 WebSocket 设计的还比较人性化,关闭之前还跟你打一声招呼,在服务器端,可以判断frame的Opcode:

var frame=decodeDataFrame(e);
console.log(frame);
if(frame.Opcode==8){
    o.end(); //断开连接
}

客户端和服务端交互的数据(帧)格式都是一样的,只要客户端发送 ws.close(), 服务器就会执行上面的操作。相反,如果服务器给客户端也发送同样的关闭帧(close frame):

o.write(encodeDataFrame({
    FIN:1,
    Opcode:8,
    PayloadData:buf
}));

客户端就会相应 onclose 函数,这样的交互还算是有规有矩,不容易出错。

二、注意事项

1. WebSocket URIs

很多人可能只知道 ws://text.com:8888,但事实上 websocket 协议地址是可以加 path 和 query 的。

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

如果使用的是 wss 协议,那么 URI 将会以安全方式连接。 这里的 wss 大小写不敏感。

2. 协议中”多余”的部分(吐槽)

握手请求中包含Sec-WebSocket-Key字段,明眼人一下就能看出来是websocket连接,而且这个字段的加密方式在服务器也是固定的,如果别人想黑你,不会太难。

再就是那个 MaskingKey 掩码,既然强制加密了(Mask为1表示加密,加密方式就是 MaskingKey 与 PayLoadData 进行异或处理),还有必要让开发者处理这个东西么?直接封装到内部不就行了?

3. 与 TCP 和 HTTP 之间的关系

WebSocket协议是一个基于TCP的协议,就是握手链接的时候跟HTTP相关(发了一个HTTP请求),这个请求被Server切换到(Upgrade)websocket协议了。websocket把 80 端口作为默认websocket连接端口,而websocket的运行使用的是443端口。

三、参考资料

四、特别感谢

再次感谢 次碳酸钴 跟我交流了几个小时 : ),本文部分 node 代码参考自他的博客。

下次将以php作为后台,讲解websocket的相关知识。

细说WebSocket-php篇


文章部分内容纠正:

1、t = “GhlIHNhbXBsZSBub25jZQ==”,串最前面少了一个d
2、console.log(crypto.createHash(‘sha1’).update(‘dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11’).digest(‘sha1’)); //输出:<SlowBuffer b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea>,不像你的,前面带0x,你是怎么弄的?

3、解码部分:if(frame.PayloadLength==126) frame.length=(e[2]<<8) + e[3], i=4; //其中的frame.length,好像应该是frame.Payloadlength。下一行中也是。看了一下,次碳酸钴,那边已经改了。

4、编码部分:s.push(126,(l&0xFF00)>>2,l&0xFF); 其中的>>2,好像应该是>>8。下一行类似。

websocket协议-websocket简史

WebSocket 简介及应用实例

HTML5 的出现,标志着后 Flash 时代各种现代浏览器的集体爆发,也是谨防 Adobe 一家独大的各家厂商们,历经多年各自为战,想换个活法儿并终于达成一定共识后,所积kao累bei的技术的一次集中释放 — 正所谓 “H5 是个筐,什么都可以往里装”。

其中引人瞩目并被广泛支持的一项,就是此次要谈论的 WebSocket 了。本文将尝试说明它被用来解决什么问题,以及与久经沙场的“传统” Socket 又有什么异同等基础问题。

I. 定义及由来

望文而生义,面对 WebSocket 这个名称,web 无需做太多解释,傻傻分不清楚的 socket 看着也是相当的面熟;甭管有没有联系,先来了解一下也无妨:

(1.1) 传统的 Socket API

Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API

如图,英文单词 “socket” 的字面原义是 “孔” 或 “插座”。

作为一个技术用语时,socket 通常取后一种意思,像一个多孔插座。用于描述一个通信链路两端的 IP 地址和端口等,可以用来实现不同设备之间的通信。SocketTCP Socket都是通用的叫法,中文一般习惯性的译作**“套接字”、“TCP套接字”** 等。

…至于为嘛把“插座儿”翻译成“套接字”,好奇的程序猿并不在少数,科考文章在文章底部参考链接中可以找到。

可以将服务端主机想象成一个布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供固定电话信号,有的则提供有线电视节目。客户端软件将插头接入不同编号的插座,就可以得到不同的服务

Socket API 所处的楼层

OSI 模型作为一种概念模型,由国际标准化组织(ISO)提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。我们熟悉的 HTTP、FTP 等协议都工作在最顶端的应用层(Application Layer)。

而 **TCP/IP 协议族(Protocol Suite)**将软件通信过程抽象化为四个抽象层,常被视为是简化的七层OSI模型。当多个层次的协议共同工作时,类似数据结构中的堆栈,因此又被称为 TCP/IP 协议栈(Protocol Stack)

单说 TCP 的话,指的是面向连接的一种传输控制协议。TCP 连接之后,客户端和服务器可以互相发送和接收消息,在客户端或者服务器没有主动断开之前,连接一直存在,故称为长连接。

Socket 其实并不是一个标准的协议,而是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,工作位置基本在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。

在设计模式中,Socket其实就是一个门面(facade)模式,它把复杂的 TCP/IP 协议族隐藏在 Socket API 后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议

最早的一套 Socket API 是采用 C 语言实现的,也就成为了 Socket 的事实标准。

常见的 Socket API 实现

一些语言的实现

传统的后端编程语言基本都有 Socket API 的封装;而在 HTML5 出现之前,要想用纯前端技术实现 TCP Socket 的客户端,也基本只有 Java Applet (java.net.Socketjava.net.DatagramSocketjava.net.MulticastSocket) 、Flash (flash.net.Socketflash.net.XMLSocket) 或 Silverlight(System.Net.Sockets) 等可以选择。

下面以 PHP 的 服务器/客户端 实现为例,演示一个最基础的例子:

<?php
//server.php

set_time_limit(0);
$ip = '127.0.0.1';
$port = 1999;
// 创建一个Socket
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
// 绑定Socket地址和端口
$ret = socket_bind($sock,$ip,$port);
// 开始监听链接
$ret = socket_listen($sock,4);

$count = 0; //最多接受几次请求后就退出
do {
	// 另一个Socket来处理通信
    if (($msgsock = socket_accept($sock)) >= 0) {        
        // 发到客户端
        $msg ="server: HELLO CLIENTS!\n";
        if (socket_write($msgsock, $msg, strlen($msg))) {
        	echo "发送成功!\n";
        }
        // 获得客户端的输入
        $buf = socket_read($msgsock,8192);
        
        $talkback = "接受成功!内容为:$buf\n";
        echo $talkback;
        
        if(++$count >= 5){
            break;
        };    
    }
    // 关闭sockets
    socket_close($msgsock);
} while (true);
socket_close($sock);
echo "TCP 连接关闭OK\n";
?>
<?php
//client.php

error_reporting(E_ALL);
set_time_limit(0);
$port = 1999;
$ip = "127.0.0.1";

// 创建Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 绑定Socket地址和端口
$result = socket_connect($socket, $ip, $port);
if ($result >= 0) echo "TCP 连接OK\n";

$in = "client: HELLO SERVER!\r\n";
if(socket_write($socket, $in, strlen($in))) {
    echo "发送成功!\n";
}
$out = '';
while($out = socket_read($socket, 8192)) {
    echo "接受成功!内容为:",$out;
}

socket_close($socket);
echo "TCP 连接关闭OK\n";
?>

(1.2) HTML5 带来的 WebSocket 协议

WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端,是一种区别于 HTTP 的全新双向数据流协议

简单的说,传统的 TCP Socket 是一套相对标准化的 API,而出现时间不久的 WebSocket 是一种网络协议 — 两码事。

WebSocket 底层是基于 TCP 协议的,所以早期草案中叫做 TCPConnection,最后之所以改名,其实是借用了传统 Socket 沟通 TCP 网络两端的意思而已。

要解决的问题

*HTTP 的工作方式*
在基于 请求/响应 模式的 HTTP/HTTPS 下,如果是对实时性要求较高的场景,客户端就需要不停的询问服务端有无可用的数据,这在各方面都是笨拙而不划算的。

*WebSocket 的工作方式*
而在 WebSocket 的全双工(允许数据在两个方向上同时传输)方式下,客户端只要静静地听招呼即可,有可用数据时服务端会自动通知它。

WebSocket 的用武之地

大部分传统的方式既浪费带宽(HTTP HEAD 是比较大的),又消耗服务器 CPU 占用(没有信息也要接受请求);而 WebSocket 则会大幅降低上述的消耗,更适用于以下场景:

  • 实时性要求高的应用
  • 聊天室
  • IoT (物联网 – internet of things)
  • 在线多人游戏

兼容性也令人满意,非要说何时不适用的话,大概就是少数必须兼容老旧浏览器,或者对实时要求明显不高的情况下了。

HTTP 的扩展

WebSocket 连接的 URL 使用 ws://wss:// 等开头,其加密、cookie 等策略和对应的 HTTP/HTTPS 基本相同。

HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的,可以把这些高级协议理解成对 TCP 的封装。


Websocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信的问题而设计的,是完全意义上的Web应用端的双向通信技术,可以取代之前使用半双工HTTP协议而模拟全双工通信,同时克服了带宽和访问速度等的诸多问题。协议定义为ws和wss协议,分别为普通请求和基于SSL的安全传输,占用端口与http协议系统,ws为80端口,wss为443端口,这样可以支持HTTP代理。

协议包含两个部分,第一个是“握手”,第二个是数据传输。

一、Websocket URI

定义的两个协议框架ws和wss与http类似,而且各自部分的要求也是在HTTP协议中使用的一样,各自的URI如下:

ws-URI = “ws:” “//” host [ “:” port ] path [ “?” query ]
wss-URI = “wss:” “//” host [ “:” port ] path [ “?” query ]

其中port是可选项,query前接“?”。

二、握手(Opening & Closing Handshake)打开连接

当建立一个Websocket连接时,为了保持基于HTTP协议的服务器软件和中间件进行兼容工作,客户端打开一个连接时使用与HTTP连接的同一个端口到服务器进行连接,这样被设计为一个升级的HTTP请求。

1、发送握手请求

此时的连接状态是CONNECTING,客户端需要提供host、port、resource-name和一个是否是安全连接的标记,也就是一个WebSocket URI。

客户端发送的一个到服务器端握手请求如下:

 

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

这个升级的HTTP请求头中的字段顺序是可以随便的。与普通HTTP请求相比多了一些字段。

    • Connection必须设置Upgrade,表示客户端希望连接升级。
    • Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
    • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
    • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
    • Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,与Referer不同的是,Origin只包含了协议和主机名称。
    • 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。
    • Sec-WebSocket-Protocol:字段表示客户端可以接受的子协议类型,也就是在Websocket协议上的应用层协议类型。上面可以看到客户端支持chat和superchat两个应用层协议,当服务器接受到这个字段后要从中选出一个协议返回给客户端。
发送请求的要求:
  • 请求的WebSocket URI必须要是定义的有效的URI。
  • 如果客户端已经有一个WebSocket连接到远程服务器端,不论是否是同一个服务器,客户端必须要等待上一个连接关闭后才能发送新的连接请求,也就是同一客户端一次只能存在一个WebSocket连接。如果想同一个服务器有多个连接,客户端必须要串行化进行。如果客户端检测到多个到不同服务器的连接,应该限制一个最大连接数,在web浏览器中应该设定最多可以打开的标签页的数目。这样可以防止到远程服务器的DDOS攻击,但这是对到多个服务器的连接,如果是到同一个服务器连接,并没有数目限制。
  • 如果使用了代理服务器,那么客户端建立连接的时候需要告知代理服务器向目标服务器打开TCP连接。
  • 如果连接没有打开,一定是某一方出现错误,此时客户端必须要关闭再次连接的尝试。
  • 连接建立后,握手必须要是一个有效的HTTP请求
  • 请求的方式必须是GET,HTTP协议的版本至少是1.1
  • Upgrade字段必须包含而且必须是”websocket”,Connection字段必须内容必须是“Upgrade”
  • Sec-Websocket-Version必须,而且必须是13 (固定版本号)

2、返回握手应答

服务器返回正确的相应头后,客户端验证后将建立连接,此时状态为OPEN。服务器响应头如下:
        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

响应头握手过程中是服务器返回的是否同意握手的依据。

  • 首行返回的是HTTP/1.1协议版本和状态码101,表示变换协议(Switching Protocol)
  • Upgrade 和 Connection:这两个字段是服务器返回的告知客户端同意使用升级并使用websocket协议,用来完善HTTP升级响应
  • Sec-WebSocket-Accept:服务器端将加密处理后的握手Key通过这个字段返回给客户端表示服务器同意握手建立连接。
  • Sec-Websocket-Procotol:服务器选择的一个应用层协议。

上述响应头字段被客户端浏览器解析,如果验证到Sec-WebSocket-Accept字段的信息符合要求就会建立连接,同时就可以发送WebSocket的数据帧了。如果该字段不符合要求或者为空或者HTTP状态码不为101,就不会建立连接。

服务器端响应步骤:
  • 解析握手请求头:获取握手依据Key并进行处理,检测HTTP的GET请求和版本是否准确,Host字段是否有权限,Upgrade字段中websocket是一个与大小写无关的ASCII字符串,Connection字段是一个大小写无关的”Upgrade”ASCII字符串,Websocket协议版本必须为13,其他的关于Origin、Protocol和Extensions可选。
  • 发送握手响应头:检测是否是wss协议连接,如果是就是用TLS握手连接,否则就是普通连接。服务器可以添加额外的验证信息到客户端进行验证。当进行一系列验证之后,服务器必须返回一个有效的HTTP响应头。响应头中每一行一个字段,结束必须为“\r\n”,使用的ABNF语法。
除了上述必要头字段之外,其他的HTTP协议定义的字段都可以使用,如Set-Cookie等。
websocket 采用 帧格式传输数据,详见  有关 websocket 基础 的博客。

 浏览器中的实现

在浏览器中可以直接调用 WebSocket 对象,其定义如下:

enum BinaryType { "blob", "arraybuffer" };
[Constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []), Exposed=(Window,Worker)]
interface WebSocket : EventTarget {
  readonly attribute USVString url;

  // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSING = 2;
  const unsigned short CLOSED = 3;
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long long bufferedAmount;

  // networking
  attribute EventHandler onopen;
  attribute EventHandler onerror;
  attribute EventHandler onclose;
  readonly attribute DOMString extensions;
  readonly attribute DOMString protocol;
  void close([Clamp] optional unsigned short code, optional USVString reason);

  // messaging
  attribute EventHandler onmessage;
  attribute BinaryType binaryType;
  void send(USVString data);
  void send(Blob data);
  void send(ArrayBuffer data);
  void send(ArrayBufferView data);
};

使用起来大概是这样的:

var ws = new WebSocket('ws://www.xxx.com/some.php');
ws.send('xxx'); //每次只能发送字符串
ws.onmessage = function(event) {
	var data = event.data;
};
ws.onerror = function() {
	ws.close();
};

II. 一个多用户交互的 WebSocket 实例

这里随便设想一个用户场景,比如我们要做一个在线纸牌游戏,肯定就是一个多人进入同一个房间的形式,并且每个人的动作能广播给其他人。

下面用 WebSocket 做一个最基础的验证原型,让每个玩家知道其他人的进入、离开、出牌、悔牌,甚至是耍赖换牌等:

(2.1) 服务器端的实现

我们用 nodejs+expressjs 搭建基础服务器,并用 https://github.com/websockets/ws 封装的库实现 WebSocket 协议的服务器端逻辑:

// server.js

var express = require('express')
var ws = require('./ws')

var app = express()

app.get('/', function (req, res) {
    res.sendFile(__dirname + '/ws.html');
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})
// ws.js

const { Server, OPEN } = require('ws');

const clients = []; //array of websocket clients
const cardsArr = []; //array of {cardId, count, title, ...}

let _lock = false;

const wss = new Server({port: 40510})
wss.on('connection', function (ws) {

	const _cid = clients.push(ws) - 1;

	ws.on('message', function (json) {

		const {
			act,
			cid,
			data
		} = JSON.parse(json);

		switch (act) {
			case 'client:join':
				_onCustomerJoin(ws, _cid);
				break;
			case 'client:leave':
				_onCustomerLeave(cid);
				break;
			case 'client:add': //增加牌
				_onAddCard(cid, data);
				break;
			case 'client:update': //修改牌
				_onUpdateCard(cid, data);
				break;
			case 'client:remove': //删除牌
				_onRemoveCard(cid, data);
				break;
			case 'client:win': //下单
				_onWin(cid);
				break;
			default:
				console.log('received: %s', act, cid)
				break;
		}

	});
});

function _ensureLock(func) {
	return function() {
		if (_lock) return;
		_lock = true;
		const rtn = func.apply(null, arguments);
		_lock = false;
		return rtn;
	};
}

function _findCard(cardId) {
	const cidx = cardsArr.map(Card=>Card.cardId).indexOf(cardId);
	return cidx;
}

const _broadcast = (excludeId, msg, data=null)=>{
	clients.forEach( (client, cidx)=>{
		if (cidx === excludeId) return;
		if (client && client.readyState === OPEN) {
			client.send(JSON.stringify({
				act: 'server:broadcast',
				msg: msg,
				data: data
			}));
		}
	} );
};

const _onCustomerJoin = (ws, cid)=>{
	ws.send(JSON.stringify({
		act: 'server:regist',
		data: {
			cid: cid
		}
	}));
	_broadcast(cid, '玩家加入:', {cid: cid});
};
const _onCustomerLeave = (cid)=>{
	clients[cid].terminate();
	clients.splice(cid, 1);
	_broadcast(cid, '玩家退出:', {cid: cid});
};
const _onAddCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d !== -1) {
		cardsArr.splice(d, 1);
	}
	cardsArr.push(data);
	_broadcast(-1, '玩家添加了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onUpdateCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d === -1) return;
	cardsArr[d] = data;
	_broadcast(-1, '玩家更改了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onRemoveCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d === -1) return;
	cardsArr.splice(d, 1);
	_broadcast(-1, '玩家删除了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onWin = _ensureLock( (cid)=>{
	//do sth. here
	_broadcast(cid, '玩家胡牌了');
} );

(2.2) 客户端的实现

<h1></h1>
<div></div>

<button onclick="_add()">出牌</button>
<button onclick="_update()">换牌</button>
<button onclick="_remove()">悔牌</button>
<button onclick="_win()">胡牌</button>
<button onclick="_leave()">离开</button>

<script>
    let cid = null; 

    const ws = new WebSocket('ws://localhost:40510');

    ws.onopen = function () {
        console.log('websocket is connected ...');

        _send({
            act: 'client:join'
        });
    };

    ws.onmessage = function (ev) {
        
        const {
            act,
            msg,
            data
        } = JSON.parse(ev.data);

        switch(act) {
            case 'server:regist':
                cid = data.cid;
                console.log(`regist: cid is ${cid}`);
                document.querySelector('h1').innerHTML = 'I AM: ' + cid;
                break;
            case 'server:broadcast':
                console.log('从服务器端接收的广播:', msg, data);
                if (data && data.cardsArr) {
                    document.querySelector('div').innerHTML = JSON.stringify(
                        data.cardsArr, null, 4
                    );
                }
                break;
            default:
                console.log(ev);
                break;
        }
    }

    function _send(json) {
        ws.send(JSON.stringify(json));
    }

    function _add() {
        _send({
            act: 'client:add',
            cid: cid,
            data: {
                cardId: 111,
                count: 1,
                title: '红桃A'
            }
        })
    }
    function _update() {
        _send({
            act: 'client:update',
            cid: cid,
            data: {
                cardId: 111,
                count: 2,
                title: '黑桃9'
            }
        })
    }
    function _remove() {
        _send({
            act: 'client:remove',
            cid: cid,
            data: {
                cardId: 111
            }
        })
    }
    function _win() {
        _send({
            act: 'client:win',
            cid: cid
        })
    }
    function _leave() {
        _send({
            act: 'client:leave',
            cid: cid
        })
    }
</script>

(2.3) 运行效果

玩家 0 加入:

玩家 1 加入:

玩家 1 出牌:

玩家 1 胡牌并退出:

与 WebSocket 类似的技术

实际上,每当谈到实时双向通信的问题时,我们自然会想起历年来一些基于 HTTP 技术的尝试;也正是基于这些之前工作中的实践和困扰,WebSocket 才应运而生。让我们大概回顾一下相关的方案及其缺陷:

轮询 (Polling)

借助于 setInterval() 等方式,客户端不断的发送请求并得到响应。这种做法比较简单,可以在一定程度上解决问题。不过对于轮询的时间间隔需要进行仔细考虑。轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。

长轮询 (Long Polling)

这是对轮询的一种改进。客户端发出请求后,服务器端用 while(true) 等方式阻塞住请求,直到有可用数据才发送响应数据,而客户端收到响应后再发送下一个请求。

这种方式又被成为 “Hanging GET”、“反向 Ajax” 或 “Comet” 等,虽然看上去很像服务器推送,但仍然是基于 HTTP 的一种慢响应;且在数据更新频繁的情况下,其效率并不优于一般的轮询。

HTTP 流 (Streaming)

使用 HTTP 1.1 且响应头中包含 Transfer-Encoding: chunked 的情况下,服务器发送给客户端的数据可以分成多个部分,保持打开(while-true, sleep等),并周期性 flush() 分块传输。

客户端只发送一个HTTP连接,在 xhr.readyState==3 状态下,用 xhr.responseText.substring 获取每次的数据。

但是数据响应可能会因 代理服务器 或 防火墙 等中间人造成延迟,所以可能还要额外探测这种情况以切换到长轮询方式。

SSE (Server-Sent Events)

SSE 规范也是 HTML 5 规范的一个组成部分。服务器端响应的内容类型是text/event-stream,在浏览器端使用 EventSource 对象处理返回的数据。

比之于 WebSocket,SSE 的缺点在于:

  • 不支持 CORS
  • 单向通道,只能服务器向浏览器端发送
  • 浏览器兼容性稍差

III. 总结

  • 传统的 TCP Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API
  • WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端
  • WebSockets 是 HTML 5 规范的一个组成部分,是一种区别于 HTTP 的全新双向数据流协议
  • 全双工通信的 WebSockets 有效改善了之前 长轮询 等方式的弊端
  • WebSockets 适用于实时性要求高的应用、聊天室、多人游戏等

参考:

https://juejin.im/post/5ae3eb9b51882567382f5767

https://www.cnblogs.com/oshyn/p/3574497.html


http://www.cnblogs.com/hustskyking/p/websocket-with-node.html

http://www.cnblogs.com/hustskyking/p/websocket-with-php.html

https://www.cnblogs.com/zxtceq/p/6963964.html

web通信方式-概述和总结

web通信,一个特别大的topic,涉及面也是很广的。因最近学习了 javascript 中一些 web 通信知识,在这里总结下。文中应该会有理解错误或者表述不清晰的地方,还望斧正!

一、前言


1. comet技术

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

随着互联网的发展,web 应用层出不穷,也不乏各种网站监控、即时报价、即时通讯系统,为了让用户得到更好的体验,服务器需要频繁的向客户端推送信息。开发者一般会采用基于 AJAX 的长轮询方式或者基于 iframe 及 htmlfile 的流方式处理。当然有些程序需要在客户端安装各种插件( Java applet 或者 Flash )来支持性能比较良好的“推”信息。

2. HTTP协议中的长、短连接

短连接的操作步骤是:建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:建立连接——数据传输…(保持连接)…数据传输——关闭连接

长连接与短连接的不同主要在于client和server采取的关闭策略不同。短连接在建立连接以后只进行一次数据传输就关闭连接,而长连接在建立连接以后会进行多次数据数据传输直至关闭连接(长连接中关闭连接通过Connection:closed头部字段)。

二、web 通信


首先要搞清楚,xhr 的 readystate 各种状态。

属性 描述
onreadystatechange 存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState 存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。

0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪

status 200: “OK”
404: 未找到页面

1.轮询

轮询是一种“拉”取信息的工作模式。设置一个定时器,定时询问服务器是否有信息,每次建立连接传输数据之后,链接会关闭。

前端实现:

var polling = function(url, type, data){
    var xhr = new XMLHttpRequest(), 
        type = type || "GET",
        data = data || null;

    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;
        }
    };

    xhr.open(type, url, true);
    //IE的ActiveXObject("Microsoft.XMLHTTP")支持GET方法发送数据,
    //其它浏览器不支持,已测试验证
    xhr.send(type == "GET" ? null : data);
};

var timer = setInterval(function(){
    polling();
}, 1000);

在轮询的过程中,如果因为网络原因,导致上一个 xhr 对象还没传输完毕,定时器已经开始了下一个询问,上一次的传输是否还会在队列中,这个问题我没去研究。如果感兴趣可以自己写一个ajax的请求管理队列。(研究结果:后一个会把前一个覆盖掉 即前一个就算返回 也不会认了)

2.长轮询(long-polling)

长轮询其实也没啥特殊的地方,就是在xhr对象关闭连接的时候马上又给他接上~ 看码:

var longPoll = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){
        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;

            longPoll(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
}

只要服务器断开连接,客户端马上连接,不让他有一刻的休息时间,这就是长轮询。

3.数据流

数据流方式,在建立的连接断开之前,也就是 readystate 状态为 3 的时候接受数据,但是麻烦的事情也在这里,因为数据正在传输,你拿到的 xhr.response 可能就是半截数据,所以呢,最好定义一个数据传输的协议,比如前2个字节表示字符串的长度,然后你只获取这个长度的内容,接着改变游标的位置。

假如数据格式为: data splitChar   data为数据内容,splitChar为数据结束标志(长度为1)。 那么传输的数据内容为 data splitChar data splitChar data splitChar…

var dataStream = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){

        // 状态为 3,数据接收中
        if(xhr.readyState == 3) {
            var i, l, s;

            s = xhr.response; //读取数据
            l = s.length;     //获取数据长度

            //从游标位置开始获取数据,并用分割数据
            s = s.slice(p, l - 1).split(splitChar);

            //循环并操作数据
            for(i in s) if(s[i])  deal(s[i]);

            p = l;  //更新游标位置

        }

        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            xhr.onreadystatechange = null;

            dataStream(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
};

这个代码写的是存在问题的,当readystate为3的时候可以获取数据,但是这时获取的数据可能只是整体数据的一部分,那后半截就拿不到了。readystate在数据传输完毕之前是不会改变的,也就是说他并不会继续接受剩下的数据。我们可以定时去监听readystate,这个下面的例子中可以看到。

这样的处理不算复杂,但是存在问题。上面的轮询和长轮询是所有浏览器都支持的,所以我就没有写兼容IE的代码,但是这里,低版本IE不允许在readystate为3的时候读取数据,所以我们必须采用其他的方式来实现。

在ajax还没有进入web专题之前,我们已经拥有了一个法宝,那就是iframe,利用iframe照样可以异步获取数据,对于低版本IE可以使用iframe来接受数据流。

if(isIE){
    var dataStream = function(url){
        var ifr = document.createElement("iframe"), doc, timer;

        ifr.src = url;
        document.body.appendChild(ifr);

        doc = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(ifr.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(ifr.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

定时去监听iframe的readystate的变化,从而获取数据流,不过,上面的处理方式还是存在问题。数据流实现“服务器推”数据的原理是什么呢,简单点说,就是文档(数据)还没有加载完,这个时候浏览器的工作就是去服务器拿数据完成文档(数据)加载,我们就是利用这点,给浏览器塞点东西过去~ 所以上述利用iframe的方式获取数据,会使浏览器一直处于加载状态,title上的那个圈圈一直在转动,鼠标的状态也是loading,这看着是相当不爽的。幸好,IE提供了HTMLFile对象,这个对象就相当于一个内存中的Document对象,它会解析文档。所以我们创建一个HTMLFile对象,在里面放置一个IFRAME来连接服务器。这样,各种浏览器就都支持了。

if(isIE){
    var dataStream = function(url){
        var doc = new ActiveXObject("HTMLFile"), 
            ifr = doc.createElement("iframe"), 
            timer, d;

        doc.write("<body/>");

        ifr.src = url;
        doc.body.appendChild(ifr);

        d = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(d.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(d.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

4.websocket

websocket是前端一个神器,ajax用了这么久了,相关技术也是很成熟,不过要实现个数据的拉取确实十分不易,从上面的代码中也看到了,各种兼容性问题,各种细节处理问题,自从有了websocket,哈哈,一口气上五楼…

var ws = new WebSocket("ws://www.example.com:8888");

ws.onopen = function(evt){};
ws.onmessage = function(evt){
    deal(evt.data);
};
ws.onclose  = function(evt){};

//ws.close();

新建一个WebSocket实例,一切就OK了,ws:// 是websocket的连接协议,8888为端口号码。onmessage中提供了data这个属性,相当方便。

(WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范,WebSocketAPI被W3C定为标准。)

5.EventSource

HTML5中提供的EventSource这玩意儿,这是无比简洁的服务器推送信息的接受函数。

new EventSource("test.php").onmessage=function(evt){
    console.log(evt.data);
};

简洁程度和websocket是一样的啦,只是这里有一个需要注意的地方,test.php输出的数据流应该是特殊的MIME类型,要求是”text/event-stream”,如果不设置的话,你试试~ (直接抛出异常)

6.ActionScript

情非得已就别考虑这第六种方式了,虽说兼容性最好,要是不懂as,出了点bug你也不会调试。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。flash现在差不多也快淘汰了)

7.Java Applet套接口

这玩意儿原理和Flash类似,不过我不懂,就不细说了。(Java Applet现在差不多也快淘汰了)

三、后端处理方式


本文主要是总结Javascript的各种通讯方式,后端配合node来处理,应该是挺给力的。

var conns = new Array();

var ws = require("websocket-server");
var server = ws.createServer();

server.addListener("connection", function(connection){
  console.log("Connection request on Websocket-Server");
  conns.push(connection);
  connection.addListener('message',function(msg){
        console.log(msg);
        for(var i=0; i<conns.length; i++){
            if(conns[i]!=connection){
                conns[i].send(msg);
            }
        }
    });
});
server.listen(8888);

下面是一个php的测试demo。

header('Content-Type:text/html; charset=utf-8');
while(1){
    echo date('Y-m-d H:i:s');
    flush();
    sleep(1);
};

四、web 通信方式利弊分析

  • 轮询,这种方式应该是最没技术含量的,操作起来最方便,不过是及时性不强,把定时器的间隔时间设置的短一些可以稍微得到缓和。
  • 长轮询,算是比较不错的一个web通讯方式,不过每次断开连接,比较耗服务器资源,客户端到无所谓。
  • 数据流,他和长轮询不同之处是接受数据的时间不一样,数据流是readystate为3的时候接受,低版本IE不太兼容,处理起来略麻烦,而且还要自己设计数据传输协议。不过他对资源的消耗比上面几种都可观。
  • websocket和EventSource,两个利器,不过,没几个浏览器支持,这是比较让人伤心~(现在2018年,主流浏览器都支持了,毕竟原博客2013年发的)
  • ActionScript和Java Applet,两者都是需要在客户端安装插件的,一个是Flash插件,一个是Java插件,而且搞前端的人一般对这东西不太熟悉,如果没有封装比较好的库可以使用,那建议还是别用了。

五、参考资料


来自:http://www.cnblogs.com/hustskyking/p/web-communication.html

docker安装配置-centos6与centos7

一、提前说明

CentOS Docker 安装
Docker支持以下的CentOS版本:
CentOS 7 (64-bit)
CentOS 6.5 (64-bit) 或更高的版本

前提条件
目前,CentOS 仅发行版本中的内核支持 Docker。
Docker 运行在 CentOS 7 上,要求系统为64位、系统内核版本为 3.10 以上。
Docker 运行在 CentOS-6.5 或更高的版本的 CentOS 上,要求系统为64位 、系统内核版本为 2.6.32-431 或者更高版本。

查看自己的内核
uname命令用于打印当前系统相关信息(内核版本号、硬件架构、主机名称和操作系统类型等)。

### 查看内核版本
[ad@book]# uname -r
4.18.8-x86_64-linode117

### 查看版本号, 命令也可以是 cat /etc/redhat-release
[ad@book]# cat /etc/centos-release
CentOS Linux release 7.5.1804 (Core)

二、centos6安装docker

1、Docker使用EPEL发布,RHEL系的OS首先要确保已经持有EPEL仓库,否则先检查OS的版本,然后安装相应的EPEL包。
yum install -y epel-release

2、安装docker
yum install -y docker-io

3、安装后的配置文件:【后面需要用到,主要是为了配置镜像加速】
/etc/sysconfig/docker

4、启动Docker后台服务:
service docker start

5、验证
docker version

6、配置镜像加速地址【网易云地址也同理】

鉴于国内网络问题,后续拉取 Docker 镜像十分缓慢,我们可以需要配置加速器来解决, 
我使用的是阿里云的本人自己账号的镜像地址(需要自己注册有一个属于你自己的): https://xxxx.mirror.aliyuncs.com 
  
vim /etc/sysconfig/docker 
   将获得的自己账户下的阿里云加速地址配置进 
other_args="--registry-mirror=https://你自己的账号加速信息.mirror.aliyuncs.com" 

7、重新启动Docker后台服务:
service docker restart

8、Linux 系统下配置完加速器需要检查是否生效
ps -ef|grep docker

三、centos7安装docker

1、yum安装gcc相关
yum -y install gcc
yum -y install gcc-c++

2、卸载旧版本
旧方法:yum -y remove docker docker-common docker-selinux docker-engine
新方法:yum remove docker \ 
                  docker-client \ 
                  docker-client-latest \ 
                  docker-common \ 
                  docker-latest \ 
                  docker-latest-logrotate \ 
                  docker-logrotate \ 
                  docker-selinux \ 
                  docker-engine-selinux \ 
                  docker-engine 

3、安装需要的软件包 yum install -y yum-utils device-mapper-persistent-data lvm2

4、设置stable镜像仓库  
海外:yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
国内:yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

5、更新yum软件包索引  
yum makecache fast

6、安装DOCKER CE 版本 
yum -y install docker-ce

7、启动docker   
systemctl start docker

8、测试
docker version

9、配置镜像加速
mkdir -p /etc/docker
vim  /etc/docker/daemon.json     
=================下面是配置文件==================
#网易云 
{"registry-mirrors": ["http://hub-mirror.c.163.com"] } 


 #阿里云 
{ 
  "registry-mirrors": ["https://{自已的编码}.mirror.aliyuncs.com"] 
} 
=================上面是配置文件==================

systemctl daemon-reload
systemctl restart docker

10、卸载
systemctl stop docker 
yum -y remove docker-ce
rm -rf /var/lib/docker

 

docker入门概述-(3)-docker原理

一、Docker是怎么工作的

Docker是一个Client-Server结构的系统,Docker守护进程运行在主机上, 然后通过Socket连接从客户端访问,守护进程从客户端接受命令并管理运行在主机上的容器 。 容器,是一个运行时环境,就是我们前面说到的集装箱。

二、为什么Docker比较比VM快

1、docker有着比虚拟机更少的抽象层。由于docker不需要Hypervisor实现硬件资源虚拟化,运行在docker容器上的程序直接使用的都是实际物理机的硬件资源。因此在CPU、内存利用率上docker将会在效率上有明显优势。

2、docker利用的是宿主机的内核,而不需要Guest OS。因此,当新建一个容器时,docker不需要和虚拟机一样重新加载一个操作系统内核。仍而避免引寻、加载操作系统内核返个比较费时费资源的过程,当新建一个虚拟机时,虚拟机软件需要加载Guest OS,返个新建过程是分钟级别的。而docker由于直接利用宿主机的操作系统,则省略了返个过程,因此新建一个docker容器只需要几秒钟。

三、Docker的基本组成

1、镜像(image)
Docker 镜像(Image)就是一个 只读 的模板。镜像可以用来创建 Docker 容器, 一个镜像可以创建很多容器 。
容器与镜像的关系,类似于面向对象编程中的对象与类。

Docker 面向对象
容器 对象
镜像

2、容器(container)
1、Docker 利用容器(Container)独立运行的一个或一组应用。 容器是用镜像创建的运行实例 。
2、它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。
3、可以把容器看做是一个简易版的 Linux 环境(包括root用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
4、容器的定义和镜像几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。

3、仓库(repository)
仓库(Repository)是 集中存放镜像 文件的场所。
仓库(Repository)和仓库注册服务器(Registry)是有区别的。仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。

仓库分为公开仓库(Public)和私有仓库(Private)两种形式。
最大的公开仓库是 Docker Hub(https://hub.docker.com/) , 存放了数量庞大的镜像供用户下载。国内的公开仓库包括阿里云 、网易云 等

总结:需要正确的理解仓储/镜像/容器这几个概念:

Docker 本身是一个容器运行载体或称之为管理引擎。我们把应用程序和配置依赖打包好形成一个可交付的运行环境,这个打包好的运行环境就似乎 image镜像文件。只有通过这个镜像文件才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

*  image 文件生成的容器实例,本身也是一个文件,称为镜像文件。
*  一个容器运行一种服务,当我们需要的时候,就可以通过docker客户端创建一个对应的运行实例,也就是我们的容器 。
* 至于仓储,就是放了一堆镜像的地方,我们可以把镜像发布到仓储中,需要的时候从仓储中拉下来就可以了。

四、Docker运行流程


Docker测试运行 hello-world) 流程:


输出这段提示以后,hello world就会停止运行,容器自动终止。很重要的要说明的一点: Docker容器后台运行,就必须有一个前台进程。


在run命令后加执行脚本命令,比如
docker run -d --name=test test:2.0 /bin/bash -c /home/test.sh
然后容器状态就是Exited,之后也无法再启动容器。
但是commit容器后再登陆进去,发现脚本确实执行了。
能不能既执行脚本还让容器处于运行状态,求解?是我命令不对吗?

你没理解docker的原理。你的脚本只是一次性执行,自然自行完毕就退出了。想让容器一直处于running状态,那么你的程序就不可以退出,一直运行。比如ping www.baidu.com【注意要想让容器一直运行,则容器需要一个前台进程】