Netty 进阶

news2024/11/24 4:27:36

粘包与半包

        粘包和半包问题的出现原因主要是因为 TCP 协议是面向流的,而不是面向报文的。即发送方给接收方传输的是一整个数据流,但是接收方并不知道数据流中的哪一部分才是一个完整的数据报,需要自行判断。

        如果是在发送方解决,通常采用的策略是在发送数据前将数据按照固定长度拆分成多个数据包,每个数据包附带特殊标记;如果发送变长数据,则在发送时加上数据的长度信息,接收方在接收到指定长度的数据后就可以认为是一个数据包。这样可以保证每个数据包都是完整的,从而避免了粘包和半包问题的出现。

示例代码

服务方

public class HelloWorldServer {
    static final Logger log = LoggerFactory.getLogger(HelloWorldServer.class);
    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected {}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect {}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            log.debug("{} binding...", channelFuture.channel());
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("stoped");
        }
    }

    public static void main(String[] args) {
        new HelloWorldServer().start();
    }
}

客户端

TODO

1.3 现象分析

粘包

  • 现象,发送 abc def,接收 abcdef

  • 原因

    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)

    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包

    • Nagle 算法:会造成粘包

半包

  • 现象,发送 abcdef,接收 abc def

  • 原因

    • 应用层:接收方 ByteBuf 小于实际发送数据量

    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包

    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

本质是因为 TCP 是流式协议,消息无边界

滑动窗口

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

 

窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用

  • 图中深色的部分即要发送的数据,高亮的部分即窗口

  • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动

  • 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动

  • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

Nagle 算法

  • 即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由

  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送

    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送

    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭

    • 如果 TCP_NODELAY = true,则需要发送

    • 已发送的数据都收到 ack 时,则需要发送

    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送

    • 除上述情况,延迟发送

1.4 解决方案

  • 短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低 (半包用这种办法还是不好解决,因为接收方的缓冲区大小是有限的 )
 ctx.writeAndFlush(buffer);
// 发完即关
ctx.close();
  • 每一条消息采用固定长度,缺点浪费空间

// 服务端优化, 此 Inbound 请加入到LoggingHandler 之前只能使用这一个解码器
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));


    
    // io.netty.channel.ChannelInboundHandlerAdapter#channelActive 执行的方法请使用此段
    private static void sendMessage1(ChannelHandlerContext ctx) {
        log.debug("Send sticky packet message");
        Random r = new Random();
        char c = 'a';
        ByteBuf buffer = ctx.alloc().buffer();
        for (int i = 0; i < 10; i++) {
            // 每次都分配8字节长度的数组
            byte[] bytes = new byte[8];
            for (int j = 0; j < r.nextInt(8); j++) {
                bytes[j] = (byte) c;
            }
            c++;
            buffer.writeBytes(bytes);
        }
        ctx.writeAndFlush(buffer);
    }

 服务端接收到的消息

 客户端发送的消息

 

  • 每一条消息采用分隔符,例如 \n,缺点需要转义 FixedLengthFrameDecoder

// 服务端优化, 此 Inbound 请加入到LoggingHandler 之前,只能使用这一个解码器
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));


// io.netty.channel.ChannelInboundHandlerAdapter#channelActive 执行的方法请使用此段
private static void sendMessage3(ChannelHandlerContext ctx) {
        log.debug("sending...");
        Random r = new Random();
        char c = 'a';
        ByteBuf buffer = ctx.alloc().buffer();
        for (int i = 0; i < 10; i++) {
            for (int j = 1; j <= r.nextInt(16)+1; j++) {
                buffer.writeByte((byte) c);
            }
            // 在 ASCII 表中,byte 10 表示换行符(\n)
            buffer.writeByte(10);
            c++;
        }
        ctx.writeAndFlush(buffer);
    }
  • 每一条消息分为 head 和 body,head 中包含 body 的长度

// 服务端替换解码器
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));


// 服务端替换发送数据方法
// 预设长度
    private static void sendMessage4(ChannelHandlerContext ctx) {
        log.debug("sending...");
        Random r = new Random();
        char c = 'a';
        ByteBuf buffer = ctx.alloc().buffer();
        for (int i = 0; i < 10; i++) {
            byte length = (byte) (r.nextInt(16) + 1);
            // 先写入长度
            buffer.writeByte(length);
            // 再写入数据
            for (int j = 1; j <= length; j++) {
                buffer.writeByte((byte) c);
            }
            c++;
        }
        ctx.writeAndFlush(buffer);
    }

