IO=等(要进行io是要有条件的,要有数据或者有空间)+拷贝。高效体现在等待的时间所占比重越低越高效。
阻塞IO:数据没有就绪,read不返回。在内核将数据准备好之前, 系统调用会一直等待。所有的套接字, 默认都是阻塞方式。
非阻塞IO:非阻塞轮询,如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
IO多路转接: 虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态。
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
同步IO和异步IO的区别在于是否参与了IO的过程。
非阻塞
void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL);//获取文件状态标记if (fl < 0){std::cerr << "error string: " << strerror(errno) << " error code: " << errno << std::endl;return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);//设置状态,成为非阻塞。
}int main()
{char buffer[64];SetNonBlock(0);LoadTask();while (true){printf(">>> ");fflush(stdout);// 1. 成功 2. 结束 3. 出错(一旦底层没有数据就绪,以出错的形式返回,但是不算真正的出错,因为一旦fd被设置成为非阻塞,会有底层数据没有就绪的情况)ssize_t n = read(0, buffer, sizeof(buffer) - 1); // (检测条件是否就绪)等 + 拷贝if (n > 0){buffer[n - 1] = 0;std::cout << "echo# " << buffer << std::endl;}else if (n == 0)//读到文件结尾{std::cout << "end file" << std::endl;break;}else{if (errno == EAGAIN || errno == EWOULDBLOCK)//{// 底层数据没有准备好,希望你下次继续来检测HandlerALLTask();//底层数据没就绪就做别的事情sleep(1);std::cout << "data not ready" << std::endl;continue;}else if (errno == EINTR){// 这次IO被信号中断,也需要重新读取continue;}else{// 一旦fd被设置成为非阻塞,std::cerr << "read error? "<< "error string: " << strerror(errno) << " error code: " << errno << std::endl;break;}// break;}}
}
多路转接
#include <iostream>
#include <string>
#include <cstring>
#include <sys/select.h>
#include "Sock.hpp"
#include "Log.hpp"const static int gport = 8888;typedef int type_t;
// static const int defaultfd = -1; // 暂时class SelectServer
{static const int N = (sizeof(fd_set) * 8);public:SelectServer(uint16_t port = gport) : port_(port){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();for (int i = 0; i < N; i++)fdarray_[i] = defaultfd;}void Accepter(){// std::cout << "有一个新连接到来了" << std::endl;// Accept, 这里在进行Accept会不会被阻塞??不会的!因为我们已经前面已经确认listen套接字有新的连接到来了std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);if (sock < 0)return;// 得到了对应的sock, 我们可不可以进行read/recv,读取sock?不能// 你怎么知道sock上有数据就绪了?不知道,所以我们需要将sock交给select,让select进行管理!logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);// 要让select 帮我们进行关心,只要把sock添加到fdarray_[]里面即可!int pos = 1;for (; pos < N; pos++){if (fdarray_[pos] == defaultfd)break;}if (pos >= N)//说明遍历整个数组,没有找到一个空余的位置容纳套接字,服务器也没有能力去处理新连接了,直接close掉。{close(sock);logMessage(Warning, "sockfd array[] full");}else{fdarray_[pos] = sock;//找到空余位置了}}// echo servervoid HandlerEvent(fd_set &rfds){for (int i = 0; i < N; i++){if (fdarray_[i] == defaultfd)continue;if ((fdarray_[i] == listensock_.Fd()) && FD_ISSET(listensock_.Fd(), &rfds))//如果当前遍历的文件描述符是listen套接字并且在就绪的文件描述符集合里,我们获取连接{Accepter();}else if ((fdarray_[i] != listensock_.Fd()) && FD_ISSET(fdarray_[i], &rfds))//否则的话就是普通文件描述符就绪,可以开始读了。{// ServiceIO();// BUGint fd = fdarray_[i];char buffer[1024];ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 读取会被阻塞吗?不会if (s > 0){buffer[s-1] = 0;std::cout << "client# " << buffer << std::endl;// 发送回去也要被select管理的,因为如何知道发送缓冲区也是就绪的了,发送缓冲区一旦打满了就只能阻塞了。TODAstd::string echo = buffer;echo += " [select server echo]";send(fd, echo.c_str(), echo.size(), 0);}else{if (s == 0)//说明客户端将连接关闭了logMessage(Info, "client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);elselogMessage(Warning, "recv error, client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);close(fdarray_[i]);fdarray_[i] = defaultfd;}}}}void Start(){// 1. 这里我们能够直接获取新的链接吗?// 2. 最开始的时候,我们的服务器是没有太多的sock的,甚至只有一个sock!listensock// 3.在最开始accept的时候,我们是无法知道已经有连接准备就绪的,所以这里如果直接accept就成了阻塞等待了,// 4. 在网络中, 新连接到来被当做读事件就绪!连接就绪我们要做的就是accept而不是read// listensock_.Accept(); 不能!// demo1fdarray_[0] = listensock_.Fd();while (true){// struct timeval timeout = {0, 0};// 因为rfds是一个输入输出型参数,注定了每次都要对rfds进行重置,重置,必定要知道我历史上都有哪些fd?fdarray_[]// 因为服务器在运行中,accept得到的sockfd的值一直在动态变化,所以maxfd也一定在变化, maxfd是不是也要进行动态更新,也要依赖fdarray_[]fd_set rfds;FD_ZERO(&rfds);int maxfd = fdarray_[0];for (int i = 0; i < N; i++){if (fdarray_[i] == defaultfd)continue;// 合法fdFD_SET(fdarray_[i], &rfds);if (maxfd < fdarray_[i])maxfd = fdarray_[i];}int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:// 成功了logMessage(Debug, "有一个就绪事件发生了: %d", n);//就绪事件不一定是listen套接字的新连接就绪了,也有可能是别的套接字数据已经就绪了,可以开始读了。HandlerEvent(rfds);DebugPrint();break;}// sleep(1);}}void DebugPrint(){std::cout << "fdarray[]: ";for (int i = 0; i < N; i++){if (fdarray_[i] == defaultfd)continue;std::cout << fdarray_[i] << " ";}std::cout << "\n";}~SelectServer(){listensock_.Close();}private:uint16_t port_;Sock listensock_;type_t fdarray_[N];// 管理所有的文件描述符fd,select服务器使用的时候需要程序员来维护一个第三方数组来进行对已经获得的socket进行管理。
};
//该版本关注读写事件
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/select.h>
#include "Sock.hpp"
#include "Log.hpp"const static int gport = 8888;#define READ_EVENT (0x1)
#define WRITE_EVENT (0x1 << 1)
#define EXCEPT_EVENT (0x1 << 2)typedef struct FdEvent//一个文件描述符有可能关心读事件也有可能关心写事件
{int fd;uint8_t event;std::string clientip;uint16_t clientport;
} type_t;// static const int defaultfd = -1; // 暂时
static const int defaultevent = 0;class SelectServer
{static const int N = (sizeof(fd_set) * 8);public:SelectServer(uint16_t port = gport) : port_(port){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();for (int i = 0; i < N; i++){fdarray_[i].fd = defaultfd;fdarray_[i].event = defaultevent;fdarray_[i].clientport = 0;//string默认就是空的}}void Accepter(){// std::cout << "有一个新连接到来了" << std::endl;// Accept, 这里在进行Accept会不会被阻塞??不会的!std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);if (sock < 0)return;// 得到了对应的sock, 我们可不可以进行read/recv,读取sock?不能// 你怎么知道sock上有数据就绪了?不知道,所以我们需要将sock交给select,让select进行管理!logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);// 要让select 帮我们进行关心,只要把sock添加到fdarray_[]里面即可!int pos = 1;for (; pos < N; pos++){if (fdarray_[pos].fd == defaultfd)break;}if (pos >= N){close(sock);logMessage(Warning, "sockfd array[] full");}else{fdarray_[pos].fd = sock;// fdarray_[pos].event = (READ_EVENT | WRITE_EVENT);fdarray_[pos].event = READ_EVENT;fdarray_[pos].clientip = clientip;fdarray_[pos].clientport = clientport;}}void Recver(int index){// ServiceIO();// BUGint fd = fdarray_[index].fd;char buffer[1024];ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 读取会被阻塞吗?不会if (s > 0){buffer[s - 1] = 0;std::cout << fdarray_[index].clientip << ":" << fdarray_[index].clientport << "# " << buffer << std::endl;// 发送回去也要被select管理的,TODOstd::string echo = buffer;echo += " [select server echo]";send(fd, echo.c_str(), echo.size(), 0);}else{if (s == 0)logMessage(Info, "client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);elselogMessage(Warning, "recv error, client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);close(fdarray_[index].fd);fdarray_[index].fd = defaultfd;fdarray_[index].event = defaultevent;fdarray_[index].clientip.resize(0);fdarray_[index].clientport = 0;}}// echo servervoid HandlerEvent(fd_set &rfds, fd_set &wfds){for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd)continue;if ((fdarray_[i].event & READ_EVENT) && (FD_ISSET(fdarray_[i].fd, &rfds)))//如果文件描述符想关心读事件并且读事件发生了{// 处理读取,1. accept 2. recvif (fdarray_[i].fd == listensock_.Fd()){Accepter();}else if (fdarray_[i].fd != listensock_.Fd()){Recver(i);}else{}}else if((fdarray_[i].event & WRITE_EVENT) && (FD_ISSET(fdarray_[i].fd, &wfds))){//TODO}else {}}}void Start(){// 1. 这里我们能够直接获取新的链接吗?// 2. 最开始的时候,我们的服务器是没有太多的sock的,甚至只有一个sock!listensock// 3. 在网络中, 新连接到来被当做 读事件就绪!// listensock_.Accept(); 不能!// demo1fdarray_[0].fd = listensock_.Fd();fdarray_[0].event = READ_EVENT;//listen套接字只需要关心读事件,不关心clientip和clientportwhile (true){// struct timeval timeout = {0, 0};// 因为rfds是一个输入输出型参数,注定了每次都要对rfds进行重置,重置,必定要知道我历史上都有哪些fd?fdarray_[]// 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也一定在变化, maxfd是不是也要进行动态更新, fdarray_[]fd_set rfds;//设定两个文件描述符集合,一个关心读事件fd_set wfds;//一个关心写事件FD_ZERO(&rfds);FD_ZERO(&wfds);int maxfd = fdarray_[0].fd;for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd)continue;// 合法fdif (fdarray_[i].event & READ_EVENT)//关心读事件FD_SET(fdarray_[i].fd, &rfds);if (fdarray_[i].event & WRITE_EVENT)//关心写事件FD_SET(fdarray_[i].fd, &wfds);if (maxfd < fdarray_[i].fd)maxfd = fdarray_[i].fd;}int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr);//现在select会关心读写两个事件,之前只关心读事件。switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:// 成功了logMessage(Debug, "有一个就绪事件发生了: %d", n);HandlerEvent(rfds, wfds);DebugPrint();break;}// sleep(1);}}void DebugPrint(){std::cout << "fdarray[]: ";for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd)continue;std::cout << fdarray_[i].fd << " ";}std::cout << "\n";}~SelectServer(){listensock_.Close();}private:uint16_t port_;Sock listensock_;type_t fdarray_[N]; // 管理所有的fd
};
select的缺点:每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不便; 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;同时每次调用select都需要在内核遍历传递进来的所有fd;这个开销在fd很多时也很大;select支持的文件描述符数量太小。
//poll解决了select支持的文件描述符数量太小存在上限的问题
#include <iostream>
#include <string>
#include <cstring>
#include <sys/poll.h>
#include "Sock.hpp"
#include "Log.hpp"const static int gport = 8888;
const static int N = 4096;
const static short defaultevent = 0;
typedef struct pollfd type_t;class PollServer
{public:PollServer(uint16_t port = gport) : port_(port), fdarray_(nullptr){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();fdarray_ = new type_t[N]; //现在空间没有上限了for (int i = 0; i < N; i++){fdarray_[i].fd = defaultfd;fdarray_[i].events = defaultevent;fdarray_[i].revents = defaultevent;}}void Accepter(){// std::cout << "有一个新连接到来了" << std::endl;// Accept, 这里在进行Accept会不会被阻塞??不会的!std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);if (sock < 0)return;// 得到了对应的sock, 我们可不可以进行read/recv,读取sock?不能// 你怎么知道sock上有数据就绪了?不知道,所以我们需要将sock交给select,让select进行管理!logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);// 要让select 帮我们进行关心,只要把sock添加到fdarray_[]里面即可!int pos = 1;for (; pos < N; pos++)//fdarray_数组{if (fdarray_[pos].fd == defaultfd)break;}if (pos >= N){close(sock); // poll -> 动态扩容,扩容失败logMessage(Warning, "sockfd array[] full");}else//找到了闲置的位置{fdarray_[pos].fd = sock;fdarray_[pos].events = POLLIN; // (POLLIN|POLLOUT)fdarray_[pos].revents = defaultevent;}}// echo servervoid HandlerEvent(){for (int i = 0; i < N; i++){int fd = fdarray_[i].fd;short revent = fdarray_[i].revents;if (fd == defaultfd)continue;if ((fd == listensock_.Fd()) && (revent & POLLIN))//如果是listen套接字并且连接事件就绪开始accept获取连接{Accepter();}else if ((fd != listensock_.Fd()) && (revent & POLLIN))//如果普通套接字数据就绪就开始读数据{// ServiceIO();// BUGchar buffer[1024];ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 读取会被阻塞吗?不会if (s > 0){buffer[s-1] = 0;std::cout << "client# " << buffer << std::endl;// fdarray_[i].events |= POLLOUT;//读完数据之后现在开始关心该文件描述符是否空间就绪,让我们可以开始写了。// 发送回去也要被select管理的,TODOstd::string echo = buffer;echo += " [select server echo]";send(fd, echo.c_str(), echo.size(), 0);}else{if (s == 0)logMessage(Info, "client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);elselogMessage(Warning, "recv error, client quit ..., fdarray_[i] -> defaultfd: %d->%d", fd, defaultfd);close(fd);fdarray_[i].fd = defaultfd;fdarray_[i].events = defaultevent;fdarray_[i].revents = defaultevent;}}}}void Start(){// 1. 这里我们能够直接获取新的链接吗?// 2. 最开始的时候,我们的服务器是没有太多的sock的,甚至只有一个sock!listensock// 3. 在网络中, 新连接到来被当做 读事件就绪!// listensock_.Accept(); 不能!// demo1fdarray_[0].fd = listensock_.Fd();fdarray_[0].events = POLLIN;//关心什么时候数据就绪可以开始读了while (true){int timeout = -1;//阻塞int n = poll(fdarray_, N, timeout); // poll内部会对文件描述符的合法性做甄别,不用担心初始化为-1的问题。fdarray_内容管理,合法fd,event全部放入到fdarray_最左侧switch (n){case 0:logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));break;case -1:logMessage(Warning, "%d: %s", errno, strerror(errno));break;default:// 成功了logMessage(Debug, "有一个就绪事件发生了: %d", n);HandlerEvent();DebugPrint();break;}// sleep(1);}}void DebugPrint(){std::cout << "fdarray[]: ";for (int i = 0; i < N; i++){if (fdarray_[i].fd == defaultfd)continue;std::cout << fdarray_[i].fd << " ";}std::cout << "\n";}~PollServer(){listensock_.Close();if(fdarray_) delete []fdarray_;}private:uint16_t port_;Sock listensock_;type_t *fdarray_; // 管理所有的fd
};
poll和select+依旧需要操作系统去进行线性遍历,去检测所有的文件描述符就绪情况,也就是进程区特征的文件当中等待,有一个就绪了就去唤醒select的系统调用,然后系统调用遍历一遍文件,哪些就绪了再加上,一旦需要管理的文件描述符多了成本就太高了。
TCP的PSH标志位就是再操作系统层面让sockfd对应的文件数据处于就绪状态,通知应用层读取。
网卡一旦有数据了,就会像cpu发送中断,cpu此时会识别中断号,然后调用中断向量表对应的下标方法,把数据从外设拷贝到内存。select和poll并不是检测硬件上面是否有数据,而是检测套接字对应的文件结构体中是否有收到数据,检测的是已经把数据从外设拷贝到内存后才通过select/poll监测是否有数据。
epoll采用红黑树维护用户关心的文件描述符,所以增删查改的效率非常高。并且不用再在底层线性遍历所有的文件来确认哪些文件描述符已经就绪了,因为已经把所有的就绪的节点放到了就绪队列当中,上层在检查是否有事件的时候只用检查就绪队列是否为恐就可以了,这就是epoll高效的原因。
V1版本
//Epoll.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <sys/epoll.h>
#include "Log.hpp"
#include "Err.hpp"static const int defaultepfd = -1;
static const int gsize = 128;class Epoller
{
public:Epoller() : epfd_(defaultepfd){}void Create(){epfd_ = epoll_create(gsize);//这个gsize参数不重要,只要是大于零都可以。成功的话返回一个文件描述符if (epfd_ < 0){logMessage(Fatal, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));exit(EPOLL_CREAT_ERR);}}// 用户 -> 内核bool AddEvent(int fd, uint32_t events)//向其中一个文件描述符添加事件{struct epoll_event ev;ev.events = events;ev.data.fd = fd; //用户数据, epoll底层不对该数据做任何修改,就是为了给未来wait就绪返回的!int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);//第一个参数是eppol模型,第二个是control操作,第三个是目标文件描述符,第四个是对应的事件是什么if(n < 0){logMessage(Fatal, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));return false;}return true;//此时红黑树和就绪队列已经创建完成了}bool DelEvent(int fd){// epoll在操作fd的时候,有一个要求,fd必须合法!return epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;//移除的时候就不关心事件了,返回值为零表示移除成功}int Wait(struct epoll_event *revs, int num, int timeout){// return epoll_wait(epfd_, revs, num, timeout);//成功就返回就绪文件描述符个数,并将这些文件描述符相关的状态拷贝到revs当中}int Fd(){return epfd_;}void Close(){if(epfd_ != defaultepfd) close(epfd_); }~Epoller(){}private:int epfd_;
};
//EpollServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <functional>
#include "Epoller.hpp"
#include "Sock.hpp"
#include "Log.hpp"const static int gport = 8888;using func_t = std::function<std::string (std::string)>;class EpollServer
{const static int gnum = 64;public:EpollServer(func_t func, uint16_t port = gport) : func_(func), port_(port){}void InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();epoller_.Create();logMessage(Debug, "init server success");}int Start(){// 1. 将listensock添加到epoll中!bool r = epoller_.AddEvent(listensock_.Fd(), EPOLLIN);assert(r);(void)r;int timeout = -1;//1000就是每个1stimeout一次,如果是0就是非阻塞等待,如果是-1就是阻塞式等待。while (true){int n = epoller_.Wait(revs_, gnum, timeout);//switch (n){case 0:logMessage(Debug, "timeout...");//break;case -1:logMessage(Warning, "epoll_wait failed");//失败了break;default:logMessage(Debug, "有%d个事件就绪了", n);//HandlerEvents(n);//处理n个事件break;}}}void HandlerEvents(int num){for (int i = 0; i < num; i++)//不用遍历到gnum{int fd = revs_[i].data.fd;uint32_t events = revs_[i].events;logMessage(Debug, "当前正在处理%d上的%s", fd, (events&EPOLLIN) ? "EPOLLIN" : "OTHER");if (events & EPOLLIN){if (fd == listensock_.Fd()){// 1. 新连接到来// logMessage(Debug, "get a new link ...");std::string clientip;uint16_t clientport;int sock = listensock_.Accept(&clientip, &clientport);//如果新连接到来了不获取,那么listen套接字就会一直处于就绪状态。if (sock < 0)continue;logMessage(Debug, "%s:%d 已经连上了服务器了", clientip.c_str(), clientport);// 1.1 此时在这里,我们能不能进行recv/read ? 不能,只有epoll知道sock上面的事件情况,将sock添加到epoll中bool r = epoller_.AddEvent(sock, EPOLLIN);assert(r);(void)r;}else//读取事件就绪{// 我们目前无法保证我们读到一个完整的报文// 为什么?完整报文由应用层协议规定, 本质就是你没有应用层协议!// 怎么办?自定义应用层协议char request[1024];ssize_t s = recv(fd, request, sizeof(request) - 1, 0); if (s > 0){request[s-1] = 0; // \r\nrequest[s-2] = 0; // \r\nstd::string response = func_(request);send(fd, response.c_str(), response.size(), 0);}else{if (s == 0)logMessage(Info, "client quit ...");elselogMessage(Warning, "recv error, client quit ...");// 在处理异常的时候,先从epoll中移除,然后再关闭epoller_.DelEvent(fd);close(fd);}} // else 读取事件} // fi : 读事件就绪}}~EpollServer(){listensock_.Close();epoller_.Close();}private:uint16_t port_;//服务器一定要提供端口号Sock listensock_;//监听端口Epoller epoller_;//struct epoll_event revs_[gnum];func_t func_;//数据处理逻辑函数
};
v2版本
select、poll、epoll最基本的情况:一旦有时间就绪,如果底层一直不取的话,底层会一直通知我事件就绪了,换句话说只要底层有数据,我们recve的时候,再也不担心数据没有读完的事情发送。属于水平触发Level Triggered 工作模式。
而边缘触发Edge Triggered工作模式:
1. 一次通知就是一 次系统调用返回,一次返回必定对应一次调用,ET有效减少系统调用次数!
2. ET倒逼程序员尽快取走所有的数据,本质是让TCP底层更新出更大的接受窗口,从而在较
大概率上,提供对方的滑动窗口的大小,提高发送效率。