SpringBoot项目整合WebSocket+netty实现前后端双向通信(同时支持前端webSocket和socket协议哦)

3 篇文章 0 订阅
2 篇文章 0 订阅
本文介绍了如何在SpringBoot项目中集成Netty,实现前后端通过WebSocket进行TCP协议通信,并通过Netty服务端处理来自客户端的连接。关键点在于自定义协议选择处理器,以支持同时处理WebSocket和TCP连接。文章详细展示了从添加依赖、编写服务端启动类、初始化类、连接处理器、协议解码器到业务处理器的整个过程,并提供了测试和代码仓库地址。
摘要由CSDN通过智能技术生成

目录

 

前言

技术栈

功能展示

一、springboot项目添加netty依赖

二、netty服务端

三、netty客户端

四、测试

五、代码仓库地址


  专属小彩蛋:前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站(前言 - 床长人工智能教程) 

前言

        最近做了一个硬件设备通信项目,需求是这样,前端使用webSocket向后端进行tcp协议的通信,后端netty服务端收到数据后,将数据发往socket客户端,客户端收到数据之后需要进行响应数据显示到前端页面供用户进行实时监控。

技术栈

        后端

  • springboot 
  • netty

        前端

  • 前端websocket

功能展示

前端页面输入webSocket地址,点击连接,输入待发送的数据,点击发送

 后端我们可以使用网络测试工具NetAssist 进行响应测试

 在工具中连接netty服务端,并点击发送按钮,可以看到,前端页面右侧对话框成功显示出了NetAssist测试工具响应的数据内容。接下来我们来看一看代码如何进行实现,关键的点在于需要同时支持前端websocket和后端socket的连接,需要自定义一个协议选择处理器。

一、springboot项目添加netty依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.dzx.netty</groupId>
    <artifactId>qiyan-project</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>qiyan-project</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.6.7</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.52.Final</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

二、netty服务端

 (1)netty服务启动类

package com.example.dzx.netty.qiyanproject.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * @author dzx
 * @ClassName:
 * @Description: netty服务启动类
 * @date 2023年06月30日 21:27:16
 */
@Slf4j
@Component
public class NettyServer {

