【手把手】深挖IO(补充篇)

news2024/11/15 14:00:41

在上一篇【深挖 IO】中已经将各大主流的 IO 网络模型介绍完了(还没看过的小伙伴墙裂推荐去瞅一眼 → https://blog.csdn.net/FeenixOne/article/details/129157665 → 不然对这篇的内容可能会有那么一乃乃的影响),那么废话不多说,趁着 IO 的热乎劲儿还没过去,咱们直接来手写个 Netty 青春版玩玩。

首先声明,上一篇中的某些概念这里就不重复解释了,譬如:channel、bytebuffer、selector 等,在 netty 中,bytebuffer 就抽象成了 bytebuf 类,其实这就是一个缓冲区,有了缓冲区之后可以大大的提高数据的读写效率,这个缓冲区可以是堆内的,也可以是堆外的。那咱们就从这个 bytebuffer 开始着手,先来看看 bytebuffer 的内部是怎样的。

NIO客户端与服务端通信

bytebuffer内部状态

客户端和服务端进行数据交互的时候,都是从 bytebuffer 中去读写数据,那么这个 buffer 到底内部是怎么工作的,写段代码运行一下看看:

public class MyNetty {

    @Test
    public void myBytebuf() {
        // 使用 Java 的 ByteBuffer 只能传一个参数,但是 Netty 的 ByteBuf 需要传两个参数
        // 需要指定初始的大小和最大的大小,趋向于慢慢平滑过渡的一个过程
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(8, 20);
        print(buf);

        // read/write 会控制索引指针的移动;get/set 则不会
        buf.writeBytes(new byte[]{1, 2, 3, 4});
        print(buf);

        buf.writeBytes(new byte[]{1, 2, 3, 4});
        print(buf);

        buf.writeBytes(new byte[]{1, 2, 3, 4});
        print(buf);

        buf.writeBytes(new byte[]{1, 2, 3, 4});
        print(buf);

        // 第 5 次写入则达到了最大容量的边界
        buf.writeBytes(new byte[]{1, 2, 3, 4});
        print(buf);

        // 第 6 次写入会不会报错?
        // buf.writeBytes(new byte[]{1, 2, 3, 4});
        // print(buf);
    }

    public static void print(ByteBuf buf) {
        // 是否可读,里面有没有东西给你读
        System.out.println("buf.isReadable >> " + buf.isReadable());

        // 读取下标,从哪个位置开始读
        System.out.println("buf.readerIndex >> " + buf.readerIndex());

        // 能读多少字节出来
        System.out.println("buf.readableBytes >> " + buf.readableBytes());

        // 是否可写
        System.out.println("buf.isWritable >> " + buf.isWritable());

        // 写入下标,从哪个位置开始写
        System.out.println("buf.writerIndex >> " + buf.writerIndex());

        // 能写多少字节进去
        System.out.println("buf.writableBytes >> " + buf.writableBytes());

        // 实际动态边界有多大
        System.out.println("buf.capacity >> " + buf.capacity());

        // 最大动态边界有多大
        System.out.println("buf.maxCapacity >> " + buf.maxCapacity());

        // true-堆外分配;false-堆内分配
        System.out.println("buf.isDirect >> " + buf.isDirect());

        System.out.println("------------------------------------------");
    }


}

代码中对 buffer 进行了 5 次的读写,每一次都打印出 buffer 的各种状态,看下每次的结果如何

初始创建完成之后,buffer 中的状态为:
buf.isReadable >> false ---- 不可读
buf.readerIndex >> 0 ------- 因为里面没东西,所以读取的下标从 0 开始
buf.readableBytes >> 0 ----- 能读的字节内容也是 0
buf.isWritable >> true ----- 可以写
buf.writerIndex >> 0 ------- 写入的下标也是从 0 开始
buf.writableBytes >> 8 ----- 可写的容量是 8
buf.capacity >> 8 ---------- 这个仅代表初始化的空间是 8
buf.maxCapacity >> 20 ------ 可膨胀到最大 20 的空间
buf.isDirect >> true ------- 现在是堆外分配

开始第 1 次写入,写入后再来看看此时 buffer 的状态:
buf.isReadable >> true ----- 刚刚写入了内容,现在可以读
buf.readerIndex >> 0 ------- 因为还没有读取过,所以指针起始还是从 0 开始读
buf.readableBytes >> 4 ----- 写入了 4 bytes,所以能读出 4 bytes
buf.isWritable >> true ----- 此时还可以往里面写入数据
buf.writerIndex >> 4 ------- 写入了 4 bytes,写入指针的位置移到 4,再写只能从 4 开始往后追加
buf.writableBytes >> 4 ----- 还能再往里写 4 bytes,基于初始化后的 8 bytes 总空间而言
buf.capacity >> 8 ---------- 这个仅代表初始化的空间是 8
buf.maxCapacity >> 20 ------ 可膨胀到最大 20 的空间
buf.isDirect >> true ------- 现在是堆外分配

第 2 次写入之后,刚好将初始化的 8 bytes 填满,所以第 3 次写入之后,总数据即便超过了 8 bytes 有还是可读 12 bytes 数据。但是有一点很有意思的是,buf.capacity >> 16,也就是说从原先的 8 bytes 扩容到了 16 bytes,默认只是扩容原先 1 倍的空间,也不是直接扩容到 20 bytes,当然最大空间还是 20 bytes。

继续写 ~ 继续写 ~ 会一直动态将空间分配到 20 bytes,这个没问题比较简单,但是前 5 次将空间全部填满之后,第 6 次写入的时候会不会报错呢?

答案是肯定的, 会直接抛出越界异常,提醒已经超出最大空间:java.lang.IndexOutOfBoundsException: writerIndex(20) + minWritableBytes(4) exceeds maxCapacity(20): PooledUnsafeDirectByteBuf(ridx: 0, widx: 20, cap: 20/20)

当然除了以上的 ByteBufAllocator.DEFAULT.buffer(8, 20); 这种分配方式之外,Netty 还引入了池化的概念,可以手动指定是否通过需要池化来提高 bytebuffer 的概念:
PooledByteBufAllocator.DEFAULT.heapBuffer(8, 20);
UnpooledByteBufAllocator.DEFAULT.heapBuffer(8, 20);

NIO客户端

在之前先简单介绍一个类:NioEventLoopGroup,这个类是 NIO 客户端模式的基础。这个类在构造的时候需要传入一个参数用来指定这个组中可用的线程数量:

NioEventLoopGroup selector = new NioEventLoopGroup(1);

当指定组中只有 1 个线程的时候,即使代码中写了两个线程分别去执行不同的任务,后面的的任务也因为没有线程而无法执行:

而当组中有 2 个线程的时候,就可以调用足够多的线程分别去执行任务:

有了这个知识点的铺垫之后,现在来正式的写 NIO 客户端代码:

    @Test
    public void clientMode() {
        // 创建循环事件组
        NioEventLoopGroup selector = new NioEventLoopGroup(1);

        // 创建客户端
        NioSocketChannel client = new NioSocketChannel();

        // 将客户端注册到循环事件组中
        // 类似 epoll_ctl(5, ADD, 3) 这么个系统调用
        selector.register(client);

        // 连接到服务端
        client.connect(new InetSocketAddress("192.168.126.129", 9090));

        // 将数据的字节流缓冲进 bytebuf 中
        ByteBuf buf = Unpooled.copiedBuffer("zhangsanfeng".getBytes());

        // 将 buf 中的数据刷写
        client.writeAndFlush(buf);


        System.out.println("client over");
    }

客户端有了,还差个服务端。使用虚拟机作为服务器,通过 nc -l 命令来监听 9090 端口:
nc -l 192.168.126.129 9090

nc 是 netcat 的简写,是一个功能强大的网络工具,有着网络界的瑞士军刀美誉。nc 命令在 Linux系统中实际命令是 ncat,nc 是软连接到 ncat。nc 命令的主要作用如下:
1、实现任意 TCP / UDP 端口的侦听,nc 可以作为 server 以 TCP 或 UDP 方式侦听指定端口
2、机器之间传输文件
3、端口的扫描,nc 可以作为 client 发起 TCP / UDP 连接
4、机器之间网络测速

运行客户端,给服务端发个数据,可以看到客户端的数据发了,也没报错,但是服务端这边原先好好的监听的状态已经退出:

这其实是响应式编程中最重要的一个特点,大部分的操作每一步都是异步的。也就是说在上面那版的代码中,连接也是异步的,连接完就会立刻返回一个ChannelFuture,通过这个返回结果设置等待双方连接成功;那么同样的,在 buf 刷写的时候也要设置保证数据书写成功:

    /**
     * NIO 客户端
     */
    @Test
    public void clientMode() throws InterruptedException {
        // 创建循环事件组
        NioEventLoopGroup selector = new NioEventLoopGroup(1);

        // 创建客户端
        NioSocketChannel client = new NioSocketChannel();

        // 将客户端注册到循环事件组中
        // 类似 epoll_ctl(5, ADD, 3) 这么个系统调用
        selector.register(client);

        // 连接到服务端
        ChannelFuture connect = client.connect(new InetSocketAddress("192.168.126.129", 9090));
        ChannelFuture sync = connect.sync();

        // 将数据的字节流缓冲进 bytebuf 中
        ByteBuf buf = Unpooled.copiedBuffer("zhangsanfeng".getBytes());

        // 将 buf 中的数据刷写到服务端
        ChannelFuture send = client.writeAndFlush(buf);
        send.sync();

        // 等待关闭,同步关闭
        sync.channel().closeFuture().sync();
        System.out.println("client over");
    }

 这么一设置之后,服务端就可以正常接受到客户端发送过来的数据:

为什么这里花大篇幅来讲需要控制同步异步的事情,因为基于 Netty 向上构建 RPC 的话,有时候需要主动的取到客户端去发送数据,并且连接需要一直保持阻塞住不能断。除非服务端自己主动断开连接,这样客户端才会继续往下走,打印 "client over"。

解决了客户端主动发送数据之后,现在需要处理接收服务端数据。原理同上面的发送基本一致,首先应该要获取一个服务端给客户端发送数据的事件,然后将读取的处理注册到这个事件上。框架上已经封装好了方法,可以通过客户端取到相应的管道,然后这个管道中的数据具体的读取后处理,需要自定义一个 Handler 去继承它提供的 ChannelInboundHandlerAdapter,重写其方法

public class MyInHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client registed >> ");
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client active >> ");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        CharSequence str = buf.readCharSequence(buf.readableBytes(), CharsetUtil.UTF_8);
        System.out.println(str);
        System.out.println("---------------------------------------------------------");
    }

}

如此一来,客户端就可以接收到服务端传来的数据:

现在是实现了客户端单方面的数据接收,如果客户端在接收到数据之后,还想要给对方回复的话,那么客户端在读取数据的时候,就不可以使用 buf.readCharSequence(buf.readableBytes(), CharsetUtil.UTF_8); 这个方法,这个方法会将 buf 中缓冲区的指针进行位移,这样再写入数据的时候由于指针在读取的时候已经位移了,就只能写入位移后指针的位置后面的数据。所以在读取数据的时候应该换成 buf.getCharSequence(0, buf.readableBytes(), CharsetUtil.UTF_8);

这样便可以将服务端发送过来的数据,再反向给它回复回去

NIO服务端

服务端的原理和客户端基本一致,也是创建一个事件组,然后将服务端的 Channel 注册到事件中,保持同步连接,等待同步关闭等等,直接看代码:

    /**
     * Netty - NIO - 服务端
     */
    @Test
    public void serverMode() throws InterruptedException {
        NioEventLoopGroup selector = new NioEventLoopGroup(1);

        NioServerSocketChannel server = new NioServerSocketChannel();

        selector.register(server);

        // 服务端和客户端在这里有着本质的不同
        // 对于客户端来说,连接中读取到的是服务端发送来的数据
        // 对于服务端来说,监听中读取到的就是 Socket,也就是一个个的client
        // 所以服务端接收到客户端之后,还要将这些客户端注册到事件中
        ChannelPipeline pipeline = server.pipeline();
        pipeline.addLast(new MyAcceptHandler(selector, new MyInHandler()));

        ChannelFuture bind = server.bind(new InetSocketAddress("192.168.31.134", 9090));
        bind.sync().channel().closeFuture().sync();
        System.out.println("server over");
    }

