Netty深入浅出(无处不在的IO)

news2024/11/15 15:37:44

为什么要有Netty

Netty是为了解决网络编程的复杂性和提供易于使用、高性能和可扩展的框架而开发的。它通过提供一组可重用的组件来处理网络通信的低级细节,例如套接字管理、线程和缓冲,简化了开发网络应用程序的过程。这使开发人员可以专注于应用程序逻辑而不是网络编程的复杂性。此外,Netty支持各种协议和传输机制,使其成为构建各种网络应用程序的多功能选择。

Java中的IO模型

Netty是一个Java编写的网络IO库,Netty在其底层仍然使用Java I/O库,如java.nio包。它使用了Java NIO(New I/O)的一些特性,例如非阻塞通道(Channel)、选择器(Selector)等,以实现高性能的网络通信。

三种通信模型

  • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

  • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架。NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

NIO

示例代码
  • 服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server is listening on port 8080...");

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue;
            }

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {
                    SocketChannel clientChannel = serverSocketChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());
                } else if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);

                    if (bytesRead == -1) {
                        clientChannel.close();
                        System.out.println("Client disconnected.");
                    } else if (bytesRead > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            clientChannel.write(buffer); // 回显客户端发送的数据
                        }
                        buffer.clear();
                    }
                }

                keyIterator.remove();
            }
        }
    }
}
  • 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));

        String message = "Hello, NIO Server!";
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());

        socketChannel.write(buffer); // 发送消息给服务器

        ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
        int bytesRead = socketChannel.read(responseBuffer); // 读取服务器的响应

        if (bytesRead != -1) {
            responseBuffer.flip();
            byte[] bytes = new byte[responseBuffer.remaining()];
            responseBuffer.get(bytes);
            String response = new String(bytes);
            System.out.println("Received from server: " + response);
        }

        socketChannel.close(); // 关闭客户端连接
    }
}

AIO

示例代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Executors;

