Netty简易聊天室

文章目录

    • 本文目的
    • 参考说明
    • 环境说明
      • maven依赖
      • 日志配置
      • 单元测试
    • 功能介绍
    • 开发步骤

本文目的

  • 通过一个简易的聊天室案例,讲述Netty的基本使用。同时分享案例代码。
  • 项目中用到了log4j2,junit5,同时分享这些基础组件的使用。
  • 项目中用到了awt,属于古董技术,只是用来做界面。非重点不用关注。

参考说明

本文内容主要来源于马士兵老师的视频教程(Java经典实战项目-坦克大战),结合了老师的讲课内容以及自己的实践做了一些补充。

环境说明

开发工具:idea2023,jdk:1.8,Maven:3.6.3

maven依赖

<?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><groupId>com.xxx</groupId><artifactId>xxx</artifactId><version>0.0.1-SNAPSHOT</version><name>xxx</name><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.21</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.28</version></dependency><!-- log4j2-slf4j-适配器 --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId><version>2.20.0</version></dependency><!-- log4j2 日志核心 --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.20.0</version></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.96.Final</version></dependency><!-- 单元测试,Junit5 --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.9.3</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

日志配置

src/main/resources/log4j2.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!-- log4j2配置文件 -->
<!-- monitorInterval="30" 自动加载配置文件的间隔时间,不低于10秒;生产环境中修改配置文件,是热更新,无需重启应用status="info" 日志框架本身的输出日志级别,可以修改为info, -->
<Configuration status="warn" monitorInterval="30"><!-- 集中配置属性,使用时通过:${LOG_HOME} --><properties><!-- 当前项目名称,供下方引用 --><property name="PROJECT_NAME" value="tank-battle"/><!-- 默认日志格式-包名自动缩减(同步异步通用) --><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%-5t|%logger{1.}: %msg%n"/><!-- 日志格式-打印代码的精确位置信息,类,方法,行。(建议同步使用)。异步如果打印位置信息,会有严重性能问题 --><property name="LOG_PATTERN_ALL" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%-5t|%location: %msg%n"/><!-- 日志主目录。如果想把日志输出到tomcat底下时使用。 --><property name="LOG_HOME">${web:rootDir}/WEB-INF/logs</property></properties><!-- 日志打印输出方式 --><Appenders><Console name="STDOUT" target="SYSTEM_OUT"><PatternLayout charset="UTF-8" Pattern="${LOG_PATTERN}"/></Console><RollingFile name="FileLog" fileName="logs/${PROJECT_NAME}.log" filePattern="logs/${PROJECT_NAME}-%d_%i.log"><PatternLayout charset="UTF-8" Pattern="${LOG_PATTERN}"/><Policies><!-- 每天生成一个,同时如果超过10MB还会再生成 --><TimeBasedTriggeringPolicy/><SizeBasedTriggeringPolicy size="50 MB"/></Policies><DefaultRolloverStrategy max="99"/></RollingFile></Appenders><!-- 将代码路径与上面的日志打印关联起来 --><Loggers><!-- 当前项目日志 --><Logger name="com.sjj" level="INFO" additivity="false"><AppenderRef ref="STDOUT"/><AppenderRef ref="FileLog"/></Logger><!-- 第三方依赖项目日志 --><logger name="org.springframework" level="info"/><logger name="org.jboss.netty" level="warn"/><!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --><!-- 根节点日志,除了上面配置的之外的日志 --><Root level="WARN"><AppenderRef ref="STDOUT"/><AppenderRef ref="FileLog"/></Root></Loggers>
</Configuration>

单元测试

确认项目已加入Junit5依赖,就是如下这段。

        <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.9.3</version><scope>test</scope></dependency>

新建单元测试类的步骤。

  1. 在要创建单元测试的功能类上,依次点Code > generate > Test
  2. 然后在弹出的窗口中,选择Junit版本为5,测试类名,测试方法等。然后点确定。
  3. 在这里插入图片描述
  4. IDEA会自动根据功能类的路径在test目录中创建相同路径但以Test结尾的测试类。并且会自动生成勾选方法的默认测试代码。
  5. 根据程序的输入和输出,编写单元测试代码。
  6. 点击方法左边的绿色三角形就可以执行单元测试用例了。

为什么要进行单元测试?

  1. 方法内部可以很复杂,如果靠肉眼观察,比较耗时间。单元测试可以根据入参和返回值测试方法是否达到要求。
  2. 代码是开发人员写的,最了解代码逻辑的还是开发人员。测试人员测试不到代码细节。
  3. 在一个大的功能中,可能会有很多方法,每个方法都要写Main方法来一个个测试比较复杂,而且也不知道测了哪些场景。

为什么有的公司不做单元测试。

  1. 代码业务可能比较简单,程序员读代码不是很费力。
  2. 写单元测试需要额外花时间,程序员工作比较忙,没时间写。

功能介绍

简易版聊天室程序。主要用于练习Netty的使用。聊天室功能如下:

  1. 聊天室支持多客户端,每个客户端都可以看到其他客户端的消息。
  2. 点击关闭按钮时,关闭当前客户端,同时在服务端的客户端列表中也删除。
  3. 系统UI非重点,一切从简。

开发步骤

  1. 首先写一个聊天室的界面(ChatFrame.java)

    1. 参考坦克大战的界面部分,设置好聊天室的长宽和坐标。

    2. 界面包含2个输入部分,中间文本域显示当前聊天室的所有聊天内容。底部文本框输入当前用户的聊天内容

    3. 聊天室窗口初始化时,需要与服务端建立连接。

    4. 当用户输入完聊天内容后回车,需要将聊天内容通过Netty客户端发送给服务端。

    5. 当用户关闭窗口时,关闭当前客户端,同时在服务端的客户端列表中也删除。

    6. /*** 聊天室客户端-界面<br>** @author namelessmyth* @version 1.0* @date 2023/8/15*/
      @Slf4j
      public class ChatFrame extends Frame {public static final int GAME_WIDTH = ConfigUtil.getInt("chat.frame.width");public static final int GAME_HEIGHT = ConfigUtil.getInt("chat.frame.height");TextArea ta = new TextArea();TextField tf = new TextField();public static final ChatFrame INSTANCE = new ChatFrame();public static void main(String[] args) throws Exception {INSTANCE.setVisible(true);ChatClient.connect();}private ChatFrame() throws HeadlessException {//创建游戏的主Framethis.setTitle("chat room");this.setSize(GAME_WIDTH, GAME_HEIGHT);this.setLocation(800, 100);this.add(ta, BorderLayout.CENTER);this.add(tf, BorderLayout.SOUTH);tf.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {ChatClient.send(tf.getText());tf.setText("");}});this.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {ChatClient.close();System.exit(0);}});log.info("chat room Main frame initialization completed");}public void updateText(String text) {ta.setText(ta.getText() + Constants.LINE_SEPERATOR + text);}
      }
      
  2. 编写Netty客户端与服务端进行消息通信(ChatClient.java)。

    1. 参考上面的描述,客户端需要实现如下方法。

      1. connect(),与服务端建立连接的方法
      2. send(),向服务端发送聊天消息的方法。
      3. channelRead,读取服务端信息更新客户端聊天内容方法
      4. 参考代码如下
    2. @Slf4j
      public class ChatClient {private static SocketChannel channel;/*** 与服务端建立连接的方法*/public static void connect() {EventLoopGroup group = new NioEventLoopGroup(1);try {Bootstrap b = new Bootstrap();b.group(group);b.channel(NioSocketChannel.class);b.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {channel = ch;ch.pipeline().addLast(new MyClientHandler());}});ChannelFuture cf = b.connect("localhost", 8888).sync();//直到服务器被关闭,否则一直阻塞。cf.channel().closeFuture().sync();log.info("the chat client has been closed.");} catch (Exception e) {log.error("ChatClient.connect.Exception.", e);} finally {group.shutdownGracefully();}}/*** 向服务端发送聊天消息的方法* @param msg 聊天内容*/public static void send(String msg) {channel.writeAndFlush(Unpooled.copiedBuffer(msg.getBytes()));log.info("client.send().{}", msg);}/*** 关闭客户端方法,向服务端发送特定消息告知其删除本客户端。*/public static void close() {send("__88__");channel.close();}
      }@Slf4j
      class MyClientHandler extends ChannelInboundHandlerAdapter {/*** 读取服务端数据* @param msg 服务端数据*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;String text = buf.toString(StandardCharsets.UTF_8);ChatFrame.INSTANCE.updateText(text);log.info("channelRead.msg:{}", text);}/*** 连接刚建立时的事件处理*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.info("connected to server.");}/*** 异常处理*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("chat client exceptionCaught:", cause);super.exceptionCaught(ctx, cause);}
      }
      
  3. 聊天室服务端(ChatServer.java)。

    1. 服务端需要记录所有的客户端。(可能有多个)

    2. 当某个客户端发来消息之后,需要将消息转发给所有客户端。

    3. 当接收到特殊消息时(客户端关闭),需要将客户端从列表中移除。

    4. @Slf4j
      public class ChatServer {static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);public static void main(String[] args) throws Exception {//总管线程组EventLoopGroup bossGroup = new NioEventLoopGroup(1);//接待员线程EventLoopGroup workerGroup = new NioEventLoopGroup(2);//服务器启动辅助类ServerBootstrap b = new ServerBootstrap();//放在第一位的是总管线程组,第二位的就是接待员线程组。b.group(bossGroup, workerGroup);//异步全双工b.channel(NioServerSocketChannel.class);//接收到客户端连接的处理,相当于BIO的acceptb.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel sc) throws Exception {log.info("a client connected:{}", sc);sc.pipeline().addLast(new MyChildHandler());}});b.bind(8888).sync();}
      }@Slf4j
      class MyChildHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ChatServer.clients.add(ctx.channel());}/*** 读取客户端通道内的数据* @param msg 客户端消息* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;String str = buf.toString(StandardCharsets.UTF_8);log.info("channelRead().input,string:{},buf:{}", str, buf);if (StrUtil.equalsIgnoreCase(str, "__88__")) {ChatServer.clients.remove(ctx.channel());ctx.close();log.info("The chat client has been closed:{}", ctx.channel());} else {ChatServer.clients.writeAndFlush(msg);log.info("ChatServer.clients.writeAndFlush:{}", msg);}}/*** 异常处理** @param ctx* @param cause* @throws Exception*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("exceptionCaught:", cause);ChatServer.clients.remove(ctx.channel());ctx.close();}
      }
      
    5. 补充服务端关闭的处理(仅思路,未实现)。

      1. 通知客户端,服务器准备关闭。
      2. 拒绝新的连接接入
      3. 等待所有客户端都处理完成。
      4. 开始关闭流程,发送消息给客户端,客户端自动处理。
      5. 确认所有客户端断开。
      6. server保存现有的工作数据。
      7. 停止线程组
      8. 退出。
    6. 服务端UI

      1. 为了可以方便的看到所有客户端的连接情况和消息,以及后续进一步实现服务端的关闭效果考虑在服务端实现UI

      2. 新增一个ServerFrame类,实现服务端UI,服务端左边显示消息,右边显示客户端的连接情况。

      3. ServerFrame类初始化时自动启动服务端。服务端接收消息时打印到消息窗口中。

      4. 有客户端连上或者关闭时显示到右边的窗口中。

      5. 实现效果如下图

      6. 在这里插入图片描述

      7. 参考代码如下。(只需要修改服务端代码,客户端不变)

      8. @Slf4j
        public class ServerFrame extends Frame {public static final int GAME_WIDTH = ConfigUtil.getInt("server.frame.width");public static final int GAME_HEIGHT = ConfigUtil.getInt("server.frame.height");TextArea tmsg = new TextArea("messages:");TextArea tclient = new TextArea("clients:");public static final ServerFrame INSTANCE = new ServerFrame();public static void main(String[] args) throws Exception {INSTANCE.setVisible(true);ChatServer.start();}private ServerFrame() throws HeadlessException {//创建游戏的主Framethis.setTitle("chat room");this.setSize(GAME_WIDTH, GAME_HEIGHT);this.setLocation(100, 100);tmsg.setFont(new Font("Calibri",Font.PLAIN,20));tclient.setFont(new Font("Calibri",Font.PLAIN,20));Panel p = new Panel(new GridLayout(1, 2));p.add(tmsg);p.add(tclient);this.add(p);this.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});log.info("Server Main frame initialization completed");}public void updateMsg(String text) {tmsg.setText(tmsg.getText() + Constants.LINE_SEPERATOR + text);}public void updateClient(String text) {tclient.setText(tclient.getText() + Constants.LINE_SEPERATOR + text);}
        }@Slf4j
        public class ChatServer {static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);public static void start(){//总管线程组EventLoopGroup bossGroup = new NioEventLoopGroup(1);//接待员线程EventLoopGroup workerGroup = new NioEventLoopGroup(2);try {//服务器启动辅助类ServerBootstrap b = new ServerBootstrap();//放在第一位的是总管线程组,第二位的就是接待员线程组。b.group(bossGroup, workerGroup);//异步全双工b.channel(NioServerSocketChannel.class);//接收到客户端连接的处理,相当于BIO的acceptb.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel sc) throws Exception {log.info("a client connected:{}", sc);sc.pipeline().addLast(new MyChildHandler());}});log.info("chat server has been started");ChannelFuture cf = b.bind(8888).sync();cf.channel().closeFuture().sync();} catch (Exception e) {log.error("ChatServer.exception", e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();log.info("chat server has been closed");}}
        }@Slf4j
        class MyChildHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ServerFrame.INSTANCE.updateClient("client connected:"+ctx.channel().remoteAddress());ChatServer.clients.add(ctx.channel());}/*** 读取客户端通道内的数据** @param msg 客户端消息* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;String str = buf.toString(StandardCharsets.UTF_8);log.info("channelRead().input,string:{},buf:{}", str, buf);if (StrUtil.equalsIgnoreCase(str, "__88__")) {ChatServer.clients.remove(ctx.channel());ctx.close();ServerFrame.INSTANCE.updateClient("client closed>"+ctx.channel().remoteAddress());log.info("The chat client has been closed:{}", ctx.channel());} else {ChatServer.clients.writeAndFlush(msg);ServerFrame.INSTANCE.updateMsg(ctx.channel().remoteAddress() + ">" + str);log.info("ChatServer.clients.writeAndFlush:{}", msg);}}/*** 异常处理** @param ctx* @param cause* @throws Exception*/@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("ChatServer.exceptionCaught:", cause);ChatServer.clients.remove(ctx.channel());ctx.close();}
        }
        
      9. 启动顺序。先启动ServerFrame,然后启动ChatFrame,ChatFrame可以启动多个。

      10. 多个客户端发送消息都会在服务端显示。

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

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