io.netty.handler.codec.LengthFieldBasedFrameDecoder

 该解码器关系两部分数据,一部分是数据长度(可能是消息的总长度 16bytes 也可能是仅实际消息长度12 bytes).

参数1        lengthFieldOffset 表示lengthField的偏移位置 

参数2        lengthFieldLength 表示lengthField占用的长度(第一行表示 2字节长度)

参数3         lengthAdjustment 表示( 当前 lengthField为16, 要接受的内容(实际内容+hed2)长度总和是13 所lengthField值要减小 -3)

参数4        initialBytesToStrip 表示(发送前数据的长度与接受数据的长度差)

 

redis 协议举例

// 更换连接端口 6379
 bootstrap.connect("127.0.0.1", 6379).sync();

// 加入 类静态字段
static byte[] LINE = {13, 10};

    // io.netty.channel.ChannelInboundHandlerAdapter#channelActive 内方法换为此段
    private static void set(ChannelHandlerContext ctx) {
        ByteBuf buf = ctx.alloc().buffer();
        buf.writeBytes("*3".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("$3".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("set".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("$3".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("aaa".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("$3".getBytes());
        buf.writeBytes(LINE);
        buf.writeBytes("bbb".getBytes());
        buf.writeBytes(LINE);
        ctx.writeAndFlush(buf);
    }

 

edis 协议是 Redis 使用的一种文本协议,它是一种行协议,即在一个 TCP 连接中,将多条 Redis 指令分别写入不同的行中,Redis 服务器读取每一行指令并返回执行结果。

Redis 协议的格式很简单,一条 Redis 指令由以下几个部分组成:

  • 指令的参数个数:指令的第一个参数表示指令后面还有多少个参数;
  • 参数的长度:参数的字节数;
  • 参数的内容:参数的具体内容。

一个简单的例子:

*3\r\n    
$3\r\n    SET\r\n
$4\r\n    key1\r\n
$5\r\n    value\r\n
// 换行符和制表符是不必要的,此处是为了数据工整

        上述指令表示执行 Redis 的 SET key1 value 指令,其中 *3 表示有 3 个参数,$3 表示第一个参数长度为 3,即 SET, $4 表示第二个参数长度为 4,即 key1,$5 表示第三个参数长度为 5,即 value。

http 协议举例


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;

@Slf4j
public class TestHttp {
    public static void main(String[] args) throws InterruptedException {
        ChannelInboundHandlerAdapter handler = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                // 会收到客户端的两个请求  DefaultHttpRequest LastHttpContent$1 // get方式 Content 的内容是空的。

                System.out.println(msg.getClass());
                super.channelRead(ctx, msg);
            }
        };

        new ServerBootstrap()
                .channel(NioServerSocketChannel.class)
                .group(new NioEventLoopGroup())
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new HttpServerCodec());
                        ch.pipeline().addLast(new LoggingHandler());
                        ch.pipeline().addLast(handler);
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
                                log.debug(msg.getUri());
                                // 给客户端响应数据
                                DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
                                String content = "<h1>Hello World</h1>";
                                // 需要传递头信息,长度,不然客户端会一直接受消息
                                response.headers().setInt(CONTENT_LENGTH, content.length());
                                response.content().writeBytes(content.getBytes());
                                ctx.writeAndFlush(response);
                            }
                        });
                    }
                })
                .bind(8080)
                .sync().channel();

    }
}

自定义协议要素

  • 魔数,用来在第一时间判定是否是无效数据包

  • 版本号,可以支持协议的升级

  • 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk

  • 指令类型,是登录、注册、单聊、群聊... 跟业务相关

  • 请求序号,为了双工通信,提供异步能力

  • 正文长度

  • 消息正文

编解码器

根据上面的要素,设计一个登录请求消息和登录响应消息,并使用 Netty 完成收发

@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 2. 1 字节的版本,
        out.writeByte(1);
        // 3. 1 字节的序列化方式 jdk 0 , json 1
        out.writeByte(0);
        // 4. 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义,对齐填充
        out.writeByte(0xff);
        // 6. 获取内容的字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("{}", message);
        out.add(message);
    }
}

测试

EmbeddedChannel channel = new EmbeddedChannel(
    new LoggingHandler(),
    new LengthFieldBasedFrameDecoder(
        1024, 12, 4, 0, 0),
    new MessageCodec()
);
// encode
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123", "张三");
//        channel.writeOutbound(message);
// decode
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, message, buf);

