目录
Netty专栏目录(点击进入…)
Netty TCP客户端(TcpClient)
Reactor Netty提供了易于使用和易于配置的TcpClient。它隐藏了创建TCP客户端所需的大部分 Netty功能,并添加了Reactive Streams背压(Reactive Streams是具有无阻塞背压的异步流处理的标准)
连接和断开
要将TCP客户端连接到给定端点,必须创建并配置一个 TcpClient实例。默认情况下,host是localhost和port是12012
创建一个TcpClient:
返回的Connection提供了一个简单的连接 API,包括disposeNow(),它以阻塞方式关闭客户端
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() // 创建一个TcpClient 准备好配置的实例 .connectNow(); // 以阻塞方式连接客户端并等待它完成初始化 connection.onDispose() .block(); } }
主机和端口
要连接到特定的host和port,可以将以下配置应用到TCP客户端
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") // 配置TCP主机 .port(80) // 配置TCP端口 .connectNow(); connection.onDispose() .block(); } }
急切初始化
默认情况下,TcpClient资源的初始化是按需进行的。这意味着connect operation吸收了初始化和加载所需的额外时间
当需要预加载这些资源时,可以进行TcpClient如下配置:
import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { TcpClient tcpClient = TcpClient.create() .host("example.com") .port(80) .handle((inbound, outbound) -> outbound.sendString(Mono.just("hello"))); // 初始化并加载事件循环组、主机名解析器、本机传输库和用于安全性的本机库 tcpClient.warmup() .block(); // 连接到远程对等方时发生主机名解析 Connection connection = tcpClient.connectNow(); connection.onDispose() .block(); } }
写入数据
要将数据发送到给定端点,必须附加一个I/O处理程序。I/O处理程序有权访问以NettyOutbound 能够写入数据
import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) // 将hello字符串发送到服务端点 .handle((inbound, outbound) -> outbound.sendString(Mono.just("hello"))) .connectNow(); connection.onDispose() .block(); } }
当需要更多地控制写入过程时,作为I/O处理程序的替代方案,可以使用Connection#outbound. 与在提供的Publisher完成时关闭连接的I/O处理程序相反(在finite的情况下Publisher),当使用时Connection#outbound,必须显式调用Connection#dispose以关闭连接。
import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) .connectNow(); connection.outbound() .sendString(Mono.just("hello 1")) // 将hello 1字符串发送到端点 .then() .subscribe(); connection.outbound() .sendString(Mono.just("hello 2")) // 将hello 2字符串发送到端点 .then() .subscribe(null, null, connection::dispose); // 将消息发送到端点后关闭连接 connection.onDispose() .block(); } }
消费数据
要从给定端点接收数据,必须附加一个I/O处理程序。I/O处理程序有权访问NettyInbound以读取数据
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) // 从给定端点接收数据 .handle((inbound, outbound) -> inbound.receive().then()) .connectNow(); connection.onDispose() .block(); } }
当需要对读取过程进行更多控制时(在不同的地方进行不同的数据处理),作为I/O处理程序的替代方案,可以使用 Connection#inbound。与在提供的Publisher完成时关闭连接的 I/O 处理程序相反(在finite的情况下Publisher),当使用Connection#inbound时,必须显式调用Connection#dispose以关闭连接。
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) .connectNow(); connection.inbound() .receive() // 从给定端点接收数据 .then() .subscribe(); connection.onDispose() .block(); } }
生命周期回调
TcpClient提供了以下生命周期回调函数以便扩展
回调函数 | 描述 |
---|---|
doAfterResolve | 在成功解析远程地址后调用 |
doOnChannelInit | 初始化通道时调用 |
doOnConnect | 当通道即将连接时调用 |
doOnConnected | 在通道连接后调用 |
doOnDisconnected | 在通道断开连接后调用 |
doOnResolve | 在即将解析远程地址时调用 |
doOnResolveError | 在远程地址未成功解析的情况下调用 |
Option和childOption参数设置
Option和childOption参数设置(点击进入…)
TCP-level配置(三种配置)
(1)Channel Options:通道参数选项
默认情况下,TCP客户端配置有以下选项
this.config = new TcpClientConfig( provider, Collections.singletonMap(ChannelOption.AUTO_READ, false), () -> AddressUtils.createUnresolved(NetUtil.LOCALHOST.getHostAddress(), DEFAULT_PORT));
如果需要其他选项或需要更改当前选项,可以应用以下配置:
import io.netty.channel.ChannelOption; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) .connectNow(); connection.onDispose() .block(); } }
(2)Wire Logger:连线日志记录
Reactor Netty支持3种不同的格式化程序:
连线日志格式化 | 描述 |
---|---|
AdvancedByteBufFormat#HEX_DUM | 同时记录事件和内容。内容将采用十六进制格式(默认) |
AdvancedByteBufFormat#SIMPLE | 使用此格式启用连线记录时,仅记录事件 |
AdvancedByteBufFormat#TEXTUAL | 同时记录事件和内容。内容将采用纯文本格式 |
import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; public class Application { public static void main(String[] args) { DisposableServer server = HttpServer.create() // 启用连线记录 .wiretap(true) .bindNow(); server.onDispose() .block(); } }
当需要更改默认格式化程序时,可以按如下方式进行配置:
import io.netty.handler.logging.LogLevel; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; import reactor.netty.transport.logging.AdvancedByteBufFormat; public class Application { public static void main(String[] args) { DisposableServer server = HttpServer.create() // 启用连线记录, AdvancedByteBufFormat#TEXTUAL用于打印内容。 .wiretap("logger-name", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) .bindNow(); server.onDispose() .block(); } }
(3)Event Loop Group:事件循环组
默认情况下,TCP客户端使用“事件循环组”,其中工作线程的数量等于初始化时运行时可用的处理器数量(但最小值为4)。当需要不同的配置时,可以使用LoopResource#create方法之一
以下清单显示了事件循环组的默认配置:ReactorNetty.java
/** *默认工作线程数,回退到可用处理器(但最小值为4) */ public static final String IO_WORKER_COUNT = "reactor.netty.ioWorkerCount"; /** *默认选择器线程计数,回退到-1(无选择器线程) */ public static final String IO_SELECT_COUNT = "reactor.netty.ioSelectCount"; /** *UDP的默认工作线程数,回退到可用处理器(但最小值为4 */ public static final String UDP_IO_THREAD_COUNT = "reactor.netty.udp.ioThreadCount"; /** * 默认的静默期,保证不会发生对底层循环资源的处置,回退到2秒 */ public static final String SHUTDOWN_QUIET_PERIOD = "reactor.netty.ioShutdownQuietPeriod"; /** * 默认情况下,无论任务是否在静默期内提交,在处理底层资源之前等待的最长时间为15秒。 */ public static final String SHUTDOWN_TIMEOUT = "reactor.netty.ioShutdownTimeout"; /** * 默认值是否首选本机传输(epoll、kqueue),回退在可用时是否首选 */ public static final String NATIVE = "reactor.netty.native";
如果需要更改这些设置,可以应用以下配置:
import reactor.netty.Connection; import reactor.netty.resources.LoopResources; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { LoopResources loop = LoopResources.create("event-loop", 1, 4, true); Connection connection = TcpClient.create() .host("example.com") .port(80) .runOn(loop) .connectNow(); connection.onDispose() .block(); } }
连接池
默认情况下,Reactor Netty client(客户端)使用一个“固定”连接池,其中500个为活动通道的最大数量(Channel),1000个为允许保持挂起状态的进一步通道采集尝试的最大数量(对于其余配置,请检查下面的系统属性或构建器配置)。这意味着,如果有人试图获取一个通道,只要创建的通道少于500个,并且由池管理,那么实现就会创建一个新的通道。当达到池中通道的最大数量时,最多会延迟1000次获取通道的新尝试(挂起),直到通道再次返回到池中,并且会因错误而拒绝进一步尝试。
默认“固定”连接池:
①500个为活动通道Channel)
②1000个为允许保持挂起状态的进一步通道采集尝试的最大数量
ReactorNetty.java
/** 默认最大连接数。退回到2个可用处理器(但最小值为16) */ public static final String POOL_MAX_CONNECTIONS = "reactor.netty.pool.maxConnections"; /** * 出错前的默认采集超时(毫秒)。如果-1在以无限制方式打开新连接之前,永远不会等待获取。后退45秒 */ public static final String POOL_ACQUIRE_TIMEOUT = "reactor.netty.pool.acquireTimeout"; /** * 默认最大空闲时间,fallback -未指定最大空闲时间。 */ public static final String POOL_MAX_IDLE_TIME = "reactor.netty.pool.maxIdleTime"; /** * 默认最大使用寿命,fallback -未指定最大使用寿命 */ public static final String POOL_MAX_LIFE_TIME = "reactor.netty.pool.maxLifeTime"; /** *默认租赁策略(先进先出、后进先出),fallback - fifo fifo-连接选择为先进先出 lifo -连接选择为后进先出 */ public static final String POOL_LEASING_STRATEGY = "reactor.netty.pool.leasingStrategy"; /** * 与{@link SamplingAllocationStrategy}一起使用的默认{@code getpermitsamplingrate}(介于0d和1d(百分比)之间)。 此策略包装了一个{@link PoolBuilder#sizeBetween(int,int)sizeBetween}{@link AllocationStrategy},并对{@link AllocationStrategy#getpermissions(int)}的调用进行了示例。回退-未启用。 */ public static final String POOL_GET_PERMITS_SAMPLING_RATE = "reactor.netty.pool.getPermitsSamplingRate"; /** * 与{@link SamplingLocationStrategy}一起使用的默认{@code returnPermitsSamplingRate}(介于0d和1d(百分比)之间)。 此策略包装了一个{@link PoolBuilder#sizeBetween(int,int)sizeBetween}{@link AllocationStrategy},并对{@link AllocationStrategy#returnpermissions(int)}的调用进行了示例。回退-未启用。 */ public static final String POOL_RETURN_PERMITS_SAMPLING_RATE = "reactor.netty.pool.returnPermitsSamplingRate";
当需要更改默认设置时,可以进行ConnectionProvider如下配置:
import reactor.netty.Connection; import reactor.netty.resources.ConnectionProvider; import reactor.netty.tcp.TcpClient; import java.time.Duration; public class Application { public static void main(String[] args) { ConnectionProvider provider = ConnectionProvider.builder("fixed") // 最大连接数 .maxConnections(50) // 将连接保持空闲的最长时间配置为 20 秒 .maxIdleTime(Duration.ofSeconds(20)) // 将连接保持活动的最长时间配置为 60 秒 .maxLifeTime(Duration.ofSeconds(60)) // 将挂起获取操作的最长时间配置为 60 秒 .pendingAcquireTimeout(Duration.ofSeconds(60)) //每两分钟,将定期检查连接池中是否存在适用于删除的连接 .evictInBackground(Duration.ofSeconds(120)) .build(); Connection connection = TcpClient.create(provider) .host("example.com") .port(80) .connectNow(); connection.onDispose() .block(); } }
当期望高负载时,请谨慎使用具有非常高的最大连接值的连接池。
reactor.netty.http.client.PrematureCloseException由于打开/获取的并发连接过多,可能会遇到 根本原因“连接超时”的异常。
需要禁用连接池,可以应用如下配置:
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.newConnection() .host("example.com") .port(80) .connectNow(); connection.onDispose() .block(); } }
(1)disposeInactivePoolsInBackground:检查连接池
启用此选项后,会在后台定期检查连接池,那些在指定时间内为空且不活动的连接池将有资格进行处理。默认情况下,禁用非活动池的这种后台处理。
(2)disposeTimeout:处理超时
当ConnectionProvider#dispose()或ConnectionProvider#disposeLater()被调用时,使用此宽限期超时触发连接池的正常关闭。从那时起,所有获取连接的调用都会快速失败,但会出现异常。但是,对于所提供的Duration,待处理的获取将有机会得到服务。注:拒绝新的获取和计时器立即启动风度,不论认购到Mono的返回ConnectionProvider#disposeLater()。随后的调用返回相同的Mono,有效地从第一个正常关闭调用中获取通知并忽略随后提供的超时。默认情况下,不指定处理超时。
(3)evictInBackground:定期检查符合删除条件的连接
启用此选项后,每个连接池都会根据驱逐标准定期检查符合删除条件的连接maxIdleTime。默认情况下,此后台驱逐是禁用的。
(4)fifo:默认租赁策略 - 先进先出
配置连接池,如果有空闲连接(即池未充分利用),下一次获取操作将获取Least Recently Used连接(LRU,即当前空闲连接中最先释放的连接)。默认租赁策略。
(5)lifo:租赁策略 - 后进先出
配置连接池,如果有空闲连接(即池未充分利用),下一次获取操作将获取Most Recently Used连接(MRU,即当前空闲连接中最后释放的连接)。
(6)maxConnections:连接数
开始挂起之前的最大连接数(每个连接池)。默认:2 * 可用处理器数(但最小值为16)。
(7)maxIdleTime:最大空闲时间
空闲时通道可以关闭的时间(分辨率:ms)。默认值:未指定最大空闲时间。
(8)maxLifeTime:最大寿命
通道有资格关闭的总生命周期(分辨率:ms)。默认值:未指定最大寿命。
(9)metrics:监控指标
启用/禁用与Micrometer的内置集成。ConnectionProvider.MeterRegistrar可以提供与另一个度量系统的集成。默认情况下,metrics未启用。
(10)pendingAcquireMaxCount:挂起队列中的最大额外尝试次数
获取连接以保留在挂起队列中的最大额外尝试次数。如果指定 -1,则挂起队列没有上限。
默认:2 * 最大连接数。
(11)pendingAcquireTimeout:挂起获取必须完成或抛出TimeoutException之前的最长时间
挂起获取必须完成或抛出TimeoutException之前的最长时间(分辨率:毫秒)。如果指定了 -1,则不会应用此类超时。默认值:45秒。
Metrics(监控指标)
TCP客户端支持内置集成的Micrometer,公开了前缀为reactor.netty.connection.provider的所有指标
启用内置集成:
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) .metrics(true) // 启用与 Micrometer 的内置集成 .connectNow(); connection.onDispose() .block(); } }
当需要TCP客户端指标与系统集成时,Micrometer或者想提供自己的集成Micrometer,可以提供自己的指标记录器:
import reactor.netty.Connection; import reactor.netty.channel.ChannelMetricsRecorder; import reactor.netty.tcp.TcpClient; import java.net.SocketAddress; import java.time.Duration; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) // 启用 TCP 客户端指标并提供ChannelMetricsRecorder实现 .metrics(true, CustomChannelMetricsRecorder::new) .connectNow(); connection.onDispose() .block(); } }
(1)TCP客户端指标的信息
指标名称 | 类型 | 描述 |
---|---|---|
reactor.netty.tcp.client.data.received | DistributionSummary | 接收的数据量,以字节为单位 |
reactor.netty.tcp.client.data.sent | DistributionSummary | 发送的数据量,以字节为单位 |
reactor.netty.tcp.client.errors | Counter | 发生的错误数 |
reactor.netty.tcp.client.tls.handshake.time | Timer | TLS握手所花费的时间 |
reactor.netty.tcp.client.connect.time | Timer | 连接到远程地址所花费的时间 |
reactor.netty.tcp.client.address.resolver | Timer | 解析地址所花费的时间 |
(2)ConnectionProvider指标
指标名称 | 类型 | 描述 |
---|---|---|
reactor.netty.connection.provider.total.connections | Gauge | 所有连接数,活动或空闲 |
reactor.netty.connection.provider.active.connections | Gauge | 已成功获取且正在使用中的连接数 |
reactor.netty.connection.provider.max.connections | Gauge | 允许的最大活动连接数 |
reactor.netty.connection.provider.idle.connections | Gauge | 空闲连接数 |
reactor.netty.connection.provider.pending.connections | Gauge | 等待连接的请求数 |
reactor.netty.connection.provider.max.pending.connections | Gauge | 等待就绪连接时将排队的最大请求数 |
(3)ByteBufAllocator metrics(指标)
指标名称 | 类型 | 描述 |
---|---|---|
reactor.netty.bytebuf.allocator.used.heap.memory | Gauge | 堆内存的字节数 |
reactor.netty.bytebuf.allocator.used.direct.memory | Gauge | 直接内存的字节数 |
reactor.netty.bytebuf.allocator.used.heap.arenas | Gauge | 堆区域的数量(当PooledByteBufAllocator) |
reactor.netty.bytebuf.allocator.used.direct.arenas | Gauge | 直接竞技场的数量(当PooledByteBufAllocator) |
reactor.netty.bytebuf.allocator.used.threadlocal.caches | Gauge | 线程本地缓存的数量(当PooledByteBufAllocator) |
reactor.netty.bytebuf.allocator.used.small.cache.size | Gauge | 小缓存的大小(当PooledByteBufAllocator) |
reactor.netty.bytebuf.allocator.used.normal.cache.size | Gauge | 正常缓存的大小(当PooledByteBufAllocator) |
reactor.netty.bytebuf.allocator.used.chunk.size | Gauge | 竞技场的块大小(当PooledByteBufAllocator) |
EventLoop metrics(指标)
指标名称 | 类型 | 描述 |
---|---|---|
reactor.netty.eventloop.pending.tasks | Gauge | 事件循环中待处理的任务数 |
Unix域套接字
TCP当使用本机传输时,客户端支持Unix域套接字(UDS)
import io.netty.channel.unix.DomainSocketAddress; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() // 指定DomainSocketAddress将被使用 .remoteAddress(() -> new DomainSocketAddress("/tmp/test.sock")) .connectNow(); connection.onDispose() .block(); } }
主机名解析
默认情况下,TcpClient使用Netty的域名查找机制异步解析域名。这是JVM内置阻塞解析器的替代方案。
当需要更改默认设置时,可以进行TcpClient如下配置:
import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; import java.time.Duration; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) // 此解析器执行的每个 DNS 查询的超时将为 500 毫秒 .resolver(spec -> spec.queryTimeout(Duration.ofMillis(500))) .connectNow(); connection.onDispose() .block(); } }
有时,可能希望切换到JVM内置解析器。可以进行TcpClient如下配置:
import io.netty.resolver.DefaultAddressResolverGroup; import reactor.netty.Connection; import reactor.netty.tcp.TcpClient; public class Application { public static void main(String[] args) { Connection connection = TcpClient.create() .host("example.com") .port(80) // 设置JVM内置解析器。 .resolver(DefaultAddressResolverGroup.INSTANCE) .connectNow(); connection.onDispose() .block(); } }
可用的配置:
(1)cacheMaxTimeToLive:缓存DNS资源记录的最大生存时间
缓存的DNS资源记录的最大生存时间(分辨率:秒)。如果DNS服务器返回的DNS资源记录的生存时间大于此最大生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最大生存时间。默认为Integer.MAX_VALUE.
(2)cacheMinTimeToLive:缓存DNS资源记录的最小生存时间
缓存的DNS资源记录的最小生存时间(分辨率:秒)。如果DNS服务器返回的DNS资源记录的生存时间小于此最小生存时间,则此解析器将忽略来自DNS服务器的生存时间并使用此最小生存时间。默认值:0。
(3)cacheNegativeTimeToLive:失败DNS查询的缓存的生存时间
失败的DNS查询的缓存的生存时间(分辨率:秒)。默认值:0。
(4)completeOncePreferredResolved:解析器会在首选地址类型的所有查询完成后立即通知
启用此设置后,解析器会在首选地址类型的所有查询完成后立即通知。禁用此设置后,解析器会在所有可能的地址类型完成时发出通知。此配置适用于DnsNameResolver#resolveAll(String)。默认:启用
(5)disableOptionalRecord:禁用可选记录的自动包含
禁用可选记录的自动包含,该记录试图向远程DNS服务器提供有关解析器每个响应可以读取多少数据的提示。默认:启用。
(6)disableRecursionDesired:解析器是否必须发送设置了递归所需(RD)标志的DNS查询
指定此解析器是否必须发送设置了递归所需(RD)标志的DNS查询。默认:启用。
(7)hostsFileEntriesResolver:文件条目分解器
设置HostsFileEntriesResolver用于主机文件条目的自定义。默认值:DefaultHostsFileEntriesResolver。
(8)maxPayloadSize:数据报包缓冲区的容量
设置数据报包缓冲区的容量(以字节为单位)。默认值:4096 byte。
(9)maxQueriesPerResolve:解析主机名时允许发送的最大DNS查询数
设置解析主机名时允许发送的最大DNS查询数。默认值:16。
(10)ndots:初始绝对查询之前必须出现在名称中的点数
设置在进行初始绝对查询之前必须出现在名称中的点数。默认值:-1(确定来自Unix上的操作系统的值,否则使用值1)
(11)queryTimeout:解析器执行的每个DNS查询的超时时间
设置此解析器执行的每个DNS查询的超时时间(分辨率:毫秒)。默认值:5000。
(12)resolvedAddressTypes:解析地址的协议族列表
解析地址的协议族列表。
(13)bindAddressSupplier:本地地址的供应商
要绑定到的本地地址的供应商。
(14)roundRobinSelection:服务器地址的随机选择
启用DnsNameResolver的AddressResolver组,如果名称服务器提供多个地址,则该组支持随机选择目标地址。请参阅RoundRobinDnsAddressResolverGroup。默认值:DnsAddressResolverGroup
(15)runOn:在给定资源上执行与DNS服务器的通信
在给定资源上执行与DNS服务器的通信。默认情况下,将使用在客户端级别指定的LoopResources。
(16)searchDomains:解析器的搜索域列表
解析器的搜索域列表。默认情况下,使用系统DNS搜索域填充有效搜索域列表。
(17)trace:解析失败的情况下生成详细跟踪信息
在解析失败的情况下生成详细跟踪信息时,此解析器将使用的特定记录器和日志级别。
猜你喜欢
- 6天前【PIMF】OpenHarmony浏览器上新,在开发板上优雅地浏览网页
- 6天前【HarmonyOS】小熊派鸿蒙系统搭建
- 6天前【前沿技术RPA】 一文了解UiPath 通过Invoke Method 和 Invoke Code增强自动化功能
- 6天前【使用opencv、python、dlib实现人脸关键点检测、眨眼检测和嘴巴开闭检测,可简单用于疲劳检测】
- 6天前多输入多输出 | Matlab实现PSO-LSTM粒子群优化长短期记忆神经网络多输入多输出预测
- 6天前微信小程序 安卓IOS兼容问题
- 6天前计算机设计大赛 深度学习 python opencv 火焰检测识别 火灾检测
- 6天前SpringMVC
- 6天前Unity 编辑器篇|(十三)自定义属性绘制器(PropertyDrawer ,PropertyAttribute) (全面总结 | 建议收藏)
- 6天前Unity手机游戏开发:从搭建到发布上线全流程实战
网友评论
- 搜索
- 最新文章
- 热门文章