    public void start(InetSocketAddress address) {
        //配置服务端的NIO线程组

        /*
         * 在Netty中,事件循环组是一组线程池,用于处理网络事件,例如接收客户端连接、读写数据等操作。
         * 它由两个部分组成:bossGroup和workerGroup。
         * bossGroup 是负责接收客户端连接请求的线程池。
         * workerGroup 是负责处理客户端连接的线程池。
         * */

        EventLoopGroup bossGroup = new NioEventLoopGroup(10);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //创建ServerBootstrap实例,boss组用于接收客户端连接请求,worker组用于处理客户端连接。
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(bossGroup, workerGroup)  // 绑定线程池
                    .channel(NioServerSocketChannel.class)//通过TCP/IP方式进行传输
                    .childOption(ChannelOption.SO_REUSEADDR, true) //快速复用端口
                    .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
                    .localAddress(address)//监听服务器地址
                    .childHandler(new NettyServerChannelInitializer())
//                    .childHandler(new com.ccp.dev.system.netty.NettyServerChannelInitializer())
                    .childOption(ChannelOption.TCP_NODELAY, true)//子处理器处理客户端连接的请求和数据
                    .option(ChannelOption.SO_BACKLOG, 1024)  //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
                    .childOption(ChannelOption.SO_KEEPALIVE, true);  //保持长连接,2小时无数据激活心跳机制

            // 绑定端口,开始接收进来的连接
            ChannelFuture future = bootstrap.bind(address).sync();
            future.addListener(l -> {
                if (future.isSuccess()) {
                    System.out.println("Netty服务启动成功");
                } else {
                    System.out.println("Netty服务启动失败");
                }
            });
            log.info("Netty服务开始监听端口: " + address.getPort());
            //关闭channel和块,直到它被关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("启动Netty服务器时出错", e);
        } finally {
            //释放资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

 (2)服务端初始化类编写,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器以及各项处理器

package com.example.dzx.netty.qiyanproject.server;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.stereotype.Component;


/**
 * @author dzx
 * @ClassName:
 * @Description: 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器以及各项处理器
 * @date 2023年06月30日 21:27:16
 */
@Component
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {

//    private FullHttpResponse createCorsResponseHeaders() {
//        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//
//        // 设置允许跨域访问的响应头
//        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
//        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE");
//        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization");
//        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "3600");
//
//        return response;
//    }

    @Override
    protected void initChannel(SocketChannel channel) {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast("active", new ChannelActiveHandler());
        //Socket 连接心跳检测
        pipeline.addLast("idleStateHandler", new IdleStateHandler(60, 0, 0));
        pipeline.addLast("socketChoose", new SocketChooseHandler());
        pipeline.addLast("commonhandler",new NettyServerHandler());
    }
}

(3) 编写新建连接处理器

package com.example.dzx.netty.qiyanproject.server;

import com.example.dzx.netty.qiyanproject.constants.General;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;

/**
 * @author dzx
 * @ClassName:
 * @Description: 客户端新建连接处理器
 * @date 2023年06月30日 21:27:16
 */

@ChannelHandler.Sharable
@Slf4j
public class ChannelActiveHandler extends ChannelInboundHandlerAdapter {

    /**
     * 有客户端连接服务器会触发此函数
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //获取客户端连接的远程地址
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        //获取客户端的IP地址
        String clientIp = insocket.getAddress().getHostAddress();
        //获取客户端的端口号
        int clientPort = insocket.getPort();
        //获取连接通道唯一标识
        ChannelId channelId = ctx.channel().id();
        //如果map中不包含此连接,就保存连接
        if (General.CHANNEL_MAP.containsKey(channelId)) {
            log.info("Socket------客户端【" + channelId + "】是连接状态,连接通道数量: " + General.CHANNEL_MAP.size());
        } else {
            //保存连接
            General.CHANNEL_MAP.put(channelId, ctx);
            log.info("Socket------客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
            log.info("Socket------连接通道数量: " + General.CHANNEL_MAP.size());
        }
    }
}

(4)编写协议初始化解码器,用来判定实际使用什么协议(实现websocket和socket同时支持的关键点就在这里)

package com.example.dzx.netty.qiyanproject.server;

/**
 * @author 500007
 * @ClassName:
 * @Description:
 * @date 2023年06月30日 21:29:17
 */

import com.example.dzx.netty.qiyanproject.constants.General;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author dzx
 * @ClassName:
 * @Description:  协议初始化解码器.用来判定实际使用什么协议,以用来处理前端websocket或者后端netty客户端的连接或通信
 * @date 2023年06月30日 21:31:24
 */
@Component
@Slf4j
public class SocketChooseHandler extends ByteToMessageDecoder {
    /** 默认暗号长度为23 */
    private static final int MAX_LENGTH = 23;
    /** WebSocket握手的协议前缀 */
    private static final String WEBSOCKET_PREFIX = "GET /";
    @Resource
    private SpringContextUtil springContextUtil;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        String protocol = getBufStart(in);
        if (protocol.startsWith(WEBSOCKET_PREFIX)) {
            springContextUtil.getBean(PipelineAdd.class).websocketAdd(ctx);

            //对于 webSocket ,不设置超时断开
            ctx.pipeline().remove(IdleStateHandler.class);
//            ctx.pipeline().remove(LengthFieldBasedFrameDecoder.class);
            this.putChannelType(ctx.channel().id(), true);
        }else{
            this.putChannelType(ctx.channel().id(), false);
        }
        in.resetReaderIndex();
        ctx.pipeline().remove(this.getClass());
    }

    private String getBufStart(ByteBuf in){
        int length = in.readableBytes();
        if (length > MAX_LENGTH) {
            length = MAX_LENGTH;
        }

        // 标记读位置
        in.markReaderIndex();
        byte[] content = new byte[length];
        in.readBytes(content);
        return new String(content);
    }

    /**
     *
     * @param channelId
     * @param type
     */
    public void putChannelType(ChannelId channelId,Boolean type){
        if (General.CHANNEL_TYPE_MAP.containsKey(channelId)) {
            log.info("Socket------客户端【" + channelId + "】是否websocket协议:"+type);
        } else {
            //保存连接
            General.CHANNEL_TYPE_MAP.put(channelId, type);
            log.info("Socket------客户端【" + channelId + "】是否websocket协议:"+type);
        }
    }
}

(5)给NettyServerChannelInitializer初始化类中的commonhandler添加前置处理器

package com.example.dzx.netty.qiyanproject.server;

import io.netty.channel.ChannelHandlerContext;
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;
import org.springframework.stereotype.Component;

/**
 * @author dzx
 * @ClassName:
 * @Description: 给NettyServerChannelInitializer初始化类中的commonhandler添加前置处理器
 * @date 2023年06月30日 21:31:24
 */
@Component
public class PipelineAdd {

    public void websocketAdd(ChannelHandlerContext ctx) {
        System.out.println("PipelineAdd");
        ctx.pipeline().addBefore("commonhandler", "http-codec", new HttpServerCodec());
        ctx.pipeline().addBefore("commonhandler", "aggregator", new HttpObjectAggregator(999999999));
        ctx.pipeline().addBefore("commonhandler", "http-chunked", new ChunkedWriteHandler());
//        ctx.pipeline().addBefore("commonhandler","WebSocketServerCompression",new WebSocketServerCompressionHandler());
        ctx.pipeline().addBefore("commonhandler", "ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));

//        ctx.pipeline().addBefore("commonhandler","StringDecoder",new StringDecoder(CharsetUtil.UTF_8)); // 解码器,将字节转换为字符串
//        ctx.pipeline().addBefore("commonhandler","StringEncoder",new StringEncoder(CharsetUtil.UTF_8));
        // HttpServerCodec:将请求和应答消息解码为HTTP消息
//        ctx.pipeline().addBefore("commonhandler","http-codec",new HttpServerCodec());
//
//        // HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
//        ctx.pipeline().addBefore("commonhandler","aggregator",new HttpObjectAggregator(999999999));
//
//        // ChunkedWriteHandler:向客户端发送HTML5文件,文件过大会将内存撑爆
//        ctx.pipeline().addBefore("commonhandler","http-chunked",new ChunkedWriteHandler());
//
//        ctx.pipeline().addBefore("commonhandler","WebSocketAggregator",new WebSocketFrameAggregator(999999999));
//
//        //用于处理websocket, /ws为访问websocket时的uri
//        ctx.pipeline().addBefore("commonhandler","ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));

    }
}

(6)编写业务处理器

package com.example.dzx.netty.qiyanproject.server;


import com.example.dzx.netty.qiyanproject.constants.General;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;
import java.util.Set;
import java.util.stream.Collectors;


/**
 * @author dzx
 * @ClassName:
 * @Description: netty服务端处理类
 * @date 2023年06月30日 21:27:16
 */
@Slf4j
public class NettyServerHandler extends SimpleChannelInboundHandler<Object> {


    //由于继承了SimpleChannelInboundHandler,这个方法必须实现,否则报错
    //但实际应用中,这个方法没被调用
    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buff = (ByteBuf) msg;
        String info = buff.toString(CharsetUtil.UTF_8);
        log.info("收到消息内容:" + info);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // WebSocket消息处理
        String strMsg = "";
        if (msg instanceof WebSocketFrame) {
            log.info("WebSocket消息处理************************************************************");
            strMsg = ((TextWebSocketFrame) msg).text().trim();
            log.info("收到webSocket消息:" + strMsg);
        }
        // Socket消息处理
        else if (msg instanceof ByteBuf) {
            log.info("Socket消息处理=================================");
            ByteBuf buff = (ByteBuf) msg;
            strMsg = buff.toString(CharsetUtil.UTF_8).trim();
            log.info("收到socket消息:" + strMsg);
        }
//        else {
//            strMsg = msg.toString();
//        }
        this.channelWrite(ctx.channel().id(), strMsg);
    }

    /**
     * 有客户端终止连接服务器会触发此函数
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();

        ChannelId channelId = ctx.channel().id();

        //包含此客户端才去删除
        if (General.CHANNEL_MAP.containsKey(channelId)) {
            //删除连接
            General.CHANNEL_MAP.remove(channelId);
            System.out.println();
            log.info("Socket------客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
            log.info("Socket------连接通道数量: " + General.CHANNEL_MAP.size());
            General.CHANNEL_TYPE_MAP.remove(channelId);
        }
    }


    /**
     * 服务端给客户端发送消息
     */
    public void channelWrite(ChannelId channelId, Object msg) throws Exception {

        ChannelHandlerContext ctx = General.CHANNEL_MAP.get(channelId);

        if (ctx == null) {
            log.info("Socket------通道【" + channelId + "】不存在");
            return;
        }

        if (msg == null || msg == "") {
            log.info("Socket------服务端响应空的消息");
            return;
        }

        //将客户端的信息直接返回写入ctx
        log.info("Socket------服务端端返回报文......【" + channelId + "】" + " :" + (String) msg);
//        ctx.channel().writeAndFlush(msg);
//        ctx.writeAndFlush(msg);
        //刷新缓存区
//        ctx.flush();
        //过滤掉当前通道id
        Set<ChannelId> channelIdSet = General.CHANNEL_MAP.keySet().stream().filter(id -> !id.asLongText().equalsIgnoreCase(channelId.asLongText())).collect(Collectors.toSet());
        //广播消息到客户端
        for (ChannelId id : channelIdSet) {
            //是websocket协议
            Boolean aBoolean = General.CHANNEL_TYPE_MAP.get(id);
            if(aBoolean!=null && aBoolean){
                General.CHANNEL_MAP.get(id).channel().writeAndFlush(new TextWebSocketFrame((String) msg));
            }else {
                ByteBuf byteBuf = Unpooled.copiedBuffer(((String) msg).getBytes());
                General.CHANNEL_MAP.get(id).channel().writeAndFlush(byteBuf);
            }
        }
    }

    /**
     * 处理空闲状态事件
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        String socketString = ctx.channel().remoteAddress().toString();

        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Socket------Client: " + socketString + " READER_IDLE 读超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Socket------Client: " + socketString + " WRITER_IDLE 写超时");
                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Socket------Client: " + socketString + " ALL_IDLE 总超时");
                ctx.disconnect();
            }
        }
    }

    /**
     * @DESCRIPTION: 发生异常会触发此函数
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        log.error("Socket------" + ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + General.CHANNEL_MAP.size(),cause);
    }
}

 (7)spring上下文工具类

package com.example.dzx.netty.qiyanproject.netty1;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author dzx
 * @ClassName:
 * @Description: spring容器上下文工具类
 * @date 2023年06月30日 21:30:02
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * @Description: 获取spring容器中的bean, 通过bean类型获取
     */
    public static <T> T getBean(Class<T> beanClass) {
        return applicationContext.getBean(beanClass);
    }

}

(8)编写全局map常量类

package com.example.dzx.netty.qiyanproject.constants;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 500007
 * @ClassName:
 * @Description:
 * @date 2023年07月02日 19:12:42
 */
public class General {

    /**
     * 管理一个全局map,保存连接进服务端的通道数量
     */
    public static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

    /**
     * 管理一个全局mao, 报存连接进服务器的各个通道类型
     */
    public static final ConcurrentHashMap<ChannelId, Boolean> CHANNEL_TYPE_MAP = new ConcurrentHashMap<>();

}

三、netty客户端

(1)编写netty客户端,用于测试向服务端的消息发送

package com.example.dzx.netty.qiyanproject.Socket;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author dzx
 * @ClassName:
 * @Description: netty客户端
 * @date 2023年06月30日 21:30:02
 */
public class SocketClient {
    // 服务端IP
    static final String HOST = System.getProperty("host", "127.0.0.1");

    // 服务端开放端口
    static final int PORT = Integer.parseInt(System.getProperty("port", "7777"));

    // 日志打印
    private static final Logger LOGGER = LoggerFactory.getLogger(SocketClient.class);

    // 主函数启动
    public static void main(String[] args) throws InterruptedException {
        sendMessage("我是客户端,我发送了一条数据给netty服务端。。");
    }

    /**
     * 核心方法(处理:服务端向客户端发送的数据、客户端向服务端发送的数据)
     */
    public static void sendMessage(String content) throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new SocketChannelInitializer());
                        }
                    });

            ChannelFuture future = b.connect(HOST, PORT).sync();
            for (int i = 0; i < 3; i++) {
                future.channel().writeAndFlush(content);
                Thread.sleep(2000);
            }
            // 程序阻塞
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

}

