网络编程进化史:Netty Channel 的崭新篇章

上篇文章(Netty 入门 — ByteBuf,Netty 数据传输的载体),我们了解了 Netty 的数据是以 ByteBuf 为单位进行传输的,但是有了数据,你没有通道,数据是无法传输的,所以今天我们来熟悉 Netty 的第三个核心组件:Channel。ByteBuf 是数据,那 Channel 则是负责传输数据的通道,它是把握 Netty 通信的命门,没有它 Netty 是无法通信的。

Channel 简介

在 Java NIO 中我们知道,Channel,即通道,是用来传输数据的一条“管道”,它与 Buffer 相辅相成,在 Java NIO 中,我们只能从 Channel 读取数据到 Buffer 中,或者从 Buffer 读取数据到 Channel 中,如下:

在 Netty 中同样有一个 Channel,该 Channel 是 Netty 的核心概念之一,它是 Netty 网络 IO 操作的抽象,即 Netty 网络通信的主体,由它来负责对端进行网络通信、注册、数据操作等一切 IO 相关的操作,其主要功能包括:

  1. 网络 IO 的读写
  2. 客户端发起连接
  3. 关闭连接
  4. 网络连接的相关参数
  5. 绑定端口
  6. Netty 框架相关操作,如获取 Channel 相关联的 EventLoop、pipeline 等。

为什么要另起炉灶?

JDK 提供了一个 Channel,为什么 Netty 还要另起炉灶自己实现一个呢?我认为主要原因有如下几个:

  1. 原生的 Channel 功能太少,不满足 Netty 的要求。
  2. 原生的 ServerSocketChannel 和 SocketChannel 是一个 SPI 接口,具体的实现由虚拟厂商来实现,直接通过原生 ServerSocketChannel 和 SocketChannel 来实现及满足 Netty 的要求,其工作量不亚于重新开发一个。
  3. Netty 的 Channel 需要符合 Netty 的整体架构设计,他需要和 Netty 的整体架构耦合在一起,比如 IO 模型、基于元数据描述配置化的TCP参数等等,原生的 Channel 都不支持。
  4. 自定义的 Channel,灵活性更高,功能更加全面。

Channel 原理

Channel 的核心原理如下图:

  1. 客户端与服务端成功建立连接后,服务端会为该连接创建一个 Channel。
  2. Channel 从 EventLoopGroup 中获取一个 EventLoop,Channel 注册到该 EventLoop 中,从此 Channel 就与该 EventLoop 绑定在一起了,在 Channel 整个生命周期内都只会与该 EventLoop 绑定在一起。
  3. 客户端发起的 IO 操作,在 Channel 中都将产生相对应的 Event,触发与该 Channel 绑定的 EventLoop 进行处理
  4. 如果是读写事件,执行线程调度 ChannelPipeline 来处理业务逻辑。ChannelPipeline 只负责 Handler 的编排,真正执行任务的是各个具体的 ChannelHandler。

Channel 的状态转换

Channel 从创建到消亡,他有四种状态,分别是:

  1. 打开状态(Open)
    1. Channel 处于打开状态时,表示它已经被创建,但尚未绑定到任何地址或连接到远端服务器。
  2. 活动状态(Active)
    1. Channel 处于活动状态时,表示它已经成功绑定到本地地址或连接到远端服务器。
    2. 这个时候可以调用 writeAndFlush() 向对方发送数据了。
  3. 非活动状态(Inactive)
    1. Channel 处于非活动状态时,表示它已经处于活动状态,但连接已经断开或由于其他原因不可用。
    2. 当连接被关闭或出现错误时,Channel 会进入非活动状态。
    3. 无法进行读取或写入操作,但可重新激活 Channel。
  4. 关闭状态(Closed):
    1. Channel 处于关闭状态时,表示它已经完全关闭,无法再进行任何操作。

状态流转如下:

Netty 提供了四个方法来判断 Channel 的状态:

  • isOpen():检查 Channel 是否为 open 状态。
  • isRegistered():检查 Channel 是否为 registered 状态。
  • isActive():检查 Channel 是否为 active 状态。
  • isWritable():这个方法有误导性,它并不是判断当前 Channel 是否可写,实际上它是用来检测当前 Channel 的写操作是否可以立刻被 IO 线程处理,当该方法返回 false 时,任何写请求都会被阻塞,知道 I/O 线程有能力能处理这些请求。

各个状态以及他们对应的操作如下表格:

