Netty系列-5 Netty启动流程

news2024/11/18 20:02:04

背景

Netty程序有固定的模板格式,以ServerBootstrap为例:

public class NettyServer {
    public void start(int port) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        EventLoopGroup boosGroup = new NioEventLoopGroup(1);
        EventLoopGroup workGroup = new NioEventLoopGroup(1);
        try {
            serverBootstrap.group(boosGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childOption(ChannelOption.SO_BACKLOG, 128)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel channel) {
                        channel.pipeline().addLast(new StringEncoder());
                        channel.pipeline().addLast(new StringDecoder());
                        channel.pipeline().addLast(new MyChannelInboundHandler());
                    }
                });
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception exception) {
            System.out.println(exception);
        } finally {
            boosGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

    private static class MyHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            System.out.println(msg);
        }
    }
    
    public static void main(String[] args) {
        new NettyServer().start(9998);
    }
}

将负责处理连接的和负责业务处理的NioEventLoopGroup线程池对象、通道类型、处理器Handler、子处理器Handler、连接配置等保存至ServerBootstrap对象,然后调用ServerBootstrap对象的bind方法启动Netty程序。

可以将ServerBootstrap和Bootstrap理解为启动引导器,将信息告知引导器,然后由引导器负责完成后续的启动流程。介绍启动流程前,需要了解一下ServerBootstrap和Bootstrap。

1.ServerBootstrap与Bootstrap

ServerBootstrap和Bootstrap作为Netty的启动引导类,分别用于启动Netty服务端和客户端。
首先看一下类的定义,二者都继承自AbstractBootstrap, 继承关系如下图:
在这里插入图片描述
AbstractBootstrap的核心属性:

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
	private volatile SocketAddress localAddress;
		
    volatile EventLoopGroup group;
    private volatile ChannelHandler handler;

    private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
    private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
    
    //...
}

localAddress用于记录本地ip和端口;group用于记录Netty使用的线程池对象NioEventLoopGroup;handler用于记录处理消息的ChannelHandler(后续加入Pipline);options和attrs记录属性,后续将被添加到通道上。
Bootstrap的核心属性如下所示:

public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
	private final BootstrapConfig config = new BootstrapConfig(this);
	private volatile SocketAddress remoteAddress;
    
	//...
}

Bootstrap用于启动客户端,因此需要指定远程服务器地址,保存在remoteAddress属性中;config属性保存了Bootstrap的详细信息,后续启动过程中,可直接通过该属性获取配置信息。
ServerBootstrap的核心属性如下所示:

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
    private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
    
    private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
    private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>();

    private volatile EventLoopGroup childGroup;
    private volatile ChannelHandler childHandler;
}

ServerBootstrap用于启动服务器,与Bootstrap相同,选择将所有信息保存在配置对象config中。
由于服务端接收客户端连接后,还需要创建子通道,并将通道信息注册到选择器上;因此需要保存子通道相关的信息:childGroup、childHandler、childOptions和childAttrs.
鉴于服务端逻辑较为复杂, 且对异步流程的设计更为巧秒,本文选择服务端启动为例进行介绍Netty的启动流程。

2.启动流程图

整体的启动流程如下图所示:
在这里插入图片描述
Netty的启动流程整体上看由两个线程完成:启动线程和NioEventLoop线程。NioEventLoop的启动由启动线程触发,启动线程执行创建和配置通道、启动NioEventLoop等主线任务,将注册和绑定等操作委托给NioEventLoop线程执行;线程间通过Future异步交互。以下将结合代码分章节对上图进行介绍。
建议阅读本文前先阅读Netty系列的其他文章。

3.启动流程

启动的入口函数为serverBootstrap.bind(port)

// 调用异步方法bind得到ChannelFuture,并调用sync阻塞等待异步逻辑执行完成
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

// 持续监听,直到关闭Netty
channelFuture.channel().closeFuture().sync();

跟踪bind方法:

public ChannelFuture bind(int inetPort) {
	return bind(new InetSocketAddress(inetPort));
}

public ChannelFuture bind(SocketAddress localAddress) {
	// doBind前的校验
	validate();
	return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
}

校验并运行doBind方法,所有核心逻辑均在doBind方法中。

3.1 doBind方法

private ChannelFuture doBind(final SocketAddress localAddress) {
	// 1.initAndRegister
	final ChannelFuture regFuture = initAndRegister();
	final Channel channel = regFuture.channel();
	if (regFuture.cause() != null) {
		return regFuture;
	}
	if (regFuture.isDone()) {
		ChannelPromise promise = channel.newPromise();
		// 2.doBind0
		doBind0(regFuture, channel, localAddress, promise);
		return promise;
	} else {
		final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
		regFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) throws Exception {
				Throwable cause = future.cause();
				if (cause != null) {
					promise.setFailure(cause);
				} else {
					promise.registered();
					// 2.doBind0
					doBind0(regFuture, channel, localAddress, promise);
				}
			}
		});
		return promise;
	}
}

doBind的核心方法是initAndRegister和doBind0,前者完成通道的创建、初始化和注册,后者执行绑定操作。initAndRegister成功执行完成,doBind0才可以执行。

上述代码可以简化为:

private ChannelFuture doBind(final SocketAddress localAddress) {
	// 1.initAndRegister
	final ChannelFuture regFuture = initAndRegister();
	final Channel channel = regFuture.channel();
	if (regFuture.cause() != null) {
		return regFuture;
	}
	ChannelPromise promise = channel.newPromise();
	// 阻塞等待regFuture异步逻辑执行完成
	regFuture.sync();
    // 执行doBind0
	doBind0(regFuture, channel, localAddress, promise);
	return promise;
}

之所以程序写的则这么复杂,是提高了程序效率:执行donBind的线程(这里是主线程)退出去可以执行其他任务,而不用阻塞在此。

ChannelFuture channelFuture = serverBootstrap.bind(port);

//... 执行其他任务

channelFuture.sync();

源码中根据regFuture异步注册是否完成有两个分支:
[1] 已完成,直接调用doBind0;
[2] 未完成,封装PendingRegistrationPromise对象,向regFuture添加监听器并返回PendingRegistrationPromise对象。后续阻塞在PendingRegistrationPromise上的线程依赖监听器唤醒(注册完成后唤醒,详细唤醒流程参考第二章的启动流程图);

if (regFuture.isDone()) {
    ChannelPromise promise = channel.newPromise();
    // 2.doBind0
    doBind0(regFuture, channel, localAddress, promise);
    return promise;
} else {
    final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
    regFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            Throwable cause = future.cause();
            if (cause != null) {
                promise.setFailure(cause);
            } else {
                promise.registered();
                // 2.doBind0
                doBind0(regFuture, channel, localAddress, promise);
            }
        }
    });
    return promise;
}

说明:分支[1] 在使用Netty过程中基本不会发生,是对一种小概率事件的效率优化。
后续将介绍注册流程需要经历:创建任务、任务加入队列、启动线程、从任务队列获取任务并执行、设置promise结果等操作;其中启动线程需要等待CPU的调度,除非设置给ServerBootstrap的NioEventLoopGroup对象的线程已被启动过(实际开发一般为新创建),否则启动netty的线程进入上述判断逻辑的时间点将绝大概率早于注册流程完成时间点。

理解上述逻辑后,initAndRegister和doBind0属于时间上顺序执行的逻辑,以下分小节分别介绍。

3.2 initAndRegister

initAndRegister忽略异常分支的主线逻辑如下:

final ChannelFuture initAndRegister() {
    // 步骤1:创建通道
	Channel channel = channelFactory.newChannel();
	// 步骤2:初始化通道
    init(channel);
    // 步骤3:注册通道
	ChannelFuture regFuture = config().group().register(channel);
	// 步骤4:返回异步对象
    return regFuture;
}

3.2.1 创建通道

通过serverBootstrap.channel(NioServerSocketChannel.class)将通道类型传递给serverBootstrap对象,这里通过反射调用NioServerSocketChannel的无参构造函数创建通道对象。

3.2.2 初始化通道

void init(Channel channel) {
	setChannelOptions(channel, newOptionsArray(), logger);
	setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));

	final EventLoopGroup currentChildGroup = childGroup;
	final ChannelHandler currentChildHandler = childHandler;
	final Entry<ChannelOption<?>, Object>[] currentChildOptions;
	synchronized (childOptions) {currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);}
	final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);
	
	ChannelPipeline p = channel.pipeline();
	p.addLast(new ChannelInitializer<Channel>() {
		@Override
		public void initChannel(final Channel ch) {
			final ChannelPipeline pipeline = ch.pipeline();
			ChannelHandler handler = config.handler();
			if (handler != null) {
				pipeline.addLast(handler);
			}

			ch.eventLoop().execute(new Runnable() {
				@Override
				public void run() {
					pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
				}
			});
		}
	});
}

逻辑分为三个部分:
[1] 将预置在ServerBootStrap的options和attrs属性设置到channel上;
[2] 创建匿名ChannelInitializer对象,其initChannel方法将添加预置在ServerBootStrap的handler对象以及ServerBootstrapAcceptor对象到channel的pipeline上;
[3] 将ChannelInitializer对象添加到channel的pipeline中;
注意:上述提到的channel,都指代NioServerSocketChannel对象。
ServerBootstrapAcceptor是一个特殊的channelHandler,为客户端连接创建通道(NioSocketChannel)并向选择器注册,因此需要保存NioSocketChannel的线程池对象、子处理器Handler、子通道连接配置等信息;ServerBootstrapAcceptor作用将在Netty处理消息时详细介绍。

3.2.3 注册通道

使用选择器从NioEventLoopGroup中选择一个NioEventLoop,并调用NioEventLoop对象的register方法:

public ChannelFuture register(Channel channel) {
	return next().register(channel);
}

进入NioEventLoop的register方法:

public ChannelFuture register(Channel channel) {
	return register(new DefaultChannelPromise(channel, this));
}

获取channel的unsafe对象,并调用其register方法:

public ChannelFuture register(final ChannelPromise promise) {
	promise.channel().unsafe().register(this, promise);
	return promise;
}

unsafe对象是Netty对底层IO的封装,进入unsafe的register方法(删除异常分支逻辑):

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
	AbstractChannel.this.eventLoop = eventLoop;
	if (eventLoop.inEventLoop()) {
		register0(promise);
	} else {
		eventLoop.execute(new Runnable() {
			@Override
			public void run() {
				register0(promise);
			}
		});
	}
}

这里的核心逻辑是register0,unsafe将register0委托给NioEventLoop线程异步执行。
注意:这里的eventLoop.execute(Runnable)会将Runnable保存在任务队列中,并将第一次启动NioEventLoop线程,启动后会从任务队列中取出任务再执行。可以参考: Netty系列-1 NioEventLoopGroup和NioEventLoop介绍

register0(剔除异常分支逻辑)的主线逻辑如下:

private void register0(ChannelPromise promise) {
	boolean firstRegistration = neverRegistered;
	// 步骤1:注册channel
    doRegister();
    // 步骤2:设置状态变量registered为已注册
	neverRegistered = false;
    registered = true;
    // 步骤3: 注册的后处理操作
	pipeline.invokeHandlerAddedIfNeeded();
	// 步骤4:设置Promise执行结果为成功状态
    safeSetSuccess(promise);
	// 步骤5: 向pipeline管道触发channelRegister事件
    pipeline.fireChannelRegistered();
	//...
}

步骤1: 注册channel

@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            //...异常逻辑
        }
    }
}

javaChannel()是NIO的ServerSocketChannel通道对象, eventLoop().unwrappedSelector()是NIO的选择器对象,抽象出来为: SelectionKey selectionKey = serverSocketChannel.register(selector,0 , this);

注意: 这里的attachment为this, 即NioServerSocketChannel对象。

步骤2:设置状态变量registered为已注册

registered表示通道是否已向select注册成功,使用该状态变量可以防止反复注册。

步骤3: 注册的后处理操作
invokeHandlerAddedIfNeeded将会调用callHandlerAddedForAllHandlers方法

final void invokeHandlerAddedIfNeeded() {
    assert channel.eventLoop().inEventLoop();
    if (firstRegistration) {
        firstRegistration = false;
        callHandlerAddedForAllHandlers();
    }
}

callHandlerAddedForAllHandlers在介绍Pipeline时介绍过:向pipeline添加处理器时,如果通道未注册,则添加处理器操作会被封装成延时任务;callHandlerAddedForAllHandlers执行时会调用这些任务:实现向pipeline添加handler,如果Handler是ChannlInitializer类型,则会调用ChannlInitializer的init方法。

步骤4:设置Promise执行结果为成功状态

safeSetSuccess(promise)方法通过promise.trySuccess()设置promise对象为执行成功:

public boolean trySuccess() {
	return trySuccess(null);
}

public boolean trySuccess(V result) {
	return setSuccess0(result);
}

private boolean setSuccess0(V result) {
	return setValue0(result == null ? SUCCESS : result);
}

private boolean setValue0(Object objResult) {
	if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
		RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
		if (checkNotifyWaiters()) {
			notifyListeners();
		}
		return true;
	}
	return false;
}

其中checkNotifyWaiters通过notifyAll()唤醒所有阻塞在该Promise上的线程,notifyListeners依次调用监听器的operationComplete方法。

此时章节3.1doBind中向regFuture添加的ChannelFutureListener监听器对象的operationComplete方法将被调用。

步骤5: 向pipeline管道触发channelRegister事件

此时,channelRegister事件将沿着pipeline的ctx链进行传递。

3.3 doBind0

继续跟进doBind0代码:

private static void doBind0(final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) {
    // 将绑定任务提交给channel绑定的NioEventLoop执行
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

注意这里的入参: channel是NioServerSocketChannel对象,待绑定的通道;localAddress是本地的地址;

regFuture是一个异步执行结果, 表示注册操作是否完成;promise也是一个异步执行结果,用于根据regFuture的状态设置promise。这里的promise是返回给Netty启动线程的:

// 这里sync阻塞等待的就是这个promise对象
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

当对promise设置结果时,sync会从阻塞中被唤醒。
继续跟进channel.bind方法:

public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // 将bind操作委托给pipeline
    return pipeline.bind(localAddress, promise);
}

public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    return tail.bind(localAddress, promise);
}

bind属于出站操作,pipeline将从tail开始逆向搜索ctx,直到第一个可以处理bind方法的ctx或者进入HeadContext:

public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
    unsafe.bind(localAddress, promise);
}

HeadContext将任务委托给unsafe对象(省去异常分支逻辑和校验逻辑):

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
	//...
	
	doBind(localAddress);
	
	//...
	
	// 设置promise的成功状态(同时唤醒阻塞的线程)
	safeSetSuccess(promise);
}

核心逻辑为执行doBind(localAddress)以及设置promise对象的状态为执行成功。

protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}

javaChannel()得到的Nio的ServerSocketChannel,因为Netty以NIO为基础进行的封装。

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

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

相关文章

番外篇 | 应对遮挡挑战,北航提出新型模型YOLOv5-FFM表现优异

前言:Hello大家好,我是小哥谈。在本文中,作者提出了一种改进的轻量级YOLOv5-FFM模型来解决行人检测遮挡问题。为了实现目标,作者在YOLOv5模型框架基础上进行了改进,并引入了Ghost模块和SE模块。此外,作者还设计了一个局部特征融合模块(FFM)来处理行人检测中的遮挡问题。…

【题解】2022ICPC杭州-K

翻译 原题链接   简述一下就是每次询问重新定义一个字母排序表&#xff0c;问在这个顺序下n个字符串的序列的逆序数是多少。 字典树计算逆序数 先考虑初始状况下&#xff0c;即 a < b < . . . < z a<b<...<z a<b<...<z的情况下&#xff0c;逆序…

基于PI控制器的车辆行驶控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 步骤一: 确定目标与测量 4.2 步骤二: 计算误差 4.3 步骤三: 设计PI控制器 4.4 步骤四: 应用控制信号 4.5 步骤五: 反馈循环 5.完整工程文件 1.课题概述 基于PI控制器的车辆行驶控制系统是一种常…

Hive数仓操作(一)

Hive 介绍 Hive 是一个基于 Hadoop 的数据仓库工具&#xff0c;旨在简化大规模数据集的管理和分析。它将结构化数据文件映射为表&#xff0c;并提供类似 SQL 的查询功能。Hive 的数据存储在 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;中&#xff0c;使用 Hive 查询语…

使用MessagePipe实现进程间通信

1、MessagePipe介绍 可以用于.NET和Unity上面的高性能的内存/分布式消息传递管道。适用于发布/订阅模式、CQRS的中介模式、Prism中的EventAggregator、IPC&#xff08;进程间通信&#xff09;-RPC等。 支持&#xff1a; 依赖注入过滤器管道更好的事件同步/异步带键值的/无键…

信息安全工程师(26)物理安全概念与要求

前言 物理安全是网络安全体系中的重要组成部分&#xff0c;它关注于保护物理环境、设备和资源免受未经授权的访问、破坏、损坏或盗窃。 一、物理安全概念 物理安全&#xff0c;也称为实体安全&#xff0c;是指通过采取各种物理措施来保护支持网络信息系统运行的硬件&#xff08…

【Qt】Qt中的窗口坐标 信号与槽

Qt中的窗口坐标 && 信号与槽 1. Qt中的窗口坐标2. 信号与槽的概述3. 信号和槽的使用3.1 connect函数的使用3.2 查看内置信号和槽3.2 connect的参数类型不匹配问题 4. 自定义信号 && 自定义槽4.1 自定义槽4.2 自定义信号 5. 带参数的信号和槽6. 信号与槽的关联方…

