当前位置: 首页 > news >正文

Netty Review - 优化Netty通信:如何应对粘包和拆包挑战

文章目录

  • 概述
  • Pre
  • 概述
  • 场景复现
  • 解决办法概览
    • 方式一: 特殊分隔符分包 (演示Netty提供的众多方案中的一种)
      • 流程分析
    • 方式二: 发送长度(推荐)
  • DelimiterBasedFrameDecoder 源码分析

在这里插入图片描述


概述

在这里插入图片描述


Pre

Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力


概述

粘包和拆包是在计算机网络通信中常见的问题,特别是在使用基于流的传输协议(如TCP)时。这两个问题涉及到数据在传输过程中的组织和解析。

  1. 粘包(Packet Concatenation):

    • 定义: 粘包指的是发送方发送的多个小数据包在接收方看来被组合成一个大的数据包。
    • 原因: 发送方连续发送的数据可能在网络中被合并成一个数据流,导致接收方无法准确分辨每个数据包的边界。
    • 可能的解决方案: 使用特殊的分隔符标记数据包的边界,或者在数据包中包含长度信息。
  2. 拆包(Packet Fragmentation):

    • 定义: 拆包是指接收方接收到的数据包过大,被拆分成多个较小的数据包。
    • 原因: 数据包在传输过程中可能被分割,到达接收方时需要重新组装。
    • 可能的解决方案: 在数据包中包含长度信息,或者使用特殊的标记表示数据包的边界。

在处理粘包和拆包问题时,通信双方需要协调一致,以确保数据的正确性和完整性。使用合适的协议和通信模式,以及采用适当的分隔符或长度字段,有助于减轻或解决这些问题。


TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区
的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成
一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的。

如下图所示,client发了两个数据包D1和D2,但是server端可能会收到如下几种情况的数据。

在这里插入图片描述
比如


正常情况:
在这里插入图片描述


发生了粘包:
在这里插入图片描述


发生了拆包:
在这里插入图片描述
或者
在这里插入图片描述


场景复现

我们的代码还是以 Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力中的代码为基础,演示一下粘包拆包

在这里插入图片描述


启动Server 和 Client ()

【TalkRoomClient2】发送10条消息

package com.artisan.pack;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class TalkRoomClient2 {public static void main(String[] args) throws InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// 分隔符解码器 (用于测试 按照 _ 分隔符 拆包)//pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer("_".getBytes())));//向pipeline加入解码器pipeline.addLast("decoder", new StringDecoder());//向pipeline加入编码器pipeline.addLast("encoder", new StringEncoder());//加入自己的业务处理handlerpipeline.addLast(new TalkRoomClientHandler());}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 1234).sync();//得到 channelChannel channel = channelFuture.channel();System.out.println("========" + channel.localAddress() + "========");// 模拟 拆包粘包for (int i = 0; i < 10; i++) {channel.writeAndFlush("小工匠123");}// 阻塞操作,closeFuture()开启了一个channel的监听器(这期间channel在进行各项工作),直到链路断开channelFuture.channel().closeFuture().sync();} finally {group.shutdownGracefully();}}}

【TalkRoomClient】接收 Client2 ---- Server — 自己的消息

package com.artisan.pack;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class TalkRoomClient {public static void main(String[] args) throws InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// 分隔符解码器 (用于测试 按照 _ 分隔符 拆包)//pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer("_".getBytes())));//向pipeline加入解码器pipeline.addLast("decoder", new StringDecoder());//向pipeline加入编码器pipeline.addLast("encoder", new StringEncoder());//加入自己的业务处理handlerpipeline.addLast(new TalkRoomClientHandler());}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 1234).sync();//得到 channelChannel channel = channelFuture.channel();System.out.println("========" + channel.localAddress() + "========");//客户端需要输入信息, 创建一个扫描器Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()) {String msg = scanner.nextLine();//通过 channel 发送到服务器端channel.writeAndFlush(msg);}// 阻塞操作,closeFuture()开启了一个channel的监听器(这期间channel在进行各项工作),直到链路断开channelFuture.channel().closeFuture().sync();} finally {group.shutdownGracefully();}}}

【测试】

在这里插入图片描述

出现了粘包和拆包的现象

在这里插入图片描述


解决办法概览

1)消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格
2)在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不
能出现分隔符。
3)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度
来判断每条数据的开始和结束。 (推荐方案)

