一、netty整合websockte

1.1、配置 NettyServer

创建NettyServer:定义两个EventLoopGroup,bossGroup辅助客户端的tcp连接请求,workGroup负责与客户端之间的读写操作。
注意:需要开启一个新的线程来执行netty server, 要不然会阻塞主线程,到时候就无法调用项目的其他controller接口了。

@Component
public class NettyServer {

    @Autowired
    private MyWebSocketHandler webSocketHandler;

    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);

    public int port = 6655;

    public void start() throws Exception{
        // 主线程组,用于接受客户端的连接,但是不做任何处理,跟老板一样
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 从线程组,主线程组会把任务丢给它,让从线程组去做相应的处理
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {

            ServerBootstrap bootstrap = new ServerBootstrap();
            //boss 等待连接的 队列长度
            bootstrap.option(ChannelOption.SO_BACKLOG,1024);
            // bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
            bootstrap.group(workGroup,bossGroup);//绑定线程池
            //指定使用的channel 设置NIO类型的channel
            bootstrap.channel(NioServerSocketChannel.class);
            //绑定监听端口
            bootstrap.localAddress(port);
            // 连接到达时会创建一个通道
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
	                log.info("收到新连接");
	                // webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
	                ch.pipeline().addLast(new HttpServerCodec());
	                //以块的方式来写处理器
	                ch.pipeline().addLast(new ChunkedWriteHandler());
	                //http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
	                ch.pipeline().addLast(new HttpObjectAggregator(8192));
	                // 自定义的handler,处理业务逻辑
	                ch.pipeline().addLast(webSocketHandler);
	                // 1、对应webSocket,它的数据是以帧(frame)的形式传递
	                // 2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
	                // 3、核心功能是将http协议升级为ws协议,保持长连接/
	                ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws",null,true,65536*10));
                }
            });
            // 配置完成,绑定server,通过调用sync同步方法阻塞直到绑定成功
            ChannelFuture cf = bootstrap.bind().sync(); // 服务器异步创建绑定
            log.info("启动监听:{}",cf.channel().localAddress());
            // 关闭服务器通道
            cf.channel().closeFuture().sync();
        } finally {//释放资源
            if(bossGroup != null){
                bossGroup.shutdownGracefully().sync();
            }
            if(workGroup != null){
                workGroup.shutdownGracefully().sync();
            }
        }
    }
}

1.2、创建通道连接池

@Component
public class MyChannelHandlerPool {

    public MyChannelHandlerPool(){}

    /**
     * 管理全局的 channel
     * GlobalEventExecutor.INSTANCE  全局事件监听器
     * 一旦将channel 加入 ChannelGroup 就不要用手动去管理channel的连接失效后移除操作,他会自己移除
     */
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存放用户与Chanel的对应信息,用于给指定用户发送消息
     * 是一个支持高并发更新与查询的哈希表
     */
    private static ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap<>();

    /**
     * 存放用户群组对应的用户
     */
    private static ConcurrentHashMap<String, List<String>> chatGroup = new ConcurrentHashMap<>();

    /**
     * 获取用户 channel map
     * @return
     */
    public static ConcurrentHashMap<String, Channel> getUserChannelMap(){
        return userChannelMap;
    }

    /**
     * 获取用户 channel map
     * @return
     */
    public static ConcurrentHashMap<String, List<String>> getChatGroupMap(){
        return chatGroup;
    }
}

1.3、业务逻辑和通道管理

