欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

WebSocket 有大用

WebSocket

阅读: 1444128

WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。

为什么传统的HTTP协议不能做到WebSocket实现的功能?这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。

这样一来,要在浏览器中搞一个实时聊天,在线炒股(不鼓励),或者在线多人游戏的话就没法实现了,只能借助Flash这些插件。

也有人说,HTTP协议其实也能实现啊,比如用轮询或者Comet。轮询是指浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。这个机制的缺点一是实时性不够,二是频繁的请求会给服务器带来极大的压力。

Comet本质上也是轮询,但是在没有消息的情况下,服务器先拖一段时间,等到有消息了再回复。这个机制暂时地解决了实时性问题,但是它带来了新的问题:以多线程模式运行的服务器会让大部分线程大部分时间都处于挂起状态,极大地浪费服务器资源。另外,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”。

以上两种机制都治标不治本,所以,HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。

WebSocket协议

WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。

首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

该请求和普通的HTTP请求有几点不同:

  1. GET请求的地址不是类似/path/,而是以ws://开头的地址;

  2. 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接;

  3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;

  4. Sec-WebSocket-Version指定了WebSocket的协议版本。

随后,服务器如果接受该请求,就会返回如下响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。

现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。

为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。

安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。

浏览器

很显然,要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx的请求。目前,支持WebSocket的主流浏览器如下:

  • Chrome

  • Firefox

  • IE >= 10

  • Sarafi >= 6

  • Android >= 4.4

  • iOS >= 8

服务器

由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的WebSocket实现,我们直接用npm安装使用即可。

评论

  • 好奇

    我是--只小小鸟 created at January 16, 2018 10:19 AM, Last updated at January 16, 2018 10:19 AM

    在线炒股为什么不鼓励

  • 峰哥。问一个问题。

    王腾蛟 created at July 18, 2017 12:05 AM, Last updated at July 18, 2017 12:05 AM

    我的项目按照写个。测试也联通了。是一个关于交响乐的私人项目。里面包含一个网页播放器页面。一个专辑页面。现在遇到一个问题。点击专辑页面的播放按钮后如何在后台将这个推到播放器页面。由于跨越了view。我想了很多办法。多线程。异步回调等等都没有解决问题。就想问问峰哥有什么好的建议或者方法。

  • 用javaEE实现的websocket

    crossoverJie created at September 5, 2016 12:53 PM, Last updated at May 19, 2017 5:13 PM

    有做javaEE的童鞋可以看下我用java实现的websocket

    • Created at September 10, 2016 1:26 PM

      厉害啊 ,学习了

    • Created at December 9, 2016 3:12 PM

      厉害厉害,刚好java socket这块一直懵懵懂懂,ssm倒是常用,学起来应该没多大问题,感谢分享

    • Created at December 12, 2016 11:49 AM

      您好 廖老师 谢谢您的分享。 在原文中——"首先,"wss:// xxx创建WebSocket连接时," 是不是应该为 ws:// 谢谢

    • Created at May 19, 2017 5:13 PM

      上面那位,仔细看教程,廖老师说的是安全WebSocket连接


来自 https://www.liaoxuefeng.com/wiki/1022910821149312/1103303693824096


使用ws

阅读: 505028

要使用WebSocket,关键在于服务器端支持,这样,我们才有可能用支持WebSocket的浏览器使用WebSocket。

ws模块

在Node.js中,使用最广泛的WebSocket模块是ws,我们创建一个hello-ws的VS Code工程,然后在package.json中添加ws的依赖:

"dependencies": {
    "ws": "1.1.1"
}

整个工程结构如下:

hello-ws/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- app.js <-- 启动js文件
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包

运行npm install后,我们就可以在app.js中编写WebSocket的服务器端代码。

创建一个WebSocket的服务器实例非常容易:

// 导入WebSocket模块:
const WebSocket = require('ws');

// 引用Server类:
const WebSocketServer = WebSocket.Server;

// 实例化:
const wss = new WebSocketServer({
    port: 3000
});

这样,我们就在3000端口上打开了一个WebSocket Server,该实例由变量wss引用。

接下来,如果有WebSocket请求接入,wss对象可以响应connection事件来处理这个WebSocket:

wss.on('connection', function (ws) {
    console.log(`[SERVER] connection()`);
    ws.on('message', function (message) {
        console.log(`[SERVER] Received: ${message}`);
        ws.send(`ECHO: ${message}`, (err) => {
            if (err) {
                console.log(`[SERVER] error: ${err}`);
            }
        });
    })
});

connection事件中,回调函数会传入一个WebSocket的实例,表示这个WebSocket连接。对于每个WebSocket连接,我们都要对它绑定某些事件方法来处理不同的事件。这里,我们通过响应message事件,在收到消息后再返回一个ECHO: xxx的消息给客户端。

