websocket入门介绍

329 阅读11分钟

websocket的出现

很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。它,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

websocket协议

Websocket 其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充,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请求有几点不同:

  • GET请求的地址不是类似/path/,而是以ws://开头的地址;
  • 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
  • Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
  • 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协议。

这样握手就完成了此时这个连接并不会断掉,而浏览器和服务端可以用这个连接相互发消息。(但是这个时候连接就不是 http 连接而是升级成了 WebSocket 连接。浏览器和服务端相互发送的不是 http 请求)。

与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

WebSocket与Http的区别

通信方式不同

WebSocket是双向通信模式,客户端与服务器之间只有在握手阶段是使用HTTP协议的“请求-响应”模式交互,而一旦连接建立之后的通信则使用双向模式交互,不论是客户端还是服务端都可以随时将数据发送给对方;而HTTP协议则至始至终都采用“请求-响应”模式进行通信。也正因为如此,HTTP协议的通信效率没有WebSocket高。 image.png 数据传输及请求

传统Web应用中浏览器与服务器进行数据交互通常需要经过以下几个步骤:

  1. DNS查询

  2. TCP三次握手

  3. 传送HTTP请求头

  4. 传送HTTP请求体(如果有)

  5. 服务器处理后传送响应头

  6. 服务器传送响应体

  7. 断开TCP连接 在WebSocket中进行交互通常为以下几个步骤:

  8. DNS查询

  9. TCP三次握手

  10. WebSocket握手

  11. 浏览器发送请求

  12. 服务器发送响应

  13. 断开TCP连接 从上面可以看到如果仅是一次通信,二者差异并不是很大,甚至WebSocket比普通方式还要多一次握手。但在需要频繁交互数据时,WebSocket的优势就显露出来了。

例如,当有10次数据交互时,前者要建立10个TCP连接(HTTP 1.0需要建立10次,HTTP 1.1可以通过长连接keep-alive复用TCP连接),然后要发送10次请求头(包含Cookie等信息,可能会达到K级别),接收的响应信息可能才几个字节(如某些心跳包),这样会极大的浪费带宽等资源。

试想,如果你在做一个聊天应用,想要获取当前在线人数,你需要向服务器发送你的全部cookie(至少要几百个字节),除此之外HTTP头中还要包含其他信息,如URL、host等,这些都是必不可少的。最后服务器返回了几百个字节,但其中真正需要用到的只有不到10字节(只需要知道在线人数,其他信息都是无用的)。

通过WebSocket,浏览器可以向服务器发送1~2字节的请求(不需要带上cookie验证身份,可以在握手时进行认证,一旦TCP连接建立,则在连接上的通信都是认证过身份的数据,这也是它的好处之一:便于服务端识别客户端的状态),这个请求仅包含一个特定的控制码(由开发者实现的应用层协议指定),服务器只需返回特定的返回码及数据即可,一切无用的字节都被省去。

协议格式不同

  • HTTP协议比较臃肿,而WebSocket协议比较轻量。
  • 对于HTTP协议来讲,一个数据包就是一条完整的消息;而WebSocket客户端与服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。即:发送端将消息切割成多个帧,并发送给服务端;服务端接收消息帧,并将关联的帧重新组装成完整的消息。
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

websocket优点和应用场景

WebSocket是一种在一个TCP连接上进行全双工通信的协议,它与HTTP同样默认工作在80和443端口,并且支持HTTP代理和中间件,所以WebSocket能够适配HTTP协议。WebSocket的工作机制是通过HTTP协议建立连接,再使用WebSocket协议进行数据通信

websocket优点

全双工通信

在传统的Web应用中,浏览器与服务器交互都是半双工通信(但并不完全是半双工通信,服务器无法主动向浏览器推送)。即同一时间内数据流向是单一的,浏览器向服务器发送请求后需要等待服务器返回数据。 而在WebSocket中,浏览器和服务器之间可随时进行通信,不必等待对方传送完毕,浏览器接收到服务器的数据后会自动触发onmessage事件。

实时性

传统的Web应用很难做到实时通信,通常是用长连接或轮询的方式进行。对于服务器来说,轮询法是被动传输数据,即使数据有更新,但浏览器还未发送请求,则消息无法进行实时推送。

更好的二进制支持。

WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容。

可以支持扩展。

WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

没有同源限制,客户端可以与任意服务器通信。

websocket应用场景

1.数据传输实时性要求较高

Web聊天室、贴吧直播贴、微博话题墙、专题讨论、实时网络攻击展示(已应用)等

2.推送类应用

网站消息通知、邮箱新邮件提醒

3.监控在线状态、精确统计在线时长

统计用户行为

4.远程调试代码、云指令系统

部分移动端调试工具是基于WebSocket开发(此处为WebSocket协议而非WebSocket API)

