WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket api 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。html5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
socket是在http基础上,对http进行升级,让连接用socket来完成。
一个典型的Websocket握手请求如下:
客户端请求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服务器回应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
const http = require('http');
const net = require('net');//TCP 原生socket
const crypto = require('crypto');
let server = net.createServer(socket => {
//握手只有一次
socket.once('data',(data)=>{
console.log('握手开始')
let str = data.toString()
let lines = str.split('\r\n')
//舍弃第一行和最后两行
lines = lines.slice(1,lines.length-2)
//切开
let headers ={}
lines.forEach(line=>{
let [key,value]= line.split(`: `)
headers[key.toLowerCase()]=value
})
if(headers[`upgrade`]!='websocket'){
console.log('其他协议',headers[`upgrade`])
socket.end()
}else if(headers[`sec-websocket-version`] != '13'){
console.log('版本不对',headers[`upgrade`])
socket.end()
}else {
let key = headers['sec-websocket-key']
let mask = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
let hash =crypto.createHash('sha1')
hash.update(key+mask)
let key2 =hash.digest('base64')
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${key2}\r\n\r\n`)
//服务器响应,握手结束
//真正的数据
socket.on('data',data1 => {
console.log(data1)//帧
let FIN = data1[0]&0x001//位运算
let opcode=data1[0]&0xF0
})
}
})
//断开
socket.on('end',() =>{
console.log('客户端断开')
})
});
server.listen(8080);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
let socket = new WebSocket('ws://localhost:8080/')
socket.emit = function(name,...args){
socket.send(JSON.stringify({name,data:args}))
alert(JSON.stringify({name,data:args}))
}
socket.onopen = function (event) {
console.log('WebSocket is open')
socket.emit('msg',12,34)
}
socket.onmessage = function (event) {
console.log('有消息过来')
}
socket.onclose = function () {
console.log('断开连接')
}
</script>
</head>
<body>
</body>
</html>
原生socket较复杂,一般都通过框架来使用websocket,socket.io封装了websocket。
socket.io文档
安装:
npm install socket.io -s
let app = require('express')();
let httpServer = require('http').createServer(app);
//得到IO对象
let io = require('socket.io')(httpServer);
//监视连接,(当有一个客户连接上时回调) io关联多个socket
io.on('connection',function (socket) {
console.log('socketio connected');
//绑定sendMsg监听,接受客户端发送的消息
socket.on('sendMsg',function (data) {
console.log('服务端接受到浏览器的信息');
/*io.emit 发送给所有连接服务器的客户端,
socket.emit 发送给当前连接的客户端*/
socket.emit('receiveMsg',data.name + '_'+ data.date);
console.log('服务器向浏览器发送消息',data)
})
})
http.listen(4000,function(){
console.log('listening on:4000')
})
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
//连接服务器,得到代表连接的socket对象
const socket = io('ws://localhost:4000');
//绑定'receiveMessage的监听,来接受服务器发送的数据
socket.on('receiveMsg',function(data){
console.log('浏览器端接受消息');
})
//向服务器发送消息
socket.emit('sendMsg',{name: 'Tom',date: Date.now()})
console.log('浏览向服务器发送消息')
</script>
上面服务端如果使用socket.emit 实现的是服务端和客户端的一对一发送数据,那么如何将服务端收到的数据发送给其他用户,来实现聊天室效果呢?
这里就需要io.emit 发送数据给当前连接此服务器的所有用户。
服务端
let app = require('express')();
let httpServer = require('http').createServer(app);
//得到IO对象
let io = require('socket.io')(httpServer);
//监视连接.
io.on('connection',function (socket) {
socket.on('chat message', function(msg){
console.log('connected',msg)
io.emit('chat message', msg);
});
socket.on('disconnected',(res)=>{
console.log('disconnected',res)
})
})
http.listen(4000,function(){
console.log('listening on:4000')
})
客户端
<head>
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
$(function () {
let socket = io("ws://localhost:4000");
$('form').submit(function(e){
e.preventDefault(); //阻止刷新
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
socket.on('chat message', function(msg){
$('#messages').append($('<li>').text(msg));
});
});
</script>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
接下来根据官网的方案进行优化
Here are some ideas to improve the application:
在服务端,通过io.on('connection') 监听用户连接。
socket.on('disconnect') 监听用户断开。
通过回调向客户端传递提示信息。 socket.id 可以用来独一无二的表示当前会话的客户端id
//服务端
socket.on('add user',function (msg) { //在用户第一次加入时,触发add user,提示所有用户
io.emit('user joined',{id: socket.id});
})
socket.on('disconnect',(reason)=>{
console.log('disconnect',socket.id,reason);
io.emit('user left',{id: socket.id})
})
//客户端
socket.on('user joined',function(data){
let {id} = data;
$('#messages').append($('<li>').text(id+'加入聊天室').addClass('log'));
})
socket.on('user left',function(data){
let {id} = data;
$('#messages').append($('<li>').text(id+'离开聊天室').addClass('log'));
})
//在客户端提供添加昵称的输入框,当输入完信息后,传递昵称给服务端
socket.emit('add user', username);
//在服务端重构
socket.on('add user', function (username) {
socket.username = username;
io.emit('user joined', {
username: socket.username
});
})
通过监听keydown事件,判定 event.which 的值是否为 13(enter的Unicode码是13)。如果是则emit 消息
通过监听input事件,来更新type信息
//update
$inputMessage.on('input', function () {
updateTyping();
});
function updateTyping() {
if (connected) {
if (!typing) {//如果当前没在输入,则更改标志,并发送正在输入的消息消息
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
setTimeout(function () {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {//如果停止输入超时,则发送停止消息
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
//服务端 传递当前正在输入或停止输入的用户名,用于让客户端显示或消失 is Typing的信息
socket.on('typing', function () {
io.emit('typing', {
username: socket.username
});
});
socket.on('stop typing', function () {
io.emit('stop typing', {
username: socket.username
});
更多案例在官方仓库中查找
namespace允许用户去分配路径,这个的好处是可以减少TCP资源,同时进行通道隔离
默认的namespace是/ 通过 of 方法可以自定义namespace
//服务端
const nsp = io.of('/my-namespace');
nsp.on('connection', function(socket){
console.log('someone connected');
});
nsp.emit('hi', 'everyone!');
//客户端
const socket = io('/my-namespace');
对于每个namespace,都可以定义多个频道,也就是room,用户可以 join 和 left
//服务端
io.on('connection', function(socket){
socket.join('some room');
});
//当要向某个房间传数据时,使用 to
io.to('some room').emit('some event');
有的时候需要将数据从一个进程发送到令一个进程,可以通过redis adapter
//一个服务端 可以应用redis adapter
const io = require('socket.io')(3000);
const redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));
//另一个服务端可以通过连接给服务,从另一个进程的任意频道发送
const io = require('socket.io-emitter')({ host: '127.0.0.1', port: 6379 });
setInterval(function(){
io.emit('time', new Date);
}, 5000);
websocket是实现浏览器和web服务器沟通的一个好方法。一个比较好的使用方法就是在web网页上使用websocket,然后再起一个webdocketd服务器。
HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并达到实时通讯。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
WebSocket是基于TCP的一种新的网络协议,并在2011年被IETF定为标准的全双工通信协议,它实现了客户端与服务器全双工通信。 WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
Web Sockets定义了一种在通过一个单一的 socket 在网络上进行全双工通讯的通道。它不仅仅是传统的 HTTP 通讯的一个增量的提高,尤其对于实时、事件驱动的应用来说是一个飞跃。
WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失
在建立web socket通信后,发送数据时,出现下图所示现象:要明白这个问题产生的原因,就需要了解websocket的几个状态。通常在实例化一个websocket对象之后,客户端就会与服务器进行连接
在 websocket 出现之前,为了实现 web 端的实时通信,通常采用的是 Ajax 轮询技术,(轮询是在特定的时间间隔内,由浏览器向服务器发送 HTTP 请求,再由服务器返回最新的数据)
websocket服务端往往需要和服务层打交道,因此需要将服务层的一些bean注入到websocket实现类中使用,但是呢,websocket实现类虽然顶部加上了@Component注解,依然无法通过@Resource和@Autowire注入spring容器管理下的bean
以下 API 用于创建 WebSocket 对象。以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。以下是 WebSocket 对象的属性。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!