本文主要给大家分享网络概念之网络编程五种IO模型,前边的章节已经给大家讲述了链路层、物理层、网络层、应用层、传输层,以及套接字socket和DNS等,欢迎学习嵌入式网络编程的朋友关注、转载和发表评论!
(绝对的好文,建议先收藏和转载!)
本文主要的讲述内容为:
1.五种 I/O 模式
1.1阻塞 I/O 模式
1.2非阻塞模式 I/O
1.3I/O 多路复用
1.4信号驱动 I/O 模式
1.5异步 I/O 模式
1.6几种 I/O 模式的比较
1.7 fcntl()函数
1.8 select()函数和 pselect()函数
8.7 五种 I/O 模式
下面我们简单的介绍一个各种 I/O 操作模式。在 Linux/UNIX 下,有下面这五种 I/O 操作方式:
· 阻塞 I/O
· 非阻塞 I/O
· I/O 多路复用
· 信号驱动 I/O(SIGIO)
· 异步 I/O
这章讲述一些 I/O 的细节,你可以在第一次阅读的时候跳过这部分,然后在第二次阅读本书的时候再来读这一节。
一般来说,程序进行输入操作有两步:
1.等待有数据可以读
2.将数据从系统内核中拷贝到程序的数据区。
对于一个对套接字的输入操作,第一步一般来说是等待数据从网络上传到本地。当数据包到达的时 后,数据将会从网络层拷贝到内核的缓存中;第二步是从内核中把数据拷贝到程序的数据区中。
8.7.1 阻塞 I/O 模式
阻塞 I/O 模式是最普遍使用的 I/O 模式。大部分程序使用的都是阻塞模式的 I/O 。缺省的,一个套接字建立后所处于的模式就是阻塞 I/O 模式。
对于一个 UDP 套接字来说,数据就绪的标志比较简单:
· 已经收到了一整个数据报
· 没有收到。
而 TCP 这个概念就比较复杂,需要附加一些其他的变量。
一个进程调用 recvfrom ,然后系统调用并不返回知道有数据报到达本地系统,然 后系统将数据拷贝到进程的缓存中。 (如果系统调用收到一个中断信号,则它的调用会被中断)
我们称这个进程在调用 recvfrom 一直到从 recvfrom 返回这段时间是阻塞的。当 recvfrom 正常返回时,我们的进程继续它的操作。
8.7.2 非阻塞模式 I/O
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核: "当我请求的 I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。"
我们开始对 recvfrom 第三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个
EWOULDBLOCK 的错误。第四次我们调用 recvfrom 函数,一个数据包已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后 recvfrom 正常返回,我们就可以对接收到的数据进行处理了。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling)。应用程序不停的 polling 内核来检查是否 I/O 操作已经就绪。这将是一个极浪费 CPU 资源的操作。这种模式使用中不是很普遍。
8.7.3 I/O 多路复用
在使用 I/O 多路技术的时候,我们调用 select()函数和 poll()函数,在调用它们的时候阻塞, 而不是我们来调用 recvfrom(或 recv)的时候阻塞。图 2-6 说明了它的工作方式。
当我们调用 select()函数阻塞的时候,select()函数等待数据报套接字进入读就绪状态。当select()函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用 recvfrom()函数来将数据拷贝到我们的程序缓冲区中。
和阻塞模式相比较,select()和 poll()并没有什么高级的地方,而且,在阻塞模式下只需要调用一个函数:读取或发送,在使用了多路复用技术后,我们需要调用两个函数了:先调用 select() 函数或 poll() 函数,然后才能进行真正的读写。
多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其 中的任意一个进入读就绪状态,select() 函数就可以返回。
假设我们运行一个网络客户端程序,要同时处理套接字传来的网络数据又要处理本地的标准输入输 出。在我们的程序处于阻塞状态等待标准输入的数据的时候,假如服务器端的程序被 kill(或是自己
Down 掉了),那么服务器程端的 TCP 协议会给客户端(我们这端)的 TCP 协议发送一个 FIN 数据代表终止连接。但是我们的程序阻塞在等待标准输入的数据上,在它读取套接字数据之前(也许是很长一段时间),它不会看见结束标志.我们就不能够使用阻塞模式的套接字。
IO 多路技术一般在下面这些情况中被使用:
· 当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字), I/O 多路复用技术将会有机会得到使用。
· 当程序需要同时进行多个套接字的操作的时候。
· 如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
· 如果一个服务器程序同时使用 TCP 和 UDP 协议。
· 如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd 就是这样的)。
I/O 多路服用技术并不只局限与网络程序应用上。几乎所有的程序都可以找到应用 I/O 多路复用的地方。
8.7.4 信号驱动 I/O 模式
我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。
使用这种模式,我们首先需要允许套接字使用信号驱动 I/O ,还要安装一个 SIGIO 的处理函数。在这种模式下,系统调用将会立即返回,然后我们的程序可以继续做其他的事情。
当数据就绪的时候, 系统会向我们的进程发送一个 SIGIO 信号。 这样我们就可以在 SIGIO 信号的处理函数中进行 I/O 操作(或是我们在函数中通知主函数有数据可读)。
我们现在还不必对 SIGIO 信号处理函数做过多的了解(在下一章中我们会介绍信号的有关内容)。对于信号驱动 I/O 模式,它的先进之处在于它在等待数据的时候不会阻塞,程序可以做自己的事情。当有数据到达的时候,系统内核会向程序发送一个 SIGIO 信号进行通知,这样我们的程序就可以获得
更大的灵活性,因为我们不必为等待数据进行额外的编码。
信号 I/O 可以使内核在某个文件描述符发生改变的时候发信号通知我们的程序。异步 I/O 可以提高我们程序进行 I/O 读写的效率。通过使用它,当我们的程序进行 I/O 操作的时候,内核可以在初始化 I/O 操作后立即返回,再进行 I/O 操作的同时,我们的程序可以做自己的事情,直到 I/O 操作结束,系统内核给我们的程序发消息通知。
基于 Berkeley 接口的 Socket 信号驱动 I/O 使用信号 SIGIO。有的系统 SIGPOLL 信号,它也是相当于 SIGIO 的。
为了在一个套接字上使用信号驱动 I/O 操作,下面这三步是所必须的。
(1)一个和 SIGIO 信号的处理函数必须设定。
(2)套接字的拥有者必须被设定。一般来说是使用 fcntl 函数的 F_SETOWN 参数来进行设定拥有者。
(3)套接字必须被允许使用异步 I/O。一般是通过调用 fcntl 函数的 F_SETFL 命令,O_ASYNC 为参数来实现。
注意:我们在设置套接字的属主之前必须将 SIGIO 的信号处理函数设好,SIGIO 的缺省动作是被忽略。因此我们如果以相反的顺序调用这两个函数调用,那么在 fcntl 函数调用之后,signal 函数调用之前就有一小段时间程序可能接收到 SIGIO 信号。那样的话,信号将会被丢弃。在 SVR4 系统中,
SIGIO 在 头文件中被定义为 SIGPOLL,而 SIGPOLL 信号的缺省动作是终止这个进程。所以我们一定要保证这两个函数的调用顺序:先调用 signal 设置好 SIGIO 信号处理函数,然后在使用 fcntl 函数设置套接字的属主。
虽然设定套接字为异步 I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定产生 SIGIO 信号发送给套接字属主的时候,程序处在什么状态。
1.UDP 套接字的 SIGIO 信号
在 UDP 协议上使用异步 I/O 非常简单.这个信号将会在这个时候产生:
· 套接字收到了一个数据报的数据包。
· 套接字发生了异步错误。
当我们在使用 UDP 套接字异步 I/O 的时候,我们使用 recvfrom()函数来读取数据报数据或是异步 I/O 错误信息。
2.TCP 套接字的 SIGIO 信号
不幸的是,异步 I/O 几乎对 TCP 套接字而言没有什么作用。因为对于一个 TCP 套接字来说,SIGIO 信号发生的几率太高了,所以 SIGIO 信号并不能告诉我们究竟发生了什么事情。在 TCP 连接中,SIGIO 信号将会在这个时候产生:
· 在一个监听某个端口的套接字上成功的建立了一个新连接。
· 一个断线的请求被成功的初始化。
· 一个断线的请求成功的结束。
· 套接字的某一个通道(发送通道或是接收通道)被关闭。
· 套接字接收到新数据。
· 套接字将数据发送出去。
· 发生了一个异步 I/O 的错误。
举例来说,如果一个正在进行读写操作的 TCP 套接字处于信号驱动 I/O 状态下,那么每当新数据到达本地的时候,将会产生一个 SIGIO 信号,每当本地套接字发出的数据被远程确认后,也会产生一个 SIGIO 信号。对于我们的程序来讲,是无法区分这两个 SIGIO 有什么区别的。在这种情况下使用
SIGIO,TCP 套接字应当被设置为无阻塞模式来阻止一个阻塞的 read 和 write(recv 和 send)操作。我们可以考虑在一个只进行监听网络连接操作的套接字上使用异步 I/O,这样当有一个新的连接的时候,SIGIO 信号将会产生。
一个对信号驱动 I/O 比较实用的方面是 NTP(网络时间协议 Network Time Protocol)服务器, 它使用 UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。
因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费的时间。 图 6-8 表示了怎样建立这样的一个 UDP 服务器。
大多数的 UDP 服务都被设计成图左边的模式。但是 NTP 服务器使用的是图右边的技术。当有一个新的数据报到达的时候,SIGIO 的处理函数会取出它放入一个程序等待读取的队列,主程序会从这个队列中读取数据。虽然这样会增加程序代码的长度,但是它能够获取数据包到达服务器程序的准确时间。
8.7.5 异步 I/O 模式
当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。
异步 I/O 和 信号驱动 I/O 的区别是:
· 信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送 SIGIO 消息。
· 异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。如下图,当我们进行一个 IO 操作的时候,我们传递给内核我们的文件描述符,我们的缓存区指针
和缓存区的大小,一个偏移量 offset,以及在内核结束所有操作后和我们联系的方法。这种调用也是
立即返回的,我们的程序不需要阻塞住来等待数据的就绪。我们可以要求系统内核在所有的操作结束后
(包括从网络上读取信息,然后拷贝到我们提供给内核的缓存区中)给我们发一个消息。
8.7.6 几种 I/O 模式的比较
下面这个表格对这几种 I/O 模式进行了对比。
我们可以从中清楚的看出各个模式的差别,自己的程序可以挑选合适的模式来使用。
8.7.7 fcntl()函数
阻塞.你应该明白它的意思。简单的说,阻塞就是"睡眠"的同义词.你也许注意到你运行上面的
listener 的时候,它只不过是简单的在那里等待接收数据。它调用 recvfrom()函数,但是那个时候
(listener 调用 recvfrom()函数的时候),它并没有数据可以接收.所以 recvfrom()函数阻塞在那里
(也就是程序停在 recvfrom()函数处睡大觉)直到有数据传过来。
很多函数都可以阻塞。像 accept()函数是阻塞的,所有以 recv 开头的函数也都是阻塞的。它们这样做的原因是他们需要这样做。
当你一开始建立一个套接字描述符的时候,系统内核就被设置为阻塞状态。如果你不想你的套接字 描述符是处于阻塞状态的,那么你可以使用函数 fcntl()。
fcntl()函数对套接字描述符进行操作,同样也可以对通用文件描述符进行操作。
fcntl()函数声明如下:
#include
#include
int fcntl (int fd, int cmd, long arg);
和套接字有关的 fcntl()函数的选项有 4 种,分为设置套接字属主、获取套接字属主、设置套接字为信号驱动类型和设置套接字为非阻塞类型,如下表所示:
fcntl()函数的命令 F_SETFL 和 F_GETFL,与 O_ASYNC 和 O_NONBLOCK 搭配可以获取或者设置套接字的非阻塞属性。常用的设置非阻塞的操作方式的代码如下:
#include
#include
……
int flags = -1; int err = -1;
……
sockfd = socket(AF_INET, SOCK_STREAM, 0); flags = fcntl(sockfd,F_GETFL,0);
if(flags < 0){
printf("fcntl F_GETFL ERROR");
}
if(!(flag & O_NONBLOCK)){ flags |= O_NONBLOCK;
err = fcntl(sockfd,F_SETFL,flags); if(err < 0){
printf("fcntl F_SETFL ERROR");
}
}else{
printf("socket %d already set to NON_BLOCK",s);
}
先读取套接字描述符属性,当没有设置 O_NONBLOCK 属性的时候,添加 O_NONBLOCK 属性。不要直接设置套接字属性为 O_NONBLOCK,这会将之前套接字的属性覆盖。例如,下面的方式是不好的使用习惯: int flags = O_NONBLOCK;
int err = -1;
err = fcntl(sockfd, F_SETFL, flags );
……
这样将一个套接字设置为无阻塞模式后,你可以对套接字描述符进行有效的"检测"。如果你尝试 从一个没有接收到任何数据的无阻塞模式的套接字描述符那里读取数据,那么读取函数会马上返回–1 代表发生错误,全局变量 errno 中的值为 EWOULDBLOCK。
一般来说,这种无阻塞模式在某些情况下不是一个好的选择。假如你的程序一直没有接收到传过来 的数据,那么你的程序就会进行不停的循环来检查是否有数据到来,浪费了大量的 CPU 时间,而这些
CPU 时间本来可以做其他事情的。
为套接字设置属主是因为信号 SIGIO 和 SIGURG,这两个信号需要使用命令 F_SETOWN 设定了进程属主才能完成。F_SETOWN 的参数 arg 为正数时表示绑定的为进程 ID,为负数时其绝对值为进程组的 ID。
F_GETOWN 获取的值含义与 F_SETOWN 一样。
注:一个套接字在使用 socket()函数生成的时候是没有属主的,当服务器的 accept()返回一个新的套接字描述符时,有了属主,其属主是从监听套接字继承来的。
另外一个比较好的检测套接字描述符的方法是调用 select() 函数。
8.7.8 select()函数和 pselect()函数
select()函数和 pselect()函数用于 I/O 复用,它们监视多个文件描述符的集合,判断是否有符合条件的时间发生。
select()函数
假想一下下面的情况:
你写的服务器程序想监听客户端的连接,但是你同时又想从你以前已经建立过的连接中来读取数据。 你可能会说: "没有问题, 我不就是需要使用一个 accept() 函数和一对儿 recv() 函数吗?"。不要这么着急,你要想想,当你调用 accept() 函数阻塞的时候,你还能调用 recv() 函数吗?"使用非阻塞套接字! "你可能会这么说。是的,你可以。但是如果你又不想浪费宝贵的 CPU 时间,该怎么办呢?
select()函数与之前的 recv()函数和 send()函数直接操作套接字描述符不同。使用 select()函数可以帮助你同时监视许多套接字,可以先对需要操作的文件描述符进行查询,它会告诉你哪一个套接字 已经可以读取数据,哪个套接字已经可以写入数据,甚至你可以知道哪个套接字出现了错误。然后当文 件描述符满足操作的条件的时候才进行真正的 I/O 操作。
下面是 select() 函数的声明:
#include
#include
#include
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select()函数的参数说明:
· numfds:一个整型的变量,它比所有文件描述符集合(readfds,writefds,exceptfds)中的文件描述符的最大值大 1。使用 select()函数前必须计算最大值的文件描述符的值,加上 1 通过 numfds 传入。
· readfds:这个文件描述符集合监视文件描述符集中的每个文件是否有数据可读,当 select() 函数返回的时候,readfds 将清除其中不可读的文件描述符,只留下可读的文件描述符,即可被 recv() 函数和 read()函数等进行读数据的操作。
· writefds:这个文件描述符集合监视文件描述符集中的每个文件是否有数据可写,当 select() 函数返回的时候,writefds 将清除其中不可写的文件描述符,只留下可写的文件描述符,即可被 send() 函数和 write()函数等进行写数据的操作。
· exceptfds: 这个文件描述符集合监视文件描述符集中的每个文件是否发生错误。
如果你想知道是是否可以从标准输入和一些套接字(sockfd)中读取数据,你就可以把文件描述符 和 sockfd 加入 readfds 中。numfds 的数值设成 readfds 中文件描述符中最大的那个加上一,也就是
sockfd+1(因为标准输入的文件描述符的值为 0 ,所以其他任何的文件描述符都会比标准输入的文件描述符大)。
当 select() 函数返回的时候,readfds 将会被修改用来告诉你哪一个文件描述符你可以用来读取数据。使用 FD_ISSET() 宏,你可以选出 select() 函数执行的结果。
在进行更深的操作前,我们来看一看怎样处理这些 fd_sets。下面这些宏可以是专门进行这类操作的:
· FD_ZERO(fd_set *set)将一个文件描述符集合清零。
· FD_SET(int fd, fd_set *set)将文件描述符 fd 加入集合 set 中。
· FD_CLR(int fd, fd_set *set)将文件描述符 fd 从集合 set 中删除。
· FD_ISSET(int fd, fd_set *set)测试文件描述符 fd 是否存在于文件描述符 set 中。
那么,struct timeval 是什么呢?是这样的,一般来说,如果没有任何文件描述符满足你的要求, 你的程序是不想永远等下去的.也许每隔 1 分钟你就想在屏幕上输出信息:
"hello!" 。这个代表时间的结构将允许你定义一个超时。在调用 select()函数中,如果时间超过 timeval 参数所代表的时间长度,而还没有文件描述符满足你的要求,那么 select()函数将会返回,允许你进行下面的操作。
这个 timeval 结构定义如下: struct timeval
{
int tv_sec ; /* 秒数 */ int tv_usec ; /* 微秒 */
};
只需要将 tv_sec 设置为你想等待的秒数,然后设置 tv_usec 为想等待的微秒数(真正的时间就是 tv_sec 所表示的秒数加上 tv_usec 所表示的微秒数)。注意,是微秒(百万分之一)而不是毫秒. 一秒有 1,000 毫秒,一毫秒有 1,000 微秒。所以,一秒有 1,000,000 微秒。
当 select()函数返回的时候,timeval 中的时间将会被设置为执行为 select() 后还剩下的时间。现在,我们拥有了一个以微秒为单位的记时器!但是因为 Linux 和 UNIX 一样,最小的时间片是
100 微秒,所以不管你将 tv_usec 设置的多小,实质上记时器的最小单位是 100 微秒。另外需要注意的是:
· 如果你将 struct timeval 设置为 0,则 select()函数将会立即返回,同时返回在你的集合中的文件描述符的状态。
· 如果你将 timeout 这个参数设置为 NULL,则 select()函数进入阻塞状态,除了等待到文件描述符的状态变化,否则 select()函数不会返回。
下面这段代码演示了从标准输入等待输入,等待 2.5 秒。
#include
#include
#include
/* 标准输入的文件描述符数值 */
#define STDIN 0
int main()
{
struct timeval tv; fd_set readfds;
/* 设置等待时间为 2 秒零 500,000 微秒 */ tv.tv_sec = 2;
tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds);
/* 因为我们只想等待输入,所以将 writefds 和 execeptfds 设为 NULL */
/* 程序将会在这里等待 2 秒零 500,000 微秒,
除非在这段时间中标准输入有操作 */ select(STDIN+1, &readfds, NULL, NULL, &tv);
/* 测试 STDIN 是否在 readfds 集合中 */ if (FD_ISSET(STDIN, &readfds))
{
/* 在,则在标准输入有输入 */ printf("A key was pressed!" );
}
else
{
/* 不在,则在标准输入没有任何输入 */ printf(" Timed out." );
}
}
在标准输入上,你需要输入回车后终端才会将输入的信息传给你的程序。所以如果你没有输入回车 的话,程序会一直等待到超时。
select()函数返回值为 0,-1 或者一个大于 1 的整数值:当监视的文件集中有文件描述符符合要求, 即读文件描述符集中有任何的文件可读、写文件描述符中的文件可写或者错误文件描述符中的文件发生错误时,返回值为大于 0 的正值;当超时的时候返回 0;当返回值为-1 的时候表示发生了错误,其错误值由 errno 指定,值和含义如下:
对 select()函数需要注意的最后一点:如果你的套接字描述符正在通过 listen()函数侦听等待一个外来的网络连接,则你可以使用 select()函数(将套接字描述符加入 readfds 集合中)来测试是否存在一个未经处理的新连接。
下面是 select()函数使用的例子: 服务器端源代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 5555
#define BACKLOG 10
#define MAX(a,b) (a)>(b)?(a):(b) void str_echo(int);
void server()
{
int sockfd, client_fd; struct sockaddr_in my_addr;
struct sockaddr_in remote_addr; socklen_t sin_size;
if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket create error!"); exit(1);
}
my_addr.sin_family=AF_INET; my_addr.sin_port=htons(SERVPORT); my_addr.sin_addr.s_addr=INADDR_ANY; bzero(&(my_addr.sin_zero), 8);
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))==-1)
{
perror("bind error!"); exit(1);
}
if(listen(sockfd, BACKLOG) == -1)
{
perror("listen error!"); exit(1);
}
while(1)
{
sin_size=sizeof(struct sockaddr_in); if((client_fd=accept(sockfd,
(struct sockaddr *)&remote_addr, &sin_size)) == -1)
{
perror("accept error!"); continue;
}
printf("received a connection from %s", inet_ntoa(remote_addr.sin_addr));
if(!fork())
{
close(sockfd); str_echo(client_fd); exit(0);
}
close(client_fd);
}
}
void str_echo(int sockfd)
{
int maxfd;
char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; fd_set rfds;
while(1)
{
FD_SET(0, &rfds); FD_SET(sockfd, &rfds); maxfd = MAX(0, sockfd) + 1;
select(maxfd, &rfds, NULL, NULL, NULL); if(FD_ISSET(sockfd, &rfds))
{
bzero(recvbuf, 1024);
recv(sockfd, recvbuf, 1024, 0); printf("C: %s", recvbuf);
}
if(FD_ISSET(0, &rfds))
{
read(0, sendbuf, 1024);
send(sockfd, sendbuf, strlen(sendbuf), 0); bzero(sendbuf, 1024);
}
}
}
int main()
{
server();
return 0;
}
客户端源代码:
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXSIZE 1024
#define SERVPORT 5555
#define MAX(a,b) (a)>(b)?(a):(b)
void client(int argc, char **argv)
{
int sockfd, maxfd;
char sendbuf[MAXSIZE] = {0}; char recvbuf[MAXSIZE] = {0}; struct sockaddr_in serv_addr;
// struct hostent *host; fd_set rfds;
/* if(argc < 2)
{
fprintf(stderr, "Please enter the sserver's name!"); exit(1);
}
if((host = gethostbyname(argv[1])) == NULL)
{
herror("get host by name error!"); exit(1);
}
*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("socket create error!"); exit(1);
}
serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVPORT);
// serv_addr.sin_addr = *((struct in_addr *)host->h_addr); serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(serv_addr.sin_zero), 8);
if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
{
printf("connection error!"); exit(1);
}
FD_ZERO(&rfds); while(1)
{
FD_SET(0, &rfds); FD_SET(sockfd, &rfds); maxfd = MAX(0, sockfd) + 1;
select(maxfd, &rfds, NULL, NULL, NULL); if(FD_ISSET(sockfd, &rfds))
{
bzero(recvbuf, MAXSIZE); recv(sockfd, recvbuf, MAXSIZE, 0); printf("S: %s", recvbuf);
}
if(FD_ISSET(0, &rfds))
{
fgets(sendbuf, MAXSIZE, stdin); send(sockfd, sendbuf, strlen(sendbuf), 0); bzero(sendbuf, MAXSIZE);
}
}
// close(sockfd);
}
int main(int argc, char **argv)
{
client(argc, argv); return 0;
}
pselect()函数
select()函数是用一种超时轮循的方式来查看文件的读、写和错误的可操作性,在Linux下,还有一个相似的函数pselect()。
下面是 pselect()函数的声明:
#include
#include
#include
#include
int pselect(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout Const sigset_t *sigmask);
pselect()函数含义基本与select()函数一致,除了如下几点:
· 超时的时间结构是一个纳秒级的结构,原型如下。struct timesped
{
long tv_sec; /*超时的秒数*/
long tv_nsec; /*超时的纳秒数*/
};
因为在Linux平台下内核调度的精度为10毫秒级,所以即使设置了纳秒级的分辨率,也达不到设置的精度。
· 增加了进入pselect()函数时替换掉的信号处理方式,当sigmask为NULL的时候,与select的方 式一致。
· select()函数在执行之后可能会改变timeout的值,修改为还有多少时间剩余,而pselect() 函数不会修改该值。
ready = pselect(numfds, &readfds, &writefds, &exceptfds,
timeout, &sigmask);
相当于如下的select()函数,在进入select()函数之前先手动将信号的掩码改变,并保存之前的掩 码值;select()函数执行后,再恢复为之前的信号掩码值。
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask); ready = select(numfds, &readfds, &writefds,
&exceptfds, timeout); sigprocmask(SIG_SETMASK, &origmask, NULL);