相关文章

小程序开发之登录授权

小程序开发登录授权流程 看懂这张图登录授权就没问题了&#xff08;哈哈哈哈哈&#xff09; 说明&#xff1a; 调用 wx.login() 获取 临时登录凭证code &#xff0c;并回传到开发者服务器。 调用 auth.code2Session 接口&#xff0c;换取 用户唯一标识 OpenID 和 会话密钥 sess…

【C++】Cmake使用教程(看这一篇就够了)

文章目录 引言一 环境搭建二 简单入门2.1 项目结构2.2 示例源码2.3 运行查看 三 编译多个源文件3.1 在同一个目录下有多个源文件3.1.1 简单版本3.1.1.1 项目结构3.1.1.2 示例代码3.1.1.3 运行查看 3.1.2 进阶版本3.1.2.1 项目结构3.1.2.2 示例源码3.1.2.3 运行查看 3.2 在不同目…

系列十一、AOP

一、概述 1.1、官网 AOP的中文名称是面向切面编程或者面向方面编程&#xff0c;利用AOP可以对业务逻辑的各个部分进行隔离&#xff0c;从而使得业务逻辑各部分之间的耦合度降低&#xff0c;提高程序的可重用性&#xff0c;同时提高了开发的效率。 1.2、通俗描述 不通过…

横扫“盲区”、“看透”缺陷,维视智造推出短波红外相机

在可见光领域&#xff0c;工业相机的视觉应用已经十分成熟&#xff0c;但在日常的客户咨询中&#xff0c;我们也经常接到一些“超纲需求”——客户想要检测“白底上的白色缺陷”、“不透明包装内的透明物体有无”等&#xff0c;均属于可见光无法实现的检测&#xff0c;而市面上…

API接口开发管理平台--多领域企业数字化管理的解决方案

随着数字化时代的到来&#xff0c;企业需要进行数字化转型才能更好地适应市场需求和用户需求。而API接口则是数字化转型中的重要组成部分&#xff0c;可以帮助企业更好地管理信息&#xff0c;提高效率。本文将介绍一种解决方案--API接口开发管理平台&#xff0c;该平台开发出多…

Pygame编程(1)初始化和退出模块

初始化和退出模块 pygame使用基础流程 初始化模块设置主屏窗口程序主循环&#xff08;处理键盘、鼠标、游戏杆、触摸屏等事件&#xff09;退出模块终止程序 import sys import pygame from pygame.locals import *# 1.初始化模块 pygame.init()# 2.设置主屏窗口 display pyg…

SocketTools.NET 11.0.2148.1554 Crack

添加新功能以简化使用 URL 建立 TCP 连接的过程。 2023 年 8 月 23 日 - 12:35新版本 特征 添加了“HttpGetTextEx”函数&#xff0c;该函数在返回字符串缓冲区中的文本内容时提供附加选项。添加了对“FileTransfer”.NET 类和 ActiveX 控件中的“GetText”和“PutText”方法的…

HDLBits-Verilog学习记录 | Verilog Language-Modules(1)

文章目录 20.Module21.Connecting ports by position | Moudle pos22.Connecting ports by name | Module name23.Three modules | Module shift24.Modules and vectors | Module shift8 20.Module practice:You may connect signals to the module by port name or port posi…

FFmpeg支持多线程编码并保存mp4文件示例

之前介绍的示例&#xff1a; (1).https://blog.csdn.net/fengbingchun/article/details/132129988 中对编码后数据保存成mp4 (2).https://blog.csdn.net/fengbingchun/article/details/132128885 中通过AVIOContext实现从内存读取数据 (3).https://blog.csdn.net/fengbingchun/…

几个nlp的小任务(机器翻译)

几个nlp的小任务(机器翻译) 安装依赖库数据集介绍与模型介绍加载数据集看一看数据集的样子评测测试数据预处理测试tokenizer处理目标特殊的token预处理函数对数据集的所有数据进行预处理微调预训练模型设置训练参数需要一个数据收集器,把处理好数据喂给模型设置评估方法参数…

美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?

周末的时候,有一位小伙伴提了一些关于 ConcurrentHashMap 的问题,都是他最近面试遇到的。原提问如下(星球原贴地址:https://t.zsxq.com/11jcuezQs ): 整个提问看着非常复杂,其实归纳来说就是两个问题: ConcurrentHashMap 为什么 key 和 value 不能为 null?ConcurrentH…

【C++ 学习 ⑱】- 多态(上)

目录 一、多态的概念和虚函数 1.1 - 用基类指针指向派生类对象 1.2 - 虚函数和虚函数的重写 1.3 - 多态构成的条件 1.4 - 多态的应用场景 二、协变和如何析构派生类对象 2.1 - 协变 2.2 - 如何析构派生类对象 三、C11 的 override 和 final 关键字 一、多态的概念和虚…

webrtc的Sdp中的Plan-b和UnifiedPlan

在一些类似于视频会议场景下&#xff0c;媒体会话参与者需要接收或者发送多个流&#xff0c;例如一个源端&#xff0c;同时发送多个左右音轨的音频&#xff0c;或者多个摄像头的视频流&#xff1b;在2013年&#xff0c;提出了2个不同的SDP IETF草案Plan B和Unified Plan&#x…

android framework之Applicataion启动流程分析

Application启动流程分析 启动方式一&#xff1a;通过Launcher启动app 启动方式二&#xff1a;在某一个app里启动第二个app的Activity. 以上两种方式均可触发app进程的启动。但无论哪种方式&#xff0c;最终通过通过调用AMS的startActivity()来启动application的。 根据上图…

ABeam×Startup | 德硕管理咨询(深圳)创新研究团队拜访微漾创客空间

近日&#xff0c;德硕管理咨询&#xff08;深圳&#xff09;&#xff08;以下简称&#xff1a;“ABeam-SZ”&#xff09;创新研究团队前往微漾创客空间&#xff08;以下简称&#xff1a;微漾&#xff09;拜访参观&#xff0c;并展开合作交流。会议上&#xff0c;双方相互介绍了…

每日一题 57. 插入区间

读研了&#xff0c;开始用python刷题 今天的题目是力扣 每日一题 57. 插入区间 难度&#xff1a;中等 思路&#xff1a; 处理新区间起点&#xff0c;要么在两个老区间之间&#xff0c;要么被一个老区间包含处理新区间中点&#xff0c;同起点一样 我的代码如下 class Solut…

解锁市场进入成功:GTM 策略和即用型示例

在最初的几年里&#xff0c;创办一家初创公司可能会充满挑战。根据美国小企业管理局的数据&#xff0c;大约三分之二的新成立企业存活了两年&#xff0c;几乎一半的企业存活了五年以上。导致创业失败的因素有市场需求缺失、资金短缺、团队不合适、成本问题等。由此&#xff0c;…

合宙Air724UG LuatOS-Air LVGL API控件--按钮 (Button)

按钮 (Button) 按钮控件&#xff0c;这个就不用多说了&#xff0c;界面的基础控件之一。 示例代码 – 按键回调函数 event_handler function(obj, event) if event lvgl.EVENT_CLICKED then print(“Clicked\n”) elseif event lvgl.EVENT_VALUE_CHANGED then print(“To…

服务器数据库中了locked勒索病毒怎么办,locked勒索病毒恢复工具

最近一段时间网络上的locked勒索病毒非常嚣张&#xff0c;自从6月份以来&#xff0c;很多企业的计算机服务器数据库遭到了locked勒索病毒的攻击&#xff0c;起初locked勒索病毒攻击用友畅捷通T用户&#xff0c;后来七月份开始攻击金蝶云星空客户&#xff0c;导致企业的财务系统…

揭秘视频号创收计划:松松一个月赚1300+

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 这是卢松松一个月视频号的收益&#xff0c;1300元。自从视频号在五月份推出创作者分成计划以来&#xff0c;许许多多的视频号创作者开始获得了一些收益&#xff0c;这绝对是一项挺不错的进展。 目前…