不过服务端和客户端在这里有着本质的不同:对于客户端来说,连接中读取到的是服务端发送来的数据;而对于服务端来说,监听中读取到的就是 Socket,也就是一个个的客户端,所以服务端接收到客户端之后,还要将这些客户端注册到事件中

public class MyAcceptHandler extends ChannelInboundHandlerAdapter {

    private final EventLoopGroup selector;
    private final ChannelHandler handler;

    public MyAcceptHandler(NioEventLoopGroup selector, ChannelHandler myInHandler) {
        this.selector = selector;
        this.handler = myInHandler;
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("server registed >> ");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 得到连接过来的客户端
        SocketChannel client = (SocketChannel) msg;

        // 注册
        selector.register((Channel) client);

        // 响应式 Handler
        ChannelPipeline clientPipeline = ((Channel) client).pipeline();
        clientPipeline.addLast(handler);
    }

}

这样有了服务端之后,使用虚拟机作为客户端,通过 nc 命令来连接到服务端的 9090 端口:
nc 192.168.31.134 9090

客户端给服务端发送数据之后,被服务端接受,并返回给了客户端

那么既然能接受一个客户端,能不能接受多个客户端呢?很遗憾,此时在开一个窗口,使用 nc 模拟客户端再来连接服务端,会看到直接报错:

核心点在于这句:MyInHandler is not a @Sharable handler, so can't be added or removed multiple times.

自定义的 Handler 并不可以用于共享,也就是说不允许在多个地方注册给多个连接共享使用。在服务端启动之后,MyInHandler 只被 new 了一次。然后每连接进来一个客户端,都是把这唯一的 MyInHandler 作为参数传递进去,单例是很香没错,但是如果在设计 MyInHandler 的时候定义了很多的属性,那这些属性的值就会乱掉。这个问题解决起来也很简单,根据提示给 MyInHandler 加上 @ChannelHandler.Sharable 注解即可

很明显,这些处理数据的 Handler 肯定是由用户自定义去实现,就无法强制性要求用户非得按照单例模式来设计,所以 @ChannelHandler.Sharable 注解不应该强压给用户。如果每次传递参数的时候,都 new 一个 MyInHandler 传递进去,但是代码没法写啊.....

所以,是不是可以设计一个没有业务功能的 Handler 作为一个包装外壳,在这个壳里面再去真正的 new 这个 MyInHandler

@ChannelHandler.Sharable
public class ChanneInit extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        Channel client = ctx.channel();
        ChannelPipeline pipeline = client.pipeline();
        pipeline.addLast(new MyInHandler());
        ctx.pipeline().remove(this);
    }

}

原先 MyInHandler 上面的注解直接挪到这个壳上,那么服务端得到连接过来的客户端先接收到的就是 ChanneInit,注册会调起 ChannelInit 的注册事件,new 新的 MyInHandler,remove 掉 ChannelInit,所以在 ChanneInit 中最后有 ctx.pipeline().remove(this); 这么一句代码

这也是为什么 Netty 框架中也设计了一个 ChannelInitializer 抽象类的原因:

并且这个抽象类中还定义了一个 initChannel 方法:

也就是说当用户需要给客户端添加 Handler 的时候,就得先 new 一个 ChannelInitializer,然后去实现其中的 initChannel 方法,其实就是将自定义的 Handler 作为参数传进去。

Netty-客户端/服务端

前面已经将 NIO 的客户端与服务端间的通信手写了一遍,对其中的原理也认识的较为清晰。那么接下来,使用 Netty 官方的手法重新梳理一遍客户端和服务端。

    @Test
    public void nettyClient() throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup(1);

        Bootstrap bs = new Bootstrap();
        ChannelFuture channelFuture = bs.group(group)
                                        .channel(NioSocketChannel.class)
                                        .handler(new ChanneInit())
                                        .connect(new InetSocketAddress("192.168.126.129", 9090));
        Channel client = channelFuture.sync().channel();

        ByteBuf buf = Unpooled.copiedBuffer("qufeiyan".getBytes());
        ChannelFuture send = client.writeAndFlush(buf);
        send.sync();

        client.closeFuture().sync();
    }

Netty 的写法上较为更加的清晰,而且在 handler 那一步也可以采用 Netty 官方提供的 ChannelInitializer :

同样的服务端也可以这么写:

    @Test
    public void nettyServer() throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup(1);

        ServerBootstrap bs = new ServerBootstrap();
        ChannelFuture bind = bs.group(group, group)
                               .channel(NioServerSocketChannel.class)
                               .childHandler(new ChannelInitializer<NioSocketChannel>() {
                                   @Override
                                   protected void initChannel(NioSocketChannel ch) throws Exception {
                                       ChannelPipeline pipeline = ch.pipeline();
                                       pipeline.addLast(new MyInHandler());
                                   }
                               })
                               .bind(new InetSocketAddress("192.168.31.134", 9090));

        bind.sync().channel().closeFuture().sync();
    }

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

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

相关文章

(一)Qt下实现多个海康工业相机内触发采集回调取流显示

系列文章目录 提示&#xff1a;这里是该系列文章的所有文章的目录 第一章&#xff1a;&#xff08;一&#xff09;Qt下实现多个海康工业相机内触发采集回调取流显示 第二章&#xff1a;&#xff08;二&#xff09;Qt下多线程实现多个海康工业相机内触发采集回调取流显示 文章目…

如何设计一个注册中心?(1)概念

1. 为什么需要注册中心 一个集群中有众多服务&#xff0c;每个服务有N个实例&#xff0c;因此需要一个第三方节点来存放每个服务的信息&#xff0c;这样服务需要其它的服务信息&#xff0c;直接到第三方节点获取就行了。这个第三方的节点&#xff0c;就是注册中心。 2. 注册中…

2023.7.14 智慧芽前端面试总结

整体面试体验感蛮不错的&#xff0c;面试官很愿意与我交流&#xff0c;面试的结尾也给出了一定的学习建议。 由于这家公司主要的业务是做搜索引擎方面&#xff0c;估摸着是个自研。面试注重点主要是偏业务处理和针对工具的使用&#xff0c;还有无障碍阅读英文文章&#xff0c;…

C++进阶—哈希/unordered系列关联式容器/底层结构(一篇文章学习哈希)

目录 0. 前言map/set和unordered_map/unordered_set 1. unordered系列关联式容器 1.1 unordered_map 1.1.2 unordered_map的接口说明 1. unordered_map的构造 2. unordered_map的容量 3. unordered_map的迭代器 4. unordered_map的元素访问 5. unordered_map的查询 6…

Spring原码学习第一篇:Spring源码概述

1、Spring获取对象的过程 2、Spring源码概述图&#xff1a; 2、一些重要的接口 BeanDefinition中实现的方法&#xff0c;把xml中定义的对象封装为一个对象&#xff0c;方便后面处理 4、BeandefinitionReader BeandefinitionReader作为一个抽象层来处理配置文件&#xff0c;定…

Autosar通信实战系列01-CanSM模块功能及配置开发详解

本文框架 前言1. CanSM General配置2. 通道详细配置解析2.1 添加一路CanSMController2.2 CanSMController配置2.3 关联DemEvent配置前言 在本系列笔者将结合工作中对通信实战部分的应用经验进一步介绍常用,包括但不限于通信各模块的开发教程,代码逻辑分析,调测试方法及典型…

【Maven二】——maven仓库

系列文章目录 Maven之POM介绍 maven命令上传jar包到nexus maven仓库 系列文章目录前言一、什么是maven仓库&#xff1f;二、仓库的分类建立私服的优势 三、远程仓库的配置四、远程仓库的认证部署至远程仓库五、快照版本&why六、从仓库解析依赖的机制七、镜像总结 前言 由…

习题—实用类

目录 1.xxx 1.包装类及其构造方法 import java.util.List; import java.util.ArrayList; import java.util.Scanner;//包装类及其构造方法 public class Test {public static void main(String[] args) {// 装箱&#xff1a;基本类型转换为包装类的对象 // 拆箱&#xff1a…

go mod verdor简明介绍

