Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍
背景
本文介绍Netty的通道组件NioServerSocketChannel和NioSocketChannel,从源码的角度介绍其实现原理。
1.NioServerSocketChannel
Netty本质是对NIO的封装和增强,因此Netty框架中必然包含了对于ServerSocketChannel的构建、配置以及向选择器注册,如下所示:
// 创建ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = SelectorProvider.provider().openServerSocketChannel();// ServerSocketChannel通道设置为非阻塞
serverSocketChannel.configureBlocking(false);// 将ServerSocketChannel通道注册至选择器
serverSocketChannel.register(Selector, opts, attachment);// 接收客户端连接得到SocketChannel通道
SocketChannel socketChannel = serverSocketChannel.accept();
其中的构建和配置过程发生在NioServerSocketChannel的实例化过程。
1.1 NioServerSocketChannel构造函数
NioServerSocketChannel实例化过程包含了对serverSocketChannel的创建以及配置
Netty启动时,通过反射调用NioServerSocketChannel的无参构造函数创建NioServerSocketChannel对象.
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();public NioServerSocketChannel() {this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}public NioServerSocketChannel(ServerSocketChannel channel) {super(null, channel, SelectionKey.OP_ACCEPT);config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
DEFAULT_SELECTOR_PROVIDER是Provider对象,用于创建通道和选择器,newSocket方法返回一个ServerSocketChannel对象,如下所示:
private static ServerSocketChannel newSocket(SelectorProvider provider) {try {return provider.openServerSocketChannel();} catch (IOException e) {throw new ChannelException("Failed to open a server socket.", e);}
}
NioServerSocketChannel中还维护了一个config对象用于储存该通道相关的配置,后续通过通道对象的config()
方法获取该config对象。
继续调用父类的构造方法:
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {super(parent);this.ch = ch;this.readInterestOp = readInterestOp;try {ch.configureBlocking(false);} catch (IOException e) {try {ch.close();} catch (IOException e2) {logger.warn("Failed to close a partially initialized socket.", e2);}throw new ChannelException("Failed to enter non-blocking mode.", e);}
}// super(parent)内容如下:
protected AbstractChannel(Channel parent) {this.parent = parent;id = newId();unsafe = newUnsafe();pipeline = newChannelPipeline();
}
因此NioServerSocketChannel中包含如下属性:
[1] SelectableChannel ch:实际为ServerSocketChannel类型,即NIO中的服务端通道类型,并将其配置为非阻塞类型,以便后续向选择器注册;
[2] int readInterestOp: 值固定为SelectionKey.OP_ACCEPT,表示仅处理连接事件;
[3] pipeline: Netty的Pipeline组件,每个channel都有一个属于自己的Pipeline对象;
[4] unsafe: 对底层IO进行了封装,实际的读写操作在该类中进行处理;
[5] 其他: id唯一ID标识,parent固定为空。
1.2 NioServerSocketChannel注册
NioServerSocketChannel包含了ServerSocketChannel对象,向选择器注册NioServerSocketChannel本质是将ServerSocketChannel注册到选择器
在Netty启动流程流程中,依次构造ServerSocketChannel, 并注册到选择器上,具体逻辑为:
// NioServerSocketChannel的父类AbstractNioChannel中
// 删除try-catch异常逻辑
protected void doRegister() throws Exception {boolean selected = false;for (;;) {selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);return;}
}
其中: javaChannel()获取NioServerSocketChannel对象的ServerSocketChannel属性;eventLoop().unwrappedSelector()为NioEventLoop这个线程绑定的选择器;此处的this表明将ServerSocketChannel注册到选择器上时,将当前的NioServerSocketChannel对象作为attachment保存到SelectionKey中,并使用volatile SelectionKey selectionKey;
属性保存了注册结果。
说明:后续选择器会执行select而阻塞,当该选择器被IO事件唤醒时,可通过SelectionKey的attachment获取NioServerSocketChannel对象,从而可以获取包括ServerSocketChannel、Pipeline、Config等其他所有相关信息。
1.3 NioServerSocketChannel处理连接
章节1.1中提到了NioServerSocketChannel的unsafe属性,unsafe用于封装底层具体的IO行为,具体的实现类为NioMessageUnsafe.
当有连接请求到达NioServerSocketChannel后,进入NioMessageUnsafe的read()方法中(详细的调用流程和线程处理关系在后续Netty的消息处理流程中介绍, 这里仅对read方法实现逻辑进行说明),read方法省去内存分配优化策略以及异常处理逻辑后的主线逻辑如下:
private final class NioMessageUnsafe extends AbstractNioUnsafe {private final List<Object> readBuf = new ArrayList<Object>();@Overridepublic void read() {// ...final ChannelPipeline pipeline = pipeline();do {// ...doReadMessages(readBuf);} while (allocHandle.continueReading());int size = readBuf.size();for (int i = 0; i < size; i ++) {readPending = false;pipeline.fireChannelRead(readBuf.get(i));}readBuf.clear();pipeline.fireChannelReadComplete();}
}
readBuf是一个列表类型,用于存放解析后的消息对象,解析完成后,依次遍历readBuf,并调用pipeline.fireChannelRead将消息对象发送至Netty的Pipeline组件(后面单独介绍)。
解析逻辑在doReadMessages方法中:
protected int doReadMessages(List<Object> buf) throws Exception {SocketChannel ch = SocketUtils.accept(javaChannel());try {if (ch != null) {buf.add(new NioSocketChannel(this, ch));return 1;}} catch (Throwable t) {logger.warn("Failed to create a new channel from an accepted socket.", t);try {ch.close();} catch (Throwable t2) {logger.warn("Failed to close a socket.", t2);}}return 0;
}// SocketUtils.accept(javaChannel())代码逻辑:
public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {// 删除try-catch异常逻辑return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {@Overridepublic SocketChannel run() throws IOException {return serverSocketChannel.accept();}});
}
javaChannel()得到ServerSocketChannel对象,serverSocketChannel.accept()得到客户端通道对象SocketChannel。将当前服务端通道NioServerSocketChannel对象和得到的客户端通道对象SocketChannel作为参数构造NioSocketChannel对象。
2.NioSocketChannel
与NioServerSocketChannel相似,NioSocketChannel也是Netty对NIO中ServerSocketChannel的封装和增强。本章节内容将包含SocketChannel的构建、配置、向选择器注册以及读取数据,如下所示:
// 得到SocketChannel对象
SocketChannel socketChannel = serverSocketChannel.accept();// SocketChannel通道设置为非阻塞
socketChannel.configureBlocking(false);// 将SocketChannel通道注册至选择器
socketChannel.register(Selector, opts, attachment);// 从SocketChannel通道读取数据值缓冲区
socketChannel.read(ByteBuffer)
2.1 NioSocketChannel构造函数
每个客户端连接对应一个通道,即一个NioSocketChannel对象。
Netty收到客户端连接时,会调用NioSocketChannel构造函数创建通道对象,如下所示:
public NioSocketChannel(Channel parent, SocketChannel socket) {super(parent, socket);config = new NioSocketChannelConfig(this, socket.socket());
}
parent为NioServerSocketChannel对象,socket为NIO中SocketChannel对象。NioSocketChannel与NioServerSocketChannel相似,维持了一个config配置类用于存放和读取通道的配置信息。
继续沿着super调用父类的构造方法:
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {super(parent, ch, SelectionKey.OP_READ);
}protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {super(parent);this.ch = ch;this.readInterestOp = readInterestOp;try {ch.configureBlocking(false);} catch (IOException e) {try {ch.close();} catch (IOException e2) {logger.warn("Failed to close a partially initialized socket.", e2);}throw new ChannelException("Failed to enter non-blocking mode.", e);}
}protected AbstractChannel(Channel parent) {this.parent = parent;id = newId();unsafe = newUnsafe();pipeline = newChannelPipeline();
}
上述构造过程逻辑较为简单,为NioSocketChannel创建一个Unsafe对象和Pipeline对象;以及将ch属性即SocketChannel设置为非阻塞。
2.2 注册选择器
NioServerSocketChannel接收客户端连接构造出NioSocketChannel对象,并通过Pipeline.fireChannelRead触发Inbound读事件后,通过Pipiline进入ServerBootstrapAcceptor处理器的channelRead方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {final Channel child = (Channel) msg;// ...childGroup.register(child).addListener(new ChannelFutureListener() {//...});
}
由章节1可知msg消息为NioSocketChannel,childGroup为线程池NioEventLoopGroup对象(workgroup)。
childGroup.register(child)
表示将NioSocketChannel注册到workgroup的一个线程中,经过Unsafe对象最终会进入NioSocketChannel的doRegister方法:
@Override
protected void doRegister() throws Exception {// ...selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);// ...
}
javaChannel()为NioSocketChannel的ch属性,即SocketChannel通道对象;eventLoop().unwrappedSelector()为选择器;this为NioSocketChannel对象本身;返回的SelectionKey也作为属性保存在NioSocketChannel类中。
说明:后续选择器会执行select而阻塞,当有可读消息到达时被唤醒。可通过SelectionKey得到NioSocketChannel对象,从而得到相关的SocketChannel、Pipeline、Config等其他所有相关信息。
2.3 读取消息
当有可读时间到达时,NioEvetLoop会从阻塞中被唤醒,从而执行processSelectedKeys处理IO事件:
private void processSelectedKeys() {// ...processSelectedKeysOptimized();// ...
}private void processSelectedKeysOptimized() {for (int i = 0; i < selectedKeys.size; ++i) {final SelectionKey k = selectedKeys.keys[i];selectedKeys.keys[i] = null;final Object a = k.attachment();processSelectedKey(k, (AbstractNioChannel) a);}
}
遍历已就绪的IO事件,调用processSelectedKey方法处理,此时k为NIO的SelectionKey对象,而attachment为NioSocketChannel对象。
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();int readyOps = k.readyOps();//...if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {unsafe.read();}// ...
}
根据SelectionKey和NioSocketChannel对象的readyOps确定此时IO事件为可读消息,进入unsafe.read():
@Override
public final void read() {final ChannelConfig config = config();final ChannelPipeline pipeline = pipeline();final ByteBufAllocator allocator = config.getAllocator();ByteBuf byteBuf = null;boolean close = false;// ...do {// ...// 1.分配ButeBuf缓冲对象byteBuf = allocHandle.allocate(allocator);// 2.将数据读取到ButeBuf缓冲对象allocHandle.lastBytesRead(doReadBytes(byteBuf));if (allocHandle.lastBytesRead() <= 0) {byteBuf.release();byteBuf = null;break;}readPending = false;// 3.向Pipeline传递可读消息pipeline.fireChannelRead(byteBuf);byteBuf = null;// 直到读取完所有消息内容} while (allocHandle.continueReading());// ...// 触发消息读取完成事件pipeline.fireChannelReadComplete();// ...
}
代码较为清晰,重点包含3个步骤:创建ByteBuf缓冲对象(Netty自定义的,而非NIO的ByteBuffer); 将消息读取到ButeBuf对象,向Pipeline触发可读事件(在Pipeline的Handler中传递并处理消息);其中,核心逻辑在于doReadBytes(byteBuf):
@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {// ...return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
javaChannel()是NIO的SocketChannel对象,继续跟进ByteBuf的writeBytes方法进入:
@Override
public int writeBytes(ScatteringByteChannel in, int length) throws IOException {//...int writtenBytes = setBytes(writerIndex, in, length);//...return writtenBytes;
}@Override
public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {try {return in.read(internalNioBuffer(index, length));} catch (ClosedChannelException ignored) {return -1;}
}
可以看到底层逻辑在于in.read(internalNioBuffer(index, length))
, 返回一个ByteBuffer对象,in此时为SocketChannel, 即本质是调用NIO通道的API将数据读取至缓冲区: SocketChannel.read(ByteBuffer).
2.3 响应消息
Netty中Pipeline的任何一个Handler中都可以发送响应消息,响应消息也会沿着Pipeline的流水线传递,并经过网卡传递出去:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {ctx.writeAndFlush("hello");
}
注意:需要在此Handler前添加StringEncoder编码器,将String类型转为ByteBuf类型,否则会抛出异常。因为NioSocketChannel的Unsafe对象也维持在了Pipeline的HeadContext对象中,所有的消息最终会经过Unsafe的write方法,而Unsafe只会处理ByteBuf类型消息,其他类型会抛出异常。
追踪ctx.writeAndFlush("hello")
进入invokeWriteAndFlush
方法:
void invokeWriteAndFlush(Object msg, ChannelPromise promise) {// ...invokeWrite0(msg, promise);invokeFlush0();// ...
}
依次调用invokeWrite0和invokeFlush0实现写操作和刷盘操作, 分别进入Unsafe对象的write和flush方法:
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {unsafe.write(msg, promise);
}public void flush(ChannelHandlerContext ctx) {unsafe.flush();
}
unsafe最终调用doWrite方法实现IO功能:
protected void doWrite(ChannelOutboundBuffer in) throws Exception {SocketChannel ch = javaChannel();int writeSpinCount = config().getWriteSpinCount();do {// ... ByteBuffer buffer = nioBuffers[0];int attemptedBytes = buffer.remaining();final int localWrittenBytes = ch.write(buffer);--writeSpinCount;// ... } while (writeSpinCount > 0);incompleteWrite(writeSpinCount < 0);
}
核心逻辑在与ch.write(buffer),其中ch和buffer分别是NIO的SocketChannel和ByteBuffer,
即Netty向客户端发送消息底层仍是借助NIO的API.
相关文章:
Netty系列-2 NioServerSocketChannel和NioSocketChannel介绍
背景 本文介绍Netty的通道组件NioServerSocketChannel和NioSocketChannel,从源码的角度介绍其实现原理。 1.NioServerSocketChannel Netty本质是对NIO的封装和增强,因此Netty框架中必然包含了对于ServerSocketChannel的构建、配置以及向选择器注册&am…...

智能客服的四大优势,提升企业服务效率
在这个信息化快速发展的时代,客户服务的重要性越来越凸显。传统的客服方式已经无法满足企业日益增长的服务需求,于是智能客服服务应运而生。智能客服服务不仅改变了企业与客户的互动方式,还提高了服务效率和客户满意度。本文将深入探讨智能客…...
AutoGPT开源项目解读
AutoGPT开源项目解读 (qq.com) AutoGPT旨在创建一个自动化的自我改进系统,能够自主执行和学习各种任务 项目基本信息 首先阅读项目的README.md,下述代理和智能体两个名词可互换 项目简介:一个创建和运行智能体的工具,这些智能体…...

Linux离线安装fontconfig
Linux离线下载yum包,安装字体库 一、下载安装包 以CentOS Linux release 7.9.2009下载fontconfig的rpm包的为例 http://mirror.centos.org/centos/7/按提示跳转历史库 找到对应版本的centos https://vault.centos.org/7.9.2009/os/x86_64/Packages/在Packages目…...

海山数据库(He3DB)+AI:(一)神经网络基础
文章目录 1 引言2 基本结构2.1 神经元2.2 模型结构 3 训练过程3.1 损失函数3.2 反向传播3.3 基于梯度的优化算法 4 总结 1 引言 神经网络可以被视为一个万能的拟合器,通过深层的隐藏层实现输入数据到输出结果的映射。神经网络的思想源于对大脑的模拟,在…...
CSS中选择器有哪些?(史上最全选择器)
CSS选择器是用来选择和应用样式到HTML元素上的工具。以下是所有主要的CSS选择器的详细分类和描述: 1. 基本选择器 通配符选择器 (*):选择所有元素。例如,* { color: red; } 会将所有元素的文字颜色设置为红色。元素选择器:选择指…...

本地部署 AI 智能体,Dify 搭建保姆级教程(下):知识库 RAG + API 调用,我捏了一个红楼解读大师
话接上篇: 本地部署 AI 智能体,Dify 搭建保姆级教程(上):工作流 Agent,把 AI 接入个人微信 相信大家已经在本地搭建好 Dify 了。 今日分享,继续介绍 Dify 的另外两项重要功能: 知…...
HarmonyOS应用开发者高级认证,Next版本发布后最新题库 - 答案纯享版
这篇文章是高级题库答案纯享版,只有需要选择的选项。如果需要查看所有选项,可以点击下方链接跳转。以考代学,还是推荐点击下方链接,查看完整的题库,边看边学习鸿蒙应用开发。此题库已更新完毕,笔者将不继续…...
基于PHP的文件包含介绍
引言:在实际开发过程中,经常会遇到部分模块功能需要重复使用的情况,比如数据库的增删改查,文件包含通过将需要重复使用的功能模块代码引入其他文件的内容,实现重用代码、分离配置等。然而,如果文件包含操作…...

K7系列FPGA多重启动(Multiboot)
Xilinx 家的 FPGA 支持多重启动功能(Multiboot),即可以从多个 bin 文件中进行选择性加载,从而实现对系统的动态更新,或系统功能的动态调整。 这一过程可以通过嵌入在 bit 文件里的 IPROG 命令实现上电后的自动加载。而…...

关于武汉芯景科技有限公司的RS232通信接口芯片XJ3243EEUI开发指南(兼容MAX3243EEUI)
一、芯片引脚介绍 1.芯片引脚 2.引脚描述 二、典型应用电路 三、功能描述 1.Transmitter 通过T1,T2可以将TTL电平转换为RS232电平 2.Receiver 通过R1,R2可以将RS232电平转换为TTL电平 3.工作模式控制 4.INVALID引脚...

TreeSize Free:你的免费磁盘空间管理专家
TreeSize Free是一款专为Windows用户设计的磁盘空间分析工具。它能够帮助用户快速识别并管理那些占用大量空间的文件夹和文件。 功能亮点 快速扫描:TreeSize Free能够迅速扫描整个磁盘卷,展示所有文件夹及其子文件夹的大小,甚至可以细化到单…...
python办公自动化:初识`python-docx`
1.1 什么是python-docx python-docx是一个用于在Python中创建和操作Word文档的库。它提供了一组简洁的API,让开发者可以轻松地生成、修改、和读取Microsoft Word (.docx)文件,而不需要安装Microsoft Office。这使得python-docx成为办公自动化、报告生成…...
LeetCode 算法:划分字母区间 c++
原题链接🔗:划分字母区间难度:中等⭐️⭐️ 题目 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。 注意,划分结果需要满足:将所有划分结果按顺序连接&#…...

PMP备考指南:策略、时间安排与心得分享
准备和时间安排,我是工作的时间把它顺便考了,大约花了一个月左右时间备考,前面的时间都在筹办婚礼,根本没时间,最后一个月都差点想放弃了,但想想还是冲一把就没有选择延考。 干货见下: ▌&…...
CentOS上通过frp实现HTTPS访问内网
要在CentOS上通过frp实现HTTPS访问内网,你需要按照以下步骤操作: 在外网服务器上安装frps(frp服务端)。 在外网服务器上配置frps,编辑配置文件frps.ini。 在frps服务器上启动frps服务。 在内网服务器上安装frpc&…...

短视频SDK解决方案,高效集成,助力商业变现
美摄科技,作为业界领先的多媒体技术服务商,其全面升级的短视频SDK解决方案,旨在为开发者与内容创作者提供一站式、高效能的创作工具,让每一个灵感都能瞬间转化为触动人心的视频作品。 【一站式解决方案,重塑短视频创作…...

C++系列-继承方式
继承方式 继承的语法继承方式:继承方式的特点继承方式的举例 继承可以减少重复的代码。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。基类父类,派生类子类,派生类是在继承了基类的部分成员基础…...

web前端之选项卡的实现、动态添加类名、动态移除类名、动态添加样式、激活、间距、tabBar
MENU 原生(一)原生(二)vue(一) 原生(一) 效果图 html 代码 <div class"card"><div class"tab_bar"><div class"item" onclick"handleTabBar(this)">tabBar1</div><div class"item" onclick&qu…...
sql 优化,提高查询速度
文章目录 一、前言二、建议2.1 使用索引2.2 避免使用select *2.3. 使用表连接代替子查询2.4. 优化WHERE子句,减少返回结果集的大小2.5 用union all代替union2.6 使用合适的聚合策略2.7 避免在WHERE子句中使用函数2.8 使用EXPLAIN分析查询2.9 小表驱动大表2.10 使用窗…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...