HTTP是单向的,客户端发送请求,服务器发送响应。每个请求都与一个对应的响应相关联,在发送响应后客户端与服务器的连接会被关闭。每个HTTP或HTTPS请求每次都会新建单独的tcp与服务器的连接,并且在获得响应后,连接将自行终止。http协议通信只能由客户端发起。如果没有socket,只能使用轮询。
http长连接、http和tcp的关系
WebSocket是HTML5规范提出的一种协议;WebSocket是双向的,在客户端-服务器通信的场景中使用的全双工协议。该规范定义了ws://以及wss://模式来分别表示WebSocket和安全WebSocket连接,这就跟http:// 以及https:// 的区别是差不多的,服务器网址就是 URL,没有同源限制,客户端可以与任意服务器通信。
以客户端-服务器通信为例,每当我们启动客户端和服务器之间的连接时,客户端-服务器进行握手随后创建一个新的连接,该连接将保持活动状态,直到被他们中的任何一方终止。建立连接并保持活动状态后,客户端和服务器将使用相同的连接通道进行通信,直到连接终止。
新建的连接被称为WebSocket。一旦通信链接建立和连接打开后,消息交换将以双向模式进行,客户端-服务器之间的连接会持续存在。如果其中任何一方(客户端服务器)宕掉或主动关闭连接,则双方均将关闭连接。套接字的工作方式与HTTP的工作方式略有不同,状态代码101表示WebSocket中的交换协议。
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。在WebSocket中,只需要服务器和浏览器通过HTTP协议进行一个握手的动作,然后单独建立一条TCP的通信通道进行数据的传送。
WebSocket连接的过程是:
全双工:通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时(瞬时)进行信号的双向传输(A→B且B→A)。指A→B的同时B→A,是瞬时同步的。
Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。
Socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口,提供一套调用TCP/IP协议的API。
当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。
WebSocket就像HTTP一样,则是一个典型的应用层协议。Socket是传输控制层接口,WebSocket是应用层协议。
websocket和http的区别:1、WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的;2、WebSocket是需要浏览器和服务器握手进行建立连接的。而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接。
websocket和http的区别
什么是WebSocket,它与HTTP有何不同?
如果我们需要通过网络传输的任何实时更新或连续数据流,则可以使用WebSocket。如果我们要获取旧数据,或者只想获取一次数据供应用程序使用,则应该使用HTTP协议,不需要很频繁或仅获取一次的数据可以通过简单的HTTP请求查询,因此在这种情况下最好不要使用WebSocket。
// 连接
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = () => {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
// {
// binaryType: "blob" // 连接使用的二进制数据类型。
// bufferedAmount: 0 // 排队数据的字节数
// extensions: "" // 服务器选择的扩展名
// onclose: null // 连接关闭的回调函数
// onerror: null // 连接错误关闭连接的回调函数
// onmessage: null // 收到服务器数据回调函数
// onopen: ƒ (evt) // 连接成功回调函数
// protocol: "" // 服务器选择的子协议
// readyState: 3 // 当前状态
// url: "wss://echo.websocket.org/" // 连接地址
// }
readyState属性返回实例对象的当前状态,共有四种
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}
实例对象的onopen属性,用于指定连接成功后的回调函数。
ws.onopen = function () {
ws.send('Hello Server!');
}
如果要指定多个回调函数,可以使用addEventListener方法。
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
实例对象的onclose属性,用于指定连接关闭后的回调函数。
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};
ws.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
ws.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});
服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。
ws.onmessage = function(event){
if(typeof event.data === String) {
console.log("Received data string");
}
if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}
除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
实例对象的onerror属性,用于指定报错时的回调函数。
socket.onerror = function(event) {
// handle error event
};
socket.addEventListener("error", function(event) {
// handle error event
});
实例对象的send()方法用于向服务器发送数据。
ws.send('your message');
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
ws.send(data);
if (ws.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}
如果仍然打开,请刷新连接(关闭然后重新打开它)
WebSocket 教程
MDN Web Docs WebSocket
后端可以通过server.tomcat.max-connections=10000
设置最大连接数
Chrome浏览器限制WebSocket的最大连接数就是256
IE 6个
Firefox 200个
safari 1273个(MAC版本)
为什么你的websocket只能建立256个连接?
目的:websocket是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒。因此为了保证连接的可持续性和稳定性,websocket心跳重连就应运而生。
遇到的问题:为了解决连接正常后,页面未关闭,放置一段时间后,再次浏览该页面,不会收到服务器的推送(后来多次测试后发现websocket其实已经断开连接了,只是浏览器未发现websocket已经断开(也就是响应超时)。实现该现象就是最简单的方法就是页面在PC调试,使用相关软件断开chrome的网络权限,会发现websocket并未能自动检测到已经与服务器断开了连接。)
实现心跳检测的方法思路算是比较简单,主要是通过定时向服务器send()相关消息,并且定下心跳包的超时时间,当收到服务器返回的消息时,清掉当前心跳计时器以及重连超时的定时器。若服务端未能够及时返回特定消息,超过设定的超时时间时,主动关闭当前websocket连接,并且尝试重新建立新的websocket连接。
方法
ws.onclose = function () {
reconnect();
};
ws.onerror = function () {
reconnect();
};
这样一般正常情况下失去连接时,触发onclose方法,我们就能执行重连了。
在使用原生websocket的时候,如果设备网络断开,不会立刻触发websocket的任何事件,前端也就无法得知当前连接是否已经断开。这个时候如果调用websocket.send方法,浏览器才会发现链接断开了,便会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发onclose函数。
var heartCheck = {
timeout: 60000,//60s
timeoutObj: null,
reset: () => {
clearTimeout(this.timeoutObj);
this.start();
},
start: () => {
this.timeoutObj = setTimeout(() => {
ws.send("HeartBeat");
}, this.timeout)
}
}
ws.onopen = () => {
heartCheck.start();
};
ws.onmessage = (event) => {
heartCheck.reset();
}
当onopen也就是连接成功后,我们便开始start计时,如果在定时时间范围内,onmessage获取到了后端的消息,我们就重置倒计时,
距离上次从后端获取到消息超过60秒之后,执行心跳检测,看是不是断连了,这个检测时间可以自己根据自身情况设定。
注意
1. 在chrome中,如果心跳检测 也就是websocket实例执行send之后,15秒内没发送到另一接收端,onclose便会执行。那么超时时间是15秒。
2. 我又打开了Firefox ,Firefox在断网7秒之后,直接执行onclose。说明在Firefox中不需要心跳检测便能自动onclose。
3. 同一代码, reconnect方法 在chrome 执行了一次,Firefox执行了两次。当然我们在几处地方(代码逻辑处和websocket事件处)绑定了reconnect(),
所以保险起见,我们还是给reconnect()方法加上一个锁,保证只执行一次
判断前端websocket断开(断网但不限于断网的情况)
当心跳检测执行send方法之后,如果当前websocket是断开状态(或者说断网了),发送超时之后,浏览器的websocket会自动触发onclose方法,重连就会立刻执行(onclose方法体绑定了重连事件),如果当前一直是断网状态,重连会2秒(时间是自己代码设置的)执行一次直到网络正常后连接成功。
如此一来,判断前端断开websocket的心跳检测就实现了。为什么说是前端主动断开,因为当前这种情况主要是通过前端websocket.send来检测并触发的onclose
如果后端因为一些情况断开了ws,是可控情况下的话,会下发一个断连的通知,这样会触发前端weboscket的onclose方法,我们便会重连。
如果因为一些异常断开了连接,前端是不会感应到的,所以如果前端发送了心跳一定时间之后,后端既没有返回心跳响应消息,前端也没有收到任何其他消息的话,我们就能断定后端发生异常断开了。
一点特别重要的发送心跳到后端,后端收到消息之后必须返回消息,否则超过60秒之后会判定后端主动断开了。
var heartCheck = {
timeout: 60000,//60ms
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
ws.send("HeartBeat");
self.serverTimeoutObj = setTimeout(function(){
ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
},
}
ws.onopen = function () {
heartCheck.start();
};
ws.onmessage = function (event) {
heartCheck.reset();
}ws.onclose = function () {
reconnect();
};
ws.onerror = function () {
reconnect();
};
** 因为目前我们这种方式会一直重连如果没连接上或者断连的话,如果有两个设备同时登陆并且会踢另一端下线,一定要发送一个踢下线的消息类型,这边接收到这种类型的消息,逻辑判断后就不再执行reconnect,否则会出现一只相互挤下线的死循环。**
还有一种方式是:让服务端发送心跳,前端来接收,这样的方式会多节约一点带宽,因为如果是前端发送心跳,后端需要返回心跳,也就是ping pong的过程会有两次数据传递。 而后端来发送心跳的话,就只需要发送ping,前端不需要回应。但是这样造成了一个问题。前端需要和后端约定好心跳间隔,比如后端设置10秒发送一次心跳,那前端就需要设置一个安全值,比如距离上次收到心跳超过12秒还没收到下一个心跳就重连。这种方式的问题在于调节时间就变得不那么灵活了,需要双方都同时确定一个时间约定。后端的逻辑也会比较多一点。而如果前端来发送ping 后端返回pong的话,那么间隔时间就只需要前端自己控制了。加上代码把收到的任何后端信息都可以当作是连接正常,从而重置心跳时间,这样也节约了一些请求次数。
使用前端发送心跳的方式,后端比较轻松,只需要在 onmessage 写一段代码
if(msg=='heartbeat') socket.send(anything);
Socket.io不是Websocket,它只是将Websocket和轮询 (Polling)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。也就是说,Websocket仅仅是 Socket.io实现实时通信的一个子集。因此Websocket客户端连接不上Socket.io服务端,当然Socket.io客户端也连接不上Websocket服务端。
vue-socket.io
socket.io的github地址
源码
npm install vue-socket.io --save
npm install socket.io-client --save
import Vue from 'vue'
import App from './App.vue'
import VueSocketIO from 'vue-socket.io'
Vue.use(new VueSocketIO({
debug: true,
connection: 'http://metinseylan.com:1992',
vuex: {
store,
actionPrefix: 'SOCKET_',
mutationPrefix: 'SOCKET_'
},
options: { path: "/my-app/" } //Optional options
}))
new Vue({
render: h => h(App)
}).$mount('#app')
debug:生产环境建议关闭,开发环境可以打开,这样你就可以在控制台看到socket连接和事件监听的一些信息,例如下面这样:
connection:连接地址前缀,注意!这里只有前缀
options.path: 这里就可以填websocket连接地址的后缀,如果不填会被默认添加/socket.io
vuex: 配置后可以在store.js的mutations或者actions监听到Vue-Socket.io事件(例如:connect、disconnect、reconnect等),这部分目前用得比较少,也挺简单,如果有疑问可以给我留言我再单独提供教程。
import VueSocketio from 'vue-socket.io';
Vue.use(new VueSocketio({
debug: true,
connection: 'http://172.16.8.61:5000/websocket/monitor/detail',
options: { 'transports': ['websocket']} //Optional options
}))
export default {
name: 'Page',
sockets: {// 通过vue实例对象sockets实现组件中的事件监听
connect: function () {// socket的connect事件
console.log('socket connected from Page')
// 获取每台客服端生成的id
this.websocketid = this.$socket.id;
},
// 监听断开连接,函数
disconnect(){
console.log('断开服务器连接');
},
STREAM_STATUS(data) {// 后端按主题名推送的消息数据
console.log('Page:' + data)
}
},
mounted() {
console.log('page mounted')
this.$socket.emit('STREAM_STATUS', { subscribe: true })// 在页面加载时发起订阅,“STREAM_STATUS”是你跟后端约定好的主题名
}
}
websocket连接地址是从后端动态获取,所以导致页面加载时VueSocketIO实例还未创建,页面中通过this.$socket.emit发起订阅报错,同时无法找到vue实例的sockets对象(写在内部的事件将无法监听到,就算后面已经连接成功)
解决办法: 保证拿到socket连接地址后再将vue实例挂载到app
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ParentApi from '@/api/Parent'
import VueSocketIO from 'vue-socket.io'
/* 使用vue-socket.io */
ParentApi.getSocketUrl().then((res) => {
Vue.use(new VueSocketIO({
debug: false,
connection: res.data.path,
options: { path: '/my-project/socket.io' }
}))
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
})