【案例1.0】基于WebSocket的请求响应

文章目录

  • 一、前言
  • 二、案例准备
    • (1)编写Server端
      • (1)编写WebSocketChannelInitializer
      • (2)编写TextWebSocketFrameHandler
    • (2)编写html客户端
    • (3)运行客户端测试
  • 三、案例总结
  • 四、WebSocket与socket的区别

一、前言

一种被称作“Upgrade handshake(升级握手)”的机制能够将标准的HTTP或者HTTPS协议转成WebSocket。所以,应用程序如果使用了WebSocket,那么它都是以HTTP/S开始,之后再进行升级,升级会发生在什么时候是不确定的,要根据具体的应用来决定:可能是在应用启动的时候,也可能是当一个特定的URL被请求的时候

WebSocket介绍 中讲解了WebSocket的相关事宜,接下来我们做一个基于WebSocket的客户端(也指Browser)与服务器间的请求响应DEMO.

在我们的应用中,要想升级协议为 \WebSocket,只有当URL请求以"/ws"结束时才可以,如果没有达到该要求,服务器仍将使用基本的 HTTP/S,一旦连接升级,之后的数据传输都将使用 WebSocket

二、案例准备

(1)编写Server端

public class MyServer {
  private static final int port = 8989;
  public static void main(String[] args) throws InterruptedException {
    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap sb = new ServerBootstrap();
      sb.group(bossGroup, workerGroup);
      sb.handler(new LoggingHandler(LogLevel.INFO));
      sb.channel(NioServerSocketChannel.class);
      sb.childHandler(new WebSocketChannelInitializer());
      //端口绑定
      ChannelFuture channelFuture = sb.bind(new InetSocketAddress(port)).sync();
      channelFuture.channel().closeFuture().sync();
    }finally {
      bossGroup.shutdownGracefully();
      workerGroup.shutdownGracefully();
    }
  }
}

(1)编写WebSocketChannelInitializer

public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
  @Override
  protected void initChannel(SocketChannel socketChannel) throws Exception {
    ChannelPipeline pipeline = socketChannel.pipeline();
    //因为这是基于http协议的,所以使用http编解码器。前面已经讲过了
    pipeline.addLast(new HttpServerCodec());
    //以块的方式去写
    pipeline.addLast(new ChunkedWriteHandler());
    /*特别重要,http数据在传输过程是分段的,
     *HttpObjectAggregator, 而他就是将多个段聚合起来*/
    pipeline.addLast(new HttpObjectAggregator(8989));
    pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
    pipeline.addLast(new WebSocketFrameHandler());
  }
}

1.扩展 ChannelInitializer

2.添加 ChannelHandler 到 ChannelPipeline

initChannel()方法用于设置所有新注册的Channel到ChannelPipeline中。总结如下:

表格 Value

WebSocketServerProtocolHandler用于处理我们没有使用的Frame,在(2)中会详细展开


(2)编写TextWebSocketFrameHandler

TextWebSocketFrameHandler用来处理WebSocket的frame(帧),用来发送聊天消息。

//这里的泛型是TextWebSocketFrame,表示一个文本帧(Frame)
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
  @Override
  protected void channelRead0(ChannelHandlerContext channelHandlerContext,
                                      TextWebSocketFrame textWebSocketFrame) throws Exception {
    System.out.println("收到客户端消息:" + textWebSocketFrame.text());
    channelHandlerContext.writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now()));  //根据绑定特定协议的处理器来定义参数类型
  }
  @Override
  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    //id表示唯一,有长有短,长的asLongText,唯一。短的asShortText()不唯一
    System.out.println("handlerAdded的ID:"+ctx.channel().id().asShortText());
  }
  /*一个很有趣的现象,如果客户端刷新一下,实际上会调用这个方法,因为连接断了,新建了一个连接*/
  @Override
  public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    System.out.println("handlerRemoved的ID:"+ctx.channel().id().asLongText());
  }
  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    System.out.println("异常" + ctx.channel().id().asLongText());
    ctx.close();
  }
}

WebSocket"Request for Comments"(RFC) 定义了六种不同的 frame; Netty都提供了一个POJO实现 ,见下表:

名称 描述
BinaryWebSocketFrame contains binary data
TextWebSocketFrame contains text data
ContinuationWebSocketFrame contains text or binary data that belongs to a previous BinaryWebSocketFrame or TextWebSocketFrame
CloseWebSocketFrame represents a CLOSE request and contains close status code and a phrase
PingWebSocketFrame requests the transmission of a PongWebSocketFrame
PongWebSocketFrame sent as a response to a PingWebSocketFrame

我们的程序只需要使用下面4个帧类型:
CloseWebSocketFrame、PingWebSocketFrame、PongWebSocketFrame、TextWebSocketFrame

