做Linux网络服务端开发这些年,read和write这两个函数几乎是每天都要见面的老熟人。可就是这么两个最常见的系统调用,它们的返回值里藏的细节比我预想的多得多。早几年带项目组的时候,发现不少同学能讲清楚业务逻辑、框架用法,但一问到“对端断开时read返回多少”“write返回成功就一定代表数据发到对端了吗”,十有八九会卡壳。这篇就把read和write在网络环境下的返回值彻底讲透,覆盖阻塞与非阻塞、常见errno、以及实际工程里的封装姿势。刚入门的朋友可以把它当基础知识补课,已经写了一段时间网络代码的人也可以回头查漏补缺。
1. 返回值到底是什么:从三层语义说起
1.1 正数、0、-1的黄金三元组
拿到一个网络socket,对它调用read和write时,返回值本质上只可能落在三种区间:正数、0、-1。先把man page的语义翻译成人话。
对read来说:
- 返回正数n,表示这次调用从socket的接收缓冲区里读到了n字节数据。n可能等于请求的长度,也可能小于请求长度。
- 返回0,表示对方关闭了连接(更准确说是对方关闭了它的“写”方向,也就是我们这边读方向的EOF)。TCP协议里,收到对端发来的FIN包后,read就会返回0。
- 返回-1,表示调用出错,具体错误要看errno。这一步里门道最多。
对write来说:
- 返回正数n,表示这次调用把n字节从用户空间复制到了内核的发送缓冲区,并不代表对端已经收到了这n字节。
- 返回0,在实际网络socket编程中极其罕见,只有你故意传入len=0让write去写0个字节时才会出现。很多文档甚至不建议依赖这个返回值。
- 返回-1,出错,具体看errno。
这里最需要建立的第一条心法:read/write发生在用户空间和内核缓冲区之间,不是直接发生在两台机器之间。返回值揭示的是“用户程序跟内核缓冲区打交道的结果”,而不是“网络传输的结果”。把这条心法学透了,后面很多坑都能绕过去。
1.2 为什么“短读”和“短写”是常态
很多新手第一次写socket循环接收时,想当然地认为:我调用read(fd, buf, 4096),对方一次性发来4096字节,read就应该返回4096。实际完全不是这样。
TCP是一个字节流协议,流上没有消息边界。内核把收到的数据按到达顺序放进接收缓冲区,read只是从缓冲区里“捞”一把。内核怎么捞呢?只要有数据,就是有多少捞多少。你请求4096,缓冲区里可能现在只有200字节,read就返回200;可能只有1字节,返回值就是1。在一次read调用期间,数据可能还在网络上排队,或者被TCP分段拆到好几个包,内核不会为了凑满你的请求量而一直等着。这就叫“短读”,它是协议栈的自然行为,不是bug。
write同样会“短写”。比如你调用write(fd, buf, 4096),但当前发送缓冲区只剩500字节空间(甚至更少)。非阻塞模式下,内核只收下它能收的500字节,然后原样返回500。如果你无视这个500,下一个4096直接又塞进去,数据就乱了,这是非常经典的丢数据事故。
为了更直观地理解,可以打个比方。接收缓冲区像一个蓄水池,read是你用水瓢从池子里舀水,你计划舀满一桶,但池子里的水本来就只有半桶,你第一瓢只能舀半桶,这不是水瓢的问题。发送缓冲区像另一个蓄水池的进水口,write往里面倒水,进水口能接纳多少,跟你开口要倒多少不是一回事。
1.3 socket上的read/write和普通文件的区别
在Linux里,read/write是文件描述符层面的通用系统调用,对普通文件、管道、socket都可以操作。但网络socket场景有个重要差异:对普通文件,你请求读4096字节,读到文件尾之前几乎都是满额返回;写普通文件一般是全部接受。文件系统给read/write的语义更“爽快”,把人惯坏了。
管道和socket则不同,它们的读写都不是磁盘上的连续空间,而是有生产者/消费者、有缓冲区水位限制的流式接口。尤其socket还要加上网络对端的处理节奏、拥塞控制、RTT等变量,返回值会更多地表现“这次实际能转移多少,我就转移多少”。
另外一个常见混淆点是send/recv与read/write。在socket上,send基本等价于write(少了一个flags参数,行为略有差异),recv等价于read。但send/recv可以传MSG_WAITALL、MSG_DONTWAIT等标志,功能上更丰富。返回值规则和read/write一致:返回实际传输字节数、0或-1。工程上既可以用read/write,也可以用recv/send,但务必弄清楚flags参数对返回值的影响。尤其是MSG_WAITALL,它会让recv在读到足够字节数后才返回,这是对“短读”的一种补丁,但使用时也要小心,它只对部分场景有效,并不能替代应用层的消息管理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 阻塞模式下,返回值的“应许”与“等待”
2.1 阻塞read:什么时候返回,什么时候一直挂
绝大多数初学写网络程序的人,第一个程序就是阻塞模型,accept一个连接之后,死循环read。阻塞read的行为,其实比想象中要“抠门”得多。
一个阻塞socket上调用read(fd, buf, 4096):
- 接收缓冲区有数据(哪怕只有1字节),立即返回,返回当前能读到的字节数,不会为了凑满你请求的4096继续等。
- 接收缓冲区为空,且对端还没有关闭,调用会挂住,直到缓冲区来数据、对端关闭(FIN到达)、或者被信号打断,或者socket设置了接收超时(SO_RCVTIMEO)。
- 对端关闭写方向后,read返回0。
- 如果在这之前对端已经发送了RST,则read返回-1,errno可能是ECONNRESET。
- 被信号打断时返回-1,errno是EINTR。
这里我想多强调第一点。很多开发者的直觉是“read请求多大就尽量读多大”,导致他们不理解为什么read返回的数据量少于请求量。尤其在“应用层消息”场景下,一个消息可能是4字节头加1000字节体,你期望一次read把整个消息读回来,但内核只会给你它现在有的东西。所以阻塞模型下也必须自己管理“读到的数据长度”这一状态。
要真正抓住阻塞read的内心,请记住:阻塞只发生在缓冲区为空的时刻;只要有数据,read的返回是即时的,且只返回当时可读的量。
2.2 阻塞write:返回值的完整性与等待窗口
阻塞write的行为与read不太一样,它是“尽量满足”的。对阻塞socket调用write(fd, buf, 4096):
- 如果发送缓冲区剩余空间足够容纳4096字节,内核一次全部拷贝,返回4096。
- 如果剩余空间不够,但大于0,内核拷贝它能塞进去的量。比如剩余2000字节,就拷贝2000字节,write返回2000。进程不会因此睡眠。
- 如果剩余空间为0,进程会睡眠,等待内核腾出空间(对端通过TCP确认消费掉数据之后,发送缓冲区释放空间)。醒来后,内核把可能腾出的空间尽量拷贝,然后返回实际拷贝的字节数。
所以阻塞write可能返回短写吗?可能。当发送缓冲区剩余空间大于0但小于你请求长度时,它做不到阻塞等待直到全部写完,只会写它能写的然后返回。这是很多人容易忽略的点:阻塞write的“阻塞”语义不是“必须返回请求长度”,而是“当缓冲区满时睡眠等待”。
真正工程上想保证一次write调用把所有数据都提交完,不能靠运气赌返回值,要靠循环。但阻塞模式下循环write又有个副作用:如果对端迟迟不读,发送缓冲区和接收缓冲区都有上限,双方最终会僵住,write会长时间阻塞。这时候应用层心跳、超时、多路复用机制就有意义了。返回值问题会牵引出整个网络模型设计,这不是小题大做。
2.3 别被select/poll的“可读可写”带偏
用select/poll/epoll等IO多路复用,事件通知后紧接着read/write,这是最基本的模式。但事件就绪和返回值之间依然有断层。
以epoll为例,当epoll_wait返回EPOLLIN,只说明此刻“读取大概率不会阻塞”,不代表你能读到你想要的消息长度。你仍需负责把read返回的数据积累起来做消息拼接。反过来,EPOLLOUT可写事件更敏感:一个socket在连接刚建立、发送缓冲区空空如也时几乎总是可写的,如果业务逻辑里有“要发数据才注册可写事件,发完注销”的状态机,就要仔细设计,否则每一次循环都触发可写,CPU空转。
多路复用模型下,read/write通常配非阻塞fd使用,这时返回值与errno的组合就上升为工程接口的核心。这也顺便提一句:阻塞socket上设置SO_RCVTIMEO超时后,如果超时到达且没有数据,read也会返回-1,errno通常是EAGAIN或EWOULDBLOCK。这个行为经常被当成“网络错误”,其实只是超时机制在工作,属于“与事件模型混着用”的边界情况,要分清楚。
3. 非阻塞模式与errno:真正见实力的地方
3.1 EAGAIN/EWOULDBLOCK:正常情况,不是错误
把socket设置为非阻塞(O_NONBLOCK)后,read/write的行为产生质变:
- 非阻塞read,接收缓冲区为空时,不会等待,而是立即返回-1,errno被设置为EAGAIN或EWOULDBLOCK。
- 非阻塞write,发送缓冲区剩余空间不足时,不会等待,而是能写多少写多少;如果完全装不下,返回-1,errno为EAGAIN或EWOULDBLOCK。
我在带团队时经常被问到EAGAIN和EWOULDBLOCK的区别。在Linux上,这两个宏的值是一样的,就是同一个errno;在部分其他Unix系统里,EWOULDBLOCK可能是另一个值,但语义就一个:现在做不了,别等,过阵子再来试。因为是同一个数字,你判断的时候写errno == EAGAIN || errno == EWOULDBLOCK最通用,但Linux下其实一个条件就够。
现实中一个典型错误写法是:非阻塞read返回-1就当成致命错误,直接关连接。其实遇到EAGAIN恰恰说明连接还健康,只是当前没有数据。正确做法是回到事件循环继续等。同样的错误发生在非阻塞write上:返回-1且是EAGAIN时,应该把没写完的数据缓存到应用层发送队列,等待下一次EPOLLOUT事件再发送,而不是立即报错。我见过不少线上故障,归根结底就是把EAGAIN当成了断连,误杀了一大堆健康连接。
3.2 EINTR:被信号打断后的重试问题
read/write被打断返回-1,errno为EINTR,这是个既阻塞又非阻塞都可能遇到的问题。对慢系统调用来说,进程在等待期间收到信号,内核会让调用返回失败,让用户进程有机会处理信号。处理办法不复杂:如果errno是EINTR,就重试原来的操作。
但注意一个细节:如果你已经读了50字节然后被打断,这时read的返回值是50(正数),而不是-1加EINTR。内核对“读入n字节后信号到达”的处理是保留实际读到的正数返回值,不会给你一个虚假的错误。只有在调用真正开始之前信号就来了,才会返回-1和EINTR。所以重试逻辑通常写在意外的-1分支里,不用反复纠结。
很多团队在代码里采用SA_RESTART来处理,文档上它确实能让部分系统调用自动重启。但真实场景里SA_RESTART并不覆盖所有调用,最稳的还是业务代码里显式判断EINTR并重试。这一点我在自己的网络库评审中反复强调:不只read/write,select、poll、epoll_wait等待系统调用也要处理EINTR,否则一个信号就可能让整个事件循环退出去,服务莫名其妙就停了。
3.3 其他常见errno:从参数错误到连接重置
除了EAGAIN和EINTR,网络read/write还会遇到一批经常出场的errno,我把它们集中整理成一个速查表:
| errno | 典型触发场景 | 怎么处理 |
|---|---|---|
| EAGAIN / EWOULDBLOCK | 非阻塞读写没有数据/没有空间 | 继续事件循环,不该关连接 |
| EINTR | 被信号中断 | 重试或退出事件循环前检查 |
| ECONNRESET | 对端发送了RST(如端口未监听、进程崩溃后对端继续写) | 关闭连接,视业务清理对端状态 |
| EPIPE | 写一个读端已关闭的socket,且SIGPIPE被忽略/触发 | 关闭连接,不要继续write |
| ENOTCONN | socket还没有建立连接就read/write | 检查连接状态/时序 |
| EBADF | fd非法(已关闭或根本不是fd) | 修复fd生命周期管理 |
| EFAULT | buf指针非法 | 检查是不是野指针 |
| EMSGSIZE | 数据报socket写超大包 | 改用流协议或分片 |
| EINVAL | 参数非法(如flags不对,或设置了超时但用法错误) | 检查参数 |
这里要特别注意与前文散点呼应的几件事。ECONNRESET对应的是TCP层面的RST,常见场景是对端进程崩溃后,它所在的内核会发送RST;又或者对端之前关闭了socket但残留数据还在路上,本端read会收到ECONNRESET而不是优雅的0。EPIPE更阴险:如果你写了一个已经收到RST/被对端关闭的连接,而进程没有处理SIGPIPE默认动作,会直接收到SIGPIPE信号并把进程干掉。所以很多服务器代码第一步就是signal(SIGPIPE, SIG_IGN),然后再靠write返回EPIPE来感知连接已关闭。这个组合拳,老工程师的代码里几乎全是标配。
3.4 从内核缓冲区看返回值:俯瞰全局
为了真正压住前文所有语义,可以把两个缓冲区放一起看。
本端接收缓冲区:对端发来的数据先进入这里,read读到的是这个缓冲区的内容。当对端也发来FIN,FIN不会立即被read“读走”,而是在所有数据读完后,read才返回0。这个顺序意味着:先返回所有可读数据,再返回0,通知你EOF。如果你在应用层有自己的消息头,EOF的处理就必须很谨慎:收到的最后几字节可能是一个完整消息的尾巴,也可能是半个消息。
发送缓冲区:write把用户数据拷贝到这里,内核协议栈负责按TCP窗口把它发到网络。缓冲区剩余空间决定了一次write能接受多少数据。TCP的发送缓冲区大小是动态的,也会受对端接收窗口影响。非阻塞下空间不足就给EAGAIN,阻塞下就睡眠等待,这是最直观的内核决策。
把返回值理解为“用户进程与内核缓冲区的操作结果”,很多困惑就消失了。这也是为什么read返回N字节永远不等于“对端应用层只发了N字节”,对端可能发来了N+M字节,但网络分段、调度时序把它拆成了几块。
4. 实战:把返回值封装成可靠读写原语
4.1 readn:必须读够指定字节数才返回
理论讲清楚了,工程上第一步是封装两个常用原语:readn和writen。TCP是流协议,你要读的是应用层消息,就必须自己定义“一个消息多长”,然后循环读,直到读满指定长度或者遇到EOF/错误。这是每个网络项目里都该有的基础函数。
先看readn的实现思路,加注释逐行拆解:
c复制ssize_t readn(int fd, void *buf, size_t n) {
size_t left = n;
ssize_t nread;
char *ptr = buf;
while (left > 0) {
if ((nread = read(fd, ptr, left)) < 0) {
if (errno == EINTR) {
continue; // 被信号打断,重试
}
return -1; // 其他错误,由调用方查errno
} else if (nread == 0) {
break; // EOF:对端关闭
}
left -= nread;
ptr += nread;
}
return n - left; // 真正读到的字节数
}
注意最后一行的返回值是实际读到的总字节数,不是请求的n。这是为了把“读到一半,对端关闭”的情况传递给上层。调用方拿到返回结果后,必须比较返回值与期望的n:相等,说明完整读完;小于n且大于0,说明半截关闭,数据不完整,该清理连接;等于0,说明一开始就没读到数据,连接已关闭。很多业务bug就出在直接使用read、把返回值当期望长度上。readn虽然多写几行,但能把错误模式收敛,后续排查问题的成本会低很多。
4.2 writen:把未写完的数据全部送出
写方向同样需要循环,不能因为一次write返回小于请求就放弃。这是write最核心的工程用法。
c复制ssize_t writen(int fd, const void *buf, size_t n) {
size_t left = n;
ssize_t nwrite;
const char *ptr = buf;
while (left > 0) {
if ((nwrite = write(fd, ptr, left)) < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
if (nwrite == 0) {
// 网络socket写0字节基本不会出现,
// 但保险起见避免死循环
continue;
}
left -= nwrite;
ptr += nwrite;
}
return n;
}
这个函数入参n是希望发送的总字节数,成功则返回n。因为while循环只有在left归零时才退出,所以返回n就是“全部提交给内核缓冲区完成”。如果某一次write遇到EAGAIN怎么办?在纯阻塞fd上基本不会出现(因为它在等);在非阻塞fd上,这个函数就不能这么写了,遇到EAGAIN需要返回给上层,让上层把它放进发送队列,等可写事件再续传。如果你直接把EAGAIN当错误返回-1,那非阻塞白配置了。
4.3 非阻塞模式下,封装返回EAGAIN的续传机制
非阻塞加事件循环是高性能服务的主流形态。这里最关键的一点是设计“发送队列”状态。简洁做法是:
- 上层调用发送接口时,先尝试writen的变体(把EAGAIN特殊处理)。如果一次性写完,开心返回。
- 如果写到一半碰到EAGAIN,记录已经提交的字节数和剩余数据,把剩余数据push到连接对象的发送队列。
- 后续epoll_wait返回EPOLLOUT,再从队列头部继续尝试writen。一旦队列为空,删除EPOLLOUT关注,避免可写事件空转。
这个过程里read/write返回值直接驱动状态机:返回值等于目标长度,说明清空队列;返回值是部分长度且后面跟了EAGAIN,说明还须等待;返回值是-1且errno为EPIPE或ECONNRESET,说明连接已经不可再用,清理连接。这个三层分支,几乎是所有事件驱动框架的公共逻辑。代码量不大,但逻辑必须严密。
我自己见过一个严重bug:EPOLLOUT触发后,发送队列处理时没检查write返回值,直接认为全部发出,结果在高负载下大量数据被静默丢弃,直到对端校验发现报文序号断裂才暴露。排查用strace一看,write返回的部分字节数就明白了。从那以后我给自己定了一条铁律:非阻塞socket的发送路径,永远先判断返回值,再决定是否从队列弹出数据。
4.4 应用层消息边界:返回值之后的事
readn/writen解决的是“读满/写完指定长度”,但TCP流的消息边界还要靠应用协议自己定义。最普遍的做法是长度前缀:固定4字节长度头加消息体。读时先读4字节头(用readn连续调用),解析出bodylen,再readn读取bodylen字节体。此时readn返回值能告诉我们EOF发生在哪个阶段:发生在头部,说明连接刚打开就断了;发生在体部,说明消息不完整。
这个阶段体现返回值的重要价值:它不只是“我读了多少”,而是“我读到哪一个协议阶段”。把readn返回值与协议解析状态机结合,网络服务的基础可靠性就建立起来了。很多线上“粘包”“半包”问题,本质就是对返回值处理不严格。所谓粘包,是多个消息连在一起被你一次read读走;所谓半包,是一个消息你只read到一半。这两件事在TCP流模型下百分之百会碰到,唯一的解法就是自己在应用层切分消息边界,而切分的第一步就是把每次read的返回值记录精确。
5. 常见坑位与调试经验清单
5.1 把read返回0和“连接彻底断开”画等号
前文提过,read返回0表示对端关闭了它的写方向,或者根本发来了FIN。但“FIN”与“RST”有本质区别。对端调用shutdown(fd, SHUT_WR),只关写,不关读,这时本端read会读到0,但本端write还能继续发数据,对端也还能收。这被称为半关闭状态。TCP协议里这是合法的。
举个例子:HTTP/1.0时代的服务端在发完响应后可以shutdown写方向,客户端read到0就知道响应结束,但此时客户端可以继续通过同一个连接给服务端发送数据。实际上现代HTTP协议已经不这么用了,但TCP半关闭机制本身仍存在。如果你在read返回0时直接close整个socket,可能把对端尚未读走的响应尾包切断。正确做法是:如果你知道业务协议可能用到半关闭,read返回0后先进入半关闭处理状态,不要急着close,要先考虑对端是否还有数据要发过来,或者你是否还要把已经排队的数据write出去。
另一个容易踩的坑是FIN与数据乱序。TCP保证数据顺序,所以read永远不会读到“FIN之前的数据之后再读到更晚的数据”。但应用不知道FIN和数据边界,你在close前必须确保所有read返回的数据都已经被业务消费完,否则最后一批字节可能还留在接收缓冲区,close会直接丢弃它们。
5.2 只检查read返回值不检查write返回值的隐形丢数据
我在很多code review里看到一种典型偏科:大家普遍会检查read返回n,然后解析应用层消息;但对write却经常一言不合直接裸调,用完不检查。写成功一次返回n,不代表对端收到了。它只代表内核发送缓冲区接受了。后面还有网络丢包重传、对端接收缓冲区、对端应用read等很多环节。
更要命的是,如果不检查write返回值且不处理短写,那么为了发送一个4000字节的应用消息,你直接write(fd, msg, 4000),可能只写入500字节,后续代码却把整个msg逻辑当作已发送,自己维护的发送序列、已读指针全都错位。在阻塞fd上这么写大概率不出事,因为阻塞write在空间不足时会尽量写,但如果空间不足还是可能短写;在非阻塞fd上就是事故现场。
所以,规矩很简单:凡是socket读写,一律走封装的readn/writen或等价的状态机,永远不要假设一次调用就完成。虽然多写几行,但这个习惯帮我避掉的线上故障,两只手数不过来。尤其在高并发系统里,发送缓冲区满几乎是常态,靠裸write拼运气,迟早踩坑。
5.3 用strace和nc验证返回值行为
纸上谈兵不如动手。Linux下用strace可以实时看到每个系统调用的返回值:
bash复制strace -e trace=read,write -p <pid>
运行一个网络服务,你会看到类似输出:
code复制read(14, "GET / HTTP/1.1\r\n", 4096) = 16
write(16, "HTTP/1.1 200 OK\r\n", 4096) = 17
你能立刻对照出“请求4096,只读了16字节”的真实情况。我在定位“消息拼接错乱”的时候,第一件事就是strace,看返回值是不是被误当成期望长度。如果发现read返回值屡屡不等于请求值,那就不是协议逻辑问题,而是你还没有建立流式读包的心智。
也可以用nc做最简单的TCP测试:开一个监听服务,客户端用nc连接后发送固定字节,自己写一个服务端程序打印每次read的返回值。结果会看到请求长度和实际返回值经常不同。这不丢人,反而说明你对流协议的理解对了。调试工具不是万能的,但配合源码确认返回值行为,比空想快得多。
5.4 由“write error 0x17循环冗余检查”得到的提醒
标题相关搜索里最近有不少人在查“write error [0x00000017] 数据错误(循环冗余检查)”。这个报错大概率不是socket编程的write返回值问题,而是磁盘或文件系统写入时报的Windows系统错误,0x17(23)对应ERROR_CRC_DATA_ERROR,多见于拷贝大文件、磁盘坏道或者USB设备异常。网络read/write返回的是ssize_t加上errno体系,跟Windows的GetLastError码完全是两套机制。
如果你搜这个关键词是为了找硬盘问题的答案,别把它和网络编程的write返回值混为一谈。如果在Linux下程序出现类似“写入循环冗余”的联想——实际上Linux没有这个errno,最接近的是EIO(输入输出错误),一般也是磁盘、文件系统或设备问题,不是TCP丢包。排查思路完全不同:网络write返回值问题要看errno和缓冲区状态,磁盘写入CRC问题要看SMART信息、dmesg日志和硬件健康状态。
5.5 常见问题速查表
| 症状 | 常见原因 | 排查方向 |
|---|---|---|
| read总是读不满请求长度 | TCP流无边界,缓冲区只有部分数据 | 使用readn或应用协议拼接 |
| read返回0后就close导致丢尾包 | 半关闭场景被忽略 | 区分shutdown/close,状态机处理半关闭 |
| 非阻塞read返回-1且errno==EAGAIN | 当前无数据,正常 | 回到事件循环 |
| 非阻塞write返回-1且errno==EAGAIN | 发送缓冲区满 | 缓存数据等待EPOLLOUT |
| write返回小于请求长度 | 发送缓冲区空间有限 | 循环writen,非阻塞配合发送队列 |
| 进程在write时莫名退出 | SIGPIPE未忽略 | signal(SIGPIPE, SIG_IGN),检查EPIPE |
| 对端崩溃后read返回ECONNRESET | 对端内核发RST | 清理连接,区分优雅关闭 |
| 数据偶尔错位、丢失 | 未处理短写 | 检查所有write返回值 |
最后再分享一点我自己这些年攒下的习惯。现在写网络代码,我基本不许业务逻辑裸调read/write,所有收发必须经过readn、writen这类封装或者等价的状态机;遇见返回值先和期望值对齐,再谈业务解析。线上日志如果出现“读到半帧”“发送队列长度异常”,第一反应也是回去盘read/write的每一条返回记录。这套方法论朴实得很,但它帮我从早期一堆时灵时不灵的网络程序里走了出来。你可以先在自己的项目里试试,哪怕只是把read循环补上,我敢说,你很快就能感受到返回值这件事到底值不值得较真。
