SocketIO-实时-Web-应用开发-全-
SocketIO 实时 Web 应用开发(全)
原文:
zh.annas-archive.org/md5/d94fdae1850caa045c07770fadeb6c06译者:飞龙
前言
实时网络应用传统上是一个具有挑战性的目标,依赖于黑客手段和幻觉。许多人因为认为其复杂性而避免实现实时性。本书将向您展示如何使用 Socket.IO 构建现代实时网络应用,并介绍 Socket.IO 的各种功能,并指导您进行聊天服务器的开发、托管和扩展。
本书涵盖的内容
第一章, 在网络上实现实时性,介绍了实时网络应用及其历史。
第二章, 开始使用 Node.js,介绍了 Node.js 及其相关技术。Node.js 是许多现代网络应用的平台,这些应用都是用 JavaScript 编写的。
第三章, 让我们聊天,使我们能够启动我们的第一个单页聊天系统,并介绍了 Socket.IO API 用于实时通信。
第四章, 让它更有趣!,为我们的聊天应用添加了更多功能,例如给我们的用户提供一个名字,拥有多个聊天室,以及将即时消息与 Socket.IO 会话集成。
第五章, Socket.IO 协议, 解释了 Socket.IO 协议、其机制和工作原理。
第六章, 部署和扩展,解释了将我们的聊天系统投入生产并扩展其规模所涉及的复杂性。
附录 A, Socket.IO 快速参考,是 Socket.IO API 的参考指南。
附录 B, Socket.IO 后端,列出了针对不同语言和平台的几种替代后端实现。
你需要这本书的内容
使用本书时,我们不假设任何特殊的软件要求。您需要一个装有 Linux 或 Windows OS 或 Mac 的 PC。您可以使用任何文本编辑器进行编码,但拥有如 Vi、Emacs、Notepad++、Sublime Text 或您选择的任何 IDE 的程序员编辑器将有所帮助。我们将随着本书的进展和需要时安装剩余的软件,如 Node.js 和 npm。
本书面向的对象
本书面向希望开始开发高度交互性和实时网络应用(如聊天系统、在线多人游戏)或希望在现有应用中引入实时更新或服务器推送机制的开发者。预期读者具备 JavaScript 开发和一般网络应用的知识。尽管本书有一章介绍 Node.js,但具备 Node.js 的先验知识将是一个加分项。读者需要能够运行 Node.js 的计算机系统,一个测试或代码编辑器,以及访问互联网以下载所需的软件和组件。
习惯用法
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词如下所示:“为了设置我们的节点服务器运行的环境,我们将环境变量NODE_ENV设置为我们要在其中运行节点的环境。”
代码块设置如下:
<!DOCTYPE html>
<html>
<head>
<title>{TITLE}</title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<header id="banner">
<h1>Awesome Chat</h1>
</header>
{CONTENT}
<footer>
Hope you enjoy your stay here
</footer>
</body>
</html>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
doctype 5
html
block head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
header#banner
h1 Awesome Chat
block content
footer Hope you enjoy your stay here
任何命令行输入或输出都应如下编写:
$ express awesome-chat
$ cd awesome-chat
$ npm install
新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“现在您可以在浏览器中的一个消息框中输入您的消息,然后点击发送。您将看到它在两个浏览器的消息区域中显示。”
注意
警告或重要注意事项将以如下框显示。
小贴士
技巧和窍门将以如下方式显示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们来说非常重要,以便我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。
如果您在某个主题领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为 www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有勘误。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过发送链接到疑似盗版材料至<copyright@packtpub.com>与我们联系。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决。
第一章. 在网络上实现实时性
阿拉伯之春革命是通过 Facebook 和 Twitter 等社交媒体网站引发的,并得到了推动。在接下来的几天里,社交媒体从仅仅是与家人和朋友互动的手段变成了赋予人民权力并带来世界重大变化的武器。每个人都注意到了人民的力量,人们也注意到了社交网络的能力。所有这一切的核心是使这一切成为可能的技术,这种技术消除了所有沟通的障碍,比野火传播得更快。这就是实时网络的力量!
什么是实时网络?
在网络上,我们已经习惯了点击链接或按钮、更改一些输入并执行某些操作,从而导致页面发生变化的应用程序和网站。但如果我们让我们的 Twitter 页面保持一段时间,当我们收到新的推文时,我们会收到警报,即使我们没有执行任何操作(如下一张截图所示)。这就是我们通常所说的“实时网络”的含义。

Twitter 上的实时更新
维基百科用以下这些话介绍了实时网络:
实时网络是一套技术和实践,使用户能够在作者发布信息时立即接收信息,而不是要求他们或他们的软件定期检查源以获取更新。
这“套技术”是网络中最热门的趋势之一。在接下来的几页中,我们将熟悉这些技术,并了解它们在各种应用中的使用。
一点历史
要理解和完全欣赏任何概念,了解它的来源和演变过程是很重要的。
实时网络并不是新事物;最早尝试使网络实现实时性的尝试之一是使用 Java 小程序。许多人会记得在 20 世纪 90 年代末在 Yahoo!聊天室聊天或下棋。然后出现了 Flash 和 ActiveX 插件。这不仅仅是为了“娱乐”(针对消费者部分),还用于企业市场。我在职业生涯的早期阶段为一家 BPM(业务流程管理)公司工作,他们当时开发了一个 ActiveX 插件,用于为他们的仪表板提供动力并实时更新流程信息。那么为什么现在很重要呢?因为实现实时功能的方式以及实现这种功能所涉及的成本已经发生了变化。从成为应用程序中的一个花哨功能,它已经变成了必需品——用户的需求。从成为应用程序中非法添加或技术上有挑战性的部分,它正在成为 WebSocket 和服务器发送事件(SSE)形式的认可标准。我们是如何从静态网络到现在的?
众所周知,网络(和网络应用)是建立在 HTTP 协议之上的。HTTP 是一个请求-响应系统,其中客户端向服务器发送信息请求,服务器以请求的信息进行响应。在大多数情况下,这些信息是浏览器将要渲染的 HTML 或相关信息,如 XML 或 JSON。以下图显示了 HTTP 浏览器-服务器交互:

HTTP 浏览器-服务器交互
在 1995 年,Sun 和 Netscape 宣布了一项合作,Netscape 将 Sun 的新 Java 运行时与其浏览器捆绑在一起。这是高度交互式网络的开始。尽管他们后来获得了非常糟糕的名声,但小程序是实时网络领域的先驱。在实时网络的早期,我们看到小程序被用于聊天、游戏,甚至是横幅广告。
在同一年,Netscape 提出了一种名为 JavaScript(最初称为 LiveScript)的脚本语言,另一家名为 FutureWave Software 的小公司开始开发一款名为 FutureSplash Animator 的动画软件。后来,这两者都成为了 Java 小程序几乎从网络中消失的原因。
FutureWave 在 1996 年被 Macromedia 收购,他们将 FutureSplash Animator 重命名为 Flash。正如我们所知,Flash 继续统治网络,成为创建动画、游戏、视频播放器和所有交互式内容的最大平台,在接下来的十年中占据了主导地位。
在 1999 年,Microsoft 使用其 iframe 技术和 JavaScript 更新了 Internet Explorer 默认主页上的新闻和股票报价(home.microsoft.com)。同年,他们为 IE 发布了一个专有的 ActiveX 扩展,称为 XMLHTTP。这是 XML 成为“热门”事物的时代,每个人都想在他们所做的事情中使用 XML。这个 XMLHTTP 组件最初是为了使用 JavaScript 异步加载页面中的 XML 数据。它很快被 Mozilla、Safari 和 Opera 采用,作为 XMLHttpRequest(或简称 XHR)。但是,随着 Gmail(由 Google 推出)的推出,Jesse James Garrett 在一篇题为 Ajax: A New Approach to Web Applications 的文章中创造的术语 AJAX(异步 JavaScript 和 XML)成为了网络开发的流行语。以下图显示了 AJAX 请求:

AJAX 请求
Gmail 也展示了网页实时更新的优势,并打开了使用 AJAX 构建的各种黑客攻击的大门,以从服务器(或至少,给人一种这样做的感觉)推送数据。
这些技术统称为 Comet——这是 Alex Russell 在 2006 年他在博客中引入的一个术语。Comet 是对 Ajax 这个词的戏仿,两者在美国都是流行的家用清洁剂。Comet 并不是单一的方法。它引入了多种机制来给人一种数据从服务器推送到客户端的感觉。这些包括隐藏 iframe、XHR 轮询、XHR 长轮询和 script 标签长轮询(或 JSONP 长轮询)。
让我们了解这些是如何工作的,因为它们仍然是所有现代浏览器中最常见的机制。
其中第一种也是最简单易实现的是 XHR 轮询,其中浏览器定期轮询数据,服务器除非有数据要发送回浏览器,否则会一直响应空响应。在事件发生后,例如接收邮件或在数据库中创建/更新记录,服务器会使用新数据响应下一个轮询请求。以下图展示了这一机制:

XHR polling
如您所见,这里有一个问题。即使没有数据,浏览器也必须不断向服务器发送请求。这导致服务器在没有要发送的内容时获取和处理数据。
解决这个问题的方法之一是修改服务器,通过不仅发送客户端请求的数据,还附加服务器拥有的其他数据来“搭载”实际的客户端请求。客户端需要被修改以理解并处理这些额外的传入数据。以下图显示了 HTTP piggybacking 的过程:

HTTP piggybacking
由于新数据仅在客户端有动作时发送,这会导致数据到达浏览器的时间延迟。在快速接收事件的同时避免频繁的服务器查询的解决方案是长轮询。
在长轮询中,当浏览器向服务器发送请求时,如果服务器没有数据响应,它不会立即响应,并将挂起请求。一旦发生事件,服务器通过向客户端发送响应来关闭挂起的请求。一旦客户端收到响应,它就发送一个新的请求:

