创建套接口在这一部分,我们将会看到创建套接口与创建管道一样的容易。虽然有一些我们将会了解到的函数参数。为了能创建成功,这些参数必须提供合适的值。 socketpair函数概要如下: #include #include int socketpair(int domain, int type, int protocol, int sv[2]); sys/types.h文件需要用来定义一些C宏常量。sys/socket.h文件必须包含进来定义socketpair函数原型。 socketpair函数需要四个参数。他们是: 套接口的域 套接口类型 使用的协议 指向存储文件描述符的指针 domain参数直到第2单我们才会解释。对于socketpair函数而言,只需提供C宏AF_LOCAL。 类型参数声明了我们希望创建哪种类型的套接口。socketpair函数的选择如下: SOCK_STREAM SOCK_DGRAM 套接口类型的选择我们将会在第4章谈到。在这一章中,我们只需要简单的使用SOCK_STREAM套接口类型。 对于socketpair函数,protocol参数必须提供为0。 参数sv[2]是接收代表两个套接口的整数数组。每一个文件描述符代表一个套接口,并且与另一个并没有区别。 如果函数成功,将会返回0值。否则将会返回-1表明创建失败,并且errno来表明特定的错误号。 使用socketpair的例子 为了演示如何使用socketpair函数,我们用下面的例子来进行演示。 1: /* Listing 1.1: 2: * 3: * Example of socketpair(2) function: 4: */ 5: #include 6: #include 7: #include 8: #include 9: #include 10: #include 11: #include 12: 13: int 14: main(int argc,char **argv) { 15: int z; /* Status return code */ 16: int s[2]; /* Pair of sockets */ 17: 18: /* 19: * Create a pair of local sockets: 20: */ 21: z = socketpair(AF_LOCAL,SOCK_STREAM,0,s); 22: 23: if ( z == -1 ) { 24: fprintf(stderr, 25: "%s: socketpair(AF_LOCAL,SOCK_STREAM,0)\n", 26: strerror(errno)); 27: return 1; /* Failed */ 28: } 29: 30: /* 31: * Report the socket file descriptors returned: 32: */ 33: printf("s[0] = %d;\n",s[0]); 34: printf("s[1] = %d;\n",s[1]); 35: 36: system("netstat --unix -p"); 37: 38: return 0; 39: } 演示程序的描述如下: 1 在第16行声明数组s[2]用来存储用来引用两个新创建的套接口的文件描述符。 2 在第21行调用socketpair函数。domain参数指定为AF_LOCAL,套接口类型参数指定为SOCK_STREAM,而协议指定为0。 3 23行的if语句用来测试socketpair函数是否成功。如果z的值为-1,就会向标准错误发送报告,并且在27行退出程序。 4 如果函数调用成功,控制语句就会转到33,并且在34行向标准输出报告返回的文件单元数。 5 36行使用system函数来调用netstat命令。命令选项--unix表明只报告Unix套接口,-p选项则是要报告进程信息。 使用提供的Makefile,我们可以用make命令来编译这个程序: $ make 01lst01 gcc -c -D_GNU_SOURCE -Wall 01LST01.c gcc 01LST01.o -o 01lst01 为了执行这个演示程序,我们可以执行下面的命令: $ ./01lst01 程序的执行结果如下: 1: $ ./01lst01 2: s[0] = 3; 3: s[1] = 4; 4: (Not all processes could be identified, non-owned process info 5: will not be shown, you would have to be root to see it all.) 6: Active UNIX domain sockets (w/o servers) 7: Proto RefCnt Flags Type . . . I-Node PID/Program name Path 8: unix 1 [] STREAM . . . 406 - @00000019 9: unix 1 [] STREAM . . . 490 - @0000001f 10: unix 1 [] STREAM . . . 518 - @00000020 11: unix 0 [] STREAM . . . 117 - @00000011 12: unix 1 [] STREAM . . . 789 - @00000030 13: unix 1 [] STREAM . . . 549 - @00000023 14: unix 1 [] STREAM . . .1032 662/01lst01 15: unix 1 [] STREAM . . .1031 662/01lst01 16: unix 1 [] STREAM . . . 793 - /dev/log 17: unix 1 [] STREAM . . . 582 - /dev/log 18: unix 1 [] STREAM . . . 574 - /dev/log 19: unix 1 [] STREAM . . . 572 - /dev/log 20: unix 1 [] STREAM . . . 408 - /dev/log 21: $ 在我们上面的输入显示中,在第1行调用可执行程序01LST01。第2行和第3行显示了我们在文件描述符3和4上打开套接口。接下来的4到20行是程序中netstat命令的输出。 尽管这个程序并没有使用创建的套接口来做任何事情,但是他确实演示了套接口的创建。并且他演示了套接口单元数的分配与打开的文件的方式一样。 在套接口上执行I/O操作 我们在前面已经了解到套接口可以像任何打开的文件一样向其中写入或是从中读取。在这一部分将我们将会亲自演示这一功能。然而为了试都讨论的完整,我们先来看一下read,write,close的函数概要: #include ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); int close(int fd); 这些应是我们已经熟悉的Linux的输入/输入函数。通过回顾我们可以看到,read函数返从文件描述符fd中返回最大count字节的输入,存放到buf缓冲区中。返回值代表实际读取的字节数。如果返回0则代表文件结束。 write函数将我们指定的buf缓冲区中总计count的字节写入文件描述符fd中。返回值代表实际写入的字节数。通常这必须与指定的count参数相匹配。然而也会有一些情况,这个值要count小,但是我们没有必要担心这样的情况。 最后,如果文件成功关闭close就会返回0。对于这些函数,如果返回-1则表明有错误发生,并且错误原因将会发送到外部变量errno中。为了可以访问这个变量,我们需要在源文件中包含errno.h头文件。 下面的例子是在套接口的两个方向上执行读取与写入操作。 /***************************************** * * Listing 1.2 * * Example performing I/O on s socket pair: * * ******************************************/ #include #include #include #include #include #include #include
int main(int argc,char **argv) { int z; /* Status return code */ int s[2]; /* Pair of sockets */ char *cp; /* A work pointer */ char buf[80]; /* work buffer */
/* * Create a pair of local sockets: */
z = socketpair(AF_LOCAL,SOCK_STREAM,0,s);
if(z == -1) { fprintf(stderr, "%s:socketpair(AF_LOCAL,SOCK_STREAM,""0)\n",strerror(errno)); return 1; /* Failed */ }
/* * Write a message to socket s[1]: */
z = write(s[1],cp="Hello?",6); if(z<0) { fprintf(stderr,"%s:wirte(%d,\"%s\",%d)\n",strerror(errno),s[1],cp,strlen(cp)); return 2; /* Failed */ }
printf("Wrote message '%s' on s[1]\n",cp);
/* * Read from socket s[0]: */
z = read(s[0],buf,sizeof buf); if(z < 0) { fprintf(stderr,"%s:read(%d,buf,%d)\n", strerror(errno),s[0],sizeof buf); return 3; /* Failed */ }
/* * Report received message: */
buf[z] = 0; /* NUL terminate */ printf("Recevie message '%s' from socket s[0]\n",buf);
/* * Send a reply back to s[1] from s[0]: */
z = write(s[0],cp="Go away!",8); if(z < 0) { fprintf(stderr,"%s:write(%d,\"%s\",%d)\n", strerror(errno),s[0],cp,strlen(cp)); return 4; /* Failed */ }
printf("Wrote message '%s' on s[0]\n",cp);
/* * Read from socket s[1]: */
z = read(s[1],buf,sizeof buf); if(z < 0) { fprintf(stderr,"%s:read(%d,buf,%d)\n", strerror(errno),s[1],sizeof buf); return 3; /* Failed */ }
/* * Report message recevied by s[0]: */ buf[z] = 0; /*NUL terminate */ printf("Received message '%s' from socket s[1]\n", buf);
/* * Close the sockets: */ close(s[0]); close(s[1]);
puts("Done"); return 0; } 程序调用的步骤总结如下: 1 在第23行调用socketpair函数,如果成功返回,则将生成的套接口存放在数组s中。 2 在第25行测试函数是否成功,如果发生错误,将会报告错误。 3 在第36行一个由6个字符组成的消息"Hello?"写入套接口s[1]。注意并没有写入空字节,因为在write函数的count参数中仅指定了6个字节。 4 第37到第42行检测并报告可能发生的错误。 5 第44行声明一个成功写操作。 6 在第49行read调用试着从另一个套接口s[0]读取消息。在这条语句中,可以读取任何最大为buf[]数组尺寸的消息。 7 第50行到第55行检测并服务在read语句中可能发生的错误。 8 第60行到第62行报告一条成功接收的消息。 9 第67行到第73行向套接口s[0]写入一条回复消息"Go away!"。这就演示了不同于管道,信息在可以作为端点的套接口中双向传送。 10 第75行声明一个成功的写操作。 11 第80行到第86行应从通信线路的另一个端点套接口s[1]中读取信息"Go away!"。 12 第91行到第93行报告成功接收的信息。 13 这两个套接口在第98行和第99行关闭。 14 在第102行程序退出。 当程序被调用时,输出结果如下: $ ./01lst02 Wrote message 'Hello?' on s[1] Received message 'Hello?' from socket s[0] Wrote message 'Go away!' on s[0] Received message 'Go away!' from socket s[1] Done. $ 如果我们跟踪我们在前面所勾画的步骤,我们就会发现信息是在套接口中双向传送的。而且我们演示了套接口用与文件相同的方式来关闭。 关闭套接口 在前面,我们看到如何来创建一对套接口,并且看到如何使用这些套接口来执行最基本的输入与输出操作。我们也可以看到套接口可以使用与通过调用close函数来关闭文件的方式来关闭。现在我们来了解一下关闭套接口所提供的函数。 当从通过pipe函数创建的管道中读取时,当接收到一个文件结尾时,接收就会认为不会再有要接收的数据。当关闭管道的写端时,文件结束的条件是通过写进程发送的。 同样的过程也可以用在套接口上。当另一个端点关闭时,接收端就会收到一个文件结束的标识。 当本地进程希望通知远程端不再接收数据时就会出现问题。如果本地进程关闭了他的套接口,这是可以适用的。然而,如果他希望从远程端接收一个确认信息,这是不可能的,因为现在他的套接口已经关闭了。这样的情况需要一个半关闭套接口的方法。 shutdown函数 下面显示了shutdown函数的概要: #include int shutdown(int s, int how); shutdown函数需要两个参数。他们是: 套接口描述符s指定了要部分关闭的套接口。 参数how指定要如何关闭这个套接口中。 如果函数成功则返回0。如果调用失败则会返回-1,错误原因将会发送到errno。 how的可能值如下: 值 宏 描述 0 SHUT_RD 在指定的套接口上不再允许读操作。 1 SHUT_WR 在指定的套接口上不再允许写操作。 2 SHUT_RDWR 在指定的套接口上不再允许读写操作。 注意当how值指定为2时,这个函数的调用与close函数调用相同。 关闭向一个套接口的写 下面的代码演示了如何指定在本地的套接口上不再执行写操作: int z; int s; /* Socket */ z = shutdown(s, SHUT_WR); if ( z == -1 ) perror("shutdown()"); 关闭套接口的写端解决了一系列难题。他们是: 清空包含任何要发送的数据的内核缓冲区。通过内核网络软件来缓冲数据来改进性能。 向远程套接口发送文件结束标识。这就通知远程读进程在这个套接口上不会再向他发送数据。 保留半关闭套接口为读打开。这就使得在套接口上发送了文件结束标识以后还可以接收确认信息。 丢弃在这个套接口上的打开引用计数。只有最后在这个套接口上的close函数将会发送一个文件结束标识。 处理复制的套接口 如果一个套接口文件描述符通过dup或者是dup2函数来调用进行复制,只有最后的close函数调用可以关闭这个套接口。这是因为另外复制的文件描述符仍处于使用状态。如下面的代码如演示的样子: int s; /* Existing socket */ int d; /* Duplicated socket */ d = dup(s); /* duplicate this socket */ close(s); /* nothing happens yet */ close(d); /* last close, so shutdown socket */ 在这个例子中,第一个close函数调用不会有任何效果。先关闭其中的任何一个都是一样的结果。关闭s或者d将会为同一个套接口保留一个文件描述符。只有通过close函数调用来关闭最后一个存在的文件描述符才会有效果。在这个例子中,关闭d文件描述符关闭了这个套接口。 shutdown函数避免了这种区别。重复这个例子代码,通过使用shutdown函数解决了这个问题: int s; /* Existing socket */ int d; /* Duplicated socket */ d = dup(s); /* duplicate this socket */ shutdown(s,SHUT_RDWR); /* immediate shutdown */ 尽管套接口s也在文件单元d上打开,shutdown函数立刻使得套接口执行关闭操作。这个操作在打开的文件描述符s和d都是同样的效果,因为他们指向同一个套接口。 这个问题出现的另一个方式就是执行了fork函数调用。任何优先级高于fork操作的套接口都会在子进程中被复制。 关闭从一个套接口读 关闭套接口的读取端将会使得待读取的任何数据都会被忽略掉。如果从远程套接口发送来更多的数据,也同样会被忽略掉。然而任何试着从这个套接口进行读取的进程都会返回一个错误。这通常用来强制协议或是调试代码。 shutdown函数的错误代码如下: 错误 描述 EBADF 指定的套接口不是一个可用的文件描述符 ENOTSOCK 指定的文件描述符不是一个套接口 ENOTCONN 指定的套接口并没有连接 从这个表中我们可以看到,对于已连接的套接口应只调用shutdown函数,否则就会返回ENOTCONN错误代码。 编写一个客户/服务器例子 现在我们所了解的套接口API的集合已经足够让我们开始一些有趣的尝试了。在这一部分,我们会检测,编译并且测试一个简单的通过套接口进行通信的客户与服务器进程。 为了使得这个程序尽可能的小,将会启动一个程序,然后复制为一个客户进程与一个服务器进程。子进程将会是客户端程序角色,而原始的父进程将会执行服务器的角色。下图显示了父进程与子进程的关系以及套接口的使用。
父进程是最初启动的程序。他立刻通过调用socketpair函数来生成一对套接口,然后通过调用fork函数将自己复制为两个进程。 服务器将会接收请求,执行请求,然后退出。类似的客户端将会执行请求,报告服务器响应,然后退出。 请求将会采用strftime函数的第三个参数的格式。这是一个用来格式化日期与时间字符串的格式字符串。服务器将会在接收到请求时得到当前的日期与时间。服务器将会使用客户端的请求字符串来将其格式化为最终的字符串,然后发送给客户端。我们先来回顾一个strftime函数的概要: #include size_t strftime(char *buf, size_t max, const char *format, const struct tm *tm); 参数buf与max分别指定了输出缓冲区以及最大长度。参数format是一个输入字符串,可以允许我们来格式化日期与时间字符串。最后参数tm用来指定必须来创建输出日期与时间字符串的日期与时间组件。 /***************************************** * * Listing 1.3 * * Client/Server Example Using socketpair * and fork: * * ******************************************/
#include #include #include #include #include #include #include #include #include
/* * As of RedHat 6.0,these are still not defined: */ #ifndef SHUT_WR #define SHUT_RD 0 #define SHUT_WR 1 #define SHUT_RDWR 2 #endif
/* * Main program */ int main(int argc,char **argv) { int z; /* Status return code */ int s[2]; /* Pair of sockets */ char *msgp; /* A message pointer */ int mlen; /* Message length */ char buf[80]; /* work buffer */ pid_t chpid; /* Child PID */
/* * Create a pair of local sockets: */ z = socketpair(AF_LOCAL,SOCK_STREAM,0,s); if(z == -1) { fprintf(stderr,"%s:socketpair(2)\n",strerror(errno)); exit(1); }
/* * Now fork() into two processes: */ if((chpid = fork()) == (pid_t)-1) { /* * Failed to fork into two processes: */ fprintf(stderr,"%s:fork(2)\n",strerror(errno)); exit(1); } else if(chpid == 0) { /* * This is child process(client) */ char rxbuf[80]; /*Receive buffer*/ printf ("Parent PID is %ld\n",(long)getppid()); close(s[0]); /* Server uses s[1] */ s[0] = -1; /*Forget this unit */
/* * Form the message and its length: */ msgp = "%A %d-%b-%Y %l:%M %p"; mlen = strlen(msgp);
printf("Child sending request '%s'\n",msgp); fflush(stdout);
/* * Write a request to the server: */
z = write(s[1],msgp,mlen); if(z<0) { fprintf(stderr,"%s:write(2)\n",strerror(errno)); exit(1); }
/* * Now indicate that we will not be writing * anything further to our socket,by shutting * down the write side of the socket: */ if(shutdown(s[1],SHUT_WR) == -1) { fprintf(stderr,"%s:shutdown(2)\n",strerror(errno)); exit(1); }
/* * Recevie the reply from the server: */ z = read(s[1],rxbuf,sizeof rxbuf); if(z<0) { fprintf(stderr,"%s:read(2)\n",strerror(errno)); exit(1); }
/* * Put a null byte at the end of what we * received from the server: */ rxbuf[z]=0;
/* * Report the result: */ printf("Server returned '%s'\n",rxbuf); fflush(stdout); close(s[1]); /*Close our end now*/ } else { /* * This is parent process(server): */
int status; /*Child termintation status*/ char txbuf[80]; /*Reply buffer*/ time_t td; /*Current date&time*/ printf("Child PID is %ld\n",(long)chpid); fflush(stdout);
close(s[1]); /* Cient uses s[0] */ s[1] = -1; /* Forget this desciptor */
/* * Wait for a request from the client: */ z = read(s[0],buf,sizeof buf); if(z<0) { fprintf(stderr,"%s:read(2)\n",strerror(errno)); exit(1); } /* * Put a null byte at the end of the * message we recevied from the client: */ buf[z] = 0;
/* * Now perform the server function on * the received message */ time(&td); /* Get current time */ strftime(txbuf,sizeof txbuf, /* Buffer */ buf, /* Input fromate*/ localtime(&td));/* Input time */ /* * Send back the response to client: */ z = write (s[0],txbuf,strlen(txbuf)); if(z<0) { fprintf(stderr,"%s:write(2)\n",strerror(errno)); exit(1); } /* * Close our end of the socket */ close(s[0]);
/* * Wait for the child process to exit: */ waitpid(chpid,&status,0);
} return 0; }
| | | |