创建WebSocket连接

现在,这个简单的服务器端WebSocket程序就编写好了。如何真正创建WebSocket并且给服务器发消息呢?方法是在浏览器中写JavaScript代码。

先在VS Code中执行app.js,或者在命令行用npm start执行。然后,在当前页面下,直接打开可以执行JavaScript代码的浏览器Console,依次输入代码:

// 打开一个WebSocket:
var ws = new WebSocket('ws://localhost:3000/test');
// 响应onmessage事件:
ws.onmessage = function(msg) { console.log(msg); };
// 给服务器发送一个字符串:
ws.send('Hello!');

一切正常的话,可以看到Console的输出如下:

MessageEvent {isTrusted: true, data: "ECHO: Hello!", origin: "ws://localhost:3000", lastEventId: "", source: null…}

这样,我们就在浏览器中成功地收到了服务器发送的消息!

如果嫌在浏览器中输入JavaScript代码比较麻烦,我们还可以直接用ws模块提供的WebSocket来充当客户端。换句话说,ws模块既包含了服务器端,又包含了客户端。

wsWebSocket就表示客户端,它其实就是WebSocketServer响应connection事件时回调函数传入的变量ws的类型。

客户端的写法如下:

let ws = new WebSocket('ws://localhost:3000/test');

// 打开WebSocket连接后立刻发送一条消息:
ws.on('open', function () {
    console.log(`[CLIENT] open()`);
    ws.send('Hello!');
});

// 响应收到的消息:
ws.on('message', function (message) {
    console.log(`[CLIENT] Received: ${message}`);
}

在Node环境下,ws模块的客户端可以用于测试服务器端代码,否则,每次都必须在浏览器执行JavaScript代码。

同源策略

从上面的测试可以看出,WebSocket协议本身不要求同源策略(Same-origin Policy),也就是某个地址为http://a.com的网页可以通过WebSocket连接到ws://b.com。但是,浏览器会发送Origin的HTTP头给服务器,服务器可以根据Origin拒绝这个WebSocket请求。所以,是否要求同源要看服务器端如何检查。

路由

还需要注意到服务器在响应connection事件时并未检查请求的路径,因此,在客户端打开ws://localhost:3000/any/path可以写任意的路径。

实际应用中还需要根据不同的路径实现不同的功能。

参考源码

hello-ws

评论

  • 如何获取websocket 的数据

    用户7127716090 created at May 21, 2019 9:57 PM, Last updated at July 2, 2019 3:38 PM

    老师我这边已经和后端握手成功了,控制台能输出后端的数据。但是我想把获取的数据传给其他函数要怎么做。我试了下赋值给全局变量没有反应

    • Created at July 2, 2019 3:38 PM

      socket后台返回的数据 赋值给全局变量是可以的呀,也能当做参数传递给其他函数啊

  • 浏览器内测试代码

    VincentAtlas created at March 12, 2019 2:13 PM, Last updated at March 12, 2019 2:13 PM

    浏览器内使用原生websocket接口的测试如下:

                let count = 0;
    
                let ws = new WebSocket('ws://localhost:3000/ws/chat');
    
                ws.onopen =  function () {
                    console.log(`[CLIENT] open()`);
                    ws.send('Hello!');
                };
    
                ws.onmessage = function (message) {
                    console.log(`[CLIENT] Received: ${message}`);
                    count++;
                    if (count > 3) {
                        ws.send('Goodbye!');
                        ws.close();
                    } else {
                        setTimeout(() => {
                            ws.send(`Hello, I'm Mr No.${count}!`);
                        }, 1000);
                    }
                };
    
  • socket连接的时候能传值到后台吗?

    视觉码农 created at December 6, 2018 11:10 AM, Last updated at December 6, 2018 1:51 PM

    比如用户登录成功的时候,我会连接上socket服务器,我需要在后台记录当前登录的用户id,所以能不能在websocket连接的时候就能通过req获取到这个用户id

  • 你好 上面的实例一定要使用 VS Code 来开发吗?

    我要带你点燃圣所的光芒 created at August 28, 2018 2:26 PM, Last updated at August 28, 2018 2:26 PM

    你好 上面的实例一定要使用 VS Code 来开发吗?

  • ws.on is not a function?

    Gingbery created at August 17, 2017 4:16 PM, Last updated at March 13, 2018 6:47 PM

    如题,按照教程中的代码写法会报错,但是写成 ws.onopen =function(){} 又完全没效果

    • Created at August 27, 2017 9:56 AM

      教材中的API是ws这个包的。在浏览器上用的话是浏览器实现的websockets协议,按照W3C规范。参考这个http://www.runoob.com/html/html5-websocket.html

    • Created at March 13, 2018 6:47 PM

      嗯嗯,这个问题我也困惑了一阵,看了回复清晰些了,on()这个是ws包的,是为了便于测试的;onopen()是在编写浏览器客户端时的写法。所以有ws.on is not a function,改成onopen以后我这边就正常了。

  • 运行npm install后,我们就可以在app.js中编写WebSocket的服务器端代码。

    东东男装 created at October 19, 2017 12:01 PM, Last updated at October 19, 2017 1:00 PM

    这一步什么意思,怎么操作直接带过? Error: Cannot find module 'ws'

    • Created at October 19, 2017 1:00 PM

      解决问题,在终端要安装下包 node_modules>npm install --save ws json代码里这么写 { "dependencies": { "ws": "^3.2.0" } }

  • Chrome报错

    Hellnola_5 created at September 29, 2017 12:53 AM, Last updated at September 29, 2017 12:53 AM

    第一个测试“先在VS Code中执行app.js,或者在命令行用npm start执行。然后,在当前页面下,直接打开可以执行JavaScript代码的浏览器Console,依次输入代码:”Chrome报错为

    <code>conn = new WebSocket('ws://localhost:3000');</code> <code>WebSocket {url: "wss://localhost:3000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null, …} </code> <code>VM557734:1 WebSocket connection to 'wss://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_CLOSED</code> <code>(anonymous) @ VM557734:1</code>

    或者 <code>conn = new WebSocket('wss://localhost:3000');</code> <code>WebSocket {url: "wss://localhost:3000/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null, …}</code> <code>VM559283:1 WebSocket connection to 'wss://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_CLOSED</code>

    更换为Firefox修改了extension的设置居然就没有报错。 不知道是不是Chrome的SSL安全策略阻止了请求。

  • WebSocket打不开localhost:3000,页面提示Upgrade Required

    peTErwUxc created at September 1, 2017 4:57 PM, Last updated at September 1, 2017 4:57 PM

    WebSocket打不开localhost:3000,页面提示Upgrade Required,response,status返回426.请问该怎么解决!求解答!感谢!

    • Created at September 2, 2017 10:56 AM

      我发现了。误解了老师的意思,并不是打开localhost:3000,而是在当前页面操作。还有老师现在的网址是https,如果

      var ws = new WebSocket('ws://localhost:3000/test');
      

      Chrome会自动替换成'wss://localhost:3000/test' ,就会报错。所以找一个http的网址测试才可以。

  • 此页面控制台键入 var ws = new WebSocket('ws://localhost:3000/test') 报错

    He-Shawn created at August 16, 2017 3:35 PM, Last updated at August 16, 2017 3:35 PM

    WebSocket connection to 'wss://localhost:55668/test' failed: Error in connection establishment: net::ERR_CONNECTION_CLOSED 一看是 wss! 新页面控制台测试正常。

  • 教程的ws版本太低了。。

    徐志要搏出位- created at June 3, 2017 2:32 PM, Last updated at June 3, 2017 2:32 PM

    1.1.1的ws在macOSX Sierra上会有bug。。 面向ss编程了半天,一直以为代码哪里出错了。 最后去github看了ws的最新版本3.0.0了,更新之后就ok了。。

  • wss.on(connection, ) 与 ws.on(open); 是自动触发的,也即新建就立即执行了

  • var ws=new WebSocket('ws://localhost:3000/test'); ws.onopen=function () { console.log([CLIENT] open()); ws.send('Hello!'); }; ws.onmessage = function(msg) { console.log(msg); };

  • 如何新建vscode工程啊?

    随风0803 created at April 28, 2017 6:59 PM, Last updated at May 2, 2017 11:31 AM

    如何新建一个vscode工程啊?app.js、package.json都是如何生成的啊?

    • Created at May 1, 2017 7:41 PM

      新建目录,然后自己创建app.js, package.json,用vscode打开目录就是打开工程

    • Created at May 2, 2017 11:31 AM

      嗯,已经搞定了,npm init生成的package.json,新手不熟悉,还是多谢老师了!

  • 在客户端控制台哪里写的代码是不是少了一个onopen事件,

    iFayeee created at September 12, 2016 10:57 PM, Last updated at March 31, 2017 4:06 PM

    我说怎么报错:

    Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.(…)
    

    加上就成功了

    • Created at March 30, 2017 3:44 PM

      我觉得不是onopen的问题,onopen()只是用来设置当ws打开时执行的回调函数,是可选的。你是整段代码复制到console执行的吧,那样的话可能websocket还没连接成功就执行到send()导致这个错误。试试一句一句的复制到控制台执行,应该就不会报错了,除非你是真的跨机器调试而网络又很差。

    • Created at March 31, 2017 4:06 PM

      依次输入代码,否则连接还没建立就发不了消息

      正式写法必须有on('open', ...)

  • ws.js兼容性如何?

    nauYieM9406 created at September 19, 2016 4:08 PM, Last updated at September 19, 2016 4:08 PM

    ws.js兼容性如何?

发表评论

来自  https://www.liaoxuefeng.com/wiki/1022910821149312/1103327377678688


编写聊天室

阅读: 172580

上一节我们用ws模块创建了一个WebSocket应用。但是它只能简单地响应ECHO: xxx消息,还属于Hello, world级别的应用。

要创建真正的WebSocket应用,首先,得有一个基于MVC的Web应用,也就是我们在前面用koa2和Nunjucks创建的Web,在此基础上,把WebSocket添加进来,才算完整。

因此,本节的目标是基于WebSocket创建一个在线聊天室。

首先,我们把前面编写的MVC工程复制一份,先创建一个完整的MVC的Web应用,结构如下:

ws-with-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- controllers/ <-- Controller
|
+- views/ <-- html模板文件
|
+- static/ <-- 静态资源文件
|
+- app.js <-- 使用koa的js
|
+- controller.js <-- 扫描注册Controller
|
+- static-files.js <-- 处理静态文件
|
+- templating.js <-- 模版引擎入口
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包

然后,把我们需要的依赖包添加到package.json

"dependencies": {
    "ws": "1.1.1",
    "koa": "2.0.0",
    "koa-bodyparser": "3.2.0",
    "koa-router": "7.0.0",
    "nunjucks": "2.4.2",
    "mime": "1.3.4",
    "mz": "2.4.0"
}

使用npm install安装后,我们首先得到了一个标准的基于MVC的koa2应用。该应用的核心是一个代表koa应用的app变量:

const app = new Koa();

// TODO: app.use(...);

app.listen(3000);

现在第一个问题来了:koa通过3000端口响应HTTP,我们要新加的WebSocketServer还能否使用3000端口?

答案是肯定的。虽然WebSocketServer可以使用别的端口,但是,统一端口有个最大的好处:

实际应用中,HTTP和WebSocket都使用标准的80和443端口,不需要暴露新的端口,也不需要修改防火墙规则。

在3000端口被koa占用后,WebSocketServer如何使用该端口?

实际上,3000端口并非由koa监听,而是koa调用Node标准的http模块创建的http.Server监听的。koa只是把响应函数注册到该http.Server中了。类似的,WebSocketServer也可以把自己的响应函数注册到http.Server中,这样,同一个端口,根据协议,可以分别由koa和ws处理:

http-ws-koa

把WebSocketServer绑定到同一个端口的关键代码是先获取koa创建的http.Server的引用,再根据http.Server创建WebSocketServer:

// koa app的listen()方法返回http.Server:
let server = app.listen(3000);

// 创建WebSocketServer:
let wss = new WebSocketServer({
    server: server
});

要始终注意,浏览器创建WebSocket时发送的仍然是标准的HTTP请求。无论是WebSocket请求,还是普通HTTP请求,都会被http.Server处理。具体的处理方式则是由koa和WebSocketServer注入的回调函数实现的。WebSocketServer会首先判断请求是不是WS请求,如果是,它将处理该请求,如果不是,该请求仍由koa处理。

所以,WS请求会直接由WebSocketServer处理,它根本不会经过koa,koa的任何middleware都没有机会处理该请求。

现在第二个问题来了:在koa应用中,可以很容易地认证用户,例如,通过session或者cookie,但是,在响应WebSocket请求时,如何识别用户身份?

一个简单可行的方案是把用户登录后的身份写入Cookie,在koa中,可以使用middleware解析Cookie,把用户绑定到ctx.state.user上。

WS请求也是标准的HTTP请求,所以,服务器也会把Cookie发送过来,这样,我们在用WebSocketServer处理WS请求时,就可以根据Cookie识别用户身份。

先把识别用户身份的逻辑提取为一个单独的函数:

function parseUser(obj) {
    if (!obj) {
        return;
    }
    console.log('try parse: ' + obj);
    let s = '';
    if (typeof obj === 'string') {
        s = obj;
    } else if (obj.headers) {
        let cookies = new Cookies(obj, null);
        s = cookies.get('name');
    }
    if (s) {
        try {
            let user = JSON.parse(Buffer.from(s, 'base64').toString());
            console.log(`User: ${user.name}, ID: ${user.id}`);
            return user;
        } catch (e) {
            // ignore
        }
    }
}

注意:出于演示目的,该Cookie并没有作Hash处理,实际上它就是一个JSON字符串。

在koa的middleware中,我们很容易识别用户:

app.use(async (ctx, next) => {
    ctx.state.user = parseUser(ctx.cookies.get('name') || '');
    await next();
});

在WebSocketServer中,就需要响应connection事件,然后识别用户:

wss.on('connection', function (ws) {
    // ws.upgradeReq是一个request对象:
    let user = parseUser(ws.upgradeReq);
    if (!user) {
        // Cookie不存在或无效,直接关闭WebSocket:
        ws.close(4001, 'Invalid user');
    }
    // 识别成功,把user绑定到该WebSocket对象:
    ws.user = user;
    // 绑定WebSocketServer对象:
    ws.wss = wss;
});

紧接着,我们要对每个创建成功的WebSocket绑定messagecloseerror等事件处理函数。对于聊天应用来说,每收到一条消息,就需要把该消息广播到所有WebSocket连接上。

先为wss对象添加一个broadcase()方法:

wss.broadcast = function (data) {
    wss.clients.forEach(function (client) {
        client.send(data);
    });
};

在某个WebSocket收到消息后,就可以调用wss.broadcast()进行广播了:

ws.on('message', function (message) {
    console.log(message);
    if (message && message.trim()) {
        let msg = createMessage('chat', this.user, message.trim());
        this.wss.broadcast(msg);
    }
});

消息有很多类型,不一定是聊天的消息,还可以有获取用户列表、用户加入、用户退出等多种消息。所以我们用createMessage()创建一个JSON格式的字符串,发送给浏览器,浏览器端的JavaScript就可以直接使用:

// 消息ID:
var messageIndex = 0;

function createMessage(type, user, data) {
    messageIndex ++;
    return JSON.stringify({
        id: messageIndex,
        type: type,
        user: user,
        data: data
    });
}

编写页面

相比服务器端的代码,页面的JavaScript代码会更复杂。

聊天室页面可以划分为左侧会话列表和右侧用户列表两部分:

chat

这里的DOM需要动态更新,因此,状态管理是页面逻辑的核心。

为了简化状态管理,我们用Vue控制左右两个列表:

var vmMessageList = new Vue({
    el: '#message-list',
    data: {
        messages: []
    }
});

var vmUserList = new Vue({
    el: '#user-list',
    data: {
        users: []
    }
});

会话列表和用户列表初始化为空数组。

紧接着,创建WebSocket连接,响应服务器消息,并且更新会话列表和用户列表:

var ws = new WebSocket('ws://localhost:3000/ws/chat');

ws.onmessage = function(event) {
    var data = event.data;
    console.log(data);
    var msg = JSON.parse(data);
    if (msg.type === 'list') {
        vmUserList.users = msg.data;
    } else if (msg.type === 'join') {
        addToUserList(vmUserList.users, msg.user);
        addMessage(vmMessageList.messages, msg);
    } else if (msg.type === 'left') {
        removeFromUserList(vmUserList.users, msg.user);
        addMessage(vmMessageList.messages, msg);
    } else if (msg.type === 'chat') {
        addMessage(vmMessageList.messages, msg);
    }
};

这样,JavaScript负责更新状态,Vue负责根据状态刷新DOM。以用户列表为例,HTML代码如下:

<div id="user-list">
    <div class="media" v-for="user in users">
        <div class="media-left">
            <img class="media-object" src="/static/user.png">
        </div>
        <div class="media-body">
            <h4 class="media-heading" v-text="user.name"></h4>
        </div>
    </div>
</div>

测试的时候,如果在本机测试,需要同时用几个不同的浏览器,这样Cookie互不干扰。

最终的聊天室效果如下:

websocket

配置反向代理

如果网站配置了反向代理,例如Nginx,则HTTP和WebSocket都必须通过反向代理连接Node服务器。HTTP的反向代理非常简单,但是要正常连接WebSocket,代理服务器必须支持WebSocket协议。

我们以Nginx为例,编写一个简单的反向代理配置文件。

详细的配置可以参考Nginx的官方博客:Using NGINX as a WebSocket Proxy

首先要保证Nginx版本>=1.3,然后,通过proxy_set_header指令,设定:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Nginx即可理解该连接将使用WebSocket协议。

一个示例配置文件内容如下:

server {
    listen      80;
    server_name localhost;

    # 处理静态资源文件:
    location ^~ /static/ {
        root /path/to/ws-with-koa;
    }

    # 处理WebSocket连接:
    location ^~ /ws/ {
        proxy_pass         http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
    }

    # 其他所有请求:
    location / {
        proxy_pass       http://127.0.0.1:3000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

参考源码

ws-with-koa

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

评论

  • 获取客户端ip

    星空__123 created at July 17, 2019 2:20 PM, Last updated at July 17, 2019 2:20 PM

    请问,通过什么方法获取用户的ip呢

  • 关于upgradeReq的问题

    Hfer_酸酸_乳川 created at July 29, 2018 3:33 PM, Last updated at July 29, 2018 3:33 PM

    参考https://www.npmjs.com/package/ws 和https://github.com/crowbartools/Firebot/issues/526

        wss.on('connection', function (ws, req) {
            let location = url.parse(req.url, true);
            console.log('[WebSocketServer] connection: ' + location.href);
            ws.on('message', onMessage);
            ws.on('close', onClose);
            ws.on('error', onError);
            if (location.pathname !== '/:3000/ws/chat') {
                // close ws:
                ws.close(4000, 'Invalid URL');
            }
            // check user:
            let user = parseUser(req);
            if (!user) {
                ws.close(4001, 'Invalid user');
            }
            ws.user = user;
            ws.wss = wss;
            onConnection.apply(ws);
        });
    

    通过加上第二个参数req,可以解决upgradeReq被remove的问题。另外输出发现location.pathname是‘/:3000/ws/chat’而不是‘/ws/chat’,遂也作了替换。

  • 既然没人评论,那我抢个沙发先 :-)

    eterinfi created at September 6, 2016 2:07 AM, Last updated at June 3, 2018 12:57 AM

    一个偶然的机会发现了这个网站开始学习,不知不觉已经过去一个多月了。起初是Python,然后JS,直至Node,方能亲身体会到Web开发之不易(下一步打算再挑战一下Python的Web开发实战部分,目测那个复杂度比起这个有过之而不及)。廖老师提供的教程结构清晰,文笔流畅,信息量大,内容新颖(比如本JS教程更新到ES6标准,而Node部分甚至用了尚未发布的ES7的方法),为网上流传的同类教程所不及,在此向廖老师聊表敬意!美中不足之处也正是由于本教程包含的内容跨度大,因篇幅所限未能对其中部分较为艰深抽象的内容充分展开,因而初学者浏览过后难免会有走马观花之感,有时需要参考其他相关教程结合自己反复摸索方可掌握。

    好在廖老师同时提供了Github上的开源代码供学习参考,赞一个!不过一路学习下来发现似乎还是小坑有可以debug的:

    一、mysql > 建立Model 末尾提到利用Sequelize的sync方法可以自动创建数据库,但是有好几个问题没讲清楚:一、前面说这个功能在开发和生产环境中没有什么用,但是在测试环境中非常有用,后面又说在开发环境下首次使用sync也可以自动创建出表结构,似乎前后矛盾;二、提供了调用sync方法的的代码(init-db.js),但没说应该如何调用该模块(自动?手动?);三、我试了在命令行中运行node init-db.js,输出显示似乎一切ok,却没能成功创建出表结构,最后还得在MySQL命令行中手动运行SQL命令,也不明白是什么原因。

    二、mocha > 编写测试 提到了三种运行测试的方法,其中方法一中的node_modules\mocha\bin\mocha在Windows环境下并非可执行文件,前面须加上node命令方可运行。

    三、WebSocket > 使用ws 末尾写到由于服务器在响应connection事件时不检查请求的路径,因此在客户端打开ws连接可以写任意的路径。的确,对于hello-ws这样级别的project来说没有问题,但在ws-with-koa这样需要读取cookie内容验证客户身份的project中问题就来了:WebSocket > 编写聊天室 中客户端页面用var ws = new WebSocket('ws://localhost:3000/ws/chat'); 创建ws连接时,实测在浏览器地址栏输入 localhost:3000 打开页面时一切正常;输入 127.0.0.1:3000 打开首页时正常,但登陆聊天室页面时ws连接会立即断开同时VSCode报错,经查原因为parseUser()中未能从cookie文件中get到name属性的值,以致parseUser()未能返回值到createWebSocketServer()中的user对象,导致ws连接被关闭及在OnConnect()中调用未定义的user对象发生错误;在局域网内其它设备的浏览器地址栏输入本机IP地址 192.168.0.x:3000登陆聊天室页面时ws连接同样立即断开,但VSCode不报错,原因暂未查明。经试验,若将views\room.html中创建WebSocket对象实例写的主机名称由localhost改成127.0.0.1或主机IP地址后,结果同样是在浏览器中输入相同的主机名称登陆聊天室页面一切正常,输入不同的主机名称则ws连接被断开,最后将创建WebSocket连接语句的路径改成ws://${location.hostname}:3000/ws/chat,总算解决了此问题。另外,经测试在Chrome、Firefox、Opera、MS Edge中显示页面均正常,惟以IE11登陆聊天室页面时脚本停止运行,不知原因何在,请高人指点(前面讲过的IE10以上版本支持WebSocket)。

     Read More

    • 需要删除init-db.js中的process.exit(0),原因我推测是因为前面的数据库操作会耗时,而process.exit(0)会让程序在数据库操作还未完成的时候就退出了。

    • init-db.js这个是需要手动执行的,而且执行的命令是NODE_DEV=test node init-db.js,这样就设置好了,是在test这个数据库中创建表格。

    • Created at September 6, 2016 10:43 AM

      实践得太仔细了,赞一个!

      因为只有Mac环境测试,Win环境可能会有一些问题

      Cookie是跟域名绑定的,所以要么全部localhost,要么全部127.0.0.1

      IE历来坑多,好在Win10以后退出历史舞台了

    • Created at September 6, 2016 11:47 AM

      喜欢上廖大的教程了,非常棒,赞助19元,等找到工作了赞助个大额的。

    • Created at September 6, 2016 1:51 PM

      感谢廖老师的回复!今天才恍然大悟原来是因为IE11也只支持到ES5语法,模板文件中路径用了模板字符串的话就会导致在用IE打开时脚本停止运行,改成普通字符串格式就好了,看来廖老师的教程内容确实够超前的,以后打算从事前端开发的话还得多留意一下浏览器兼容性的问题。

    • Created at October 13, 2016 2:57 PM

      好文,socket.io更简单

    • Created at January 3, 2017 3:29 PM

      原来是localhost和127.0.0.1不同的原因呀。。。 赞一个

    • Created at August 2, 2017 10:26 PM

      出现了和层主同样的问题,但不知道是哪里出错,看完层主的回答改了程序但还是出错,在app.jscreateWebSocketServer方法中的

      wss.on('connection', function (ws) {
          let location = url.parse(ws.upgradeReq.url, true);
          ...
      

      这句话一直报错,不知是什么原因,希望层主或廖老师能够解答 :)

    • Created at August 8, 2017 11:33 PM

      路径改成ws://${location.hostname}:3000/ws/chat 也是不行的,改成var localpath = (window.location.href).substring(6); var ws = new WebSocket('ws://'+localpath+'ws/chat');问题就解决了。这样动态的获得当前url,既可以实现内网访问,也可以实现外网访问

    • Created at November 28, 2017 2:38 PM

      对于第一个问题,结合我这几天的经验来看

      希望能帮到你。

    • Created at December 30, 2017 8:23 PM

      谢谢层主,原来是localhost和127.0.0.1对应不同的cookie的问题啊,找了一个晚上的原因 -.-

    • Created at June 3, 2018 12:57 AM

      @不卡若斯基 ,没用啊 node_dev=test node init-db.js根本没法运行

  • ws 在是使用了3.0以上的版本的一些问题

    Vincent_13 created at September 22, 2017 3:41 PM, Last updated at June 1, 2018 6:30 PM

    1、server.clients

    {Set} A set that stores all connected clients. Please note that this property is only added when the clientTracking is truthy.

    server.clients 是一个Set 对象

    function onConnect() {
        let user = this.user;
        let msg = createMessage('join', user, `${user.name} joined.`);
        this.wss.broadcast(msg);
        // build user list:
        // *** 这里请使用 forEach 遍历
        let users = this.wss.clients.map(function (client) {
            return client.user;
        });
        this.send(createMessage('list', user, users));
    }
    

    2、Event: 'connection'

    socket {WebSocket} request {http.IncomingMessage} Emitted when the handshake is complete. request is the http GET request sent by the client. Useful for parsing authority headers, cookie headers, and other information.

    ws升级到 3.0后 upgradeReq was removed from WebSocket.

    wss.on('connection', function (ws) {
        let location = url.parse(ws.upgradeReq.url, true);
        // ...
    });
    
    // ***
    wss.on('connection', function (ws, req) {
        ws.upgradeReq = req;
        let location = url.parse(ws.upgradeReq.url, true);
        // ...
    });
    

     Read More

  • if (typeof obj === 'string') { s = obj; } else if (obj.headers) { let cookies = new Cookies(obj, null); // 就是这儿 s = cookies.get('name'); }

  • 我不懂这个this的意思,指向的是什么呢?

    谢嘉欣是我呀 created at May 16, 2018 4:27 PM, Last updated at May 16, 2018 4:27 PM

    function onConnect() { let user = this.user; // 为什么不是undefined let msg = createMessage('join', user, ${user.name} joined.); this.wss.broadcast(msg); // build user list: let users = this.wss.clients.map(function (client) { return client.user; }); this.send(createMessage('list', user, users)); }

  • upgradeReq属性已经不存在了

    南海的椰子 created at April 8, 2018 10:30 PM, Last updated at April 9, 2018 11:10 AM

    使用最新版的ws模块发现ws.upgradeReq.url都是提示url未定义,搜索发现新版ws模块中已经移除了upgradeReq属性,那么替代方案是什么啊,国内根本没找到,国外只找到这个更新说明,文档也没有说代替的方法

  • ws模块能实现多房间聊天吗

    脉动90 created at January 17, 2018 9:52 AM, Last updated at January 17, 2018 9:52 AM

    ws模块能实现多房间聊天吗,怎么实现多房间聊天呢

  • 前端vue渲染貌似有问题吧

    毛damao created at October 7, 2017 9:22 PM, Last updated at October 7, 2017 9:22 PM

    vue 在data 里面定义的空数组不是真正的数组,没有length属性,push方法也不能用,大家有没有遇到这样的问题。

  • app.js有个很困惑的替代的地方

    深圳邦道营销管理咨询公司 created at May 20, 2017 8:26 PM, Last updated at May 20, 2017 8:26 PM

    function onMessage(message) { console.log(message); if (message && message.trim()) { let msg = createMessage('chat', this.user, message.trim()); //this.wss.broadcast(msg); this.send(msg); } }

    这里 this.wss.broadcast(msg); 替换为 this.send(msg);

    消息就会值刷新单个页面的

    function onConnect() { let user = this.user; let msg = createMessage('join', user, ${user.name} joined.); this.wss.broadcast(msg); // build user list: let users = this.wss.clients.map(function (client) { return client.user; }); //this.wss.broadcast(createMessage('list', user, users)); this.send(createMessage('list', user, users)); }

    这里 his.wss.broadcast(createMessage('list', user, users)); 替换为 this.send(createMessage('list', user, users));

    消息就会刷新所有页面的,两句完全相等

  • 几个冗余和可以用相同方法的地方

    深圳邦道营销管理咨询公司 created at May 20, 2017 8:05 PM, Last updated at May 20, 2017 8:05 PM

    app.js: wss.broadcast = function broadcast(data) { wss.clients.forEach(function(client) { client.send(data); }); };

    function each(Client)  这个 each 可以省略掉
    

    app.js function onConnect() { let user = this.user; let msg = createMessage('join', user, ${user.name} joined.); this.wss.broadcast(msg); // build user list: let users = this.wss.clients.map(function (client) { return client.user; }); //this.wss.broadcast(createMessage('list', user, users)); this.send(createMessage('list', user, users)); }

    下面这2句是相等的 this.wss.broadcast(createMessage('list', user, users)); this.send(createMessage('list', user, users));

  • 按登陆后不会跳转

    不想说话_Torres created at December 29, 2016 5:15 PM, Last updated at December 29, 2016 5:15 PM
    'POST /signin': async (ctx, next) => {
            index ++;
            let name = ctx.request.body.name || '路人甲';
            let user = {
                id: index,
                name: name,
                image: index % 10
            };
            let value = Buffer.from(JSON.stringify(user)).toString('base64');
            console.log(`Set cookie value: ${value}`);
            ctx.cookies.set('name', value);
            ctx.response.redirect('/');
        },
    

    在/sign下,输入用户名按登陆后并不会跳转回"/",把

     ctx.response.redirect('/');
    

    换成

      ctx.render('room.html',{     
            ...});
    

    就可以了,求解

     Read More

  • 请问大家能实现廖大大截图的那个效果吗?

    孙廉杰 created at December 14, 2016 10:21 PM, Last updated at December 14, 2016 10:21 PM

    我感觉我的代码已经与廖大大在GitHub上的代码完全一样了,但是我的完全实现不了聊天的功能?图片也加载不出来,信息也发不出去。。。

  • 为什么运行的demo时登录直接Internal Server Error。。。

    死道友不要死我就好 created at October 27, 2016 8:12 PM, Last updated at November 8, 2016 11:05 AM

    。。。win10环境 运行demo登录直接Internal Server Error vscode报错 TypeError: this is not a typed array. 求解

  • 对于github上的demo运行是出现的问题的一个反馈

    虚幻男孩儿 created at September 20, 2016 5:37 PM, Last updated at September 20, 2016 5:37 PM

    win环境下运行项目时,多出出现这种报错,

    E:\WEB TEST\test\nodeLearnDemo3\controllers\signin.js:10
                let names = '甲乙丙丁戊己庚辛壬癸';
                ^^^
    SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
    

    多方查阅之后,发现是文件的的开头未添加"use strict";

    在此评论一下,为后面的初学者提个醒,不然真的会找很久找不到原因的呢 /(ㄒoㄒ)/~~

  • 为什么匿名函数不使用箭头函数的形式呢?

    已经1V5 created at September 16, 2016 7:55 PM, Last updated at September 18, 2016 10:01 AM

    为什么匿名函数不使用箭头函数的形式呢?而是大量使用了function(){}的形式。

  • 这里突然冒出个vue框架让我措手不及.....

    iFayeee created at September 14, 2016 11:02 AM, Last updated at September 14, 2016 4:10 PM

    表示原来只听说过,并没有用过啊

发表评论


来自  https://www.liaoxuefeng.com/wiki/1022910821149312/1103332447876608

普通分类: