上节实验了关于标准IO能够明显提升运行效率, 那么本节就来将之前使用的系统IO改为标准IO试试.
修改为标准IO
本节修改的代码是epoll
中修改后的代码. 其实就只需要把客服端的read
函数换成标准IO的fgets
即可.
while(1){
testrfds = rfds;
select(sockfd + 1, &testrfds, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &testrfds))
{
// 将read替换成fgets即可
if(fgets(buf, sizeof(buf), stdin) == NULL){
shutdown(sockfd, SHUT_WR);
stat = 1;
FD_CLR(STDIN_FILENO, &rfds);
continue;
}
send(sockfd, &buf, strlen(buf), 0);
}
if(FD_ISSET(sockfd, &testrfds))
{
n = recv(sockfd, buf, sizeof(buf), 0);
if(n == 0){
if(stat == 1)
break;
else
exit(1);
}
write(STDOUT_FILENO, buf, n);
}
}
运行程序的时候你会发现并没有任何的问题, 一切都与之前的效果一样. 恰恰这种非常隐患的问题不容易暴露出来.
标准IO的问题
什么? 不管怎么运行都与之前的功能一样啊, 哪里还会有错误呢? 看不出来的原因是buf
的大小很大, 我们只需要将buf
的大小修改一下, 这里为了明显我改成buf[3]
接下来再来运行看看.
客服端修改 :
// char buf[1024] 修改
char buf[3];
完整代码 : IObuffer.c
服务端 :
./a.out 1 8080 127.0.0.1
客服端
./a.out 2 8080 127.0.0.1
剩下的45没有被发送给服务端还在客服端的用户缓冲区中. 这种现象是不是很像epoll
的ET模式.
标准IO告诫
标准IO是带有缓冲区的, 会先将数据拷贝到IO缓冲区中, 所以345还在IO缓冲区中没有被发送; 而系统IO是不带缓冲的, 所以直接写入TCP输入或者输出缓冲区等待发送.
以下问题参考 : socket中用户自定义缓冲区的原因及方式
-
既然标准IO还要将数据复制一次是不是效率就低很多啊?
不是. 带缓冲的IO的传输效率要高.
- 假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP连接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试
write
,因为这样有可能打乱数据的顺序。 - 假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理 ( 有点像 Nagle算法 )
举一个例子 : 当要发送很多数据时, 此时的套接字的发送缓冲区已经满了, 此时
write
函数就会阻塞直到有发送缓冲区剩余的空间能够装下新的数据为止. 如果自带缓冲区, 那么就算发送缓冲区满了IO操作也不会阻塞, 直接将数据写入用户缓冲区中然后又可以继续调用IO操作, 当发送(接收)缓冲区有空间了自动将数据从用户缓冲区复制到套接字中就行了.
- 假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP连接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试
-
那为什么不能直接使用标准IO呢?
UNIX网络编程
中提到过 : 不要将 stdio 库提供的 C 语言函数与 IO 复用混合使用. 标准IO与IO多路复用混合会有非常隐患的问题.
-
那么怎么实现用户缓冲区呢 ?
-
方法1: 每次接收到数据的时候开辟一个缓冲区(数据头是数据大小的);然后接受数据填入缓冲区,把缓冲区和IP信息付给任务,压入到任务队列,等任务线程处理; 发送亦然;(小数据可以用栈拷贝的形式)
- 好处:是接收线程可以一直接收,任务线程一直处理,除了任务锁没有其他交互
- 缺点: 每次都重新申请空间,malloc(或new)消耗量大(可以使用内存池优化)
-
方法2: 预先申请一块大的缓冲区(每个连接各申请一个接收缓冲区和发送缓冲区),接收线程有新数据到来的时候从缓冲区中数据结尾获得可用空间插入数据,把连接信息和整个缓冲区压入任务队列,任务线程处理一个任务的数据,就清空缓冲区该段的数据,然后将缓冲区中后面的数据前移(所以每次都是处理的第一个数据区的数据)
- 好处:减少了malloc
- 缺点:在数据插入和使用的时候都使用的锁,而且有严重的拷贝复制情况,(如果想任务处理和数据接收不互相锁,必须使用多的一份儿数据拷贝,情况就更糟)
-
方法3:使用线程池,每个线程独立的读数据,当数据满足一个包的时候就当做任务处理;然后将链接信息和用户缓冲区添加到监听队列中;有新的数据来到就激活用一个新的线程处理
- 好处:仅使用了线程间的锁,(可以使用无锁队列优化),减少数据拷贝
- 缺点:线程上下文切换开销大, 数据接收分散
-
方法4: 使用绑定线程(一个CPU核一个线程),线程分摊活动连接句(使用管道分发(hash)),单线程接收数据到缓存,然后结合协程做任务处理。
- 这个可以参考
Nginx
.
- 这个可以参考
-
小结
关于使用标准IO, 花了一节来说明有隐患问题, 还花了一个说明缓冲区的好处(提升传输效率). 主要是对缓冲有一个理解, 特别后续接触到非阻塞时就能明白缓冲区的作用是很大的. 后续提升
libevent
源码Nginx
源码