leetcode_55:跳跃游戏

给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 1&#xff1a; 输…

C#由窗体原子表溢出造成的软件闪退的问题解决方法

报错信息 由于在MS.Win32.UnsafeNativeMethods.RegisterClassEx产生了报错信息&#xff0c;但是一直向外部抛出错误但始终没有被捕捉成功&#xff0c;直到报错被UI线程捕获&#xff0c;但是仍然没有进行处理&#xff0c;所有造成WPF的应用闪退。 解析报错信息 1.从异常初始位…

Camera Raw:打开图像

在图像工作流程中&#xff0c;无论是 Raw 格式图像文件还是 JPEG、TIFF 文件&#xff0c;都可以先使用 Camera Raw 打开并调整后&#xff0c;再进入其它 Adobe 软件如 Photoshop 中进行进一步的编辑和处理。 一、打开 Raw 格式图像 1、通过 Adobe Bridge 打开 在 Adobe Bridge …

Excel插件:dd统计与排名

Excel插件&#xff1a;dd统计与排名 使用教程 专门为学校成绩统计与排名设计的插件 一、安装后如图 二、 功能介绍&#xff1a; &#xff08;一&#xff09;单科统计与排名 1、 模板说明&#xff08;单科用&#xff09; 2、 单科三分四率统计 PS&#xff1a;可以设置界值&am…

哈希知识点总结:哈希、哈希表、位图、布隆过滤器

目录 哈希 哈希表 哈希常用方法 1、直接定址法 2、存留余数法 哈希冲突 哈希冲突的解决办法 1、闭散列&#xff1a;开放定址法 &#xff08;1&#xff09;线性探测法 &#xff08;2&#xff09;二次探测法 2、开散列 哈希桶 / 拉链法 哈希的运用 位图 set操作 …

07-阿里云镜像仓库

07-阿里云镜像仓库 注册阿里云 先注册一个阿里云账号&#xff1a;https://www.aliyun.com/ 进入容器镜像服务控制台 工作台》容器》容器服务》容器镜像服务 实例列表》个人实例 仓库管理》镜像仓库》命名空间》创建命名空间 仓库管理》镜像仓库》镜像仓库》创建镜像仓库 使…

c++11~c++20 内联命名空间

在工作&#xff0c;我们经常会引入第三方库&#xff0c;偶尔会碰到同名的函数和类型&#xff0c;造成编译冲突的问题。一般我们可以使用命名空间&#xff0c;例如 #include <iostream> #include <iostream> using namespace std;namespace S1 {void foo(){cout &l…

Meta首款多模态Llama 3.2开源:支持图像推理,还有可在手机上运行的版本 | LeetTalk Daily...

“LeetTalk Daily”&#xff0c;每日科技前沿&#xff0c;由LeetTools AI精心筛选&#xff0c;为您带来最新鲜、最具洞察力的科技新闻。 Meta最近推出的Llama Stack的发布标志着一个重要的里程碑。这一新技术的推出不仅为开发者提供了强大的多模态能力&#xff0c;还为企业和初…

重构部队信息安全:部队涉密载体建设新策略

一、完善保密体系架构 1. 加强保密规章制度&#xff1a;制定或刷新关于机密信息管理的相关规定&#xff0c;明确机密信息的生成、复制、传输、使用、储存及销毁等核心环节的操作准则与责任分配&#xff0c;确保整个流程的标准化运作。 2. 明确个人保密义务&#xff1a;通过保密…

古老的啤酒酿造技艺:传承与发扬

在人类文明的浩瀚历史中&#xff0c;啤酒酿造技艺源远流长&#xff0c;承载着世代匠人的智慧与匠心。这些古老的技艺&#xff0c;不仅是一种手艺&#xff0c;更是一种文化的传承。今天&#xff0c;我们将一起走进这神秘的酿造世界&#xff0c;探寻古老啤酒酿造技艺的传承与发扬…

性能调优知识点(mysql)三

SQL底层执行原理 MySQL的内部组件结构&#xff1a;大体来说&#xff0c;MySQL 可以分为 Server 层和存储引擎层store两部分 Server层:主要包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有的内置函数&#xf…

基于Python大数据可视化的民族服饰数据分析系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

基于51单片机的多通道数字电压表proteus仿真

地址&#xff1a;https://pan.baidu.com/s/1zfDI2sjSGFHkYh33Sw6gHQ 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectron…