Socket编程权威指南(二)完美掌握TCP流式协议及Socket编程的recv()和send()
在上一篇文章中,我们学习了Socket编程的基础知识,包括创建Socket、绑定地址、监听连接、接收连接等操作。然而,真正的套接字编程远不止于此。本文将重点介绍TCP 流式协议,什么是粘包问题?如何解决粘包问题 ?以及recv()和send()这两个函数详细介绍,它们分别用于读取和发送数据,是网络编程中最为关键的环节。我们将详细剖析函数原型、参数含义,并通过实例代码展示具体用法,助你彻底掌握数据传输的精髓。
想详细了解socket 编程基础知识的同学,请前往查阅Socket编程权威指南(一)打通网络通信的任督二脉。
我们先来回顾一下 socket 编程的基本流程:
接下来,开始我们今天的正题。
一、TCP 流式协议
1、TCP 流式协议介绍
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
以下是 TCP 作为流式协议的一些关键特性和详细说明:
-
面向连接:
- TCP 需要在数据传输开始前建立一个连接。通过三次握手过程,客户端和服务器交换初始序列号,建立稳定的连接。
-
字节流:
- 与数据报(如 UDP)不同,TCP 将数据视为字节流,而不是独立的数据包。这意味着 TCP 不保留数据包边界,应用程序需要自己处理数据的边界。
-
可靠性:
- TCP 保证数据的可靠传输。它使用序列号和确认应答机制来确保数据按顺序、完整地到达目的地。
-
有序性:
- TCP 保证数据包的有序传输。如果数据包丢失或乱序,TCP 会重传丢失的数据包并重新排序。
-
拥塞控制:
- TCP 内置拥塞控制机制,通过调整数据的发送速率来避免网络拥塞。这是通过算法如慢启动、拥塞避免、快重传和快恢复实现的。
-
流量控制:
- TCP 使用滑动窗口机制进行流量控制,接收方根据自己的接收能力告知发送方可以发送多少数据。
-
全双工通信:
- TCP 允许双向通信,即客户端和服务器可以独立地发送和接收数据。
-
数据传输:
- TCP 通过套接字接口提供数据传输服务。应用程序可以使用
read()
、write()
或专门的套接字系统调用如recv()
和send()
进行数据传输。
- TCP 通过套接字接口提供数据传输服务。应用程序可以使用
-
连接管理:
- TCP 连接的建立和终止通过三次握手和四次挥手过程管理。这些过程确保了连接的稳定建立和优雅关闭。
-
超时和重传:
- 如果发送的数据没有得到及时确认,TCP 会自动重传数据,并且随着时间的推移,重传间隔会指数增长。
-
保活和死锁检测:
- TCP 提供保活(keepalive)机制,以检测死连接。如果连接长时间没有活动,TCP 可以自动关闭连接。
-
适用场景:
- TCP 适用于需要可靠传输的应用,如 Web 浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)和远程登录(SSH)。
-
限制:
- 由于 TCP 是面向连接的协议,它在某些场景下可能不如 UDP 高效,特别是在实时通信或广播通信中。
-
编程模型:
- TCP 编程通常涉及创建套接字、绑定、监听、连接、数据传输和关闭连接等步骤。
TCP 作为流式协议,其设计目标是提供可靠的数据传输服务。它通过多种机制确保数据的正确、有序传输,并通过拥塞控制和流量控制适应不同的网络条件。
也就是说,内核实现的 TCP 协议将保证将一个 TCP 包发送给另一端。
如上图:
拥塞控制是确保可靠数据传输协议有效运作的关键组成部分,因此,在TCP中,发送缓冲区和接收缓冲区成为了必不可少的元素。
在标准的Linux操作系统中,TCP的发送缓冲区和接收缓冲区默认的大小通常被设置为208KB。这意味着,如果进程A没有及时从其接收缓冲区中提取数据,那么传入的数据将继续在缓冲区内积累,直至达到其容量上限。
2、TCP 流式协议引发“粘包问题”
在网络上传输的数据本质上是二进制格式的,这意味着接收缓冲区里存放的内容同样是以二进制数据的形式存在。
数据根据其到达的顺序被依次放入接收缓冲区,这就可能导致所谓的“粘包”现象。
“粘包”这个词更像是一个方便描述的概念,实际上它并非一个正式的术语。
这个说法类似于描述从水龙头流出的水“粘”在一起,这当然不是字面上的粘连,而是用来形象地表达数据包在缓冲区中连续存放的情况。
简而言之,流式协议引发的一个问题是,我们如何识别接收缓冲区中的数据分别属于哪个TCP数据包?
以一个具体示例来说明,如果客户端向服务器发送了三个TCP数据包,分别包含内容QA、QB、QC,每个内容代表一个不同的请求。那么在服务器端的接收缓冲区(RecvBuffer)中,这些数据可能会连续排列成一串数据QAQBQC。服务器需要将这串连续的数据正确地拆分成三个独立的数据包,并对每个数据包进行相应的处理。
3、如何解决“粘包”问题?
通常,为了解决如何从接收缓冲区中区分不同TCP数据包的问题,我们会采用 包头+包体 的方式 。
正如上图所演示:在每个数据包前添加一个固定长度的包头,并在包头中记录数据包的总长度。包体部分则承载实际要传输的数据。
处理接收缓冲区数据的步骤如下:
-
Step 1: 首先从接收缓冲区读取固定大小的包头(例如20字节)。
-
Step 2: 解析包头,从中获取数据包的总长度,这里假设包头中包含的数据长度字段名为
Header.Length
。 -
Step 3 : 根据
Header.Length
的值,确定接下来需要从接收缓冲区读取的数据量。例如,如果包头之后的数据总长度为1048字节,减去已读取的20字节包头,还需读取1028字节的数据。
通过这种方式,即使数据在接收缓冲区中是连续存放的,服务器也能够根据每个数据包的包头信息,正确地拆分和识别出独立的数据包。
二、recv()函数详解
recv()
函数是专用于
scoket 上的
read 操作,用于接收 TCP 套接字数据的系统调用。它在服务器端或客户端程序中用来从连接的对端读取数据,本质上是从接收缓冲区读取数据 ,该系统调用将返回实际读取的字节数,其值可能会小于传入的
length 参数。
以下是 recv()
函数的详细说明:
(1)、函数原型
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
(2)、参数
-
sockfd
:套接字描述符,表示要从中读取数据的 TCP 套接字。 -
buf
:指向一个缓冲区的指针,用于存储接收到的数据。 -
len
:缓冲区的大小,即buf
可以存储的最大字节数。 -
flags:用来修改recv()行为的选项。常用的值包括:
-
0
:正常接收数据。 -
MSG_PEEK
:窥视接收的数据,不从接收缓冲区中移除数据。 -
MSG_WAITALL
:等待直到接收到len
个字节的数据,或者出现错误。
-
(3)、返回值:
-
成功时,返回接收到的字节数,该值通常小于或等于
len
。 -
失败时,返回
-1
,并设置全局变量errno
以指示错误类型。
(4)、错误处理:
EAGAIN
或EWOULDBLOCK
:在非阻塞模式下,如果操作会阻塞,则返回此错误。ECONNRESET
:远程主机强行关闭了一个现有的连接。ENOTCONN
:套接字未连接,即未调用connect()
或accept()
。ENOTSOCK
:sockfd
不是一个套接字。
(5)、使用场景:
- 主要用于 TCP 套接字上的数据接收。对于 UDP 套接字,通常使用
recvfrom()
函数。
(6)、阻塞和非阻塞行为:
- 默认情况下,
recv()
是阻塞的,它会等待直到至少接收到一个字节的数据。 - 对于非阻塞套接字,如果接收缓冲区中没有数据,
recv()
会立即返回,返回值为 0。
(7)、与 read()
的区别:
-
read()
是一个通用的系统调用,用于读取文件描述符,而recv()
专门用于套接字。 -
recv()
可以处理套接字选项和状态,而read()
不能。
(8)、示例代码:
接下来我们通过一个简单的客户端示例,演示如何使用recv()接收数据:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");servaddr.sin_port = htons(8000);connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));char buffer[1024];ssize_t nbytes = recv(sockfd, buffer, sizeof(buffer), 0);buffer[nbytes] = '\0';std::cout << "Received: " << buffer << std::endl;close(sockfd);return 0;
}
在这个例子中,客户端连接到本机的8000端口,然后使用recv()接收数据并输出到控制台。你可以自行运行并在服务器端发送数据,以验证recv()的效果。
(9)、注意事项:
- 接收到的数据可能小于请求的长度,因此应用程序应该总是检查返回值。
recv()
可以与select()
或poll()
结合使用,以实现更复杂的非阻塞操作。
recv()
函数是 TCP 网络编程中接收数据的基本工具。正确使用它需要考虑套接字的状态、缓冲区的大小以及可能的错误情况。
三、send()函数解析
send()
函数专用于
scoket 上的
write
操作,在套接字上发送数据的系统调用。
本质上是向发送缓冲区中写入数据,内核在发送 TCP 数据时,通常会使用 Nagle 算法把多个小的数据包合并成一个发送给另一端,以提高效率。
它是 TCP 网络编程中发送数据的基本工具之一。
以下是 send()
函数的详细解析:
(1)、函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
(2)、参数
sockfd
:套接字描述符,表示要从中发送数据的套接字。buf
:指向要发送数据缓冲区的指针。len
:要发送数据的长度,单位为字节。- flags:用来修改发送行为的选项。常用的值包括:
0
:正常发送数据。MSG_DONTWAIT
:使send()
调用非阻塞。MSG_MORE
:暗示更多的数据要发送,可以用于优化传输效率。
(3)、返回值
- 成功时,返回已发送的字节数,该值通常小于或等于
len
。 - 失败时,返回
-1
,并设置全局变量errno
以指示错误类型。
(4)、错误处理
-
EAGAIN
或EWOULDBLOCK
:在非阻塞模式下,如果操作会阻塞,则返回此错误。 -
ECONNRESET
:远程主机强行关闭了连接。 -
ENOTCONN
:套接字未连接到任何远程主机。 -
ENOTSOCK
:sockfd
不是一个套接字。
(5)、使用场景
-
主要用于已连接的 TCP 套接字上的数据发送。对于 UDP 套接字,通常使用
sendto()
函数。
(6)、阻塞和非阻塞行为
-
默认情况下,
send()
是阻塞的,它会等待直到数据被发送。 -
对于非阻塞套接字,如果数据不能立即发送,
send()
会返回-1
并设置errno
为EAGAIN
或EWOULDBLOCK
。
(7)、与 write()
的区别
-
write()
是一个通用的系统调用,用于写文件描述符,而send()
专门用于套接字。 -
send()
可以处理套接字选项和状态,而write()
不能。
(8)、示例代码
const char* message = "Hello, World!";
ssize_t nbytes = send(sockfd, message, strlen(message), 0);
if (nbytes < 0) {perror("send failed");exit(EXIT_FAILURE);
}
(9)、注意事项
- 发送的数据可能因为网络或其他原因没有立即发送出去,
send()
只是将数据交给了内核。 - 对于非阻塞套接字,可以使用
select()
或poll()
来等待套接字变得可写。
send()
函数是 TCP 网络编程中发送数据的基础。正确使用它需要考虑套接字的状态、数据的长度以及可能的错误情况。此外,send()
与 recv()
配合使用,可以实现完整的数据发送和接收流程。
需要注意的是,recv()和send()都可能出现中断或部分接收/发送的情况,因此在实际应用中需要循环调用,确保所有数据都被完整传输。此外,它们还有一些特殊的flags可以控制阻塞行为等,具体用法视实际情况而定。
四、权威之作待续
恭喜你已经完整学习了TCP 流式协议,什么是粘包?粘包如何处理?以及recv()和send()函数,这标志着你正在向Socket编程高手的行列迈进。当然,Socket编程远不止如此,还有select()、poll()等I/O复用函数、Unix域Socket编程、原始套接字编程等更高阶的技巧等待你去发掘。我们期待在不久的将来,为你呈现一部Socket编程的不朽权威之作。敬请期待!
相关文章:
Socket编程权威指南(二)完美掌握TCP流式协议及Socket编程的recv()和send()
在上一篇文章中,我们学习了Socket编程的基础知识,包括创建Socket、绑定地址、监听连接、接收连接等操作。然而,真正的套接字编程远不止于此。本文将重点介绍TCP 流式协议,什么是粘包问题?如何解决粘包问题 ?…...
当C++的static遇上了继承
比如我们想要统计下当前类被实例化了多少次,我们通常会这么写 class A { public:A() { Count_; }~A() { Count_--; }int GetCount() { return Count_; }private:static int Count_; };class B { public:B() { Count_; }~B() { Count_--; }int GetCount() { return …...
Three.js中的Raycasting技术:实现3D场景交互事件的Raycaster详解
前言 在Web开发中,Three.js是一个极为强大的库,它让开发者能够轻松地在浏览器中创建和展示3D图形。随着3D技术在网页设计、游戏开发、数据可视化等领域的广泛应用,用户与3D场景的交互变得日益重要。而要实现这种交互,一个核心的技…...
5 分钟内构建一个简单的基于 Python 的 GAN
文章目录 一、说明二、代码三、训练四、后记 一、说明 生成对抗网络(GAN)因其能力而在学术界引起轩然大波。机器能够创作出新颖、富有灵感的作品,这让每个人都感到敬畏和恐惧。因此,人们开始好奇,如何构建一个这样的网…...
智能硬件产品中常用的参数存储和管理方案
一、有哪些参数需要管理? 在智能硬件产品中,一般有三类数据需要存储并管理: 1. 系统设置数据 系统设置数据是指产品自身正常工作所依赖的一些参数。 这类数据的特点:只能在生产过程中修改,出厂后用户无权限修改。 比如:产品SN、产品密钥/token/license、传感器校准值…...
SwiftUI中Mask修饰符的理解与使用
Mask是一种用于控制图形元素可见性的图形技术,使用给定视图的alpha通道掩码该视图。在SwiftUI中,它类似于创建一个只显示视图的特定部分的模板。 Mask修饰符的定义: func mask<Mask>(alignment: Alignment .center,ViewBuilder _ ma…...
全光网络与传统网络架构的对比分析
随着信息技术的飞速发展,网络已经成为我们日常生活中不可或缺的一部分。在这个信息爆炸的时代,全光网络和传统网络架构作为两种主流的网络技术,各有其特点和适用范围。本文将对这两种网络架构进行详细的对比分析,帮助读者更好地了…...
stack overflow复现
当你在内存的栈中,存放了太多元素,就有可能在造成 stack overflow这个问题。 今天看看如何复现这个问题。 下图,是我写的程序,不断的创造1KB的栈,来看看执行了多少次,无限循环。 最后结果是7929kB时, 发…...
mybatis使用笔记
文章目录 打印sql日志mybatis-config.xml方式application.yml里面配置配置类配置方式 其他扫描方式官网文档 mybatis用了那么久,实际一直不明白,做个笔记吧。 打印sql日志 实测,mybatis-config.xml方式好用(记得注掉yml里的相关配置) mybat…...
学习笔记——路由网络基础——路由概述
一、路由概述 1、路由定义与作用 路由(routing)是指导报文转发路径信息,通过路由可以确认转发IP报文的路径。 路由:是指路由器从一个接口上收到数据包,根据数据包的目的地址进行定向并转发到另一个接口的过程。 路由(routing)的定义是指分…...
在量子计算时代,大数据技术将面临哪些挑战和机遇?
在量子计算时代,大数据技术将面临以下挑战和机遇: 挑战: 处理速度:量子计算机具有极高的计算速度,大数据技术需要适应和充分利用这种速度。现有的大数据算法和架构可能需要重新设计和优化,以充分发挥量子计…...
怎么换自己手机的ip地址
在互联网时代,IP地址已经成为了我们数字身份的一部分。无论是浏览网页、下载文件还是进行在线交流,我们的IP地址都在默默发挥着作用。然而,有时出于安全或隐私保护的考虑,我们可能需要更换手机的IP地址。那么,如何轻松…...
搭建 Langchain-Chatchat 详细过程
前言 本文参考官网和其他多方教程,将搭建 Langchain-Chatchat 的详细步骤进行了整理,供大家参考。 我的硬件 4090 显卡win10 专业版本 搭建环境使用 chatglm2-6b 模型 1. 创建虚拟环境 chatchat ,python 3.9 以上 conda create -n chat…...
C++期末复习
目录 1.基本函数 2.浅拷贝和深拷贝 3.初始化列表 4.const关键字的使用 5.静态成员变量和成员函数 6.C对象模型 7.友元 8.自动类型转换 9.继承 1.基本函数 (1)构造函数,这个需要注意的就是我们如果使用类名加括号,括号里面…...
2005-2022年各省居民人均消费支出数据(无缺失)
2005-2022年各省居民人均消费支出数据(无缺失) 1、时间:2005-2022年 2、来源:国家统计局、统计年鉴 3、指标:全体居民人均消费支出 4、范围:31省 5、缺失情况:无缺失 6、指标解释 居民人…...
swaggerHole:针对swaggerHub的公共API安全扫描工具
关于swaggerHole swaggerHole是一款针对swaggerHub的API安全扫描工具,该工具基于纯Python 3开发,可以帮助广大研究人员检索swaggerHub上公共API的相关敏感信息,整个任务过程均以自动化形式实现,且具备多线程特性和管道模式。 工具…...
【Rust】——面向对象设计模式的实现
🎼个人主页:【Y小夜】 😎作者简介:一位双非学校的大二学生,编程爱好者, 专注于基础和实战分享,欢迎私信咨询! 🎆入门专栏:🎇【MySQL࿰…...
C#朗读语音
最近有个需求,需要在C#程序发生异常时候,朗读文字,C#提供了.net framework可以提供简单的语音朗读功能。 引入依赖 using System.Media; using System.Speech.Synthesis; using System.Runtime.InteropServices; //报警音量 SystemSounds.…...
c++ 简单的日志类 CCLog
此日志类,简单地实现了向标准输出控制台和文件输出日志信息的功能,并能在这两者之间进行切换输出,满足输出日志的不同需求。 代码如下: /** CCLog.h* c_common_codes** Created by xichen on 12-1-12.* Copyright 2012 cc_te…...
一文读懂 Compose 支持 Accessibility 无障碍的原理
前言 众所周知,Compose 作为一种 UI 工具包,向开发者提供了实现 UI 的基本功能。但其实它还默默提供了很多其他能力,其中之一便是今天需要讨论的:Android 特色的 Accessibility 功能。 采用 Compose 搭建的界面,完美…...
Redis到底支不支持事务?
文章目录 一、概述二、使用1、正常执行:2、主动放弃事务3、全部回滚:4、部分支持事务:5、WATCH: 三、事务三阶段四、小结 redis是支持事务的,但是它与传统的关系型数据库中的事务是有所不同的 一、概述 概念: 可以一次执行多个命令,本质是一…...
美颜相机「BeautyCam」v12.0.80 祛广告解索会员版(美妆相机功能,展现女神魅力)
软件介绍 美颜相机,一款由知名移动互联网企业Meitu Inc.开发的移动设备照片编辑与美化应用,起初主要针对娱乐消费市场,随后集成了商业营销功能。目前,它已跻身全球最受欢迎的手机摄影应用程序之列。在中国,美颜相机和…...
Oracle的优化器
sql优化第一步:搞懂Oracle中的SQL的执行过程 从图中我们可以看出SQL语句在Oracle中经历了以下的几个步骤: 语法检查:检查SQL拼写是否正确,如果不正确,Oracle会报语法错误。 语义检查:检查SQL中的访问对象…...
[线程与网络] 网络编程与通信原理(六):深入理解应用层http与https协议(网络编程与通信原理完结)
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 🧀Java …...
个人博客的未来出路在哪里?
说起个人博客的未来这就是个悲伤的话题,估计不少个人博客站长们都在苦苦的坚持和挣扎着吧,反正明月这两年感受最深刻的就是又有不少个人博客站点停更和 404 了都。自从坚持写博客这近十来年这种情况也都见怪不怪了,但这两年最突出的就是很多站长都是迷茫和悲观。 明月去年在…...
【TensorFlow深度学习】实现Actor-Critic算法的关键步骤
实现Actor-Critic算法的关键步骤 实现Actor-Critic算法的关键步骤:强化学习中的双剑合璧Actor-Critic算法简介关键实现步骤代码示例(使用TensorFlow)结语 实现Actor-Critic算法的关键步骤:强化学习中的双剑合璧 在强化学习的广阔…...
微服务架构-可见可观测与量化分析体系
目录 一、可见可观测 1.1 概述 1.2 服务可见性 1.2.1 概述 1.2.2 服务描述 1.2.3 服务所有权 1.2.4 服务对外接口 1.2.5 服务SLA 1.2.6 服务的上下游拓扑 1.2.7 服务变更 1.2.8 服务接入和资源配额管理 1.2.9 服务线上部署和线下测试环境信息 1.3 变更可见性 1.4 …...
PostgreSQL的视图pg_indexes
PostgreSQL的视图pg_indexes 基础信息 OS版本:Red Hat Enterprise Linux Server release 7.9 (Maipo) DB版本:16.2 pg软件目录:/home/pg16/soft pg数据目录:/home/pg16/data 端口:5777pg_indexes 是 PostgreSQL 中的一…...
暂停系统更新
电脑左下角搜索注册表编辑器 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings 找到这个目录 打开FlightSettingsMaxPauseDays,没找到的话就创建一个同名文件夹然后选择10进制填入3550 最后进入系统暂停更新界面选择最下面…...
Python离线查询IP地址对应的国家和城市
使用场景: 在没网的情况下使用python代码实现对ip地址进行查询国家和地市 代码实现: 需要安装 pip install geoip2 库 import geoip2.databasedef get_location_by_ip(ip_address, db_path):reader geoip2.database.Reader(db_path)try:response r…...
网站建设优点/网络营销公司全网推广公司
目录 RequestContext 和上下文处理器 auth debug i18n media static csrf request messages 自动转义 HTML 在单个变量中禁用 在模板中的块里禁用 RequestContext 和上下文处理器 首先,我们快速回顾一下前面介绍的几个术语: • 模板是文本文…...
西安网站开发工程师/武汉seo首页
13-Figma-组件管理 常见操作 创建组件,选择,点击顶部创建多个组件,框选多个,点击顶部组件使用,对组件进行复制,就创建了组件的实例实例跳到模板,右键-》转到组件模板所有组件如何管理…...
天猫网站做真丝服装批发/海南网站制作公司
在计算机系统中,通过文件的名称对信息进行管理,计算机的文件管理系统使信息按名称存取成为可能。典型的文件名由主文件名 ( 简称文件名 ) 和文件扩展名 ( 类型名 ) 组成,在文件名和文件扩展名之间加一个点(1)DOS 操作系统中文件的命名规则早期…...
购物网站首页分成几个模块/关键词制作软件
实现需求:每天凌晨2点对Linux服务器上的mysql数据库进行自动备份。 实现步骤:1,编写数据库备份脚本 2,编写crontab定时任务 1,编写数据库备份脚本 mysql数据库导出脚本,脚本名称可以定义为 “db-bac…...
四川广安网站建设/网络软文范文
前面唠叨 最近公司app中有些列表在滑动的时候会有卡顿现象,我就开始着手解决这些问题,解决问题之前首先要分析列表滑动的性能瓶颈在什么地方。因为之前不会正确使用TraceView这个工具,主要是看不懂TraceView界面下方数据指标的值代表什么意思…...
建设工程安全监督备案网站/如何弄一个自己的网站
本文由Jamie Shields和Wern Ancheta进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态! SoundCloud提供了一个API,使开发人员可以获取他们想要的几乎所有数据。 但是它的用法可能会引起混淆,尤其是对于初学者而言…...