@Component
@ChannelHandler.Sharable
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static final Logger log = LoggerFactory.getLogger(MyWebSocketHandler.class);

    /**
     * channel连接状态就绪以后调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        MyChannelHandlerPool.channelGroup.add(channel);
        log.info("新设备上线了~");

    }

    /**
     * channel连接状态断开后触发
     * 同时当前channel会自动从ChannelGroup中被移除
     * @param ctx
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        Attribute<String> userId = channel.attr(key);
        //这里 ChannelGroup 底层封装会遍历给所有的channel发送消息
        //MyChannelHandlerPool.channelGroup.writeAndFlush(new TextWebSocketFrame("【用户"+userId.get()+"】离开聊天!==> "+new Date()));
        MyChannelHandlerPool.getUserChannelMap().remove(userId.get());
        //打印 ChannelGroup中的人数
        log.info("【{}】连接关闭!",userId.get());
        log.info("当前在线数为: {}", MyChannelHandlerPool.channelGroup.size());
    }

    /**
     * 连接发生异常时触发
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("连接发生异常!");
        ctx.close();
        log.error(cause.toString());
    }

    /**
     * 通道数据读取
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //首次连接是FullHttpRequest
        //以http请求形式接入,但是走的是websocket
        if (null != msg && msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String url = request.uri();
            Map paramMap = getUrlParams(url);
            log.info("接收到的参数是:{}", JSONObject.toJSONString(paramMap));
            // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
            AttributeKey<String> key = AttributeKey.valueOf("userId");
            ctx.channel().attr(key).setIfAbsent((String) paramMap.get("id"));
            //用户id与连接channel绑定
            MyChannelHandlerPool.getUserChannelMap().put((String) paramMap.get("id"), ctx.channel());
            //如果url包含参数,需要处理
            if(url.contains("?")){
                String newUri=url.substring(0,url.indexOf("?"));
                request.setUri(newUri);
            }
        }else if(msg instanceof TextWebSocketFrame){
            //正常的TEXT消息类型
            TextWebSocketFrame frame=(TextWebSocketFrame)msg;
            String data = frame.text();
            log.info("客户端收到服务器数据:{}",data);
            JSONObject jsonObject = (JSONObject) JSONObject.parse(data);
            String uid = jsonObject.getString("uid");
            String targetId = jsonObject.getString("targetId");
            jsonObject.remove("targetId");
            jsonObject.put("source",uid);
            jsonObject.remove("uid");
            sendTargetUserMessage(jsonObject,targetId,uid);
        }
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {

    }

    /**
     * 收到信息后,发给指定用户所在的channel
     * @param message
     */
    private void sendTargetUserMessage(JSONObject message, String targetId,String sourceId){
        JSONObject newMsg = new JSONObject();
        Channel sourceChannel = MyChannelHandlerPool.getUserChannelMap().get(sourceId);
        Channel channel2 = MyChannelHandlerPool.getUserChannelMap().get(targetId);
        if (null != channel2) {
            channel2.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(message)));
            if("printTask".equals(message.getString("msgType"))){
                newMsg.put("msgType","havePrinter");
                sourceChannel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(newMsg)));
            }
        }else {
            newMsg.put("msgType","noPrinter");
            sourceChannel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(newMsg)));
            log.error("目标{}不存在",targetId);
        }
    }

    /**
     * 获取路径中的参数值
     * @param url
     */
    private static Map getUrlParams(String url){
        Map<String,String> map = new HashMap<>();
        url = url.replace("?",";");
        if (!url.contains(";")){
            return map;
        }
        if (url.split(";").length > 0){
            String[] arr = url.split(";")[1].split("&");
            for (String s : arr){
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key,value);
            }
            return  map;

        }else{
            return map;
        }
    }

}

1.4、服务启动

实现CommandLineRunner 接口,在容器启动后执行如下操作。

@Service
public class NettyStart implements CommandLineRunner {
    @Autowired
    private NettyServer nettyServer;

