基于Netty实现WebSocket服务端

news2024/11/14 12:16:25

本文基于Netty实现WebSocket服务端,实现和客户端的交互通信,客户端基于JavaScript实现。

在【WebSocket简介-CSDN博客】中,我们知道WebSocket是基于Http协议的升级,而Netty提供了Http和WebSocket Frame的编解码器和Handler,我们可以基于Netty快速实现WebSocket服务端。

一、基于Netty快速实现WebSocket服务端

我们可以直接利用Netty提供了Http和WebSocket Frame的编解码器和Handler,快速启动一个WebSocket服务端。服务端收到Client消息后,转发给其他客户端。

服务端代码:

ChannelSupervise:用户存储客户端信息

import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 用户存储客户端信息
 */
public class ChannelSupervise {
    private static ChannelGroup GlobalGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    private static ConcurrentMap<String, ChannelId> ChannelMap = new ConcurrentHashMap();

    public static void addChannel(Channel channel) {
        GlobalGroup.add(channel);
        ChannelMap.put(channel.id().asShortText(), channel.id());
    }

    public static void removeChannel(Channel channel) {
        GlobalGroup.remove(channel);
        ChannelMap.remove(channel.id().asShortText());
    }

    public static Channel findChannel(String id) {
        return GlobalGroup.find(ChannelMap.get(id));
    }

    public static void send2All(TextWebSocketFrame tws) {
        GlobalGroup.writeAndFlush(tws);
    }
}

服务端Netty配置:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * 
 * 实现长链接 客户端与服务端;
 */
public class SimpleWsChatServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).
            // 在 bossGroup 增加一个日志处理器
                handler(new LoggingHandler(LogLevel.INFO)).childHandler(new 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 将多个段聚合起来
                         * 当浏览器发起大量数据的时候,会发起多次http请求
                         */
                        pipeline.addLast(new HttpObjectAggregator(8192));
                        /**
                         * 对于websocket是以frame的形式传递
                         * WebSocketFrame
                         *  浏览器 ws://localhost:7070/ 不在是http协议
                         *  WebSocketServerProtocolHandler 将http协议升级为ws协议 即保持长链接
                         */
                        pipeline.addLast(new WebSocketServerProtocolHandler("/helloWs"));

                        // 自定义handler专门处理浏览器请求
                        pipeline.addLast(new MyTextWebSocketFrameHandler());

                    }
                });
            ChannelFuture channelFuture = serverBootstrap.bind(7070).sync();
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

消息转发逻辑:

import com.huawei.websocket.nio2.global.ChannelSupervise;

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

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * TextWebSocketFrame  表示一个文本贞
 * 浏览器和服务端以TextWebSocketFrame 格式交互
 */
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:MM:ss");

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

        System.out.println("服务端收到消息:" + textWebSocketFrame.text());
        // 回复浏览器

        String resp = sdf.format(new Date()) + ": " + textWebSocketFrame.text();
        // 转发给所有的客户端
        ChannelSupervise.send2All(new TextWebSocketFrame(resp));
        // 发送给当前客户单
        // channelHandlerContext.channel().writeAndFlush(new TextWebSocketFrame(resp));
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 添加连接
        System.out.println("客户端加入连接:" + ctx.channel());
        ChannelSupervise.addChannel(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 断开连接
        System.out.println("客户端断开连接:" + ctx.channel());
        ChannelSupervise.removeChannel(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生:" + cause.getMessage());
        ctx.channel().close();
    }
}

参考:NIO框架Netty+WebSocket实现网页聊天_nio实现websocket-CSDN博客

一、基于Netty,手动处理WebSocket握手信息:

参考:GitCode - 开发者的代码家园icon-default.png?t=N7T8https://gitcode.com/Siwash/websocketWithNetty/blob/master/README.md

 基于netty搭建websocket,实现消息的主动推送_websocket_rpf_siwash-GitCode 开源社区

 服务端代码:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
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.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

import org.apache.log4j.Logger;

/**
 * https://gitcode.com/Siwash/websocketWithNetty/blob/master/README.md
 */
public class NioWebSocketServer {
    private final Logger logger = Logger.getLogger(getClass());

