温州网站建设推广专家wordpress 获取文章标签
2026/1/3 1:34:09 网站建设 项目流程
温州网站建设推广专家,wordpress 获取文章标签,磁县网站建设,网页微信怎么换行不管是普通文件#xff0c;还是硬件设备、管道、网络套接字#xff0c;在Linux甚至还有信号和定时器都共享一套相似的api#xff0c;大家可以用类似的代码完成各种不同的任务#xff0c;大大简化了代码复杂度和学习成本。当然这只是理想中的情况#xff0c;现实是普通文件…不管是普通文件还是硬件设备、管道、网络套接字在Linux甚至还有信号和定时器都共享一套相似的api大家可以用类似的代码完成各种不同的任务大大简化了代码复杂度和学习成本。当然这只是理想中的情况现实是普通文件和硬件设备是两种完全不同的东西普通文件和网络套接字尤其是UDP协议的那种更是风马牛不相及强行把这些行为属性完全不同的事物整合进同一套api导致了read/write/send/recv这几个系统调用的行为极其复杂bug丛生更是给很多新手带来了无尽的困扰。而且由于系统差异和资料分散这类问题就连求助于AI都很难得到有效解决。这也是我写这篇文章的原因。进入正题之前我们先限定一下讨论范围和实验环境因为这个主题太复杂了包罗万象是不可能的。讨论范围行为会基于POSIX 2008这版标准进行讨论但也会加入一下Linux和macOS上特有的行为这些会特别标注。实验环境Linux环境内核版本高于4.0即可macOS 15及以上。基础回顾之部分读部分写有一些重要的概念会贯穿整个我们对系统调用行为的讨论这里必须先介绍一下。我们先来看看接下来要说的系统调用长什么样#include sys/types.h#include unistd.h// 从文件描述符里读数据ssize_t read(int fd, void *buf, size_t nbyte);// 向文件描述符里写数据ssize_t write(int fd, const void *buf, size_t nbyte);// 从套接字中读取数据不可用于套接字以外ssize_t recv(int sockfd, void *buf, size_t nbyte, int flags);// 向套接字写入数据不可用于套接字以外ssize_t send(int sockfd, const void *buf, size_t nbyte, int flags);他们长得很像核心逻辑也差不多——围绕一块nbyte长度的缓冲区进行操作把数据从缓冲区写入描述符或者从描述符里读取数据填进缓冲区。这些系统调用是文件和网络io的核心。通常读取类的系统调用会尽可能多地读入数据直到填满缓冲区而写入类的系统调用则会尽可能把缓冲区里所有的数据写入描述符。然而现实是POSIX除了少数操作之外并没有规定读写操作不能被打断因此经常会出现读或者写了一半时操作被中断的情况进程收到了信号导致系统调用中断当然一部分系统会在中断后自动重启系统调用但这个行为是可配置且有系统差异的所以我们不能忽略这种中断场景网络套接字的缓冲区中只有少量数据可读/少量空间可写系统调用在一些情况下中止并返回读写中遇到错误比如网络中断、硬盘故障等这些情况会导致缓冲区里的数据只有一部分被写入目标或者只从目标中读取了一部分数据没能填满缓冲区简单的说就是调用返回的值比nbyte小且没有设置errno我们把这些情况统一叫做部分读和部分写英文叫short read/write或者partial read/write。这不是bug而是需要处理的正常的系统行为尤其是在非阻塞io中。不同类型的对象在这方面有很大的行为差异这也是本文下面要讨论的内容。普通文件上的读写行为普通文件是指在你硬盘里的那些文本文件、程序代码、音乐、图片、视频、PPT之类的东西。这些统称regular files。普通文件上没有非阻塞io且无法被poll、select监听。bsd系统上的kqueue对普通文件做了扩展但这不属于POSIX规范且超出了讨论范围我们就不提了。虽然普通文件特性少也因此read和write在它们上的行为更直观也更符合预期。read的行为几乎总是阻塞到填满缓冲区文件可读取内容比缓冲区小的时候会把文件中剩下可读的数据全部读取然后返回这是返回值小于缓冲区大小读取过程中可以被中断如果读取出错了则返回值是-1errno会被设置缓冲区里很可能会有垃圾数据如果返回0EOFend-of-file说明文件所有内容已经读取完毕这也是正常情况errno不会被设置从POSIX标准和Linux的文档上来看read是会有部分读存在的然而标准是标准实现是实现现实情况是不管是macOS的APFS上还是Linux上常见的文件系统read一但准备工作完成就不可被信号中断因此部分读无法发生。以Linux为例所有想利用page cache的文件系统在进行文件读写时都会调用filemap_read这个函数会接着调用filemap_get_pagesfilemap_get_pages里有完成读取的主要逻辑而在它的最开头处初始化完所有资源就会调用fatal_signal_pendingfatal_signal_pending会让当前线程屏蔽包括SIGKILL在内的所有信号。这意味着一但read开始就会忽略所有信号read也就不可能存在读取一部分数据后被中断的场景。这么做当然是为了数据一致性和安全考虑虽然代价是和标准有了小小的冲突但也无可厚非。想要测试也很简单准备一个1GB的文件然后一个线程每次读写1MB并且让另一个线程不停发信号理论上下面这段代码不应该看到有“Short Read”的输出#include iostream#include thread#include atomic#include signal.h#include unistd.h#include fcntl.h#include cstring#include chronostd::atomicint sigint_count{0};void handle_sigint(int signo) {if (signo SIGINT) {sigint_count;}}int main() {struct sigaction sa;memset(sa, 0, sizeof(sa));sa.sa_handler handle_sigint;sigemptyset(sa.sa_mask);sa.sa_flags 0; // 不设置SA_RESTART这会禁止系统调用自动重启if (sigaction(SIGINT, sa, nullptr) -1) {perror(sigaction);return 1;}int fd open(test.data, O_RDONLY); // 1GBif (fd 0) {perror(open);return 1;}auto main_thread pthread_self();// 每隔100ns就发一次信号std::thread([main_thread]() {while (true) {// 给主线程发送信号使用kill (2) 会给进程内没有屏蔽此信号的任意一个线程发送pthread_kill(main_thread, SIGINT);std::this_thread::sleep_for(std::chrono::nanoseconds(100));}}).detach();const size_t buffer_size 1024 * 1024; // 1MBchar* buffer new char[buffer_size];ssize_t bytes_read;while (sigint_count.load()1);while ((bytes_read read(fd, buffer, buffer_size)) 0) {if (bytes_read ! 1024*1024) {std::cout Short Read: bytes_read bytes\n;}}if (bytes_read 0) {perror(read);}close(fd);delete[] buffer;std::cout SIGINT received: sigint_count.load() times\n;return 0;}输出$ g-15 -Wall -Wextra -stdc20 read.cpp$ head -c 1073741824 /dev/random test.data$ ./a.outSIGINT received: 1099 times可以看到我们发送了1000多次信号没有对read产生任何影响。说完了read说说write。write在普通文件上的行为正常情况下阻塞到buff全部写入文件出错的时候直接返回-1比如磁盘空间不够一个字节都写不进去、没有写入权限等并设置对应的errno。可以被信号中断这时会发生部分写如果信号在任何数据实际写入之前收到write返回-1并且设置errno为EINTR。如果磁盘的空间不够或者进程有写入配额限制则发生部分写入还有多少空间就写入多少write在本次写入后正常返回我们可以轻松得用ulimit来限制进程可写入的文件大小并模拟磁盘空间不够的情况#include iostream#include unistd.h#include fcntl.h#include cstringint main() {int fd open(test.data, O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd 0) {perror(open);return 1;}const size_t chunk_size 12345;const size_t total_writes 10000;char buffer[chunk_size];memset(buffer, c, chunk_size);for (size_t i 0; i total_writes; i) {ssize_t written write(fd, buffer, chunk_size);if (written 0) {perror(write);break;}if (written ! chunk_size) {std::cout Short Write: written bytes\n;std::cout Count of write: i \n;}}close(fd);return 0;}运行$ ulimit -f 102400$ ./a.outShort Write: 11515 bytesCount of write: 8493core dump‰./a.out可以看到最后一次写入已经没有足够的空间写入12345字节了所以只写入了11515字节。真实的磁盘耗尽会在部分写入发生后下一次write理论上应该返回-1并设置errno但ulimit模拟的会直接杀死进程因为POSIX要求在这种情况下发送信号SIGXFSZ给进程这个信号的默认处理行为的进程崩溃。和read一样对于普通文件的write在准备工作完成后也会屏蔽掉所有信号这使得在macOS和Linux普通文件的写也不会发生部分写入。对于普通文件write还有一个特殊行为如果第三个参数是0则不写入任何数据但会探测写入操作是否会出错比如硬盘挂掉了或者权限不够。总结对于普通文件macOS和Linux上的read/write总是会读取/写入和buf长度相等的数据。不对普通文件处理部分读和部分写正在成为越来越多人的共识毕竟代码写起来简单。但标准留了口子为了可移植性开发者最好还是不要对此做出过多假设为好。管道上的读写行为管道大家应该不陌生POSIX规定了两种类型的管道pipe和FIFO。pipe是匿名管道FIFO是有名字的且需要在支持管道文件的文件系统上生成一个对应的实体。除此之外两者行为上没有差别。所以这一节两者合并在一起讨论。管道的读写行为和普通文件不同也和下一节要说的套接字有些出入所以需要单独拿出来作为一节内容。管道有一个固定的总容量超过此容量的数据无法继续写入。管道也支持非阻塞io。管道上的读写行为还受到读写端是否开启的影响所以整体上管道的复杂度比普通文件高了至少一个数量级。先总结一下读的行为条件 read行为 read返回值 errno 是否是部分读写端的管道被关闭 遇到EOF 0 不设置同步io管道里没有数据 阻塞到有数据能读为止但有多少读多少不要求填满buf可被中断 buf长度 不设置 ✔非阻塞io管道中没有数据 直接出错 -1 EAGAIN同步io管道里有数据 不阻塞有多少读多少不要求填满buf可被中断 buf长度 不设置 ✔非阻塞io管道中有数据 不阻塞有多少读多少不要求填满buf可被中断 buf长度 不设置 ✔任何模式下读取开始前被信号中断 直接出错 -1 EINTR任何模式下读取了部分数据被信号中断 正常返回 buf长度 不设置 ✔最后一种情况其实涵盖在第四和第五中了但我还是单独列出方便大家理解。简单地说管道的读大部分都是部分读管道里有多少数据就读多少唯一会发生阻塞的场景是管道里一点数据都没有的时候。看个例子就知道了管道里只有11个字节数据我们的读取buf有1024长但和读普通文件不一样read读完11字节就正常返回#define _GNU_SOURCE // for pipe2#include fcntl.h#include unistd.h#include stdio.hint main(){int pair[2] {-1, -1};if (pipe2(pair, 0) 0) { // 没有设置任何标志默认是同步ioperror(pipe2);return 1;}if (write(pair[1], Hello World, 11) 0) {perror(write);return 1;}char buf[1024] {0};int n read(pair[0], buf, 1024);if (n 0) {perror(read);return 1;}printf(read %d bytes\n, n); // output: read 11 bytes// read(pair[0], buf, 1024);// 已经没数据了同步io下程序会阻塞在这非阻塞io下返回-1errno被设置为EAGAIN}写入的行为则比读取要复杂的多会同时受到管道容量、原子写、信号的影响。原子写是管道特有的概念任何大小小于等于PIPE_BUF大小的写操作都是原子的要么全部写入要么彻底失败且不可被中断。PIPE_BUF的值在不同系统上也是不同的在macOS上是512字节而在Linux上是4096。管道的总容量也是一样的在Linux是16个page size大小而且容量可以手动修改在macOS管道默认大小16kb但可以扩展到64kb。写入行为总结条件 write行为 write返回值 errno 是否是部分写读取端关闭 直接出错 -1 EPIPE同步io原子写管道中容量足够容纳所有内容 不阻塞原子地写入所有数据不可中断 len(buf) 不设置同步io原子写管道没有容量或者容量不足以容纳所有内容 阻塞到所有内容可被写入为止不可中断 len(buf) 不设置非阻塞io原子写管道中容量足够容纳所有内容 不阻塞原子地写入所有数据不可中断 len(buf) 不设置非阻塞io原子写管道没有容量或者容量不足以容纳所有内容 不阻塞直接出错 -1 EAGAIN同步io普通写管道中容量足够容纳所有内容 不阻塞写入所有数据可中断 len(buf) 不设置 ✔同步io普通写管道没有容量或者容量不足以容纳所有内容 阻塞到所有内容可都写入为止可中断 len(buf) 不设置 ✔非阻塞io普通写管道中容量足够容纳所有内容 不阻塞写入所有数据可中断 len(buf) 不设置 ✔非阻塞io普通写管道没有任何容量 不阻塞直接出错 -1 EAGAIN非阻塞io普通写管道容量不足以写入所有数据 不阻塞有多少写入多少可中断 len(buf) 不设置 ✔任何模式写入没开始前被信号中断 直接出错 -1 EINTR可以看到部分写主要发生在非原子写的情况下。看一个非阻塞io时容量不够导致部分写的例子#include fcntl.h#include unistd.h#include stdio.h#include string.h#include errno.h#define PIPE_MAX (4096*16)#ifdef __linux__// #include linux/limits.h#define PIPE_BUF 4096#else#define PIPE_BUF 512#endifint main(){int pair[2] {-1, -1};if (pipe(pair) 0) {perror(pipe2);return 1;}// 设置为非阻塞iomacOS不支持pipe2为了跨平台只能用这种原始办法if (fcntl(pair[0], F_SETFL, fcntl(pair[0], F_GETFL) | O_NONBLOCK) 0) {perror(fcntl pair[0]);return 1;}if (fcntl(pair[1], F_SETFL, fcntl(pair[1], F_GETFL) | O_NONBLOCK) 0) {perror(fcntl pair[1]);return 1;}// 非原子写入因此发生部分写char buf[PIPE_MAX-1] {0};memset(buf, c, sizeof(buf));if (write(pair[1], buf, PIPE_MAX-1) ! PIPE_MAX-1) {printf(this can not be a short write\n);return 1;}char new_buf[PIPE_BUF1] {0};int n write(pair[1], new_buf, PIPE_BUF1);if (n 0) {perror(write);return 1;}printf(short %d bytes\n, n);if (read(pair[0], read_buf, PIPE_BUF-100) ! PIPE_BUF-100) {printf(this can not be a short read\n);return 1;}// 原子写入会立即失败char atomic_buf[PIPE_BUF];memset(atomic_buf, c, PIPE_BUF);n write(pair[1], atomic_buf, PIPE_BUF);if (n 0) {perror(atomic write failed);return 0;} else {printf(no way!\n);return 1;}}程序首先写入数据只留一字节给管道然后非阻塞写入一个比原子写限制大一字节的数据这时候程序就会发生部分写只写入一字节。如果这里是同步io的话程序则会阻塞住直到剩下的所有数据都能写入。接着我们把读取PIPE_BUF-100的数据现在管道的容量只有PIPE_BUF-100字节然后又再往管道里原子写入PIPE_BUF长度的数据这一步应该直接失败。运行结果$ ./a.outshort 1 bytesatomic write failed: Resource temporarily unavailable输出中的Resource temporarily unavailable就是EAGAIN的文字描述。可见即使还有空间只要不能容纳下原子写入要求的全部数据就会立即失败。总结尽量每次读写管道都使用PIPE_BUF大小的buf可以免去很多麻烦但我还是建议每次读写之后检查返回值和errno以免发生问题毕竟读写加起来差不多有20种情况存在了。这也是APUE这本书推荐的做法。有一点需要注意POSIX规定了所有errno被设置成EPIPE的场景进程都会收到SIGPIPE这个信号默认行为会导致进程崩溃。但这个信号并不意味着程序发生了无法挽回的错误所以常见的做法是彻底屏蔽它然后检查write调用的返回值和errno。UDP协议套接字上的读写行为终于来到最复杂的套接字了这里说的套接字包含网络类型为INET和UDS这两种尽管他们的实现完全不同处理数据的方式也大相径庭但在read、write、send、recv这些系统调用上的行为是一样的。POSIX规定read、write如果操作对象是socket那么效果等同于调用recv和send。所以在socket的两节里我们只讨论recv和send。对UDP套接字的操作是比较简单的每次recv和send都会读取/发送一个UDP数据报而且这个操作是原子的不可中断。这意味send会把buf中所有东西全部写入后才会成功返回而且写入一但开始就不可被中断。所以不存在部分写。而recv则会尽量把下一个待读取的数据报全部读入缓冲区如果数据报的大小超过缓冲区大小则会截断截断之后数据报剩余的数据会被全部丢弃recv在截断时也会正常返回。recv同样一但开始读取就不可中断所以不存在部分读。UDP有读写缓冲区的概念这会影响它在不同io模式下的行为如果读缓冲区是空的同步io时recv会一直阻塞到有数据进来才返回非阻塞io下则直接报错并设置EAGAIN如果读缓冲区有数据不管什么模式下都会立即读取一个数据报并返回如果发送缓冲区是满的同步io时send会一直阻塞到所有数据都能写入为止而非阻塞下会直接报错并设置EAGAIN如果发送缓冲区有空间但不足以写入所有内容同步io的send会阻塞到缓冲区有足够空间然后一次性写入所有内容非阻塞io时则直接报错并设置EAGAIN如果发送缓冲区有空间写入所有数据则任意模式都不会阻塞会立即把所有数据写入并返回向没有服务监听的地址端口写数据并不会发生错误这是udp协议的特性除非你把套接字的对端地址进行了绑定总体UDP很简单没有部分读写问题只有数据截断需要特别注意。这在后文会讲。TCP协议套接字上的读写行为TCP是这些总结里面最复杂的因为它受io模式和信号影响同时也有读写缓冲区的概念并且TCP是面向连接的协议连接状态还会额外影响读写的行为。场景实在太多用文字描述会非常费篇幅因此我们直接上表格io模式 读缓冲区状态 连接状态 recv行为 是否能被中断 recv返回值 errno 是否是部分读同步 缓冲区空 正常连接 阻塞到有数据为止然后尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔同步 缓冲区有数据或者满 正常连接 不阻塞尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔同步 缓冲区空 连接已经关闭 不阻塞直接返回 可中断 0 不设置同步 缓冲区有数据或者满 连接已经关闭 不阻塞尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔非阻塞 缓冲区空 正常连接 直接出错 可中断 -1 EAGAIN非阻塞 缓冲区有数据或者满 正常连接 不阻塞尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔非阻塞 缓冲区空 连接已经关闭 不阻塞直接返回 可中断 0 不设置非阻塞 缓冲区有数据或者满 连接已经关闭 不阻塞尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔任意 缓冲区空 连接异常终止收到RST 直接出错 可中断 -1 ECONNRESET任意 缓冲区有数据或者满 连接异常终止收到RST 不阻塞尽可能多读取信息直到缓冲区里没数据或者buf填满 可中断 len(buf) 不设置 ✔任意 任意 本地close了socket然后继续调用recv 直接出错 可中断 -1 EBADFrecv返回0EOF说明所有的数据都已经被读取连接的生命周期也应该正常结束了。由此可见除了部分异常情况TCP下几乎所有的读都是部分读而且可被信号中断因此必须去检查recv的返回值并做处理。写入时的情况类似io模式 写缓冲区状态 连接状态 send行为 是否能被中断 send返回值 errno 是否是部分写同步 缓冲区有足够空间写入全部数据 正常连接 不阻塞写入全部数据 可中断 len(buf) 不设置 ✔同步 缓冲区有空间但不能写入全部数据或者满 正常连接 先写入数据然后阻塞到缓冲区有空间接着写入循环往复直到全部写入 可中断 len(buf) 不设置 ✔非阻塞 缓冲区有足够空间写入全部数据 正常连接 不阻塞写入全部数据 可中断 len(buf) 不设置 ✔非阻塞 缓冲区有空间但不能写入全部数据 正常连接 不阻塞尽可能多写入然后返回 可中断 len(buf) 不设置 ✔非阻塞 缓冲区满 正常连接 直接出错 可中断 -1 EAGAIN任意 任意 连接已经关闭 直接出错 可中断 -1 EPIPE任意 任意 连接异常终止收到RST 直接出错 可中断 -1 ECONNRESET任意 任意 本地close了socket然后继续调用send 直接出错 可中断 -1 EBADFsend要简单一些因为它对连接状态的要求更为严格。同步io下send会尽量发生全部数据但会被信号中断非阻塞io下则是能写多少是多少几乎都是部分写。所以针对tcp必须检查所有读写操作的返回值和errno这也是为什么UNP这本网络编程的名著会在头两章就给出下面这样的帮助函数/* Like write(), but retries in case of partial write */ssize_t writen(int fd, const void *buf, size_t count){size_t n 0;while (count 0) {int r write(fd, buf, count);if (r 0) {if (errno EINTR)continue;return r;}if (r 0)return n;buf (const char *)buf r;count - r;n r;}return n;}这样的检查和处理逻辑每次都写一遍代码很快就能进化成屎山了所以作者给出了这个函数而且这种帮助函数在c/c项目中很常见。不同读写行为导致的问题不同的读写行为经常会带来心智负担最后在代码里留下问题。比如前文中提到的UDP数据截断问题。我刚入行的时候就被坑过当时我写的代码在解析一些特定种类的数据报信息时算错了数据长度导致读取用的缓冲区设置小了这些种类的数据报都被截断了。然而recv和recvFrom都不会报告截断问题还会正常返回这导致调试过程异常艰难最后还是有经验的前辈和我一起抓包对比接收到的数据才发现发送的数据比接收的大这才想到了是recv截断数据的问题。当然recvmsg和recvmmsg这两个系统调用会在返回的结构体的flags字段里设置MSG_TRUNC标志来表示数据报被截断但相对来说recv和read因为接口更简单所以大多数人优先选择使用它们我也不例外。除了上面的UDP数据截断问题部分写入也会出问题比如不检查返回值导致需要的数据没有全部写入这种问题在新接触TCP编程的人的代码里很常见。不过物极必反有时候太谨慎也不好比如最近我在审查golang的代码时发现有人把上一节的writen函数搬到go里了最后搞出了下面这样的代码func (c *Client) sendData(data []byte) error {header : 从data里生成headerif err : util.WriteAll(c.tcpConn, header); err ! nil {return err}if err : util.WriteAll(c.tcpConn, data); err ! nil {return err}return nil}代码看起来很清晰开发者还想到了TCP的部分写入问题简直无可挑剔啊。然而这是go语言go对io做了很多封装把异步非阻塞io操作封装成了同步操作因此部分写问题被已经考虑到并且强制要求所有实现io.Writer接口的类型保证不出现部分写的$ go doc io.Writerpackage io // import iotype Writer interface {Write(p []byte) (n int, err error)}Writer is the interface that wraps the basic Write method.Write writes len(p) bytes from p to the underlying data stream. It returnsthe number of bytes written from p (0 n len(p)) and any errorencountered that caused the write to stop early. Write must return a non-nilerror if it returns n len(p). Write must not modify the slice data,even temporarily.Implementations must not retain p.其中Write must return a non-nil error if it returns n len(p).就说明了如果写操作没有出错则数据必须全部写入因此没有部分写问题。实际上net.TCPConn也是这么做的func (fd *FD) Write(p []byte) (int, error) {...var nn intfor {max : len(p)if fd.IsStream max-nn maxRW {max nn maxRW}n, err : ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max])if n 0 {if n max-nn {// This can reportedly happen when using// some VPN software. Issue #61060.// If we dont check this we will panic// with slice bounds out of range.// Use a more informative panic.panic(invalid return from write: got itoa.Itoa(n) from a write of itoa.Itoa(max-nn))}nn n}if nn len(p) {return nn, err}if err syscall.EAGAIN fd.pd.pollable() {if err fd.pd.waitWrite(fd.isFile); err nil {continue}}if err ! nil {return nn, err}if n 0 {return nn, io.ErrUnexpectedEOF}}}这是一个非阻塞版本的writen在收到EAGAIN的时候会调用poll之类的系统调用来等待文件描述符可写。所以util.WriteAll是完全多余的并会成为性能杀手。golang这么做很正常因为在同步io模式下除了被信号中断几乎所有的写入操作都是保证buf里的数据全部写入才返回给调用者的模拟同步io的go没有不这样做的理由。另一个原因是这样可以减轻开发者的心智负担。当然如果一个第三方库没有按要求处理部分写那就会引发新的问题了但这属于是第三方库的责任。总结

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询