Netty基础—5.Netty的使用简介
大纲
1.Netty服务端的启动流程
2.服务端IO事件的处理类
3.Netty客户端的启动流程
4.客户端IO事件的处理类
5.启动Netty服务端和客户端的方法说明
6.Netty服务端和客户端使用总结
7.什么是TCP粘包拆包
8.TCP粘包拆包的几种情况
9.TCP粘包拆包的原因
10.粘包问题的解决策略
11.拆包的原理
12.粘包问题演示
13.换行符解码器LineBasedFrameDecoder
14.分隔符解码器DelimiterBasedFrameDecoder
15.固定长度解码器FixedLengthFrameDecoder
16.基于长度域解码器LengthFieldBasedDecoder
17.Java序列化的缺点
18.Netty基本组件与BIO的对应
1.Netty服务端的启动流程
步骤一:
首先创建两个NioEventLoopGroup实例,bossGroup实例用于接收客户端的连接,workerGroup实例用于处理每个连接的读写。NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于处理网络事件。
步骤二:
然后创建ServerBootstrap实例,ServerBootstrap是Netty用于启动NIO服务端的启动引导类。
步骤三:
接着调用ServerBootstrap的group()方法指定线程模型,也就是将两个NIO线程组传递到ServerBootstrap中。然后调用ServerBootstrap的channel()方法指定IO模型为NIO,也就是指定创建的Channel为NioServerSocketChannel。然后调用ServerBootstrap的option()方法指定TCP参数,也就是配置NioServerSocketChannel的TCP参数。然后调用ServerBootstrap的childHandler()方法指定业务处理逻辑,也就是绑定IO事件的处理类ChildHandler等。
步骤四:
完成服务端的辅助启动类的配置后,就调用它的bind()方法来异步绑定监听端口。然后继续调用它的sync()方法进行同步阻塞来等待绑定操作完成,绑定操作完成后会返回一个ChannelFuture。
步骤五:
接着使用ChannelFuture的方法进行阻塞,直到服务端链路关闭。
步骤六:
最后使用NIO线程组(NioEventLoopGroup)的shutdownGracefully()方法进行优雅退出。
public class NettyServer {public static void main(String[] args) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();//相当于Netty的服务器b.group(bossGroup, workerGroup)//指定线程模型.channel(NioServerSocketChannel.class)//指定IO模型为NIO.option(ChannelOption.SO_BACKLOG, 1024)//指定TCP参数.childHandler(new ChannelInitializer<NioSocketChannel>() {//指定IO处理逻辑 @Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new NettyServerHandler());//针对网络请求的处理逻辑}});ChannelFuture f = b.bind(50070).sync();//绑定端口,同步等待成功f.channel().closeFuture().sync();//等待服务端监听端口关闭} catch (Exception e) {e.printStackTrace(); } finally {//优雅退出,释放线程资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
2.服务端IO事件的处理类
说明一:
IO事件的处理类继承自ChannelInboundHandlerAdapter,服务端IO事件的处理类主要需要关注三个方法:channelRead()、channelReadComplete()、exceptionCaught()。
说明二:
ByteBuf类似于NIO的ByteBuffer,但功能更强大和灵活。通过ByteBuf的readableBytes()方法可以获得缓冲区可读的字节数,然后就可以根据缓冲区可读的字节数创建byte数组,接着通过ByteBuf的readBytes()方法便可以将缓冲区的字节数组复制到新创建的byte数组中。
说明三:
通过ChannelHandlerContext的write()方法会把待发送的消息放到发送缓冲区中,通过ChannelHandlerContext的flush()方法会将发送缓冲区中的消息写入到SocketChannel中发送出去。
为了防止频繁唤醒Selector进行消息发送,ChannelHandlerContext的write()方法并不直接将消息写入SocketChannel中,而只是把消息放到发送缓冲区中。当调用ChannelHandlerContext的flush()方法时,才会将发送缓冲区中的消息写入到SocketChannel中发送出去。
public class NettyServerHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf requestBuffer = (ByteBuf) msg;byte[] requestBytes = new byte[requestBuffer.readableBytes()];requestBuffer.readBytes(requestBytes);String request = new String(requestBytes, "UTF-8");System.out.println("接收到的请求:" + request); String response = "你好,我收到你的消息了";ByteBuf responseBuffer = Unpooled.copiedBuffer(response.getBytes());ctx.write(responseBuffer);//Netty底层就有类似Processor的东西,负责从网络连接中读取请求//然后把读取出来的请求交给这里的Handler来处理,处理完以后把响应返回回去}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}
3.Netty客户端的启动流程
步骤一:
首先创建客户端处理IO读写的NioEventLoopGroup实例。NioEventLoopGroup是个线程组,它包含了一组NIO线程,专门用于处理网络事件。
步骤二:
然后创建Bootstrap实例,Bootstrap是Netty用于启动NIO客户端的启动引导类。
步骤三:
接着调用Bootstrap的group()、channel()、option()、handler()方法配置好:线程模型、IO模型、TCP参数、业务处理逻辑。
步骤四:
完成客户端启动辅助类的配置后,就调用它的connect()方法来异步发起连接,然后调用sync()方法进行同步阻塞来等待连接成功。
步骤五:
接着使用ChannelFuture的方法进行阻塞,直到客户端连接关闭。
步骤六:
最后使用NIO线程组(NioEventLoopGroup)的shutdownGracefully()方法进行优雅退出。
public class NettyClient {public static void main(String[] args) {NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group)//指定线程模型.channel(NioSocketChannel.class)//指定IO模型为NIO.option(ChannelOption.TCP_NODELAY, true)//指定TCP参数.handler(new ChannelInitializer<Channel>() {//指定IO处理逻辑@Overrideprotected void initChannel(Channel channel) throws Exception {channel.pipeline().addLast(new NettyClientHandler()); }});ChannelFuture f = bootstrap.connect("127.0.0.1", 50070).sync();//建立连接f.channel().closeFuture().sync();} catch (Exception e) {e.printStackTrace();} finally {group.shutdownGracefully();}}
}
4.客户端IO事件的处理类
说明一:
IO事件的处理类继承自ChannelInboundHandlerAdapter,客户端IO事件的处理类主要需要关注三个方法:channelActive()、channelRead()、exceptionCaught()。
说明二:
当客户端和服务端建立好TCP连接后,Netty的NIO线程会调用channelActive()方法,而调用ChannelHandlerContext的writeAndFlush()方法会将请求消息发送给服务端。
说明三:
当服务端返回应答消息时,channelRead()方法会被调用。当发生异常时,exceptionCaught()方法会被调用。
注意:Netty里的数据是以ByteBuf为单位的,所有需要读和写的数据都必须放到一个ByteBuf中。其中通过ctx.alloc().buffer()可以分配一个ByteBuf,通过Unpooled.buffer()也可以分配一个ByteBuf。
public class NettyClientHandler extends ChannelInboundHandlerAdapter {private ByteBuf requestBuffer;public NettyClientHandler() {byte[] requestBytes = "你好,我发送第一条消息".getBytes();requestBuffer = Unpooled.buffer(requestBytes.length);requestBuffer.writeBytes(requestBytes);}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.writeAndFlush(requestBuffer);}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf responseBuffer = (ByteBuf) msg;byte[] responseBytes = new byte[responseBuffer.readableBytes()];responseBuffer.readBytes(responseBytes);String response = new String(responseBytes, "UTF-8"); System.out.println("接收到服务端的响应:" + response); }@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}
5.启动Netty服务端和客户端的方法说明
一.ServerBoostrap的bind()方法
bind()方法是一个异步方法,调用之后会立即返回一个ChannelFuture,通过ChannelFuture的addListener()方法可以添加监听器监听端口是否绑定成功。
二.ServerBoostrap的handler()方法
childHandler()方法用于处理新连接的数据读写逻辑,handler()方法用于处理服务端启动过程中的一些逻辑,但一般不用该方法。
三.ServerBoostrap的attr()方法
attr()方法可以给服务端Channel也就是NioServerSocketChannel指定自定义属性,然后可以通过channel.attr()取出这些属性。
四.ServerBoostrap的childAttr()方法
childAttr()方法也可以给每个连接指定自定义属性。
五.ServerBoostrap的option()方法
option()方法可以给服务端Channel也就是NioServerSocketChannel设置TCP参数,比如b.option(ChannelOption.SO_BACKLOG, 1024)就是设置临时存放已完成三次握手的请求的队列的最大长度。
六.ServerBoostrap的childOption()方法
childOption()方法可以给每一个连接都设置一些TCP参数,ChannelOption.SO_KEEPALIVE表示是否开启TCP底层的心跳机制,ChannelOption.TCP_NODELAY表示是否开启Nagle算法。如果要求实时性则可以关闭Nagle算法,如果要求减少发送次数和网络交互次数则可以开启Nagle算法。
七.Boostrap的connect()方法
connect()方法是一个异步方法,调用之后会立即返回一个ChannelFuture,通过ChannelFuture的addListener()方法可以添加监听器监听连接是否建立成功。
八.Boostrap的attr()方法
attr()方法可以给客户端Channel也就是NioSocketChannel指定自定义属性。
九.Boostrap的option()方法
option()方法可以给客户端Channel也就是NioSocketChannel设置TCP参数。
6.Netty服务端和客户端使用总结
一.Netty服务端的启动流程
首先创建一个服务端的启动引导类ServerBootstrap,然后给它指定线程模型、IO模型、TCP参数、业务处理逻辑,最后通过它的bind()方法绑定端口后,服务端就启动起来了。
二.Netty客户端的启动流程
首先创建一个客户端的启动引导类Bootstrap,然后给它指定线程模型、IO模型、TCP参数、业务处理逻辑,最后通过它的connect()方法连接上服务端后,客户端就启动起来了。
三.bind()方法和connect()方法
bind()方法和connect()方法都是异步的,这两方法调用后都会立即返回一个ChannelFuture,都可以通过ChannelFuture的addListener()方法添加监听器监听端口是否绑定成功以及连接是否建立成功。
四.Netty的基本组件与BIO的对应
NIO的三大组件是:Buffer、Channel、Selector。NioEventLoop对应于BIO的线程(监听客户端连接 + 处理客户端连接的读写)。Channel对应于BIO的Socket,ByteBuf对应于BIO的IO Bytes,Pipeline对应于BIO的逻辑处理链,ChannelHandler对应于BIO的逻辑处理块。
7.什么是TCP粘包拆包
TCP协议是一个流协议。所谓流,就是没有界限的一串数据。就像河里的流水,它们是连成一片的,其中并没有分界线。
TCP协议的底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以在业务上认为,一个完整的包可能会被拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包进行发送。这就是TCP粘包和拆包问题。
8.TCP粘包拆包的几种情况
假设客户端发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能有如下情况:
情况一:服务端分两次读取到了两个独立的数据包
也就是数据包D1和D2是分开的,没有出现粘包和拆包的情况。
情况二:服务端一次接收到了两个数据包
也就是数据包D1和D2粘在一起了,出现了TCP粘包的情况。
情况三:服务端分两次读取到了两个数据包
第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,出现了TCP拆包的情况。
情况四:服务端分两次读取到了两个数据包
第一次读取到了D1包的部分内容,第二次读取到了D1包的剩余内容和完整的D2包,出现了TCP拆包的情况。
情况五:服务端分多次读取到两个数据包
如果服务端的TCP接收滑窗非常小,而数据包D1和D2比较大,那么服务端可能要分多次才能将D1和D2包接收完,期间发生了多次拆包。
9.TCP粘包拆包的原因
(1)粘包问题的原因
由于TCP协议是基于三次握手的可靠的传输协议,会让客户端与服务端维持一个连接。所以在连接不断开的情况下,客户端能够持续不断地将多个数据包发往服务端。
但如果发送的数据包太小,则会因Nagle算法而对较小的数据包进行合并再发送。开启Nagle算法会让TCP的网络延迟高一些,当然可以设置关闭Nagle算法。这样服务端在收到数据包时就无法区分哪些数据包是独立的,从而产生粘包问题。
服务端在收到数据包后会将其放到缓冲区中,如果数据包没有及时从缓存区取走,那么下次获取数据包时就可能会出现一次取出多个数据包的情况,从而产生粘包问题。
由于UDP协议本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),所以不会对数据包进行合并发送(也就不会使用Nagle算法)。每一个数据包都是独立和完整的,但服务端接收的缓冲区大小也会影响是否出现粘包问题。
(2)半包问题的原因
产生半包问题的核心原因就是一个数据包被分成了多次接收,比如:
一.可能是IP分片传输导致的
二.可能是传输过程中丢失部分包导致出现半包
三.可能是一个包被分成了两次传输,在获取数据时先取到一部分
四.可能与接收的缓冲区大小有关系,在获取数据时先取到一部分
半包问题更具体的原因有三个,分别如下:
一.应用程序写入数据的字节大小大于Socket套接字发送缓冲区的大小
二.进行MSS大小的TCP分段
MSS是最大报文段长度的缩写,MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个TCP报文段,所以MSS并不是TCP报文段的最大长度,而是TCP报文段长度减去TCP首部长度。
三.以太网的payload大于MTU进行IP分片
MTU指一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上。
10.粘包问题的解决策略
由于TCP协议的底层无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。
策略一:消息定长
例如每个报文的大小为固定长度200字节,如果不够,空位补空格。FixedLengthFrameDecoder
策略二:加分割符
在包尾增加回车换行符等特殊分隔符进行分割,如FTP协议。LineBasedFrameDecoder、DelimiterBasedFrameDecoder
策略三:消息带上长度字段
比如将消息分为消息头和消息体,消息头中包含消息的总长度。LengthFieldBasedFrameDecoder
11.拆包的原理
拆包的基本原理就是不断从TCP缓冲区中读取数据,每次读取完都要判断是否是一个完整的数据包。
如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从TCP缓冲区中读取,直到得到一个完整的数据包。
如果当前读取的数据加上已读取的数据足以拼接成一个完整的业务数据包,那就将已读取的数据拼接上当前读取的数据,构成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下一次读取的数据进行拼接。
12.粘包问题演示
客户端和服务端链路建立成功后,就触发执行channelActive()方法循环发送100条消息。每发送一条消息就通过writeAndFlush()方法刷新一次,保证每条消息都被写入Channel。
期望服务端应该会收到100条消息,但实际上只收到了两条消息。一条包含"57条消息",另一条包含"43条消息"。
public class NettyClientHandler extends ChannelInboundHandlerAdapter {public void channelActive(ChannelHandlerContext ctx) {for (int i = 0; i < 100; i++) {ByteBuf buffer = ctx.alloc().buffer();buffer.writeBytes("Hello".getBytes());ctx.channel().writeAndFlush(buffer);}}
}public class NettyServerHandler extends ChannelInboundHandlerAdapter {public void channelRead(ChannelHandlerContext ctx, Object msg) {ByteBuf byteBuf = (ByteBuf) msg;System.out.println("服务端读到数据" + byteBuf.toString());}
}
13.换行符解码器LineBasedFrameDecoder
LineBasedFrameDecoder也可以称为行拆包器。从字面意思看,发送端发送数据包时,每个数据包之间以换行符作为分隔,接收端通过这个行拆包器将粘过的ByteBuf拆分成一个个完整的应用层数据包。
它的工作原理是依次遍历ByteBuf中的可读字节,然后判断是否有"\n"或者"\r\n"。如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。
它是以换行符为结束标志的解码器,支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,那么就会抛出异常,并忽略之前读取到的异常码流。
//添加换行符解码器
//在绝大多数情况下,这些解码器会添加到对应ChannelPipeline的起始位置
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));//响应消息要拼接换行符
ByteBuf resp = Unpooled.copiedBuffer(("响应内容" + System.getProperty("line.separator")).getBytes());
ctx.writeAndFlush(resp);
14.分隔符解码器DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder也可以称为分隔符拆包器,它是行拆包器的通用版本,可以传递两个参数,可以自定义分隔符。
第一个参数1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符则抛异常。第二个参数就是分隔符ByteBuf对象。
//添加分隔符解码器
//在绝大多数情况下,这些解码器会添加到对应ChannelPipeline的起始位置
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));//响应消息要拼接分隔符
ByteBuf resp = Unpooled.copiedBuffer(("响应内容" + "$_").getBytes());
ctx.writeAndFlush(resp);
15.固定长度解码器FixedLengthFrameDecoder
FixedLengthFrameDecoder也可以称为固定长度拆包器。如果应用层协议非常简单,每个数据包的长度都是固定的,比如100。那么只需要把这个固定长度解码器添加到Pipeline中,Netty就会把一个个长度为100的数据包(ByteBuf)传递到下一个ChannelHandler。
因为利用固定长度解码器,无论一次接收多少数据,都会按照构造函数中设置的固定长度进行解码。如果是半包消息,固定长度解码器会缓存半包消息并等下一个数据包到达后再进行拼包处理。
//添加固定长度解码器
//在绝大多数情况下,这些解码器会添加到对应ChannelPipeline的起始位置
ch.pipeline().addLast(new FixedLengthFrameDecoder(200));
16.基于长度域解码器LengthFieldBasedDecoder
LengthFieldBasedDecoder也可以称为基于长度域的拆包器,这种基于长度域的拆包器是最通用的一种拆包器。只要我们的自定义协议中包含长度域字段,均可以使用这个解码器来实现应用层拆包。
假如一个自定义协议的数据结构如下,那么长度域的偏移量就是4 + 1 + 1 + 1 = 7。