websocket功能介绍

WebSocket()构造函数

语法

var aWebSocket = new WebSocket(url [, protocols]);

url: 要连接的URL;这应该是WebSocket服务器将响应的URL。

//服务器网址就是 URL
ws://example.com:80/some/path
wss://example.com:80/some/path//加密

protocols: 一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个WebSocket子协议(例如,您可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

WebSocket属性

WebSocket.binaryType 返回websocket连接所传输二进制数据的类型。

  • blob:传输的是 Blob 类型的数据。
  • 传输的是 ArrayBuffer 类型的数据。
aWebSocket.binaryType = "blob"; 
// or 
aWebSocket.binaryType = "arraybuffer";  //ArrayBuffer是存放二进制数据的数组

WebSocket.bufferedAmount是一个只读属性,用于返回已经被send()方法放入队列中但还没有被发送到网络中的数据的字节数。一旦队列中的所有数据被发送至网络,则该属性值将被重置为0。但是,若在发送过程中连接被关闭,则属性值不会重置为0。如果你不断地调用send(),则该属性值会持续增长

var bufferedAmount = aWebSocket.bufferedAmount;
var data = new ArrayBuffer(10000000);
aWebSocket.send(data);

if (aWebSocket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

WebSocket.readyState属性返回实例对象的当前状态,共有四种。

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。 webSocket.onopen属性定义一个事件处理程序,当WebSocket 的连接状态readyState 变为1时调用;这意味着当前连接已经准备好发送和接受数据。这个事件处理程序通过 事件(建立连接时)触发。
aWebSocket.onopen = function () {
  aWebSocket.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法。

aWebSocket.addEventListener('open', function (event) {
  aWebSocket.send('Hello Server!');
});

WebSocket.onclose 属性返回一个事件监听器,这个事件监听器将在 WebSocket 连接的readyState 变为 CLOSED时被调用,它接收一个名字为“close”的 CloseEvent 事件。

aWebSocket.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

aWebSocket.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

WebSocket.onmessage 属性是一个当收到来自服务器的消息时被调用的 event handler。它由一个MessageEvent调用。

aWebSocket.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

aWebSocket.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。

aWebSocket.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");
  }
}

WebSocket.onerror 属性中,你可以定义一个发生错误时执行的回调函数,此事件的事件名为"error"

aWebSocket.onerror = function(event) {
  // handle error event
};

aWebSocket.addEventListener("error", function(event) {
  // handle error event
});

WebSocket 方法

WebSocket.close()

WebSocket.close();

参数

  • close:(可选)一个数字状态码,它解释了连接关闭的原因。如果没有传这个参数,默认使用1005。

  • reason (可选)一个可读的字符串,它解释了连接关闭的原因。这个UTF-8编码的字符串不能超过123个字节。

WebSocket.send()

aWebSocket.send('your message');

WebSocket.send() 方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的data bytes的大小来增加 bufferedAmount的值 。

参数

  • 文本字符串。字符串将以 UTF-8 格式添加到缓冲区,并且 bufferedAmount 将加上该字符串以 UTF-8 格式编码时的字节数的值。

  • ArrayBuffer可以使用一有类型的数组对象发送底层二进制数据;其二进制数据内存将被缓存于缓冲区,bufferedAmount 将加上所需字节数的值。

  • Blob 类型将队列 blob 中的原始数据以二进制中传输。 bufferedAmount 将加上原始数据的字节数的值。

发送 Blob 对象的例子。


var file = document.querySelector('input[type="file"]').files[0];
aWebSocket.send(file);

发送 ArrayBuffer 对象的例子。

var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
aWebSocket.send(binary.buffer);

websocket心跳机制

心跳重连原由

心跳和重连的目的用一句话概括就是客户端和服务端保证彼此还活着,避免丢包发生。

前端断开

在使用websocket过程中,可能会出现网络断开的情况,比如信号不好,或者网络临时关闭,这时候websocket的连接已经断开,而不同浏览器有不同的机制,触发onclose的时机也不同,并不会理想执行websocket的onclose方法,我们无法知道是否断开连接,也就无法进行重连操作。

后端断开

如果后端因为一些情况需要断开ws,在可控情况下,会下发一个断连的消息通知,之后才会断开,我们便会重连。 如果因为一些异常断开了连接,我们是不会感应到的,所以如果我们发送了心跳一定时间之后,后端既没有返回心跳响应消息,前端又没有收到任何其他消息的话,我们就能断定后端主动断开了。

因此需要一种机制来检测客户端和服务端是否处于正常连接的状态。通过在指定时间间隔发送心跳包来保证连接正常,如果连接出现问题,就需要手动触发onclose事件,这时候便可进行重连操作。因此websocket心跳重连就应运而生。

心跳重连的实现

通过createWebSocket创建连接

function createWebSocket() {
  try {
    aWebSocket = new WebSocket(wsUrl);
    init();
  } catch(e) {
    console.log('catch');
    reconnect(wsUrl);
  }
}

初始化一些监听事件,如果希望websocket连接一直保持, 我们会在close或者error上绑定重新连接方法。

function init() {
  aWebSocket.onclose = function () {
    console.log('链接关闭');
    reconnect(wsUrl);
  };
  aWebSocket.onerror = function() {
    console.log('发生异常了');
    reconnect(wsUrl);
  };
  aWebSocket.onopen = function () {
    //心跳检测重置
    heartCheck.start();
  };
  aWebSocket.onmessage = function (event) {
     console.log('接收到消息');
    //拿到任何消息都说明当前连接是正常的
    heartCheck.start();
  }
}

重连操作,通过设置lockReconnect变量避免重复连接


var lockReconnect = false;//避免重复连接
function reconnect(url) {
      if(lockReconnect) {
        return;
      };
      lockReconnect = true;
      //没连接上会一直重连,设置延迟避免请求过多
      tt && clearTimeout(tt);
      tt = setTimeout(function () {
        createWebSocket(url);
        lockReconnect = false;
      }, 4000);
}

心跳检测

//心跳检测
var heartCheck = {
      timeout: 10000, //每隔十秒发送心跳
      num: 3,  //3次心跳均未响应重连
      timeoutObj: null,
      serverTimeoutObj: null,
      start: function(){
        var _this = this;
        var _num = this.num;
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
        this.timeoutObj = setTimeout(function(){
              //这里发送一个心跳,后端收到后,返回一个心跳消息,
              //onmessage拿到返回的心跳就说明连接正常
              aWebSocket.send("123456789"); // 心跳包
              _num--;
              //计算答复的超时次数
              if(_num === 0) {
                   aWebSocket.colse();
              }
        }, this.timeout)
      }
}

如何使用websocket

在客户端使用WebSocket

在Web网页中使用WebSocket需要浏览器支持,不同浏览器软件版本对WebSocket的支持情况详见浏览器兼容性

var url = "ws://localhost:8080/websocket/text";
var aWebSocket = new WebSocket(url);
aWebSocket.onopen = function(event) {
    console.log("websocket connection open.");
    console.log(event);
};

aWebSocket.onmessage = function(event) {
    console.log("websocket message received.")
    console.log(event.data);
};

aWebSocket.onclose = function (event) {
    console.log("websocket connection close.");
    console.log(event.code);
};

aWebSocket.onerror = function(event) {
    console.log("websocket connection error.");
    console.log(event);
};

在服务端使用WebSocket

在服务端使用WebSocket需要服务器组件支持,如下以在Tomcat 8.5.41(Tomcat 7之后才支持WebSocket)中使用原生WebSocket为例。

由于在服务端使用WebSocket需要使用到WebSocket的API,因此需要添加API依赖管理:

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-websocket-api</artifactId>
    <version>8.5.41</version>
</dependency>

服务端:

@ServerEndpoint(value="/websocket/text")
public class WebSocketTest {
	private static final Logger logger = LoggerFactory.getLogger(WsChatAnnotation.class);
	
	private static final AtomicInteger counter = new AtomicInteger(0);                                    // 客户端计数器
	private static final Set<WsChatAnnotation> connections = new CopyOnWriteArraySet<WsChatAnnotation>(); // 客户端websocket连接集合
	private Session session = null;                                                                       // WebSocket会话对象
	private Integer number = 0;                                                                           // 客户端编号

	public WsChatAnnotation() {
		number = counter.incrementAndGet();
	}
	
	/**
	 * 客户端建立websocket连接
	 * @param session
	 */
	@OnOpen
	public void start(Session session) {
		logger.info("on open");
		this.session = session;
		connections.add(this);
		try {
			session.getBasicRemote().sendText(new StringBuffer().append("Hello: ").append(number).toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 客户端断开websocket连接
	 */
	@OnClose
	public void close() {
		logger.info("session close");
		try {
			this.session.close();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			connections.remove(this);
		}
	}
	
	/**
	 * 接收客户端发送的消息
	 * @param message
	 */
	@OnMessage
	public void message(String message) {
		logger.info("message: {}", message);
		for(WsChatAnnotation client : connections) {
			synchronized (client) {
				try {
					client.session.getBasicRemote().sendText(message);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	@OnError
	public void error(Throwable t) {
		logger.error("client: {} error", number, t.getMessage());
	}
}

反向代理对WebSocket的支持

目前Nginx,Haporxy都已经支持WebSocket协议。

如下为在使用nginx作为反向代理的场景下,配置nginx代理websocket协议。

# add websocket proxy
location ~ /ws {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://8080;
}