- 自定义缓冲区
我们在使用TCP/IP编程的时候除了socket有收发数据缓冲区之外,通常我们还要自己定一个数据的收发缓冲区。
1.1 为什么要自定义缓冲区
假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP连接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试write(),因为这样有可能打乱数据的顺序。
另外的话,假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理。
1.2 缓冲区设计的原则
- 一方面希望减少系统调用,一次读的数据越多越划算,那么应该准备一个大的缓冲区;
- 另一方面,希望减少内存占用。如果有10000个并发连接,每个连接一建立就分配各50kB的读写缓冲区(s)的话,将占用1GB内存,而大多数时候这些缓冲区的使用率很低,可以用readv(2)结合栈上空间解决了这个问题;
1.3 建立缓冲区的方式
1.3.1 每次都重新申请缓冲区
每次接收到数据的时候开辟一个缓冲区,然后将接收到的数据填入缓冲区,把缓冲区和IP信息付给任务,压入到任务队列,等任务线程处理; 发送亦然;(小数据可以用栈拷贝的形式)
- 优点:是接收线程可以一直接收,任务线程一直处理,除了任务锁没有其他交互;
- 缺点: 每次都重新申请空间,malloc(或new)消耗量大(可以使用内存池优化);
1.3.2 预先申请缓冲区
预先申请一块大的缓冲区(每个连接各申请一个接收缓冲区和发送缓冲区),接收线程有新数据到来的时候从缓冲区中数据结尾获得可用空间插入数据,把连接信息和整个缓冲区压入任务队列,任务线程处理一个任务的数据,就清空缓冲区该段的数据,然后将缓冲区中后面的数据前移(所以每次都是处理的第一个数据区的数据)
- 优点:减少了malloc
- 缺点:在数据插入和使用的时候都使用的锁,而且有严重的拷贝复制情况,(如果想任务处理和数据接收不互相锁,必须使用多的一份儿数据拷贝,情况就更糟)
1.3.3 使用线程池
使用线程池,每个线程独立的读数据,当数据满足一个包的时候就当做任务处理;然后将链接信息和用户缓冲区添加到监听队列中;有新的数据来到就激活用一个新的线程处理。 好处:仅使用了线程间的锁,(可以使用无锁队列优化),减少数据拷贝; 缺点:线程上下文切换开销大, 数据接收分散;
- 使用read和write读写大量内容
2.1 读大量内容
1. `ssize_t readn(int fd, char *buf, int size)`
2. `{`
3. `char *pbuf = buf;`
4. `int total ,nread;`
5. `for(total = 0; total < size; )`
6. `{`
7. `nread=read(fd,pbuf,size-total);`
8. `if(nread==0)`
9. `return total;`
10. `if(nread == -1)`
11. `{`
12. `if(errno == EINTR)`
13. `continue;`
14. `else`
15. `return -1;`
16. `}`
17. `total += nread;`
18. `pbuf += nread;`
19. `}`
20. `return total;`
21. `}`
2.2 写大量内容
1. `ssize_t writen(int fd, char *buf, int size)`
2. `{`
3. `char *pbuf=buf;`
4. `int total ,nwrite;`
5. `for(total = 0; total < size; )`
6. `{`
7. `nwrite=write(fd,pbuf,size-total);`
8. `if( nwrite <= 0 )`
9. `{`
10. `if( nwrite == -1 && errno == EINTR )`
11. `continue;`
12. `else`
13. `return -1;`
14. `}`
15. `total += nwrite;`
16. `pbuf += nwrite;`
17. `}`
18. `return total;`
19. `}`
更多c++及python系列文章,请关注我的公众号:晟夏的叶。