ByteBuf s1 = buf.slice(0, 100);
ByteBuf s2 = buf.slice(100, buf.readableBytes() - 100);
s1.retain(); // 引用计数 2
channel.writeInbound(s1); // release 1
channel.writeInbound(s2);

 💡 什么时候可以加 @Sharable

  • 当 handler 不保存状态(无字段)时,就可以安全地在多线程下被共享

  • 但要注意对于编解码器类,不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler 父类,他们的构造方法对 @Sharable 有限制

  • 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类

对MessageCodec 的优化

@Slf4j
@ChannelHandler.Sharable
/**
 * 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的
 */
public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception {
        ByteBuf out = ctx.alloc().buffer();
        // 1. 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4});
        // 2. 1 字节的版本,
        out.writeByte(1);
        // 3. 1 字节的序列化方式 jdk 0 , json 1
        out.writeByte(0);
        // 4. 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节
        out.writeInt(msg.getSequenceId());
        // 无意义,对齐填充
        out.writeByte(0xff);
        // 6. 获取内容的字节数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
        outList.add(out);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("{}", message);
        out.add(message);
    }
}

 

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

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

相关文章

微服务链路追踪SkyWalking的介绍和部署

skywalking和链路追踪 SkyWalking介绍 首先我们要明白一点&#xff0c;在微服务的架构中&#xff0c;为什么要做链路追踪&#xff1f;解决问题的痛点在哪里&#xff1f;其实无外乎是如下几个问题&#xff1a; 如何将整个调用链路串起来&#xff0c;并能够快速定位问题&#…

供应链管理是干什么的,企业为什么要用供应链管理?

供应链管理的核心是&#xff1a;需求、生产、供应等方面的管理。没有对需求的管理&#xff0c;供应链管理就没有存在的价值&#xff0c;需求管理主要是产品生命周期管理&#xff0c;订单及预测管理&#xff0c;尽管预测永远是错误的&#xff0c;但这并不排除你可以做出一个相对…

C语言学习笔记:单链表

✨博文作者&#xff1a;烟雨孤舟 &#x1f496; 喜欢的可以 点赞 收藏 关注哦~~ ✍️ 作者简介: 一个热爱大数据的学习者 ✍️ 笔记简介&#xff1a;作为大数据爱好者&#xff0c;以下是个人总结的学习笔记&#xff0c;如有错误&#xff0c;请多多指教&#xff01; 目录 单链表…

深度学习-网络模型的可视化工具总结

强烈感谢公众号&#xff1a;尤而小屋 提供的文章思路 神经网络可视化难点在哪里&#xff1f; 神经网络可视化的难点在于以下几个方面&#xff1a; 复杂性&#xff1a;神经网络的结构通常非常复杂&#xff0c;包含大量的神经元和连接。对于大规模网络&#xff0c;准确地可视化每…

基于STM32的四旋翼无人机项目(二):MPU6050姿态解算(含上位机3D姿态显示教学)

前言&#xff1a;本文为手把手教学飞控核心知识点之一的姿态解算——MPU6050 姿态解算&#xff08;飞控专栏第2篇&#xff09;。项目中飞行器使用 MPU6050 传感器对飞行器的姿态进行解算&#xff08;四元数方法&#xff09;&#xff0c;搭配设计的卡尔曼滤波器与一阶低通滤波器…

五大自动化测试的Python框架详解

目录 1.Robot Framework 2.Pytest 3.UnitTest/PyUnit 4.Behave 5.Lettuce 结语 在此为大家准备了五种Python类型的自动化测试框架&#xff0c;以供比较和讨论。 1.Robot Framework 作为最重要的Python测试框架之一&#xff0c;Robot Framework主要被用在测试驱动(test-…

【序列dp】最长上升子序列(二)

文章目录 最长上升子序列-序列dp1016 最大上升子序列和1010. 拦截导弹187. 导弹防御系统272.最长公共上升子序列n^3 TLE优化 最长上升子序列-序列dp 什么是序列相关的 DP &#xff1f;序列相关 DP&#xff0c;顾名思义&#xff0c;就是将动态规划算法用于数组或者字符串上&…

前端项目工程化搭建

ESLint 在开发过程中&#xff0c;需要遵循一些规范&#xff0c;可以使用下面的工具来配置不同项目需要遵循的规范&#xff0c;来帮助我们检查错误、约束开发过程。 ESLint 配置 使用 Taro CLI 创建的项目&#xff0c;会自动生成 .eslintrc 文件。只需要在这个文件的 rules 配…

web渗透

首先这道题目与ctf还是有点关系的&#xff0c;首先看一下题目&#xff1a; 通过浏览器访问http://靶机服务器IP/1&#xff0c;对该页面进行渗透测试, 找到flag格式&#xff1a;flag&#xff5b;Xxxx123&#xff5d;&#xff0c;括号中的内容作为flag值并提交&#xff1b;&…