状态isOpen()isActive()close()writeAndFlush()读操作写操作
打开(Open)truefalsetruefalsetruetrue
活动(Active)truetruetruetruetruetrue
非活动(Inactive)truefalsetruefalsefalsefalse
关闭(Closed)falsefalsefalsefalsefalsefalse

Channel 的 API

Channel 常用的 API 非常多,如下图(部分):

方法虽然多,但是总体大致分为如下几类:

类 getter API

这里方法主要用于获取 Channel 相关的属性,如绑定地址,相关配置等等

  • SocketAddress localAddress():返回与 Channel 绑定的本地地址
  • SocketAddress remoteAddress():返回与 Channel 绑定的远端地址
  • ChannelConfig config():返回一个 ChannelConfig 对象,通过这个对象可以配置Channel相关的参数
  • ChannelMetadata metadata():返回一个 ChannelMetadata 对象,ChannelMetadata 可以查询 Channel 实现是否支持某种操作,目前它还只要一个方法 hasDisconnect(),用来判断 Channel 实现是否支持 disconnect() 操作。
  • Channel parent():返回 Channel 的 parent Channel。SocketChannel 返回的是相对一个的 ServerSocketChannel,而 ServerSocketChannel 则返回 null。为什么 SocketChannel 返回的是 ServerSocketChannel 呢?因为所有的 SocketChannel (客户端发起连接)都是由 ServerSocketChannel 接受连接而创建的,所以 SocketChannel 的 parent() 返回的就是对应的 ServerSocketChannel 。
  • EventLoop eventLoop():返回 Channel 注册的 EventLoop。
  • ChannelPipeline pipeline():返回与 Channel 关联的 ChannelPipeline。
  • ByteBufAllocator alloc():返回与 Channel 关联的 ByteBufAllocator 对象。
  • Unsafe unsafe():返回 Channel 的 Unsafe 对象。Unsafe 是 Channel 的内部类,只供 Channel 内部使用。
Future 相关 API

Netty 中的所有 IO 操作都是异步的,这就意味着任何的 IO 调用都将立刻返回,但是并不能保证所有的操作都在调用结束后就完成了,而且我们也不知道 IO 操作执行的结果。Netty 在完成 IO 调用后会返回一个 Future 对象,该对象就是 Channel 异步 IO 的结果。Channel 提供了我们操作这些 Future 的方法:

  • ChannelFuture closeFuture():当 Channel 关闭时返回一个 ChannelFuture,我们可以通过该方法来来对 Channel 关闭后做一些处理。
  • ChannelPromise voidPromise():返回一个 ChannelPromise 实例对象。
  • ChannelPromise newPromise():返回一个 ChannelPromise。
  • ChannelProgressivePromise newProgressivePromise():返回一个新的 ChannelProgressivePromise 实例对象。
  • ChannelFuture newSucceededFuture():创建一个新的 ChannelFuture,并将其标注为 succeed 。
  • ChannelFuture newFailedFuture(Throwable cause):创建一个新的 ChannelFuture,并将其标注为 failed。

ChannelFutureChannelPromise 是 Netty 提供的两个特殊 Future,利用他们我们能够在 Netty 完成一些异步操作的处理。

判断状态 API

Channel 提供了四个 isXxx 方法用来判断 Channel 的状态:

  • boolean isOpen():判断 Channel 是否 opened
  • boolean isRegistered():判断 Channel 是否 registered
  • boolean isActive():判断 Channel 是否 active
  • boolean isWritable():判断 Channel 是否可以立刻处理 IO 事件
事件触发类方法

这些方法都会触发 IO 事件,他们都会通过 ChannelPipeline 传播然后被 ChannelHandler 处理。

  • ChannelFuture bind(SocketAddress localAddress):服务端绑定本地端口,开始监听客户端的连接请求。
  • ChannelFuture connect(SocketAddress remoteAddress):客户端向服务端发起连接请求。
  • ChannelFuture disconnect():断开连接,但是需要注意的是该方法不会释放资源,它还可以再次通过 connect() 再次与服务端建立连接。
  • ChannelFuture close():关闭通道,释放资源。
  • Channel read():读取通道
  • ChannelFuture write(Object msg):向 Channel 中写入数据,该方法并不会将数据真实地写入通道,它只将数据写入到了通断缓存区,我们需要调用 flush() 将缓存区的数据刷入到 Channel 中。
  • Channel flush():将数据刷写到 Channel 中。
  • ChannelFuture writeAndFlush(Object msg):相当于调用了 write()flush()

Channel 的配置

ChannelConfig