长轮询
长轮询有多种实现方式,例如永久性 iframe、多部分 XHR、带有 JSONP 的 script 标签和长生存期 XHR。
虽然所有这些技术都有效,但这些是黑客手段,通过弯曲 HTTP 和 XHR 来实现双向通信,而这并不是它们的目的。
随着 Firefox 和 Chrome 等浏览器快速的发展,HTML 的长期升级,即 HTML5,正在被广泛采用。在 HTML5 中,有两种新的从服务器向客户端推送数据的方法。一种是服务器发送事件(SSE),另一种是全双工 WebSockets。
服务器端事件尝试在浏览器之间标准化类似 Comet 的通信。在这种方法中,有一个 JavaScript API 用于创建事件源,即服务器可以发送事件的流。这是一个单向协议。我们仍然会使用老式的 XHR。当您不需要全双工通信时,这是一个很好的方法;只需从服务器向客户端推送更新。
另一个实现全双工通信协议的规范是 WebSockets。在 WebSockets 中,客户端与支持此协议的服务器建立 socket 连接,服务器和客户端将在此 socket 连接上发送和接收数据。
实时网络的应用
让我们快速看一下实时网络是如何改变我们每天在网络上遇到的应用程序的。
游戏
随着 Zynga 和其他社交游戏公司的成功,在线游戏已成为一种热门趋势。WordSquared 是一个大规模并行的在线多人填字游戏,而 BrowserQuest 是 Mozilla 尝试构建的浏览器内实时角色扮演游戏。基于 socket.io 构建的更受欢迎和公开宣传的游戏之一是 Rawkets。有许多基于 canvas 和实时通信系统构建的开源游戏引擎。
社交流更新
Twitter 是获取实时数据(推文)到浏览器而不需要用户操作的最好例子。Google+和 Facebook 也有。在社交网络上,重要的是能够实时了解发生的事情。
商业应用
CRM(客户关系管理)是商业并购中最重要的组成部分之一。将问题跟踪系统作为 CRM 出售的日子已经过去了。CRM 正在不断改进和自我革新。大多数 CRM 都在添加社交功能;他们每天都在增加更多功能。Salesforce——最受欢迎的托管 CRM 解决方案之一,推出了 Chatter。Chatter 为 CRM 添加了社交功能,并带来了许多由实时更新驱动的优势。它允许客户在系统上添加评论或发布更新,这些问题会实时出现在支持人员的系统中。BPM(业务流程管理)解决方案也在整合实时组件,以跟踪流程状态和更新。
基于网络的监控器
Google Analytics 的最新更新包括了一个功能,可以查看访问您网站的用户的实时更新。Splunk——一个广泛用于监控基础设施和机器数据的事件跟踪系统——允许您在实时更新的图表上监控和跟踪事件更新。
摘要
在本章中,我们看到了实时网络的样子,它的应用是什么,以及围绕实时网络的技术是如何在十年发展中演变的。在下一章中,我们将了解 Node.js,这是一个 JavaScript 网络应用程序开发平台,它是 socket.io 的主要目标。
第二章:Node.js 入门
在 Node.js 网站 (nodejs.org/) 上给出的 Node.js 定义如下:
Node.js 是一个基于 Chrome 的 JavaScript 运行时构建的平台,用于轻松构建快速、可扩展的网络应用程序。Node.js 使用事件驱动的、非阻塞的 I/O 模型,使其轻量级且高效,非常适合运行在分布式设备上的数据密集型实时应用程序。
对我们来说重要的是,作为平台的一部分,Node.js 提供了一个可扩展且高性能的 Web 应用程序开发框架,允许使用 JavaScript 进行编程。
我们中的许多人是在构建网站或 Web 应用程序以进行 DOM 操作、AJAX 和相关内容时接触到 JavaScript 的。但 JavaScript 远不止于此。就像 C、Java、Python 等一样,JavaScript 也是一个完整的编程语言。在所有浏览器中,JavaScript 都在浏览器的上下文中在虚拟机 (VM) 中执行。但它也可以在另一个上下文中执行——就像 Node.js 后端的情况一样——而不需要浏览器。
Node.js 使用 Google Chrome 的 JavaScript VM 在浏览器之外执行 JavaScript 应用程序,在服务器上。除了这个运行时环境之外,Node.js 还提供了一组模块库,为构建网络应用程序提供了一个框架。Node.js 不是一个像 Apache HTTP 服务器那样的网络服务器,也不是像 Tomcat 那样的应用程序服务器;但作为其模块库的一部分,Node.js 也提供了一个 HTTP 服务器,可以用来构建 Web 应用程序。
除了将 JavaScript 作为应用程序的编程语言之外,Node.js(以及大多数 Node.js 模块和应用)与传统服务器和应用程序区分开来的一点是异步事件驱动开发模型,我们将在后面的章节中看到这一点。
Node.js 的起源
这并不是 JavaScript 首次被用于服务器端编程。Netscape 在 1996 年推出了 Netscape Enterprise Server,允许使用 JavaScript 进行服务器端编程。从那时起,许多服务器,如 RingoJS (ringojs.org/)、Persevere (www.persvr.org/)、基于 Mozilla Rhino 的服务器以及其他服务器,都尝试效仿。
这些服务器没有被认真对待的主要原因之一是它们使用的 JavaScript 虚拟机 (VM) 的性能可怜。浏览器中的 JavaScript 性能也不是很好。直到 Google 推出了其 Chrome 网络浏览器,这种情况才有所改变。
在其发布时,Chrome 的 JavaScript VM,称为 V8,几乎比其他任何 JavaScript VM 快 10-20 倍,并且从那时起一直是最快的。
Node.js 是基于这个虚拟机在 2008 年由 Ryan Dahl 开发的。他想要构建一个能够支持并赋能实时交互式网络应用,如 Gmail 的服务器。但 Node.js 并不是他第一个构建的服务器。Ryan 构建了基于 Ruby 和 C 的 Ebb 服务器,但意识到它并没有达到他期望的运行速度。随后,他进行了一系列构建多个小型服务器的实验。
借助从实验和各种平台研究中获得的知识,他决定开发一个事件驱动或异步服务器。2008 年 1 月,他提出了基于 JavaScript 构建小型服务器的想法。他倾向于选择 JavaScript,因为它独立于操作系统,并且没有 I/O API。他辞去了工作,用 6 个月的时间专注于 Node.js。2009 年 11 月,他在 JSConf 上展示了 Node.js,并从那时起为 Joyent 工作。最初,Node.js 只能在基于 Unix 的系统上运行;后来,它也增加了对 Windows 操作系统的支持。
为什么选择 Node.js
Node.js 是一个新兴的平台,仍在不断发展(甚至还没有发布 1.0 版本),但即使在其初期,它可能也是 Web 上最受欢迎的平台之一。它已经为大量流行的服务提供动力。让我们看看是什么让 Node.js 如此诱人和受欢迎。
JavaScript 到处都是
Node.js 的首要优势是 JavaScript。如果你知道并经常用 JavaScript 编码,你已经了解了 Node.js 的大部分内容;剩下的学习内容可以看作是 API 和最佳实践。
基于 Google Chrome 的 V8 JavaScript 引擎构建的 Node.js 允许使用 JavaScript 编写整个应用程序。我们已经在用 JavaScript 编写前端;有了 Node.js,我们也可以用同样的语言编写后端,这是我们磨练技能并深深喜爱的语言。它让每个前端开发者都无需学习另一种语言或依赖其他开发者来暴露应用程序所需的 RESTful API。
事件驱动设计
Node.js 是围绕事件和回调进行设计的。作为一个 JavaScript 开发者,你早已熟悉监听事件和使用回调的概念。Node.js 将这种理念融入到平台的每一个方面。无论是服务器请求处理、I/O 还是数据库交互,Node.js 中的所有操作理想上都将由一个事件监听器附加的回调来处理。
这带我们来到了 Node.js 背后最重要的概念之一,那就是 事件循环。我喜欢 Dan York (code.danyork.com) 用快餐店类比来解释基于事件循环的系统。
考虑一家餐厅,你走到收银台,下单,然后等待你的食物准备好。在这种情况下,收银员在你下单之前不能服务其他顾客,队列被阻塞。如果餐厅有大量的顾客涌入并需要扩展,他们就必须投资雇佣更多的收银员,创建更多的收银台等等。这类似于传统的多线程模型。
相比之下,让我们看看许多其他餐厅使用的模型。在这种情况下,你走到收银台并下单(他/她将其交给厨房);然后他/她接受你的付款并给你一个凭证。然后你走到一边,收银员继续服务下一个顾客。当你的订单准备好时,厨房服务器通过叫你的名字或闪烁你的凭证号码来宣布这一点,然后你走过去取你的订单。这种面向事件的方法优化了收银员的工作,并让你在旁边等待,直到你的工作完成,从而释放相关资源为其他人提供服务。
在 Node.js 中,服务器就是收银台,所有的处理程序都是厨房工作人员。服务器接受一个请求并将其分配给一个处理程序。然后它继续接受其他请求。当请求被处理并且结果就绪时,响应被排队在服务器上,并在到达队列前端时发送回客户端。
与传统的启动服务器线程或进程的方法(这类似于增加更多的收银员)相比,这种方法更高效,因为启动的工作者有专门的责任。这比复制整个服务器要轻便和便宜得多。
在接下来的章节中,我们将看到我们如何将处理程序或工作者注册到服务器上以处理某些请求,而服务器所做的只是将这些请求委派给这些工作者。
事件驱动设计的优势在于我们设计的所有内容都是非阻塞的。“你不等待我,我召唤你”这句箴言使我们免受等待请求完成的痛苦。它释放了本应花费在等待请求上的系统资源,以便它们可以被用于队列中的其他任务。这使得 Node.js 应用程序能够提供非常高的性能和非常高的负载处理能力。
Node.js 是一个模块化框架,从底层开始就拥有一个现代的模块系统。Node.js 中的所有内容都是构建在 V8 JavaScript 引擎中运行的模块。平台上的每个功能都是通过模块提供的。这使平台保持精简,只引入所需的内容。拥有本地的模块系统也有助于保持我们的应用程序模块化。
在过去几年中,JavaScript 已经成为最广泛使用的语言之一,并且拥有一个充满活力的社区。Node.js 为开发者提供了一个良好的平台,帮助他们使用 JavaScript 开发端到端应用程序。Node.js 还引入了许多革命性的概念,例如始终异步、非阻塞 I/O、面向事件的服务器等。这导致了一个非常活跃、庞大且活跃的社区。新的模块不断涌现,社区提供积极的支持,非常有帮助。为 Node.js 构建的大多数流行模块和框架通常来自社区,并且大多是开源的。
企业支持
在过去几年中,许多公司都在 Node.js 上投入了大量资金。从 Ryan Dahl 的雇主 Joyent 到 Mojito 框架的创造者(互联网巨头 Yahoo!),许多公司都围绕 Node.js 构建了产品、平台、框架和服务。这种企业承诺确保了稳定的未来。
如何获取 Node.js
由于 Node.js 的流行,它在任何操作系统上运行都非常容易。你可以访问nodejs.org/下载适合你操作系统的适当版本。
小贴士
尽管 Node.js 可以在任何操作系统上运行,但由于它来自*nix 背景,许多模块可能只能在 Linux 或其他 Unix 系统上运行;因此,如果你手头有这样一个系统,最好使用它。
如果你使用 Linux,在大多数情况下,你应该能够使用你发行版的包管理器安装 Node.js。由于这些信息不断变化,我只会指出位置。你可以在以下位置找到使用包管理器安装 Node.js 的说明:
github.com/joyent/node/wiki/Installing-Node.js-via-package-manager
如果你使用 Mac OS 或 Windows,你应该知道 Node.js 现在为这些平台提供了安装程序,这是推荐的安装方法。你也可以使用源代码进行安装。为了避免在这里重复该过程,这个过程也可能再次发生变化,我建议你遵循 Node.js 维基百科上的官方安装说明,GitHub(github.com/joyent/node/wiki/Installation)。
Node.js 包管理器(npm)
如果你使用 Node.js 网站上的安装程序安装了 Node.js,那么你将已经安装了 npm。
此外,如果你按照说明从源代码构建,你可能会已经安装了 npm。如果是这样的话,非常好!如果没有,请现在就安装。为此,我建议你遵循 npm 安装文档中提到的说明(github.com/isaacs/npm/)。
你可以通过输入以下命令来检查你是否已安装 npm:
$ npm -v
这应该会显示已安装的 npm 版本。
对于那些还在疑惑 npm 是什么以及为什么你需要为 Node.js 安装包管理器的人来说,npm 正是其名字所描述的那样;它为 Node.js 提供了一个基础设施,用于分发和管理包。正如我之前所说的,Node.js 非常模块化。Node.js 应用程序使用许多模块和第三方包来构建,因为 npm 为我们提供了添加和管理第三方依赖项的简单方法。我们稍后会看到它的更多用途。
Hello World with Node.js
在这里,必做的 Hello World 示例使用了 Node.js。在一个名为helloworld.js的文件中写下以下行,并保存:
console.log("Hello World");
现在要运行它,请执行以下命令:
node helloworld.js
这应该在控制台打印出Hello World。所有 JavaScript 开发者都会立即认出,这些是我们开发 Web 应用程序时在控制台上打印任何内容的步骤。
发生的事情是,Node.js 在 JavaScript 虚拟机中加载 JavaScript 文件,为其执行提供环境,虚拟机解释脚本。当它遇到console.log时,它会检查环境中的控制台,在这种情况下是STDOUT,并将Hello World写入其中。
但我们在这里是为了开发 Web 应用程序,对吧?所以让我们向 Web 问好!
Hello Web
让我们创建一个非常简单的 Web 应用程序,它会对用户说“你好”。在文件中写下以下代码,并将其命名为helloweb.js:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<html>");
response.write("<head><title>Node.js</title></head>");
response.write("<body>Hello Web</body>");
response.write("</html>");
response.end();
}).listen(9999);
要运行前面的代码片段,请在 Node.js 中执行helloweb.js:
node helloweb.js
然后在浏览器中打开http://localhost:9999/。你应该会看到一个写着Hello Web的页面。这里有很多事情在进行中!所以让我们一步步地看代码,了解发生了什么。
代码的第一行向我们介绍了 Node.js 的一个基本构建块,即模块系统。Node.js 有一个基于 CommonJS 的非常简单的模块系统。那些熟悉使用 RequireJS 进行前端开发并使用异步模块定义(AMD)的人会立即联想到这一点。Node.js 中的所有功能都作为模块构建,你需要使用require在代码中导入它。Node.js 有几个以二进制形式编译的模块,称为核心模块,HTTP 就是其中之一。我们也可以使用require创建和包含我们自己的自定义或第三方模块。在文件模块的情况下,文件和模块之间存在一对一的映射;因此,我们将在自己的文件中编写每个模块。我们稍后会看到更多关于编写我们自己的模块的内容。
var http = require("http");
通过这个语句,Node.js 将加载核心 HTTP 模块,并且它将在名为http的变量中可用。下一个任务是使用 HTTP 模块创建一个服务器。这是通过模块中的createServer方法完成的。createServer方法接受requestListener。
http.createServer([requestListener]);
requestListener 是一个处理传入请求的函数。在我们的例子中,这个函数是直接传递的。就像浏览器中的 JavaScript 一样,Node.js 也运行在一个单独的进程和线程中。这与传统的应用服务器不同,传统的应用服务器会为新的请求创建一个新的线程或进程。因此,为了扩展和处理多个请求,Node.js 使用异步事件处理。所以每个传入的请求都会触发一个事件,然后由事件处理器异步处理。这就是前面章节中解释的事件循环机制。
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<html>");
response.write("<head><title>Node.js JS</title></head>");
response.write("<body>Hello Web</body>");
response.write("</html>");
response.end();
});
小贴士
下载示例代码
您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
createServer 的工作方式与 JavaScript 中的任何事件处理器类似。在这个例子中,事件是接收一个请求来服务。正如我们所看到的,requestListener 接受两个参数,request 和 response。request 对象是 http.ServerRequest 的一个实例,它将包含有关请求的所有信息,例如 URL、方法、头和数据。
response 对象是 ServerResponse 的一个实例,它实现了 WritableStream 接口。它暴露了各种方法来将响应写入客户端;目前我们最感兴趣的方法是 writeHead、write 和 end。让我们首先看看 writeHead:
response.writeHead(statusCode, [reasonPhrase], [headers]);
在这里,statusCode 是 HTTP 响应码,reasonPhrase 是可选的供人类阅读的响应短语,headers 是包含要发送在响应中的头的对象。这个函数应该只调用一次,在调用 response.end 之前。如果我们在这个之前调用 response.write 或 response.end,隐式/可变的头将被计算,并且下面的函数将自动被调用:
response.writeHead(200, {"Content-Type": "text/html"});
在这次调用中,我们将状态码设置为 200,即 HTTP OK,并且我们只设置了 Content-Type 头为 text/html。这里的方法是 response.write,它用于将响应内容写入客户端。对这个方法的调用如下:
response.write(chunk, [encoding]);
在这次调用中,chunk 是要写入的内容,encoding 是要使用的编码。如果 chunk 是一个字符串并且没有指定编码,默认将使用 UTF-8。
response.write("<html>");
response.write("<head><title>Node.js JS</title></head>");
response.write("<body>Hello Web</body>");
response.write("</html>");
在上述代码中,第一次调用 write 时,Node.js 会发送响应头和响应体的一个片段。但 write 方法可以被多次调用。Node.js 会假设我们正在流式传输数据,并且会在每次调用时发送数据块。最后对响应的调用是用来告诉 Node.js 我们已经完成。这正是 response.end 所做的。
response.end([data], [encoding]);
response.end 向服务器发出信号,表示所有响应头和主体内容都已发送,服务器应认为这条消息已完整。我们必须为每条消息调用此方法。
response.end();
在我们的情况下,我们调用 response.end 而不带任何可选参数。如果我们确实传递了参数,那么它等同于调用 response.write 并传递参数,然后调用 response.end。我更喜欢将它们分开,并且明确地这样做。
最后,我们需要告诉 HTTP 服务器它应该监听哪个端口。在这种情况下,我们告诉它监听端口 9999。
listen(9999);
请求路由
几乎任何 Web 应用程序都服务于多个资源。因此,现在我们知道如何使用 HTTP 服务器提供内容;但我们如何处理多个资源呢?路由就是关键。我们需要理解传入的请求并将其映射到适当的请求处理器。这比之前的例子要复杂一些,所以我们将逐步构建它,每一步都进行改进。
为了演示请求的路由,让我们构建一个简单的应用程序,该应用程序在 /start 和 /finish 路径上提供两个资源,分别显示 Hello 和 Goodbye。为了简化代码,我们将提供纯文本。因此,在所有其他事情之前,让我们先看看代码:
var http = require("http");
var url = require("url");
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
if(pathname === "/start"){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
}else if(pathname === "/finish"){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
}else{
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("404 Not Found");
}
}
http.createServer(onRequest).listen(9999);
console.log("Server has started.");
将之前的代码片段保存到名为 routing.js 的文件中,并按以下方式执行:
node routing.js
现在,当我们访问 http://localhost:9999/start 时,我们将在浏览器中看到 Hello。同样,当我们访问 http://localhost:9999/finish 时,我们将看到一个显示 Goodbye 的消息。如果我们尝试访问任何其他路径,我们将得到一个 HTTP 404 或未找到错误。现在让我们尝试理解在这个例子中引入的新内容。
为了路由一个请求,我们首先需要解析 URL;为此,我们将引入另一个内置模块,称为 url。当使用 url 模块解析 URL 字符串时,它返回一个 URL 实例。在这种情况下,我们感兴趣的是路径名。
var pathname = url.parse(request.url).pathname;
在上一行代码中,我们正在传递请求中的 url 字符串,并使用 url 模块对其进行解析,以获取 pathname。下一步是根据访问的路径发送适当的响应:
if(pathname === "/start"){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
}else if(pathname === "/finish"){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
}
在这里,我们正在将路径名与我们期望处理的路径名进行比较,并相应地发送适当的响应。那么,对于我们没有处理的请求会发生什么呢?这就是 if-else-if 阶梯的最后一部分所做的事情。它发送一个 HTTP 404 错误。
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("404 Not Found");
现在,让我们考虑扩展这个应用程序。要处理更多的路径,我们必须添加更多的 if-else 条件。但这看起来并不干净,难以阅读,并且在执行上非常低效。想想 if-else 层级结构的最后一步处理的路由;这个过程仍然必须通过整个层级结构,检查每个条件。此外,向其中添加新路由将需要我们遍历并编辑这个 if-else 层级结构,这至少会让人困惑,也可能导致错误、打字错误,以及无意中修改现有路由的高度可能性。因此,让我们通过将处理程序放入按路径映射的对象中,并也提供一个 API 来扩展它,使其变得更加干净。因此,让我们将我们的代码修改如下:
var http = require("http");
var url = require("url");
var route = {
routes : {},
for: function(path, handler){
this.routes[path] = handler;
}
};
route.for("/start", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
});
route.for("/finish", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
});
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
if(typeof route.routes[pathname] ==='function'){
route.routespathname;
}else{
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("404 Not Found");
}
}
http.createServer(onRequest).listen(9999);
console.log("Server has started.");
要运行此代码片段,请使用以下命令使用 Node.js 执行文件:
node resources.js
应用程序的功能将与上一个示例的结果相同。当我们访问/start或/finish时,它将分别为前者响应Hello,为后者响应Goodbye。尝试访问任何其他路径时,我们将收到 HTTP 404 消息。
我们在这里所做的改变是,我们放弃了 if-else-if 的层级结构,转而采用了一种干净且高效的设计方法。在这种方法中,我们不需要与现有的路由打转,可以通过从任何模块中调用route.for方法来添加新的路由。路由有一个指向handler函数的路径映射,并且还有一个on方法来添加新的路由。
var route = {
routes : {},
for: function(path, handler){
this.routes[path] = handler;
}
}
route.on("/start", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
});
route.on("/finish", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
});
在这里,我们为路径/start和/finish添加了两个新的处理程序。处理程序的签名与主请求处理程序类似。我们期望处理程序能够获取请求和响应,这样处理程序就有处理请求和发送响应所需的一切。
if(typeof(route.routes[pathname])==='function')
在 if 条件中,我们检查路径名对应的路由是否存在,以及它是否是一个函数。如果我们找到了请求路径的处理程序,我们将执行handler函数,并将请求和响应传递给它。
route.routespathname;
如果未找到,我们将以 HTTP 404 错误响应。现在,要添加新的路径,我们可以通过调用带有路径及其处理程序的route.on方法来注册它。
route.on("/newpath", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("new response");
response.end();
});
HTTP 方法
HTTP 不仅关乎路径,我们还要考虑 HTTP 方法。在本节中,我们将增强我们的应用程序以处理不同的 HTTP 方法:GET、POST、PUT和DEL TE。
作为这一步骤的第一步,我们将添加为不同的方法添加不同处理程序的能力。我们将在 resources.js 中的映射中添加方法,这是一个小的改动。这在上面的代码片段中显示:
var http = require("http");
var url = require("url");
var route = {
routes : {},
for: function(method, path, handler){
this.routes[method + path] = handler;
}
}
route.for("GET", "/start", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
});
route.for("GET", "/finish", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
});
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + request.method + pathname +" received.");
if(typeof(route.routes[request.method +pathname])==='function'){
route.routesrequest.method + pathname;
}else{
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("404 Not Found");
}
}
http.createServer(onRequest).listen(9999);
console.log("Server has started.");
保存文件并使用 Node.js 执行。功能仍然保持不变,但我们将能够使用不同的处理程序处理不同的方法。让我们创建一个新的处理程序来在POST上回显传入的数据。
route.on("POST", "/echo", function(request, response){
var incoming = "";
request.on('data', function(chunk) {
incoming += chunk.toString();
});
request.on('end', function(){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(incoming);
response.end();
});
});
在这里,我们为/echo路径上的POST请求添加了一个新的处理器。我们再次看到了 Node.js 事件驱动方法的使用,这次是在处理通过POST传入的数据。由于request是一个事件发射器,我们为每个任务(处理传入的数据块和完成请求处理)都附加了一个事件处理器。
request.on('data', function(chunk) {
incoming += chunk.toString();
});
在之前的代码中,我们在请求上添加了一个监听器来处理传入的数据块。在这种情况下,我们只是累积传入的数据。
request.on('end', function(){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(incoming);
response.end();
});
在end事件处理器中,一旦接收到所有通过POST发送的数据,它将被调用。这是我们完成接收所有数据的时刻。为了构建一个回显服务,我们将发送回所有累积的数据。现在,我们将创建一个表单来向此处理器提交请求。
route.on("GET", "/echo", function(request, response){
var body = '<html>' +
'<head><title>Node.js Echo</title></head>' +
'<body>' +
'<form method="POST">' +
'<input type="text" name="msg"/>' +
'<input type="submit" value="echo"/>' +
'</form>' +
'</body></html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
});
我们将在相同的路径(/echo)上添加一个事件处理器,但这次是为了处理GET请求。在处理器中,我们将返回一个包含表单的 HTML 页面,该表单将提交到相同的路径。
将这两个处理器添加到我们的route-handlers.js中,并使用 Node.js 执行它。要打开我们的表单,请访问http://localhost:9999/echo;然后,要触发我们的处理器,请在表单的文本框中输入一条消息,并点击回显按钮。这将提交表单的内容,浏览器中我们将看到msg=<your text>。
创建我们自己的模块
模块是 Node.js 应用程序的基本构建块。随着我们所有的更改,我们的文件正变得有些杂乱;此外,我们将基础设施(服务器和路由器)与应用程序逻辑(处理器)放在了同一个地方。如前所述,Node.js 建立在 CommonJS 模块系统之上。在 Node.js 中,模块和文件有一对一的关系。让我们将服务器和路由器移动到它们自己的模块中。将以下内容保存到一个名为server.js的文件中:
var http = require("http");
var url = require("url");
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + request.method + pathname +" received.");
if(typeof(routes[request.method + pathname])==='function'){
routesrequest.method + pathname;
}
else{
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("404 Not Found");
}
}
var routes = {};
exports.forRoute = function(method, path, handler){
routes[method + path] = handler;
};
exports.start = function(){
http.createServer(onRequest).listen(9999);
console.log("Server has started.");
};
代码的逻辑方面大部分保持不变,但我们做了一些非常微妙的结构变化。第一个变化是我们将路由从route对象中移除。在文件中声明的任何变量都在模块内部可用,且不可从外部访问。
if(typeof(routes[request.method + pathname])==='function'){
routesrequest.method + pathname;
}
由于route对象已不存在,我们现在可以直接访问模块内的路由,而不是通过route对象。
另一个更明显的改变是exports。由于模块内部没有任何内容可供外部访问,我们必须将想要公开的方法/对象添加到隐式的exports对象中。理想情况下,你应该只公开与模块最终用户相关的那些方法。
exports.forRoute = function(method, path, handler){
routes[method + path] = handler;
};
exports.start = function(){
http.createServer(onRequest).listen(9999);
console.log("Server has started.");
}
我们从模块中公开了两个方法:forRoute方法(原本是route对象中的on方法),以及包装启动 HTTP 服务器的start方法。我们还把应用程序逻辑移动到名为app.js的单独模块中,如下代码片段所示:
var server = require("./server.js");
server.forRoute("GET", "/start", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello");
response.end();
});
server.forRoute("GET", "/finish", function(request, response){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Goodbye");
response.end();
});
server.forRoute("POST", "/echo", function(request, response){
var incoming = "";
request.on('data', function(chunk) {
incoming += chunk.toString();
});
request.on('end', function(){
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(incoming);
response.end();
});
});
server.forRoute("GET", "/echo", function(request, response){
var body = '<html>' +
'<head><title>Node.js Echo</title></head>' +
'<body>' +
'<form method="POST">' +
'<input type="text" name="msg"/>' +
'<input type="submit" value="echo"/>' +
'</form>' +
'</body></html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
});
server.start();
再次强调,逻辑方面保持不变;变化仅在于结构。
var server = require("./server.js");
上一行,也是前一个代码片段中的第一行,展示了我们的服务器模块是如何被加载的。这类似于加载核心模块如 HTTP 或 URL,但在这里我们加载了模块并传递了其文件名。由这个require方法创建的对象,即server对象,将有两个将被公开的方法:forRoute和start。
接下来,我们将所有对route.on的调用替换为server.forRoute方法。最后,我们调用server.start方法来启动 HTTP 服务器。
server.start();
文件服务
我们可以看到,使用我们当前的基础设施编写 HTML 页面并不直观或容易。将 HTML 以字符串形式写入 JS 代码并不有趣。我们希望从 HTML 文件中服务 HTML 内容。我们将从需要从磁盘中的文件读取的app模块中的两个模块开始:
var path = require('path');
var fs = require('fs');
第一个,path,是我们用来处理路径的模块,而fs是用于与文件系统交互的模块。下一步是获取应用程序根路径。
var root = __dirname;
__dirname是由 Node.js 管理的变量,包含 Node.js 应用程序脚本的绝对路径。现在,我们添加一个将执行读取文件并发送到浏览器的大量工作的方法。将此方法添加到app.js中:
var serveStatic = function(response, file){
var fileToServe = path.join(root, file);
var stream = fs.createReadStream(fileToServe);
stream.on('data', function(chunk){
response.write(chunk);
});
stream.on('end', function(){
response.end();
});
}
我们创建的serveStatic方法接受两个参数,即 HTTP 响应对象和要服务的文件路径。
var fileToServe = path.join(root, file);
我们将文件路径追加到我们的根路径,以构建要服务的文件的绝对路径。这里我们隐式地假设所有要服务的文件都是相对于 Node.js 应用程序根目录的;这将防止错误地由应用程序之外的文件提供服务。Node.js 以流的形式处理 I/O。我们可以看到这类似于它处理传入的POST数据的方式。
var stream = fs.createReadStream(fileToServe);
我们使用fs模块中的createReadStream创建一个从文件读取的流。这个流再次展示了 Node.js 的非阻塞、异步和事件驱动方法。
stream.on('data', function(chunk){
response.write(chunk);
});
流作为一个事件发射器,当流上有新数据时触发'data'事件。这允许应用程序继续进行其他处理活动,而无需等待数据被读取。我们在这里所做的是,将每次read接收到的数据写入response流。
stream.on('end', function(){
response.end();
});
一旦读取了文件中的所有数据,并且流接收到 EOF(文件结束标志),它将触发'end'事件。我们也会在我们的响应上调用end。最后,我们将修改GET处理器"/echo"以从名为echo.html的文件中提供服务,并将要服务的 HTML 内容写入该文件。
server.forRoute("GET", "/echo", function(request, response){
serveStatic(response, "echo.html");
});
我们已经移除了所有构建response字符串的代码,用对serveStatic的调用替换了它,以服务echo.html。
<html>
<head>
<title>Node.js Echo</title>
</head>
<body>
<form method="POST">
<input type="text" name="msg"/>
<input type="submit" value="echo"/>
</form>
</body>
</html>
我们之前作为字符串构建的内容现在被写入这个文件。一旦我们做出这些更改并使用 Node.js 运行app.js,你应该能够在http://localhost:9999/echo上看到表单并保留其原始功能。
熟悉类 Unix 操作系统的用户会发现,我们刚刚实现的功能,即从一个流读取并写入另一个流,也可以通过使用管道(|)来实现。Node.js 提供了一个高级函数来完成完全相同的功能,这并不令人惊讶。
使用 Node 中stream提供的pipe方法,我们可以按如下方式修改serveStatic方法:
var serveStatic = function(response, file){
var fileToServe = path.join(root, file);
var stream = fs.createReadStream(fileToServe);
stream.pipe(response);
}
在这里,我们使用stream.pipe(response)替换了数据和结束事件处理器。
第三方模块和 Express JS
现在我们已经构建了自己的路由器并了解了 Node.js 的基础知识,是时候介绍 Node.js 最广泛使用的框架之一——Express (expressjs.com)了。Node.js 提供了构建 Web 应用程序的基础组件,但处理的东西太多。这就是 Web 框架的作用所在。有相当多的 Web 框架为 Node 提供了构建应用程序的高级抽象。您可以在以下列表中看到其中大部分:
github.com/joyent/node/wiki/Modules#wiki-web-frameworks-full
Express 是一个基于 Connect 中间件的 Node Web 应用程序框架,它提供了许多辅助工具和结构化方面来构建我们的 Web 应用程序。
要开始使用 Express 框架,我们需要使用 npm,Node.js 包管理器,来安装它。
sudo npm install -g express
之前的命令将 Express 作为一个全局(-g)模块安装,并使express作为一个命令可用。让我们创建一个 Express 应用:
express hello-express
这将创建一个名为hello-express的文件夹,其中包含一些文件和文件夹。让我们看看并理解这些文件。
首先需要理解的是package.json文件。这是定义 Node.js 应用程序包的文件。它包含应用程序元数据,如名称、描述和版本。更重要的是,它列出了模块依赖项。依赖项列表被 npm 用于下载所需的模块。
{
"name": "application-name",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.1.0",
"jade": "*"
}
}
在你的package.json中最重要的东西是name和version字段。这些字段是必需的,并且它们共同构成了包特定版本的唯一标识符。首先,让我们将我们的包名称更改为hello-express。version字段由以下内容组成(按相同顺序):
-
一个数字(主要版本)
-
一个点
-
一个数字(次要版本)
-
一个点
-
一个数字(补丁版本)
-
可选:一个连字符,后跟一个数字(构建)
-
可选:一大堆几乎任何非空白字符的集合(标签)
如果我们将 private 设置为 true,npm 将不会将其发布到仓库。这确保了您不会不小心将代码发布到公共仓库。
scripts 对象将命令映射到应用程序生命周期中它们应该运行的那些点。在这个包中,我们告诉 Node.js,当使用 npm 启动应用程序时,它应该运行 node app 命令。有一些预定义的生命周期命令,如 start、stop、restart 和 test,可以使用 npm <command> 运行,例如 npm start。
您也可以使用 run-script 运行任意命令。为此,您将命令添加到 scripts 对象中,然后运行它作为 npm run-script <command>。
最后——包中最有趣的部分,引入了魔法——dependencies。此对象是依赖包名称和版本的映射,将由 npm 用于拉取所有必需的依赖项。
在我们的包中,express 已经定义了对 Express 和 Jade 的依赖。要引入这些依赖项,请运行以下命令:
npm install
输出将列出它下载的所有依赖项。
jade@0.27.2 node_modules/jade
├── commander@0.6.1
└── mkdirp@0.3.0
express@3.0.0rc2 node_modules/express
├── methods@0.0.1
├── fresh@0.1.0
├── range-parser@0.0.4
├── cookie@0.0.4
├── crc@0.2.0
├── commander@0.6.1
├── debug@0.7.0
├── mkdirp@0.3.3
├── send@0.0.3 (mime@1.2.6)
└── connect@2.4.2 (pause@0.0.1, bytes@0.1.0, qs@0.4.2, formidable@1.0.11)
依赖项将被放置在一个名为 node_modules 的文件夹中。
接下来,我们将查看应用程序文件 app.js:
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path');
var app = express();
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});
app.configure('development', function(){
app.use(express.errorHandler());
});
app.get('/', routes.index);
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
});
在前几行中,Node.js 加载了我们工作所需的模块。我们已经熟悉 http 和 path。express 模块引入了 Express 框架。我们还在加载一个模块 ./routes,它将加载本地 routes 文件夹中定义的模块,该文件夹由 ./routes/index.js 定义。以下代码片段关注前一个代码片段的前几行:
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path');
在下一行,它将 Express 框架实例化为一个应用:
var app = express();
然后是应用程序配置:
app.configure(function(){
});
在前面的几行中,我们正在定义一个将配置应用的函数。app.configure 的签名是 app.configure([env], callback),其中 env 是运行时环境变量 或 生产环境变量,如由 process.env.NODE_ENV 定义。当我们不指定 env 时,它将为所有环境设置。
以下设置已提供,以改变 Express 的行为:
-
env: 环境模式,默认为process.env.NODE_ENV或 "development" -
trust proxy: 启用反向代理支持,默认禁用 -
jsonp callback: 启用 JSONP 回调支持,默认启用 -
jsonp callback name: 改变默认的回调名称?callback= -
json replacer: JSON 替换回调,默认为null -
json spaces: 用于格式化的 JSON 响应空格;开发中默认为2,生产中为0 -
case sensitive routing: 启用大小写敏感的路由,默认禁用,将/Foo和/foo视为相同 -
strict routing: 启用严格路由,默认情况下/foo和/foo/由路由器视为相同 -
view cache: 启用视图模板编译缓存,默认在生产环境中启用 -
view engine: 当省略时使用的默认引擎扩展 -
views: 视图目录路径
以下代码片段演示了如何将设置分配给应用程序:
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
这里应用程序使用的设置是 port、views 和 view engine,指定应用程序应在端口 3000 上运行,视图将放置在 views 文件夹中,使用的引擎是 Jade。我们将在后面看到更多关于视图的内容。也可以指定某些功能,如下面的代码片段所示:
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
由于 Express 基于 Connect 中间件构建,它带来了许多来自 Connect 的现有功能。Connect 的 use 方法配置应用程序以利用给定的中间件处理程序来处理给定的路由,其中路由默认为 /。你可以在 www.senchalabs.org/connect/ 看到 Connect 提供的中间件列表。
让我们遍历这里使用的中间件组件。Favicon 中间件将提供应用程序的 favicon。Logger 中间件以自定义格式记录请求。
Body 解析器解析支持不同格式的请求体;这包括 application/json、application/x-www-form-urlencoded 和 multipart/form-data。
方法覆盖启用模拟 HTTP 方法支持。这意味着如果我们想刺激对应用程序的 DELETE 和 PUT 方法调用,我们可以通过在请求中添加 _method 参数来实现。
app.router 提供了 Connect 的 router 模块的增强版本。基本上,这是当我们使用 app.get 等路由方法时确定要做什么的组件。最后一个中间件,静态文件服务器,提供并配置了它,从 public 目录提供文件。
对于开发环境,以下两行代码显示了如何设置错误处理中间件,以便在响应中提供堆栈跟踪和错误消息。
app.configure('development', function(){
app.use(express.errorHandler());
});
下一行配置路由器将 / 路由处理为由 routes 模块中的 index 方法处理:
app.get('/', routes.index);
最后,我们启动配置为使用我们刚刚配置的应用实例的 HTTP 服务器。
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
});
在应用程序配置中,我们看到了一些文件夹开始发挥作用,即 routes、views 和 public。
routes 是我们将编写所有处理程序的模块。在 / 的情况下,我们将其映射到使用 index 方法从 routes.index 方法提供服务。如果你打开 routes/index.js,你会看到 index 是从这个模块公开的方法。
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
这个函数签名与我们编写的处理程序类似。它是一个接受请求和响应作为参数的函数。在这里,我们使用 Express 方法 response.render,它将使用第二个参数作为模型或数据来渲染提到的视图。
视图位于 views 文件夹中,并使用 Jade (jade-lang.com/) 视图引擎。Jade 是一个高性能的模板引擎,深受 Haml (haml.info/) 的影响,并使用 JavaScript 为 Node.js 实现。像许多现代 HTML 生成引擎一样,Jade 试图使 UI 代码更简单、更干净、更简单,去除了内联代码和 HTML 标签的噪音。
现在,我们将看到在 Express 应用程序中定义的视图。这里有两大文件需要查看:layout.jade 和 index.jade。
如其名所示,layout.jade 是我们将由应用程序中的不同页面使用的布局模板。它可以用来放置应用程序页面骨架的公共代码,如下面的代码片段所示:
doctype 5
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
在 Jade 中,我们不需要 start 和 end 标签,因为它通过缩进的代码块来识别标签的开始和结束。因此,当我们渲染这个 Jade 文件时,它将生成以下 HTML 代码:
<!DOCTYPE html>
<html>
<head>
<title>{TITLE}</title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>{CONTENT}</body>
</html>
在前面的代码片段中,有两个东西尚未定义,{TITLE} 和 {CONTENT}。在 Jade 模板中,我们如下定义标题:
title= title
我们告诉 Jade 使用传递给 render 的数据中的标题作为 title。第二件事,{CONTENT},在 Jade 中定义为块。
block content
block content 是在布局模板中提供的插件点,它可以由任何扩展它的模板描述。
index.jade 继承自 layout.jade。在我们的索引处理程序中,我们使用数据 {title: 'Express'} 来渲染索引视图。请看以下代码片段:
extends layout
block content
h1= title
p Welcome to #{title}
在上一个文件中,我们定义了内容块包含 h1 和 p 标签。因此,根据给定的输入并且因为它扩展了布局,Jade 引擎将生成以下 HTML:
<!DOCTYPE html>
<html>
<head>
<title>Express</title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
</body>
</html>
在下一章中,我们将通过我们的聊天应用程序来工作,届时我们将看到更多 Jade 的功能和语法。
在生成的 HTML 代码中,我们可以看到 /stylesheets/style.css 正在被引用;此文件由我们在应用程序中配置的静态文件服务器提供。我们可以在公共文件夹中找到此文件和其他文件。
要运行此应用程序,我们将使用 npm。在控制台运行以下命令:
npm start
然后转到 http://localhost:3000/。
摘要
在本章中,我们介绍了 Node.js 和 Express 网络框架。如前所述,这绝对不是对 Node.js 或 Express 的完整介绍。要了解更多信息,请参考在线上可用的广泛文档或任何关于 Node.js 网络开发的书籍。
第三章。让我们聊天
从 2000 年初的 Yahoo! Chat 到今天流行的 Google Talk 或 Facebook Chat,聊天一直是互联网上最受欢迎的实时通信形式。在本章中,我们将使用我们在上一章中学到的 node 和 express 以及本章将要学习的 socket.io 库来构建一个聊天室。
创建应用程序
与我们在上一章中创建应用程序的方式类似,我们将通过在命令行中执行以下命令来创建一个新的 awesome-chat 应用程序:
$ express awesome-chat
$ cd awesome-chat
$ npm install
这将创建我们的应用程序并安装 express 应用程序依赖项。打开 package.json 文件,将其名称更改为 awesome-chat,如下所示:
{
"name": "awesome-chat",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.0.0rc2express": "3.x",
"jade": "*"
}
}
设计聊天室
让我们修改视图,使其看起来像聊天室。我们需要一个区域来显示消息,一个文本输入框供用户输入消息,以及一个发送消息的按钮。我们将添加一些美学元素,如页眉、横幅和页脚。完成之后,我们的聊天室用户界面应该看起来像以下截图所示:

令人惊叹的聊天 UI
让我们从编辑 layout.jade 开始,向其中添加页眉和页脚:
doctype 5
html
block head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
header#banner
h1 Awesome Chat
block content
footer Hope you enjoy your stay here
我们做的第一个更改是在 head 前添加 block 关键字。这使得 head 成为一个块,我们可以从中添加扩展页面的内容。
另一个变化是添加了新的页眉和页脚。请注意,我们正在使用 HTML5 中的 header 和 footer 标签。此代码还向我们介绍了一种新的 jade 语法。当我们写 header#banner 时,它将生成具有 banner 作为 id 值的页眉。生成的 HTML 代码如下:
<!DOCTYPE html>
<html>
<head>
<title>{TITLE}</title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<header id="banner">
<h1>Awesome Chat</h1>
</header>
{CONTENT}
<footer>
Hope you enjoy your stay here
</footer>
</body>
</html>
接下来,我们将编辑 index.jade 以添加消息区域、消息输入和 发送 按钮:
extends layout
block content
section#chatroom
div#messages
input#message(type='text', placeholder='Enter your message here')
input#send(type='button', value='Send')
让我们运行一下,看看我们的 awesome-chat 应用程序看起来怎么样。使用 npm 执行应用程序,并在浏览器中打开 http://localhost:3000/:
npm start
嘿,所有元素都在那里,但看起来不太对!没错;为了改善应用程序的外观和感觉,我们需要编辑样式表,它位于 public/stylesheets/style.css。
我们可以根据自己的喜好进行编辑。这里有一个对我来说效果很好的例子:
html {
height: 100%;
}
body {
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
margin: 0px;
padding: 0px;
height: -moz-calc(100% - 20px);
height: -webkit-calc(100% - 20px);
height: calc(100% - 20px);
}
section#chatroom {
height: -moz-calc(100% - 80px);
height: -webkit-calc(100% - 80px);
height: calc(100% - 80px);
background-color: #EFFFEC;
}
div#messages {
height: -moz-calc(100% - 35px);
height: -webkit-calc(100% - 35px);
height: calc(100% - 35px);
padding: 10px;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
box-sizing:border-box;
}
input#message {
width: -moz-calc(100% - 80px);
width: -webkit-calc(100% - 80px);
width: calc(100% - 80px);
}
input#send {
width: 74px;
}
header{
background-color:#4192C1;
text-align: right;
margin-top: 15px;
}
header h1{
padding: 5px;
padding-right: 15px;
color: #FFFFFF;
margin: 0px;
}
footer{
padding: 6px;
background-color:#4192C1;
color: #FFFFFF;
bottom: 0;
position: absolute;
width: 100%;
margin: 0px;
margin-bottom: 10px;
-moz-box-sizing:border-box;
-webkit-box-sizing:border-box;
box-sizing:border-box;
}
a {
color: #00B7FF;
}
保存此 CSS 并刷新页面后,聊天室看起来是这样的:

令人惊叹的聊天室
引入 jQuery
jQuery 在 JavaScript 库中几乎是无处不在的,我们也将它在我们的应用程序中使用。要将 jQuery 添加到我们的应用程序中,让我们从 www.jquery.com/ 下载最新版本并将其保存到 public/javascript/jquery.min.js。然后,我们在 layout.jade 中添加脚本以将 jQuery 拉入我们的应用程序页面:
script(type='text/javascript', src='/javascripts/jquery.min.js')
Socket.IO
自从 Web 应用程序的兴起以来,开发者们一直在寻找不同的方法来实现服务器和浏览器之间的全双工通信。无论是使用 Java、Flash、Comet 还是许多其他解决方案,所有这些目标都是相同的。但第一次,有一个规范可以通过使用 HTML5 WebSockets 来构建全双工通信系统。WebSocket是 HTML5 规范中的一项革命性的新通信功能,它定义了一个通过单个 socket 在 Web 上操作的全双工通信通道。
虽然 WebSocket RFC 已经发布,但它并不,也永远不会在仍在使用的旧浏览器上可用。Socket.io 是 WebSocket 的一个抽象层,具有 Flash、XHR、JSONP 和 HTMLFile 后备方案。Socket.io 提供了一个简单易用的服务器和客户端库,用于在 Web 服务器和浏览器客户端之间进行实时、流式更新。
Socket.io 是通过 npm 提供的 node 模块,我们将将其添加到我们的包依赖项中。socket.io 的当前版本是0.9.10。要将此添加到依赖项中,请将以下行添加到package.json中的依赖项对象:
"socket.io": "0.9.10"
使用 npm 安装它:
$ npm install
这将把 socket.io 放入node_modules文件夹中。现在让我们看看我们将如何使用它。
事件处理
由于 socket.io 框架同时包含服务器和客户端的组件,我们将使用这些组件在双方进行通信编码。在一侧 socket 上发出的事件将由另一侧相应的事件处理器处理。Socket.io 就是这样构建的,使得双方都可以发送消息或附加处理器来处理传入的消息。
让我们先了解消息的流动方式。重要的是要记住,“消息”在这里并不是聊天系统用户实际发送和接收的消息,而是客户端和服务器用于通信的消息。将有两种类型的消息,如下所示:
-
系统消息:这些消息将由我们的聊天系统发送到客户端,比如当用户连接、其他人连接或用户断开连接时。让我们用
serverMessage来标识它。 -
用户消息:这些消息将由客户端发送到服务器,实际上会在负载中携带用户的消息内容。我们可能想要区分我们发送的消息和其他用户发送的消息。所以让我们分别称它们为
myMessage和userMessage。
当用户第一次连接时,服务器将发送一条欢迎消息给用户,作为serverMessage消息。
当用户输入一条消息并按下发送按钮时,我们将从浏览器发送一条userMessage消息到服务器。
接收到用户消息后,服务器将向所有其他用户广播这条消息。它还会将相同的信息作为myMessage发送回最初发送消息的用户。
当从服务器接收到任何消息时,浏览器将在消息区域显示消息的内容。
服务器
现在我们将实现服务器,它将执行之前提到的中继消息的任务。在routes文件夹中创建一个名为sockets.js的文件,并将以下代码插入其中:
var io = require('socket.io');
exports.initialize = function(server) {
io = io.listen(server);
io.sockets.on("connection", function(socket){
socket.send(JSON.stringify(
{type:'serverMessage',
message: 'Welcome to the most interesting chat room on earth!'}));
socket.on('message', function(message){
message= JSON.parse(message);
if(message.type == "userMessage"){
socket.broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
}
});
});
};
在第一行代码(你现在应该已经熟悉了),我们导入socket.io模块;我们将通过io变量来识别这个模块。
由于 socket.io 与通信层一起工作,我们需要将其设置为监听 HTTP 服务器。HTTP 服务器只能从主应用程序模块访问,因此我们必须在我们模块之前将server传递给我们的模块,这样我们的模块才能执行任何操作。因此,我们从我们的模块中导出一个名为initialize的方法,该方法将设置 socket.io 服务器并绑定所有消息处理程序:
exports.initialize = function(server) {
//work to do
}
initialize方法将接受 HTTP server对象作为参数。这是 socket.io 所必需的:
io = io.listen(server);
在方法的第一行,我们将服务器传递给 socket.io 模块的listen方法。服务器是 node HTTP 服务器模块的一个实例;socket.io 将在该服务器上配置各种处理程序。这是设置 socket.io 所需的唯一模板代码。接下来,我们需要设置我们的 socket.io 消息处理程序。
我们的服务器将接收到的第一个事件是来自新客户端的新连接。这通过io.sockets对象的connection事件来识别,并通知我们的应用程序一个新客户端已经打开了一个新的连接,并且所有协议协商(对我们来说是透明的)已经完成,现在我们有一个可以与这个客户端通信的套接字:
io.sockets.on("connection", function(socket){
//Add other event handlers to the socket
});
connection事件处理程序将被触发,传递刚刚建立的套接字。套接字是一个事件发射器,可以根据它接收到的消息触发不同的事件,我们也将使用这个套接字与为它创建的客户端进行通信。有多个事件被公开,例如connection事件来处理服务器上的事件。让我们快速看一下这些事件:
-
io.sockets.on('connection', function(socket) {}):客户端的初始连接。socket参数应在后续与客户端的通信中使用。 -
socket.on('message', function(message, callback) {}):当通过socket.send发送的消息被接收时,message处理程序被触发。message参数是发送的消息,callback是一个可选的确认函数。 -
socket.on('anything', function(data) {}):anything事件可以是任何事件,除了保留事件。 -
socket.on('disconnect', function() {}):当套接字断开连接时,此事件被触发。
现在我们已经看到了如何处理套接字事件,让我们看看我们如何从服务器向客户端发送消息:
socket.send(JSON.stringify(
{type:'serverMessage',
message: 'Welcome to the most interesting chat room on earth!'}));
socket.send 方法将在套接字上发送消息,这将触发客户端上的 message 事件。发送的消息必须是一个字符串,所以我们将使用 JSON.stringify 将消息数据作为字符串发送。在这里,我们的消息有两个部分,一个类型和一个消息。
我们的任务的一部分已经完成,我们现在能够欢迎用户。下一个任务是处理当用户消息到来时的用户消息。为此,我们在套接字上设置了一个 message 事件处理器:
socket.on('message', function(message){
message= JSON.parse(message);
if(message.type == "userMessage"){
socket.broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
}
});
就像任何其他事件连接器一样,socket.on 将接受两个参数,即要处理的事件和相应的事件处理器。在这种情况下,与 io.sockets.on 事件不同,这个事件处理器将接收消息作为参数,而不是套接字。
由于消息是一个字符串,我们将解析消息的 JSON 字符串来创建一个 message 对象。如果这是一个用户发送的消息,这个消息将是 userMessage 类型,这就是我们要检查的。
现在,我们必须将这条消息发送给所有已连接的用户。为此,socket.io 为我们提供了一个 broadcast 对象。当我们使用 broadcast 对象发送消息时,它将被发送到所有已连接的客户端,除了创建此套接字的客户端。在这里发送消息的语法是相同的;区别在于它是在 broadcast 对象上调用的,在 socket.io 中被称为消息标志,而不是套接字本身。
此外,我们还想将相同的内容发送回发送此消息的客户端,但只需将类型更改为 myMessage。为此,我们直接在套接字上发送消息。
就这样。我们已经为服务器编写了代码;但现在我们必须实际初始化这个服务器。为此,修改 app.js 中的服务器创建,设置 server 变量,如下面的代码片段所示:
var server = http.createServer(app).listen(app.get('port'),
function(){
console.log("Express server listening on port " + app.get('port'));
});
既然我们已经修改了 HTTP 服务器,我们可以调用套接字模块的 initialize 方法,并将这个服务器作为参数传递给它。将以下行添加到 app.js 的末尾:
require('./routes/sockets.js').initialize(server);
客户端
既然我们已经看到了服务器的工作方式,让我们看看客户端会做什么。socket.io 的最好之处在于它为服务器和客户端提供了相同的 API。对于客户端的聊天逻辑,让我们在 public/javascripts 文件夹中创建一个名为 chat.js 的文件,并将以下代码添加到其中:
var socket = io.connect('/');
socket.on('message', function (data) {
data = JSON.parse(data);
$('#messages').append('<div class="'+data.type+'">' + data.message + '</div>');
});
$(function(){
$('#send').click(function(){
var data = {
message: $('#message').val(),
type:'userMessage'
};
socket.send(JSON.stringify(data));
$('#message').val('');
});
});
开始聊天的第一步是连接到服务器:
var socket = io.connect('/');
这将从加载页面的服务器发送一个连接请求。这还将协商实际的传输协议,并最终在服务器应用程序上触发 connection 事件。
以下代码片段连接了 message 事件的处理器:
socket.on('message', function (data) {
data = JSON.parse(data);
$('#messages').append('<div class="'+data.type+'">' + data.message + '</div>');
});
我们需要做的只是将收到的消息追加到messages区域。我们在这里添加了一个额外的细节,通过设置新追加的div标签的class属性与消息类型相同。我们稍后可以使用这个属性来为不同类型的消息提供不同的外观。
在客户端最后要做的就是发送用户的消息。这将在用户在消息框中写下他的/她的消息并点击发送按钮时完成。因此,让我们给发送按钮添加一个事件处理器。关于 UI 元素的事件处理器,重要的是它们应该在元素被添加到文档中后附加,也就是说,在它被创建并准备好之后。jQuery 提供了一个方便的方法来检测文档何时准备好,并将处理函数添加到执行。有两种方法可以做到这一点;一种如下所示:
$(document).ready(function(){
//Execute once the document is ready
});
同样的快捷方式如下所示:
$(function(){
//Execute once the document is ready
});
一旦文档准备好,我们就将事件处理器附加到发送按钮的click事件上:
$(function(){
$('#send').click(function(){
var data = {
message: $('#message').val(),
type:'userMessage'
};
socket.send(JSON.stringify(data));
$('#message').val('');
});
});
在点击发送按钮时,我们创建我们的data对象,将消息框的内容设置为message,并将type设置为userMessage。然后我们可以使用socket.send方法将此数据发送到服务器。正如您从前面的代码片段中可以看到,从客户端发送消息的语法与服务器相同,并且在这里消息也将以字符串的形式发送,我们使用JSON.stringify(data)创建这个字符串。
就像服务器上的connection事件和其他预定义事件一样,客户端也有一些预定义的事件。这些如下所示:
-
socket.on('connect', function () {}):当套接字成功连接时,会触发connect事件。 -
socket.on('connecting', function () {}):当套接字尝试与服务器连接时,会触发connecting事件。 -
socket.on('disconnect', function () {}):当套接字断开连接时,会触发disconnect事件。 -
socket.on('connect_failed', function () {}):当 socket.io 无法与服务器建立连接且没有更多传输可以回退时,会触发connect_failed事件。 -
socket.on('error', function () {}):当发生错误且无法由其他事件类型处理时,会触发error事件。 -
socket.on('message', function (message, callback) {}):当通过socket.send发送的消息被接收时,会触发message事件。message参数是发送的消息,callback是一个可选的确认函数。 -
socket.on('anything', function(data, callback) {}):anything事件可以是任何事件,除了保留的事件。data参数表示数据,callback可以用来发送回复。 -
socket.on('reconnect_failed', function () {}):当 socket.io 在连接断开后无法重新建立有效连接时,会触发reconnect_failed事件。 -
socket.on('reconnect', function () {}): 当 socket.io 成功重新连接到服务器时,会触发reconnect事件。 -
socket.on('reconnecting', function () {}): 当套接字尝试重新连接到服务器时,会触发reconnecting事件。
在客户端的最后任务是添加 socket.io 和聊天脚本到我们的聊天室页面。由于这些脚本不会在每一个页面上使用,我们不会将它们添加到layout.jade中,而是将它们添加到index.jade中。
记得我们对layout.jade所做的更改,将代码从head更改为block head?这将允许我们将index.jade中的内容追加到head标签:
block append head
script(type='text/javascript', src='/socket.io/socket.io.js')
script(type='text/javascript', src='/javascripts/chat.js')
在下一行代码中,我们使用 Jade 的功能将内容追加到从子元素继承的模板中的块。这是通过使用append关键字完成的。语法如下:
block append <blockname>
简短形式如下:
append <blockname>
以下两行代码通过将socket.io.js和chat.js添加到我们的页面中,添加了脚本标签。你可能想知道/socket.io/socket.io.js文件从何而来,因为我们既没有添加它,它也不存在于文件系统中。这是服务器上io.listen所做魔法的一部分。它会在服务器上创建一个处理程序来提供socket.io.js脚本文件。
我们已经准备好了。重新启动 node 服务器,并浏览到http://localhost:3000 /以打开聊天室。你将看到欢迎信息欢迎来到地球上最有趣的聊天室在消息区域显示。
要了解我们的聊天应用程序是如何工作的,请同时在两个不同的浏览器实例中打开它。现在你可以在一个浏览器中的消息框中输入你的消息,并点击发送。你将看到它在两个浏览器的消息区域显示。
恭喜!我们现在有一个聊天室了。如果你将其部署到服务器上,或者允许系统上的端口3000访问,你可以邀请你的朋友来聊天。
摘要
在本章中,我们学习了 socket.io,并探讨了 socket.io 提供的一些非常基本的概念和 API。我们还看到了如何在服务器和客户端上设置 socket.io,以及如何发送和接收消息。在这个过程中,我们还使用我们到目前为止所学的一切构建了一个聊天室应用程序。
在下一章中,我们将基于我们创建的应用程序添加其他功能,例如会话数据、多个聊天室、命名空间和身份验证,同时熟悉 socket.io 的相关功能。
第四章:让它更有趣!
在上一章中,我们创建了一个聊天室。在本章中,我们将通过给我们的用户提供名字、拥有多个聊天室以及集成 express 和 socket.io 会话来改进这个聊天室。
为用户命名
如果我们的用户没有名字,聊天就会变得困难。无法识别谁发送了消息。所以让我们提供一个方法,让我们的用户可以为自己设置昵称,这样他们的消息就可以通过他们的名字来识别。
我们已经使用 socket.io 的message事件来发送和接收消息。我们还看到了 socket.io 模块的预定义事件。在本节中,我们将了解更多关于这些事件的信息,并了解我们如何处理我们自己的事件。我们还将了解如何为会话保存一些信息。
让我们首先创建一个用户界面,当用户来到我们的聊天室时,可以接受他们的名字。为此,我们将通过向其中添加以下代码来修改index.jade文件:
//EXISTING LAYOUT
section#nameform.modal
div.backdrop
div.popup
div.pophead Please enter a nickname
div.popbody
input#nickname(type='text')
input#setname(type='button', value='Set Name')
我们在这里所做的就是在modal覆盖层中添加一个新的部分。这个部分有一个背景div标签,然后是一个实际的表单div标签。这个外观和感觉将在style.css文件中定义,所以让我们也更新一下。在修改样式表时,请参考以下代码块:
//EXISTING CSS
.modal{
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
height: -moz-calc(100% - 102px);
height: -webkit-calc(100% - 102px);
height: calc(100% - 102px);
left: 0;
position: absolute;
top: 62px;
width: 100%;
z-index: 1000;
}
.backdrop{
width: 100%;
height:100%;
background-color: #777777;
}
.popup {
position: absolute;
height: 100px;
width: 300px;
left: -moz-calc(50% - 150px);
left: -webkit-calc(50% - 150px);
left: calc(50% - 150px);
top: -moz-calc(50% - 50px);
top: -webkit-calc(50% - 50px);
top: calc(50% - 50px);
background: #FFFFFF;
}
.pophead {
background-color: #4192C1;
color: #FFFFFF;
font-weight: bold;
padding: 8px 3px;
vertical-align: middle;
}
.popbody {
padding: 10px 5px;
}
现在我们刷新 UI,它将看起来像这样:

用户名表单
接下来,我们想要做的是,当用户输入一个名字并点击设置名字按钮时,将名字发送到服务器,将其存储在那里,并在该用户发送的每条消息前加上名字前缀。首先,我们将更改文档就绪处理程序,为设置名字按钮附加一个事件处理器。为此,编辑public/javascripts/chat.js:
$(function(){
$('#setname').click(function(){
socket.emit("set_name", {name: $('#nickname').val()});
});
});
在之前的代码中,我们看到了一个新的 socket.io API 和概念,即socket.emit。这是用来触发自定义事件的。对emit的调用如下:
socket.emit(<event_name>, <event_data>);
我们触发一个set_name事件,并将用户在用户名框中输入的值传递过去。我们还从socket.emit声明中移除了发送消息的事件处理器。我们稍后会回到这个话题。
在一个 socket 上发出的事件(服务器端)将在 socket 的另一侧(客户端)被处理。在我们的案例中,即在之前的代码片段中,我们在客户端触发set_name事件,所以我们将它在服务器端处理。为此,我们将编辑routes/sockets.js如下:
var io = require('socket.io');
exports.initialize = function(server) {
io = io.listen(server);
io.sockets.on("connection", function(socket){
socket.on('message', function(message){
message= JSON.parse(message);
if(message.type == "userMessage"){
socket.get('nickname', function(err, nickname){
message.username=nickname;
socket.broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
});
}
});
socket.on("set_name", function(data){
socket.set('nickname', data.name, function(){
socket.emit('name_set', data);
socket.send(JSON.stringify({type:'serverMessage',
message: 'Welcome to the most interesting chat room on earth!'}));
});
});
});
}
在保持简单易用的实践中,socket.io 使用了相同的socket.on API,这是我们之前用来处理connection或message事件的,来处理自定义事件。传递给处理函数的数据将包含我们在触发事件时发送的数据。
这带我们来到了 socket.io 的一个新特性,即向会话中的 socket 附加额外的信息。这是通过使用socket.set函数实现的。对该函数的调用如下:
socket.set(<name>, <value>, <optional_callback>);
在上一行代码中,<name> 是我们想要设置的键的名称,而 <value> 是我们想要设置的值。对 set 的调用是异步的;它不会在值被设置之前阻塞。为了执行一个需要确保值已经被设置的动作,我们可以向 set 方法传递一个回调函数。在之前的代码中,我们传递了一个 callback 函数,该函数将触发另一个 name_set 自定义事件,并且还会发送欢迎信息。就像 set_name 事件一样,name_set 事件将在套接字的另一端被处理,在这种情况下是客户端。
这太棒了。现在名字已经设置,让我们通过在每条消息中显示它来真正地使用它,这样我们聊天室中的人就知道是谁发送了消息。
要从套接字获取设置的值,socket.io 提供了一个 get 方法。我们将使用这个 get 方法从套接字获取用户名并将其附加到之前的消息上。
让我们重新整理 public/javscripts/chat.js 以处理 name_set 事件,然后开始实际的通信:
var socket = io.connect('/');
socket.on('name_set', function(data){
$('#nameform').hide();
$('#messages').append('<div class="systemMessage">' +
'Hello '+data.name+'</div>');
$('#send').click(function(){
var data = {
message: $('#message').val(),
type:'userMessage'
};
socket.send(JSON.stringify(data));
$('#message').val('');
});
socket.on('message', function (data) {
data = JSON.parse(data);
if(data.username){
$('#messages').append('<div class="'+data.type+
'"><span class="name">' +
data.username + ":</span> " +
data.message + '</div>');
}else{
$('#messages').append('<div class="'+data.type+'">' +
data.message + '</div>');
}
});
});
$(function(){
$('#setname').click(function(){
socket.emit("set_name", {name: $('#nickname').val()});
});
});
在之前的代码片段中,我们添加了两行新代码来隐藏覆盖层并将问候语附加到 messages 区域。除此之外,我们还把处理发送和接收消息的代码移动到这个处理器中,这样它就只在用户设置了名字之后设置,避免了人们使用 Firebug 或其他类似工具仅隐藏覆盖层。在消息接收处理器中还有一个最后的更改;我们需要检查传入数据中是否存在用户名,如果存在,则将其作为前缀附加到显示的消息中。
要看到代码的实际效果,让我们重新启动我们的 node 服务器并刷新浏览器。一旦你输入名字,它就会弹出聊天室并显示你刚刚输入的名字以及欢迎信息:

用名字打招呼
在另一个浏览器窗口中打开我们的聊天室,这次以 Friend 的身份登录。在新消息框中输入一条消息并点击 发送。消息会出现在两个浏览器中的消息区域。从你打开的第一个聊天室尝试:

命名聊天
更多关于事件
在上一节中,我们看到了如何通过套接字使用自定义事件。有趣的是,就像你的消息一样,事件也可以被广播。让我们看看我们如何使用事件广播来宣布聊天室中参与者的加入。
对于这个,我们首先要做的事情是从服务器开始触发一个新的 user_entered 事件,一旦用户加入聊天,数据中就包含用户的名字。让我们将我们的 routes/socket.js 文件更改一下以实现这一点。我们将在用户名设置后添加代码来广播 user_entered 事件。
socket.on("set_name", function(data){
socket.set('nickname', data.name, function(){
socket.emit('name_set', data);
socket.send(JSON.stringify({type:'serverMessage',
message: 'Welcome to the most interesting" +
"chat room on earth!'}));
socket.broadcast.emit('user_entered', data);
});
});
要向连接到这个套接字的所有客户端发送广播,我们使用 emit 方法,但是在 socket.broadcast 而不是在 socket 本身上。该方法的签名是相同的。
现在,user_entered事件将被发送给所有已连接的客户端,因此我们需要在客户端的chat.js文件中添加一个事件处理器。
socket.on('name_set', function(data){
// EXISTING CODE
socket.on("user_entered", function(user){
$('#messages').append('<div class="systemMessage">' +
user.name + ' has joined the room.' + '</div>');
});
});
在这里,我们为user_entered事件添加了一个事件处理器,并向用户显示消息。让我们再次启动我们的服务器并登录到我们的聊天室:

第一个用户的聊天室
现在打开另一个浏览器窗口,并使用不同的名字登录:

第二个用户的聊天室
如您将注意到的,在第一个用户的窗口中,我们将看到Friend001和Friend002的进入消息,以及在第二个用户(Friend001)的窗口中的Friend002。
与命名空间一起工作
在本节中,我们不会向我们的聊天室添加任何新的功能,而是仅仅使用 socket.io 的一个特性来使我们的应用程序设计更好,代码更容易维护。
我们在客户端和服务器之间发送不同的消息,并通过type来区分它们。如果我们能在不同的消息通道上发送不同的消息会更好吗?我们当前的方法也并不完美,可能会在应用程序或模块是更大系统的一部分时引起冲突。但是,也存在一些问题,打开多个连接的成本是什么?这将对性能产生什么影响?
这就是命名空间发挥作用的地方。命名空间提供了一种方式来扩展 socket.io 连接,这样我们就可以为不同类型的消息获得不同的通道,而不会给系统及其性能带来很大的开销。让我们看看我们如何在我们的聊天系统中使用命名空间。
在我们的聊天应用程序中,我们发送了两种不同类型的消息或事件。这些是基础设施性的,例如设置名字和欢迎消息,以及用户之间的通信。
因此,让我们继续创建两个命名空间,即chat_com和chat_infra。我们将在chat_com上发送通信消息(用户消息),在chat_infra上发送基础设施消息(欢迎、用户进入等)。为此,让我们首先编辑服务器上的socket.js文件:
var io = require('socket.io');
exports.initialize = function (server) {
io = io.listen(server);
var chatInfra = io.of("/chat_infra")
.on("connection", function(socket){
socket.on("set_name", function (data) {
socket.set('nickname', data.name, function () {
socket.emit('name_set', data);
socket.send(JSON.stringify({type:'serverMessage',
message:'Welcome to the most interesting ' +
'chat room on earth!'}));
socket.broadcast.emit('user_entered', data);
});
});
});
var chatCom = io.of("/chat_com")
.on("connection", function (
socket) {
socket.on('message', function (message) {
message = JSON.parse(message);
if (message.type == "userMessage") {
socket.get('nickname', function (err, nickname) {
message.username = nickname;
socket.broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
});
}
});
});
}
从前面的代码中我们可以看出,大部分代码保持不变,除了突出显示的片段和一些代码重组。
我们在这里所做的,是将消息和事件分离成两个与它们的命名空间相对应的代码块。我们使用io.of方法创建一个新的命名空间。在命名空间创建后,它可以像任何 socket 对象一样使用。
在我们的情况下,我们创建了两个命名空间,并为它们各自添加了一个connection事件处理器。一个用于chat_infra,如下面的代码片段所示:
var chatInfra = io.of("/chat_infra")
.on("connection", function(socket){
以及另一个用于chat_com:
var chatCom = io.of("/chat_com")
.on("connection", function (socket) {
一旦建立连接,我们将在connection事件处理程序中获得一个socket对象,我们将像之前一样使用它。对于chat_infra,我们添加所有不属于用户间通信的消息和事件:
socket.on("set_name", function (data) {
socket.set('nickname', data.name, function () {
socket.emit('name_set', data);
socket.send(JSON.stringify({type:'serverMessage',
message:'Welcome to the most interesting ' +
'chat room on earth!'}));
socket.broadcast.emit('user_entered', data);
});
});
因此,我们将set_name处理程序、name_set的事件发射器、serverMessage的消息以及user_entered的事件广播器移动到chat_infra命名空间。
socket.on('message', function (message) {
message = JSON.parse(message);
if (message.type == "userMessage") {
socket.get('nickname', function (err, nickname) {
message.username = nickname;
socket.broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
});
}
});
这就只留下了chat_com命名空间上的标准User消息组件。
让我们现在看看这如何影响我们的客户端代码:
var chatInfra = io.connect('/chat_infra'),
chatCom = io.connect('/chat_com');
chatInfra.on('name_set', function (data) {
chatInfra.on("user_entered", function (user) {
$('#messages').append('<div class="systemMessage">' + user.name
+ ' has joined the room.' + '</div>');
});
chatInfra.on('message', function (message) {
var message = JSON.parse(message);
$('#messages').append('<div class="' + message.type + '">'
+ message.message + '</div>');
});
chatCom.on('message', function (message) {
var message = JSON.parse(message);
$('#messages').append('<div class="' +
message.type + '"><span class="name">' +
message.username + ':</span> ' +
message.message + '</div>');
});
$('#nameform').hide();
$('#messages').append('<div class="systemMessage">Hello ' +
data.name + '</div>');
$('#send').click(function () {
var data = {
message:$('#message').val(),
type:'userMessage'
};
chatCom.send(JSON.stringify(data));
$('#message').val('');
});
});
$(function () {
$('#setname').click(function () {
chatInfra.emit("set_name", {name:$('#nickname').val()});
});
});
在前面的代码中,我们看到的第一件和最重要的事情是我们正在连接两个套接字:
var chatInfra = io.connect('/chat_infra'),
chatCom = io.connect('/chat_com');
实际上,socket.io 将建立一个单一的套接字连接,并在其上多路复用两个命名空间。但建立这两个连接将使我们能够分别处理chat_infra和chat_com命名空间的消息或事件。
在下面的代码片段中,我们添加了对应于我们在服务器上添加的chat_infra发射器的处理程序。name_set处理程序将位于chat_infra命名空间:
chatInfra.on('name_set', function (data) {
我们也将对user_entered处理程序做同样的事情:
chatInfra.on("user_entered", function (user) {
$('#messages').append('<div class="systemMessage">' + user.name
+ ' has joined the room.' + '</div>');
});
接下来,我们添加on处理程序以监听chat_infra上的消息;这将接收所有服务器消息:
chatInfra.on('message', function (message) {
var message = JSON.parse(message);
$('#messages').append('<div class="' + message.type + '">'
+ message.message + '</div>');
});
我们还在chat_infra上发射set_name事件:
chatInfra.emit("set_name", {name:$('#nickname').val()});
在chat_com命名空间上,我们发送用户消息,如下面的代码所示:
$('#send').click(function () {
var data = {
message:$('#message').val(),
type:'userMessage'
};
chatCom.send(JSON.stringify(data));
此外,我们将使用以下代码片段将处理程序附加到接收从服务器中继的用户消息:
chatCom.on('message', function (message) {
var message = JSON.parse(message);
$('#messages').append('<div class="' +
message.type + '"><span class="name">' +
message.username + ':</span> ' +
message.message + '</div>');
});
现在我们已经理解了命名空间并利用它们来清理我们的设计和代码,让我们继续添加一些新功能。
房间
在本节中,我们将使用 socket.io 的另一个多路复用功能,称为房间。我们将使用它来完成名字所描述的事情,创建房间。如果网络中的每个人都同一个聊天室聊天,聊天室将会非常嘈杂和混乱。因此,作为第一步,让我们将我们的聊天从网站首页移动到/chatroom。为此,我们应该将我们的代码从index.jade移动到chatroom.jade,并在index.jade中放入以下代码:
extends layout
block content
section#welcome
div Welcome
a#startchat(type="button", class="btn", href="/chatroom") Start now
基本上,我们将创建一个带有欢迎信息和进入聊天室链接的首页。让我们也在style.css中添加以下样式:
#welcome div{
font-family: fantasy;
font-size: 100px;
margin-left: 20px;
margin-top: 100px;
}
.btn {
background-color: #5BB75B;
background-image: linear-gradient(to bottom, #62C462, #51A351);
background-repeat: repeat-x;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
color: #FFFFFF;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
border-image: none;
border-radius: 4px 4px 4px 4px;
border-style: solid;
border-width: 1px;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset,
0 1px 2px rgba(0, 0, 0, 0.05);
cursor: pointer;
display: inline-block;
font-size: 14px;
line-height: 20px;
margin-bottom: 0;
padding: 4px 12px;
text-align: center;
vertical-align: middle;
position: absolute;
right: 40px;
bottom: 80px;
text-decoration: none;
}
现在,我们的首页将如下所示:

首页
立即开始链接将带您进入聊天室,但目前那里还没有任何内容。因此,让我们修改我们的routes/index.js文件以提供chatroom服务。将以下片段添加到文件末尾:
exports.chatroom = function(req, res){
res.render('chatroom', { title: 'Express Chat' });
}
我们还必须在app.js中添加映射:
app.get('/chatroom', routes.chatroom);
现在我们有了首页,我们准备添加多个房间。我们现在将添加对聊天室页面的支持,使其能够接受一个room参数,并在请求时连接到该房间。因此,连接到聊天室以进入的调用将如下所示:
http://localhost:3000/chatroom?room=jsconf
为了做到这一点,我们需要编辑我们的chat.js客户端脚本文件:
var chatInfra = io.connect('/chat_infra'),
chatCom = io.connect('/chat_com');
var roomName = decodeURI(
(RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search)
|| [, null])[1]);
if (roomName) {
chatInfra.on('name_set', function (data) {
chatInfra.emit('join_room', {'name':roomName});
//EXISTING CODE
});
}
$(function () {
$('#setname').click(function () {
chatInfra.emit("set_name", {name:$('#nickname').val()});
});
});
第一件事是解析 URL 查询以获取房间名称,以下是这样做的方式:
var roomName = decodeURI(
(RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search)
|| [, null])[1]);
在前面的代码中,我们创建了一个正则表达式来解析出room=和&之间或内容末尾之间的值。在下一行中,我们检查是否提供了房间名称,一旦用户输入了名称,我们就会加入房间。
要加入房间,我们通过roomName作为参数发出join_room事件。此事件将在服务器上被处理:
if (roomName) {
chatInfra.on('name_set', function (data) {
chatInfra.emit('join_room', {'name':roomName});
由于我们只使用房间来限制广播消息(其他消息无论如何只发送到接收者的套接字),所以这就是我们在客户端需要做的所有事情。
现在,我们将编辑服务器上的sockets.js文件,以处理chat_infra上的join_room事件,并将广播更改以发送到它们打算发送的房间。让我们看看sockets.js中的更改:
var io = require('socket.io');
exports.initialize = function (server) {
io = io.listen(server);
var self = this;
this.chatInfra = io.of("/chat_infra");
this.chatInfra.on("connection", function (socket) {
//EXISTING CODE
});
socket.on("join_room", function (room) {
socket.get('nickname', function (err, nickname) {
socket.join(room.name);
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
comSocket.room = room.name;
socket.in(room.name).broadcast
.emit('user_entered', {'name':nickname});
});
});
});
this.chatCom = io.of("/chat_com");
this.chatCom.on("connection", function (socket) {
socket.on('message', function (message) {
message = JSON.parse(message);
if (message.type == "userMessage") {
socket.get('nickname', function (err, nickname) {
message.username = nickname;
socket.in(socket.room).broadcast.send(JSON.stringify(message));
message.type = "myMessage";
socket.send(JSON.stringify(message));
});
}
});
});
}
因此,这带来了一些小的结构变化。由于我们需要在chatInfra中引用chatCom,我们将它们两个都添加到当前对象中,该对象也存储为自身,以便在闭包中可以访问。在chat_infra连接处理器中,我们为join_room注册了一个新的事件处理器:
socket.on("join_room", function (room) {
socket.get('nickname', function (err, nickname) {
socket.join(room.name);
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
comSocket.room = room.name;
socket.in(room.name).broadcast
.emit('user_entered', {'name':nickname});
});
});
在处理器中,我们接收到的room对象将包含要加入的房间名称。接下来,我们将chat_infra套接字连接到该房间。这是通过使用socket对象的join方法来完成的:
socket.join(room.name);
join方法接受一个表示房间的名称字符串。如果不存在,房间将被创建,否则套接字将连接到现有房间。
现在,一旦我们的客户端加入房间,它将获取chat_infra命名空间中针对特定房间的所有消息。但是,这在我们也在chat_com命名空间中加入房间之前是没有用的。为此,我们需要获取与chat_com命名空间中当前socket对象相对应的socket对象,然后在其上调用相同的join方法:
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
要获取chat_com上的相应socket对象,我们使用当前socket对象的 ID(因为它将是相似的)从chatCom命名空间对象的sockets数组中检索它。下一行只是调用它的join方法。现在,在两个命名空间中,两者都已加入房间。但是,当我们从chat_com命名空间接收消息时,我们需要知道这个套接字连接到的房间名称。为此,我们将room属性设置在comSocket对象上,指向它连接到的房间:
comSocket.room = room.name;
现在一切都已经设置好了,我们将在房间中宣布用户已加入:
socket.in(room.name).broadcast
.emit('user_entered', {'name':nickname});
});
如我们之前所做的那样,我们仍然使用broadcast.emit,但不是在套接字上调用它,而是限制它只发送到房间中,使用in(room.name)。我们做出的另一个改变将是通过限制广播消息只发送到房间来再次广播用户消息:
socket.in(socket.room).broadcast.send(JSON.stringify(message));
现在,您可以通过访问以下 URL 来打开聊天室:
http://localhost:3000/chatroom?room=test001
在两个浏览器窗口中打开此链接并使用不同的名称登录。在另一个浏览器窗口中使用以下链接打开另一个聊天房间:
http://localhost:3000/chatroom?room=test002
只在房间 test001 中发送的消息和警报将在前两个浏览器中可见,而连接到 test002 的用户将无法看到它们:

用户一连接到房间 test001
这是第二个用户连接到房间 test001 的截图:

用户二连接到房间 test001
在下面的屏幕截图中,可以看到第三个用户已连接到房间 test002:

用户三连接到房间 test002
列出房间
既然我们支持创建多个房间,让我们继续添加一个页面来列出、加入和创建新的房间。我们将首先添加一个名为 rooms.jade 的 Jade 视图,其代码如下:
extends layout
block append head
script(type='text/javascript', src='/socket.io/socket.io.js')
script(type='text/javascript', src='/javascripts/rooms.js')
block content
section#chatrooms
div#new_room
span Start a new Room
input#new_room_name(type="text")
input#new_room_btn(type="button", value="Start")
div#allrooms
div#header Or join a room
div#rooms_list
此视图有一个输入框用于接受新的房间名称,以及一个 div 标签用于添加现有房间的列表。我们还添加了 socket.io.js 的脚本以及一个新的客户端代码脚本文件,用于列出房间,名为 rooms.js。接下来,创建 rooms.js 脚本文件,其代码如下:
var chatInfra = io.connect('/chat_infra');
chatInfra.on("connect", function(){
chatInfra.emit("get_rooms", {});
chatInfra.on("rooms_list", function(rooms){
for(var room in rooms){
var roomDiv = '<div class="room_div"><span class="room_name">' + room + '</span><span class="room_users">[ '
+ rooms[room] + ' Users ] </span>'
+ '<a class="room" href="/chatroom?room=' + room
+ '">Join</a></div>';
$('#rooms_list').append(roomDiv);
}
});
});
$(function(){
$('#new_room_btn').click(function(){
window.location = '/chatroom?room=' +
$('#new_room_name').val();
});
});
在前面的代码中,我们基于 chat_infra 命名空间进行连接,请求其上的聊天房间,并在视图中渲染它们。让我们快速看一下这里发生的一个重要步骤:
chatInfra.emit("get_rooms", {});
如前述代码所示,连接后我们首先向 get_rooms 发射一个事件。这将请求从服务器获取房间列表。接下来,我们设置一个监听器来接收房间列表并将其渲染:
chatInfra.on("rooms_list", function(rooms){
在处理程序中,如以下代码块所示,我们正在遍历房间及其中的用户数量映射,并将其追加到房间列表中:
for(var room in rooms){
var roomDiv = '<div class="room_div"><span class="room_name">' + room + '</span><span class="room_users">[ '
+ rooms[room] + ' Users ] </span>'
+ '<a class="room" href="/chatroom?room=' + room
+ '">Join</a></div>';
$('#rooms_list').append(roomDiv);
}
最后,我们有创建新房间的代码。要创建新房间,我们只需将聊天房间重定向到带有新房间名称的 URL 参数:
$('#new_room_btn').click(function(){
window.location = '/chatroom?room=' +
$('#new_room_name').val();
});
接下来,我们需要在服务器上添加一个 get_rooms 处理程序来返回房间列表。为此,我们将在 sockets.js 中的 chat_infra 命名空间上添加处理程序:
this.chatInfra.on("connection", function (socket) {
//EXISTING CODE
socket.on("get_rooms", function(){
var rooms = {};
for(var room in io.sockets.manager.rooms){
if(room.indexOf("/chat_infra/") == 0){
var roomName = room.replace("/chat_infra/", "");
rooms[roomName] = io.sockets.manager
rooms[room].length;
}
}
socket.emit("rooms_list", rooms);
});
});
我们可以使用 io.sockets.manager 获取所有房间的列表,然后我们可以通过遍历列表来构建客户端期望的映射。在我们的例子中,我们过滤以仅获取 chat_infra 中的房间,因为它们也将创建在 chat_com 中,我们不希望有重复。一旦我们有了映射,我们将将其作为 rooms_list 发射。在此之后,我们需要将条目添加到我们的 routes/index.js 文件中,如下所示:
exports.rooms = function(req, res){
res.render('rooms', { title: 'Express Chat' });
}
我们还需要在 app.js 中添加映射,以便将服务器房间添加到 /rooms:
app.get('/rooms', routes.rooms);
最后,让我们在我们的新房间页面 style.css 中添加一些 CSS 样式:
#chatrooms{
margin: 20px;
}
#new_room {
font-size: 17px;
}
#new_room span{
padding-right: 15px;
}
#allrooms #header{
border-bottom: 1px solid;
border-top: 1px solid;
font-size: 17px;
margin-bottom: 10px;
margin-top: 16px;
padding: 5px 0;
}
.room_div {
border-bottom: 1px solid #CCCCCC;
padding-bottom: 5px;
padding-top: 12px;
}
.room_name {
display: inline-block;
font-weight: bold;
width: 165px;
}
.room_div a{
position: absolute;
right: 40px;
}
前往 /rooms 并创建一些房间,然后当你在新浏览器中打开房间页面时,你会看到如下类似的内容:

我们聊天服务器上的房间列表
分享会话
现在我们支持多个房间,但每次进入房间时都输入昵称非常麻烦。让我们修改我们的系统,在进入系统时只接受一次昵称,并在所有房间中使用它。
为了这个目的,让我们首先修改登录页面,添加一个输入框来接受昵称,并添加一个 JavaScript 文件来添加逻辑:
extends layout
block append head
script(type='text/javascript', src='/javascripts/landing.js')
block content
section#welcome
div Welcome
span
input#nickname(type="text",
placeholder="Enter a nickname")
a#startchat(class="btn") Login
在这里,在之前的代码中,我们添加了一个脚本条目来添加 landing.js,并将立即开始按钮替换为输入名字的字段和登录按钮。接下来,让我们看一下 landing.js:
$(function(){
$('#startchat').click(function(){
document.cookie = "nickname=" + $('#nickname').val()
+ ";; path=/";
window.location = "/rooms";
});
});
在之前的代码中,我们为 startchat 按钮附加了一个 click 处理器。在处理器中,我们将用户输入的昵称添加到 cookie 中,并将用户重定向到 /rooms。在连接 socket 时,我们将读取这个 cookie 信息,并将其设置在 socket 上。在 socket 连接中可以访问这个 cookie 信息之前,我们需要做一些准备工作来在 Express.js 应用程序中启用 cookies。为此,通过以下代码块编辑 app.js 代码:
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path')
, connect = require('connect');
var app = express();
var sessionStore = new connect.session.MemoryStore();
app.configure(function(){
//EXISTING CODE
app.use(express.bodyParser());
app.use(express.cookieParser('somesuperspecialsecrethere'));
app.use(express.session({ key: 'express.sid',
store: sessionStore}));
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});
//EXISTING CODE
第一步是在 package.json 文件中添加 connect 作为依赖项,并在 app.js 中添加 require 关键字。connect 关键字用于创建会话存储;在这种情况下,一个内存会话存储:
var sessionStore = new connect.session.MemoryStore();
我们还在 Express 应用程序中启用了 cookieParser 中间件和 session 模块。Express 的 cookieParser 中间件将接受一个 secret 参数,该参数将用于加密 cookies。Express 的 session 模块在传递给它密钥(express.sid 是会话的密钥)和会话应该维护的存储位置时初始化。在下面的代码中,我们传递给它一个内存存储,这是我们之前步骤中创建的:
app.use(express.bodyParser());
app.use(express.cookieParser('somesuperspecialsecrethere'));
app.use(express.session({ key: 'express.sid',
store: sessionStore}));
app.use(express.methodOverride());
app.use(app.router);
关于之前代码的一个重要注意事项是添加这两个中间件组件的顺序。这些应该在添加 bodyParser 中间件之后和添加 router 中间件之前添加。如果你现在打开浏览器并浏览到登录页面,你可以在浏览器调试工具的 Cookies 选项卡下看到带有 express.sid 键的 cookie。如果你输入一个名字并点击进入按钮,你将再次看到一个新的 cookie,其名称为你的昵称:
var io = require('socket.io');
exports.initialize = function (server) {
io = io.listen(server);
io.set('authorization', function (data, accept) {
if (data.headers.cookie) {
data.cookie = require('cookie').parse(data.headers.cookie);
data.sessionID = data.cookie['express.sid'].split('.')[0];
data.nickname = data.cookie['nickname'];
} else {
return accept('No cookie transmitted.', false);
}
accept(null, true);
});
var self = this;
this.chatInfra = io.of("/chat_infra");
this.chatInfra.on("connection", function (socket) {
socket.on("join_room", function (room) {
var nickname = socket.handshake.nickname;
socket.set('nickname', nickname, function () {
socket.emit('name_set', {'name':
socket.handshake.nickname});
socket.send(JSON.stringify({type:'serverMessage',
message:'Welcome to the most interesting ' +
'chat room on earth!'}));
socket.join(room.name);
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
comSocket.room = room.name;
socket.in(room.name).broadcast.emit('user_entered',
{'name':nickname});
});
});
//EXISTING CODE
}
之前代码块中的第一个更改介绍了 socket.io 中的一个新功能;这个更改在以下突出显示的代码块中显示:
io.set('authorization', function (data, accept) {
if (data.headers.cookie) {
data.cookie = require('cookie').parse(data.headers.cookie);
data.sessionID = data.cookie['express.sid'].split('.')[0];
data.nickname = data.cookie['nickname'];
} else {
return accept('No cookie transmitted.', false);
}
accept(null, true);
});
在这个代码片段中,我们为 socket 设置了一个 authorization 方法。这个方法将获取两个参数,包含所有 HTTP 请求信息的数据和 accept 方法回调。当请求 socket.io 连接但尚未建立连接时,将调用 authorization 方法。
我们可以使用这个方法来实际执行授权,但在这个案例中,我们只是用它来从 cookies 中获取昵称,因为这是唯一一个带有 HTTP 数据的 socket.io 方法。
我们正在从 HTTP 数据中读取 cookie 头信息,并使用cookie模块的parse方法对其进行解析。从 cookie 中,我们提取sessionID值和昵称,并将其设置到data对象中。这个对象作为handshake属性在 socket 上可用。最后,我们将调用accept回调函数,该函数接受两个参数,第一个是消息,另一个是布尔变量,指示授权是否成功。
我们将移除set_name处理器,因为这个处理器不需要被调用,因为我们已经拥有了这个名字。我们将把set_name处理器中的逻辑移动到join_room处理器:
socket.on("join_room", function (room) {
var nickname = socket.handshake.nickname;
socket.set('nickname', nickname, function () {
socket.emit('name_set', {'name': nickname});
socket.send(JSON.stringify({type:'serverMessage',
message:'Welcome to the most interesting ' +
'chat room on earth!'}));
socket.join(room.name);
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
comSocket.room = room.name;
socket.in(room.name).broadcast.emit('user_entered',
{'name':nickname});
});
});
在join_room处理器中,我们将从socket.handshake映射中获取昵称,并将其设置为 socket 上的一个属性。在设置nickname属性时,我们仍然会触发name_set事件,以将客户端上的更改保持在最小:
var chatInfra = io.connect('/chat_infra'),
chatCom = io.connect('/chat_com');
var roomName = decodeURI((RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]);
if (roomName) {
chatInfra.emit('join_room', {'name':roomName});
chatInfra.on('name_set', function (data) {
//EXISTING CODE
});
}
由于join_room处理器是服务器上房间的初始化器,我们将它从name_set处理器中移除,并在页面加载期间直接调用它。其余的代码保持不变。
要尝试这段代码,您需要打开两个不同的浏览器或在不同匿名会话中的浏览器,因为对于同一浏览器,cookie/sessions 将会被共享。
摘要
在本章中,我们学习了如何为会话设置数据,如何处理命名空间和房间,以及如何与 express 会话集成。在这里,我们已经完成了一个良好且可工作的聊天系统。基于我们在这里学到的概念,构建更多功能将是一个很好的练习。可以构建的一些有趣的功能包括用户存在警报、房间用户列表、私密聊天等。在下一章中,我们将探讨 socket.io 协议及其工作原理。
第五章. Socket.IO 协议
Socket.io 提供了一个非常简单但易于使用的 API,同时暴露了大量的功能。此外,这些功能在所有浏览器和 socket.io 提供的各种传输机制中都是统一的。为了实现这一点,socket.io 客户端和服务器在后台做了很多工作。在本章中,我们将检查并尝试理解 socket.io 中的通信以及一些 socket.io 内部机制。
为什么我们需要另一个协议?
对于熟悉 WebSocket 的人来说,第一个问题是,为什么我们已经有 WebSocket 了,还需要另一个协议?答案是双重的;socket.io 在所有浏览器中都以统一的方式工作(追溯到 Internet Explorer 6),并且 socket.io 提供了一个功能更丰富的 API
WebSocket 规范仍在开发中,并且不支持许多正在使用的浏览器。事实上,任何版本的 Internet Explorer 10 之前的版本都没有 WebSocket 支持。仍然有很多人在使用不支持 WebSocket 的旧浏览器。
对于 WebSocket 来说,另一个问题是防火墙和代理。大多数防火墙会阻止任何通信(除了标准的 HTTP 1.0/1.1),并且可能不允许建立 WebSocket 连接。同样适用于大多数代理服务器。
因此,如果我们决定只使用 WebSocket 协议,我们必须理解会有很多人可能无法使用我们的应用程序。
与此相反,当我们使用 socket.io 构建应用程序时,能够使用 WebSocket 的人将继续使用它,而那些不能使用的人将退而求其次,使用下一个最好的可用传输机制,然后是下一个,直到他们在浏览器中找到一个可以工作的机制,即使是通过防火墙和代理,一直到 iframe(很少使用)。默认顺序如下:
-
WebSocket
-
FlashSocket
-
XHR 长轮询
-
XHR 多部分流式传输
-
XHR 轮询
-
JSONP 轮询
-
iframe
值得注意的是,使用 JSONP 轮询,socket.io 提供了跨域通信的支持,而无需在服务器上进行任何特殊配置或客户端上的任何特殊代码:
现在,让我们看看 API 中的差异。为此,我们将只看到 JavaScript 客户端 API,因为任何服务器都将根据使用的编程语言有自己的实现和 API。
WebSocket API
让我们从快速查看一个显示 WebSocket 客户端骨架的代码片段开始:
<script>
var socket = new WebSocket('ws://localhost:8080');
socket.onopen = function(event) {
socket.send('Client socket connected');
};
socket.onmessage = function(event) {
console.log('Client received a message', event);
};
socket.onclose = function(event) {
console.log('Client socket disconnected', event);
};
//socket.close()
</script>
第一步,如前一个代码片段所示,是创建一个新的 WebSocket 实例;在这里,我们必须传递 WebSocket 服务器的 URI。这个 URI,像任何其他 URI 一样,有一个指定协议的部分,在这个情况下可以是ws(未加密)或wss(加密);服务器地址(服务器的 IP 地址或有效的域名);最后,端口。
理想情况下,我们还需要检查用户所使用的浏览器是否支持 WebSocket,但我已经跳过了这部分内容,以专注于 API。
在创建 WebSocket 对象之后,我们可以向其附加事件处理器。WebSocket 公开了三个事件,以及它们对应的事件处理器:
-
open:onopen事件处理器 -
message:onmessage事件处理器 -
close:onclose事件处理器
如其名称所示,这些处理器将在套接字连接打开时、套接字上有新消息时以及关闭套接字连接时分别被调用。
对于每个事件,客户端都会接收到事件数据。如果事件是消息,它将包含该消息以及其他数据。WebSocket 客户端不会尝试解释消息或其类型,也就是说,它将所有消息视为纯文本,并且由应用程序负责解释和理解。此外,没有提及消息的命名空间或套接字连接的多路复用。
如果您看到onopen处理器,您会注意到send方法,这是客户端用来发送消息的方法。同样,它只能发送纯文本,因此您必须负责序列化和反序列化。
最后,我们有close方法,正如其名称所暗示的,可以用来从客户端关闭套接字连接。
Socket.IO API
让我们看看使用 sockt.io 的相同代码:
<script>
var socket = io.connect('http://localhost:8080');
socket.on('connect', function() {
socket.send('Client socket connected');
});
socket.on('message', function(data) {
console.log('Received a message from the server!',data);
});
socket.on('disconnect', function() {
console.log('The client socket disconnected!');
});
</script>
上述代码片段看起来与 WebSocket 的代码相似,并且不出所料,与之前的代码执行相同的工作。然而,有一些细微的变化:我们不是使用onopen、onmessage和onclose,而是使用 socket.io 的on方法来附加处理器。优点是,当我们使用 socket.io 的自定义事件功能时,处理事件的 API 保持不变。
正如我们已经看到的,您可以使用以下代码行来发出新事件:
socket.emit("myevent", {"eventData": "..."});
然后使用以下方式接收它:
socket.on("myevent", function(event){...});
如您所见,在这种情况下,我们正在传递一个 JSON 对象作为数据;socket.io 将为我们处理序列化和反序列化。
此外,socket.io 提供了对消息命名空间、连接多路复用、断开检测、重新连接以及向所有客户端广播消息的 API 支持。
考虑到本节中涵盖的所有内容,我们可以得出结论,socket.io 将需要自己的协议和机制来工作。
Socket.IO 套接字
socket.io 套接字通过不同的传输机制模拟网络套接字。就像任何其他套接字一样,它在其生命周期中有各种阶段,这取决于连接的状态。以下是这些阶段:
-
正在连接
-
已连接
-
正在断开连接
-
断开连接
当客户端向服务器发送连接请求并开始握手时,套接字建立。
一旦握手完成,将使用握手期间协商的传输方式打开连接,并将套接字的状态设置为已连接。
为了根据服务器配置检查套接字的活动性,服务器可能需要客户端定期向服务器发送心跳消息。如果没有这样的消息,或者底层传输失败,套接字将被断开连接。
在这种情况下,客户端将发起一个重新连接。如果连接在连接终止时间或握手时同意的超时时间内恢复,则缓冲的消息将被发送。如果连接未恢复,客户端将启动一个新的连接请求,从新的握手开始。
此外,可选地,为了确保通过套接字的消息传递,我们可以强制套接字确认消息传递。
当在客户端或服务器上调用close方法时,套接字将被终止。
Socket.IO 连接
socket.io 连接从握手开始。这使得握手成为协议的一个特殊部分。除了握手之外,协议中的所有其他事件和消息都是通过套接字传输的。
Socket.io 旨在与 Web 应用程序一起使用,因此假设这些应用程序始终能够使用 HTTP。正因为如此,socket.io 的握手才发生在 HTTP 上。
为了发起连接并执行握手,客户端在握手 URI(由传递给connect方法的 URI 构建)上执行一个POST请求。让我们以相同的 socket.io 连接 URI 为例,尝试理解其各个部分。假设 URI 如下:
让我们分解并理解这个 URI。
http 是正在使用的协议。我们可以将其设置为使用https,在客户端的connect ct方法中使用https。
myhost.com 再次来自connect方法,是你想要连接的主机的名称或 IP 地址。默认值为localhost。
8080 是服务器监听的端口。当我们调用它时,这也传递给了connect方法。默认值为80。
socket.io 是处理所有连接请求的命名空间。
1 是 socket.io 协议版本号。
服务器可以通过以下三种方式之一对此做出响应:
-
200 OK– 当握手成功时,服务器将给出此响应。除了状态外,响应体应该是一个冒号分隔的列表,包含分配给此连接的会话 ID、心跳超时、连接关闭超时以及通过逗号分隔的支持的传输列表。一个示例响应体如下:8749dke9387:20:10:websocket,flashsocket,xhr-polling -
401 Unauthorized– 如果授权处理程序未能授权客户端,服务器将给出此响应。正如我们在上一章中看到的,这是我们附加到服务器上authorize事件的处理器,它使用连接和 cookie 信息来授权用户。 -
503 服务不可用– 当服务器有任何其他原因(包括错误)拒绝向客户端提供服务时。
如果握手成功,基于服务器提供的传输和客户端支持的传输,socket.io 客户端将开始在特定的 URI 上与服务器通信。此 URI 的形式为[scheme]://[host]/[namespace]/[version]/[transportId]/[sessionId]。
-
[scheme]是客户端将用于通信的协议。在 WebSocket 的情况下,这将是ws或wss,而在 XHR 的情况下,则是http或https。 -
[host]是服务器名称或 IP 地址。 -
[namespace]是我们想要发送消息的 socket.io 命名空间。 -
[version]是我们使用的 socket.io 协议版本,目前为1。 -
[transportId]是为通信选择的传输机制名称。 -
[sessionId]是服务器在握手期间分配给客户端的会话 ID。
在双向传输的情况下,例如 WebSocket,在此 URI 上打开的连接将用于发送和接收消息。
对于单向传输,如 XHR 长轮询,客户端将在此 URI 上执行GET请求,服务器将保持此请求直到有数据要发送,而客户端将在此 URI 上执行POST请求,每当它需要向服务器发送消息或事件时。
Socket.IO 消息
一旦建立了传输的连接,客户端和服务器之间的所有通信都通过套接字上的消息进行。消息需要按照 socket.io 指定的格式进行编码。
此格式使 socket.io 能够确定消息的类型和消息中发送的数据,以及一些对操作有用的元数据。消息格式为[type] : [id ('+')] : [endpoint] (: [data])。
-
type是一个单数字整数,指定消息的类型。 -
id是消息 ID,它是一个递增整数;它用于 ACK。它是可选的。 -
如果存在
+符号,它告诉 socket.io 不要处理 ACK,因为应用程序打算自己处理它。 -
endpoint是消息打算发送到的 socket 端点。这是可选的,用于在命名空间中复用套接字。如果省略,消息将发送到默认套接字。 -
data是要发送到套接字的相关数据。在消息的情况下,它被视为纯文本,而在事件的情况下,它将被解析为 JSON。
在接下来的章节中,我们将看到消息的类型。
断开连接(0)
当类型为零(0)时,消息是一个断开信号。这将告诉 socket.io 关闭连接和提到的套接字。如果未指定端点,消息将发送到默认套接字,这将导致整个套接字关闭,并且该套接字上的所有端点都将终止。例如:
-
消息:
0– 结果是套接字被关闭,所有连接/端点都被终止。 -
消息:
0::/endpoint– 将关闭到/endpoint的套接字连接,并且无法向该端点发送或接收消息。其他端点将继续运行。
连接(1)
此消息仅用于多路复用,并由客户端发送到服务器以打开新的连接。因此,此消息必须始终包含一个端点。第一个(默认)套接字连接是通过前面解释的手 shake 建立的。端点可能后跟 URL 查询格式的查询参数。如果连接成功,服务器将回显相同的消息,否则服务器可以发送错误消息。例如:
-
消息:
1::/endpoint– 请求服务器打开一个到端点的多路复用套接字。 -
消息:
0::/endpoint?param=one– 请求服务器打开一个到端点的多路复用套接字,传递一个名为param的参数,其值为one。
心跳(2)
这是心跳消息。必须在握手期间协商的超时时间内从客户端发送到服务器。服务器也将回复心跳消息。在这种情况下,我们没有端点,也不需要任何其他信息。这是因为它服务于整个套接字。例如:
- 消息:
2– 向另一端发送心跳消息。
消息(3)
这是通过套接字发送的消息。在 API 中,此消息将在您使用 socket.send 时发送,并在接收端引发消息事件。此消息将携带数据,将其视为纯文本。例如:
-
消息:
3:1::Some message– 这将向另一端发送消息,其中消息事件处理程序将触发事件数据中的Some message消息。 -
消息:
3:1:/endpoint:Some message– 再次,消息将被发送到套接字的另一端,但是在多路复用端点上。
JSON 消息(4)
这与发送消息类似,但在此情况下,消息必须使用 JSON 进行序列化,并在发送到处理程序之前在另一端进行解析。在版本 0.6 中,这是使用与 send() 消息相同的 API 实现的,只是传递一个 JSON 消息而不是字符串消息。但从版本 0.7 开始,由于这引入了比发送纯文本更高的性能开销,我们必须使用 json 标志来发送 JSON 消息;例如,socket.json.send。例如:
- 消息:
4:1::{"some":"content"}– 将 JSON 消息发送到套接字的另一端。
事件(5)
事件消息是一种特殊的 JSON 消息,用于通过套接字发送事件。在事件中,数据负载的形式为 {"name":"eventName", "args":{"some":"content"}}。
在这里,name 是事件的名称,args 是要发送到处理程序的参数。socket.emit 调用用于在应用程序中发送事件。
以下事件名称是保留的,不能在应用程序中使用:
-
message -
connect -
disconnect -
open -
close -
error -
retry -
reconnect
例如:
- 消息:
5:1::{"name": "myEvent", "args":{"some": "data"}}– 结果是事件将被发送到另一端,并且将调用适当的事件处理器,并将参数传递给它。
ACK (6)
当消息被接收并且启用了 ACK 请求时,将发送确认(ACK)消息;或者,它也可以由应用程序发送。ACK 消息中的数据部分可以是正在确认的消息的 ID。如果消息 ID 后面跟着+和附加数据,它被视为一个事件包。例如:
-
消息:
6:::1– 发送对 ID 为1的消息的接收确认。 -
消息:
6:::1+["A", "B"]– 这将发送包含数据的消息确认。
错误 (7)
当服务器在处理连接到端点的connect请求时出现错误等情况时,会发送此消息。此消息的数据部分将包含错误消息和可选的建议,由+符号分隔。例如:
- 消息:
7:::Unauthorized– 结果是将错误发送到客户端。
NOOP (8)
此消息表示没有操作,用于在轮询超时后关闭轮询。
概述
在本章中,我们看到了 socket.io 服务器和客户端的通信机制。理解其工作原理和消息格式,有助于我们在开发 socket.io 应用程序时调试遇到的问题。
在下一章中,我们将学习如何在生产环境中部署和扩展 socket.io 应用程序。同时,我们还将获得一些关于如何最小化生产服务器上麻烦的提示。
第六章。部署和扩展
在本地服务器上运行我们的应用程序是可以的,但要使一个网络应用程序真正有用,需要将其部署到公共服务器并使其对他人可访问。要在 Node.js 上运行我们的聊天服务器应用程序,除了使用 WebSocket 等协议外,还需要考虑一些特殊因素。在本章中,我们将探讨以下内容:
-
部署我们的应用程序时需要考虑的事项
-
生产就绪部署的建议
-
为什么 socket.io 应用程序的扩展与其他网络应用程序不同
-
我们如何扩展我们的聊天应用程序
生产环境
在生产服务器上运行应用程序之前,我们应该做的第一件事是将环境设置为 production。每个现代服务器或框架都有开发和生产模式,node 也不例外。实际上,在 node 中,你可以将环境设置为任何名称,然后在代码中为该名称设置不同的配置。要设置我们的 node 服务器运行的环境,我们设置一个环境变量 NODE_ENV 为我们想要运行 node 的环境。因此,要运行 node 在 production 环境中,我们使用以下行:
$ export NODE_ENV=production
然后运行你的 node 应用程序。在 第二章 中,入门 Node.js,我们看到了 app.configure 中的第一个参数是我们需要配置的环境变量:
app.configure('development', function(){
app.use(express.errorHandler());
});
在这个片段中,我们正在将应用程序设置为在 development 环境中激活 express.errorHandler,这是默认环境。如果我们已将 NODE_ENV 设置为 production,则不会使用 express.errorHandler。
运行应用程序
使用 node 在命令行上运行应用程序,就像我们到目前为止所做的那样,在开发期间是可行的;但在我们远程连接的生产服务器上,通常不切实际或建议保持控制台运行。有两种处理方式,要么我们将 node 作为后台进程运行,将所有控制台输出重定向到文件,要么我们在持久控制台中运行它,我们可以使用 screen 或 byobu 重新连接到该控制台。
要将节点作为后台进程运行,就像在 Linux 上的任何其他进程一样,我们将使用 & 操作符,并且为了确保我们在登出后它仍然继续运行,我们将使用 nohup:
$ nohup npm start 2>&1 >> npmout.log &
之前的命令将重定向 stdout 和 stderr 命令到 npmout.log,并将 npm 进程置于后台。
另一个选项是使用 screen 或 byobu 等实用程序在持久控制台上运行 node。要使用此方法,启动 screen,然后运行你的应用程序,如下所示:
$ screen
$ npm start
现在,我们可以通过使用 Ctrl +a 然后按 d 来断开与此屏幕的连接。这将使我们回到默认的 shell。然后我们可以断开连接。当我们再次连接到服务器时,为了查看服务器输出,我们可以使用以下命令重新连接到屏幕:
$ screen -r
保持运行
我们不仅希望应用程序在我们登出时运行,我们还希望我们的应用程序能够可靠地持续运行。生产服务器不经常重启,通常我们希望它们在发生崩溃、故障或错误时尽快恢复。对于 node,通常意味着在失败时立即重启进程。有许多方法可以保持 node 服务器运行。在本节中,我们将看到其中两种:
-
Monit
-
Forever
这是 Monit 在其网站上如何描述的(mmonit.com/monit/):
Monit 是一个免费的开源实用程序,用于管理和监控 UNIX 系统上的进程、程序、文件、目录和文件系统。Monit 执行自动维护和修复,并在错误情况下执行有意义的因果操作。
让我们从安装 Monit 开始。在基于 RPM 或 Yum 的系统上,如 RHEL、Fedora 或 CentOS,你可以使用yum命令安装它,如下所示:
$ sudo yum install monit
或者,在基于 Debian 或 apt-get 的系统上,你可以使用apt-get安装 Monit:
$ apt-get install monit
对于其他系统,你可以在 Monit 网站上查看安装说明。
一旦安装了 Monit,我们就可以配置它来管理我们的 node 应用程序。为此,我们将在/etc/monit.d/或/etc/monit/conf.d/中创建一个配置文件(在我们的例子中我们将称之为awesome-chat),具体取决于你的 Monit 安装:
check host objdump with address 127.0.0.1
start program = "/bin/sh -c \
'NODE_ENV=production \
node /opt/node_apps/awesome-chat/app.js 2>&1 \
>> /var/log/awesome-chat.log'"
as uid nobody and gid nobody
stop program = "/usr/bin/pkill -f \
'node /opt/node_apps/awesome-chat/app.js'"
if failed port 3000 protocol HTTP
request /
with timeout 10 seconds
then restart
在这个文件中,你应该注意到高亮的部分。我们强调的是程序,或者更重要的是,启动/停止我们的应用程序的命令,然后最终配置 Monit 在发生故障时重启应用程序。这是通过向端口3000发送 HTTP 请求来检测的。
就这样;我们可以使用以下命令启动我们的应用程序:
$ monit start awesome-chat
使用以下代码停止它:
$ monit stop awesome-chat
在发生崩溃的情况下,Monit 将负责重启应用程序。
Monit 可以用来运行和监控任何守护进程服务。它还有一个网页界面,如果你想要检查状态,默认情况下它运行在端口2812上。你可以在 Monit 的网站上及其在线手册中了解更多关于 Monit 的信息。
另一种更具体的 node 方法来确保我们的服务器持续运行是Forever (github.com/nodejitsu/forever)。Forever 描述自己为:
一个简单的 CLI 工具,用于确保给定的脚本持续运行。
这就是它的作用。给定你的 node 应用程序脚本,Forever 将启动它并确保它持续运行。由于 Forever 本身也是一个 node 应用程序,我们将使用 npm 来安装它:
$ sudo npm install forever -g
现在,要使用 Forever 启动应用程序,只需执行app.js文件并使用forever。只需运行以下命令:
$ forever start app.js
我们可以使用以下命令查看持续运行的应用程序列表:
$ forever list
0 app.js [ 24597, 24596 ]
要停止应用程序,请使用forever stop命令:
$ forever stop 0
访问 Forever 的 GitHub 页面,了解更多关于 Forever 及其工作原理的信息。
在 *nix 系统上还有其他一些工具可以将 node 运行为守护进程。以下是一些:
-
Supervisord (
supervisord.org/) -
Daemontools (
cr.yp.to/daemontools.html)
扩展
现在我们已经确保了我们的应用程序将持续运行,并且可以从故障中恢复,是时候开始考虑如何处理成千上万用户涌入我们的聊天室了。首先,第一步是在服务器前设置一个负载均衡器代理。这里有多种选择,我们可以使用 Apache HTTP 服务器、Nginx 等。所有这些服务器在平衡传统 HTTP 流量方面都工作得很好,但还需要一些时间才能赶上处理 WebSocket。因此,我们将使用一个本身就在负载均衡 TCP/IP 上工作的服务器。这是 HAProxy 在其官方网站上的描述:
HAProxy 是一个免费、非常快速且可靠的解决方案,提供高可用性、负载均衡和 TCP 及 HTTP 应用程序的代理功能。它特别适合在需要持久性或第 7 层处理的情况下,处理承受极高负载的网站。在今天硬件的支持下,显然可以支持数万个连接。
HAProxy 与前端和后端一起工作。这些是通过位于 /etc/haproxy/haproxy.cfg 的 HAProxy 配置文件配置的。以下文件在端口 80 创建了一个前端监听器,并将其转发到单个位于 3000 的服务器:
global
maxconn 4096
defaults
environment http
frontend all 0.0.0.0:80
default_backend www_Node.js
backend www_Node.js
environment http
option forwardfor
server Node.js 127.0.0.1:3000 weight 1 maxconn 10000 check
在此文件中,我们正在定义一个位于 0.0.0.0:80 的前端监听器,默认的 www_Node.js 后端在相同的 127.0.0.1 服务器上监听 3000。
但此配置尚未准备好处理 WebSocket。要支持和处理 WebSocket,请参考以下代码块:
global
maxconn 4096
defaults
environment http
frontend all 0.0.0.0:80
timeout client 86400000
default_backend www_Node.js
acl is_websocket hdr(upgrade) -i websocket
acl is_websocket hdr_beg(host) -i ws
use_backend www_
Node.js if is_websocket
backend www_Node.js
environment http
option forwardfor
timeout server 86400000
timeout connect 4000
server Node.js 127.0.0.1:3000 weight 1 maxconn 10000 check
我们首先做的事情是增加客户端超时值,这样当客户端长时间无活动时,客户端连接不会断开。acl 代码行指示 HAProxy 理解并检查我们何时收到 websocket 请求。
通过使用 use_backend 指令,我们配置 HAProxy 使用 www_Node.js 后端来处理 websocket 请求。当你想从任何服务器(如 Apache HTTP)提供静态页面,并希望仅使用 node 来处理 socket.io 时,这很有用。
现在我们来到想要请求由多个节点服务器/进程处理的环节。为此,首先我们将告诉代理通过添加以下指令到后端来实现请求的轮询:
balance roundrobin
然后我们将向后端添加更多服务器条目:
server Node.js 127.0.0.1:4000 weight 1 maxconn 10000 check
server Node.js 192.168.1.101:3000 weight 1 maxconn 10000 check
在这里,我们添加了两个新的节点实例:一个是在同一服务器上监听端口 4000 的新进程,另一个运行在另一服务器上,该服务器可通过负载均衡器在 192.168.1.101 的端口 3000 上访问。
我们已完成服务器的配置,现在进入的请求将现在在配置的三个节点实例之间路由。
节点集群
Node 现在自带完全重写的集群模块。集群模块允许节点在集群前端启动多个进程,并监控和管理它们。我们将快速查看如何使用此模块创建应用程序集群,但请注意,这只是为了创建多个进程,我们仍然需要设置一个工具来监控集群主进程,以及一个代理来将请求转发到节点服务器。
让我们看看如何使用集群模块。集群模块最好的部分是,你实际上不需要更改你的应用程序。集群将运行一个主实例,我们可以启动我们应用程序的多个实例,它们都将监听一个共享端口。
这里是我们可以使用来集群化 app.js 文件的脚本:
var cluster = require('cluster');
if (cluster.isMaster) {
var noOfWorkers =
process.env.NODE_WORKERS || require('os').cpus().length;
while(workers.length < noOfWorkers) {
cluster.fork();
}
} else {
require('./app.js');
}
那么,这里发生了什么?我们首先使用 require 引入 cluster 模块。在下一行,我们检查启动的实例是主进程还是工作进程。
如果是主进程,我们检查是否设置了 NODE_WORKERS 环境变量,否则获取服务器运行系统上的处理器数量。要设置 NODE_WORKERS 环境变量,你可以运行以下命令:
$ export NODE_WORKERS=2
之前的命令将告诉集群启动两个节点。
现在,在循环中,我们在集群上调用 fork。这调用 child_process.fork,以便主进程和启动的工作进程可以通过 IPC 进行通信。
当从 fork 运行集群进程时,cluster.isMaster 为 false,因此我们的 app.js 脚本位于当前工作进程。
在我们的应用程序中,当我们调用 server.listen(3000) 时,工作进程将序列化此操作并将请求发送到服务器,服务器检查是否已经在该端口上监听,如果存在,则返回监听器的句柄。否则,服务器将开始在该端口上监听,并将句柄传递给新创建的监听器。
由于所有我们的工作进程都请求监听端口 3000,服务器将在第一个工作进程启动时开始监听该端口,并将相同的处理程序传递给所有工作进程。当请求到来时,它将由任何可以处理并处理该请求的工作进程处理。
由于我们的监控工具(Monit 或 Forever,或其他)现在将仅监控主进程,因此监控工作进程成为主进程的责任。这意味着集群应该重新启动任何意外死亡的工作进程。我们将通过在主进程中添加以下事件处理程序来完成此操作:
cluster.on('exit', function (worker, code, signal){
var exitCode = worker.process.exitCode;
console.log('worker ' + worker.process.pid +
' died ('+exitCode+'). restarting...');
cluster.workers[worker.id].delete();
cluster.fork();
});
通过监听套接字的 exit 事件来监控进程。这是任何工作进程死亡时将触发的事件。事件处理程序将获取工作进程、其退出代码以及导致进程被杀死的信号。在处理程序中,我们记录死亡情况,并使用 cluster.fork() 启动一个新的工作进程。
现在,我们可以启动新的集群应用程序;我们将运行 cluster.js 而不是 app.js。因此,将 package.json 中的 start 脚本更改为运行 cluster.js:
"scripts": {
"start": "node cluster",
}
然后使用 npm 运行应用程序。
npm start
这将启动应用程序,一切看起来都和之前一样。但当你开始使用它时,你会注意到在尝试连接到房间或发送消息时出现了错误。这些错误是因为我们正在使用内存存储来存储 Express.js 会话,而 socket.io 使用内存存储来存储和传输所有消息。
应用程序扩展
在前面的部分中,我们看到了如何对 Node.js 应用程序进行集群化以及它如何由于我们的应用程序机制而受到限制。在其当前状态下,应用程序使用内存存储来保持会话数据。此存储是 Node.js 实例本地的,因此不会在任何其他集群实例中可用。此外,数据将在 Node.js 实例重启时丢失。因此,我们需要一种将会话存储在持久存储中的方法。此外,我们希望配置 socket.io,使其所有实例都使用共享的 pub-sub 和数据存储。Connect 框架有一个扩展机制,因此可以插入新的存储,并且有一个存储既持久又擅长 pub-sub。它是 Redis 会话存储。
Redis (redis.io/) 是一个高性能、分布式、开源的键值存储,也可以用作队列。我们将使用 Redis 和相应的 Redis 存储来提供一个可靠、分布式和共享的存储和 pub-sub 队列。请查看在您的操作系统上安装 Redis 服务器并启动它的说明。
让我们对我们的聊天应用程序做一些更改,从 package.json 开始:
"connect-redis":"*",
"redis":"*"
这将为 Connect/Express.js Redis 存储和 Redis 连接客户端添加支持。让我们首先让 Express.js 使用 Redis;为此,通过以下代码片段编辑 app.js:
var express = require('express')
, routes = require('./routes')
, http = require('http')
, path = require('path')
, connect = require('connect')
, RedisStore = require('connect-redis')(express);
var app = express();
var sessionStore = new RedisStore();
//Existing Code
因此,我们在这里做的两个更改是引入 Redis 会话存储,然后我们可以将会话存储替换为 RedisStore 的实例。这就是 Express 使用 Redis 存储运行所需的所有内容。
下一步,我们需要使用 Redis 来配置 socket.io。因此,让我们编辑 socket.js:
var io = require('socket.io')
, redis = require('redis')
, RedisStore = require('socket.io/lib/stores/redis')
, pub = redis.createClient()
, sub = redis.createClient()
, client = redis.createClient();
exports.initialize = function (server) {
io = io.listen(server);
io.set('store', new RedisStore({
redisPub : pub
, redisSub : sub
, redisClient : client
}));
//Existing Code
}
在前面的代码片段中,我们首先执行的是 require('redis'),它提供了客户端和 socket.io 的 redisStore,后者为 socket.io 提供了 Redis 支持。然后我们创建了三个不同的 Redis 客户端,用于 pub-sub 和数据存储:
io.set('store', new RedisStore({
redisPub : pub
, redisSub : sub
, redisClient : client
}));
在之前的代码片段中,我们配置了 socket.io 使用 Redis 作为队列和数据存储。现在我们可以开始了!现在使用以下命令再次运行应用程序:
npm start
生产中 node 的技巧
这里有一些帮助我们执行 node 在生产中的技巧:
-
在
生产环境中运行服务器。 -
永远不要直接将 node 应用程序暴露在互联网上;总是使用代理。例如 Apache HTTP、Nginx 和 HAProxy 这样的服务器在多年的生产中已经得到了加固和增强,以使其能够抵御各种类型的攻击,特别是 DOS 和 DDOS。Node 是新的;它可能随着时间的推移而变得稳定,但今天不建议将其直接放在前端。
-
永远不要以 root 身份运行 node。嗯,这是对任何应用服务器的建议,也适用于 node。如果我们以 root 身份运行 node,黑客可能会获得 root 访问权限或以 root 身份运行一些有害的代码。所以,永远不要以 root 身份运行它!
-
总是运行多个节点进程。Node 是一个单线程、单进程的应用服务器。应用中的错误可能会导致服务器崩溃。因此,为了可靠性,总是要有多个进程。此外,以 1+ 进程的方式思考,使我们能够在需要时扩展。
-
总是使用监控工具。Monit、Forever、Upstart 选择你喜欢的,但总是使用它。安全比抱歉更好。
-
永远不要在
生产环境中使用MemoryStore;MemoryStore是为开发环境准备的;我建议即使在开发环境中也使用RedisStore。 -
记录所有错误。一切运行顺利,直到它不顺利!当出现问题的时候,日志是你的最佳朋友。尽量在尽可能接近原因的地方捕获异常,并在上下文中记录所有相关信息。不要只记录一些错误消息,要记录所有相关的对象。
-
除非没有其他选择,否则永远不要阻塞。Node 运行在事件循环上,对单个请求的阻塞将导致不必要的开销,并降低所有请求的性能。
-
总是保持你的服务器、node 和所有依赖模块的最新状态。
摘要
在本节中,我们看到了将我们的应用程序部署到生产环境所涉及的工作。我们必须记住,这些并不是唯一的方法。对于我们所做的每一项任务,都有许多其他的方法可以完成,没有一种解决方案适合所有场景。但既然我们已经知道了 生产 环境的期望,我们就可以研究选项,并根据我们的需求选择一个。
附录 A. Socket.IO 快速参考
在本附录中,我们将查看 socket.io 提供的 API。目的是快速浏览所有 API,以便我们知道在工作的过程中是否有函数可以帮助我们。socket.io 正在积极开发中,API 本身也可能发生变化。尽管文档化的方法可能不会改变,但 socket.io 中总是会有新的函数和功能被添加。所以,请始终检查 socket.io 网站和 wiki,以确认所需功能的可用性。
服务器
如我们所知,socket.io 提供了用于服务器和客户端的库。让我们首先看看为服务器提供的 API。
实例化套接字
与其他 node 模块一样,使用require导入模块来实例化socket.io模块:
var io = require('socket.io');
启动 Socket.IO
通过使用listen方法启动 socket.io 服务器组件,该方法将 socket.io 附加到 node HTTP 服务器:
var sio = io.listen(<server>)
在这里,server是 node HTTP 服务器的实例。
监听事件
使用on方法将事件处理程序附加到套接字上。on方法接受事件名称和回调/处理函数作为参数:
sio.on(<event>, function(eventData){
//DO SOMETHING
});
在这里,event是事件的名称,而eventData代表在调用处理程序时传递给处理程序的事件特定数据。
发射事件
我们使用emit方法来触发一个事件。这个事件将在客户端被处理:
socket.emit(<event>, <event_data>, ack_callback);
在这里,event是要触发的事件的名称,event_data是作为 JSON 对象的事件数据,而ack_callback是在客户端成功接收到事件时调用的可选回调函数。
发送消息
使用send方法向客户端发送消息:
socket.send(<message>, ack_callback);
其中message是要发送给客户端的消息,而ack_callback是在客户端成功接收到消息时调用的可选回调函数。
发送 JSON 消息
可以通过在send方法之前使用json标志来发送 JSON 消息:
socket.json.send(<message>, ack_callback);
在这里,message是要发送给客户端的消息,而ack_callback是在客户端成功接收到消息时调用的可选回调函数。
广播消息/事件
可以使用broadcast标志将消息或事件广播到所有已连接的套接字:
socket.broadcast.emit(<event>, <event_data>);
在这里,event是要发射的事件的名称,而event_data是与事件一起发送的 JSON 数据。以下代码行显示了如何广播消息:
socket.broadcast.send(<message>);
在这里,message是要发送给客户端的消息,而ack_callback是在客户端成功接收到消息时调用的可选回调函数。
发送一个易失性消息
有时发送的消息并不重要,如果未送达,则可以忽略。因此,这些方法不需要排队或尝试重新发送。这是通过volatile标志完成的:
socket.volatile.send(<message>);
在这里,message 是将发送给客户端的消息,ack_callback 是在客户端成功接收到消息时调用的可选回调函数。
存储套接字数据
我们可以在套接字上调用 set 方法来存储一些数据在套接字上。这是一个异步方法调用,它需要一个键、值和一个回调函数:
socket.set(<key>, <value>, function(){
//DO SOMETHING
});
这里,key 是此数据的键名,value 是要存储的值。
获取套接字数据
我们使用 get 方法从套接字获取存储的值。这是一个异步方法,它需要一个键和一个回调函数,该回调函数将获取值:
socket.get(<key>, function(value){
//DO SOMETHING
});
在这里,key 是要获取的数据的键,value 是如果与套接字一起存储的值。如果没有存储值,这将是一个 null。
限制到命名空间
我们可以通过使用 of 方法来复用套接字并将消息/事件限制到命名空间,从而实现限制。此方法返回一个套接字,它可以像任何其他套接字一样使用,但消息将被限制为仅连接到此命名空间的客户端:
var namespace_socket = socket.of(<namespace>);
这里,namespace 是我们想要限制套接字的命名空间。
加入房间
我们使用 socket 的 join 方法加入一个房间。如果房间不存在,它将创建一个新的房间:
socket.join(<room>);
在这里,room 是要加入的房间的名称。
在房间中广播消息/事件
我们可以通过使用 broadcast 中的 in 标志向房间中的所有已连接客户端发送消息:
socket.broadcast.in(<room>).send(<message>);
socket.broadcast.in(<room>).emit(<event>, <event_data>);
在这里,room 是要发送消息的房间,message 是要发送的消息,event 是要发出的事件,event_data 是与事件一起发送的数据。
离开房间
使用 leave 方法离开房间。如果套接字正在退出,我们不需要显式地这样做。此外,空房间将自动被销毁:
socket.leave(<room>);
这里,room 是要退出的房间。
更改配置
Socket.io 使用 configure 方法回调处理器的 set 方法进行配置:
io.configure('environment', function () {
io.set(<property>, <value>);
});
在这里,environment 是此配置将使用的可选环境,property 是要设置的属性,value 是属性的值。
服务器事件
我们将在本节中讨论一些与服务器相关的事件。
连接
当与客户端建立初始连接时,会触发此事件:
io.sockets.on('connection', function(socket) {})
在这里,socket 用于与客户端的进一步通信。
消息
当使用 socket.send 发送的消息被接收时,会发出 message 事件:
socket.on('message', function(<message>, <ack_callback>) {})
在这里,message 是发送的消息,ack_callback 是一个可选的确认函数。
断开连接
当套接字断开连接时,会触发此事件:
socket.on('disconnect', function() {})
客户端
在本节中,我们将了解客户端 API。
连接到套接字
我们通过客户端 io 对象上的 connect 方法连接到套接字:
var socket = io.connect(<uri>);
在这里,uri 是要连接的服务器 URI。它可以绝对或相对。如果不是 / 或其绝对等效项,它将连接到命名空间。
监听事件
我们可以使用on方法将事件处理器附加到套接字上:
socket.on(<event>, function(event_data, ack_callback){});
在这里,event是要监听的事件,event_data是事件的数据,ack_callback是在成功接收事件时调用的可选回调方法。
触发事件
我们使用emit方法来触发一个事件。这个事件将在服务器上处理:
socket.on(<event>, <event_data>, ack_callback);
在这里,event是要触发的事件的名称,event_data是事件的 JSON 对象数据,ack_callback是在服务器成功接收消息时调用的可选回调函数。
发送消息
使用send方法向服务器发送消息:
socket.send(<message>, ack_callback);
在这里,message是要发送到服务器的消息,ack_callback是在服务器成功接收消息的可选回调函数。
客户端事件
在本节中,我们将了解一些客户端事件。
connect
当套接字成功连接时,会触发connect事件:
socket.on('connect', function () {})
connecting
当套接字尝试与服务器连接时,会触发connecting事件:
socket.on('connecting', function () {})
disconnect
当套接字断开连接时,会触发disconnect事件:
socket.on('disconnect', function () {})
connect_failed
当 socket.io 由于传输失败或授权失败等原因无法与服务器建立连接时,会触发connect_failed事件:
socket.on('connect_failed', function () {})
error
当发生错误且无法由其他事件类型处理时,会触发error事件:
socket.on('error', function () {})
message
当使用socket.send发送的消息被接收时,会触发message事件:
socket.on('message', function (<message>, <ack_callback>) {})
在这里,message是发送的消息,ack_callback是一个可选的确认函数。
reconnect
当 socket.io 成功重新连接到服务器时,会触发reconnect事件:
socket.on('reconnect', function () {})
reconnecting
当套接字尝试与服务器重新连接时,会触发reconnecting事件:
socket.on('reconnecting', function () {})
reconnect_failed
当 socket.io 在连接断开后无法重新建立有效连接时,会触发reconnect_failed事件:
socket.on('reconnect_failed', function () {})
附录 B. Socket.IO 后端
Socket.io 始于 Node.js,其主后端仍然是 Node.js。本书专注于使用 socket.io、Node.js 和 Express.js 构建聊天系统。但如果你的首选平台不是 Node.js,或者你正在从事一个希望获得与 socket.io 相同功能但无法实现的项目,因为你有一个现有的标准化平台并且不能引入新的系统。在你之前,许多人面临过同样的困境,本着开源的精神,socket.io 服务器适用于各种平台。在本附录中,让我们看看 socket.io 后端的各种实现。
每个平台都将要求你应用本书中的学习和逻辑来重写针对该平台的服务器端代码。客户端代码可以保持不变。
以下是一个按语言/平台字母顺序排列的实现列表:
Erlang
在 erlang 上,socket.io 有两个不同的后端,Yurii Rashkovskii 的socket.io-erlang (github.com/yrashk/socket.io-erlang) 和 Yongboy 的 erlang-socketio (code.google.com/p/erlang-scoketio/)。
Yurii 似乎对 socket.io 0.6.x 之后的版本所采取的道路存在分歧,因此该库仅支持规范的第 0.6 版。自然地,本书中的大多数示例以及互联网上的许多其他示例,都无法在它上面运行。
Yongboy 的 erlang-socketio 似乎在持续关注 socket.io 的最新动态,并且在撰写本文时与 socket.io-1.0 的最新规范兼容。因此,我们将在本节的剩余部分专注于这个库。
这个库适用于Cowboy和Mochiweb,这两个是 erlang 中流行的服务器端框架。这两个版本都支持 socket.io 规范 1.0。Cowboy 版本支持所有传输方式,而 Mochiweb 版本仅限于xhr-polling、htmlfile和json-polling。
Google Go
Go是一种处于早期阶段的语言,但正在获得越来越多的关注,这主要得益于 Google 的企业支持,并且它是 Google App Engine 上支持的三种语言之一,除了 Python 和 Java。
go-socket.io实现为 Go 提供了 socket.io 支持。该项目支持几乎所有传输方式,并且也支持 Google App Engine 上的 socket.io。该项目的原始代码库位于github.com/madari/go-socket.io,但那里的开发已经停滞了一段时间;但其他人似乎已经接过了接力棒。socket.io 维基指向这个分支:github.com/davies/go-socket.io。
这里需要注意的一点是,这个代码库仍然不支持高于 0.6.x 的版本。
查看 GitHub 上创建的分支,你会发现代码正在进行的有趣开发。就像这个更新较新的分支:github.com/justinfx/go-socket.io。
如果你想要使用 socket.io 的新版本,github.com/murz/go-socket.io这个分支应该支持到 0.8.x 版本(这是在撰写本文时的情况)。
Java
在 Java 服务器上,socket.io 有多种实现方式。让我们来看看它们。
第一个是Socket.IO-Java,在github.com/Ovea/Socket.IO-Java上维护得最为活跃。它已经被分支并修改,以适应各种服务器和平台。
然后是Atmosphere。Atmosphere 最初是一个将服务器推送引入 glassfish 服务器的项目,但后来作为一个独立的项目分离出来,并且几乎与任何 Java 服务器一起工作。Atmosphere 服务器自带 atmosphere.js,这是它自己的 JS 客户端,但任何 Atmosphere 应用都可以与 socket.io 客户端无缝工作,无需任何修改;使用github.com/Atmosphere/atmosphere/wiki/Getting-Started-with-Socket.IO开始使用 Atmosphere。如果你正在启动一个新的 Java 项目或者在你的现有 Java 项目中引入推送,在做出决定之前,一定要看看 Atmosphere。
Netty为 Java 带来了异步服务器;并且非常重要的一点是,Yongboy 的socket io-netty (code.google.com/p/socketio-netty/)。由于其异步特性,Netty 非常适合这些应用,因此它被高度推荐。
Gisio (bitbucket.org/c58/gnisio/wiki/Home)将 socket.io 引入了 GWT 框架,这是谷歌的 Java 编写和编译为 JS 的库。如果你的应用是用 GWT 构建的,并且你想要在你的应用中引入服务器推送,你可以使用这个库。
对于新出现的完全异步服务器Vert.x,有mod-socket-io (github.com/keesun/mod-socket-io)。再次提醒,如果你正在寻找高度异步应用的应用,我建议你看看这个服务器和这个模块。
Perl
Perl 可能是一个非常古老的语言,但仍然在许多地方被使用,并且有一个名为pocketio (github.com/vti/pocketio)的活跃维护的 socket.io 服务器模块。
Python
Python 是另一种正在获得广泛接受和流行的语言。Python 也有多个 socket.io 服务器实现。
我们首先来看的是gevent-socket.io (github.com/abourget/gevent-socketio),它与任何基于 WSGI 的 Web 框架兼容。所以如果你使用的是任何框架,如 Pyramid、Pylons、Flask 和 Django,这将适用于你。唯一的依赖项是gevent和gevent-websocket。
如果 Tornado 是你的首选框架,请查看Tornadio 2 (github.com/MrJoes/tornadio2),它提供了对 socket.io 0.7 及以上版本的支持。再次强调,Tornado 是一个异步框架,非常适合此类应用。
专注于将 socket.io 引入 Django 的是django-socketio (github.com/stephenmcd/django-socketio)。
摘要
在本章中,我们看到了一些流行平台的 socket.io 后端实现。如果你使用的是其他平台,只需在互联网上搜索 socket.io 服务器实现,我确信你一定能找到。它可能不是最好的,也可能不是处于理想状态,但你肯定能找到一个开始的方法。


浙公网安备 33010602011771号