关于深度学习训练的工程技巧

前置基础 不同精度数据类型的动态范围 FP16的动态范围(6x10-8 ~ 65504) FP32的动态范围(1.4x10-45 ~ 1.7x1038) 可以看出Fp32的动态范围远大于fp16; 其中BF16的取值范围&#xff1a; BF16&#xff08;BFloat16&#xff09;的取值范围也是按照IEEE 754标准定义的&#xff0c;…

破解时间序列:移动平均法的综合指南

目录 前言一、时间序列介绍1-1、时间序列定义1-2、时间序列特性1-3、时间序列作用 二、统计学方法2-1、移动平均法介绍2-1-1、基本原理、计算过程2-1-2、移动平均法分类2-1-3、简单移动平均法2-1-4、加权移动平均法2-1-5、指数移动平均法&#xff08;Exponential Moving Averag…

C# 反射(Reflection)总结

目录 什么是反射&#xff1f; 为什么使用反射&#xff1f; 反射机制的优缺点 如何使用反射&#xff1f; 一&#xff0c;Type访问元数据 获取/修改类中公有成员&#xff08;属性PropertyI和字段Field等&#xff09; 调用类中的公有构造函数Constructor 调用类中的公有方…

【软件工程】软件工程期末考试复习题

填空题&#xff08;每空1分&#xff0c;共25分&#xff09; 软件生存周期一般可以划分为&#xff0c;问题定义、可行性研究、需求分析、设计、编码、测试和运行和维护。基于软件的功能划分&#xff0c;软件可以划分成___系统软件_、支撑软件、应用软件__三种。可行性研究&…

【UE 从零开始制作坦克】10-炮弹溅射伤害

目录 一、解决炮弹穿过坦克炮塔问题 二、炮弹溅射伤害 效果 一、解决炮弹穿过坦克炮塔问题 打开“PHYS_West_Tank_M1A1Abrams”这个物理资产 造成这种现象的原因是&#xff0c;炮弹只会与如下紫色区域产生碰撞事件 选中坦克炮塔的骨骼 添加盒体外形 缩放盒体外形使其包裹住…

数据库原理之数据库事物

文章目录 一、事物介绍1.1 事物的目的是保证数据的一致性1.2 事物的ACID A、I、D是为了实现 C1.3 什么是本地事物(Local Transactions) 二、数据库系统如何实现ACID2.1 影响深远的ARIES理论2.2 本地事物如何实现原子性和持久性 A、D2.2.1 实现原子性和持久性的Commit Logging方…

基于高精度三维机器视觉的汽车曲轴无序抓取系统应用

Part.1 行业背景 汽车产业的高速发展&#xff0c;对零部件自动化生产提出了更高要求。随着汽车销量的水涨船高&#xff0c;传统的手工生产模式已经难以满足大批量生产的需求&#xff0c;自动化生产是必然趋势。 曲轴是汽车发动机的关键组件之一&#xff0c;生产过程复杂&#…

【MySQL】如何速通MySQL(4)

&#x1f4cc;前言&#xff1a;本篇博客介绍如何速通MySQL的第四篇&#xff0c;主要介绍Mysql中主要的基础的入门&#xff0c;学习MySQL之前要先安装好MySQL&#xff0c;如果还没有安装的小伙伴可以看看博主前面的博客&#xff0c;里面有详细的安装教程。或者看一下下面这个链接…

Linux(centos )防火墙常见操作

1、查看防火墙当前状态 systemctl status firewalld 2、开启防火墙 systemctl start firewalld 3、关闭防火墙 systemctl stop firewalld.service 4、如果报错&#xff1a;-bash: firewall-cmd: command not found&#xff0c;可能是没有安装 firewall。安装命令&#xff1a…

处理错误 Xcode 编译找不到文件 libarclite_iphonesimulator.a

处理错误 Xcode 编译找不到文件 libarclite_iphonesimulator.a 视频 https://youtu.be/ZBMFs2PwkB4 错误描述 Error (Xcode): File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.aEr…

双非硕士-美国联培-马普所博士后-985系主任的逆袭之路

本科和硕士都是双非学校&#xff0c;博士期间曾有美国联合培养经历&#xff0c;毕业后到德国马普所从事博士后研究。现任985高校特聘教授、博/硕士生导师&#xff0c;系主任。知识人网小编特刊介绍李志明博士的逆袭之路。 随着国内就业压力的增大&#xff0c;高校招聘教师也呈现…