教你使用Websockets和Go编程语言构建实时聊天应用程序

现代网页应用程序正日趋丰富而复杂。像这样有趣又有活力的体验很受用户欢迎。用户无需向服务器发起调用,或刷新浏览器,就可以让页面实时更新。早期的开发者依赖 AJAX 来创建具备近乎实时体验的应用程序。而现在,他们运用 WebSockets 就能创建完全实时的应用程序了。

本教程中我们将使用 Go 编程语言以及 WebSockets 来创建一个实时的聊天应用程序。前端将会使用 HTML5 和 VueJS 来编写。该内容需要你对 Go 语言, JavaScript 以及 HTML5 有一个基础的了解,最好有一点点使用 VueJS 的经验。

WebSocket 是什么?

通常 Web 应用使用一个或多个请求对 HTTP 服务器提供对外服务。客户端软件通常是 Web 浏览器向服务器发送请求,服务器发回一个响应。响应通常是 HTML 内容,由浏览器来渲染为页面。样式表,JavaScript 代码和图像也可以在响应中发送回来以完成整个网页。每个请求和响应都属于特定的单独的连接的一部分,像 Facebook 这样的大型网站为了渲染单个页面实际上可以产生数百个这样的连接。

AJAX 的工作方式跟这个完全相同。使用 JavaScript,开发人员可以向 HTTP 服务器请求一小段信息,然后根据响应更新部分页面。这可以在不刷新浏览器的情况下完成,但仍然存在一些限制。

每个 HTTP 请求/响应的连接在被响应之后都会关闭,因此获得任何新的信息必须新建另一个连接。如果没有新的请求发送给服务器,它就不知道客户端正在查找新的信息。能让 AJAX 应用程序看起来像实时的一种技术是定时循环发送 AJAX 请求。在设置了时间间隔之后,应用程序可以重新将请求发送到服务器,以查看是否有任何更新需要反馈给浏览器。这比较适合小型应用程序,但并不高效。这时候 WebSockets 就派上用场了。WebSockets 是由 Internet 工程任务组(IETF)创建的建议标准的一部分。 RFC6455 中详细描述了 WebSockets 实现的完整技术规范。下面是该文档定义 WebSocket 的节选:

WebSocket 协议用于客户端代码和远程主机之间进行通信,其中客户端代码是在可控环境下的非授信代码

换句话说,WebSocket 是一个总是打开的连接,允许客户端和服务器自发地来回发送消息。服务器可在必要时将新信息推送到客户端,客户端也可以对服务器执行相同操作。

JavaScript 中的 WebSockets

大多数现代浏览器都在其 JavaScript 实现中支持 WebSockets。要从浏览器中启动一个 WebSocket 连接,你可以使用简单的 WebSocket JavaScript 对象,如下:

您唯一需要的参数是一个 URL,WebSocket 连接可通过此 URL 连接服务器。该请求实际是一个 HTTP 请求,但为了安全连接我们使用“ws://”或“wss://”。这使服务器知道我们正在尝试创建一个新的 WebSocket 连接。之后服务器将“升级”该客户端和服务之间的连接到永久的双向连接。一旦新的 WebSocket 对象被创建,并且连接成功创建之后,我们就可以使用“send()”方法发送文本到服务器,并在 WebSocket 的“onmessage”属性上定义一个处理函数来处理从服务器发送的消息。具体逻辑会在之后的聊天应用程序代码中解释。

Go 中的 WebSockets

WebSockets 并不包含在 Go 标准库中,但幸运的是有一些不错的第三方包让 WebSockets 的使用轻而易举。在这个例子中,我们将使用一个名为“gorilla/websocket”的包,它是流行的 Gorilla Toolkit 包集合的一部分,多用于在 Go 中创建 Web 应用程序。请运行以下命令进行安装:

构建服务器