public class AIOTimeServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(10));

        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(group);
        serverChannel.bind(new InetSocketAddress(port));
        System.out.println("Server is listening on port " + port);

        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                serverChannel.accept(null, this); // 接受下一个连接

                String response = "Current time: " + System.currentTimeMillis();
                ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                clientChannel.write(buffer, null, new CompletionHandler<Integer, Void>() {
                    @Override
                    public void completed(Integer result, Void attachment) {
                        try {
                            clientChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void failed(Throwable exc, Void attachment) {
                        exc.printStackTrace();
                        try {
                            clientChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });

        try {
            group.awaitTermination(Long.MAX_VALUE, java.util.concurrent.TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

为什么Netty依旧使用NIO的API?

Netty 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL(后续会讲),没有很好实现 AIO,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化。

Linux的IO模型

Java的I/O模型是在Java编程语言层面的抽象,而Linux的I/O模型是操作系统内核层面的实现。因此,虽然它们有一些相似之处,但并不是完全相同的概念。

LINUX五种IO模型

同步和异步:同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。

阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。

  1. 阻塞式IO

  2. 非阻塞式IO

  3. IO多路复用

  4. 信号驱动

  5. 异步IO

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

从图中可以看出用户态与内核态的切换,那么什么是用户/内核态呢?这也是下面的内容。

用户态与内核态

I/O(Input/Output,输入/输出)和用户态与内核态之间存在密切的关系,特别是在操作系统中。用户态和内核态是操作系统中的两个不同特权级别,它们用于管理和保护计算机系统的资源。以下是关于I/O、用户态和内核态之间的关系的重要信息:

  1. I/O操作涉及用户态和内核态

    • I/O操作包括从应用程序到外部设备(如磁盘、网络、键盘、显示器等)的数据传输。这些操作通常涉及到用户态和内核态之间的切换。
    • 当应用程序需要执行I/O操作时,它会调用操作系统提供的I/O系统调用(例如,读取文件或发送数据包)。这些系统调用是在用户态执行的。
    • 操作系统内核负责管理系统资源和I/O设备,因此在执行I/O操作时,控制必须从用户态切换到内核态,以便内核可以直接访问硬件资源。
  2. 用户态和内核态的切换

    • 用户态和内核态是不同的CPU执行模式。在用户态下,应用程序只能访问有限的资源,而在内核态下,操作系统内核可以访问系统的全部资源。
    • 当应用程序需要执行需要特权访问的操作,例如执行系统调用或访问设备驱动程序,它必须通过软中断或异常将控制权切换到内核态。这个切换是由操作系统的内核来处理的。
    • 用户态到内核态的切换会涉及一些开销,因为需要保存和恢复CPU寄存器、切换堆栈等操作。因此,频繁的切换会影响性能。
  3. 内核态的I/O处理

    • 一旦控制切换到内核态,操作系统内核就可以执行I/O操作。它会管理设备驱动程序、缓冲区、中断处理等细节,以确保数据的正确传输。
    • 内核还会维护I/O请求队列,以有效地处理多个I/O请求。
  4. 异步I/O和用户态I/O

    • 异步I/O是一种I/O模型,允许应用程序继续执行其他任务,而不必等待I/O操作完成。在这种情况下,通常使用异步I/O库来管理I/O操作,而不需要频繁地切换用户态和内核态。
    • 异步I/O通常通过回调函数或事件通知机制来处理I/O完成事件。

I/O操作涉及用户态和内核态之间的切换,因为操作系统内核必须管理和控制I/O设备。这个切换是操作系统的核心功能之一,用于确保计算机系统的稳定性、安全性和性能。不同的I/O模型可以影响用户态和内核态之间的切换方式和频率。说到内核态切换,下面不得不介绍的就是零拷贝。

Zero Copy(零拷贝)

Netty与零拷贝(Zero-Copy)之间有密切的关系,因为Netty是一个网络应用框架,专门设计用于高性能的网络通信,而零拷贝是一项技术,可以用于提高数据传输的效率,特别是在网络通信中。

以下是Netty与零拷贝的关系和如何在Netty中利用零拷贝技术的一些重要信息:

Netty的高性能特性:Netty被设计为高性能的网络应用框架,它旨在处理大量并发连接和高吞吐量的网络通信。为了实现这一目标,Netty采用了多种性能优化技术,其中之一就是零拷贝。

零拷贝是一种优化技术,旨在减少数据在内存之间的复制次数。传统的数据传输通常涉及将数据从一个缓冲区复制到另一个缓冲区,这会引入额外的CPU和内存开销。零拷贝技术通过操作系统或硬件支持,允许数据在不复制的情况下从一个地方传输到另一个地方,从而提高了数据传输的效率。
   
`ByteBuf`是Netty的自定义缓冲区类型,它支持零拷贝和引用计数等特性。在Netty中,`ByteBuf`可以在数据传输时直接暴露底层数据,而不需要进行数据复制,从而减少了CPU和内存开销。

零拷贝的实现

传统IO

read:将数据从磁盘通过DMA读取到内核缓存区中,在拷贝到用户缓冲区

write: 先将数据写入到socket缓冲区中,经过DMA写入网卡设备

在这里插入图片描述

4次切换,4次拷贝

虚拟内存 mmap

1.虚拟内存空间可以远远大于物理内存空间
2.多个虚拟内存可以指向同一个物理地址


正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数。用户态可以直接访问内核态的数据。

4次切换,3次拷贝

sendFile

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作。

在这里插入图片描述

2次切换,3次拷贝

sendfile + DMA scatter/gather实现的零拷贝

linux2.4版本后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡,这样的话还可以省去CPU拷贝。

在这里插入图片描述

2次切换,2次拷贝,CPU全程不参与数据搬运

直接内存 Direct ByteBuf

Netty通常使用直接内存(Direct Memory)来提高性能。直接内存是一种特殊的内存分配方式,不同于Java堆内存。

  1. ByteBuf与直接内存:Netty中的ByteBuf是一个用于处理字节数据的缓冲区抽象。ByteBuf可以使用直接内存分配,这称为"Direct ByteBuf"。直接内存分配意味着ByteBuf中的数据存储在堆外内存,而不是在Java堆中。

  2. 减少内存复制:在进行网络数据传输时,数据通常需要从应用程序的缓冲区复制到操作系统内核缓冲区,然后再从内核缓冲区复制到网络适配器。使用直接内存,可以在这些步骤中减少或消除数据复制,提高了性能。

  3. 零拷贝:直接内存可以与零拷贝相结合,使数据可以在应用程序和操作系统之间进行高效的传输。

  4. 缓冲区池:Netty通常使用池化的ByteBuf来管理直接内存的分配和释放。这种方式可以避免频繁地分配和释放直接内存,提高了内存管理的效率。

  5. 内存管理控制:Netty提供了一些工具和机制,帮助开发者有效地管理直接内存,包括手动释放、自动回收等。

示例代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

public class NettyDirectMemoryExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个EventLoopGroup,一个用于接受客户端连接,一个用于处理客户端请求
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建ServerBootstrap
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new EchoServerHandler());
                    }
                });

            // 绑定端口并启动服务器
            serverBootstrap.bind(8080).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    // 自定义ChannelHandler处理客户端消息
    static class EchoServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf in = (ByteBuf) msg;
            ByteBuf out = Unpooled.directBuffer(); // 创建Direct ByteBuf

            try {
                out.writeBytes(in); // 将接收到的数据写入Direct ByteBuf
                ctx.write(out); // 写入回应数据到客户端
                ctx.flush();
            } finally {
                in.release(); // 释放接收缓冲区
                out.release(); // 释放Direct ByteBuf
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

使用直接内存需要谨慎管理,以避免内存泄漏和其他问题。

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

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

相关文章

前端学习| 第二章

CSS学习|第一章 前言一、概述1. 语法规定2. 代码风格 二、选择器1. 基础选择器标签选择器类选择器id选择器通配符选择器 2. 复合选择器后代选择器子元素选择器并集选择器伪类选择器链接伪类选择器focus 伪类选择器 三、引入方式四、显示模式1. 块元素2. 行内元素3. 行内块元素4…

FreeRTOS自我救赎3之USB虚拟串口

任何项目的功能都从需求出发&#xff0c;在这里我用的是斥侯蜂的一块STM32F407ZGT6 在开发一个项目的过程中&#xff0c;免不了串口调试&#xff0c;而这块板子板载的mircousb不是直接连的引脚而是一个OTGUSB

Qt实战 数据统计柱状图显示

前段时间有朋友找我做个问卷调查的软件&#xff0c;我说现在很多在线文档都有这功能&#xff0c;为啥还要使用Qt撸一个&#xff0c;他说要申请软著&#xff0c;我说&#xff0c;欧了。 我们先看看WPS在线问卷的统计中&#xff0c;柱状图统计的效果吧 我认为主要有以下几个关键…

集成显卡安装Pytorch

1、安装pytorch的第一步需要先安装Anaconda&#xff0c;安装步骤可看我这篇教程【Anaconda的下载与安装】 2、安装完Anaconda之后&#xff0c;第二步查看自己电脑的显卡类型&#xff0c;是集成显卡还是独立显卡&#xff0c;我的电脑是集成显卡&#xff0c;若你的是独立显卡&am…

磁盘满了对日志打印(Logback)的影响

背景 我们生产环境有一个服务半夜报警&#xff1a;磁盘剩余空间不足10%&#xff0c;请及时处理。排查后发现是新上线的一个功能&#xff0c;日志打太多导致的&#xff0c;解决方法有很多&#xff0c;就不赘述了。领导担心报警不及时、或者报警遗漏&#xff0c;担心磁盘满了对线…

OSPF的7大状态和5大报文详讲

- Down OSPF的初始状态 - Init 初始化——我刚刚给别人发Hello报文 我们可以将OSPF邻居建立的过程理解为&#xff1a;我和你打招呼&#xff0c;你和我打招呼&#xff0c;然后咱俩成了邻居 比如&#xff1a; R1和R2要建立OSPF邻居 R1给R2发送了Hello报文&#xff0c;但是R1此时…

如何提升和扩展 PostgreSQL — 从共享缓冲区到内存数据网格

利用共享缓存和操作系统缓存利用 RAM Postgres 是一个基于磁盘的数据库&#xff0c;即使您的整个架构是围绕磁盘访问设计的&#xff0c;利用 RAM 也很重要。如果按照人类规模的延迟来判断&#xff0c;这可以将延迟从几天缩短到几分钟&#xff08;图 1&#xff09;。只需看一下…

【SV中的多线程fork...join/join_any/join_none】

SV中fork_join/fork_join_any/fork_join_none 1 一目了然1.1 fork...join1.2 fork...join_any1.3 fork...join_none 2 总结 SV中fork_join和fork_join_any和fork_join_none; Note: fork_join在Verilog中也有&#xff0c;只有其他的两个是SV中独有的&#xff1b; 1 一目了然 1.…

CCF CSP认证 历年题目自练Day26

题目一 试题编号&#xff1a; 202012-1 试题名称&#xff1a; 期末预测之安全指数 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 题目分析&#xff08;个人理解&#xff09; 还是先看输出&#xff0c;此题简单的离谱&#xff0c;第一行输入小菜有几个测评依据…

1.3 第一个JAVA程序

1.3 第一个JAVA程序 **1.3 第一个Java程序** **前言:** 在1.2节中&#xff0c;我们学习了如何搭建Java开发环境。本节将带你编写并执行你的第一个Java程序。 **1. 编写Java源文件** - 在JDK的bin文件夹下新建文本文档&#xff0c;并重命名为HelloWorld.java。 - 使用记…

Linux命令笔记

终端命令格式&#xff1a; bash command [-options] [parameter] 7个常见Linux命令&#xff1a; 01 ls | list | 查看当前文件夹下的内容 02 pwd | print work directory | 查看当前所在文件夹 03 cd [目录名] | change directory | 切换文件夹 04 touch [文件名] | touc…

使用ChatGPT和MindShow一分钟生成PPT模板

对于最近学校组织的实习答辩&#xff0c;由于时间太短了&#xff0c;而且小编也特别的忙&#xff0c;于是就用ChatGPT结合MindShow一分钟快速生成PPT&#xff0c;确实很实用。只要你跟着小编后面&#xff0c;你也可以快速制作出这个PPT&#xff0c;下面小编就来详细介绍一下&am…

idea(添加jsp文件模板)

代码模板 初始化&#xff1a; <%-- Created by IntelliJ IDEA. User: ${USER} Date: ${DATE} Time: ${TIME} To change this template use File | Settings | File Templates. --%> <% page contentType"text/html;charsetUTF-8" language&…

微服务09-Sentinel的入门

微服务中的雪崩现象 首先&#xff0c;我们介绍一下微服务中雪崩现象&#xff1a;因为微服务中服务是互相调用的&#xff0c;错综复杂&#xff0c;当一个服务D出现问题时&#xff0c;那么调用D的服务请求就会失败&#xff0c;当请求累积到一定的量时&#xff0c;请求D的服务也会…

常见排序算法Java版(待续)

冒泡排序O(n^2) public class Main {public static void main(String[] args) {Random random new Random();int[] nums new int[]{random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100), random.nextInt(100)};for (i…

抽象轻松的java-mybatis简单入门

第一步&#xff1a;用IDEA新建一个java包 第二步&#xff1a;在IDEA中添加数据库&#xff08;ps&#xff1a;自己百度&#xff09; 点击数据库 第二步&#xff0c;新建数据库 选择你使用的数据库 用户与密码根据自己的设置进行配置 为了更方便的查看数据库&#xff0c;可以像图…

一篇短小精悍的文章让你彻底明白KMP算法中next数组的原理

以后保持每日一更&#xff0c;由于兴趣较多&#xff0c;更新内容不限于数据结构&#xff0c;计算机组成原理&#xff0c;数论&#xff0c;拓扑学......&#xff0c;所谓&#xff1a;深度围绕职业发展&#xff0c;广度围绕兴趣爱好。往下看今日内容 一.什么是KMP算法 KMP&#x…

C#对字典容器Dictionary<TKey, TValue>内容进行XML序列化或反序列化报错解决方法

一、问题描述 在使用C#对字典容器Dictionary<TKey, TValue>内容进行XML序列化报错【System.Exception:“不支持类型 System.Collections.Generic.Dictionary2[[System.String, mscorlib, Version2.0.0.0, Cultureneutral, PublicKeyTokenb77a5c561934e089],[System.Strin…

力扣刷题 day39:10-09

1.统计有序矩阵中的负数 给你一个 m * n 的矩阵 grid&#xff0c;矩阵中的元素无论是按行还是按列&#xff0c;都以非递增顺序排列。 请你统计并返回 grid 中 负数 的数目。 方法一&#xff1a;二分法 #方法一&#xff1a;二分法 def countNegatives(grid):res0for nums in…

Altium Designer实用系列(二)----PCB绘图小技巧

一、技巧总结 1.1 丝印大小 在导入PCB之后&#xff0c;元器件的丝印一般都是strock font&#xff0c;个人感觉比较大&#xff0c;也不美观&#xff0c;但是一个个修改成true type又比较麻烦。简便方法是使用相似查找全部修改:   此时会选中所有stroke 类型的丝印&#xff…