WebSocket 介绍以及配合 STOMP 的使用

2022-03-25 14:51:38 浏览数 (1)

由于近期需要使用 WebSocket 的部分功能,然而在工作过程中,发现自己对这部分知识点不是很了解,而且对于后台同学提出的 WebSocket 和 STOMP 的组合,不知如何下手。经过相关资料查证,分享与大家,如有纰漏,希望不吝指出。 本文行文为三个部分,分别讲述:Socket 是什么,WebSocket 是什么,STOMP 是什么,如何结合后两者投入使用。

1. Socket

目前来说,我们经常说的 Socket 的有好几种意思,而且这几种意思还都与通信有关,他们分别是:

  1. Socket 连接 socket 连接,是端到端的一种连接方式,连接上之后,双方可以互发数据,完成交互;socket 连接的建立也是一个三次握手的过程,经过这个过程之后,双方都可以通过事件监听来获取来自对方的消息(connect, data, close …),也可以主动发送消息给对方(Socket.write)。Socket 连接在不同语言的网络模块均有提供,以上方法都是 node 的 net 模块提供的一些方法和事件,可以用来建立一个完整的 socket 连接。
  2. Socket 抽象封装层 这一种意思是说,它是作为我们所说的网络分层结构里面的,网络层和应用层之间的一层抽象封装。它的作用,就是将功能强大的网络层的操作做了一个封装,将其复杂的操作,抽象为几个简单的接口供应用层调用,以实现进程在网络中通信。按照网络上流行的说法,TCP/IP(网络层)是功能强大的发动机引擎,Socket 层是汽车,我们只需要动动方向盘,就能调动起强大的引擎为我所用。
  3. 套接字 这个部分,说的是 Socket 连接建立起来之后,双方维护的一个对象,用来发送和接受数据包。一个 Socket 连接建立,对应的是连接两端对应的一对套接字对象,其维护的信息为:连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。通过如上信息,即可确定传输的位置和传输的方式。

2. WebSocket

是什么WebSocket 是 H5 规范提出的一种应用层协议(与 HTTP 处于同一层级),是建立在 TCP/IP 协议族之上的一种长连接,可进行全双工通信。

为什么需要它它的提出确实是极其必要的。主要有两方面的考虑:一是,在H5规范的描述下,web应该是一个丰富多彩的世界,能提供应用程序级别的使用体验。既然是应用程序级别体验,自然应该有应用程序级别的网络基础支持,而这种支持就应该包含长连接,实时通信这种级别的支持;二是,使用目前的 HTTP 协议,模拟出两端长连接的效果(轮询,阻塞),消耗太大。

实现的过程WebSocket 连接实现的过程分为两个部分:建立连接的过程,连接之后的 Socket 通信过程。WebSocket 连接建立的过程,是用到了 HTTP 请求的。在一开始建立连接的过程中,希望建立连接的客户端会向服务端发送一个 HTTP 请求,询问服务器是不是支持 WebSocket,并且告诉服务端,我使用 WebSocket 请求,希望服务端进行相应的响应。此处为了区分普通的 HTTP 请求,此处上传了其他的头部信息:在客户端校验 Sec-WebSocket-Accept 通过之后,连接即可建立完成。这之后的信息通讯均是WebSocket定义的通过长连接进行的,而且此长连接会复用刚才 HTTP 请求建立的 TCP 长连接。之后的消息发送,消息接受,连接建立,连接关闭等交互,与 Socket 基本类似。

如何使用 node 搭建一个简单的ws服务器 此处的 demo 是,通过 sockjs,建立一个ws服务器,连接两个或者多个客户端,当某一个客户端发送消息给服务器,服务器可以主动将该消息发送给别的客户端。

代码语言:javascript复制
// 客户端主要代码
var sockjs_url = '/echo';
var sockjs = new SockJS(sockjs_url);

sockjs.onopen    = function()  {print('[*] open', sockjs.protocol);};
sockjs.onmessage = function(e) {print('[.] message', e.data);};
sockjs.onclose   = function()  {print('[*] close');};

// 产生交互信息
sockjs.send(‘some message’);

3. STOMP

Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理(Broker)进行交互。 简单来说,就好像HTTP定义了TCP的相关细节一样,STOMP在WebSocket协议之上,告诉信息交互的双方,消息的格式是什么,应该怎样收发的文本协议。具体的定义内容为: STOMP 是基于 frame(帧)的协议,每个frame都包含了一个 command,一系列的可选 headers 和消息本身的 body,如下:

代码语言:javascript复制
COMMAND
header1:value1
header2:value2

Body^@

上面的空行部分必需,分割 headers 和 body。除了上述的帧内容的定义,协议还对不同的操作定义了不同 COMMAND 的帧。

代码语言:javascript复制
// 客户端:
SEND			// 发送消息到服务端,可添加自定义的 header,body 携带内容
SUBSCRIBE		// 用于注册给定目的地send帧,被注册的目的地收到任何消息豆浆通过MESSAGE帧发送过来
UNSUBSCRIBE	// 取消注册监听
BEGIN			// 事务操作开始
COMMIT			// 事务提交
ABORT			// 事务过程中的回滚
ACK			// 确认订阅消息的消费
NACK			// NACK有ACK相反地作用。它地作用是告诉 server client 不想消费这个消息
DISCONNECT		// 断开连接
// 服务端
CONNECT		// 连接建立
RECEIPT		// server 成功处理请求带有 receipt 的 client frame 后的返回
ERROR			// 如果出错的话,server将发送ERRORframe
MESSAGE		// 将订阅的消息发送给client

更多命令详解,可参考STOMP协议参考

4. 结合使用

在了解了上诉两个协议之后,我们需要把两方结合起来,让 WebSocket 消息操作变得规范,可控,易于理解。因为 STOMP 协议和 WebSocket 都有已经实现了且可靠的库,在这里我们直接采用。WebSocket 采用 sockjs,STOMP 采用 stompjs。

代码语言:javascript复制
// 服务端主要代码:
var http = require("http");
var StompServer = require('stomp-broker-js');

var server = http.createServer();

server.listen(61614);

var stompServer = new StompServer({
  server: server,
  path: '/stomp'
});
// 将监听的客户端放入列表中,方便服务端在接受到消息之后进行转发
stompServer.on('connected', function(sessionId, headers){
  var clientId = headers.clientId;
  if(clientId) {
    clients.push(clientId);
  }
});

stompServer.on('error', function(error) {
  // 将订阅对象减少一个(错误对象)
  clients.splice(clients.length - 1, 1);
  return;
});

// 在每次对应的 roomid 频道收到消息时,转发给所有的订阅者
stompServer.subscribe(config.desination, function(msg, headers) {
  for(var i = 0; i < clients.length; i  ) {
    // 如果时debug,打印每次的请求和消息
    if(config.debug) {
      console.log('header: n'   headers);
      console.log('msg: n'   msg);
    }
    // 消息转发
    stompServer.send(clients[i], {'content-type': 'application/json'}, JSON.stringify({
      headers: headers,
	  msg: msg
    }));
  }
});

此处的服务端代码,是直接传入创建的 server,即可使得 server 支持 STOMP 协议。其实在这一步时做了很多工作。其中就有,调用 stompjs 库,将 sockjs 的消息发送用 stomp 进行改写,将 WebSocket 的方法统统用 STOMP 协议的方法进行了包装一遍。这里举消息包装和方法包装的例子说明。

代码语言:javascript复制
// 当调用 websocket 的 send 方法的时候
this.send = function (topic, headers, body) {
  // 将消息内容组装成 stomp 协议的一帧
  var _headers = {};
  if (headers) {
    for (var key in headers) {
      _headers[key] = headers[key];
    }
  }
  var frame = {
    body: body,
    headers: _headers
  };
  var args = {
    dest: topic,
    frame: this.frameParser(frame)
  };
  this.onSend(selfSocket, args);
}

this.onSend = function (socket, args, callback) {
  ...
  this.emit('send', {frame: {headers: frame.headers, body: bodyObj}, dest: args.dest});
  // 将消息发送给订阅方
  this._sendToSubscriptions(socket, args);
  if (callback) {
    callback(true);
  }
};

this._sendToSubscriptions = function (socket, args) {
  ...
  // 确定订阅方,凭借上 command,进行发送
  args.frame.command = "MESSAGE";
  var sock = sub.socket;
  if (sock !== undefined) {
    stomp.StompUtils.sendFrame(sock, args.frame);
  } else {
    this.emit(sub.id, args.frame.body, args.frame.headers);
  }
};
// 发送的时候,还是采用 WebSocket 的发送
function sendFrame(socket, _frame) {
  var frame_str = null;
  if (!_frame.hasOwnProperty('toString')) {
    var frame = new Frame({
      'command': _frame.command,
      'headers': _frame.headers,
      'body': _frame.body
    });
    frame_str = frame.toString();
  } else {
    frame_str = _frame.toString();
  }
  // WebSocket 发送
  socket.send(frame_str);
  return true;
}
代码语言:javascript复制
// 客户端主要代码:
var url = "ws://localhost:61614/stomp";
var client = Stomp.client(url);

function afterConnect(roomid) {
  btn.addEventListener('click', function () {
    var msg = input.value;
    // 发送信息
    client.send(roomid, {}, msg);
  }, false);
}

function createConnect(roomid, uid) {
  client.connect(headers, function (error) {
    if (error.command == "ERROR") {
      console.error(error.headers.message);
    } else {
      afterConnect(roomid);
      // 订阅自己的客户端id,方便收到服务器发送过来的信息
      client.subscribe(uid, function (msg) {
        var body = msg.body;
        if (msg.headers['content-type'] == 'application/json') {
          body = JSON.parse(msg.body)
        }
      });
    }
  });
}

点击查看完整demo

总结

在各方面了解完 WebSocket 和 STOMP 相关内容之后,其实我们可以发现,STOMP 是个很简单的协议,但是这个简单协议却能有效的规约前后端的交互过程,使交互过程清晰有效。这种用简单高效的抽象,完成通用复杂的工作的方法,其实是很值得我们去借鉴的。另外,在完成这部分内容的探索学习过程中,还顺便学习了一下 npm 包发布的相关内容。感觉学习新东西确实是总能给人带来益处,大家加油!

0 人点赞