第3节 套接字(socket

  socket 在所有的网络操作系统中都是必不可少的,而且在所有的网络应用程序中也是必不可少的。它是网络通信中应用程序对应的进程和socket 在所有的网络操作系统中都是必不可少的,而且在所有的网络应用程序中也是必不可少的。它是网络通信中应用程序对应的进程和网络协议之间的接口之间的接口,如图12.7所示。

  2、socket是网络编程的入口,它提供了大量的系统调用,构成了网络程序的主体;

  3、在Linux系统中,socket属于文件系统的一部分,网络通信可以被看作是对文件的读取,使得我们对网络的控制和对文件的控制一样方便。

  Linux 支持多种套接字种类,不同的套接字种类称为“地址族”,这是因为每种套接字种类拥有自己的通讯寻址方法。Linux 所支持的套接字地址族见表12.3。

  Linux将上述套接字地址族抽象为统一的 BSD 套接字接口,应用程序关心的只是 BSD 套接字接口,而 BSD 套接字由各地址族专有的软件支持。一般而言,BSD 套接字可支持多种套接字类型,不同的套接字类型提供的服务不同,Linux 所支持的部分 BSD 套接字类型见表 12.4,但表12.3中的套接字地址族并不一定全部支持表12.4中的这些套接字类型。

  下面我们以INET套接字地址族、流套接字类型为例,详细介绍套接字的工作原理和通信过程。

  INET 套接字就是支持 Internet 地址族的套接字,它位于TCP协议之上, BSD套接字之下,如图12.8,这里也体现了Linux网络模块分层的设计思想。

  但在 INET 套接字层中,它利用自己的 sock 数据结构来代表该套接字,因此,这两个结构之间存在着链接关系,Sock结构定义于include/net/sock.h(此结构有80多行,在此不予列出)。在 BSD 的 socket 数据结构中存在一个指向sock的指针sk,而在sock中又有一个指向socket的值中,这两个指针将 BSD socket 数据结构和sock 数据结构链接了起来。通过这种链接关系,套接字调用就可以方便地检索到 sock 数据结构。实际上,sock 数据结构可适用于不同的地址族,它也定义有自己的协议操作集proto。在建立套接字时,sock 数据结构的协议操作集指针指向所请求的协议操作集。如果请求 TCP 协议,则 sock 数据结构的协议操作集指针将指向 TCP 的协议操作集。

  进程在利用套接字进行通讯时,采用客户-服务器模型。服务器首先创建一个套接字,并将某个名称绑定到该套接字上,套接字的名称依赖于套接字的底层地址族,但通常是服务器的本地地址。套接字的名称或地址通过 sockaddr 数据结构指定,该结构定义于include/linux/socket.h中:

  对于 INET 套接字来说,服务器的地址由两部分组成,一个是服务器的 IP 地址,另一个是服务器的端口地址。已注册的标准端口可查看 /etc/services 文件。将地址绑定到套接字之后,服务器就可以监听请求链接该绑定地址的传入连接。连接请求由客户生成,它首先建立一个套接字,并指定服务器的目标地址以请求建立连接。传入的连接请求通过不同的协议层最终到达服务器的监听套接字。服务器接收到传入的请求后,如果能够接受该请求,服务器必须创建一个新的套接字来接受该请求并建立通讯连接(用于监听的套接字不能用来建立通讯连接),这时,服务器和客户就可以利用建立好的通讯连接传输数据。

  BSD 套接字上的详细操作与具体的底层地址族有关,底层地址族的不同实际意味着寻址方式、采用的协议等的不同。Linux 利用 BSD 套接字层抽象了不同的套接字接口。在内核的初始化阶段,内建于内核的不同地址族分别以 BSD 套接字接口在内核中注册。然后,随着应用程序创建并使用 BSD 套接字,

  内核负责在 BSD 套接字和底层的地址族之间建立联系。这种联系通过交叉链接数据结构以及地址族专有的支持例程表建立。

  每个地址族由其名称以及相应的初始化例程地址代表。在引导阶段初始化套接字接口时,内核调用每个地址族的初始化例程,这时,每个地址族注册自己的协议操作集。协议操作集实际是一个例程集合,其中每个例程执行一个特定的操作。

  Linux在利用socket()系统调用建立新的套接字时,需要传递套接字的地址族标识符、套接字类型以及协议,其函数定义于net/socket.c中:

  实际上,套接字对于用户程序而言就是特殊的已打开的文件。内核中为套接字定义了一种特殊的文件类型,形成一种特殊的文件系统sockfs,其定义于net/socket.c:

  static DECLARE_FSTYPE(sock_fs_type, sockfs, sockfs_read_super, FS_NOMOUNT);

  在系统初始化时,要通过kern_mount()安装这个文件系统。安装时有个作为连接件的vfsmount数据结构,这个结构的地址就保存在一个全局的指针sock_mnt中。所谓创建一个套接字,就是在sockfs文件系统中创建一个特殊文件,或者说一个节点,并建立起为实现套接字功能所需的一整套数据结构。所以,函数sock_create()首先是建立一个socket数据结构,然后将其“映射”到一个已打开的文件中,进行socket结构和sock结构的分配和初始化。

  新创建的 BSD socket 数据结构包含有指向地址族专有的套接字例程的指针,这一指针实际就是 proto_ops 数据结构的地址。

  BSD 套接字的套接字类型设置为所请求的 SOCK_STREAM 或 SOCK_DGRAM 等。然后,内核利用 proto_ops 数据结构中的信息调用地址族专有的创建例程。

  之后,内核从当前进程的 fd 向量中分配空闲的文件描述符,该描述符指向的 file 数据结构被初始化。初始化过程包括将文件操作集指针指向由 BSD 套接字接口支持的 BSD 文件操作集。所有随后的套接字(文件)操作都将定向到该套接字接口,而套接字接口则会进一步调用地址族的操作例程,从而将操作传递到底层地址族,如图12.10所示。

  实际上,socket结构与sock结构是同一事物的两个方面。如果说socket结构是面向进程和系统调用界面的,那么sock结构就是面向底层驱动程序的。可是,为什么不把这两个数据结构合并成一个呢?

  我们说套接字是一种特殊的文件系统,因此,inode结构内部的union的一个成分就用作socket结构,其定义如下:

  由于套接字操作的特殊性,这个结构中需要大量的结构成分。可是,如果把这些结构成分全都放在socket结构中,则inode结构中的这个union就会变得很大,从而inode结构也会变得很大,而对于其他文件系统,这个union成分并不需要那么庞大。因此,就把套接字所需的这些结构成分拆成两部分,把与文件系统关系比较密切的那一部分放在socket结构中,把与通信关系比较密切的那一部分则单独组成一个数据结构,即sock结构。由于这两部分数据在逻辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系。

  为了监听传入的 Internet 连接请求,每个服务器都需要建立一个 INET BSD 套接字,并且将自己的地址绑定到该套接字。绑定操作主要在 INET 套接字层中进行,还需要底层 TCP 层和 IP 层的某些支持。将地址绑定到某个套接字上之后,该套接字就不能用来进行任何其他的通讯,因此,该 socket数据结构的状态必须为 TCP_CLOSE。传递到绑定操作的 sockaddr 数据结构中包含要绑定的 IP地址,以及一个可选的端口地址。通常而言,要绑定的地址应该是赋予某个网络设备的 IP 地址,而该网络设备应该支持 INET 地址族,并且该设备是可用的。利用 ifconfig 命令可查看当前活动的网络接口。被绑定的 IP 地址保存在 sock 数据结构的rcv_saddr 和 saddr 域中,这两个域分别用于哈希查找和发送用的 IP 地址。端口地址是可选的,如果没有指定,底层的支持网络会选择一个空闲的端口。

  当底层网络设备接受到数据包时,它必须将数据包传递到正确的 INET 和 BSD 套接字以便进行处理,因此,TCP维护多个哈希表,用来查找传入 IP 消息的地址,并将它们定向到正确的socket/sock 对。TCP 并不在绑定过程中将绑定的 sock 数据结构添加到哈希表中,在这一过程中,它仅仅判断所请求的端口号当前是否正在使用。在监听操作中,该 sock 结构才被添加到 TCP 的哈希表中。

  创建一个套接字之后,该套接字不仅可以用于监听入站的连接请求,也可以用于建立出站的连接请求。不论怎样都涉及到一个重要的过程:建立两个应用程序之间的虚拟电路。出站连接只能建立在处于正确状态的 INET BSD 套接字上,因此,不能建立于已建立连接的套接字,也不能建立于用于监听入站连接的套接字。也就是说,该 BSD socket 数据结构的状态必须为 SS_UNCONNECTED。

  在建立连接过程中,双方 TCP 要进行三次“握手”,具体过程在 本章第二节网络协议一文中有详细介绍。如果 TCP sock 正在等待传入消息,则该 sock 结构添加到 tcp_listening_hash 表中,这样,传入的 TCP 消息就可以定向到该 sock 数据结构。

  当某个套接字被绑定了地址之后,该套接字就可以用来监听专属于该绑定地址的传入连接。网络应用程序也可以在未绑定地址之前监听套接字,这时,INET 套接字层将利用空闲的端口编号并自动绑定到该套接字。套接字的监听函数将 socket 的状态改变为 TCP_LISTEN。

  当接收到某个传入的 TCP 连接请求时,TCP 建立一个新的 sock 数据结构来描述该连接。当该连接最终被接受时,新的 sock 数据结构将变成该 TCP 连接的内核bottom_half部分,这时,它要克隆包含连接请求的传入 sk_buff 中的信息,并在监听 sock 数据结构的 receive_queue 队列中将克隆的信息排队。克隆的 sk_buff 中包含有指向新 sock 数据结构的指针。

  接受操作在监听套接字上进行,从监听 socket 中克隆一个新的 socket 数据结构。其过程如下:接受操作首先传递到支持协议层,即 INET 中,以便接受任何传入的连接请求。相反,接受操作进一步传递到实际的协议,例如 TCP 上。接受操作可以是阻塞的,也可以是非阻塞的。接受操作为非阻塞的情况下,如果没有可接受的传入连接,则接受操作将失败,而新建立的 socket 数据结构被抛弃。接受操作为阻塞的情况下,执行阻塞操作的网络应用程序将添加到等待队列中,并保持挂起直到接收到一个 TCP 连接请求为至。当连接请求到达之后,包含连接请求的 sk_buff 被丢弃,而由 TCP 建立的新 sock 数据结构返回到 INET 套接字层,在这里,sock 数据结构和先前建立的新 socket 数据结构建立链接。而新 socket 的文件描述符(fd)被返回到网络应用程序,此后,应用程序就可以利用该文件描述符在新建立的 INET BSD 套接字上进行套接字操作。

  socket 系统调用是 socket 最有价值的一部分,也是用户唯一能够接触到的一部分,它是我们进行网络编程的接口。

您可能还会对下面的文章感兴趣: