Linux Socket学习(六)

套接口类型与协议

在第一章我们看到了如何使用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()");

你可能感兴趣的:(数据结构,linux,socket,unix,网络协议)