Netty提供了多个解码器,可以进行分包的操作,如下:

  • LineBasedFrameDecoder (回车换行分包)
  • DelimiterBasedFrameDecoder(特殊分隔符分包)
  • FixedLengthFrameDecoder(固定长度报文来分包)

我们先使用第二种方案来描述一下

方式一: 特殊分隔符分包 (演示Netty提供的众多方案中的一种)

我们来看下如何改造?

【TalkRoomServer 】

重点关注的地方是DelimiterBasedFrameDecoder,这是一个基于分隔符的帧解码器,用于处理客户端发送的按照特定分隔符(在这里是下划线_)分割的数据包。

package com.artisan.pack;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class TalkRoomServer {public static void main(String[] args) throws InterruptedException {// 创建主事件循环组,用于接受进来的连接EventLoopGroup bossGroup = new NioEventLoopGroup(1);// 创建工作事件循环组,用于处理已接受连接的IO操作EventLoopGroup workerGroup = new NioEventLoopGroup(8);try {ServerBootstrap bootstrap = new ServerBootstrap();// 配置服务器bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel接受进来的连接.option(ChannelOption.SO_BACKLOG, 1024) // 设置连接队列大小.childHandler(new ChannelInitializer<SocketChannel>() { // 配置子通道初始化器@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// 分隔符解码器,按照下划线拆包pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(Delimiter.SPLIT.getBytes())));// 添加字符串解码器pipeline.addLast("decoder", new StringDecoder());// 添加字符串编码器pipeline.addLast("encoder", new StringEncoder());// 添加自定义的业务处理handlerpipeline.addLast(new TalkRoomServerHandler());}});// 绑定端口并同步等待成功,然后返回ChannelFuture对象ChannelFuture channelFuture = bootstrap.bind(1234).sync();// 打印服务器启动成功信息System.out.println("Talk Room Server启动成功,监听1234端口");// 等待服务器socket关闭channelFuture.channel().closeFuture().sync();} finally {// 释放资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

initChannel方法中,DelimiterBasedFrameDecoder被加入到管道中。它用于接收按分隔符(这里是下划线_)分割的数据包,并把这些数据包转换成一个个的Frame对象,这样就可以在后续的处理器中逐个处理这些数据包了。这种方式的优点是可以有效处理大量且不定长度的数据包,而不需要担心数据包过大导致内存溢出的问题。


【TalkRoomServerHandler】

因为我们队数据进行了加工转发,所以加工后的消息,也得按照DelimiterBasedFrameDecoder的处理规则增加 “_”

在这里插入图片描述


同样的Client的Pipeline中别忘了增加解码器

在这里插入图片描述


启动Server和Client ,我们来测试下

在这里插入图片描述


附上其他的代码

package com.artisan.pack;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class TalkRoomClient2 {public static void main(String[] args) throws InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// 分隔符解码器 (用于测试 按照 _ 分隔符 拆包)pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer(Delimiter.SPLIT.getBytes())));//向pipeline加入解码器pipeline.addLast("decoder", new StringDecoder());//向pipeline加入编码器pipeline.addLast("encoder", new StringEncoder());//加入自己的业务处理handlerpipeline.addLast(new TalkRoomClientHandler());}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 1234).sync();//得到 channelChannel channel = channelFuture.channel();System.out.println("========" + channel.localAddress() + "========");// 模拟 拆包粘包for (int i = 0; i < 10; i++) {channel.writeAndFlush("小工匠123" + Delimiter.SPLIT);}} finally {group.shutdownGracefully();}}}
package com.artisan.pack;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;import java.util.Scanner;/*** @author 小工匠* @version 1.0* @mark: show me the code , change the world*/
public class TalkRoomClient {public static void main(String[] args) throws InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// 分隔符解码器 (用于测试 按照 _ 分隔符 拆包)pipeline.addLast(new DelimiterBasedFrameDecoder(10240, Unpooled.copiedBuffer(Delimiter.SPLIT.getBytes())));//向pipeline加入解码器pipeline.addLast("decoder", new StringDecoder());//向pipeline加入编码器pipeline.addLast("encoder", new StringEncoder());//加入自己的业务处理handlerpipeline.addLast(new TalkRoomClientHandler());}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 1234).sync();//得到 channelChannel channel = channelFuture.channel();System.out.println("========" + channel.localAddress() + "========");//客户端需要输入信息, 创建一个扫描器Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()) {String msg = scanner.nextLine();//通过 channel 发送到服务器端channel.writeAndFlush(msg);}// 阻塞操作,closeFuture()开启了一个channel的监听器(这期间channel在进行各项工作),直到链路断开channelFuture.channel().closeFuture().sync();} finally {group.shutdownGracefully();}}}
package com.artisan.pack;/*** @author artisan*/
public interface Delimiter {String  SPLIT = "_" ;
}

流程分析

在这里插入图片描述


方式二: 发送长度(推荐)

TODO


DelimiterBasedFrameDecoder 源码分析

在这里插入图片描述

A decoder that splits the received ByteBufs by one or more delimiters. It is particularly useful for decoding the frames which ends with a delimiter such as NUL or newline characters.
Predefined delimiters
Delimiters defines frequently used delimiters for convenience' sake.
Specifying more than one delimiter
DelimiterBasedFrameDecoder allows you to specify more than one delimiter. If more than one delimiter is found in the buffer, it chooses the delimiter which produces the shortest frame. For example, if you have the following data in the buffer:+--------------+| ABC\nDEF\r\n |+--------------+a DelimiterBasedFrameDecoder(Delimiters.lineDelimiter()) will choose '\n' as the first delimiter and produce two frames:+-----+-----+| ABC | DEF |+-----+-----+rather than incorrectly choosing '\r\n' as the first delimiter:+----------+| ABC\nDEF |+----------+
    /*** 从{@link ByteBuf}中创建一个帧并返回。** @param   ctx             此{@link ByteToMessageDecoder}所属的{@link ChannelHandlerContext}* @param   buffer          要从中读取数据的{@link ByteBuf}* @return  frame           表示帧的{@link ByteBuf},如果没有帧可创建,则返回{@code null}*/protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {if (lineBasedDecoder != null) {// 如果设置了行解码器,则使用行解码器进行解码return lineBasedDecoder.decode(ctx, buffer);}// 尝试所有的分隔符,并选择产生最短帧的分隔符int minFrameLength = Integer.MAX_VALUE;ByteBuf minDelim = null;for (ByteBuf delim: delimiters) {int frameLength = indexOf(buffer, delim);if (frameLength >= 0 && frameLength < minFrameLength) {minFrameLength = frameLength;minDelim = delim;}}if (minDelim != null) {int minDelimLength = minDelim.capacity();ByteBuf frame;if (discardingTooLongFrame) {// 刚刚丢弃了一个非常大的帧// 回到初始状态discardingTooLongFrame = false;buffer.skipBytes(minFrameLength + minDelimLength);int tooLongFrameLength = this.tooLongFrameLength;this.tooLongFrameLength = 0;if (!failFast) {fail(tooLongFrameLength);}return null;}if (minFrameLength > maxFrameLength) {// 丢弃读取的帧buffer.skipBytes(minFrameLength + minDelimLength);fail(minFrameLength);return null;}if (stripDelimiter) {// 如果需要去除分隔符,则从buffer中读取帧frame = buffer.readRetainedSlice(minFrameLength);buffer.skipBytes(minDelimLength);} else {// 否则,直接从buffer中读取包含分隔符的帧frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);}return frame;} else {if (!discardingTooLongFrame) {if (buffer.readableBytes() > maxFrameLength) {// 丢弃buffer中的内容,直到找到分隔符tooLongFrameLength = buffer.readableBytes();buffer.skipBytes(buffer.readableBytes());discardingTooLongFrame = true;if (failFast) {fail(tooLongFrameLength);}}} else {// 由于没有找到分隔符,仍在丢弃buffertooLongFrameLength += buffer.readableBytes();buffer.skipBytes(buffer.readableBytes());}return null;}}

这段代码是DelimiterBasedFrameDecoder类的decode方法的实现。这个方法的主要作用是根据指定的分隔符将输入的ByteBuf对象中的数据分割成一个个的帧。

首先,我们来看一下方法的定义:

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {// ...
}

decode方法接收两个参数:

  1. ctx:解码器所在的ChannelHandlerContext对象。
  2. buffer:待解码的ByteBuf对象。

接下来,我们逐行解析代码并添加中文注释:

if (lineBasedDecoder != null) {return lineBasedDecoder.decode(ctx, buffer);
}

如果存在行基于的解码器,则使用该解码器进行解码。

int minFrameLength = Integer.MAX_VALUE;
ByteBuf minDelim = null;
for (ByteBuf delim: delimiters) {int frameLength = indexOf(buffer, delim);if (frameLength >= 0 && frameLength < minFrameLength) {minFrameLength = frameLength;minDelim = delim;}
}

遍历所有的分隔符,并找到能产生最短帧的分隔符。

if (minDelim != null) {// ...
} else {// ...
}

如果找到了分隔符,则根据分隔符分割数据;如果没有找到分隔符,则跳过超过最大帧长度的数据。

if (discardingTooLongFrame) {// ...
} else {// ...
}

如果正在丢弃过长的帧,则回到初始状态;否则,检查当前帧长度是否超过最大帧长度。

if (stripDelimiter) {frame = buffer.readRetainedSlice(minFrameLength);buffer.skipBytes(minDelimLength);
} else {frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
}

根据stripDelimiter的值来确定是否需要去除分隔符。

return frame;

返回分割后的帧。

if (!discardingTooLongFrame) {// ...
} else {// ...
}

如果不在丢弃过长的帧,则检查缓冲区中可读字节数是否超过最大帧长度;否则,继续丢弃缓冲区中的数据。

tooLongFrameLength += buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());

累加过长的帧长度,并跳过过长的数据。

return null;

如果没有找到分隔符,则返回null

通过以上代码,DelimiterBasedFrameDecoder可以根据指定的分隔符将输入的ByteBuf对象中的数据分割成一个个的帧。这样,就可以在后续的处理器中逐个处理这些帧了。

在这里插入图片描述

相关文章:

Netty Review - 优化Netty通信:如何应对粘包和拆包挑战

文章目录 概述Pre概述场景复现解决办法概览方式一&#xff1a; 特殊分隔符分包 &#xff08;演示Netty提供的众多方案中的一种&#xff09;流程分析 方式二&#xff1a; 发送长度(推荐) DelimiterBasedFrameDecoder 源码分析 概述 Pre Netty Review - 借助SimpleTalkRoom初体验…...

vue介绍以及基本指令

目录 一、vue是什么 二、使用vue的准备工作 三、创建vue项目 四、vue插值表达式 五、vue基本指令 六、key的作用 七、v-model 九、指令修饰符 一、vue是什么 Vue是一种用于构建用户界面的JavaScript框架。它可以帮助开发人员构建单页应用程序和复杂的前端应用程序。Vue…...

重塑数字生产力体系,生成式AI将开启云计算未来新十年?

科技云报道原创。 今天我们正身处一个历史的洪流&#xff0c;一个巨变的十字路口。生成式AI让人工智能技术完全破圈&#xff0c;带来了机器学习被大规模采用的历史转折点。 它掀起的新一轮科技革命&#xff0c;远超出我们今天的想象&#xff0c;这意味着一个巨大的历史机遇正…...

JFreeChart 生成图表,并为图表标注特殊点、添加文本标识框

一、项目场景&#xff1a; Java使用JFreeChart库生成图片&#xff0c;主要场景为将具体的数据 可视化 生成曲线图等的图表。 本篇文章主要针对为数据集生成的图表添加特殊点及其标识框。具体包括两种场景&#xff1a;x轴为 时间戳 类型和普通 数值 类型。&#xff08;y轴都为…...

vue整合axios 未完

一、简介 1、介绍 axios前端异步请求库类似jouery ajax技术&#xff0c;axios用来在前端页面发起一个异步请求&#xff0c;请求之后页面不动&#xff0c;响应回来刷新页面局部&#xff1b;Axios 是一个基于 promise 的 HTTP 库&#xff0c;可以用在浏览器和 node.js 中 2、特…...

java代码编写twitter授权登录

在上一篇内容已经介绍了怎么申请twitter开放的API接口。 下面介绍怎么通过twitter提供的API&#xff0c;进行授权登录功能。 开发者页面设置 首先在开发者页面开启“用户认证设置”&#xff0c;点击edit进行信息编辑。 我的授权登录是个网页&#xff0c;并且只需要进行简单的…...

​ SK Ecoplant借助亚马逊云科技,海外服务器为环保事业注入新活力

在当今全球面临着资源紧缺和环境挑战的大背景下&#xff0c;数字技术所依赖的海外服务器正成为加速循环经济转型的关键利器。然而&#xff0c;很多企业在整合数字技术到运营中仍然面临着一系列挑战&#xff0c;依然存在低效流程导致的不必要浪费。针对这一问题&#xff0c;SK E…...

RPC(5):AJAX跨域请求处理

接上一篇RPC&#xff08;4&#xff09;&#xff1a;HttpClient实现RPC之POST请求进行修改。 1 修改客户端项目 1.1 修改maven文件 修改后配置文件如下&#xff1a; <dependencyManagement><dependencies><dependency><groupId>org.springframework.b…...

用大白话举例子讲明白区块链

什么是区块链&#xff1f;网上这么说&#xff1a; 区块链是一种分布式数据库技术&#xff0c;它以块的形式记录和存储交易数据&#xff0c;并使用密码学算法保证数据的安全性和不可篡改性。每个块都包含了前一个块的哈希值和自身的交易数据&#xff0c;形成了一个不断增长的链条…...

Java URL

URL&#xff1a;统一资源定位符&#xff0c;说白了&#xff0c;就是一个网络 通过URLConnection类可以连接到URL&#xff0c;然后通过URLConnection可以获取读数据的通道。非文本数据用字节流来读取。 读完之后写入本地即可。 public class test {public static void main(S…...

ETL-从1学到100(1/100):ETL涉及到的名词解释

本文章主要介绍ETL和大数据中涉及到名词&#xff0c;同时解释这些名词的含义。由于不是一次性收集这些名词&#xff0c;所以这篇文章将会持续更新&#xff0c;更新日志会存放在本段话下面&#xff1a; 12-19更新&#xff1a;OLTP、OLAP、BI、ETL。 12-20更新&#xff1a;ELT、…...

Jenkins + gitlab 持续集成和持续部署的学习笔记

1. Jenkins 介绍 软件开发生命周期(SLDC, Software Development Life Cycle)&#xff1a;它集合了计划、开发、测试、部署的集合。 软件开发瀑布模型 软件的敏捷开发 1.1 持续集成 持续集成 (Continuous integration 简称 CI): 指的是频繁的将代码集成到主干。 持续集成的流…...

R语言【cli】——通过cli_abort用 cli 格式的内容显示错误、警告或信息,内部调用cli_bullets和inline-makeup

cli_abort(message,...,call .envir,.envir parent.frame(),.frame .envir ) 先从那些不需要下大力气理解的参数入手&#xff1a; 参数【.envir】&#xff1a;进行万能表达式编译的环境。 参数【.frame】&#xff1a;抛出上下文。默认用于参数【.trace_bottom】&#xff…...

cka从入门到放弃

无数次想放弃&#xff0c;最后选择了坚持 监控pod日志 监控名为 foobar 的 Pod 的日志&#xff0c;并过滤出具有 unable-access-website 信息的行&#xff0c;然后将 写入到 /opt/KUTR00101/foobar # 解析 监控pod的日志&#xff0c;使用kubectl logs pod-name kubectl logs…...

通过 jekyll 构建 github pages 博客实战笔记

jekyll 搭建教程 jekyll 搭建教程 Gem 安装 Ruby&#xff0c;请访问 下载地址。 Jekyll Jekyll 是一个简单且具备博客特性的静态网站生成器。 Jekyll 中文文档 极客学院中文文档 使用以下命令安装 Jekyll。 $ gem install jekyll在中国可能需要使用代理软件。然后&#xff…...

【AI美图】第09期效果图,AI人工智能汽车+摩托车系列图集

期待中的未来AI汽车 欢迎来到未来的世界&#xff0c;一个充满创新和无限可能的世界&#xff0c;这里有你从未见过的科技奇迹——AI汽车。 想象一下&#xff0c;你站在十字路口&#xff0c;繁忙的交通信号灯在你的视线中闪烁&#xff0c;汽车如潮水般涌来&#xff0c;但是&…...

网线的制作集线器交换机路由器的配置--含思维导图

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《产品经理如何画泳道图&流程图》 ⛺️ 越努力 &#xff0c;越幸运 一、网线的制作 1、网线的材料有哪些&#xff1f; 网线 网线是一种用于传输数据信号的电缆&#xff0c;广泛应…...

LLM微调(四)| 微调Llama 2实现Text-to-SQL,并使用LlamaIndex在数据库上进行推理

Llama 2是开源LLM发展的一个巨大里程碑。最大模型及其经过微调的变体位居Hugging Face Open LLM排行榜&#xff08;https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard&#xff09;前列。多个基准测试表明&#xff0c;就性能而言&#xff0c;它正在接近GPT-3.5…...

柔性数组(结构体成员)

目录 前言&#xff1a; 柔性数组&#xff1a; 给柔性数组分配空间&#xff1a; 调整柔性数组大小&#xff1a; 柔性数组的好处&#xff1a; 前言&#xff1a; 柔性数组&#xff1f;可能你从未听说&#xff0c;但是确实有这个概念。听名字&#xff0c;好像就是柔软的数…...

C#合并多个Word文档(微软官方免费openxml接口)

g /// <summary>/// 合并多个word文档&#xff08;合并到第一文件&#xff09;/// </summary>/// <param name"as_word_paths">word文档完整路径</param>/// <param name"breakNewPage">true(默认值)&#xff0c;合并下一个…...

MySQL 5.7依赖的软件包和下载地址

​​​​​​​yum install ncurses-devel openssl openssl-devel gcc gcc-c ncurses ncurses-devel bison make -y mysql下载地址 下载地址...

图论 | 网络流的基本概念

文章目录 流网路残留网络增广路径割最大流最小割定理最大流Edmonds-Karp 算法算法步骤程序代码时间复杂度 流网路 流网络&#xff1a; G ( V , E ) G (V, E) G(V,E) 有向图&#xff0c;不考虑反向边s&#xff1a;源点t&#xff1a;汇点 c ( u , v ) c(u, v) c(u,v)&#xff…...

【音视频 | AAC】AAC音频编码详解

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…...

redis基本用法学习(C#调用NRedisStack操作redis)

redis官网文档中推荐C#中使用NRedisStack包连接并操作redis&#xff0c;本文学习C#调用NRedisStack操作redis的基本方式。   新建Winform项目&#xff0c;在Nuget包管理器中搜索并安装NRedisStack包&#xff0c;如下图所示&#xff1a; 主要调用StackExchange.Redis命名空间下…...

[CVPR 2023:3D Gaussian Splatting:实时的神经场渲染]

文章目录 前言小结 原文地址&#xff1a;https://blog.csdn.net/qq_45752541/article/details/132854115 前言 mesh 和点是最常见的3D场景表示&#xff0c;因为它们是显式的&#xff0c;非常适合于快速的基于GPU/CUDA的栅格化。相比之下&#xff0c;最近的神经辐射场&#xf…...

【SpringBoot快速入门】(4)SpringBoot项目案例代码示例

目录 1 创建工程3 配置文件4 静态资源 之前我们已经学习的Spring、SpringMVC、Mabatis、Maven&#xff0c;详细讲解了Spring、SpringMVC、Mabatis整合SSM的方案和案例&#xff0c;上一节我们学习了SpringBoot的开发步骤、工程构建方法以及工程的快速启动&#xff0c;从这一节开…...

Linux服务器 部署飞书信息发送服务

项目介绍&#xff1a; 飞书信息发送服务是指将飞书信息发送服务部署到一个Linux服务器上。飞书是一款企业级的即时通讯和协作工具&#xff0c;支持发送消息给飞书的功能。通过部署飞书信息发送服务&#xff0c;可以方便内网发送信息给外网飞书。 项目代码结构展示&#xff1a; …...

用C#也能做机器学习?

前言✨ 说到机器学习&#xff0c;大家可能都不陌生&#xff0c;但是用C#来做机器学习&#xff0c;可能很多人还第一次听说。其实在C#中基于ML.NET也是可以做机器学习的&#xff0c;这种方式比较适合.NET程序员在项目中集成机器学习模型&#xff0c;不太适合专门学习机器学习&a…...

Python PDF格式转PPT格式

要将PDF文件转换为PPT&#xff0c;我实在python3.9 环境下转成功的&#xff0c;python3.11不行。 需要 pip install PyMuPDF代码说话 # -*- coding: utf-8 -*-""" author: 赫凯 software: PyCharm file: xxx.py time: 2023/12/21 11:20 """im…...

搭建知识付费平台?明理信息科技为你提供全程解决方案

明理信息科技saas知识付费平台 在当今数字化时代&#xff0c;知识付费已经成为一种趋势&#xff0c;越来越多的人愿意为有价值的知识付费。然而&#xff0c;公共知识付费平台虽然内容丰富&#xff0c;但难以满足个人或企业个性化的需求和品牌打造。同时&#xff0c;开发和维护…...

做自己的网站logo/重庆网络seo公司

我试图用控制台获得快速时间事件类型的交互&#xff0c;并设法使用conio库获取它。可悲的是&#xff0c;我正在开发的这个项目需要在Windows和Linux上编译代码&#xff0c;我无法找到一种方法来改变它。有没有办法在标准C中替换kbhit()和getch()函数&#xff1f;有什么我可以做…...

泉州网站关键词推广/学seo如何入门

Java之封装与访问权限控制(一)对于封装的概念&#xff0c;我总觉得自己还是挺了解的&#xff0c;但是真要我说&#xff0c;还真说不出个啥来。我只能默默地通过身边的例子加上书本理论完善我对封装的认识。就比如&#xff0c;我们在玩游戏的时候&#xff0c;我们只能通过完成指…...

杨和网站建设/百度账号怎么改用户名

本人ubuntu新人一个&#xff0c;最近想在ubuntu里编辑文本&#xff0c;但无奈系统自带LibreOffice比较坑爹&#xff0c;所以研究了一下怎么装WPS&#xff0c;遇到各种问题&#xff0c;最后成功解决&#xff0c;经验给大家分享一下。 1. wps官网下载wps for linux的deb安装包&am…...

wordpress插件的使用/谷歌推广费用

一、硬件材料 1*Arduino UNO R3开发板 1*超声波HCSR04传感器模块 1*HC-05蓝牙模块 1*5V电池 二、硬件接线图 CSDN 赤鱼科技...

wordpress小工具文件/南京网络营销服务

原生js实现上拉加载其实超级简单&#xff0c;把原理整明白了你也会&#xff0c;再也不用去引一个mescroll啦~ 好了&#xff0c;废话不多说&#xff0c;开始进入正题&#xff1a;上拉加载是怎么去做的&#xff0c;原理就是监听滚动条滑到页面底部&#xff0c;然后就去做一次请求…...

企业备案网站可以做论坛吗/百度下载安装官方下载

CodeIgniter 的错误处理1.CI在引导文件index.php中设置了“执行环境常量 EVIROMENT”&#xff0c;在值为“development”打开php的全部报错。2.在Common文件中&#xff0c;CI载入了Exception类&#xff0c;该类可以让用户使用show_error等函数主动输出错误。3.在Common文件&…...