(2)编写netty客户端初始化处理器

package com.example.dzx.netty.qiyanproject.Socket;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;


/**
 * @author dzx
 * @ClassName:
 * @Description: netty客户端初始化时设置出站和入站的编码器和解码器
 * @date 2023年06月30日 21:30:02
 */
public class SocketChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        p.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
        p.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
        p.addLast(new SocketHandler());
    }
}

(3)netty客户端业务处理器,用于接收并处理服务端发送的消息数据

package com.example.dzx.netty.qiyanproject.Socket;


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;


/**
 * @author dzx
 * @ClassName:
 * @Description: netty客户端处理器
 * @date 2023年06月30日 21:30:02
 */
@Slf4j
public class SocketHandler extends ChannelInboundHandlerAdapter {

    // 日志打印
    private static final Logger LOGGER = LoggerFactory.getLogger(SocketHandler.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        LOGGER.debug("SocketHandler Active(客户端)");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        LOGGER.debug("####接收服务端发送过来的消息####");
        LOGGER.debug("SocketHandler read Message:" + msg);
        //获取服务端连接的远程地址
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        //获取服务端的IP地址
        String clientIp = insocket.getAddress().getHostAddress();
        //获取服务端的端口号
        int clientPort = insocket.getPort();
        log.info("netty服务端[IP:" + clientIp + "--->PORT:" + clientPort + "]");

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        LOGGER.debug("####客户端断开连接####");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

至此,netty服务端和netty客户端都编写完毕,我们可以来进行测试了。

四、测试

(1)前端 websocket 向 后端NetAssist测试工具发送消息

 

 在前端窗口向后端 发送了一个 22222的 字符串,后端测试工具成功接收到消息并展示在对话框中。

(2)后端NetAssist向 前端 websocket 发送消息

 

  在后端窗口向前端 发送了一个{"deviceId":"11111","deviceName":"qz-01","deviceStatus":"2"}的 字符串,前端测试工具成功接收到消息并展示在对话框中。

五、代码仓库地址

完整项目已上传至gitee仓库,请点击下方传送门自行获取,一键三连!!

https://gitee.com/dzxmy/netty-web-socketd-dnamic

无法访问就点击下方传送门去我的资源下载即可

https://download.csdn.net/download/qq_31905135/88044942?spm=1001.2014.3001.5503

以下是一个简单的 Spring Boot 整合 NettyWebSocket 实现音视频通话的前后端代码示例: 前端代码(HTML + JavaScript): ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Video Chat</title> <style> #localVideo, #remoteVideo { width: 320px; height: 240px; } </style> </head> <body> <video id="localVideo" autoplay muted></video> <video id="remoteVideo" autoplay></video> <script> var localVideo = document.querySelector('#localVideo'); var remoteVideo = document.querySelector('#remoteVideo'); var peerConnection; navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(function (stream) { localVideo.srcObject = stream; peerConnection = new RTCPeerConnection(); peerConnection.addStream(stream); peerConnection.onaddstream = function(event) { remoteVideo.srcObject = event.stream; }; peerConnection.onicecandidate = function(event) { if (event.candidate) { sendIceCandidate(event.candidate); } }; startCall(); }) .catch(function (err) { console.log('getUserMedia error:', err); }); function startCall() { // 发送一个开始通话的消息给服务器 var socket = new WebSocket('ws://localhost:8080/videochat'); socket.onopen = function () { socket.send(JSON.stringify({ type: 'start' })); }; socket.onmessage = function (event) { var message = JSON.parse(event.data); if (message.type === 'offer') { peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer)) .then(function () { return peerConnection.createAnswer(); }) .then(function (answer) { return peerConnection.setLocalDescription(answer); }) .then(function () { socket.send(JSON.stringify({ type: 'answer', answer: peerConnection.localDescription })); }) .catch(function (err) { console.log(err); }); } else if (message.type === 'iceCandidate') { peerConnection.addIceCandidate(new RTCIceCandidate(message.iceCandidate)) .catch(function (err) { console.log(err); }); } }; } function sendIceCandidate(candidate) { // 发送一个 ICE candidate 到服务器 var socket = new WebSocket('ws://localhost:8080/videochat'); socket.onopen = function () { socket.send(JSON.stringify({ type: 'iceCandidate', iceCandidate: candidate })); }; } </script> </body> </html> ``` 后端代码(Java + Netty): ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(videoChatHandler(), "/videochat").setAllowedOrigins("*"); } @Bean public WebSocketHandler videoChatHandler() { return new VideoChatHandler(); } } public class VideoChatHandler extends TextWebSocketHandler { private static final Logger logger = LoggerFactory.getLogger(VideoChatHandler.class); private Session session; private RTCPeerConnection peerConnection; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { logger.info("WebSocket connection established"); this.session = session; } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { String json = (String) message.getPayload(); JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject(); String type = jsonObject.get("type").getAsString(); if ("start".equals(type)) { startCall(); } else if ("offer".equals(type)) { String sdp = jsonObject.get("offer").getAsString(); SessionDescription offer = new SessionDescription(Type.OFFER, sdp); peerConnection.setRemoteDescription(offer); createAnswer(); } else if ("answer".equals(type)) { String sdp = jsonObject.get("answer").getAsString(); SessionDescription answer = new SessionDescription(Type.ANSWER, sdp); peerConnection.setLocalDescription(answer); sendAnswer(); } else if ("iceCandidate".equals(type)) { JsonObject iceCandidateJson = jsonObject.get("iceCandidate").getAsJsonObject(); IceCandidate iceCandidate = new IceCandidate(iceCandidateJson.get("sdpMid").getAsString(), iceCandidateJson.get("sdpMLineIndex").getAsInt(), iceCandidateJson.get("candidate").getAsString()); peerConnection.addIceCandidate(iceCandidate); } } private void startCall() { peerConnection = new RTCPeerConnection(); peerConnection.addStream(getMediaStream()); peerConnection.setIceCandidateListener(new IceCandidateListener() { @Override public void onIceCandidate(IceCandidate iceCandidate) { JsonObject message = new JsonObject(); message.addProperty("type", "iceCandidate"); JsonObject iceCandidateJson = new JsonObject(); iceCandidateJson.addProperty("sdpMid", iceCandidate.getSdpMid()); iceCandidateJson.addProperty("sdpMLineIndex", iceCandidate.getSdpMLineIndex()); iceCandidateJson.addProperty("candidate", iceCandidate.getCandidate()); message.add("iceCandidate", iceCandidateJson); try { session.sendMessage(new TextMessage(message.toString())); } catch (IOException e) { logger.error("Error sending ICE candidate", e); } } }); createOffer(); } private void createOffer() { peerConnection.createOffer(new CreateSessionDescriptionObserver() { @Override public void onSuccess(SessionDescription sessionDescription) { peerConnection.setLocalDescription(sessionDescription); sendOffer(); } @Override public void onFailure(Throwable throwable) { logger.error("Error creating offer", throwable); } }, new MediaConstraints()); } private void sendOffer() { JsonObject message = new JsonObject(); message.addProperty("type", "offer"); message.addProperty("offer", peerConnection.getLocalDescription().description); try { session.sendMessage(new TextMessage(message.toString())); } catch (IOException e) { logger.error("Error sending offer", e); } } private void createAnswer() { peerConnection.createAnswer(new CreateSessionDescriptionObserver() { @Override public void onSuccess(SessionDescription sessionDescription) { peerConnection.setLocalDescription(sessionDescription); sendAnswer(); } @Override public void onFailure(Throwable throwable) { logger.error("Error creating answer", throwable); } }, new MediaConstraints()); } private void sendAnswer() { JsonObject message = new JsonObject(); message.addProperty("type", "answer"); message.addProperty("answer", peerConnection.getLocalDescription().description); try { session.sendMessage(new TextMessage(message.toString())); } catch (IOException e) { logger.error("Error sending answer", e); } } private MediaStream getMediaStream() { MediaStream mediaStream = new MediaStream(); MediaConstraints constraints = new MediaConstraints(); MediaStreamTrack videoTrack = getVideoTrack(); mediaStream.addTrack(videoTrack); MediaStreamTrack audioTrack = getAudioTrack(); mediaStream.addTrack(audioTrack); return mediaStream; } private MediaStreamTrack getVideoTrack() { VideoCaptureModule videoCaptureModule = new VideoCaptureModule(); VideoCapturer videoCapturer = new Camera2Enumerator(VideoChatHandler.this.getContext()).createCapturer("0", null); VideoSource videoSource = peerConnection.createVideoSource(videoCapturer, new MediaConstraints()); VideoTrack videoTrack = peerConnection.createVideoTrack("video", videoSource); videoCapturer.startCapture(320, 240, 30); return videoTrack; } private MediaStreamTrack getAudioTrack() { AudioSource audioSource = peerConnection.createAudioSource(new MediaConstraints()); AudioTrack audioTrack = peerConnection.createAudioTrack("audio", audioSource); return audioTrack; } } ``` 其中,`VideoChatHandler` 类是 Netty 的 `WebSocketHandler` 的实现,用于处理 WebSocket 消息。在 `afterConnectionEstablished` 方法中,保存了 WebSocketSession 的引用。在 `handleMessage` 方法中,处理各种消息类型,包括开始通话、发送 offer、发送 answer、发送 ICE candidate 等。在 `startCall` 方法中,创建了一个 `RTCPeerConnection` 对象,并且添加了本地的媒体流。在 `createOffer` 方法中,创建了一个 offer,并设置为本地的 SDP。在 `sendOffer` 方法中,将 offer 发送给客户端。在 `createAnswer` 方法中,创建了一个 answer,并设置为本地的 SDP。在 `sendAnswer` 方法中,将 answer 发送给客户端。在 `getMediaStream` 方法中,创建了一个媒体流,包括视频和音频轨道。在 `getVideoTrack` 方法中,创建了一个视频轨道,使用了 Android 的 Camera2 API。在 `getAudioTrack` 方法中,创建了一个音频轨道。最后,通过 `WebSocketHandlerRegistry` 注册了 `VideoChatHandler`。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小米吃辣椒2022

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值