Go 语言在 go 1.6 版本以后编译 go 代码会优先从 vendor 目录先寻找依赖包&#xff0c;它具有以下优点&#xff1a; 复制依赖&#xff1a;go mod vendor 会把程序所依赖的所有包复制到项目目录下的vendor 文件夹中&#xff0c;所以即使这些依赖包在外部源&#xff08;如 GitHu…

「C/C++」C/C++宏定义#define

✨博客主页&#xff1a;何曾参静谧的博客 &#x1f4cc;文章专栏&#xff1a;「C/C」C/C程序设计 目录 术语说明宏定义 #define定义常量定义函数定义代码块常用标识符用宏包含头文件 术语说明 定义宏是一种预处理器指令&#xff0c;它可以将一些代码片段或者常量直接替换为另一…

刘二大人Pytorch课程笔记

Lecture01. Overview 没啥好记的&#xff0c;理解就好 人工智能和机器学习等的关系&#xff1a; 正向传播 正向传播本质上是按照输入层到输出层的顺序&#xff0c;求解并保存网络中的中间变量本身。 反向传播 反向传播本质上是按照输出层到输入层的顺序&#xff0c;求解并…

LangChain 联合创始人下场揭秘:如何用 LangChain 和向量数据库搞定语义搜索?

近期&#xff0c;关于 ChatGPT 的访问量有所下降的消息引发激烈讨论&#xff0c;不过这并不意味着开发者对于 AIGC 的热情有所减弱&#xff0c;例如素有【2023 最潮大语言模型 Web 开发框架】之称的大网红 LangChain 的热度就只增不减。 原因在于 LangChain 作为大模型能力“B2…

1快速入门MyBatis

开发前的准备 准备数据库表&#xff1a;汽⻋表t_car 确定表中的字段以及字段的数据类型 guide_price是decimal类型&#xff0c;专⻔为财务数据准备的类型produce_time可以用char类型 , 格式’2022-10-11’ 使用navicat for mysql⼯具向t_car表中插⼊两条数据 配置IDEA中ma…

【C++修炼之路】vector 模拟实现

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录 一、读源码二、成员变量三、默认成员函数1、构造2、析构3、拷贝构造4、赋值重载 四、访问1、[ ] 重载2、迭代器 五、容量1、cap…

Profibus DP主站转Modbus TCP网关profibus从站地址范围

远创智控YC-DPM-TCP网关。这款产品在Profibus总线侧实现了主站功能&#xff0c;在以太网侧实现了ModbusTcp服务器功能&#xff0c;为我们的工业自动化网络带来了全新的可能。 远创智控YC-DPM-TCP网关是如何实现这些功能的呢&#xff1f;首先&#xff0c;让我们来看看它的Profib…

Oracle解析JSON字符串

Oracle解析JSON字符串 假设某个字段存储的JSON字符串&#xff0c;我们不想查出来后通过一些常见的编程语言处理&#xff08;JSON.parse()或者是JSONObject.parseObject()等&#xff09;&#xff0c;想直接在数据库上处理&#xff0c;又该如何书写呢&#xff1f; 其实在ORACLE中…

算法06-搜索算法-广度优先搜索

文章目录 参考&#xff1a;总结大纲要求搜索算法-广度优先搜索迷宫问题问题迷宫的存储迷宫的移动搜索方式代码实现 图的广度优先遍历题目描述用邻接矩阵表示图 搜索算法-广度优先搜索 参考&#xff1a; 【算法设计】用C类和队列实现图搜索的广度优先遍历算法 C/C 之 广度优先…

梯度下降(Gradient Descent)

基本思想 梯度下降是一个用来求函数最小值的算法&#xff0c;本次&#xff0c;我们将使用梯度下降算法来求出代价函数的最小值。 梯度下降背后的思想是&#xff1a;开始时我们随机选择一个参数的组合&#xff0c;计算代价函数&#xff0c;然后我们寻找下一个能让代价函数值下降…

Linux:squid透明代理

在传统代理上进行修改并添加网卡 这次不使用手动代理&#xff0c;而是把网关搞成代理 在下面这个链接里的文章实验下进行修改 Linux&#xff1a;squid传统代理_鲍海超-GNUBHCkalitarro的博客-CSDN博客 完成以后不用再win10上去配置&#xff0c;代理的那一步&#xff0c;然后…

Python(十二)常见的数据类型

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…