    @Override
    public void run(String... args) {
        new Thread(()->{
            try {
                nettyServer.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

1.5、浏览器页面连接websocket

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Netty-Websocket</title>
</head>
<body>
<form onSubmit="return false;">
    <label>当前用户ID</label><input type="text" id="uid" value="1" readonly="true"/> <br />
    <label>目标用户ID</label><input type="text" id="targetId" placeholder="这里输入目标用户ID"/> 
    <br/>
    <label>发送的消息</label><input type="text" id="message" placeholder="这里输入消息"/> <br />
   
    <input type="button" value="连接webSocket" onClick="connect(this)" />
    <input type="button" value="断开连接" onClick="closeConnect()" />
    <input type="button" value="发送ws消息" onClick="send()" />
    
    <hr color="black" />
    <h3>服务端返回的应答消息</h3>
    <textarea id="responseText" style="width: 1024px;height: 300px;"></textarea>
</form>
</body>
<script type="text/javascript">
    let socket;
    if(!window.WebSocket){
        window.WebSocket = window.MozWebSocket;
    }
    function connect() {
        if(window.WebSocket){
            var uid = document.getElementById('uid').value;
            socket = new WebSocket("ws://127.0.0.1:6655/ws?id="+uid);
            //有消息时触发
            socket.onmessage = function(event){
                var ta = document.getElementById('responseText');
                ta.value += event.data+"\r\n";
            };
            //socket打开时触发
            socket.onopen = function(event){
                var ta = document.getElementById('responseText');
                ta.value = "欢迎您上线! \r\n";
            };
            //socket关闭时触发
            socket.onclose = function(event){
                var ta = document.getElementById('responseText');
                ta.value += "下线成功,欢迎再次上线!\r\n";
            };
        }else{
            alert("您的浏览器不支持WebSocket协议!");
        }
    }
    //发送消息
    function send(message){
        if(!window.WebSocket){return;}
        // WebSocket 是否建立连接
        if(socket.readyState == WebSocket.OPEN){
            var msgData = {
                uid:document.getElementById('uid').value,
                targetId:document.getElementById('targetId').value,
                msg:document.getElementById('message').value,
            };
            socket.send(JSON.stringify(msgData));
            document.getElementsByName('message').value = "";
        }else{
            alert("WebSocket 连接没有建立成功!");
        }
    }
    function closeConnect() {
        socket.close()
    }
</script>
</html>

使用netty整合springmvc_json


使用netty整合springmvc_websocket_02

二、ws升级wss

ws升级wss需要使用ssL证书,ssL证书是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密。

2.1、自签证书

注意:自签的证书有且之能在本机使用,如将A机生成的证书拷贝B机使用也会出现同样的错误

1.1、win+r输入cmd打开命令窗口
1.2、生成自己jks文件,指定文件保存位置

注意:自签的证书有且之能在本机使用,如将A机生成的证书拷贝B机使用也会出现同样的错误

jks文件地址:C:\Users\admin\Desktop\aa\mystore.jks
dns解析的域名:www.wu.com

keytool -genkey -alias server -keyalg RSA -validity 3650 -keystore C:\Users\admin\Desktop\aa\mystore.jks -ext san=dns:www.wu.com -storepass 1234567
# keytool -genkey -alias server -keyalg RSA -validity 3650 -keystore jks文件地址 -ext san=dns:解析域名 -storepass 密码

使用netty整合springmvc_java_03

查看信息jks文件信息

# keytool -list -keystore jks地址 -v 
keytool -list -keystore mystore.jks -v

1.3、生成cer证书

# keytool -alias server -exportcert -keystore jks文件地址 -file 生成证书的位置 -storepass jks密码
keytool -alias server -exportcert -keystore C:\Users\admin\Desktop\aa\mystore.jks -file C:\Users\admin\Desktop\aa\mystore.cer -storepass 1234567

使用netty整合springmvc_websocket_04


成功生成的证书:

使用netty整合springmvc_java_05


1.4、NettyServer配置改动

1.4.1、NettyServer配置改动

新建一个ssl工具类

public class SslUtil {
    
    /**
     * @param type 证书类型
     * @param path 证书路径
     * @param password 证书密码
     * @throws Exception
     */
    public static SSLContext  createSSLContext(String type , String path , String password) throws Exception {
        InputStream inputStream = new FileInputStream(path);
        char[] passArray = password.toCharArray();
        SSLContext sslContext = SSLContext.getInstance("SSLv3");
        KeyStore ks = KeyStore.getInstance(type);
        //加载keytool 生成的文件
        ks.load(inputStream, passArray);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, passArray);
        sslContext.init(kmf.getKeyManagers(), null, null);
        inputStream.close();
        return sslContext;
    }
}

在NettyServer中添加如下代码

SSLContext sslContext = SslUtil.createSSLContext("JKS","C:\\Users\\admin\\Desktop\\aa\\mystore.jks","1234567");
//SSLEngine 此类允许使用ssl安全套接层协议进行安全通信
SSLEngine engine = sslContext.createSSLEngine();
engine.setUseClientMode(false); //服务器端模式
engine.setNeedClientAuth(false); //不需要验证客户端
ch.pipeline().addFirst("ssl", new SslHandler(engine));

使用netty整合springmvc_ssl_06

1.4.2、前端连接改动,将ws改为wss,并将127.0.0.1改成www.wu.com(生成jks文件时指定dns解析为www.wu.com),hosts文件添加域名映射

修改hosts文件
Windows系统中hosts文件路径:C:\Windows\System32\drivers\etc\hosts
在末尾添加域名ip映射,修改hosts后需要刷新DNS缓存使之生效
在cmd命令行中执行命令:ipconfig/flushdns

使用netty整合springmvc_ssl_07


1.5、服务启动,前端页面访问

后端报错 io.netty.handler.codec.DecoderException: javax.net.ssl.SSLException: Received fatal alert: certificate_unknown,

这是因为jdk认为我们的证书不可信,我们需要将证书变成jdk信任的证书。

使用netty整合springmvc_json_08


1.6、对证书添加信任

  1. 双击cer证书,安装证书




  2. 导入证书到信任库中
    密钥库默认密码:changeit
keytool -import -alias mycacert -file C:\Users\admin\Desktop\aa\mystore.cer -storepass changeit
  1. 查看密钥库中的证书
keytool -v -list -keystore .keystore
  1. 查看密钥库中指定的证书
# keytool -list -keystore .keystore -alias 证书别名
keytool -list -keystore .keystore -alias foo
检查密钥库,当证书成功导入后们就可以重新启动服务,连接websocket

2.2、权威机构签发的证书

1. 下载证书jks文件,文件密钥
2. 获取网站证书

使用netty整合springmvc_java_09


使用netty整合springmvc_java_10


使用netty整合springmvc_websocket_11


使用netty整合springmvc_websocket_12


使用netty整合springmvc_json_13


使用netty整合springmvc_ssl_14


使用netty整合springmvc_json_15


使用netty整合springmvc_ssl_16


3. 安装证书、导入证书到信任库中和上述相同,不在赘述。

三、证书失效

当证书过了有效期限,证书则需要更换,在更换新证书前。务必将旧证书卸载并将证书移除密钥库。

3.1、 证书卸载

Win+R ==> certmgr.msc ==> 找到对应的证书,点击上面的删除

使用netty整合springmvc_使用netty整合springmvc_17


使用netty整合springmvc_ssl_18


使用netty整合springmvc_java_19


使用netty整合springmvc_websocket_20

注意:这里的“颁发给“名称,我们点击要安装的cer时,看到的名称,可能并不是我们的XXX.cer这个文件名

3.2、将证书从密钥库中移除

# keytool -delete -alias 证书别名 -storepass 密钥库密码
keytool -delete -alias mycacert -storepass changeit

3.3、重新获取证书,按上述步骤操作即可