(一)
理解套接口在我们试着使用套接口之前理解套接口后面的一些内容是很重要的。这一部分描绘出围绕着套接口的一些高级内容。
定义套接口
要与使用电话的某人进行交流,我们必须拿起话筒,拨打对方的电话号码,并且等待对方的应答。当我们与对方通话时,就建立了两个通信端点。
我们的电话,在我们的位置
远方的对方电话,在他的位置。
只要我们仍在通话,就我们之间调用两个端点,建立了一条通信线路。
Linux下的套接口也与电话相类似。套接口代表通信线路中的两个端点。在这两个端点之间存在着数据通信网络。
在另一个方式上,套接口也与电话类似。当我们要打给某人,我们拨打我们要联系的人的电话号码。套接口有网络地址而不是电话号码。通过指定远程套接口地址,我们的程序可以在我们的本地套接口与远程端点之间建立一条通信线路。
由此我们可以推断,一个套接口是通信中的一个端点。有许多的Linux函数调用可以操作套接口,而我们将会慢慢的了解这些。
使用套接口也许我们会认为Linux套接口很特殊,因为套接口有一个可以在其上进行操作的特定的函数集合。尽管套接口有一些特殊的属性,但是他与我们十分熟悉的文件描述十分相似。
例
如,当我们使用Linux的open调用打开一个文件,如果open函数调用成功,我们就会得到一个返回的文件描述符。在我们有了这个文件描述符以后,我
们的程序可以使用文件描述符来read,write,lseek,以及close打开的指定文件。相似的,当创建了一个套接口时,他与文件描述符十分想
似。我们可以使用相同的文件I/O函数来读,写以及关闭套接口。
然而在套接口与打开的文件之间也存在一些不同。下面列出了其中的一些不同之处:
我们不可以在套接口上执行lseek函数。
套接口有与其相关联的地址。文件和管道没有网络地址。
套接口有可以使用ioctl函数进行查询与设置的不同选项功能。
为了执行输入或输出,套接口必须处理正确的状态。相反,打开的磁盘文件可以在任何时候进行读取或是写入。
引用套接口当
我们使用open函数调用来打开一个新文件时,Linux内核就会返回下一个可用的并且是最小的文件描述符。文件描述符,或者是常称之为文件单元数,是零
或者正的整数值,用来引用打开的文件。这个句柄会用在在打开的文件上进行操作的所有函数中。现在我们就会知道文件单元数也可以引用特定的套接口。
我们的程序已经打开了0,1和2三个文件单元(标准输入,标准输出,标准错误),接下来的程序操作将会被执行。注意内核是如何分配文件描述符的:
1 调用open函数来打开一个文件
2 返回文件单元3来引用打开的文件。因为这个单元数当前并没有用,并且是可用的最小的单元数,所以为文件选择了3作为文件单元数。
3 通过一个合适的函数调用来创建一个新的套接口。
4 返回文件单元4来引用这个新的套接口。
5 通过调用open打开另一个文件。
6 返回文件单元5来引用这个新打开的文件。
注意:当分配单元数时,Linux内核在文件与套接口之间并没有区别。用一个文件描述符来引用一个打开的文件或是一个网络套接口。
这就意味着,我们作为一个程序员,可以将套接口当作打开的文件一样来使用。通过文件单元数交互的来引用文件和套接口的能力提供给了我们极大的灵活性。这就意味着read和write函数可以同时在打开的文件和套接口上进行操作。
套接口与管道的比较在我们介绍任何套接口函数之前,我们来回忆一下我们已经熟悉的pipe函数调用。让我们看一下他返回的文件描述符与套接口的不同。下面是由pipe的手册中得到的函数概要:
#include
int pipe(int filedes[2]);
当
这个调用成功时,pipe函数会返回两个文件描述符。数组元素filedes[0]包含管道读端的文件描述符。filedes[1]元素是管道写端的文件
描述符。两个文件描述符的这种安排提示了在每一端使用文件描述符的通信连接。这与使用套接口有何不同呢?不同就在于pipe函数创建了一个单向的通信线。
信息只可以写入filedes[1]中的文件单元数,并且只可以从filedes[0]中进行读取。任何向相反方向写入数据的尝试都会得到Linux内核
返回的错误。
另一个方面,套接口允许在两个方向处理通信。例如,一个进程可以使用在文件单元3上打开的套接口向远端进程发送数据。与使用管道不同,同一个本地进程也可以从文件单元3上接收到与其相通信的远端进程发送的数据。
(二)
创建套接口
在这一部分,我们将会看到创建套接口与创建管道一样的容易。虽然有一些我们将会了解到的函数参数。为了能创建成功,这些参数必须提供合适的值。
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;
}
(三)
无名套接口
套接口并不总是需要有一个地址。例如,
socketpair函数创建了两个彼此相连的两个套接口,但是却没有地址。实际上,他们是无名套接口。想像一下冷战期间美国总统与苏联之间的红色电话。
他们任何一端并不需要电话号码,因为他们是直接相连的。同样,socketpair函数也是直接相连的,也并不需要地址。
匿名调用
有时在实际上,连接中的两个套接口中的一个也没有地址。对于要连接的远程套接口,他必须要有一个地址来标识。然而,本地套接口是匿名的。建立起来的连接具有一个有地址的远程套接口和另一个无地址的套接口。
生成地址
有
时我们并不会介意我们的本地址是什么,但是我们需要一个来进行通信。这对于需要连接到一个服务器(例如一个RDBMS数据服务)的程序来说通常是正确的。
他们的本地地址仅为持续的连接所需要。分配确定的地址也可以完成,但是这增加了网络管理的工作。相应的,当地址可用时才会生成地址。
理解域
当Berkeley开发组正在构思BSD套接口接口时,TCP/IP仍在开发之中。与此同时,有一些其他的即将完成的协议正在为不同的组织所使用,例如X.25协议。其他的协议也正在研究之中。
我
们在上一章所见的socketpair函数,以及我们将会看到的socket函数,很明智的允许了其他协议需不是TCP/IP也许会用到的可能性。
socketpair函数的domain参数允许这种约束。为了讨论的方便,让我们先来回顾一下socketpair函数的概要:
#include
#include
int socketpair(int domain, int type, int protocol, int sv[2]);
通常,protocol参数指定为0。0允许操作系统选择我们所选择的domain的所用的默认协议。对于这些规则有一些例外,但是这超出了我们讨论的范围。
现在我们要来解释一下domain参数。对于socketpair函数,这个值必须为AF_LOCAL或者AF_UNIX。在上一章,我们已经指出AF_UNIX宏与旧版的AF_LOCAL等同。然而AF_LOCAL意味着什么?他选择了什么呢?
常量的AF_前缘指明了地址族。domain参数选择要使用的地址族。
格式化套接口地址
每
一个通信协议指明了他自己的网络地址的格式。相应的,地址族用来指明要使用哪种类型的地址。常量AF_LOCAL(AF_UNIX)指明了地址将会按照本
地(UNIX)地址规则来格式化。常量AF_INET指明了地址将会符合IP地址规则。在一个地址族中,可以有多种类型。
在下面的部分中,我们将会检测各种地址族的格式以及物理布局。这是需要掌握的重要的一部分。人们使用BSD套接口接口时所遇到的困难,很多与地址初始化相关。
检测通常的套接口地址
因为BSD套接口地址的开发早于ANSI C标准,所以没有(void *)数据指针来接受任何结构地址。相应的BSD的解决选择是定义一个通用的地址结构。通用的地址结构是用下面的C语言语句来定义的:
#include
struct sockaddr {
sa_family_t sa_family; /* Address Family */
char sa_data[14]; /* Address data. */
};
这里的sa_family_t数据类型是一个无符号短整数,在Linux下为两个字节。整个结构为16个字节。结构元素的sa_data[14]代表了地址信息的其余14个字节。
下图显示了通用地址结构的物理布局:
通用套接口地址结构对于程序而言并不是那样有用。然而,他确实提供了其他地址结构必须适合的引用模型。例如,我们将会了解到所有地址必须在结构中的同样的位置定义一个sa_family成员,因为这个元素决定了地址结构的剩余字节数。
格式化本地地址
这个地址结构用在我们的本地套接口中(我们的运行Linux的PC)。例如,当我们使用lpr命令排除要打印的文件时,他使用一个本地套接口与我们的PC上假脱机服务器进行通信。虽然也可以用TCP/IP协议来进行本地通信,但是事实证明这是低效的。
传统上,本地地址族已经被称这为AF_UNIX域。这是因为这些地址使用本地UNIX文件来作为套接口名字。
AF_LOCAL或者AF_UNIX的地址结构名为sockaddr_un。这个结构是通过在我们的C程序中包含下面的语句来定义的:
#include
sockaddr_un的地址结构:
struct sockaddr_un {
sa_family_t sun_family;/* Address Family */
char sun_path[108]; /* Pathname */
};
结构成员sun_family的值必须为AF_LOCAL或者AF_UNIX。这个值表明这个结构是通过sockaddr_un结构规则来进行格式化的。
结构成员sun_path[108]包含一个可用的UNIX路径名。这个字符数组并不需要结尾的null字节。
在下面的部分中,我们将会了解到如何来初始化一个AF_LOCAL地址与定义他的长度。
格式化传统本地地址
传统本地地址的地址名空间为文件系统路径名。一个进程也许会用任何可用的路径名来命名他的本地套接口。然则为了可用,命名套接口的进程必须可以访问路径名的所有目录组件,并且有权限来在指定的目录中创建最终的套接口对象。
一些程序员喜欢在填充地址结构之前将其全部初始化为0。这通常是通过memset函数来做到的,并且这是一个不错的主意。
struct sockaddr_un uaddr;
memset(&uaddr,0,sizeof uaddr);
这个函数会为我们将这个地址结构的所有字节设置为0。
下面的例子演示了一个简单的初始化sockaddr_un结构的C程序,然后调用netstat命令来证明他起到了作用。在这里我们要先记住在socket与bind上的程序调用,这是两个我们还没有涉及到的函数。
/*****************************************
*
* af_unix.c
*
* AF_UNIX Socket Example:
*
* ******************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
perror(on_what);
exit(1);
}
int main(int argc,char **argv,char **envp)
{
int z; /* Status return code */
int sck_unix; /* Socket */
struct sockaddr_un adr_unix; /* AF_UNIX */
int len_unix; /* length */
const char pth_unix[] = "/tmp/my_sock"; /* pathname */
/*
* Create a AF_UNIX (aka AF_LOCAL) socket:
*/
sck_unix = socket(AF_UNIX,SOCK_STREAM,0);
if(sck_unix == -1)
bail("socket()");
/*
* Here we remove the pathname for the
* socket,in case it existed from a
* prior run.Ignore errors (it maight
* not exist).
*/
unlink(pth_unix);
/*
* Form an AF_UNIX Address:
*/
memset(&adr_unix,0,sizeof adr_unix);
adr_unix.sun_family = AF_LOCAL;
strncpy(adr_unix.sun_path,pth_unix,
sizeof adr_unix.sun_path-1)
[sizeof adr_unix.sun_path-1] = 0;
len_unix = SUN_LEN(&adr_unix);
/*
* Now bind the address to the socket:
*/
z = bind(sck_unix,
(struct sockaddr *)&adr_unix,
len_unix);
if(z == -1)
bail("bind()");
/*
* Display all of our bound sockets
*/
system("netstat -pa --unix 2>/dev/null |"
"sed -n '/^Active UNIX/,/^Proto/P;"
"/af_unix/P'");
/*
* Close and unlink our socket path:
*/
close(sck_unix);
unlink(pth_unix);
return 0;
}
上面的这个例子的步骤如下:
1 在第28行定义了sck_unix来存放创建的套接口文件描述符。
2 在第29行定义了本地地址结构并且命名为adr_unix。这个程序将会用一个AF_LOCAL套接口地址来处理这个结构。
3 通过调用socket函数来在第37行创建了一个套接口。在第39行检测错误并报告。
4 在第48行调用unlink函数。因为AF_UNIX地址将会创建一个文件系统对象,如果不再需要必须进行删除。如果这个程序最后一次运行时没有删除,这条语句会试着进行删除。
5 在第53行adr_unix的地址结构被清0。
6 在第55行将地址族初始化为AF_UNIX。
7 第57行到第59行向地址结构中拷贝路径名"/tmp/my_sock"。在这里使用代码在结构中添加了一个null字节,因为在第61行Linux提供了宏SUN_LEN()需要他。
8 在第61行计算地址的长度。这里的程序使用了Linux提供的宏。然而这个宏依赖于adr_unix.sun_path[]结构成员的一个结束字符。
9 在第66行到68行调用bind函数,将格式化的地址赋值给第37行创建的套接口。
10 在第76行调用netstat命令来证明我们的地址已绑定到了套接口。
11 在第83 行关闭套接口。
12 当调用bind函数时为套接口所创建的UNIX路径名在第66行被删除。
在
第61行将长度赋值给len_unix,在这里使用了SUN_LEN()宏,但是并不会计算拷贝到adr_unix.sun_path[]字符数组中的空
字节。然而放置一个空字节是必要的,因为SUN_LEN()宏会调用strlen函数来计算UNIX路径名的字符串长度。
程序的执行结果如下:
$ ./af_unix
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node PID/Program name Path
unix 0 [] STREAM 104129 800/af_unix /tmp/my_sock
$
格式化抽象本地地址
传统AF_UNIX套接口名字的麻烦之一就在于总是调用文件系统对象。这不是必须的,而且也不方便。如果原始的文件系统对象并没有删除,而在bind调用时使用相同的文件名,名字赋值就会失败。
Linux 2.2内核使得为本地套接口创建一个抽象名了成为可能。他的方法就是使得路径名的第一个字节为一个空字节。在路径名中空字节之后的字节才会成为抽象名字的一部分。下面的这个程序是上一个例子程序的修改版本。这个程序采用了一些不同的方法来创建一个抽象的名字。
/*****************************************
* af_unix2.c
*
* AF_UNIX Socket Example
* Create Abstract Named AF_UNIX/AF_LOCAL
* ******************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
perror(on_what);
exit(1);
}
int main(int argc,char **argv,char **envp)
{
int z; /* Status return code */
int sck_unix; /* Socket */
struct sockaddr_un adr_unix; /* AF_UNIX */
int len_unix; /* length */
const char pth_unix[] /* Abs .Name */
= "Z*MY-SOCKET*";
/*
* Create an AF_UNIX (aka AF_UNIX) socket:
*/
sck_unix = socket(AF_UNIX,SOCK_STREAM,0);
if(sck_unix == -1)
bail("socket()");
/*
* Form an AF_UNIX Address
*/
memset(&adr_unix,0,sizeof adr_unix);
adr_unix.sun_family = AF_UNIX;
strncpy(adr_unix.sun_path,pth_unix,
sizeof adr_unix.sun_path-1)
[sizeof adr_unix.sun_path-1] = 0;
len_unix = SUN_LEN(&adr_unix);
/*
* Now make first byte null
*/
adr_unix.sun_path[0] = 0;
z = bind(sck_unix,(struct sockaddr *)&adr_unix,len_unix);
if(z == -1)
bail("bind()");
/*
* Display all of our bound sockets:
*/
system("netstat -pa --unix 2>/dev/null |"
"sed -n '/^Active UNIX/,/^Proto/P;"
"/af_unix/P'");
/*
* Close and unlink our socket path:
*/
close(sck_unix);
return 0;
/*
* Now bind the address to the socket:
*/
}
这个程序的运行结果如下:
$ ./af_unix2
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags Type State I-Node PID/Program name Path
unix 0 [] STREAM 104143 5186/af_unix2 @*MY- SOCKET*
$
从这个输出结果中我们可以看到,套接口地址是以 @*MYSOCKET*的名字出现的。开头的@标志是为netstat命令用来标识抽象UNIX套接口名字。其余的字符是拷贝到字符数组剩余位置的字符。注意@字符出现在我们的Z字符应出现的地方。
整个程序的步骤与前一个程序的相同。然而,地址的初始化步骤有一些不同。这些步骤描述如下:
1 在第31行和第32行定义了套接口抽象名字的字符串。注意字符串的第一个字符为Z。在这个字符串这个多余的字符只是起到占位的作用,因为实际上他会在第6步被一个空字节代替。
2 在第45行通过调用memset函数将整个结构初始经为0。
3 在第47行将地址族设置为AF_UNIX。
4 在第49行使用strncpy函数将抽象名字拷贝到adr_unix.sun_path中。在这里要注意,为了SUN_LEN()宏的使用在目的字符数组的放置了一个结束的空字节。否则就不需要这个结束的空字节。
5 在第53通过Linux所提供的SUN_LEN() C 宏来计算地址的长度。这个宏会在sun_path[]上调用strlen函数,所以需要提供了一个结束字符。
6 这一步是新的:sun_path[]数组的第一个字节被设置为空字节。如果使用SUN_LEN()宏,必须最后执行这一步。
在这一部分,我们了解了如何来创建AF_LOCAL和AF_UNIX的套接口地址。为了计算套接口地址的长度,我们使用SUN_LEN()宏。然而,当计算抽象套接口名字时,我们要十分注意。
(四)
格式化IPv4套接口地址
在Linux下使用最多的地址族为AF_INET。这为一个套接口指定一个IPv4套接口地址,从而使得这个套接口可以通过TCP/IP网络与其他的主机进行通信。定义了sockaddr_in结构的包含头文件是由下面的C语句来进行定义的:
#include
下面的例子是一个用于网络地址的sockaddr_in结构。另外显示了一个in_addr结构,因为sockaddr_in结构会在他的定义中使用这个结构。
struct sockaddr_in {
sa_family_t sin_family; /* Address Family */
uint16_t sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Pad bytes */
};
struct in_addr {
uint32_ t s_ addr; /* Internet address */
};
其成员描述如下:
sin_family成员出现在与通用套接口定义中的sa_family相同的存储位置。sin_family会被初始化为AF_INET。
sin_port成员为这个套接口地址定义了TCP/IP的端口号。这个值必须为网络字节顺序。
sin_addr成员定义为in_addr结构,用于以网络字节顺序存放IP地址。如果我们检测in_addr,我们就会发现他由32位无符号整数组成。
最后,结构的剩余部分由8个字节的成员sin_zero[8]填充为16个字节。
这个地址结构的物理布局如下图所示:
从上图中我们可以看出,sin_port成员使用两个字节,而sin_addr使用4个字节。这两个成员都在其上放置了一个标志用来表明这些值必须为网络字节顺序。
理解网络字节顺序
不同的CPU体系结构对于多个字节的数据,16位,32位或者更多,会有不同的安排方式。最基本的两个字节顺序为:
大端
小端
其他的组合也是可以的,但是我们在这里并不考虑这些情况。下面这个图显示了这两种不同的字节顺序:
上个图所演示的为十进制的数4660,以十六进制表示则为0x1234.这个数值需要用两个字节来表示。从这个图我们可以看到或者我们可以首先放置最重的字节(大端),或者是我们可以首先放置最不重要的字节(小端)。这种选择是非常模糊的,而这最终涉及到CPU的设计。
我们也许已经知道,Intel CPU使用小端字节顺序。其他的CPU,例如Motorola 68000系列使用大端字节顺序。在这里我们要考虑的最重要的事情就是这两种类型的CPU都存在,而且他们要连接到同一个网络。
如果一个Motorola的CPU向网络中写入一个16位的数字,并且为一个Intel CPU接收时会发生什么呢?这些字节将会为Intel CPU进行反序解释,从而这个值看起来就是十六进制的0x3412。
为网络存在的协议,大端字节顺序将在网络上使用。只要通过网络传输的所有消息遵循这个序列,所有软件就可以顺利通信。
这就将我们带回了AF_INET地址族。TCP/IP端口号(sin_port)以及IP地址(sin_addr)必须是网络字节顺序。BSD套接口地址要求作为程序员的我们在格式化地址必须考虑到这一点。
执行端转换
有一些函数提供用来帮助我们简化端转换。需要考虑两个方向的端转换:
主机顺序到网络顺序
网络顺序到主机顺序
主机顺序是指我们的CPU所使用的字节顺序。对于Intel CPU来说是指小端字节顺序。网络字节顺序,正如我们已经了解到的,为大端字节顺序。
同时也有两类转换函数:
短整数(16位)转换
长整数(32位)转换
下面所提供的是转换函数的概要:
#include
unsigned long htonl(unsigned long hostlong);
unsigned short htons(unsigned short hostshort);
unsigned long ntohl(unsigned long netlong);
unsigned short ntohs(unsigned short netshort);
这些函数的使用是很简单的。例如,要将一个短整数转换为网络顺序,我们可以使用下面的代码:
short host_ short = 0x1234;
short netw_short;
netw_short = htons(host_short);
netw_short值将接收转换为网络字节后的合适值。将一个网络字节顺序转换为一个主机顺序也是一样简单的:
host_short = ntohs(netw_short);
初始化一个宽网地址
现
在我们已准备好来创建一个网络地址了。在这里演示的这个例子需要这个地址必须为宽的。这经常是当我们连接到一个远程服务时完成的。这个原因是因为我们的主
机也许会有两个或是多个网卡,每一个网卡有一个不同的IP地址。而且,Linux同时也允许每一个网卡有多个IP地址。当我们指定一个宽的IP地址,我们
允许系统选择到远程服务的路由。内核会在连接建立时确定我们的最终本地套接口地址。
有时我们希望内核为我们赋一个本地端口号,这是通过将sin_port指定为0来做到的。下面的代码演示了如何使用一个宽IP地址与一个宽端口号来初始化一个AF_INET地址。
1: struct sockaddr_in adr_inet;
2: int adr_len;
3:
4: memset(&adr_inet,0,sizeof adr_inet);
5:
6: adr_inet.sin_family = AF_INET;
7: adr_inet.sin_port = ntohs(0);
8: adr_inet.sin_addr.s_addr = ntohl(INADDR_ANY);
9: adr_len = sizeof adr_inet;
描述如上:
1 使用sockaddr_in结构来定义一个adr_inet变量。
2 通过调用memset函数将adr_inet结构清0。
3 通过将AF_INET赋给adr_inet.sin_family来建立地址族。
4 在第7行指定一个宽端口号。注意ntohs函数的使用。值0指明一个宽端口号。
5 在第8行赋一个宽IP地址。注意执行端转换的ntohl函数的使用。
6 地址的尺寸简单的由adr_inet结构的尺寸来计算。
另一个常用的IP地址为127.0.0.1。这是指loopback设备。回环设备允许我们在同一个主机上与另一个进程进行通信。第8行的代码可以用下面的代码为进行替换:
adr_inet.sin_addr.s_addr = ntohl(INADDR_LOOPBACK);
这会通过回环设备来定位我们的主机。
初始化一个指定的网络地址
在前一个部分我们处理了一个简单的AF_INET地址的例子。当我们要在地址中建立一个指定的IP地址事情就会变得更为复杂。下面是一个程序示例:
/*
* af_inet.c
* Establishing a specific AF_INET
* Socket Address
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
perror(on_what);
exit(1);
}
int main(int argc,char **argv,char **envp)
{
int z; /* Status return code */
int sck_inet; /* Socket */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
const unsigned char IPno[] = {
127,0,0,23 /* Local loopback */
};
/* Create an IPv4 Internet Socket */
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if(sck_inet == -1)
bail("socket()");
/*Create an AF_INET address */
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
memcpy(&adr_inet.sin_addr.s_addr,IPno,4);
len_inet = sizeof adr_inet;
/* Now bind the address to the socket */
z = bind(sck_inet,(struct sockaddr *)&adr_inet,len_inet);
if(z == -1)
bail("bind()");
/* Display all of our bound sockets */
system("netstat -pa --tcp 2>/dev/null | "
"sed -n '1,/^Proto/P;/af_inet/P'");
close(sck_inet);
return 0;
}
在这个程序所用的步骤是我们前面的例子程序相同。然而43到48行还需要一些解释:
1 在第30行使用sockaddr_in结构来定一个名为adr_inet的变量名。另外,在第31行将套接口地址的长度定义为一个整数len_inet。
2 在第32行与第33行定义了一个无符号字符数组IPno[4]。在这里指定了一个IP地址:127.0.0.23。
3 在第43行将adr_inet结构清0。
4 在第45行建立了AF_INET地址族。
5 在这个例子中在第46行选择TCP/IP的9000端口建立连接。在这里我们要注意在第46行htons函数的用法。
6 在第47行将字符数组IPno[4]拷贝到adr_inet.sin_addr.s_addr的位置。因为这些字节是按照网络字节顺序来定义的,所以不需要端转换函数。
7 计算地址结构的大小。
在这里我们可以看出网络地址一个确定的长度。如果我们回顾一下上一个例子,我们就可以很容易的看出来。然而,我们要记得AF_LOCAL的地址长度是变化的。对于AF_INET的地址,我们只需简单的提供sockaddr_in结构的大小。在C语言中为:
sizeof(struct sockaddr_in)
指定一个X.25地址
套接口接口允许程序员使用在Linux下可用的其他的协议。我们要处理的代码之间的一个主要区别就是套接口是如何编址的。我们已经知道如何初始化一个AF_INET或是AF_LOCAL地址。X.25地址的创建也是类似的。
用来定义X.25协议地址的结构为sockaddr_x25结构。下面的包含语句定义了这个结构:
#include
X.25套接口地址结构如下:
struct sockaddr_x25 {
sa_family_t sx25_family; /* Must be AF_X25 */
x25_address sx25_addr; /* X.121 Address */
};
typedef struct {
char x25_addr[16];
} x25_address;
我们可以注意到有一个sx25_family成员出现在与通用套接口结构相同的前两个字节处。对于这个地址而言,必须为AF_X25。
一个X.25网络地址是由一系列的十进制数组成的。下面的af_x25.c程序用来演示如何创建一个X.25地址。
/*af_x25.c
*
* x.25 Socket Address Example:
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and exits back to the shell:
*/
static void bail(const char *on_what)
{
perror(on_what);
exit(1);
}
int main(int argc,char **argv,char **envp)
{
int z; /* Status return code */
int sck_x25; /* Socket */
struct sockaddr_x25 adr_x25; /* AF_X25 */
int len_x25; /* length */
const char x25_host[] /* X.121 addr */
= "79400900";
/* create an AF_X25 socket */
sck_x25 = socket(AF_X25,SOCK_SEQPACKET,0);
if(sck_x25 == -1)
bail("Socket()");
/* Form an AF_X25 Address */
adr_x25.sx25_family = AF_X25;
strcpy(adr_x25.sx25_addr.x25_addr,x25_host);
len_x25 = sizeof adr_x25;
/* Bind the address to the socket */
z = bind(sck_x25,(struct sockaddr *)&adr_x25,len_x25);
if(z == -1)
bail("bind()");
puts("X.25 SOCKETS :");
system("cat /proc/net/x25");
return 0;
}
创建地址的代码包含如下的基本步骤:
1 在第29行使用sockaddr_x25结构来定义adr_x25变量。在第30行定义一个int类型的长度变量len_x25。
2 在第31行与第32行定义了一个固定的x25_host[],作为要建立了X.25地址。
3 在第41行将地址簇指定为AF_X25。
4 在第42行将主机地址号拷贝到地址结构中,并且指定了一个结束符。
5 sockaddr_x25结构的长度是在当前Linux实现下使用的正确长度。
注
意,这个程序并没有使用netstat命令。这时因为此时netstat命令并不会报告AF_X25套接口。相反,在这个例子中我们使用cat命令来将
/proc/net/x25的内容拷贝到标准输出。然而为了这个例子能够成功,我们必须将proc文件系统的支持编译进入我们的内核。
程序的运行结果如下:
$ ./af_x25
X.25 SOCKETS :
dest_addr src_addr dev lci st vs vr va t t2 t21 t22 t23 Snd-Q Rcv-Q inode
* 79400900 ??? 000 0 0 0 0 0 3 200 180 180 0 0 104172
$
阅读(1654) | 评论(0) | 转发(0) |