在 Netty 中,每个 Channel 都有与之相对应的 ChannelConfig , 可以通过调用 config() 来获取。ChannelConfig 是一个接口,每个特定的 Channel 都有具体的 ChannelConfig 实现类,例如:

  • NioSocketChannel 的对应的配置类为 NioSocketChannelConfig。
  • NioServerSocketChannel 的对应的配置类为 NioServerSocketChannelConfig。

整体的 UML 图如下:

具体的实现我们这篇文章不需要关系,我们需要关注的是它提供了哪些 Config。

  • ChannelConfig 提供通用型配置

    • ChannelOption.CONNECT_TIMEOUT_MILLIS:连接超时时间,默认值30000毫秒即30秒。
    • ChannelOption.WRITE_SPIN_COUNT:写操作的最大循环数,即一次写事件处理期间最多调用 write() 的次数。它有点儿像 Java 中的自旋锁。引入该参数的主要木的是为了避免一个 Channel 写入大量数据,对其他网络通道的读写处理带来延时。
    • ChannelOption.ALLOCATOR:设置内存分配器。
    • ChannelOption.RCVBUF_ALLOCATOR:对读事件设置内存分配器。
    • ChannelOption.AUTO_READ:配置是否自动触发 read() ,默认为 True,程序不需要显示调用 read()
    • ChannelOption.AUTO_CLOSE:配置当写事件失败时,是否自动关闭 Channel,默认为 True。
    • ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK:设置写缓存区的高水位线。如果写缓存区中的数据超过该值, Channel#isWritable() 方法将返回 false。
    • ChannelOption.WRITE_BUFFER_LOW_WATER_MARK:设置写缓存区的低水位线。如果写缓存区的数据超过高水位线后,通道将变得不可写,等写缓存数据降低到低水位线后通道恢复可写状态(Channel#isWritable()将再次返回true)。
    • ChannelOption.MESSAGE_SIZE_ESTIMATOR:设置用于检测通道消息大小的检测器:MessageSizeEstimator。

    这里引入了两个概念:高水位线和低水位线,这两个概念我们在讲缓冲区的时候再细说。

  • NioSocketChannelConfig

    NioSocketChannelConfig 在 ChannelConfig 的基础上增加了如下几个配置:

    • ChannelOption.SO_KEEPALIVE: 连接保持,默认为 False,我们可以将这个参数视为 TCP 的心跳机制。
    • ChannelOption.SO_REUSEADDR:地址复用,默认值False。
    • ChannelOption.SO_LINGER:关闭 Socket 的延迟时间,默认值为 -1,表示禁用该功能
    • ChannelOption.TCP_NODELAY:立即发送数据,默认值为 Ture。该值其实是设置 Nagle 算法的启用。关于 Nagle 算法我们后面再细说。
    • ChannelOption.SO_RCVBUF:TCP 数据接收缓冲区大小。该缓冲区即 TCP 接收滑动窗口。
    • ChannelOption.SO_SNDBUF:TCP 数据发送缓冲区大小。该缓冲区即 TCP 发送滑动窗口。
    • ChannelOption.IP_TOS:IP 参数,设置 IP 头部的 Type-of-Service 字段,用于描述 IP 包的优先级和 QoS 选项。
    • ChannelOption.ALLOW_HALF_CLOSURE:一个连接的远端关闭时本地端是否关闭,默认值为False。
  • NioServerSocketChannelConfig

    • ChannelOption.SO_REUSEADDR:地址复用,默认值False。
    • ChannelOption.SO_RCVBUF:TCP 数据接收缓冲区大小。该缓冲区即 TCP 接收滑动窗口。
    • ChannelOption.SO_BACKLOG:服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。

从上面我们可以看到 ChannelConfig 提供的都是一些通用型的配置,而 NioSocketChannelConfig 和 NioServerSocketChannelConfig 提供的基本上都是 Socket 相关的配置参数,每个都与 java.net.StandardSocketOptions 定义的标准 TCP 参数一一对应。

由于这个是入门篇,所以这里大明哥就不再扩展阐述了,对这些配置参数更加详细的说明,大明哥后面会专门有文章类分析的,这里我们只需要了解 ChannelConfig 是我们配置 Channel 通道相关参数的服务类即可。

Channel 的使用方法

看完上面部分,大明哥相信你对 Channel 有了一个基本的了解。其实Channel 的 API 没啥好演示的,因为这些 API 都不是单独使用的,需要一些其他的组件来配合,但是咱们还是要有仪式感对吧,就写一个简单的 demo 来看看 Channel 的状态变化以及简单感受下异步的风采。

  • 服务端
public static void main(String[] args) throws InterruptedException {Channel channel = new ServerBootstrap().group(new NioEventLoopGroup()).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new LoggingHandler());}}).bind(8081).channel();System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive());System.out.println("eventLoop():" + channel.eventLoop());System.out.println("pipeline():" + channel.pipeline());TimeUnit.SECONDS.sleep(5);System.out.println("=============================");System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive());
}
  • 客户端
public static void main(String[] args) throws InterruptedException {Channel channel = new Bootstrap().group(new NioEventLoopGroup()).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LoggingHandler());}}).connect("127.0.0.1",8081).channel();System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive());System.out.println("eventLoop():" + channel.eventLoop());System.out.println("pipeline():" + channel.pipeline());TimeUnit.SECONDS.sleep(5);System.out.println("=============================");System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive());
}
  • 运行结果
// server
isOpen:true;;;isRegistered:false;;;isActive:false
eventLoop():io.netty.channel.nio.NioEventLoop@2f333739
pipeline():DefaultChannelPipeline{(ServerBootstrap$1#0 = io.netty.bootstrap.ServerBootstrap$1)}
=============================
isOpen:true;;;isRegistered:true;;;isActive:true//client
isOpen:true;;;isRegistered:false;;;isActive:false
eventLoop():io.netty.channel.nio.NioEventLoop@6ed3ef1
pipeline():DefaultChannelPipeline{(ChannelTestClient$1#0 = com.sike.netty.rumen.ChannelTestClient$1)}
=============================
isOpen:true;;;isRegistered:true;;;isActive:true

从结果中可以看出,无论是服务端还是客户端,Channel 都是异步的,当服务端调用 bind() 方法后返回的 Channel,它仅仅只完成了新建,注册以及绑定工作都没有完成,等待 5 秒后,我们再看其状态,则都是 true 了。

代码地址:http://m6z.cn/5O6hON

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

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

相关文章

【Gan教程 】 什么是变分自动编码器VAE?

名词解释&#xff1a;Variational Autoencoder&#xff08;VAE&#xff09; 一、说明 为什么深度学习研究人员和概率机器学习人员在讨论变分自动编码器时会感到困惑&#xff1f;什么是变分自动编码器&#xff1f;为什么围绕这个术语存在不合理的混淆&#xff1f;本文从两个角度…

高等数学啃书汇总重难点(七)微分方程

同济高数上册的最后一章&#xff0c;总的来说&#xff0c;这篇章内容依旧是偏记忆为主&#xff0c;说难不难说简单不简单&#xff1a; 简单的是题型比较死&#xff0c;基本上就是记公式&#xff0c;不会出现不定积分一般花样繁多的情况&#xff1b;然而也就是背公式并不是想的…

基于Java的在线教育网站管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

python输出与数据类型

目标 1、使用print输出内容 2、熟悉字符串类型 3、熟悉数字类型 4、熟悉数字与字符串操作 输出 print可控制输出内容也可配合、-、*、/进行运算&#xff0c;和整数型配合可进行运算和字符型配合有不同效果&#xff0c;如为拼接&#xff0c;*为多次输出注&#xff1a;整数型如&…

flutter开发实战-hero实现图片预览功能extend_image

flutter开发实战-hero实现图片预览功能extend_image 在开发中&#xff0c;经常遇到需要图片预览&#xff0c;当feed中点击一个图片&#xff0c;开启预览&#xff0c;多个图片可以左右切换swiper&#xff0c;双击图片及手势进行缩放功能。 这个主要实现使用extend_image插件。在…

【软件测试】自动化测试selenium

目录 一、什么是自动化测试 二、Selenium介绍 1、Selenium是什么 2、Selenium的原理 三、了解Selenium的常用API 1、webDriver API 1.1、元素定位 1.1.1、CSS选择器 1.1.2、Xpath元素定位 1.1.3、面试题 1.2、操作测试对象 1.3、添加等待 1.4、打印信息 1.5、浏…

kvm webvirtcloud 如何添加直通物理机的 USB 启动U盘

第一步&#xff1a;查看USB设备ID 在物理机上输入 lsusb 命令 rootubuntu:/media/usb1# lsusb Bus 002 Device 002: ID 0781:5581 SanDisk Corp. Ultra Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 004: ID 0424:2514 Microchip Technolo…

UE4/5 批量进行贴图Texture压缩、修改饱和度

该插件下载地址&#xff1a; &#x1f35e;正在为您运送作品详情https://mbd.pub/o/bread/ZZWYmpxw 适用于 UE4 4.25/4.26/4.27 UE5 以上版本 在Edit - Plugins中分别开启 插件 Python Editor Script Plugin 插件 Editor Scripting Utilites 如果会python代码&#xff0c;…

分享从零开始学习网络设备配置--任务4.2 使用IPv6静态及默认路由实现网络连通

任务描述 某公司利用IPv6技术搭建网络&#xff0c;公司3个部门所有PC机连接在同一交换机上&#xff0c;PC1代表行政部划分到VLAN10中&#xff0c;PC2代表财务部划分到VLAN20中&#xff0c;PC3代表销售部划分到VLAN30中&#xff0c;R1代表公司出口路由器&#xff0c;R2模拟Inter…

统计学习方法 支持向量机(下)

文章目录 统计学习方法 支持向量机&#xff08;下&#xff09;非线性支持向量机与和核函数核技巧正定核常用核函数非线性 SVM 序列最小最优化算法两个变量二次规划的求解方法变量的选择方法SMO 算法 统计学习方法 支持向量机&#xff08;下&#xff09; 学习李航的《统计学习方…

操作系统:计算机系统概述

一战成硕 1.1 手工操作阶段1.2 批处理阶段1.3 分时操作系统1.4 实时操作系统1.5 中断和异常的概念1.6 系统调用 1.1 手工操作阶段 1.2 批处理阶段 单道批处理系统 自动性 顺序性 单道性多道批处理系统 多道 宏观上并行 微观上串行 优点&#xff1a;资源利用率高&#xff0c;多…

Postman —— 配置环境变量

PostMan是一套比较方便的接口测试工具&#xff0c;但我们在使用过程中&#xff0c;可能会出现创建了API请求&#xff0c;但API的URL会随着服务器IP地址的变化而改变。 这样的情况下&#xff0c;如果每一个API都重新修改URL的话那将是非常的麻烦&#xff0c;所以PostMan中也提供…

hive窗口函数记录

记录工作中和学习中的窗口函数&#xff0c;方便以后使用&#xff0c;本记持续更新和完善&#xff0c;版本&#xff1a;231019 文章目录 1.什么是窗口函数2.窗口函数的表达式3.窗口函数的类型1&#xff09; 排名函数2&#xff09; 聚合函数3&#xff09; 跨行取值函数 4.[frame…

如何实现前端实时通信(WebSocket、Socket.io等)?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测(多指标,多图)

回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现IWOA-LSTM改进鲸鱼算法算法优化长短期记忆神经网络的数据回归预测&#xff08;多指标&#xff0c;多图&#…

【图解数据结构】手把手教你如何实现顺序表(超详细)

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板、汇编语言 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️线性表1.1 &#x1f514;线性表的定义1.2 &#x1f514;线性表的存储结构 二. ⛳️…

Premiere Pro(Pr)2023软件下载及安装教程

目录 一.简介 二.安装步骤 软件&#xff1a;Pr版本&#xff1a;2023语言&#xff1a;简体中文大小&#xff1a;8.30G安装环境&#xff1a;Win11/Win10&#xff08;1809版本以上&#xff09;硬件要求&#xff1a;CPU2.6GHz 内存8G(或更高&#xff0c;不支持7代以下CPU&#xf…

【微服务保护】初识 Sentinel —— 探索微服务雪崩问题的解决方案,Sentinel 的安装部署以及将 Sentinel 集成到微服务项目

文章目录 前言一、雪崩问题及其解决方案1.1 什么是雪崩问题1.2 雪崩问题的原因1.3 解决雪崩问题的方法1.4 总结 二、初识 Sentinel 框架2.1 什么是 Sentinel2.2 Sentinel 和 Hystrix 的对比 三、Sentinel 的安装部署四、集成 Sentinel 到微服务 前言 微服务架构在现代软件开发…

如何利用数字化转型升级,重塑企业核心竞争力?

工程机械行业是一个周期性明显的行业&#xff0c;企业经营受到宏观经济与国家基础设施建设的影响较大&#xff0c;例如企业经济上行时&#xff0c;加大投资扩大生产规模&#xff0c;以满足市场需求的增长&#xff0c;当经济下行时&#xff0c;企业可能面临减产和裁员等问题&…

Ubuntu 安装 npm 和 node

前言 最近学习VUE&#xff0c;在ubuntu 2204 上配置开发环境&#xff0c;涉及到npm node nodejs vue-Cli脚手架等内容&#xff0c;做以记录。 一、node nodejs npm nvm 区别 &#xff1f; node 是框架&#xff0c;类似python的解释器。nodejs 是编程语言&#xff0c;是js语言的…