这个应用程序的第一部分是服务器。这是一个处理请求的简单 HTTP 服务器。它将为我们提供 HTML5 和 JavaScript 代码,以及建立客户端的 WebSocket 连接。另外,服务器还将跟踪每个 WebSocket 连接并通过 WebSocket 连接将聊天信息从一个客户端发送到所有其他客户端。首先创建一个新的空目录,然后在该目录中创建一个“src”和“public”目录。在“src”目录中创建一个名为“main.go”的文件。搭建服务器首先要进行一些设置。我们像所有 Go 应用程序一样启动应用程序,并定义包命名空间,在本例中为“main”。接下来我们导入一些有用的包。 “log”和“net/http”都是标准库的一部分,将用于日志记录并创建一个简单的 HTTP 服务器。最终包“http://github.com/gorilla/websocket”将帮助我们轻松创建和使用 WebSocket 连接。

下面的两行代码是一些全局变量,在应用程序的其它地方会被用到。全局变量的实践较差,不过这次为了简单起见我们还是使用了它们。第一个变量是一个 map 映射,其键对应是一个指向 WebSocket 的指针,其值就是一个布尔值。我们实际上并不需要这个值,但使用的映射数据结构需要有一个映射值,这样做更容易添加和删除单项。

第二个变量是一个用于由客户端发送消息的队列,扮演通道的角色。在后面的代码中,我们会定义一个 goroutine 来从这个通道读取新消息,然后将它们发送给其它连接到服务器的客户端。

接下来我们创建一个 upgrader 的实例。这只是一个对象,它具备一些方法,这些方法可以获取一个普通 HTTP 链接然后将其升级成一个 WebSocket,稍后会有相关代码介绍。

最后我们将定义一个对象来管理消息,数据结构比较简单,带有一些字符串属性,一个 email 地址,一个用户名以及实际的消息内容。我们将利用 email 来展示 Gravatar 服务所提供的唯一身份标识。

由反引号包含的文本是 Go 在对象和 JSON 之间进行序列化和反序列化时需要的元数据。

Go 应用程序的主要入口总是 "main()" 函数。代码非常简洁。我们首先创建一个静态的文件服务,并将之与 "/" 路由绑定,这样用户访问网站时就能看到 index.html 和其它资源。在这个示例中我们有一个保存 JavaScript 代码的 "app.js" 文件和一个保存样式的 "style.css" 文件。

我们想定义的下一个路由是 "/ws",在这里处理启动 WebSocket 的请求。我们先向处理函数传递一个函数的名称,"handleConnections",稍后再来定义这个函数。

下一步就是启动一个叫 "handleMessages" 的 Go 程序。这是一个并行过程,独立于应用和其它部分运行,从广播频道中取得消息并通过各客户端的 WebSocket 连接传递出去。并行是 Go 中一项强大的特性。关于它如何工作的内容超出了这篇文章的范围,不过你可以自行查看 Go 的官方教程网站。如果你熟悉 JavaScript,可联想一下并行过程,作为后台过程运行的 Go 程序,或 JavaScript 的异步函数。

最后,我们向控制台打印一个辅助信息并启动 Web 服务。如果有错误发生,我们就把它记录下来然后退出应用程序。

接下来我们创建一个函数处理传入的 WebSocket 连接。首先我们使用升级的 "Upgrade()" 方法改变初始的 GET 请求,使之成为完全的 WebSocket。如果发生错误,记录下来,但不退出。同时注意 defer 语句,它通知 Go 在函数返回的时候关闭 WebSocket。这是个不错的方法,它为我们节省了不少可能出现在不同分支中返回函数前的 "Close()" 语句。

接下来把新的客户端添加到全局的 "clients" 映射表中进行注册,这个映射表在早先已经创建了。

最后一步是一个无限循环,它一直等待着要写入 WebSocket 的新消息,将其从 JSON 反序列化为 Message 对象然后送入广播频道。然而 "handleMessages()" Go 程序就能把它送给连接中的其它客户端。

如果从 socket 中读取数据有误,我们假设客户端已经因为某种原因断开。我们记录错误并从全局的 “clients” 映射表里删除该客户端,这样一来,我们不会继续尝试与其通信。

另外,HTTP 路由处理函数已经被作为 goroutines 运行。这使得 HTTP 服务器无需等待另一个连接完成,就能处理多个传入连接。

服务器的最后一部分是 "handleMessages()"函数。这是一个简单循环,从“broadcast”中连续读取数据,然后通过各自的 WebSocket 连接将消息传播到所以客户端。同样,如果写入 Websocket 时出现错误,我们将关闭连接,并将其从“clients” 映射中删除。

构建客户端

如果没有漂亮的 UI,聊天应用程序将无法完成。 我们需要使用一些 HTML5 和 VueJS 来创建一个简单、干净的界面,再利用一些诸如 Materialize CSS 和 EmojiOn 的库来生成一些样式和表情符号。 在“public”目录中,创建一个名为“index.html”的新文件。第一部分很基础。为了美观,我们也会放入一些样式表和字体。“style.css”是自定义的样式表,用于自定义一些内容。

下一部分仅与接口相关,其中只包含一些用于选择用户名、发送消息和显示新的聊天信息的字段。与 VueJS 交互的细节超出本文的介绍范围,你可阅读此文档了解更多。

最后一步只需要导入所有需要的 JavaScript 库,包括 Vue、EmojiOne、jQuery 和 Materialize。我们需要 MD5 库获取来自 Gravatar 的头像 URL,这用 JavaScript 代码写出来就好理解了。最后导入 "app.js"。

然后在 "public" 目录下创建一个 "style.css" 文件。其中会放入一些样式。

客户端的最后一部分是 JavaScript 代码。在 "public" 目录下创建文件 "app.js"。

对于 VueJS 应用程序来说,一开始都是创建新的 Vue 对象。我们将它与 id 为 "#app" 的 div 绑定。这会让 div 内的所有东西与 Vue 实现共享作用域。下面定义一些变量。

Vue 提供了一个叫 "created" 的属性,这是一个函数,会在 Vue 实例刚刚创建时调用。这里非常适合对应用做一些设置工作。在这个示例中我们希望创建一个新的 WebSocket 连接与服务器连接,并创建一个处理器用于处理从服务器发送过来的消息。我们把新的 WebSocket 对象保存在 "data" 属性的 "ws" 变量中。

"addEventListener()"方法接受一个用于处理传入消息的函数。我们期望所有消息都是 JSON 字符串,以便统一解析为一个对象字面量。然后我们可以用各个属性和 avater 头像一起组成漂亮的消息行。"gravatarURL()" 方法会在后面详述。我们用了一个叫 EmojiOne 的表情库来解析emoji 代码。"toImage()" 方法会把 emoji 代码转换为实际的图片。比如,如果你输入 ":robot:",它会被替换为一个机器人 emoji 表情图。

"methods" 属性可以定义各种函数,我们会在 VueJS 应用中使用这些函数。"send"方法用于向服务器发送消息。我们先确保消息不是空的,然后把消息组织成一个对象,再用"stringify"把它变成 JSON 字符串,以便服务器能正确解析。我们使用 jQuery 来处理传入消息中 HTML 和 JavaScript 中的特殊字符,以防止各种类型的注入攻击。

"join"函数会确保用户在发送消息前输入 email 地址和用户名。一旦他们输入了这些信息,我们将 joined 设置为 "true",同时允许他们开始交谈。同样,我们会处理 HTML 和 JavaScript 的特殊字符。

最后一个函数是一个很好的辅助函数,用于从 Gravatar 获取头像。URL 的最后一段需要用户的 email 地址的 MD5 编码。MD5 是一种加密算法,它能隐藏 email 地址同时还能让 email 地址作为一个唯一标识来使用。

运行应用程序

要运行该应用程序,请打开控制台窗口并确保进入应用程序的“src”目录中,然后运行以下命令。

接下来打开 Web 浏览器并导航到“http://localhost:8000”站点。 然后就会显示聊天屏幕,你可以在聊天屏幕中输入电子邮件和用户名。

如果要查看该应用多个用户之间的通信方式,只需另外打开一个浏览器标签页或窗口,然后导航到“http://localhost:8000”。 输入不同的电子邮件和用户名。然后轮流从两个窗口发送消息,这样就可以看到多个用户之间的通信方式了。

结论

这只是一个基本的聊天应用程序,你可以在此基础上进行更多的改进,添加更多的其他功能,并上传源代码,期待你能实现新用户加入或者离开聊天时的私人提醒或者通知。尽情创造吧,此处不设限!我希望这篇文章对你有所帮助,并希望借此启发你使用 WebSockets 和 Go 开始创建自己的实时应用程序。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容