在上面我们只使用了TextWebSocketFrame,是因为其他Frame会由 WebSocketServerProtocolHandler自动处理。TA处理所有规定的WebSocket帧类型和升级握手本身

  • 若握手成功,则所需的ChannelHandler被添加到管道,而那些不再需要的则被去除。管道升级之前的状态如下图。这代表了ChannelPipeline刚刚经过ChatServerInitialize 初始化。
    【案例1.0】基于WebSocket的请求响应_第1张图片
    握手升级成功后 WebSocketServerProtocolHandler负责替换HttpRequestDecoder 为 WebSocketFrameDecoder,HttpResponseEncoder 为WebSocketFrameEncoder。 为了最大化性能,WebSocket 连接不需要的 ChannelHandler 将会被移除。其中就包括了 HttpObjectAggregator 和 HttpRequestHandler
    【案例1.0】基于WebSocket的请求响应_第2张图片

(2)编写html客户端

对于webSocket,数据传输是以frame(帧)的形式传递,"/ws"表示的websocket的地址。

 <html lang="en">
<head> <meta charset="UTF-8"> <title>Titletitle> head>
<body>
<script>
  var socket;
  if(window.WebSocket){
    socket=new WebSocket("ws://localhost:8989/ws");
    //相当于channelRead0,ev收到服务器端发来的消息
    socket.onmessage=function (ev) {
      var rt=document.getElementById("responseText");
      rt.value=rt.value+"\n"+ev.data;
    }
    //相当于连接开启
    socket.onopen=function (ev) {
      var rt=document.getElementById("responseText");
      rt.value="连接开启";
    }
    //相当于连接关闭
    socket.onclose=function (ev) {
      var rt=document.getElementById("responseText");
      rt.value=rt.value+"\n"+"连接关闭";
    }
  }
  else { alert("您的浏览器不适合WebSocket"); }
  function send(message) {
    if (!window.socket){ return; }
    if (socket.readyState == WebSocket.OPEN){ socket.send(message); }
    else { alert("连接未开启"); }
  }
script>
<form onsubmit="return false">
  <textarea name="message" style="height: 200px;width: 400px">textarea>
  
  <input type="button" value="发送数据" onclick="send(this.form.message.value)">
  <textarea id="responseText" style="width: 400px;height: 200px">textarea>
  <input type="button" onclick="document.getElementById('responseText').value=''" value="清空内容">
form>
body>
html>

:在channelRead0()中的writeAndFlush()方法里面传入的是object类型,但是这里要传一个TextWebSocketFrame对象,表示要根据传入xxxContext的实际类型定义要返回内容,否则传送不了.


(3)运行客户端测试

【案例1.0】基于WebSocket的请求响应_第3张图片
输入一段字符串数据,点击发送数据,服务器控制台上打印了连接服务器的ChannelID、客户端发送的数据,如下图:
【案例1.0】基于WebSocket的请求响应_第4张图片
此时如果关闭了浏览器(注:断网、断电不会检测到),服务器的handlerRemoved()方法自动别调用 —> 输出了以下内容:

`handlerRemoved的ID:d4258bfffe5eb145-00001de8-00000001-0d93c897363c01c4-88ecd330

三、案例总结

基于WebSocket的HTTP请求响应图如下:
【案例1.0】基于WebSocket的请求响应_第5张图片
1. Client连接到服务器并加入聊天

2. HTTP请求页面或WebSocket升级握手

3. 服务器处理所有客户端/用户

4.1 如果服务器响应URI"/"的请求,直接返回void.

4.2 如果访问的是URI"/ws",则处理WebSocket升级握手

5. 升级握手完成后 ,通过 WebSocket 发送聊天消息


在开发者工具的ws中,可以看到
【案例1.0】基于WebSocket的请求响应_第6张图片

【案例1.0】基于WebSocket的请求响应_第7张图片

字段 Value
101状态码 表示由HTTP/S协议转成ws协议(协议升级)
Connection 必须设置 Upgrade,表示客户端希望连接升级。
Upgrade 必须设置 Websocket,表示希望升级到 Websocket 协议。
Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
Origin 可选,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。


四、WebSocket与socket的区别

软件通信有七层结构:

1. 下3层结构偏向与数据通信
2. 上3层更偏向于数据处理
3. 中间的传输层则是连接上三层与下三层之间的桥梁。每一层都做不同的工作,上层协议依赖与下层协议。

一、Socket并不是一个协议,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。当两台主机通信时,让Socket去组织数据,以符合指定的协议。

二、WebSocket则是一个典型的应用层协议

三、总的来说:Socket 是传输控制层协议的抽象,WebSocket是应用层协议。

附:TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。


你可能感兴趣的:(●,Netty)