- 浏览: 1306853 次
- 性别:
- 来自: 武汉
文章分类
最新评论
-
flowershade:
javabean到底包不包含业务逻辑?
JavaBean总结 -
ahwoniu7:
很强大呀
北京工作这几年 – 一个软件工程师 -
YYang5968:
有些不明白是什么意思,不过我想也不需要很明白吧,会用就行了,呵 ...
MySQL数据类型简介 -
<>++<:
zhufeng1981 写道huang_yong 写道感觉还是 ...
Java 枚举7常见种用法 很好的文章 -
<>++<:
>JDK1.5引入了新的类型——枚举枚举1.2就有了 后 ...
Java 枚举7常见种用法 很好的文章
在套接口上使用标准I/O
在前面章节的例子代码中我 们已经使用了read(2)或是write(2)系统调用在套接口上执行读取和写入操作。这个规则的一个例外就是recvfrom(2)和sendto (2)函数,这两个函数用来读写数据报。然而,使用read和writte函数调用却有一些程序上的缺点。
这一章我们将会讨论以下内容:
如何使用fdopen(3)将一个套接口与一个FILE流相关联
如何创建并读写FILE流
关闭与套接口相关联的流的问题
为我们的FILE流选择并创建合适的缓冲技术
中断系统调用的问题
掌握了这些内容将为我们提供解决我们网络程序的额外方法。
理解标准I/O的需要
Linux中的stdio(3)程序符合ANSI C3.159-1989标准。这个接口的标准化可以帮助程序将程序移植到多个平台。例如,当我们要将源代码从其他的UNIX系统移植到我们自己的Linux平台时,这个程序对于我们来说也许有用。
stdio包本身是与read和writte调用相冲突。然而,我们却使用标准的I/O调用,因为根据我们程序的需要,他们可以方便的每次为我们提供一行或是一个字符。例如,read调用,并不会为我们的程序返回一个文本行。相反,他们返回尽可能多的数,甚至是多行文本。
当写入套接口时,标准I/O例程可以允许我们的程序每次写入一个字符,而不会造成大的内容覆盖。另一方面,每次调用writte写入一个字符花费太大。标准I/O函数允许我们的程序使用合适的数据单元进行操作。
stdio同时我们的程序提供了数据缓冲功能,包括输入与输出。当使用缓冲功能,可以极大的改善我们程序的性能。不幸的是,缓冲会为某些格式的通信造成困难,所以并不是总是使用缓冲。
在这一章我们假设已经熟悉了stdio的基本功能。这通常是在C程序教程,同时也是C语言本身所教的内容。相应的,这一章我们将会专注于我们应密切关注的东西,而其他的一些细微的地方并不明显,因为他适用于套接口编程。
将一个套接口与一个流相关联
stdio流是由FILE控制块来进行管理的。例如,我们也许已经多次编写了如下所示的代码:
FILE *in;
in = fopen(pathname,"r");
if ( in == NULL ) {
fprintf(stderr,"%s: opening %s for read.\n",strerror(errno),pathname);
exit(1);
}
在这个例子中,变量pathname所指定的文件将会打开来读。如果open调用成功,变量in将会接收一个指向为我们管理流I/O的FILE结构。否则,变量in接收一个NULL指针,而我们的程序也必须处理或是报告这个错误。
然而,对于套接口编程,并没有可以打开一个套接口的stderr调用。那么程序员如何将一个流与一个套接口相关联呢?下面的内容将会我们找到答案。
使用fdopen(3)将一个套接口与一个流相关联
for函数也许对于十分熟悉。然而,对于许多人来说,fdopen函数却是新的,或是不熟悉的。因为这个函数对于我们中的许多人来说是新鲜的。这个函数的概要如下:
#include <stdio.h>
FILE *fdopen(int fildes,const char *mode);
这个函数需要两个参数:
1 为执行I/O而使用的一个整型文件描述符(fildes)
2 要使用的标准I/O模式。这可以是一个打开模式,与fopen模式参数相同。例如,r表示这个流要打开来读,而w表示这个流要打开来写。
与fopen函数相同,如果函数成功,则会返回一个指向控制FILE结构的指针。否则,将会返回一个NULL指针来表明发生了错误,而错误号将会存放在外部变量errno中。
在这里要注意,第一个参数是一个文件描述符。我们也许会记起由socket函数返回的也是一个文件描述符。这样将一个已存在的套接口与一个流相关联与为可能。下面的代码是一个将一个套接口与一个可以进行读或是写的流进行关联的例子:
int s; /* socket */
FILE *io; /* stream */
s = socket(PF_INET,SOCK_STREAM,0);
...
io = fdopen(s,"r+");
if ( io == NULL ) {
fprintf(stderr,"%s: fdopen(s)\n",strerror(errno));
exit(1);
}
上面的例子演示了存放在变量s中的套接口号如何与一个名为io的FILE流建立关联。这个例子中的fdopen函数中的模式参数为输入和输出建立了一个流。在这个函数调用成功执行以后,其他的标准I/O函数,如fgetc(3)就可以进行操作了。
关闭一个套接口流
上面的代码演示了如何将一个套接口与一个标准I/O流建立关联。程序编写者也许会问,如何关闭这个套接口或是流?
阅 读fdopen的手册页可以知道传递到函数的文件描述符是不可复制的。这就意味着fildes参数是为物理读写而用的文件描述符。使用dup函数不可以复 制这个文件描述符。相应的,在成功的执行了fdopen函数调用以后,我们并不希望调用close函数。这会关闭流变量io所用的文件描述符。
然而,如果我们调用fclose(io),实际上我们就会关闭套接口s。这可以通过检测fclose函数所用的内部步骤来理解:
1 冲洗任何缓冲的数据到文件描述符。如果存在缓冲的数据,这就会造成为文件描述符调用writte函数。
2 使用close函数关闭流所用的文件描述符。
3 释放缓冲所占用的存储空间以及FILE结构本身。这是通地free函数来执行的。
我们在第2步可以看到套接口s可以由通常的fclose函数来关闭。
使用单独的读写流
在 上面的代码例子中我们看到如何将一个套接口与一个允许输入和输出的流进行关联。尽管这在概念上可行,实际上为输入和输出打开一个单独的流却是更为安全的做 法。原因就在于流缓冲在一个流上比在两个单独的流上起着更为复杂的角色作用。Linux的fdopen的手册页表明对于I/O流而言,在读和写的相互切换 之间通常需要执行fgetpos(3)调用。
在这里我们并不会试着解释为什么以及何时这些情况会适用于一个I/O流,相反,我们在这里建议为读和写使用两个单独的流。这个技术会有一些额外的开销,但是在许多情况下却提供了更好的缓冲性能。
下面的例子显示了如何由一个文件描述符创建一个单独的读和写流。
int s; /* socket */
FILE *rx; /* read stream */
FILE *tx; /* write stream */
s = socket(PF_INET,SOCK_STREAM,0);
rx = fdopen(s,"r");
if(rx==NULL)
{
fprintf(stderr,"%s:fdopen(s,'r')\n",strerror(errno));
exit(1);
}
tx = fdopen(dup(s),"w");
if(tx==NULL)
{
fprintf(stderr,"%s:fdopen(s,'w')\n",strerror(errno));
exit(1);
}
在这里也许我们已经看到了dup函数调用。这是非常重要的,因为不同的流应使用不同的文件描述符。因为这个简单的原因,当调用fclose(tx)时,他并不会关闭rx流所用的同一个文件描述符。
复制一个套接口
为了理解为什么上面的代码例子可以工作,我们需要在这里介绍dup(2)函数的使用:
#include <unistd.h>
int dup(int oldfd);
UNIX 系统,与Linux相类似,允许多个文件描述符指向同一个打开的文件(或者,在此种情况下为同一个套接口)。通过使用套接口s作为输入参数来调用dup函 数,我们将会得到一个新的文件描述符。这个新的文件描述符同样指向原始的套接口s。然而,在执行了这个复制以后,只有当这两个文件描述中的最后一个关闭 时,内核才会关闭套接口本身。
数字总是有助于阐明一个例子。正因为这样,在上面的例子代码中,在文件描述符3上创建一个套接口。并且假设套接口s用下面的代码进行复制:
int s2; /* dup'ed socket */
s2 = dup(s); /* duplicate */
如果文件描述符4当前并没有使用,在上面的例子中,Linux内核将会返回4。这允许文件描述符3(变量s)和文件描述符4(变量s2)同时指向同一个套接口。
关闭双重流
在 我们创建了两个流之后,我们可以安全的在我们的rx流上使用如fgetc(3)或是fgets(3)之类的函数。使用fputs(3)或是fputc (3)的写入函数将会在输出流tx上执行。在我们的程序流中使用单独的流可以消除缓冲交互并且移除在各种点上调用fgetpos(3)函数的需要。
然而,当我们处理完毕这些流以后,我们必须执行下面的操作:
调用fclose(rx)来关闭输入流
调用fclose(tx)来关闭输出流
前一个步骤会执行如下操作:
为写入流冲洗缓冲数据
关闭文件描述符
释放缓冲区
释放由FILE对象管理的流
结束通信
读者也许会想起我们在前面所介绍的shutdown(2)函数。如何在需要的时候调用这个函数呢?
使 用双重流的方法,我们也许会基于一个错误的假设而误用shutdown函数。例如,在上面的例子代码中实际上有两个文件描述符,也许会试着在每一个文件描 述符上调用shutdown函数。在其中一个上,我们也许会关闭读边,而在另一个文件描述符上,我们也许会关闭写边。不要这样做!
回忆第 一章,我们在那里介绍了shutdown函数。在那里我们介绍到,shutdown函数的一个优点就是他们忽略在套接口的打开引用的数目。相应的,在复制 的套接口上调用shutdown函数也会影响到同一个套接口的所用引用。从而,也会影响到我们连接到这个套接口上的所有已存在的流。
当结束我们的进程通过套接口与远程进程上的通信时,需要考虑三个基本的条件:
进程不再写入任何数据,但是却期望接收数据(只关闭写边)
进程不再接收任何数据,但是却期望写入数据(只关闭读边)
进程不再读取或是写入任何数据(关闭读写边)
在上面的例子代码中使用两个流的条件将会在下面的内容中进行相应的讨论。
仅关闭写入边
在这种情况下,调用shutdown函数向Linux内核表明调用进程不再写入任何数据。
因为shutdown函数影响套接口,而不影响文件描述符,也不影响实际使用的文件描述符。然而为了解释程序的需要,我们在这里使用写入流来完成这个任务。
这个任务由以下几步组成:
1 使用fflush(3)函数来冲洗可能存在于流缓冲区中的任何数据。
2 使用shutdown函数关闭套接口的写入边。
3 使用fclose(3)函数关闭流。
在关闭写入边之前,我们必须冲洗输出流。这是非常重要的,因为在缓冲区中也许存在一些未写入的数据。这可以用下面的代码来完成:
fflush (tx); /* Flush buffer out */
要完成shutdown操作,我们需要得到流tx所用的文件描述符。我们可以用下面的C宏来完成这个操作:
#include <stdio.h>
int fileno(FILE *stream);
我们只需要简单的将流指针作为输入传递给宏,而这个宏就会返回这个流指针所用的文件描述符。这是一个可移植的,而且是唯一可以接受的方法。使用这个宏,我们可以用下面的代码来执行关闭操作:
shutdown(fileno(tx),SHUT_WR);
这个任务的最后一步就是简单的调用fclose函数来关闭我们不再需要的tx流:
fclose(tx);
将所有这些代码放在一起,关闭操作如下面的C代码所示:
fflush(tx);
shutdown(fileno(tx),SHUT_WR);
fclose(tx);
这个序列保持rx流完整从而可以进行读操作,但是却强制tx流中的所有缓冲数据写入套接口。shutdown函数通知内核加速发送套接口数据,因为不再有更多的数据需要发送。最后,在tx流上的fclose函数关闭文件描述符,并且释放也流相关联的内存资源。
只关闭读取端
这个过程与只关闭写入端相类似。这个过程有一些轻微的变化:
1 调用shutdown函数来表明不再希望更多的接收数据
2 使用fclose函数关闭流
我们会注意到在这里并不需要fflush操作。这个过程总结如下:
shutdown(fileno(rx),SHUT_RD);
fclose(rx);
再一次注意为流rx获取文件描述符的fileno宏的移植用法。尽管在上面的例子代码中原始的套接口号存放在变量s中,为了程序解释的需要,我们使用fileno宏来得到流所使用的文件描述符。
这个过程向Linux内核表明不再读取更多的数据,同时关闭并释放rx的流资源。然而,程序仍然可以向流tx中写入数据。
同时关闭读取和写入端
这个过程初看起来更为复杂,但是实际上是相当简单的:
1 通过fclose调用关闭写入流
2 通过fclose调用关闭读取流
在第一步并不需要fflush调用,因为写入流的fclose函数会隐式的保证冲洗操作。第2步关闭读取流会关闭套接口的最后一个打开的文件描述符,所以套接口会隐式的关闭读取和写入端。
这 个规则的一个例外,也许会是一个关键点,依赖于我们程序的设计。如果我们的进程进行了fork操作,那么也许会其他的打开的文件描述符指向我们的套接口。 我们也许会记起只有最后一个close发生时才会真正的关闭一个套接口。如果对于这一点仍有怀疑,我们可以跟随如下的更为详细的步骤来进行查看:
1 使用fclose来关闭写入流。这会强制所有未写入的数据写入套接口,并且释放写入流的资源。
2 调用shutdown函数来关闭套接口的读取和写入。
3 使用fclose函数关闭读取流来关闭读文件描述符,并且释放流缓冲和FILE结构。
这个过程的一个变化就是如下所示的调用了shutdown函数:
shutdown(fileno(rx),SHUT_RDWR);
完整代码所下:
fclose(tx);
shutdown(fileno(rx),SHUT_RDWR);
fclose(rx);
这个过程是最后的方法,尽管我们对两步的过程没有任何问题。这个过程总是可以完成我们的任务,而不管任何其他的也许会影响其他过程的程序修改。
处理中断
阅读fread(3)或是fwrite(3)的Linux手册页并不会得到他们可能返回的错误的更多信息。相反,只描述了返回值,来表明是否发生了错误。
在AT&T System V接口定义文档中对fread和fwrite进行了更为详细的描述。这个UNIX文档比较有趣的就是也许会返回EINTR错误代码的事实。
EINTR 错误表明发生了一个中断系统调用。当我们的进程被信号通知,调用一个信号处理函数来进行处理,并且这个处理由他的调用返回时,会返回这个错误。并不是所有 的函数都会返回这个错误代码,而是函数被阻塞相当长的时间时才会返回。当然,在一个套接口上等待到来数据的read调用属于这一类。
我们也许会记起fread函数只是read函数调用上的一个简单的函数层,而read函数会在缓冲区需要时调用。相应的,如果我们的进程收到信号,fread函数也会得到EINTR错误代码。
这对于fwrite函数调用也是相同的。如果要有大量的数据要写入一个套接口,底层的write函数调用也会阻塞较长时间。如果他被阻塞了,就会收到一个信号并进行处理,write函数就会返回一个EINTR错误代码,而这也会使得fwrite函数返回这个错误。
我们这里使用"也许"这个单词,是因为这依赖于我们正在使用的stdio库的设计。一些UNIX实现会隐藏这些错误,而其他的一些系统会返回EINTR代码。因为Linux已经由libc5库移到较新的glibc2版本,所以我们的情况也许会有变化。
如果我们在代码中允许EINTR,那么我们也许可以用下面的代码片段:
int ch;
do {
clearerr(rx);
ch = fgetc(rx);
} while ( ferror(rx) && errno == EINTR );
基本步骤如下:
1 调用clearerr函数来清除在这个流上可能发生的任何错误。
2 执行我们的输入,输出操作。
3 如果操作失败,并且errno的值为EINTR,那么返回步骤1。
如果代码由这个循环退出,那么就表明这个操作或者成功了,或者因为一个非EINTR的不同错误代码而失败了。这里的基本原则就是当返回EINTR代码时,我们重试此操作。
如果我们的情况属于下面情况之一,我们就要包含处理EINTR的代码:
源代码要移植到基他的UNIX平台上
在我们的程序中偶尔发生EINTR错误
GNU C库改变了隐藏EINTR错误的方针
如果我们的程序并不处理信号,或者是他已经忽略信号,我们就不再需要关心EINTR错误代码了。
为其他函数处理EINTR
在这里需要注意的一点就是EINTR是我们将会用来进行套接口编程的主机上其他函数的一个潜在瓿。信号将会影响的函数包括:
connect(2)
accpet(2)
read(2)
write(2)
readv(2)
writev(2)
recvfrom(2)
sendto(2)
select(2)
poll(2)
这个列表并不是一份详尽的列表,而其中的一些函数我们也没有讨论到。然而,这些函数只是通常使用的,会被信号处理影响的函数。
为了保持例子程序的矮小和易于理解,在我们的例子中并没有考虑EINTR的问题。然而,我们必须允许任何产品级别代码中的EINTR的发生。
定义缓冲操作
当我们使用stdio程序时,我们通常使用了其后的一些缓冲操作。例如,缓冲写减少了write系统调用的频繁程序。这会增加整个系统的输出效率。同样的,读请求也是会进行缓冲的。
例如,fgetc会由缓冲区中取得一个字符。只有缓冲区为空,并且要请求更多的数据时才会调用read系统调用。这样的操作改善了I/O效率。
当一个流的文件描述符是一个终端设备时,Linux的I/O是行缓冲的。另一方面,文件是全缓冲的(在一个大的块中进行缓冲)。
在Linux下使用FILE流有三种基本的缓冲模式可以选择。他们是:
全缓冲(或块缓冲)
行缓冲
无缓冲
对于一些套接口编程而言,选择无缓冲模式是比较合适的,虽然在这样的方式下并不会由缓冲得到效率。然而,这确实为我们省去了调用fflush的担忧。
当我们的套接口交互是基于文本行时,通常使用行缓冲模式。使用行缓冲模式意味着我们从不会强制调用fflush来强制将最后一行文本写入套接口。
如果我们选择使用全缓冲模式,那么我们就要在希望实际的写入套接口时调用fflush函数。否则,我们的数据也许就会处在一个输出缓冲区中,而我们的程序却在等待响应,因为输出数据并没有发送。
缓冲控制函数概要如下所示:
#include <stdio.h>
int setbuf(FILE *stream,char *buf);
int setbuffer(FILE *stream, char *buf, size_t size);
int setlinebuf(FILE *stream);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
这些函数会允许调用者来改变指定流的缓冲模式。Linux文档表明这些函数可以在任何时候调用来改变流的缓冲特性。
这些函数的参数描述如下:
参数stream是将会影响到的流的FILE指针
参数buf是一个指向提供的缓冲区的指针。这个指针可以为NULL。如果需要一个缓冲区,但是却指定了NULL,那么就会分配一个内部缓冲区。
参数size为所提供的缓冲区的大小,或者是分配的内部缓冲区的大小。
参数mode为要使用的缓冲区的模式。
建议的缓冲区尺寸是由包含的stdio.h文件作为BUFSIZ宏来定义的。下表列出了setvbuf函数可用的mode参数值:
C宏 描述
_IOFBF 流上的输入/输出操作将会全缓冲
_IOLBF 流上的输入/输出操作将会行缓冲
_IONBF 流上的输入/输出操作不会进行缓冲
作为例子,如何使用这些函数来改变套接口流tx来使用行缓冲模式,我们可以在fdopen调用后使用下面的函数代码:
setlinebuf(tx); /* Line Buffered Mode */
我们也可以使用setvbuf函数来直接进行改变:
setvbuf(tx,NULL,_IOLBF,BUFSIZ);
在这个例子中,我们允许软件分配他自己的BUFSIZ字节的大小的内部缓冲区。然而,流的缓冲模式被设置为行缓冲,因为在函数调用中的使用了_IOLBF宏。
在套接口上应用流
现在我们要来介绍一些使用我们已经讨论的这些概念的源码了。我们下面要介绍的服务器程序实现了一个计算器。他会接受两个长整型数,用栈进行存储,然后在其上执行运算。运算的结果会放在栈顶。
整型算法将会由GNU多精度库(GMP)来进行执行。这个库允许无限制尺寸的整型数进行计算。在这里我们并不会描述GMP库,而这个段代码的目的只是简单的的演示使用FILE流的一些服务器概念。这个服务器会帮助演示一些我们将会下一章涉及的高级概念。
实现mkaddr()函数
在这里将会提供mkaddr.c子函数,从而使得这个工程更易读。这里所介绍地mkaddr将会接受一个包含IP地址和一个可选的端口号,或者是一个主机名和可选端口号的输入字符串。端口号也可以是一个符号网络服务名,如telnet或是ftp。这个函数概要如下:
int mkaddr
(void *addr, int *addr_len, char *str_addr, char *protocol);
函数的参数描述如下:
1 参数addr指向接收套接口地址结构。这是返回的套接口地址。这个值不可以为空。
2 参数addr_len是一个指向整数值的指针,当函数返回时将会由所addr中所创建的地址的长度来进行填充。输入值所指向的必须为包含由addr所指向的区域的最大尺寸。
3 参数str_addr为符号主机名或是可选的端口号。这个参数将会在后面进行描述。一个NULL指针意为一个"*"字符串。
4 参数protocol指明了服务将会使用的协议。一个NULL指针意为tcp字符串。
str_addr输入字符串的设计将尽可能灵活。他包含由冒号分隔的两部分:
host_name:service
字符串的host_name可以为下列内容:
如127.0.0.1格式的IP地址
如sunsite.unc.edu.cn格式的主机名
一个星号,指明IP地址可以为INADDR_ANY值
str_addr中的冒号以及service部分是可选的。当没有忽略时,这部分可以为下列内容:
如8080的端口号
如telnet的服务名
一个星号,表明端口0。当使用了这个值值时bind函数为其赋一个端口号
下面的例子显示了mkaddr函数调用中的可用的str_addr参数值:
www.lwn.net:80
127.0.0.1:telnet
sunsite.unc.edu.cn:ftp
mkaddr函数可能返回下列值:
0表明转换成功
-1表明字符串的主机部分不可用,或是主机名未知
-2表明端口号不可用,或者是服务名未知
下面提供了mkaddr函数的代码。这个子函数对于我们将会编写的工程中会十分有用。当编译整个服务器时我们会介绍编译mkaddr.c的命令。
RPN计算器引擎代码
下面我们将会介绍RPN计算器引擎代码。我们并不期望能够完全理解GMP函数调用,因为我们并没有介绍这些内容。
/*
* rpneng.c
*
* RPN Engine:
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <gmp.h>
typedef void (*mpz_func)(mpz_t,const mpz_t,const mpz_t);
typedef void (*mpz_unary)(mpz_t,const mpz_t);
typedef int (*rpn_spec)(void);
/*
* RPN Stack:
*/
#define MAX_STACK 32
static mpz_t *stack[MAX_STACK];
static int sp = 0;
/*
* Allocate a new mpz_t value:
*/
static mpz_t *rpn_alloc(void)
{
mpz_t *v = malloc(sizeof(mpz_t));
mpz_init(*v);
return v;
}
/*
* Duplicate a mpz_t value:
*/
static mpz_t *rpn_duplicate(mpz_t *value)
{
mpz_t *v = rpn_alloc();
mpz_set(*v,*value);
return v;
}
/*
* Free all allocated mpz_t value:
*/
static void rpn_free(mpz_t **v)
{
mpz_clear(**v);
free(*v);
*v = NULL;
}
/*
* Push an mpz_t value ont the stack:
*/
static int rpn_push(mpz_t *value)
{
if(sp >= MAX_STACK)
return -1;
stack[sp] = value;
return sp++;
}
/*
* Pop a mpz_t value from the stack:
*/
static int rpn_pop(mpz_t **value)
{
if(sp <= 0)
return -1;
*value = stack[--sp];
return sp;
}
/*
* Duplicate the top value on the stack:
*/
static int rpn_dup(void)
{
mpz_t *opr2;
if(sp <= 0)
return -1;
opr2 = rpn_alloc();
mpz_set(*opr2,*stack[sp-1]);
return rpn_push(opr2);
}
/*
* Swap the top tow values on the stack:
*/
static int rpn_swap(void)
{
mpz_t *opr1,*opr2;
if(sp <2 )
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
rpn_push(opr1);
return rpn_push(opr2);
}
/*
* Dump the stack:
*/
static void rpn_dump(FILE *tx)
{
int sx;
for(sx=sp-1;sx>=0;--sx)
{
fprintf(tx,"%d:",sx);
mpz_out_str(tx,10,*stack[sx]);
fputc('\n',tx);
}
fputs("E:end of stack dump\n",tx);
}
/*
* Operation "seed":
*
* Operands:
* 1: the least significant 32 bits
* will seed the randm number
* generator via srand(3):
*
* Result:
* none.
*/
static int rpn_seed(void)
{
int z;
mpz_t *opr;
long lv;
if((z = rpn_pop(&opr)) <0 )
/* No operand available */
return -1;
/*
* Get long value,ignoring errors.
* Then seed the random number
* generator:
*/
lv = mpz_get_si(*opr);
srand((int)lv);
rpn_free(&opr);
return z;
}
/*
* Operation "random":
*
* Operands:
* 1. A modulo value to apply after
* the random number is generated.
* Result:
* 1. A random value: 0 <modulo value.
*/
static int rpn_random(void)
{
mpz_t *opr,*res;
mpz_t r;
size_t limbs;
if(rpn_pop(&opr) <0)
/* No operand available */
return -1;
mpz_init(r);
res = rpn_alloc();
/*
* Pop the top to use as the modulo
* operand.Generate a random number
* r.The compute r % opr as the
* final result:
*/
limbs = mpz_size(*opr);
mpz_random(r,limbs);
mpz_tdiv_r(*res,r,*opr);
mpz_clear(r);
rpn_free(&opr);
return rpn_push(res);
}
/*
* Operation "tprime":
*
* Test for probability of being
* a prime number:
*
* Operands:
* 1. Number to test
* 2. Number of tests to try
* (typically 25)
* Result:
* 1. Number tested is probably
* prime when value =1.
* Number tested is not prime
* when result is zero.
*/
static int rpn_test_prime(void)
{
mpz_t *opr1,*opr2;
long reps;
int z;
if(sp < 2)
/* Insufficient operands */
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
if(mpz_size(*opr2) >1)
/* Too many limbs in size */
return -1;
reps = mpz_get_si(*opr2);
if(reps < 1L || reps > 32768L)
/* too large for opr2 */
return -1;
z = mpz_probab_prime_p(*opr1,reps);
mpz_set_si(*opr1,(long)z);
rpn_free(&opr2);
return rpn_push(opr1);
}
/*
* Operation "genprime":
*
* Generate a random prime number.
*
* Operands:
* 1. The modulo value to apply
* to the randomizing value
* (see "random")
* 2. The number of primality
* tests to perform(typically
* this value is 25).
* Result:
* 1. The randomly generated prime
* number(actually,only a high
* probability of being prime).
*/
static int rpn_genprime(void)
{
mpz_t *opr1;
mpz_t *opr2;
mpz_t *res;
if(sp < 2)
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
for(;;)
{
rpn_push(rpn_duplicate(opr1));
rpn_random();
rpn_dup();
rpn_push(rpn_duplicate(opr2));
rpn_swap();
rpn_test_prime();
rpn_pop(&res);
if(mpz_cmp_si(*res,0L) != 0)
break;
rpn_free(&res);
rpn_pop(&res);
rpn_free(&res);
}
rpn_free(&res);
rpn_free(&opr2);
rpn_free(&opr1);
return sp-1;
}
/*
* Standard binary arithmetic operations:
*
* Operands:
* 1. Operand 2
* 2. Operand 1
*
* Result:
* 1. Operand 1 op Operand 2
*/
static int rpn_binoper(mpz_func f)
{
mpz_t *res,*opr1,*opr2;
if(sp < 2)
/* Insufficient operands */
return -1;
res = rpn_alloc();
rpn_pop(&opr2);
rpn_pop(&opr1);
f(*res,*opr1,*opr2);
rpn_free(&opr1);
rpn_free(&opr2);
return rpn_push(res);
}
/*
* Standard Unary Operations:
*
* Operands:
* 1. Operand 1
*
* Result:
* 1. Result of unary operation.
*/
static int rpn_unaryop(mpz_unary f)
{
mpz_t *res,*opr1;
if(sp < 1)
/* Insufficient operands */
return -1;
res = rpn_alloc();
rpn_pop(&opr1);
f(*res,*opr1);
rpn_free(&opr1);
return rpn_push(res);
}
/*
* Execute RPN operation:
*
* Returns:
* 0 Successful.
* -1 Failed.
*/
static int rpn_opr(char *oper)
{
int x;
static struct
{
char *oper;
rpn_spec func;
}spec[] ={
{"dup",rpn_dup},
{"swap",rpn_swap},
{"seed",rpn_seed},
{"random",rpn_random},
{"tprime",rpn_test_prime},
{"genprime",rpn_genprime},
{0}
};
static struct
{
char *oper;
mpz_func func;
}binops[] = {
{"+",mpz_add},
{"-",mpz_sub},
{"*",mpz_mul},
{"/",mpz_tdiv_q},
{"%",mpz_tdiv_r},
{"gcd",mpz_gcd},
{0}
};
static struct
{
char *oper;
mpz_unary func;
}unary[] = {
{"abs",mpz_abs},
{"neg",mpz_neg},
{"sqrt",mpz_sqrt},
{0}
};
/*
* Special Cases:
*/
for(x=0;spec[x].oper;++x)
if(!strcmp(spec[x].oper,oper))
return spec[x].func();
/*
* Test for a match on binary operators:
*/
for(x=0;binops[x].oper;++x)
if(!strcmp(binops[x].oper,oper))
return rpn_binoper(binops[x].func);
/*
* Test for a match on unary operators:
*/
for(x=0;unary[x].oper;++x)
if(!strcmp(unary[x].oper,oper))
return rpn_unaryop(unary[x].func);
return -1; /* Failured:unknown operator */
}
void rpn_process(FILE *tx,char *buf)
{
int z;
mpz_t *t;
char *operation;
char *operand;
operation = strtok(buf,":\n\r");
operand = strtok(NULL,"\n\r");
if(!strcmp(operation,"dump"))
{
rpn_dump(tx);
}
else if(!strcmp(operation,"="))
{
/*
* pop off the result:
*/
if((z=rpn_pop(&t)) == -1)
fputs("E:Nothing to pop\n",tx);
else
{
fprintf(tx,"%d:",z);
mpz_out_str(tx,10,*t);
fputc('\n',tx);
rpn_free(&t);
}
}
else if(!strcmp(operation,"#"))
{
/*
* Push an operand onto the stack:
*/
t = rpn_alloc();
if(!mpz_set_str(*t,operand,10))
fprintf(tx,"%d:\n",rpn_push(t));
else
{
fputs("E:Invalid number\n",tx);
rpn_free(&t);
}
}
else
{
/*
* Perform an operation:
*/
z = rpn_opr(operation);
if(z==-1)
fprintf(tx,"E:operation failed.\n");
ele
fprintf(tx,"%d:\n",z);
}
fflush(tx);
}
服务器代码如下:
/*rpnsrv.c
*
* Example RPN Server:
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#ifndef SHUT_RDWR
#define SHUT_RDWR 3
#endif
extern int mkaddr(void *addr,
int *addr_len,
char *input_address,
char *protocol);
extern void rpn_process(FILE *tx,
char *buf);
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
if(errno != 0)
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
}
fputs(on_what,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
char *srvr_addr = "127.0.0.1:9090";
struct sockaddr_in adr_srvr; /* AF_INET */
struct sockaddr_in adr_clnt; /* AF_INET */
int len_inet; /* length */
int s = -1; /* Socket */
int c = -1; /* Client socket */
FILE *rx = NULL; /* Read Stream */
FILE *tx = NULL: /* Write stream */
char buf[4096]; /* I/O buffer */
/*
* use a server address from the command
* line,otherwise default to 127.0.0.1:
*/
if(argc >= 2)
srvr_addr = argv[1];
len_inet = sizeof adr_srvr;
z = mkaddr(&adr_srvr,&len_inet,srvr_addr,"tcp");
if(z<0 || !adr_srvr.sin_port)
{
fprintf(stderr,"Invalid server "
"address,or no port number "
"was specified.\n");
exit(1);
}
/*
* Create a TCP/IP socket to use:
*/
s = socket(PF_INET,SOCK_STREAM,0);
if(s==-1)
bail("socket(2)");
/*
* Bind the server address:
*/
z = bind(s,(struct sockaddr *)&adr_srvr,len_inet);
if(z==-1)
bail("bind(2)");
/*
* Make it a listening socket:
*/
z = listen(s,10);
if(z == -1)
bail("listen(2)");
/*
* start the server loop:
*/
for(;;)
{
/*
* Wait for a connect:
*/
len_inet = sizeof adr_clnt;
c = accept(s,(struct sockaddr *)&adr_clnt,&len_inet);
if(c==-1)
bail("accept(2)");
/*
* Create steams:
*/
rx = fdopen(c,"r");
if(!rx)
{
/* Failured */
close(c);
continue;
}
tx = fdopen(dup(c),"w");
if(!tx)
{
fclose(rx);
continue;
}
/*
* set both streams to line
* buffered mod:
*/
setlinebuf(rx);
setlinebuf(tx);
/*
* Process client's requests:
*/
while(fgets(buf,sizeof buf,rx))
rpn_process(tx,buf);
/*
* Close this client's connection:
*/
fclose(tx);
shutdown(fileno(rx),SHUT_RDWR);
fclose(rx);
}
return 0;
}
在前面章节的例子代码中我 们已经使用了read(2)或是write(2)系统调用在套接口上执行读取和写入操作。这个规则的一个例外就是recvfrom(2)和sendto (2)函数,这两个函数用来读写数据报。然而,使用read和writte函数调用却有一些程序上的缺点。
这一章我们将会讨论以下内容:
如何使用fdopen(3)将一个套接口与一个FILE流相关联
如何创建并读写FILE流
关闭与套接口相关联的流的问题
为我们的FILE流选择并创建合适的缓冲技术
中断系统调用的问题
掌握了这些内容将为我们提供解决我们网络程序的额外方法。
理解标准I/O的需要
Linux中的stdio(3)程序符合ANSI C3.159-1989标准。这个接口的标准化可以帮助程序将程序移植到多个平台。例如,当我们要将源代码从其他的UNIX系统移植到我们自己的Linux平台时,这个程序对于我们来说也许有用。
stdio包本身是与read和writte调用相冲突。然而,我们却使用标准的I/O调用,因为根据我们程序的需要,他们可以方便的每次为我们提供一行或是一个字符。例如,read调用,并不会为我们的程序返回一个文本行。相反,他们返回尽可能多的数,甚至是多行文本。
当写入套接口时,标准I/O例程可以允许我们的程序每次写入一个字符,而不会造成大的内容覆盖。另一方面,每次调用writte写入一个字符花费太大。标准I/O函数允许我们的程序使用合适的数据单元进行操作。
stdio同时我们的程序提供了数据缓冲功能,包括输入与输出。当使用缓冲功能,可以极大的改善我们程序的性能。不幸的是,缓冲会为某些格式的通信造成困难,所以并不是总是使用缓冲。
在这一章我们假设已经熟悉了stdio的基本功能。这通常是在C程序教程,同时也是C语言本身所教的内容。相应的,这一章我们将会专注于我们应密切关注的东西,而其他的一些细微的地方并不明显,因为他适用于套接口编程。
将一个套接口与一个流相关联
stdio流是由FILE控制块来进行管理的。例如,我们也许已经多次编写了如下所示的代码:
FILE *in;
in = fopen(pathname,"r");
if ( in == NULL ) {
fprintf(stderr,"%s: opening %s for read.\n",strerror(errno),pathname);
exit(1);
}
在这个例子中,变量pathname所指定的文件将会打开来读。如果open调用成功,变量in将会接收一个指向为我们管理流I/O的FILE结构。否则,变量in接收一个NULL指针,而我们的程序也必须处理或是报告这个错误。
然而,对于套接口编程,并没有可以打开一个套接口的stderr调用。那么程序员如何将一个流与一个套接口相关联呢?下面的内容将会我们找到答案。
使用fdopen(3)将一个套接口与一个流相关联
for函数也许对于十分熟悉。然而,对于许多人来说,fdopen函数却是新的,或是不熟悉的。因为这个函数对于我们中的许多人来说是新鲜的。这个函数的概要如下:
#include <stdio.h>
FILE *fdopen(int fildes,const char *mode);
这个函数需要两个参数:
1 为执行I/O而使用的一个整型文件描述符(fildes)
2 要使用的标准I/O模式。这可以是一个打开模式,与fopen模式参数相同。例如,r表示这个流要打开来读,而w表示这个流要打开来写。
与fopen函数相同,如果函数成功,则会返回一个指向控制FILE结构的指针。否则,将会返回一个NULL指针来表明发生了错误,而错误号将会存放在外部变量errno中。
在这里要注意,第一个参数是一个文件描述符。我们也许会记起由socket函数返回的也是一个文件描述符。这样将一个已存在的套接口与一个流相关联与为可能。下面的代码是一个将一个套接口与一个可以进行读或是写的流进行关联的例子:
int s; /* socket */
FILE *io; /* stream */
s = socket(PF_INET,SOCK_STREAM,0);
...
io = fdopen(s,"r+");
if ( io == NULL ) {
fprintf(stderr,"%s: fdopen(s)\n",strerror(errno));
exit(1);
}
上面的例子演示了存放在变量s中的套接口号如何与一个名为io的FILE流建立关联。这个例子中的fdopen函数中的模式参数为输入和输出建立了一个流。在这个函数调用成功执行以后,其他的标准I/O函数,如fgetc(3)就可以进行操作了。
关闭一个套接口流
上面的代码演示了如何将一个套接口与一个标准I/O流建立关联。程序编写者也许会问,如何关闭这个套接口或是流?
阅 读fdopen的手册页可以知道传递到函数的文件描述符是不可复制的。这就意味着fildes参数是为物理读写而用的文件描述符。使用dup函数不可以复 制这个文件描述符。相应的,在成功的执行了fdopen函数调用以后,我们并不希望调用close函数。这会关闭流变量io所用的文件描述符。
然而,如果我们调用fclose(io),实际上我们就会关闭套接口s。这可以通过检测fclose函数所用的内部步骤来理解:
1 冲洗任何缓冲的数据到文件描述符。如果存在缓冲的数据,这就会造成为文件描述符调用writte函数。
2 使用close函数关闭流所用的文件描述符。
3 释放缓冲所占用的存储空间以及FILE结构本身。这是通地free函数来执行的。
我们在第2步可以看到套接口s可以由通常的fclose函数来关闭。
使用单独的读写流
在 上面的代码例子中我们看到如何将一个套接口与一个允许输入和输出的流进行关联。尽管这在概念上可行,实际上为输入和输出打开一个单独的流却是更为安全的做 法。原因就在于流缓冲在一个流上比在两个单独的流上起着更为复杂的角色作用。Linux的fdopen的手册页表明对于I/O流而言,在读和写的相互切换 之间通常需要执行fgetpos(3)调用。
在这里我们并不会试着解释为什么以及何时这些情况会适用于一个I/O流,相反,我们在这里建议为读和写使用两个单独的流。这个技术会有一些额外的开销,但是在许多情况下却提供了更好的缓冲性能。
下面的例子显示了如何由一个文件描述符创建一个单独的读和写流。
int s; /* socket */
FILE *rx; /* read stream */
FILE *tx; /* write stream */
s = socket(PF_INET,SOCK_STREAM,0);
rx = fdopen(s,"r");
if(rx==NULL)
{
fprintf(stderr,"%s:fdopen(s,'r')\n",strerror(errno));
exit(1);
}
tx = fdopen(dup(s),"w");
if(tx==NULL)
{
fprintf(stderr,"%s:fdopen(s,'w')\n",strerror(errno));
exit(1);
}
在这里也许我们已经看到了dup函数调用。这是非常重要的,因为不同的流应使用不同的文件描述符。因为这个简单的原因,当调用fclose(tx)时,他并不会关闭rx流所用的同一个文件描述符。
复制一个套接口
为了理解为什么上面的代码例子可以工作,我们需要在这里介绍dup(2)函数的使用:
#include <unistd.h>
int dup(int oldfd);
UNIX 系统,与Linux相类似,允许多个文件描述符指向同一个打开的文件(或者,在此种情况下为同一个套接口)。通过使用套接口s作为输入参数来调用dup函 数,我们将会得到一个新的文件描述符。这个新的文件描述符同样指向原始的套接口s。然而,在执行了这个复制以后,只有当这两个文件描述中的最后一个关闭 时,内核才会关闭套接口本身。
数字总是有助于阐明一个例子。正因为这样,在上面的例子代码中,在文件描述符3上创建一个套接口。并且假设套接口s用下面的代码进行复制:
int s2; /* dup'ed socket */
s2 = dup(s); /* duplicate */
如果文件描述符4当前并没有使用,在上面的例子中,Linux内核将会返回4。这允许文件描述符3(变量s)和文件描述符4(变量s2)同时指向同一个套接口。
关闭双重流
在 我们创建了两个流之后,我们可以安全的在我们的rx流上使用如fgetc(3)或是fgets(3)之类的函数。使用fputs(3)或是fputc (3)的写入函数将会在输出流tx上执行。在我们的程序流中使用单独的流可以消除缓冲交互并且移除在各种点上调用fgetpos(3)函数的需要。
然而,当我们处理完毕这些流以后,我们必须执行下面的操作:
调用fclose(rx)来关闭输入流
调用fclose(tx)来关闭输出流
前一个步骤会执行如下操作:
为写入流冲洗缓冲数据
关闭文件描述符
释放缓冲区
释放由FILE对象管理的流
结束通信
读者也许会想起我们在前面所介绍的shutdown(2)函数。如何在需要的时候调用这个函数呢?
使 用双重流的方法,我们也许会基于一个错误的假设而误用shutdown函数。例如,在上面的例子代码中实际上有两个文件描述符,也许会试着在每一个文件描 述符上调用shutdown函数。在其中一个上,我们也许会关闭读边,而在另一个文件描述符上,我们也许会关闭写边。不要这样做!
回忆第 一章,我们在那里介绍了shutdown函数。在那里我们介绍到,shutdown函数的一个优点就是他们忽略在套接口的打开引用的数目。相应的,在复制 的套接口上调用shutdown函数也会影响到同一个套接口的所用引用。从而,也会影响到我们连接到这个套接口上的所有已存在的流。
当结束我们的进程通过套接口与远程进程上的通信时,需要考虑三个基本的条件:
进程不再写入任何数据,但是却期望接收数据(只关闭写边)
进程不再接收任何数据,但是却期望写入数据(只关闭读边)
进程不再读取或是写入任何数据(关闭读写边)
在上面的例子代码中使用两个流的条件将会在下面的内容中进行相应的讨论。
仅关闭写入边
在这种情况下,调用shutdown函数向Linux内核表明调用进程不再写入任何数据。
因为shutdown函数影响套接口,而不影响文件描述符,也不影响实际使用的文件描述符。然而为了解释程序的需要,我们在这里使用写入流来完成这个任务。
这个任务由以下几步组成:
1 使用fflush(3)函数来冲洗可能存在于流缓冲区中的任何数据。
2 使用shutdown函数关闭套接口的写入边。
3 使用fclose(3)函数关闭流。
在关闭写入边之前,我们必须冲洗输出流。这是非常重要的,因为在缓冲区中也许存在一些未写入的数据。这可以用下面的代码来完成:
fflush (tx); /* Flush buffer out */
要完成shutdown操作,我们需要得到流tx所用的文件描述符。我们可以用下面的C宏来完成这个操作:
#include <stdio.h>
int fileno(FILE *stream);
我们只需要简单的将流指针作为输入传递给宏,而这个宏就会返回这个流指针所用的文件描述符。这是一个可移植的,而且是唯一可以接受的方法。使用这个宏,我们可以用下面的代码来执行关闭操作:
shutdown(fileno(tx),SHUT_WR);
这个任务的最后一步就是简单的调用fclose函数来关闭我们不再需要的tx流:
fclose(tx);
将所有这些代码放在一起,关闭操作如下面的C代码所示:
fflush(tx);
shutdown(fileno(tx),SHUT_WR);
fclose(tx);
这个序列保持rx流完整从而可以进行读操作,但是却强制tx流中的所有缓冲数据写入套接口。shutdown函数通知内核加速发送套接口数据,因为不再有更多的数据需要发送。最后,在tx流上的fclose函数关闭文件描述符,并且释放也流相关联的内存资源。
只关闭读取端
这个过程与只关闭写入端相类似。这个过程有一些轻微的变化:
1 调用shutdown函数来表明不再希望更多的接收数据
2 使用fclose函数关闭流
我们会注意到在这里并不需要fflush操作。这个过程总结如下:
shutdown(fileno(rx),SHUT_RD);
fclose(rx);
再一次注意为流rx获取文件描述符的fileno宏的移植用法。尽管在上面的例子代码中原始的套接口号存放在变量s中,为了程序解释的需要,我们使用fileno宏来得到流所使用的文件描述符。
这个过程向Linux内核表明不再读取更多的数据,同时关闭并释放rx的流资源。然而,程序仍然可以向流tx中写入数据。
同时关闭读取和写入端
这个过程初看起来更为复杂,但是实际上是相当简单的:
1 通过fclose调用关闭写入流
2 通过fclose调用关闭读取流
在第一步并不需要fflush调用,因为写入流的fclose函数会隐式的保证冲洗操作。第2步关闭读取流会关闭套接口的最后一个打开的文件描述符,所以套接口会隐式的关闭读取和写入端。
这 个规则的一个例外,也许会是一个关键点,依赖于我们程序的设计。如果我们的进程进行了fork操作,那么也许会其他的打开的文件描述符指向我们的套接口。 我们也许会记起只有最后一个close发生时才会真正的关闭一个套接口。如果对于这一点仍有怀疑,我们可以跟随如下的更为详细的步骤来进行查看:
1 使用fclose来关闭写入流。这会强制所有未写入的数据写入套接口,并且释放写入流的资源。
2 调用shutdown函数来关闭套接口的读取和写入。
3 使用fclose函数关闭读取流来关闭读文件描述符,并且释放流缓冲和FILE结构。
这个过程的一个变化就是如下所示的调用了shutdown函数:
shutdown(fileno(rx),SHUT_RDWR);
完整代码所下:
fclose(tx);
shutdown(fileno(rx),SHUT_RDWR);
fclose(rx);
这个过程是最后的方法,尽管我们对两步的过程没有任何问题。这个过程总是可以完成我们的任务,而不管任何其他的也许会影响其他过程的程序修改。
处理中断
阅读fread(3)或是fwrite(3)的Linux手册页并不会得到他们可能返回的错误的更多信息。相反,只描述了返回值,来表明是否发生了错误。
在AT&T System V接口定义文档中对fread和fwrite进行了更为详细的描述。这个UNIX文档比较有趣的就是也许会返回EINTR错误代码的事实。
EINTR 错误表明发生了一个中断系统调用。当我们的进程被信号通知,调用一个信号处理函数来进行处理,并且这个处理由他的调用返回时,会返回这个错误。并不是所有 的函数都会返回这个错误代码,而是函数被阻塞相当长的时间时才会返回。当然,在一个套接口上等待到来数据的read调用属于这一类。
我们也许会记起fread函数只是read函数调用上的一个简单的函数层,而read函数会在缓冲区需要时调用。相应的,如果我们的进程收到信号,fread函数也会得到EINTR错误代码。
这对于fwrite函数调用也是相同的。如果要有大量的数据要写入一个套接口,底层的write函数调用也会阻塞较长时间。如果他被阻塞了,就会收到一个信号并进行处理,write函数就会返回一个EINTR错误代码,而这也会使得fwrite函数返回这个错误。
我们这里使用"也许"这个单词,是因为这依赖于我们正在使用的stdio库的设计。一些UNIX实现会隐藏这些错误,而其他的一些系统会返回EINTR代码。因为Linux已经由libc5库移到较新的glibc2版本,所以我们的情况也许会有变化。
如果我们在代码中允许EINTR,那么我们也许可以用下面的代码片段:
int ch;
do {
clearerr(rx);
ch = fgetc(rx);
} while ( ferror(rx) && errno == EINTR );
基本步骤如下:
1 调用clearerr函数来清除在这个流上可能发生的任何错误。
2 执行我们的输入,输出操作。
3 如果操作失败,并且errno的值为EINTR,那么返回步骤1。
如果代码由这个循环退出,那么就表明这个操作或者成功了,或者因为一个非EINTR的不同错误代码而失败了。这里的基本原则就是当返回EINTR代码时,我们重试此操作。
如果我们的情况属于下面情况之一,我们就要包含处理EINTR的代码:
源代码要移植到基他的UNIX平台上
在我们的程序中偶尔发生EINTR错误
GNU C库改变了隐藏EINTR错误的方针
如果我们的程序并不处理信号,或者是他已经忽略信号,我们就不再需要关心EINTR错误代码了。
为其他函数处理EINTR
在这里需要注意的一点就是EINTR是我们将会用来进行套接口编程的主机上其他函数的一个潜在瓿。信号将会影响的函数包括:
connect(2)
accpet(2)
read(2)
write(2)
readv(2)
writev(2)
recvfrom(2)
sendto(2)
select(2)
poll(2)
这个列表并不是一份详尽的列表,而其中的一些函数我们也没有讨论到。然而,这些函数只是通常使用的,会被信号处理影响的函数。
为了保持例子程序的矮小和易于理解,在我们的例子中并没有考虑EINTR的问题。然而,我们必须允许任何产品级别代码中的EINTR的发生。
定义缓冲操作
当我们使用stdio程序时,我们通常使用了其后的一些缓冲操作。例如,缓冲写减少了write系统调用的频繁程序。这会增加整个系统的输出效率。同样的,读请求也是会进行缓冲的。
例如,fgetc会由缓冲区中取得一个字符。只有缓冲区为空,并且要请求更多的数据时才会调用read系统调用。这样的操作改善了I/O效率。
当一个流的文件描述符是一个终端设备时,Linux的I/O是行缓冲的。另一方面,文件是全缓冲的(在一个大的块中进行缓冲)。
在Linux下使用FILE流有三种基本的缓冲模式可以选择。他们是:
全缓冲(或块缓冲)
行缓冲
无缓冲
对于一些套接口编程而言,选择无缓冲模式是比较合适的,虽然在这样的方式下并不会由缓冲得到效率。然而,这确实为我们省去了调用fflush的担忧。
当我们的套接口交互是基于文本行时,通常使用行缓冲模式。使用行缓冲模式意味着我们从不会强制调用fflush来强制将最后一行文本写入套接口。
如果我们选择使用全缓冲模式,那么我们就要在希望实际的写入套接口时调用fflush函数。否则,我们的数据也许就会处在一个输出缓冲区中,而我们的程序却在等待响应,因为输出数据并没有发送。
缓冲控制函数概要如下所示:
#include <stdio.h>
int setbuf(FILE *stream,char *buf);
int setbuffer(FILE *stream, char *buf, size_t size);
int setlinebuf(FILE *stream);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
这些函数会允许调用者来改变指定流的缓冲模式。Linux文档表明这些函数可以在任何时候调用来改变流的缓冲特性。
这些函数的参数描述如下:
参数stream是将会影响到的流的FILE指针
参数buf是一个指向提供的缓冲区的指针。这个指针可以为NULL。如果需要一个缓冲区,但是却指定了NULL,那么就会分配一个内部缓冲区。
参数size为所提供的缓冲区的大小,或者是分配的内部缓冲区的大小。
参数mode为要使用的缓冲区的模式。
建议的缓冲区尺寸是由包含的stdio.h文件作为BUFSIZ宏来定义的。下表列出了setvbuf函数可用的mode参数值:
C宏 描述
_IOFBF 流上的输入/输出操作将会全缓冲
_IOLBF 流上的输入/输出操作将会行缓冲
_IONBF 流上的输入/输出操作不会进行缓冲
作为例子,如何使用这些函数来改变套接口流tx来使用行缓冲模式,我们可以在fdopen调用后使用下面的函数代码:
setlinebuf(tx); /* Line Buffered Mode */
我们也可以使用setvbuf函数来直接进行改变:
setvbuf(tx,NULL,_IOLBF,BUFSIZ);
在这个例子中,我们允许软件分配他自己的BUFSIZ字节的大小的内部缓冲区。然而,流的缓冲模式被设置为行缓冲,因为在函数调用中的使用了_IOLBF宏。
在套接口上应用流
现在我们要来介绍一些使用我们已经讨论的这些概念的源码了。我们下面要介绍的服务器程序实现了一个计算器。他会接受两个长整型数,用栈进行存储,然后在其上执行运算。运算的结果会放在栈顶。
整型算法将会由GNU多精度库(GMP)来进行执行。这个库允许无限制尺寸的整型数进行计算。在这里我们并不会描述GMP库,而这个段代码的目的只是简单的的演示使用FILE流的一些服务器概念。这个服务器会帮助演示一些我们将会下一章涉及的高级概念。
实现mkaddr()函数
在这里将会提供mkaddr.c子函数,从而使得这个工程更易读。这里所介绍地mkaddr将会接受一个包含IP地址和一个可选的端口号,或者是一个主机名和可选端口号的输入字符串。端口号也可以是一个符号网络服务名,如telnet或是ftp。这个函数概要如下:
int mkaddr
(void *addr, int *addr_len, char *str_addr, char *protocol);
函数的参数描述如下:
1 参数addr指向接收套接口地址结构。这是返回的套接口地址。这个值不可以为空。
2 参数addr_len是一个指向整数值的指针,当函数返回时将会由所addr中所创建的地址的长度来进行填充。输入值所指向的必须为包含由addr所指向的区域的最大尺寸。
3 参数str_addr为符号主机名或是可选的端口号。这个参数将会在后面进行描述。一个NULL指针意为一个"*"字符串。
4 参数protocol指明了服务将会使用的协议。一个NULL指针意为tcp字符串。
str_addr输入字符串的设计将尽可能灵活。他包含由冒号分隔的两部分:
host_name:service
字符串的host_name可以为下列内容:
如127.0.0.1格式的IP地址
如sunsite.unc.edu.cn格式的主机名
一个星号,指明IP地址可以为INADDR_ANY值
str_addr中的冒号以及service部分是可选的。当没有忽略时,这部分可以为下列内容:
如8080的端口号
如telnet的服务名
一个星号,表明端口0。当使用了这个值值时bind函数为其赋一个端口号
下面的例子显示了mkaddr函数调用中的可用的str_addr参数值:
www.lwn.net:80
127.0.0.1:telnet
sunsite.unc.edu.cn:ftp
mkaddr函数可能返回下列值:
0表明转换成功
-1表明字符串的主机部分不可用,或是主机名未知
-2表明端口号不可用,或者是服务名未知
下面提供了mkaddr函数的代码。这个子函数对于我们将会编写的工程中会十分有用。当编译整个服务器时我们会介绍编译mkaddr.c的命令。
RPN计算器引擎代码
下面我们将会介绍RPN计算器引擎代码。我们并不期望能够完全理解GMP函数调用,因为我们并没有介绍这些内容。
/*
* rpneng.c
*
* RPN Engine:
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <gmp.h>
typedef void (*mpz_func)(mpz_t,const mpz_t,const mpz_t);
typedef void (*mpz_unary)(mpz_t,const mpz_t);
typedef int (*rpn_spec)(void);
/*
* RPN Stack:
*/
#define MAX_STACK 32
static mpz_t *stack[MAX_STACK];
static int sp = 0;
/*
* Allocate a new mpz_t value:
*/
static mpz_t *rpn_alloc(void)
{
mpz_t *v = malloc(sizeof(mpz_t));
mpz_init(*v);
return v;
}
/*
* Duplicate a mpz_t value:
*/
static mpz_t *rpn_duplicate(mpz_t *value)
{
mpz_t *v = rpn_alloc();
mpz_set(*v,*value);
return v;
}
/*
* Free all allocated mpz_t value:
*/
static void rpn_free(mpz_t **v)
{
mpz_clear(**v);
free(*v);
*v = NULL;
}
/*
* Push an mpz_t value ont the stack:
*/
static int rpn_push(mpz_t *value)
{
if(sp >= MAX_STACK)
return -1;
stack[sp] = value;
return sp++;
}
/*
* Pop a mpz_t value from the stack:
*/
static int rpn_pop(mpz_t **value)
{
if(sp <= 0)
return -1;
*value = stack[--sp];
return sp;
}
/*
* Duplicate the top value on the stack:
*/
static int rpn_dup(void)
{
mpz_t *opr2;
if(sp <= 0)
return -1;
opr2 = rpn_alloc();
mpz_set(*opr2,*stack[sp-1]);
return rpn_push(opr2);
}
/*
* Swap the top tow values on the stack:
*/
static int rpn_swap(void)
{
mpz_t *opr1,*opr2;
if(sp <2 )
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
rpn_push(opr1);
return rpn_push(opr2);
}
/*
* Dump the stack:
*/
static void rpn_dump(FILE *tx)
{
int sx;
for(sx=sp-1;sx>=0;--sx)
{
fprintf(tx,"%d:",sx);
mpz_out_str(tx,10,*stack[sx]);
fputc('\n',tx);
}
fputs("E:end of stack dump\n",tx);
}
/*
* Operation "seed":
*
* Operands:
* 1: the least significant 32 bits
* will seed the randm number
* generator via srand(3):
*
* Result:
* none.
*/
static int rpn_seed(void)
{
int z;
mpz_t *opr;
long lv;
if((z = rpn_pop(&opr)) <0 )
/* No operand available */
return -1;
/*
* Get long value,ignoring errors.
* Then seed the random number
* generator:
*/
lv = mpz_get_si(*opr);
srand((int)lv);
rpn_free(&opr);
return z;
}
/*
* Operation "random":
*
* Operands:
* 1. A modulo value to apply after
* the random number is generated.
* Result:
* 1. A random value: 0 <modulo value.
*/
static int rpn_random(void)
{
mpz_t *opr,*res;
mpz_t r;
size_t limbs;
if(rpn_pop(&opr) <0)
/* No operand available */
return -1;
mpz_init(r);
res = rpn_alloc();
/*
* Pop the top to use as the modulo
* operand.Generate a random number
* r.The compute r % opr as the
* final result:
*/
limbs = mpz_size(*opr);
mpz_random(r,limbs);
mpz_tdiv_r(*res,r,*opr);
mpz_clear(r);
rpn_free(&opr);
return rpn_push(res);
}
/*
* Operation "tprime":
*
* Test for probability of being
* a prime number:
*
* Operands:
* 1. Number to test
* 2. Number of tests to try
* (typically 25)
* Result:
* 1. Number tested is probably
* prime when value =1.
* Number tested is not prime
* when result is zero.
*/
static int rpn_test_prime(void)
{
mpz_t *opr1,*opr2;
long reps;
int z;
if(sp < 2)
/* Insufficient operands */
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
if(mpz_size(*opr2) >1)
/* Too many limbs in size */
return -1;
reps = mpz_get_si(*opr2);
if(reps < 1L || reps > 32768L)
/* too large for opr2 */
return -1;
z = mpz_probab_prime_p(*opr1,reps);
mpz_set_si(*opr1,(long)z);
rpn_free(&opr2);
return rpn_push(opr1);
}
/*
* Operation "genprime":
*
* Generate a random prime number.
*
* Operands:
* 1. The modulo value to apply
* to the randomizing value
* (see "random")
* 2. The number of primality
* tests to perform(typically
* this value is 25).
* Result:
* 1. The randomly generated prime
* number(actually,only a high
* probability of being prime).
*/
static int rpn_genprime(void)
{
mpz_t *opr1;
mpz_t *opr2;
mpz_t *res;
if(sp < 2)
return -1;
rpn_pop(&opr1);
rpn_pop(&opr2);
for(;;)
{
rpn_push(rpn_duplicate(opr1));
rpn_random();
rpn_dup();
rpn_push(rpn_duplicate(opr2));
rpn_swap();
rpn_test_prime();
rpn_pop(&res);
if(mpz_cmp_si(*res,0L) != 0)
break;
rpn_free(&res);
rpn_pop(&res);
rpn_free(&res);
}
rpn_free(&res);
rpn_free(&opr2);
rpn_free(&opr1);
return sp-1;
}
/*
* Standard binary arithmetic operations:
*
* Operands:
* 1. Operand 2
* 2. Operand 1
*
* Result:
* 1. Operand 1 op Operand 2
*/
static int rpn_binoper(mpz_func f)
{
mpz_t *res,*opr1,*opr2;
if(sp < 2)
/* Insufficient operands */
return -1;
res = rpn_alloc();
rpn_pop(&opr2);
rpn_pop(&opr1);
f(*res,*opr1,*opr2);
rpn_free(&opr1);
rpn_free(&opr2);
return rpn_push(res);
}
/*
* Standard Unary Operations:
*
* Operands:
* 1. Operand 1
*
* Result:
* 1. Result of unary operation.
*/
static int rpn_unaryop(mpz_unary f)
{
mpz_t *res,*opr1;
if(sp < 1)
/* Insufficient operands */
return -1;
res = rpn_alloc();
rpn_pop(&opr1);
f(*res,*opr1);
rpn_free(&opr1);
return rpn_push(res);
}
/*
* Execute RPN operation:
*
* Returns:
* 0 Successful.
* -1 Failed.
*/
static int rpn_opr(char *oper)
{
int x;
static struct
{
char *oper;
rpn_spec func;
}spec[] ={
{"dup",rpn_dup},
{"swap",rpn_swap},
{"seed",rpn_seed},
{"random",rpn_random},
{"tprime",rpn_test_prime},
{"genprime",rpn_genprime},
{0}
};
static struct
{
char *oper;
mpz_func func;
}binops[] = {
{"+",mpz_add},
{"-",mpz_sub},
{"*",mpz_mul},
{"/",mpz_tdiv_q},
{"%",mpz_tdiv_r},
{"gcd",mpz_gcd},
{0}
};
static struct
{
char *oper;
mpz_unary func;
}unary[] = {
{"abs",mpz_abs},
{"neg",mpz_neg},
{"sqrt",mpz_sqrt},
{0}
};
/*
* Special Cases:
*/
for(x=0;spec[x].oper;++x)
if(!strcmp(spec[x].oper,oper))
return spec[x].func();
/*
* Test for a match on binary operators:
*/
for(x=0;binops[x].oper;++x)
if(!strcmp(binops[x].oper,oper))
return rpn_binoper(binops[x].func);
/*
* Test for a match on unary operators:
*/
for(x=0;unary[x].oper;++x)
if(!strcmp(unary[x].oper,oper))
return rpn_unaryop(unary[x].func);
return -1; /* Failured:unknown operator */
}
void rpn_process(FILE *tx,char *buf)
{
int z;
mpz_t *t;
char *operation;
char *operand;
operation = strtok(buf,":\n\r");
operand = strtok(NULL,"\n\r");
if(!strcmp(operation,"dump"))
{
rpn_dump(tx);
}
else if(!strcmp(operation,"="))
{
/*
* pop off the result:
*/
if((z=rpn_pop(&t)) == -1)
fputs("E:Nothing to pop\n",tx);
else
{
fprintf(tx,"%d:",z);
mpz_out_str(tx,10,*t);
fputc('\n',tx);
rpn_free(&t);
}
}
else if(!strcmp(operation,"#"))
{
/*
* Push an operand onto the stack:
*/
t = rpn_alloc();
if(!mpz_set_str(*t,operand,10))
fprintf(tx,"%d:\n",rpn_push(t));
else
{
fputs("E:Invalid number\n",tx);
rpn_free(&t);
}
}
else
{
/*
* Perform an operation:
*/
z = rpn_opr(operation);
if(z==-1)
fprintf(tx,"E:operation failed.\n");
ele
fprintf(tx,"%d:\n",z);
}
fflush(tx);
}
服务器代码如下:
/*rpnsrv.c
*
* Example RPN Server:
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#ifndef SHUT_RDWR
#define SHUT_RDWR 3
#endif
extern int mkaddr(void *addr,
int *addr_len,
char *input_address,
char *protocol);
extern void rpn_process(FILE *tx,
char *buf);
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
if(errno != 0)
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
}
fputs(on_what,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
char *srvr_addr = "127.0.0.1:9090";
struct sockaddr_in adr_srvr; /* AF_INET */
struct sockaddr_in adr_clnt; /* AF_INET */
int len_inet; /* length */
int s = -1; /* Socket */
int c = -1; /* Client socket */
FILE *rx = NULL; /* Read Stream */
FILE *tx = NULL: /* Write stream */
char buf[4096]; /* I/O buffer */
/*
* use a server address from the command
* line,otherwise default to 127.0.0.1:
*/
if(argc >= 2)
srvr_addr = argv[1];
len_inet = sizeof adr_srvr;
z = mkaddr(&adr_srvr,&len_inet,srvr_addr,"tcp");
if(z<0 || !adr_srvr.sin_port)
{
fprintf(stderr,"Invalid server "
"address,or no port number "
"was specified.\n");
exit(1);
}
/*
* Create a TCP/IP socket to use:
*/
s = socket(PF_INET,SOCK_STREAM,0);
if(s==-1)
bail("socket(2)");
/*
* Bind the server address:
*/
z = bind(s,(struct sockaddr *)&adr_srvr,len_inet);
if(z==-1)
bail("bind(2)");
/*
* Make it a listening socket:
*/
z = listen(s,10);
if(z == -1)
bail("listen(2)");
/*
* start the server loop:
*/
for(;;)
{
/*
* Wait for a connect:
*/
len_inet = sizeof adr_clnt;
c = accept(s,(struct sockaddr *)&adr_clnt,&len_inet);
if(c==-1)
bail("accept(2)");
/*
* Create steams:
*/
rx = fdopen(c,"r");
if(!rx)
{
/* Failured */
close(c);
continue;
}
tx = fdopen(dup(c),"w");
if(!tx)
{
fclose(rx);
continue;
}
/*
* set both streams to line
* buffered mod:
*/
setlinebuf(rx);
setlinebuf(tx);
/*
* Process client's requests:
*/
while(fgets(buf,sizeof buf,rx))
rpn_process(tx,buf);
/*
* Close this client's connection:
*/
fclose(tx);
shutdown(fileno(rx),SHUT_RDWR);
fclose(rx);
}
return 0;
}
相关推荐
Linux Socket学习(八).txt和Linux Socket学习(十).txt可能涉及到连接建立和接受过程。对于服务器端,accept()函数用于接收客户端的连接请求;对于客户端,connect()函数用于发起连接。这两部分的细节是理解TCP/...
《实战Linux Socket编程》是...总之,《实战Linux Socket编程》的配套源代码是一个宝贵的资源,它使学习者能够动手实践,从而更好地掌握网络编程的核心概念和技术,对于提升Linux系统下的网络编程能力具有重要意义。
在IT行业中,Linux Socket是进行网络通信的重要工具,尤其对于系统和网络程序员来说,理解和掌握Linux Socket编程至关重要。本示例代码提供了客户端(client)和服务器端(server)的实现,帮助初学者深入理解如何在...
在IT领域,Linux Socket编程是网络通信的核心技术之一,它为开发者提供了在Linux操作系统上实现进程间通信(IPC)和网络通信的接口。本实战指南将深入探讨这一主题,帮助你掌握如何在Linux环境中构建高效的网络应用...
在Linux操作系统中,Socket是一种进程间通信机制,它允许不同进程或者不同计算机之间的通信。本教程将深入探讨Linux下的socket编程,包括服务器和客户端的实现。我们主要关注以下几个知识点: 1. **Socket基本概念*...
本压缩包“linuxsocket.zip”包含了基于TCP/IP协议的socket通信测试代码,是学习Linux应用编程的一个实用资源。这里我们将深入探讨Linux TCP/IP socket编程的相关知识点。 1. **TCP/IP协议栈**:TCP/IP协议栈是...
本篇文章将根据给定文件“linux socket学习.pdf”的内容进行展开,重点讨论 socket 的定义、如何创建 socket 以及 socket 与其他常见 I/O 操作的区别。 #### 二、理解 Socket ##### 2.1 定义 Socket Socket 可以...
通过本书的学习,读者可以掌握如何在Linux环境中使用Socket进行网络通信,构建高性能、稳定的网络应用。下面将根据提供的压缩包文件名“实战 Linux Socket编程代码”来解析其中可能包含的知识点。 1. **Socket基础...
Linux Socket编程是网络编程的重要组成部分,它为开发者提供...通过这个实战教程,开发者将学习如何在Linux环境中创建和管理Socket,实现网络通信功能,这对于开发服务器应用、网络工具或分布式系统是至关重要的技能。
在Linux操作系统中,Socket接口是应用程序与网络协议交互的主要接口,它允许程序通过网络发送和接收数据。在本文中,我们将深入探讨“Linux Socket网络驱动深度分析”这一主题,特别是当应用尝试创建一个用于捕获...
Linux Socket是Linux操作系统中用于进程间通信(IPC)的一种接口,它允许程序通过网络协议进行数据传输。在本文中,我们将深入探讨Linux ...通过IBM技术论坛的文章,你可以更深入地了解和学习Linux Socket的各种细节。
### Linux Socket Programming (Linux 套接字编程) #### 知识点概览: 1. **Socket编程基础** ...通过以上知识点的学习,读者可以深入了解Linux环境下的Socket编程,并能够掌握如何设计和实现网络通信程序。
Linux Socket实战编程是深入理解网络通信机制的...总的来说,这份资源对于想要深入理解和精通Linux Socket编程的IT从业者来说是一份宝贵的资料,通过学习和实践,可以显著提升网络编程能力,为职业发展打下坚实基础。
Gay撰写,为读者提供了丰富的实例和深入的理论知识,是学习Linux Socket编程的绝佳资源。 **二、基本Socket概念** ### 1. Socket简介 在Linux环境下,Socket是一种进程间通信(IPC)的方式,它提供了一种在两个...
在IT行业中,网络通信是至关重要的部分,而Linux Socket编程是实现这一目标的关键技术。Socket是一种接口,允许应用程序通过网络发送和接收数据。本篇将深入探讨标题为"Linux Socket两则示例"的资源,其中包括`echo_...
10. **学习资源**:学习Socket编程,可以参考《UNIX Network Programming》等经典书籍,同时网上有许多关于Linux Socket编程的教程和示例代码可供学习。 总之,"Linux下的socket聊天室程序"是一个很好的实践项目,...
本资源“实战Linux Socket编程”旨在帮助你深入理解并掌握这一关键技能,尤其对于那些希望在嵌入式领域有所建树的开发者来说,它是不可或缺的学习资料。 在Linux系统中,Socket编程主要涉及以下几大知识点: 1. **...
《Linux Socket编程示例》...以上仅为《Linux Socket编程示例》部分章节的知识点总结,实际书中还包含了更多深入的技术细节和实战案例,对于希望掌握Linux环境下网络编程技术的学习者而言,是一本不可多得的参考书籍。
Linux Socket编程是网络编程的重要组成部分,它允许...总之,"实战Linux Socket编程源码"提供了一个实践Socket编程和多客户端处理的平台,通过对源代码的学习和分析,可以加深对Socket编程的理解,提升网络编程技能。