理解套接口
在我们试着使用套接口之前理解套接口后面的一些内容是很重要的。这一部分描绘出围绕着套接口的一些高级内容。
定义套接口
要与使用电话的某人进行交流,我们必须拿起话筒,拨打对方的电话号码,并且等待对方的应答。当我们与对方通话时,就建立了两个通信端点。
我们的电话,在我们的位置
远方的对方电话,在他的位置。
只要我们仍在通话,就我们之间调用两个端点,建立了一条通信线路。
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 <unistd.h>
int pipe(int filedes[2]);
当 这个调用成功时,pipe函数会返回两个文件描述符。数组元素filedes[0]包含管道读端的文件描述符。filedes[1]元素是管道写端的文件 描述符。两个文件描述符的这种安排提示了在每一端使用文件描述符的通信连接。这与使用套接口有何不同呢?不同就在于pipe函数创建了一个单向的通信线。 信息只可以写入filedes[1]中的文件单元数,并且只可以从filedes[0]中进行读取。任何向相反方向写入数据的尝试都会得到Linux内核 返回的错误。
另一个方面,套接口允许在两个方向处理通信。例如,一个进程可以使用在文件单元3上打开的套接口向远端进程发送数据。与使用管道不同,同一个本地进程也可以从文件单元3上接收到与其相通信的远端进程发送的数据。
创建套接口
在这一部分,我们将会看到创建套接口与创建管道一样的容易。虽然有一些我们将会了解到的函数参数。为了能创建成功,这些参数必须提供合适的值。
socketpair函数概要如下:
#include <sys/types.h>
#include <sys/socket.h>
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 <stdio.h>
6: #include <stdlib.h>
7: #include <unistd.h>
8: #include <errno.h>
9: #include <string.h>
10: #include <sys/types.h>
11: #include <sys/socket.h>
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 <unistd.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
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 <sys/socket.h>
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 <time.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
/*
* 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 <sys/types.h>
#include <sys/socket.h>
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 <sys/socket.h>
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 <sys/un.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
/*
* 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 <netinet/in.h>
下面的例子是一个用于网络地址的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 <netinet/in.h>
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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
/*
* 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 <linux/x25.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/x25.h>
/*
* 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
$
上一章中,我们已经了可以分配与初始化各种类型的套接口。这些是由一个常量进行初始化的简单例子。由一个使用变化地址的C字符串设置一个地址需要更多的编程努力。在这一章,我们将会关注建立网络地址的传统问题,以及了解可以在这一领域帮助我们的函数。
在这一章,我们了解到如下内容:
网络地址分类
IP网络掩码
私有的以及保留的IP地址
IP转换函数
然而在我们开始之前,这是一个很好的机会来回顾一下IP地址的设计。这样我们就会更为理解我们将要进行的工作。
网络IP地址
IP地址是由四个十进制数组成的,其中由十进制的点来分隔,通常为点。每一个十进制值以网络字节顺序来表示一个字节的无符号值。在这里我们记住网络字节顺序要求最重要的字节先出现(大端顺序)。
每一个字节都作为一个无符号的8位值。这将每一个字节的值限制在0到255之间。因为这个值是无符号的,这个值不可以是负的,加号也是不允许的。例如,考虑下地址192.168.0.1,网络顺序的第一个字节必须为十进制的192。
当我们看到一个电影在屏幕上显示192.168.300.5的IP地址,我们就会知道这个制作者对于TCP/IP编程了解较少。虽然这个地址在句法上是正确的,但是十进制的300超过了最大的无符号数255。
网络地址分类
网络地址是由下面的两个组件构成的:
网络号(最重要位)
主机号(次重要位)
网络号标识主机可以连接到的网络。主机号标识一个特定网络中的一个主机(例如我们的PC)。
正如我们已经知道的,IP地址是32位的(或者是4个8位字节)。然而,网络号与主机号组件之间的分隔并不是固定的位置。分隔线取决于网络地址的分类,这是由地址的最重要的字节的检测来决定的。下表显示了IP地址是如何分类的:
Table 3.1: Internet Address Classes
Class Lowest Highest Network Bits Host Bits
A 0.0.0.0 127.255.255.255 7 24
B 128.0.0.0 191.255.255.255 14 16
C 192.0.0.0 223.255.255.255 21 8
D 224.0.0.0 239.255.255.255 28 N/A
E 240.0.0.0 247.255.255.255 27 N/A
A,B,C类定义了主机的特定IP地址。对于D类与E类地址,在地址没有主机位可用。D类地址用于多播,其中28位用于描述一个多播组。E类地址的27位保留的。
下图描述了32IP地址的分隔。下图显示的常用的A,B,C类地址:
理解网络掩码
有时我们必须决定一个地址的网络掩码。如是要我们设置我们的网络时这尤为正确。所以,什么是一个网络掩码。
如果我们将一个IP地址作为32位,网络ID是由地址的最重要的位来标识的。另外,同一个地址的主机ID是由次重要的位来决定的。网络掩码是一个简单的值,我们可以用来与一个地址进行按位与,从而只保留网络ID。下图显示了IP地址192.168.9.1是如何进行掩码从而得到网络ID位的。
结果就得到了IP地址中表示网络部分的最重要的位,而没有主机ID。下图演示了一个网络掩码如何转换为一个十进制IP地址的:
如果我们必须设置我们的网络,我们就必须确定我们的网络掩码是多少。下表列出A,B,C类地址的网络掩码:
Class Lowest Highest Netmask
A 0.0.0.0 127.255.255.255 255.0.0.0
B 128.0.0.0 191.255.255.255 255.255.0.0
C 192.0.0.0 223.255.255.255 255.255.255.0
有时,在一个网络软件中,我们的软件必须可以分类一个网络地址。有时,这是通过确定一个默认的网络掩码来简单完成的。
下面提供了一个简单的例子程序来演示如何由一个套接口地址分类一个IP地址。
在这个程序中,在一个网络套接口地址结构中设置了四个不同的IP地址。然后对这个地址进行检测与分类。这就演示了如何分类连接到我们服务器的远程客户端的IP地址。
/*
* netmask.c
*
* Classify an IP address:
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char **argv)
{
int x; /* Index variable */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
unsigned msb; /* most significant byte */
char class;
char *netmask;
static struct
{
unsigned char ip[4];
}addresses[]={
{{44,135,86,12}},
{{127,0,0,1}},
{{172,16,23,95}},
{{192,168,9,1}}
};
for (x=0;x<4;x++)
{
/*
* set up the socket address,to
* demonstrate how to classify it;
*/
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,addresses[x].ip,4);
len_inet = sizeof adr_inet;
/*
* classify this address
*
* 1 get the most significant byte
* 2 classify by that byte
*/
msb = *(unsigned char*)&adr_inet.sin_addr.s_addr;
if((msb &0x80) == 0x00)
{
class = 'A';
netmask = "255.0.0.0";
}
else if((msb &0xc0) == 0x80)
{
class = 'B';
netmask = "255.255.0.0";
}
else if((msb &0xe0) == 0xc0)
{
class = 'C';
netmask = "255.255.255.0";
}
else if((msb &0xf0) == 0xe0)
{
class = 'D';
netmask = "255.255.255.255";
}
else
{
class = 'E';
netmask = "255.255.255.255";
}
printf("Address %u.%u.%u.%u is class %c "
"netmask %s\n",
addresses[x].ip[0],
addresses[x].ip[1],
addresses[x].ip[2],
addresses[x].ip[3],
class,
netmask);
}
return 0;
}
通过这个程序例子,我们就可以了解如何来分类一个正在处理的IP地址。
分配IP地址
在前一个例子中,我们已经了解了如何来分类一个IP地址。IP地址是由一个名InterNIC的组织分配给各种个人或是组织的。然而一些范围的IP地址是设置为私有的,而其他的一些保留为特殊的用途。
私有IP地址
通常IP地址必须由InterNICd rs.internic.net进行注册。然而,如果我们的系统并没有直接连接到网络,我们并不需要一个全球唯一的地址。相反,我们可以使用私用的IP地址。
紧随着的第一个问题就是"我们应使用什么IP地址?"。在这一节,我们就会解答这个问题。
RFC 1597是一个描述私有IP地址是如何分配的网络标准文档。下表是一个简要的描述:
Class Lowest Highest Netmask
A 10.0.0.0 10.255.255.255 255.0.0.0
B 172.16.0.0 172.31.255.255 255.255.0.0
C 192.168.0.0 192.168.255.255 255.255.255.0
A,B,C类IP地址的选择在很大程度上取决于单个的网络数量以及我们要建立的主机数。如果网络以及主机数很小,那个一个C类就足够了。相对于,一个A类地址允许一个网络,但是却有大量的主机数目。B类地址提供了大量的网络数与主机数。
保留IP地址
存在大量的保留IP地址,而这些内容位RFC 1166中。作为保留系列地址的一个例子,在下表中将Amateur广播IP地址系列作为例子。现在AX.25协议已经构建进入Linux内核,将会有更多的广播业余爱好者使用这些IP地址。
Class Lowest Highest Netmask
A 44.0.0.0 44.255.255.255 255.0.0.0
处理IP地址
为了简化将字符串格式的IP地址转换为可用的套接口地址的编程负担,提供了一些函数来完成这样的工作。这些函数将会在下面的部分中进行描述。
使用inet_addr(3)函数
我们首先要了解的函数是一个较老的函数,这在新的代码中将不会再使用。然而,我们仍可以在已存在的网络代码中看到这个函数,所以我们应了解他,并且知道他的限制。
inet_addr(3)函数概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_addr(const char *string);
这个函数接受一个输入的C字符串参数string,并且将其解析为一个32位的网络地址值。32位地址值是以网络字节顺序返回的。
如果输入参数string并不是一个可用的地址值,则会返回INADDR_NONE。其他的返回值则代表转换的值。
下面的这个例子程序演示了如何使用这个函数。当这个程序运行时,会将包含一个IP地址的C字符串转换为一个网络顺序的32位IP地址。然后把这个值放入AF_INET套接口地址,并且绑定到这个套接口。
/*
* inetaddr.c
*
* Example using inet_addr(3)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* this function reports the error and
* exits back to the shell
*/
static void bail(const char *on_what)
{
fputs(on_what,stderr);
fputs("/n",stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
int sck_inet; /* Socket */
/* create a socket */
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if(sck_inet == -1)
bail("socket()");
/* Establish address */
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr.s_addr = inet_addr("127.0.0.95");
if(adr_inet.sin_addr.s_addr == INADDR_NONE)
bail("bad address");
len_inet = sizeof adr_inet;
/* Bind it to the socket */
z = bind(sck_inet,(struct sockaddr *)&adr_inet,len_inet);
if(z == -1)
bail("bind()");
/* Display our socket address */
system("netstat -pa --tcp 2>/dev/null"
" | grep inetaddr");
return 0;
}
程序运行结果如下:
$ ./inetaddr
tcp 0 0 127.0.0.95:9000 *:* CLOSE 992/inetaddr
$
inet_aton(3)函数
inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
inet_aton函数接受两个参数。参数描述如下:
1 输入参数string包含ASCII表示的IP地址。
2 输出参数addr是将要用新的IP地址更新的结构。
如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。使用这个函数并没有错误码存放在errno中,所以他的值会被忽略。
对于这个函数有一点迷惑的就是这个函数调用所需要的两个参数。如果我们定义了一个AF_INET套接口地址:
struct sockaddr_in adr_inet; /* AF_INET */
提供给inet_aton函数调用的参数指针为 &adr_inet.sin_addr
下面这个程序使用inet_aton函数,而不是我们在前面所谈到的in_addr函数。
/*
* inetaton.c
*
* Example using inet_aton
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* this function reports the error and
* exits back to the shell
*/
static void bail(const char *on_what)
{
fputs(on_what,stderr);
fputs("\n",stderr);
}
int main(int argc,char **argv)
{
int z;
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
int sck_inet; /* Socket */
/* Create a Socket */
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if(sck_inet == -1)
bail("Socket()");
/* Establish address */
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if( !inet_aton("127.0.0.1",&adr_inet.sin_addr))
bail("bad address");
len_inet = sizeof adr_inet;
/* Bind it to the socket */
z = bind(sck_inet,(struct sockaddr *)&adr_inet,len_inet);
if(z == -1)
bail("bind()");
/* Display our socket address */
system("netstat -pa --tcp 2>/dev/null"
" | grep inetaton");
return 0;
}
程序的运行结果如下:
S$ ./inetaton
tcp 0 0 127.0.0.23:9000 *:* CLOSE 1007/inetaton
使用inet_ntoa(3)函数
有时一个套接口地址代表一个连接到我们服务器的用户的地址,或者是一个UDP包。将一个网络顺序的32位值转换为一个点分隔的IP地址值是不方便的。从而提供了inet_ntoa函数。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr addr);
这个函数仅需要一个输入参数addr。注意struct in_addr是网络套接口地址的一个内部部分。这个地址被转换为函数个内部的的static缓冲区。字符数组的指针作为返回值返回。只有当下一次调用这个函数时结果才会可用。
如果在我们的程序中addr作为一个sockaddr_in结构而存在,那么下面的代码就显示了如何使用inet_ntoa函数来执行这个转换。IP地址转换为一个字符串,并且使用printf函数进行输出。
struct sockaddr_in addr; /* Socket Address */
printf("IP ADDR: %s\n",
inet_ntoa(addr.sin_addr));
下面提供一个完整的程序例子。
/*
* inetntoa.c:
*
* Example using inet_ntoa(3):
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
/*
* Establish address (pretend we got
* this address from a connecting client):
*/
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if(!inet_aton("127.0.0.23",&adr_inet.sin_addr))
puts("bad address");
len_inet = sizeof adr_inet;
/*
* Demonstrate use of inet_ntoa(3):
*/
printf("The IP Address is %s\n",
inet_ntoa(adr_inet.sin_addr));
return 0;
}
这个程序的运行结果如下:
$ ./inetntoa
The IP Address is 127.0.0.23
使用inet_network(3)
有时会有这样的情况,将一个点分隔的IP地址转换为一个32位的主机顺序的值是比较方便的。当我们要执行掩码值从地址中得到主机位或是网络位是更为方便。
inet_network的函数概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_network(const char *addr);
这个函数需要一个在参数addr中包含一个点分隔的地址输入字符串。返回值是IP地址的32位值,但是以主机顺序的格式而存在。然而,如果输入值不合法,返回结果就会为0xFFFFFFFF。
拥有一个主机端顺序的返回值意味着我们可以安全的执行掩码或是位操作。如果返回值为网络端顺序,那么对于不同的CPU平台就会有不同的操作。
下面的例子程序演示了如何使用inet_network函数。下面显示了如何从一个C地址得到一个网络地址:
unsigned long net_addr;
net_addr =
inet_network("192.168.9.1") & 0xFFFFFF00;
赋给net_addr的值应为)0xC0A80900(或者是点分隔的192.168.9.0)。与操作屏蔽掉低8位从而得到网络ID,而没有主机ID。
/*
* network.c
*
* Example using inet_network(3):
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
int x;
const char *addr[]={
"44.135.86.12",
"127.0.0.1",
"172.16.23.95",
"192.168.9.1"
};
unsigned long net_addr;
for(x=0;x<4;x++)
{
net_addr = inet_network(addr[x]);
printf("%14s = 0x%08lX net 0x%08lX\n",
addr[x],net_addr,(unsigned long)htonl(net_addr));
}
return 0;
}
程序的运行结果如下:
$ ./network
44.135.86.12 = 0x2C87560C net 0x0C56872C
127.0.0.1 = 0x7F000001 net 0x0100007F
172.16.23.95 = 0xAC10175F net 0x5F1710AC
192.168.9.1 = 0xC0A80901 net 0x0109A8C0
$
使用inet_lnaof(3)函数
inet_lnaof函数将一个包含在套接口地址中的网络字节顺序的IP地址转换为一个主机ID,而没有网络ID。返回值为主机端顺序。
这个函数省去我们确定IP地址然后得到主机ID部分的繁琐操作。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_lnaof(struct in_addr addr);
输入参数必须为我们通常操作所用的套接口地址的struct in_addr成员。这个值必须为网络字节顺序,而这也正是这个函数所要求的。下面的例子演示了如何使用一个sockaddr_in地址为调用这个函数:
struct sockaddr_in addr; /* Socket Address */
unsigned long host_id; /* Host ID number */
host_id = inet_lnaof(addr.sin_addr);
下表列出了一些可以应用inet_lnaof函数的值以及返回结果。
IP Number Class Hexadecimal Dotted-Quad
44.135.86.12 A 0087560C 0.135.86.12
127.0.0.1 A 00000001 0.0.0.1
172.16.23.95 B 0000175F 0.0.23.95
192.168.9.1 C 00000001 0.0.0.1
我们可以注意到在上表中A类地址在反回结果中只有第一个字节为0,而B类地址在返回结果高十六位为0.最后C类地址前三个字节为0,只保留最后一位的主机号。
使用inet_netof(3)函数
inet_netof函数是与inet_lnaof函数相对的。inet_netof函数返回网络ID而不是主机ID。在其他方面,这两个函数是相同的。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long inet_netof(struct in_addr addr);
如下面的例子所示:
struct sockaddr_in addr; /* Socket Address */
unsigned long net_id; /* Network ID number */
net_id = inet_netof(addr.sin_addr);
下表列出这个函数的一些例子返回值:
IP Number Class Hexadecimal Dotted-Quad
44.135.86.12 A 0000002C 0.0.0.44
127.0.0.1 A 0000007F 0.0.0.127
172.16.23.95 B 0000AC10 0.0.172.16
192.168.9.1 C 00C0A809 0.192.168.9
使用inet_makeaddr(3)函数
使用inet_netof与inet_lnaof函数我们可以得到主机ID与网络ID。要使用网络ID与主机ID重新组合为IP地址,我们可以使用inet_makeaddr函数。这个函数的概要如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
struct in_addr inet_makeaddr(int net,int host);
这个函数的参数描述如下:
1 net参数是网络ID,右对齐并且是主机端顺序。这也由函数inet_netof函数的返回值相同。
2 host参数是主机ID,主机端顺序。这也由函数inet_lnaof返回值相同。
返回值存放在sockaddr_in套接口地址中的struct in_addr成员中。这个值是网络字节顺序。
下面所演示的例子程序使用了inet_netof,inet_lnaof,inet_makeaddr三个函数。sockaddr_in结构中的IP地址将会被分解为主机ID与网络ID。然后套接口地址清零,并且由刚才得到的网络部分与主机部分重新进行组合。
/*
* makeaddr.c
*
* Demonstrate inet_lnaof,inet_netof
* and inet_makeaddr functions
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
int x;
struct sockaddr_in adr_inet; /* AF_INET */
const char *addr[] =
{
"44.135.86.12",
"127.0.0.1",
"172.16.23.95",
"192.168.9.1"
};
unsigned long net,hst;
for(x=0;x<4;x++)
{
/*
* Create a socket address
*/
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
if(!inet_aton(addr[x],&adr_inet.sin_addr))
puts("bad address");
/*
* Split address into Host & Net ID
*/
hst = inet_lnaof(adr_inet.sin_addr);
net = inet_netof(adr_inet.sin_addr);
printf("%14s : net=0x%08lx host=0x%08lx\n",
inet_ntoa(adr_inet.sin_addr),net,hst);
/*
* Zero the address to prove later that
* we can reconstruct this value:
*/
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr = inet_makeaddr(net,hst);
/*
* Now display the reconstructed address;
*/
printf("%14s : %s\n\n",
"inet_makeaddr",
inet_ntoa(adr_inet.sin_addr));
}
return 0;
}
程序的运行结果如下:
$ ./makeaddr
44.135.86.12 : net=0x0000002C host=0x0087560C
inet_makeaddr : 44.135.86.12
127.0.0.1 : net=0x0000007F host=0x00000001
inet_makeaddr : 127.0.0.1
172.16.23.95 : net=0x0000AC10 host=0x0000175F
inet_makeaddr : 172.16.23.95
192.168.9.1 : net=0x00C0A809 host=0x00000001
inet_makeaddr : 192.168.9.1
$
套接口类型与协议
在第一章我们看到了如何使用socketpair函数来创建一对本地套接口。在这一章我们将会了解使用socket函数来创建一个套接口。通常情况下这两个函数都有域,套接口类型,以及协议参数。
这一章将会建立在前几章的基础之上,并且主要关注于socket函数调用。这包括下面的一些内容:
域参数
套接口类型参数
协议参数
指定一个套接口的域
在第一章,我们可以看到,对于socketpair函数,域参数必须为AF_LOCAL或是AF_UNIX(这两个值是等同的)。然后在第二章,我们可以注意到我们使用了socket函数调用,并且将其域参数指定为AF_INET。在这些以及其他的情况下,我们可以推测出域参数在一定程度上指明要使用的协议。
从技术上讲,域参数实际上指明了要使用的协议族,而不是一个特定的协议。这需要一些历史的解释。
BSD套接口接口经历了一系列的革命性变化。在早期的套接口实现中,人们预想当指定一个套接口时会遇到下面的问题:
一个协议族的一个或是多个协议
一个或是多个协议的一个或是多个地址格式
基于这些可能的认识,原始的套接口接口在创建一个套接口之前提供了一些方法来定义下面的内容:
1 要使用的协议簇。例如,C宏PF_INET表明将会使用协议的网络IP族。
2 要使用的族中特定的协议。例如,宏IPPROTO_UDP将指明将要使用UDP协议。
3 要使用的地址族。例如,宏AF_INET表明一个特定的协议将会使用一个网络IP地址。
从后面的我们学习我们将会了解到对于一个给定的协议簇再也不会有多于一个的地址格式定义。这是继承现代套接口接口的结果。Linux使用现代的。这对于我们意味着什么呢?这就意味着一个套接口接口只是简单的接受PF_INET宏或是AF_INET宏来指明要使用的域。
选择PF_INET或是AF_INET
标准推荐使用PF_INET而不是AF_INET来指定域(也就是要使用的协议族)。然而,大量的存在的C程序代码与旧版本保持一致,而且许多程序员拒绝做出这种改变。在
在前面的章节中,我们在socketpair函数和socket函数的域参数中使用AF_UNIX,AF_LOCAL,AF_INET。这可以正常工作是因为AF_UNIX=PF_UNIX以及AF_INET=PF_INET,等等。然而,在将来也许就不会这样的情况了。
为了使用新的标准与习惯,我们这里所提供的例子与演示程序将会使用新的标准。这就意味着当调用socketpair函数时将会指定PF_LOCAL,而不是AF_LOCAL来指定域参数值。相类似的,socket函数也会使用相同的方式。
使用PF_LOCAL与AF_LOCAL宏
我们将会注意到套接口地址仍然要使用正确的地址族常量来进行初始化,例如AF_INET。PF_INET选择在套接口创建函数中的协议族,而AF_INET宏选择套接口地址结构中的地址族。下面的代码显示了如何使用PF_LOCAL和AF_LOCAL:
int z; /* Status Code */
int sp[2]; /* Socket Pair */
struct sockaddr_un adr_unix; /* AF_LOCAL */
z = socketpair (PF_LOCAL,SOCK_STREAM,0,sp);
. . .
adr_unix.sun_family = AF_LOCAL;
socketpair函数中使用PF_LOCAL宏来指定在域参数中要使用的协议族。注意当在adr_unix结构中建立套接口地址时,我们使用AF_LOCAL。
使用socket(2)函数
在我们学习更多的套接口类型参数以及协议参数之前,我们要先来了解一下socket函数。与域参数只可以指定为PF_LOCAL的socketpair函数不同,socket函数可以用来创建任何协议族支持的套接口。这个函数的概要如下:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
这个函数接受三个输入参数:
1 套接口的domain(要使用的协议族)
2 套接口需要的type
3 协议族要使用的特定的protocol
如果调用成功,套接口将会作为函数的返回值返回。与文件描述符相类似,如果值为零或是一个正值时,返回一个套接口。如果出错则会返回-1。当返回错误时,外部变量errno将会记录错误号。
对于新程序员来说socket函数的一个很困难的方面就是必须为三个输入参数做出的选择很多。我们这一章的目的就是来了解这些选择以及如何进行选择。
选择一个套接口类型
我们已经知道为socket或是socketpair函数选择一个域值就是选择了一个要使用的协议族。例如,我们已经知道:
PF_LOCAL表明指定了一个本地UNIX套接口协议族。
PF_INET表明使用网络协议族
从而我们现在只需要了解另外的两个输入参数。
socket和socketpair函数调用中的套接口类型参数指明了一个套接口如何与我们的程序进行接口。但是这并不是全部,因为这个参数与所选择的协议有关。
通常程序员会为套接口类型参数选择下列之中的一个:
•SOCK_STREAM *
•SOCK_DGRAM *
•SOCK_SEQPACKET
•SOCK_RAW
标识为星号的两个是我们通常情况下会用到的两个。SOCK_SEQPACKET通常用于非网络协议,例如X.25,或是广播协议AX.25。
理解SOCK_STREAM套接口类型
当我们希望要与远程的套接口执行I/O操作时可以使用SOCK_STREAM套接口类型。套接口中的流与UNIX管道中流的概念相同。写入管道(或是套接口)一端的字节会作为连续的字节流在另一个端接收。在这里没有分隔线或是边界。在接收端没有记录长度,块尺寸,或是包的概念。在接收端当前可用的任何数据都会返回到调用者的缓冲区中。
下面我们用一个例子来演示流I/O的概念。在这个例子中,在我们的本地主机上有一个本地进程,连接到远程主机的一个远程进程上。本地主机通过两个独立的write调用向远程主机发送数据:
1 本地进程将要通过套接口向远程进程发送25字节的数据。Linux内核会或者不会选择缓冲这些数据。缓冲有助于改进内核或是网络的性能。
2 本地进程另外写入30个字节发送到远程进程。
3 远程进程执行一个设计用来从套接口获取数据的函数。在这个例子中的接收缓冲区最多可以读取256个字节。远程进程获取步骤1和2发送的55个字节。
在这里我们注意到发生的事情。本地进程执行向套接口的两次单独的写入。这可以是两个不同的消息,或是两个不同的数据结构。然而远程进程接收到所有写入的数据,总计55个字节。
另一个看待这个例子的方式就是本地进程也许必须以两个部分写来创建一个消息。接收端作为一个集合单元来接收消息。
在其他的情况下,依赖于可用的时间或是缓冲,远程进程也许会首先得到原始的25个字(或者更少)。然后,一旦接收函数调用成功,得到剩余的30个字节。简言之,一个流式套接口并不会保留任何消息边界。他只是简单向接收程序返回数据。
接收端并不会区区原始消息的边界。在我们的这个例子中,他并不会区分第一次写的25个字节和第二次写的30个字节。他所知道的只是他接收到的数据字节,并且总字节数为55。
一个流式套接口有一个其他的重要属性。与UNIX管道相类似,写入一个流套接口的字节被认为在另一端以写入的顺序接收到。例如IP协议,在其中包可以通过不同的路由到达目的地,经常会发生后面的包先到达的情况。SOCK_STREAM套接口保证我们接收程序所接收到的数据就是我们写入的顺序。
让我们回顾一下SOCK_STREAM套接口:
并不会保留消息边界。接收端并不会确定使用了多和个write函数来发送数据。他也并不会确定在接收到的字节流中write开始与结束的位置。
认为接收到的数据字节顺序就是我们写入的字节顺序。
认为所有写入的数据都会被远程进程接收到,而没有错误。如果失败,就会在所有的合理修复尝试之后报告错误。所有的修复尝试都是自动,而不会受我们程序的控制。
最后一点对我们的讨论来说是新的。一个流套接口意味着将会做出所有合理的努力来将数据传送到另一个套接口。如果不能这样做,就会向接收端以及写入端报告错误。在这一点上,SOCK_STREAM是可靠的数据传送。这个特征使他为一个非常流行的套接口类型。
另外一点关于SOCK_STREAM类型套接口的属性就是:
数据是通过一对连接的套接口来传送的
为了保证数据传输以及强制字节顺序,底层的协议使用了一对连接的套接口。对于此时,我们只需要简单的知道SOCK_STREAM类型意味着必须在操作之前建立连接。
理解SOCK_DGRAM套接口类型
有时会有这样的情况:并不完全需要数据按序列到达远程端。另外,也许并不需要数据传输是可靠的。下面列出了SOCK_DGRAM套接口类型的一些特点:
传输包,也许在接收端是乱序的。
包也许会丢失。并不会试着修复这种错误。而且也并不必须通知接收端发生了包丢失。
数据报包有一定的尺寸限制。超过这些限制的包并不会通过一定的路由或是节点进行传输。
包是以无连接的方式发送到远程进程。这允许一个程序将他的消息定从位到一个不同的远程进程,从而每一个消息可以写入同一个套接口。
与连接的流式套接口不同,一个数据报套接口只是简单的通过单个包传输数据。在这里我们要记得的就是例如IP这样的协议,单个的包可以通过不同的方式进行路由。这经常会造成到达目的地的包的顺序与他们发送的顺序不同。SOCK_DGRAM套接口类型就意味着这种无序的信息发送对于程序来说是可以接受的。
发送一个数据报包是不可靠的。如果一个传送的包并没有被一个中间路由或是接收主机正确的接收,这个包就会被简单的丢弃。并不会保留他存在的记录,也并不会试着修复这种传输错误。
如果一个包很大也会造成丢失。如果一个包很大或是没有足够的缓冲空间来进行输送,发送主机与接收主机之间路径上的路由器也会丢弃这个包。并且在发生这种情况时,SOCK_DGRAM并不会进行错误修复。
SOCK_DGRAM类型套接口最后一个对于我们来说有趣的特点就是这种套接口并不意味着一个连接。每一次当我们使用这个套接口来发送一个消息时,他也许会到达另一个接收端。
另一方面,面向连接的协议求建立一个连接过程。这就要求为了建立连接必须发送一定的数据包。从这一点来看,SOCK_DGRAM类型的套接口是很有效率的,因为他们不需要建立连接。
然而在我们选择使用SOCK_DGRAM之前,我们必须仔细的考虑下面的内容:
可靠性的需求
顺序数据的需求
数据尺寸的需求
理解SOCK_SEQPACKET套接口类型
这种套接口类型对于X.25和AX.25协议是非常重要的。他与SOCK_STREAM相类似,但是却有一个明显的区别。区别就在于SOCK_STREAM不提供信息边界,但是SOCK_SEQPACKET提供。例如,当使用X.25协议时,就会选择SOCK_SEQPACKET,每一个数据包会按着他原始写入的单元尺寸进行接收。
例如,假设发送端地下面的两个写操作:
1 写入一个25字节的信息
2 写入一个30字节的信息
尽管接收进程表明在一个调用中接收55个字节,事实上是发生下面的接收事件:
1 接收25字节长度的信息。这对应着由发送进程写入的第一个信息。
2 接收30字节长度的第二个信息。这对应着由发送进程写入的第二个信息。
尽管接收缓冲区可以接收55字节长度的信息,但是在套接口上的第一个read调用只接收第一个25字节长度的信息。这会通知程序这个信息是精确的25个字节长度。下一个read调用会接收下一个30字节的信息,而不论是否可以接收更多的数据。
由于这种特性,我们可以发现SOCK_SEQPACKET保留了原始的信息边界。下面是这种套接口的小结:
保留信息边界。这个特性使其与SOCK_STREAM类型相区别。
接收的数据是按着所发送的顺序进行精确接收。
所有的数据假定无误的发送到接收端。如果在合理的自动修复尝试后不可以传输,就会向发送端与接收端报告错误。
数据是在一对连接的套接口上进行传输的。
选择协议
也许我们会认为为一个新的套接口指定了协议族与套接口类型,就不再需要其他的内容了。尽管通常而言对于一个给定的协议族与套接口类型只使用一个协议,但是在有的情况下并不是这样的。socket与socketpair函数的协议参数可以使得我们当这种需要出现时可以更明确。
一个好消息就通常我们并不需要为这个参数指定一个值。通常我们只需要将协议指定为零。这会允许Linux内核为我们所指定的其他参数选择合适的协议。
然而有一些程序员更喜欢显式的指定协议参数值。这对需要一个特定的协议并且不允许替换的特定程序来说是非常重要的。这允许我们选择最终的协议,而不依赖于最新的内核。但是这样做的一个缺点就是当网络和协议发生变化时,就必须有人返回查看我们的代码并做出相应的修改。
使用PF_LOCAL和SOCK_STREAM
在socket或是socketpair函数中对于PF_LOCAL套接口,我们会将协议参数指定为零。这是这个参数唯一支持的值。使用PF_LOCAL和SOCK_STREAM的正确socket函数调用如下所示:
int s;
s = socket
(PF_LOCAL,SOCK_STREAM,0);
if ( s == -1 )
perror("socket()");
这会创建一个允许一个进程与本地机器上的另一个进程进行通信的流式套接口。
使用PF_LOCAL和SOCK_DGRAM
当我们希望保留信息边界时,我们可以在本地套接口上使用SOCK_DGRAM。此时对PF_LOCAL域套接口并没有允许特定的协议。如下面的例子所示:
int s;
s = socket(PF_LOCAL,SOCK_DGRAM,0);
if ( s == -1 )
perror("socket()");
数据报套接口对于PF_LOCAL套接口是很合适的,因为他们更可靠,并且他们可以保留信息边界。他们并不会在网络传输中丢失错误,而PF_INET数据报会,因为他们保留在本地主机内部。然而,我们必须了解由内核缓冲区的缺少会造成PF_LOCAL包的丢失,尽管这很少会发生。
使用PF_INET和SOCK_STREAM
现在,对于PF_INET域的socket函数协议参数指定为零时,内核会选择IPPROTO_TCP。这会使得套接口使用TCP/IP协议。TCP/IP指令的TPC部分是建立在IP层上的传输层协议。这会提供数据包顺序化,错误控制以及修复。简言之,TCP使得使用网络协议来提供一个流式套接口成为可能。
如下面的例子所示,大多数的程序会选择简单的将协议参数指定为零,从而允许内核选择合适的协议:
int s;
s = socket(PF_INET,SOCK_STREAM,0);
if ( s == -1 )
perror("socket()");
将socket函数中的协议参数指定零意味着使用TCP/IP协议。然而,如果我们希望完全的控制,或是我们担心将来的内核默认协议不合适,我们可以显式的选择协议:
int s;
s = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
if ( s == -1 )
perror("socket()");
使用PF_INET和SOCK_DGRAM
这里我们将会描述我们将会用到的最后一种组合。PF_INET和SOCK_DGRAM的组合使得内核选择UDP协议。UDP是用户数据报协议的简称。一个数据报是一个独立的消息包。
这个协议允许程序从我们的套接口向我们所标识的远程套接口发送数据报。注意这种服务是不可靠的,但是对于许多要求高效率的服务类型而言是很合适的。例如,网络时间协议(NTP)使用UDP协议,因为他是高效的,并且是面向消息的,而消息的丢失是可以容忍的。消息丢失的影响在于也许时间的同步会花费更长的时间来实现,或者是当希望从多个NTP服务器响应时的精度缺失。
要创建一个UDP套接口,我们将协议参数指定为零。如下面的例子所示:
int s;
s = socket(PF_INET,SOCK_DGRAM,0);
if ( s == -1 )
perror("socket()");
然而如果我们更喜欢显示的指定UDP协议,我们可以用下面的例子方式:
int s;
s = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
if ( s == -1 )
perror("socket()");
将地址绑定到一个套接口
在前面的章节中我们准备了足够的知识来创建套接,并且格式化套接口地址。这一章将会这些基础上进行扩展我们将会理解bind是如何工作的,并且如何来正确的使用。
在这一章,我们将会学到下列内容:
bind函数如何将一个地址赋给一个套接口
如何由一个已经具有地址的套接口得到本地套接口地址
如何得到同等的套接口地址
bind如何选择用于通信的网络接口
bind函数的目的
当我们用socket函数创建套接口时,他们是无名套接口。当演示socket函数时,这些套接口没有地址,但是也可以使用。然而,这些套接口可以工作只是因为他们是用这样的方法来创建的,在同一个Linux内核内。对于连接两个不同的主机的套接口而言,这是不可以的。
一个无名套接口是难于使用的。没有人可以向我们的无名套接口发送信息,因为这就像是一个没有电话号码的电话。因而,程序必须将一个名字绑定到套接口,从而可以通过其他的方法来访问。这就像将一个电话号码指定给某一个新电话,从而可以进行拨打。bind函数允许我们用同样的方式将一个地址赋给一个套接口。
在这一章中名字的内容与主机名没有任何关系。当讨论bind函数时,我们会经常用到名字这个单词,而这是指一个套接口地址。毕竟地址是一个名字的排列。为了避免混淆,在这一章我们会使用地址这个词。
使用bind函数
bind函数的目的是将一个套接口地址赋给一个无名套接口。这个函数的概要如下:
#include <sys/types.h>
include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
这个函数接受下列的三个输入参数:
1 由socket函数调用返回的sockfd文件描述符。
2 要赋给套接口的my_addr地址。
3 以字节表示的my_addr的地址长度(参数addrlen)。
如果成功,这个函数会返回0。如果失败则会返回-1,并且将错误号存放在errno变量中。
地址参数必须为一个指向地址结构的指针。我们将会注意到通常所用的地址类型为sockaddr结构类型。这就意味着我们必须使用C不应该的转换操作符来转换我们所传递的指针类型,从而来满足编译器的要求。下面的例子演示了一个建立网络地址的bind函数。在这里我们注意inet_aton以及bind函数的使用。
/*
* af_inet.c
*
* Demonstrating the bind function
* by establishing a Specific AF_INET
* Socket address
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* this function reports the error and
* exits back to the shell
*/
static void bail(const char *on_what)
{
perror(on_what);
}
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 */
/* create and IPv4 Inter 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);
inet_aton("127.0.0.24",&adr_inet.sin_addr);
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;/bind/p'");
close(sck_inet);
return 0;
}
这个程序的输出结果如下所示:
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.24:9000 *:* CLOSE 934/bind
获得套接口地址
如果我们编写的C库函数接收一个套接口作为输入参数,这时我们并不知道这个套接口的地址是多少。这时我们的函数并没有创建这个套接口,除非这个套接口是作为输入传递给我们的函数的,否则我们并不会知道这个地址。函数getsockname(2)函数允许我们获得这个地址。
getsockname函数概要如下:
int getsockname(int s, struct sockaddr *name, socklen_t *namelen)
这个函数接收下列的输入参数:
1 套接口s查询套接口地址
2 指向接收缓冲区的指针(参数name)
3 指向最大长度变量的指针。这个变量以字节方式提供了可以为缓冲区所接受的最大长度。这个值是由实际写入接收缓冲区的字节数来进行更新的。
注意,也bind函数相类似,getsockname使用通用地址结构sockaddr,这是因为他可以用于多种套接口类型。这就意味着我们可能需要在参数中提供的指针上执行C语言转换操作。
长度参数namelen指定了可以在参数中接收的最大字节长度。然而,在返回给调用者之前,namelen的值会被重写来指明有多少字节实际写入输入缓冲区。这会小于或是等于所提供的原始值。
如果函数调用成功则会返回0值。如果发生错误,则会返回-1,错误原因将会存放在变量errno中。
编写一个sock_addr()函数
为了演示getsockname的用法,下面提供一个小函数,这个函数接收一个套接口描述符作为输入。这个函数通过调用getsockname,然后向调用者提者返回一个格式化的字符串,从而可以用在printf调用中。
/*
* sckname.c
*
* Demonstrate getsockname(2):
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* this saves lines of code later:
*/
static void bail(const char *on_what)
{
perror(on_what); /* report error */
exit(1); /* exit programming */
}
/*
* this function accepts as input a socket
* for which a socket address must be
* is converted into a string and returned
*
* if an error occurs,NULL is returned.
*/
char *sock_addr(int s,char *buf,size_t bufsize)
{
int z; /* status return code */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
/*
* obtain the address of the socket:
*/
len_inet = sizeof adr_inet;
z = getsockname(s,(struct sockaddr *)&adr_inet,&len_inet);
if(z==-1)
return NULL; /* failed */
/*
* convert address into a string
* form that can be displayer:
*/
snprintf(buf,bufsize,"%s:%u",
inet_ntoa(adr_inet.sin_addr),
(unsigned)ntohs(adr_inet.sin_port));
return buf;
}
/*
* main program
*/
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 */
char buf[64]; /* work buffer */
/*
* 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);
inet_aton("127.0.0.24",&adr_inet.sin_addr);
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()");
/*
* now test our sock_addr() function:
*/
if(!sock_addr(sck_inet,buf,sizeof buf))
bail("sock_addr()");
printf("Address is '%s'\n",buf);
close(sck_inet);
return 0;
}
这个函数的执行结果如下:
$ ./sckname
Address is '127.0.0.24:9000'
$
获得点套接口地址
在最后的部分,我们将会看到函数getsockname在获得一个套接口地址是相当有用的。然而,当我们的代码希望确定我们的套接口连接到哪一个远程套接口地址时需要花费相当的时间。确定一个套接口的远程地址就像当我们接到一个电话时我们要查找出拨打电话人的电话号码一样。
来完成这个任务是getpeername(2)函数。当我们开始检测和编写一个服务器代码时这个函数是相当有用的。在这里进行介绍是因为他与getsockname相类似。getpeername函数概要如下:
#include <sys/socket.h>
int getpeername(int s, struct sockaddr *name, socklen_t *namelen);
在这里我们可看到这个函数的参数与getsockname函数完全相同。
下面的代码定义了一个名为peer_addr()的函数。这个代码的设计与前面的sock_addr()函数相类似,但是这并不是一个完整的代码,因为这里只是显示了函数本身的代码,而没有主程序。
/*
* getpeer.c
*
* Demonstrate getpeername(2):
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* this function accepts as input a socket
* for which a peer socket address must be
* is converted into a string and returned
*
* if and error occurs,NULL is returned
*/
char *peer_addr(int s,char *buf,size_t bufsize)
{
int z; /* status return code */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
/*
* obtain the address of the socket:
*/
len_inet = sizeof adr_inet;
z = getpeername(s,(struct sockaddr *)&adr_inet,&len_inet);
if(z==-1)
bail("getpeername()");
/*
* convert address into a string
* form that can be displayed:
*/
z = snprintf(buf,bufsize,"%s:%u",
inet_ntoa(adr_inet.sin_addr),
(unsigned)ntohs(adr_inet.sin_port));
if(z==-1)
return NULL;
return buf;
}
接口与地址
在我们继续套接口编程的其他方面之前,有一个我们必须理解的与套接口地址相关的其他概念。这就是接口地址的概念。
使用我们所熟悉的电话作为例子,想像一个总统办公室,在他的桌子上放有两部电话。使用其中的一个他可以与他的妻子进行联系。另一方面,使用红色的电话,他可以与俄罗斯总统进行联系。在某种意义上说,这两部的电话的每一个,是两个不同网络的接口。他们是:
普通的国内电话网
通过安全线的私有网络
在这个例子中的关键点就在于我们必须使用正确的接口来访问正确的网络。例如,总统不可以使用红色的电话与他的妻子进行联系。同样的,国内电话网也不可以拨通俄罗斯总统的电话。
相类似的方式,当我们的套接口程序要指明当试图与远程套接口建立连接所用的接口时需要花费相当的时间。当我们知道只有一个接口可以访问上的网络时就会变得简单了。
绑定一个指定的接口地址
为了给我们的通信指定一个接口,我们需要执行下面的步骤:
1 使用socket创建我们的套接口
2 使用bind函数将我们希望接受连接的接口IP地址绑定到本地套接口
下面的例子演示了如何指定一个网络接口地址。这些工作必须在套接口通信开始之前完成。
nt z;
int sck_inet; /* Socket */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if ( sck_inet == -1 )
abort(); /* Failed */
/* Establish address */
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr.s_addr("192.168.0.1");
adr_inet.sin_addr.s_addr == INADDR_NONE )
abort(); /* Failed */
len_inet = sizeof adr_inet;
z = bind(sck_inet, (struct sockaddr *)&adr_inet, len_inet);
绑定任何接口
我们如何接受任何接口的连接呢?我们可以执行下面的步骤:
1 使用socket创建一个套接口
2 使用bind函数将IP地址INADDR_ANY绑定到套接口
如下面的代码所示:
int z;
int sck_inet; /* Socket */
struct sockaddr_in adr_inet; /* AF_INET */
int len_inet; /* length */
sck_inet = socket(AF_INET,SOCK_STREAM,0);
if ( sck_inet == -1 )
abort(); /* Failed */
/* Establish address */
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9000);
adr_inet.sin_addr.s_addr = htonl(INADDR_ANY);
if ( adr_inet.sin_addr.s_addr == INADDR_NONE )
abort(); /* Failed */
len_inet = sizeof adr_inet;
z = bind(sck_inet, (struct sockaddr *)&adr_inet, len_inet);