于是就可以根据长度域偏移量和长度域的长度来构造一个解码器,其中第一个参数是数据包的最大长度,第二个参数是长度域的偏移量,第三个参数是长度域的长度。
//添加基于长度域的解码器
//在绝大多数情况下,这些解码器会添加到对应ChannelPipeline的起始位置
ch.pipeline().addLast(new LengthFieldBasedDecoder(Integer.MAX_VALUE, 7, 4));
注意:设计协议时通常会在数据包的开头加上一个魔数,以便可以尽早屏蔽非本协议的客户端。通常会在第一个Handler对每个客户端发过来的数据包做一次快速判断,看是否满足自定义协议。比如可以继承LengthFieldBasedDecoder的decode()方法,然后在decode()之前判断前4字节是否等于定义的魔数。
17.Java序列化的缺点
缺点一:无法跨语言
缺点二:序列化的码流太大
缺点三:序列化性能太差
所以一般使用Protobuf、Hessian、Kyro等工具进行序列化。
18.Netty基本组件与BIO的对应
Channel -> BIO的Socket
ByteBuf -> BIO的IO Bytes
Pipeline -> BIO的逻辑处理链
ChannelHandler -> BIO的逻辑处理块
NioEventLoop -> BIO的线程(监听客户端连接 + 处理客户端连接的读写)
相关文章:
Netty基础—5.Netty的使用简介
大纲 1.Netty服务端的启动流程 2.服务端IO事件的处理类 3.Netty客户端的启动流程 4.客户端IO事件的处理类 5.启动Netty服务端和客户端的方法说明 6.Netty服务端和客户端使用总结 7.什么是TCP粘包拆包 8.TCP粘包拆包的几种情况 9.TCP粘包拆包的原因 10.粘包问题的解决…...
C++初阶——类和对象(一)
C初阶——类和对象(一) 一、面向过程和面向对象 1.面向过程 面向过程的程序设计(Procedure-Oriented Programming),简称POP,是一种是以程序执行流程为核心的编程范式。它是先分析出解决问题所需要的的步…...
1141. 【贪心算法】排队打水
题目描述 有n(n<1000)个人在一个水龙头前排队接水,假如每个人接水的时间为Ti, 请编程找出这n个人排队的一种顺序,使得n个人的平均等待时间最小。输入 输入文件共两行,第一行为n; 第二行分别…...
RabbitMQ入门:从安装到高级消息模式
文章目录 一. RabbitMQ概述1.1 同步/异步1.1.1 同步调用1.1.2 异步调用 1.2 消息中间件1.2.1 概念1.2.2 作用1.2.3 常见的消息中间件1.2.4 其他中间件 1.3 RabbitMQ1.3.1 简介1.3.2 特点1.3.3 方式1.3.4 架构1.3.5 运行流程 二. 安装2.1 Docker 安装 RabbitMQ 三. 简单队列&…...
Linux应用:进程的回收
进程的诞生和消亡 程的诞生通常是通过系统调用(如fork、exec等)来创建新进程。当一个进程完成其任务或者出现错误时,它会进入消亡阶段。进程可以通过exit函数主动结束自身,也可能由于操作系统的调度策略(如资源耗尽、…...
如何利用 AI 技术快速定位和修复生产环境问题
网罗开发 (小红书、快手、视频号同名) 大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等…...
Linux find 命令完全指南
find 是 Linux 系统最强大的文件搜索工具,支持 嵌套遍历、条件筛选、执行动作。以下通过场景分类解析核心用法,涵盖高效搜索、文件管理及高级技巧: 一、基础搜索模式 1. 按文件名搜索(精确/模糊匹配) <BASH> f…...
市场波动中的风险管理与策略优化
市场波动中的风险管理与策略优化 在市场交易中,价格的波动性为投资者提供了交易机会,但同时也带来了风险。如何在市场不确定性中进行有效的风险管理,并优化交易策略,是每位交易者都需要思考的问题。本文将探讨市场波动的影响因素、…...
(链表)206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,2,1]示例 2: 输入:head [1,2] 输出:[2,1]示例 3: 输入&am…...
Jetson Orin NX jupyter lab的安装和使用
主要是为了梳理一下整个过程,其实步骤很简单,但容易出错。 注意,实际只有两个文件需要写入,一个是jupyter_lab_config.py,一个是jupyter.service。 配置文件的名字要写对,如果总是copy网上的代码࿰…...
前端npm包- CropperJS
文章目录 一、CropperJS**核心特性****官网与文档****安装与使用**1. **通过 npm/yarn/pnpm 安装**2. **HTML 结构**3. **引入 CSS 和 JS**4. **初始化裁剪器** **相关插件/替代方案****适用场景****注意事项** 总结 一、CropperJS cropperjs 是一个轻量级、功能强大的 图片裁…...
农业建设项目管理系统评测:8款推荐工具优缺点分析
本文主要介绍了以下8款农业建设项目管理系统:1.PingCode; 2. Worktile ;3. 建米农业工程项目管理系统;4. 开创云数字农业管理平台; 5. Trimble Ag Software;6.Conservis; 7. Agworld ࿱…...
linux 命令 tail
tail 是 Linux 中用于查看文件末尾内容的命令,常用于日志监控和大文件快速浏览。以下是其核心用法及常见选项: 基本语法 tail [选项] 文件名 常用选项 显示末尾行数 -n <行数> 或 --lines<行数> 指定显示文件的最后若干行(…...
测试开发 - 正浩创新 - 一面面经(已OC)
自我介绍 实习过程中,有遇到过什么问题,是如何解决的 实习成果中的数据指标变化,人力消耗一直在递减,是什么原因 实习工作有很多模块,那一块工作对你的提升或者收获是比较大的 讲一下,简历中所罗列的几…...
实验8 搜索技术
实验8 搜索技术 一、实验目的 (1)掌握搜索技术的相关理论,能根据实际情况选取合适的搜索方法; (2)进一步熟悉盲目搜索技术,掌握其在搜索过程中的优缺点; (3)…...
VSTO(C#)Excel开发9:处理格式和字体
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...
LinkedList底层结构和源码分析(JDK1.8)
参考视频:韩顺平Java集合 特点 LinkedList 底层实现了 双向链表 和 双端队列 的特点。可以添加任意元素(元素可以重复),包括 null。线程不安全,没有实现同步。 LinkedList 底层结构 LinkedList 底层维护了一个双向链…...
数字内容体验的技术支柱是什么?
数据分析引擎构建基础 数字内容体验的技术底座始于对海量用户行为数据的深度解析。作为技术体系的根基,数据分析引擎通过实时采集、清洗与结构化处理,将分散的点击轨迹、停留时长及交互偏好转化为可操作的洞察。其核心能力体现在三方面:一是…...
C# 使用Markdown2Pdf把md文件转换为pdf文件
NuGet安装Markdown2Pdf库,可以把格式简单markdown文件转换为pdf。但该库用了Puppeteer Sharp,因此会在运行过程中提示指定Chrome浏览器路径或自动下载Chrome浏览器。 代码如下: using Markdown2Pdf;var converter new Markdown2PdfConverte…...
专家系统如何运用谓词逻辑进行更复杂的推理
前文,我们讲解了命题逻辑和谓词逻辑的基本概念、推理规则、应用以及一些简单的示例。具体内容可以先看我的文章:人工智能的数学基础之命题逻辑与谓词逻辑(含示例)-CSDN博客 那么形如专家系统这类复杂系统,是如何通过谓…...
html css网页制作成品——糖果屋网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
Ubuntu上部署Flask+MySQL项目
一、服务器安装python环境 1、安装gcc(Ubuntu默认已安装) 2、安装python源码 wget https://www.python.org/ftp/python/3.13.2/Python-3.13.2.tar.xz 3、安装Python依赖库 4、配置python豆瓣源 二、服务器安装虚拟环境 1、安装virtualenv pip3.10 ins…...
落雪音乐Pro 8.8.6 | 内置8条音源,无需手动导入,纯净无广告
洛雪音乐Pro版内置多组稳定音源接口,省去手动导入的繁琐操作,安装即可畅听海量音乐。延续原版无广告的纯净体验,支持歌单推荐与音源切换,满足个性化听歌需求。此版本仅支持在线播放,无法下载音乐,且与原版不…...
什么是全栈?
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点下班 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 📃文章前言 🔷文章均为学习工…...
一些docker命令
一、基础命令 查看 Docker 版本 docker --version 或 docker version:显示 Docker 客户端和服务器的版本信息。 查看 Docker 系统信息 docker info:显示 Docker 系统的详细信息,包括镜像、容器数量、存储驱动类型等。 Docker 服务管理 s…...
《DeepSeek 开源 DeepGEMM:开启AI计算新时代的密钥》:此文为AI自动生成
《DeepSeek 开源 DeepGEMM:开启AI计算新时代的密钥》:此文为AI自动生成 引言:AI 计算的新曙光 在当今科技飞速发展的时代,人工智能(AI)无疑是最为耀眼的领域之一。从语音助手到自动驾驶,从图像…...
OpenCV实现图像特征提取与匹配
一、特征检测与描述子提取 选择特征检测器 常用算法包括: ORB:一种高效的替代SIFT和SURF的算法,主要用于移动机器人和增强现实等领域。适合实时应用,结合FAST关键点与BRIEF描述子。SIFT(尺度不变特征变…...
将分支`XXX`合并到远程分支`master
将分支feat-task合并到远程分支master 首先,切换到本地的 master 分支 git checkout master确保你的本地 master 分支是最新的,拉取远程的更新 git pull origin master将 feat-task 分支的代码合并到 master 分支 git merge feat-task如果在合并过程…...
程序化广告行业(13/89):DSP的深入解析与运营要点
程序化广告行业(13/89):DSP的深入解析与运营要点 大家好!一直以来,我都对程序化广告行业保持着浓厚的学习兴趣,在探索的过程中积累了不少心得。今天就想把这些知识分享出来,和大家一起学习进步…...
XML文件格式的简介及如何用Python3处理XML格式对象
诸神缄默不语-个人技术博文与视频目录 文章目录 1. XML格式简介2. 格式化XML文件的工具3. Python处理XML:xml库1. xml.etree.\(c\)ElementTree2. xml.dom.minidom 4. 本文撰写过程中参考的其他网络资料 1. XML格式简介 可扩展标记语言 (Extensible Markup Language…...