    private void init() {
        logger.info("正在启动websocket服务器");
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup work = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, work);
            bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("logging", new LoggingHandler("DEBUG"));// 设置log监听器,并且日志级别为debug,方便观察运行流程
                    ch.pipeline().addLast("http-codec", new HttpServerCodec());// 设置解码器
                    ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));// 聚合器,使用websocket会用到
                    ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());// 用于大数据的分区传输
                    ch.pipeline().addLast("handler", new NioWebSocketHandler());// 自定义的业务handler,这里处理WebSocket建链请求和消息发送请求
                }
            });
            Channel channel = bootstrap.bind(7070).sync().channel();
            logger.info("webSocket服务器启动成功:" + channel);
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
            logger.info("运行出错:" + e);
        } finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
            logger.info("websocket服务器已关闭");
        }
    }

    public static void main(String[] args) {
        new NioWebSocketServer().init();
    }
}
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;

import com.huawei.websocket.nio2.global.ChannelSupervise;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;

import org.apache.log4j.Logger;

import java.util.Date;

/**
 * 这里处理WebSocket建链请求和消息发送请求
 */
public class NioWebSocketHandler extends SimpleChannelInboundHandler<Object> {

    private final Logger logger = Logger.getLogger(getClass());

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.debug("收到消息:" + msg);
        if (msg instanceof FullHttpRequest) {
            // 以http请求形式接入,但是走的是websocket
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            // 处理websocket客户端的消息
            handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 添加连接
        logger.debug("客户端加入连接:" + ctx.channel());
        ChannelSupervise.addChannel(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 断开连接
        logger.debug("客户端断开连接:" + ctx.channel());
        ChannelSupervise.removeChannel(ctx.channel());
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 判断是否ping消息
        if (frame instanceof PingWebSocketFrame) {
            logger.debug("服务端收到ping消息");
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        // 本例程仅支持文本消息,不支持二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            logger.debug("本例程仅支持文本消息,不支持二进制消息");
            throw new UnsupportedOperationException(
                String.format("%s frame types not supported", frame.getClass().getName()));
        }
        // 返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        logger.debug("服务端收到:" + request);
        TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString() + ctx.channel().id() + ":" + request);
        // 群发
        ChannelSupervise.send2All(tws);
        // 返回【谁发的发给谁】
        // ctx.channel().writeAndFlush(tws);
    }

    /**
     * 唯一的一次http请求,用于创建websocket
     * */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        // 要求Upgrade为websocket,过滤掉get/Post
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            // 若不是websocket方式,则创建BAD_REQUEST的req,返回给客户端
            sendHttpResponse(ctx, req,
                new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        logger.debug("服务端收到WebSocket建链消息");
        WebSocketServerHandshakerFactory wsFactory =
            new WebSocketServerHandshakerFactory("ws://localhost:7070/helloWs", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }
    }

    /**
     * 拒绝不合法的请求,并返回错误信息
     * */
    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回应答给客户端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        // 如果是非Keep-Alive,关闭连接
        if (!isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

 这里我们手动处理了握手信息:

可以看到服务端handler日志:

2024-05-24 09:43:51 DEBUG [NioWebSocketHandler] 收到消息:HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0))
GET /helloWs HTTP/1.1
Host: localhost:7070
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Sec-WebSocket-Key: KYh4//SBKLi+nSu6v1kYqw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
content-length: 0
2024-05-24 09:43:51 DEBUG [NioWebSocketHandler] 服务端收到WebSocket建链消息

Client1的发送和接收消息,在F12小可以看到:

测试用的Client代码如下:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>
			WebSocket Client
		</title>
	</head>
	<body>
		<script>
			var socket;
			//check current explorer wether support WebSockek
			if (window.WebSocket) {
				socket = new WebSocket("ws://localhost:7070/helloWs");
				//event : msg from server
				socket.onmessage = function(event) {
					console.log("receive msg:" + event.data);
					var rt = document.getElementById("responseText");
					rt.value = rt.value + "\n" + event.data;
				}
				//eq open WebSocket
				socket.onopen = function(event) {
					var rt = document.getElementById("responseText");
					rt.value = "open WebSocket";
				}
				socket.onclose = function(event) {
					var rt = document.getElementById("responseText");
					rt.value = rt.value + "\n" + "close WebSocket";
				}
			
			} else {
				alert("current exploer not support websocket")
			}

			//send msg to WebSocket Server
			function send(message) {
				if (socket.readyState == WebSocket.OPEN) {
					//send msg to WebSocket by socket
					socket.send(message);
				} else {
					alert("WebSocket is not open")
				}
			}
		</script>
		<form onsubmit="return false">
			<textarea name="message" style="height: 300px; width: 300px;"></textarea>
			<input type="button" value="发送消息" onclick="send(this.form.message.value)">
			<textarea id="responseText" style="height: 300px; width: 300px;"></textarea>
			<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
		</form>
	</body>
