SpringBoot+Netty实现WebSocket服务器

前言

         传统的请求-应答模式(http)越来越不能满足现实需求,服务器过于被动,而采用轮训或者long poll的方式过于浪费资源,这便有了WebSocket。WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说,二者区别如下。

  • HTTP是运行在TCP协议传输层上的应用协议,而WebSocket是通过HTTP协议协商如何连接,然后独立运行在TCP协议传输层上的应用协议。
  • Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
  • websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信

接下来使用一个小例子来实现服务器往客户端的主动推送功能。

示例

index.html




    
    无标题文档
    





服务端返回的应答消息

第一次握手请求由客户端发起,当服务器收到握手请求后,返回响应,这时客户端收到详情并打开socket完成握手,这样就建立了服务器与客户端之间的tcp长连接,对于 WebSocket 来说,它必须依赖HTTP协议的第一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

服务器目录结构如下:

   SpringBoot+Netty实现WebSocket服务器_第1张图片

首先看下启动类:

WebsocketApplication.java
package com.jhz.websocket;

import com.jhz.websocket.server.NettyServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebsocketApplication {
	public static void main(String[] args) throws Exception {
		SpringApplication.run(WebsocketApplication.class, args);
		new NettyServer(12345).start();
	}
}

在启动类中启动NettyServer(Netty服务器)。

NettyServer.java
package com.jhz.websocket.server;

import com.jhz.websocket.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;


/**
 * @author jhz
 * @date 18-10-21 下午9:45
 */
public class NettyServer {
    private final int port;

    public NettyServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap sb = new ServerBootstrap();
            sb.option(ChannelOption.SO_BACKLOG, 1024);
            sb.group(group, bossGroup) // 绑定线程池
                    .channel(NioServerSocketChannel.class) // 指定使用的channel
                    .localAddress(this.port)// 绑定监听端口
                    .childHandler(new ChannelInitializer() { // 绑定客户端连接时候触发操作

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("收到新连接");
                            //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            //以块的方式来写的处理器
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new HttpObjectAggregator(8192));
                            ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
                            ch.pipeline().addLast(new WebSocketHandler());
                        }
                    });
            ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
            System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
            cf.channel().closeFuture().sync(); // 关闭服务器通道
        } finally {
            group.shutdownGracefully().sync(); // 释放线程池资源
            bossGroup.shutdownGracefully().sync();
        }
    }
}

这里要注意这四个Handler,HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler,其中HttpServerCodec用于对HttpObject消息进行编码和解码,但是HTTP请求和响应可以有很多消息数据,你需要处理不同的部分,可能也需要聚合这些消息数据,这是很麻烦的。为了解决这个问题,Netty提供了一个聚合器,它将消息部分合并到FullHttpRequest和FullHttpResponse,因此不需要担心接收碎片消息数据,这就是HttpObjectAggregator的作用;ChunkedWriteHandler,允许通过处理ChunkedInput来写大的数据块;而WebSocketServerProtocolHandler是Netty封装好的WebSocket协议处理类,有了它可以少写很多步骤,包括握手的过程,以及url的定义(这里的/ws其实就定义了url指定的后缀)。

WebSocketHandler.java
package com.jhz.websocket.handler;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Scanner;

/**
 * @author jhz
 * @date 18-10-21 下午9:51
 */
public class WebSocketHandler extends SimpleChannelInboundHandler{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端建立连接,通道开启!");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("与客户端断开连接,通道关闭!");
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("客户端收到服务器数据:" + msg.text());
        Scanner s = new Scanner(System.in);
        System.out.println("服务器推送:");
        while(true) {
            String line = s.nextLine();
            if(line.equals("exit")) {
                ctx.channel().close();
                break;
            }
            String resp= "(" +ctx.channel().remoteAddress() + ") :" + line;
            ctx.writeAndFlush(new TextWebSocketFrame(resp));
        }
    }

}

可以看到,建立长连接的过程都由WebSocketServerProtocolHandler为我们做完了(但是个人觉得还是要去自己写一次http握手的处理过程,Netty也做了一些封装,非常方便),客户端与服务器之间形成了一个全双工通讯的管道。

DefaultController.java
package com.jhz.websocket.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author jhz
 * @date 18-10-21 下午8:25
 */
@Controller
public class DefaultController {
    @RequestMapping("/")
    public String index(){
        return "index";
    }
}

application.properties

# 定位模板的目录
spring.mvc.view.prefix=classpath:/templates/
# 给返回的页面添加后缀名
spring.mvc.view.suffix=.html

附上依赖:

build.gradle

buildscript {
	ext {
		springBootVersion = '2.0.6.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.jhz'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	maven {
		name "aliyunmaven"
		url "http://maven.aliyun.com/nexus/content/groups/public/"
	}
}


dependencies {
	implementation('org.springframework.boot:spring-boot-starter-websocket')
	testImplementation('org.springframework.boot:spring-boot-starter-test')
	compile group: 'io.netty', name: 'netty-all', version: '5.0.0.Alpha2'
	compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.4.RELEASE'
	compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.0.4.RELEASE'
}

测试结果

在前端页面发送123:

SpringBoot+Netty实现WebSocket服务器_第2张图片

在服务器的控制台可以看到已经收到了消息:

SpringBoot+Netty实现WebSocket服务器_第3张图片

在服务器控制台推送消息“456”、“789”,再次查看前端页面:

SpringBoot+Netty实现WebSocket服务器_第4张图片

WebSocket的小Demo便完成了。

小结

          在前面的IO章节中,已经对比了使用Netty与传统的NIO方式的区别,Netty是高度封装的NIO框架,用起来会比传统的NIO编程方式方便很多,而其对WebSocket的支持同样为我们带来了极大的便利,WebSocket服务器在接收到客户端消息时需要对其判断,这个消息是http消息还是已经建立tcp连接的WebSocketFrame消息,若是前者,则代表是握手请求,服务器需要对握手请求进行响应,通常的写法如下:

private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {

        //如果不是WebSocket握手请求消息,那么就返回 HTTP 400 BAD REQUEST 响应给客户端。
        if (!req.getDecoderResult().isSuccess()
                || !("websocket".equals(req.headers().get("Upgrade")))) {
            sendHttpResponse(ctx, req,
                    new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

        //如果是握手请求,那么就进行握手
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                WEB_SOCKET_URL, null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else {

            // 通过它构造握手响应消息返回给客户端,
            // 同时将WebSocket相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码,
            // 添加WebSocketEncoder和WebSocketDecoder之后,服务端就可以自动对WebSocket消息进行编解码了
            handshaker.handshake(ctx.channel(), req);
        }
    }

而使用WebSocketServerProtocolHandler就能为我们省下很多事了。其实通常使用tomcat不需要我们实现WebSocket,从tomcat7之后就开始支持Websocket了,这里为了进一步的学习一下Netty,但是万一不用Tomcat呢?相对于Tomcat这种Web Server(顾名思义主要是提供Web协议相关的服务的),Netty是一个 是一个Network Server,是处于Web Server更下层的网络框 架,也就是说你可以使用Netty模仿Tomcat做一个提供HTTP服务的Web容器。简而言之,Netty通过使用NIO的很多新特性,对TCP/UDP编程进行了简化和封 装,提供了更容易使用的网络编程接口,让你可以根据自己的需要封装独特的HTTP Server或者FTP Server等.
 

你可能感兴趣的:(netty)