Unix网络编程学习日记(五):基于epoll与管道的socket客户端

来源:互联网 发布:手机淘宝如何清除收藏 编辑:程序博客网 时间:2024/06/05 07:14

这又是一个不成熟的想法。为了尽可能减少对多线程、进程的依赖,减少内存占用,将之前的客户端程序中“发送”和“接收”线程合二为一,尝试使用非阻塞模式socket配合I/O复用实现socket客户端。不过,查阅资料后发现I/O复用更适合用于服务器程序而不是客户端。
其中很重要的一个原因是,使用I/O复用模型时需要对流进行监控,动态调用流对应的处理程序。对于一个简单的客户端程序,动态接收服务器发来的消息很简单,只要监控socket的“读”事件即可。而如何主动触发I/O复用的“写”功能是个问题。以下是《Unix网络编程》中对select函数触发socket“写”事件的条件。

下列四个条件中的任何一个满足时,一个套接字准备好写。
1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前 大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我 们把这样的套接字设置成非阻寒(第16章),写操作将不阻塞并返回个正值(例如传输层接受的字节数)。我们可以使用SQ_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP 和UDP套接字而言,其默认值通常为2048.
2. 该连接的写半部关闭.对这样的套接字的写操作将产生SIGPIPE信号(5.12节)。
3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。 d)其上有一个套接字错误特处理。对这样的套接字的写操作将不阻塞并返回一I(也就是返 回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定 SQ.ERROR套接字选项调用getsockopt获取并清除

可见,在网络连接正常的情况下,I/O复用模型持续触发“写”事件,难以由程序手动控制。
在Unix网络编程学习日记(四):基于select的单线程半双工socket客户端的实现中,我使用select监控标准输入stdin,即键盘键入数据并按下回车时,select触发stdin的读事件,再在处理事件的过程中将stdin中的数据通过socket发送出去。但是使用这种思路编写的程序难以扩展功能。
后来在网上找到了一个思路,即通过匿名管道作为中间层来手动触发I/O复用模型的“读”事件:http://blog.csdn.net/aquester/article/details/36627721。虽然局限性也很大,而且徒增了数次内存复制,不过管道总比标准输入流容易控制许多。
想象中的流程大概是这样:
这里写图片描述
但是现实很骨感,由于使用了I/O复用模型需要配合非阻塞socket使用,实际会遇到很多问题。特别是发包速度快时,send函数会抛出EAGAIN错误。所以要考虑到数据的重发。好在epoll有一个边缘触发模式,可以在发送事件(EPOLLOUT)从无效到有效这一过程发生时激活重发功能。为了重发,又需要建立缓存暂存发送失败的数据,而重新发送的时候,又可能遇到发送失败,这时还要考虑多次发送失败的数据与新数据的时序……
这里写图片描述
实际的程序其实远比流程图麻烦,各种循环、判断层层嵌套。同时,由于从磁盘读取数据的速度远快于网络发送速度,缓冲队列常常占用大量内存空间。如果想让生产者/消费者速度匹配,又要涉及到阻塞队列、线程同步等机制了。所以说为何不一开始就用多线程/进程来实现这个客户端程序呢?
《Unix网络编程》中讲解非阻塞式I/O时有这样一句话,对此表示认同:

(socket客户端的)非阻塞版本几乎比select加阻塞式I/O版本快出一倍。fork版本比非阻塞版本稍慢,然而考虑到非阻塞版本代码相比fork版本代码的复杂性,我们推荐简单得多的fork版本。

由此得到了个教训,在写程序之间要尽可能地做好概要设计,防止走弯路。当然,在学习阶段走的这些弯路总带来意外收获。

阅读全文
0 0
原创粉丝点击