</html>

直接保存为html文件,使用浏览器打开即可运行测试;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1686127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

9.3 Go语言入门(变量声明和函数调用)

Go语言入门&#xff08;变量声明和函数调用&#xff09; 目录二、变量声明和函数调用1. 变量声明1.1 使用 var 关键字声明1.2 简短声明1.3 零值1.4 常量 2. 函数调用2.1 函数定义2.2 多个返回值2.3 命名返回值2.4 可变参数2.5 匿名函数和闭包 目录 Go 语言&#xff08;Golang&a…

Windows11下使用Qt5.14.2编译QtXlsx驱动详细步骤

原有&#xff1a;由于系统需要将QTableWidget表格中的数据导出、在Windows下最开始使用Excel.Application组件实现了导出功能&#xff0c;后面将代码转换到Ubuntu20.04下进行编译&#xff0c;发现项目.pro文件中的QT axcontainer和代码.h文件中的#include <QAxObject>跟…

接口自动化基础

1、接口自动化测试 接口自动化&#xff1a;使用工具或代码代替人对接口进行测试的技术。 测试目的&#xff1a;防止开发修改代码时引入新的问题。 l测试时机&#xff1a; 开发进行系统测试转测前&#xff0c;可以先进行接口自动化脚本的编写。 开发进行系统测试转测后&…

dubbo复习:(4) 和springboot 整合时,客户端负载均衡的配置

需要在DubboReference注解指定loadbalance属性。示例如下&#xff1a; package cn.edu.tju.service;import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Ser…

【全开源】活动报名表单系统(ThinkPHP+Uniapp+uView)

轻松构建高效报名平台 一、引言 随着线上活动的日益增多&#xff0c;一个高效、易用的活动报名表单系统成为了举办各类活动的必备工具。为了满足不同组织和个人的需求&#xff0c;我们推出了功能强大的“活动报名表单系统源码”。本文将为您详细介绍该源码的特点、功能以及使…

燃数科技前端25-40K*14薪一面超简单,下周二面啦

一面 1、自我介绍 2、低代码如何设计的 3、react路由原理 4、react生命周期 5、什么是回调地狱&#xff0c;如何解决 6、jwt和session有什么区别 7、js文件相互引用有什么问题&#xff1f;如何解决 8、一个很大的json文件&#xff0c;前端读取如何优化 面试我的不像是…

实战之快速完成 ChatGLM3-6B 在 GPU-8G的 INT4 量化和本地部署

ChatGLM3 (ChatGLM3-6B) 项目地址 https://github.com/THUDM/ChatGLM3大模型是很吃CPU和显卡的&#xff0c;所以&#xff0c;要不有一个好的CPU&#xff0c;要不有一块好的显卡&#xff0c;显卡尽量13G&#xff0c;内存基本要32GB。 清华大模型分为三种(ChatGLM3-6B-Base&…

部署运行petalinux系统镜像

参考文档《编译 petalinux 系统镜像》编译获取 petalinux 系统镜像&#xff0c;编译生成的各种镜像文件如下&#xff1a; scilogyhunterubuntu1804:~/petalinux/workspace/project0/petalinux$ ls images/linux/ bl31.bin Image pxelinux.cfg rootfs.cpio.gz.u-boot …

【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器

【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器 1 初识JVM1.1 什么是JVM1.2 JVM的功能1.2.1 即时编译 1.3 常见JVM 2 字节码文件详解2.1 Java虚拟机的组成2.2 字节码文件的组成2.2.1 正确打开字节码文件2.2.2 字节码组成2.2.3 基础信息2.2.3.1 魔数2.2.3.1 主副版本号…

Java 数组的基本使用

