利用 Node.js 搭建一个简单聊天室

前言

终于写下第三篇啦!本篇将利用 Node.js 实现中转服务器,在 iOS 上实现一个简单的聊天室。

本文是 iOS 程序员的 JavaScript 之旅 系列的第三篇,这个系列主要用于记录我在学习 JavaScript 中的一点心得,文中内容由于参考文献版本不一定是最新 & 个人理解水平有限,可能出现一些错误,还请谅解。如果有任何讨论 / 建议 / 意见,欢迎评论留言或是邮件联系。

需求分析

聊天室顾名思义即是一个聊天的地方,普遍的场景自然是房间内的一个用户发送消息,所有在房间内用户都收到对应的消息,这个过程就涉及到两个步骤:

  1. 发送消息的用户将消息发送至服务器。
  2. 服务器将该消息广播给在聊天室内的所有用户。

HTTP?

基于 HTTP 协议进行数据传输是第一反应,将消息封装成一个 HTTP 请求发送至服务器似乎是一个不错的选择,但 HTTP 协议的连接更多是基于“任务请求”式的,即便是 HTTP1.1 之后加入了 keep-alive 的连接方式,HTTP 请求依然是严格遵循“一个 request,一个 response”的模式,而且服务器不能主动向客户端推送消息,尽管可以使用轮询的方式来模拟服务器推送,但这样的实现方式显然不够好。

WebSocket

本例中,WebSocket 将是更好的技术方案,WebSocket 的建立是基于 HTTP 协议的,但当服务端和客户端都切换到 WebSocket 模式后,其工作过程就和 HTTP 没有任何关系了,WebSocket 支持任意方作为数据发送方,因此服务端可以主动推送消息给客户端,同时其消息传输没有一一对应的要求,因此一方可以同时发送多条消息,而另一方并不一定要发送回应。更多关于 HTTP、WebSocket 的异同,我推荐你看看这个

动手实现

本文执笔时,使用的编辑器是 VSCode 1.11.2,node 版本为 v7.8.0,Xcode 版本 8.3,Swift 版本 3.0.2。

Server Side

不得不说,习惯了在 iOS 上写个 demo 都要连几个 outlet,初始化一些界面元素这样麻烦的前期工作,如果想在真机上跑,还要弄开发者账号、证书等一堆东西,起手写 node 的体验实在是太好,VSCode 内建 node 编译环境,只需要创建 .js 后缀文件,直接运行,你的第一个 Node.js 程序就这样跑起来了。

本文中所用到的第三方库,均是通过 Google 搜索得到的结果,如果你阅读本文后,想在本文的基础上添加一些新的功能却无从下手,请记得:Google is your best friend.
WebSocket 服务端,我选择的是 nodejs-websocket,更大众化的选择是 ws,但我已经不记得为什么我选择了前者了 :( ,anyway ,it gets the job done !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var ws = require('nodejs-websocket')

//WEBSOCKET
var wsServer = ws.createServer(function _onConnect(conn) {
console.log("connection established.")

conn.on('text', function _getNewChatMessage(msg) {
console.log('New message : ' + msg)
wsServer.broadcast(msg)
});
conn.on('close', function _connectClosed(code, reason) {
console.log('connection closed.');
});
conn.on('error', function (err) {
if (err.code !== 'ECONNRESET') {
// Ignore ECONNRESET and rethrow anything else
throw err
}
})
}).listen(8081);

wsServer.broadcast = function _broadcast(data) {
wsServer.connections.forEach(function _forEachConnection(conn) {
if (conn.OPEN) {
conn.send(data);
}
});
}

首行的 require 方法,类似 iOS 中的 import,是为了引入其它文件中的方法。但与 oc/swift 不同的是,这里并没有头文件,或是 public,private 等概念,因此引入依赖的机制和 iOS 中不太一样,具体可以看看 这里的内容。接下来就是利用 WebSocket 库,创建服务器、定义连接事件、指定监听接口以及实现广播方法,again,当你遇到问题的时候,谷歌就是你最好的朋友。

不管你信不信,但服务端的核心代码已经写好了,现在只需要运行 js 文件,并在你的客户端上用 WebSocket 协议连接 http://youripaddress:8081 就可以了!

在调试服务端代码的时候,我们往往需要验证从客户端-服务端-客户端的消息传递流程,而用 iOS 写一个客户端的 demo 实在是有些复杂(又要配 AppTransport Security,又要找第三方 WebSocket 库,UI 又很花时间,更不用提你还需要两台设备来测试收发消息了),而你又是一个熟悉 js 的好同志 - 为什么不写个简单的 HTML,用 js 来实现客户端呢?毕竟现在,最重要的就是快速验证服务端逻辑。

Client Side

确保服务端逻辑无误后,就可以开始客户端的开发了。除了工作项目外,我已经很少使用 Objective-C 来进行开发了,使用 Swift 这样更简洁、高级的语言不但能更快速建立起原型,也同样能反作用于日常的 oc 开发,让你的 oc 代码更“年轻”和安全。在本例的开发中,我使用的是 SwiftWebSocket,类似的库非常多,当然你也可以选择自己实现。接下来贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class RequestManager{

//MARK: Singleton
static private let mgr = RequestManager() //class stored property not support
open class var `default` : RequestManager{
get{
return mgr
}
}

//MARK: Properties
fileprivate let ip = "172.26.41.2"
fileprivate let chatPort = "8081"
fileprivate var ws : WebSocket!
//MARK: Life Cycle
private init(){}
}

extension RequestManager{ //WebSocket
func connectToServer(
name:String,
open:@escaping ()->() = {},
close:@escaping (Int,String,Bool)->() = {_,_,_ in},
error:@escaping (Error)->() = {_ in},
message:@escaping (Any)->() = {_ in}
){
if ws == nil {
ws = WebSocket()
}
ws.close()
let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "IllegalName"
ws.open("ws://\(self.ip):\(self.chatPort)/?username=\(encodedName)")

ws.event.open = open
ws.event.error = error
ws.event.close = close
ws.event.message = message
}

func disconnect(){
ws?.close()
}

func send(text msg:String){
ws?.send(text: msg)
}
}

看到 Swift 的代码你会发现,这里看起来和服务端的 js 非常相似,消息的传递均是通过对事件的监听,就连语法好像都差不多。事实上,如果 JavaScript 加上类型定义(是的我说的就是 TypeScript),你会发现它和 Swift 是多么地相像。RequestManager 所实现的功能非常简单,提供连接服务器的接口,让调用方传入事件监听的闭包,并提供发送消息、断开连接的接口。这里传入的 name 参数,是因为实际上我还在服务端加入了一个用户名去重的逻辑,但这部分功能和 WebSocket 本身没有太大关系,不影响整个聊天室的正常使用。

不管你信不信,但客户端的网络连接部分也做好了,接下来的 UI 搭建、调用逻辑,就十分简单了。

总结

其实本篇内容十分简单,服务端使用 nodejs-websocket 库,几句代码就实现了支持 WebSocket Server 的创建,客户端使用 SwiftWebSocket 也非常轻松地实现了与服务端的连接、交互。完整的项目还加入了基于 HTTP 的聊天室登录、用户名唯一性检查功能,代码可以在 这里 下载。