目录 含义语法格式语句特点数组的长度数组的元素打印数组显示数组数组的复制扩展示例【12】&#xff1a; 含义 数组&#xff08;array&#xff09;是一种最简单的复合数据类型&#xff0c;它是有序数据的集合&#xff0c;数组中的每个元素具有相同的数据类型&#xff0c;可以用…

一款好用的SSH连接工具-Tabby Terminal 使用教程

简介 Tabby Terminal 是一款现代化的终端应用程序&#xff0c;旨在提供流畅、高效且可定制的用户体验。它具有跨平台兼容性&#xff0c;支持多种操作系统&#xff0c;包括 Windows、macOS 和 Linux。其界面设计简洁美观&#xff0c;允许用户通过插件和主题进行个性化定制。同时…

LeetCode674:最长连续递增序列

题目描述 给定一个未经排序的整数数组&#xff0c;找到最长且 连续递增的子序列&#xff0c;并返回该序列的长度。 连续递增的子序列 可以由两个下标 l 和 r&#xff08;l < r&#xff09;确定&#xff0c;如果对于每个 l < i < r&#xff0c;都有 nums[i] < nums…

selenium 爬取今日头条

由于今日头条网页是动态渲染&#xff0c;再加上各种token再验证&#xff0c;因此直接通过API接口获取数据难度很大&#xff0c;本文使用selenium来实现新闻内容爬取。 selenium核心代码 知识点&#xff1a; 代码中加了很多的异常处理&#xff0c;保证错误后重试&#xff0c;…

抖音:当之无愧的短视频NO.1,新老用户奖励丰厚

论起短视频&#xff0c;如不提行业老大抖音&#xff0c;那是说不过去的。年底抖音也加入了波涛汹涌的红包大战&#xff0c;小伙伴们动动手指就能赚到真金白银的现金&#xff0c;何乐而不为&#xff01; 抖音简介 抖音是北京微播视界科技有限公司于2016年9月20日上线的一款音乐…

系统分析与校正方法——时域法

一、概述 时域法是一种直接在时间域中对系统进行分析和校正的方法。 优点&#xff1a;可以提供系统时间响应的全部信息&#xff0c;直观、准确。缺点&#xff1a;研究系统参数改变引起系统性能指标变化的趋势&#xff0c;及对系统进行校正设计时&#xff0c;时域法不是非常方…

钉钉算是在线办公系统的设计标杆,尽管它依然很难用

不吹不黑&#xff0c;钉钉界面谁的的确简洁&#xff0c;无奈它面向的是场景复杂的办公领域&#xff0c;导致其越来越臃肿难用&#xff0c;反正我是该研究研究&#xff0c;但绝对不会用的。 举报 评论 1

【Axure教程】拖动换位选择器

拖动换位选择器通常用于从一个列表中选择项目并将其移动到另一个列表中。用户可以通过拖动选项来实现选择和移动。这种交互方式在许多Web应用程序中很常见&#xff0c;特别是在需要对项目分组的情况下。 所以今天作者就教大家怎么在Axure用中继器制作一个拖动换位选择器的原型…

RK3568笔记二十五:RetinaFace人脸检测训练部署

若该文为原创文章&#xff0c;转载请注明原文出处。 一、介绍 Retinaface是来自insightFace的又一力作&#xff0c;基于one-stage的人脸检测网络。RetinaFace是在RetinaNet基础上引申出来的人脸检测框架&#xff0c;所以大致结构和RetinaNet非常像。 官方提供两种主干特征提取网…

4月手机行业线上市场销售数据分析

政府对智能手机行业的支持政策&#xff0c;如5G推广&#xff0c;以及相关的产业政策&#xff0c;都在一定程度上推动了智能手机市场的发展&#xff0c;再加上AI应用的推广和全球科技迅猛发展&#xff0c;中国手机市场在2024年迎来了恢复性增长。 据鲸参谋数据统计&#xff0c;…

鸟击防治设备 | 机场专用声光定向驱鸟系统分析

飞机与鸟类相撞&#xff0c;导致飞机受损&#xff0c;鸟类死亡&#xff0c;这被称作“鸟击事故”。全球范围内&#xff0c;每年都会发生多起鸟击事件&#xff0c;而大部分的鸟击都发生在飞机起降或低空飞行的阶段。因此&#xff0c;机场需要驱鸟、防鸟&#xff0c;确保鸟类远离…