socket网络编程复习笔记(三):套接字描述符背后的秘密

时间:2022-02-01 10:19:27

1.套接字概念回顾

(1)套接字是一个标识符;
(2)套接字是一个结构;
(3)套接字是一个包含标识、数据和操作的复合体,是服务访问点。
我们已经知道,一个套接字描述符s实际上是一个整形数据,在winsock.h头文件中,SOCKET是这样被定义的:

typedef unsigned int u_int;
typedef u_int SOCKET;

但是,我们不得不提出一个问题,简简单单的一个整形数据,怎么可能完成如此复杂的通信功能呢?

2.套接口结构

真实的情形当然复杂一些。
我们应该已经注意到,申请套接字描述符是一个动作,申请套接字描述符所要占用的资源是另一个动作:

s = socket(af,type,proto);

socket函数的作用就是制造s对应的套接口结构。
来看一看套接口结构的定义:

struct socket{
short so_type;
short so_option;
short so_linger;
short so_state;
caddr_t so_pcb;
struct protosw *so_proto;
struct socket *so_head;
struct socket *so_q0;
struct socket *so_q;
short so_q0len;
short so_qlen;
short so_qlimit;
short so_timeo;
u_shurt so_error
pid_t so_pgid;
u_long so_oobmark;
struct sockbuf so_rcv, so_snd;
caddr_t so_tpcb;
void (*so_upcall)(struct socket *so, caddr_t arg, int waitf);
caddr_t so_upcallarg;
}

系统通过套接字标识符,找寻的就是这样一个套接口结构。
不难看出,套接口结构中包含了一次通信所需要的丰富的资源和服务。
我们可以从两个层次去理解这样做的用意:
一,从用户角度来看:只需给出标识符就能找到,具有透明性、方便性(用户不需掌握找寻的方法);
二,从系统的角度来看,系统如何根据标识符找到套接口,这是一个系统设计的问题。

自然而然地,接下来的问题就变成了:该如何去设计套接字标识符和套接口结构的对应呢?系统该如何设计?

3.尊卑有别:套接口与套接字

我们可以考虑利用索引表的形式,通过套接字描述符找到对应的套接口结构。我们假设索引表是一个进程符号表,第一列是套接字描述符向量(socket_id),第二列是对应的关系映射(到的套接口结构起始地址)。
因此,套接字描述符可以看做是套接口资源的编号,那么该如何分配套接字描述符呢?
参照端口号的分配方式,无外乎是两种:全局分配(统一分配)和局部分配(本地分配)。
若是全局分配,则系统的所有套接口资源进行统一编号,不同进程得到的套接字描述符各不相同,虽然便于管理但进程的独立性差(要求保证各个进程套接字描述符相异,会降低效率);
若是局部分配,则套接口资源仅在一个进程内部统一编号,且不同进程可使用相同的套接字描述符。虽然保证了进程的独立性,但是管理上会变得混乱(给定一个套接字,不能辨别出来自哪个进程)。
在实用模型中,我们采取的方案是局部分配,因为全局分配对系统来说确实太过复杂(或者说低效)。

我们将套接字编程的关系以套接字为中心展开,上面我们讨论的是从上至下的映射方式(套接字描述符到套接口资源),接下来我们从下往上的映射方式(从端口到套接口资源)再来考虑考虑。

4.另一种关系:端口与套接口

我们想问的是这样三个问题:
第一,TCP实体依据什么来处理接收到的报文?
第二,TCP实体如何处理报文?
第三,用户进程如何获取报文?
前面已经提到,一个端点地址结构中应当包含ip地址和端口号,而bind()函数(或者是隐式绑定亦然)会在套接字上绑定端点地址。因此第一个问题的答案非常容易:根据报文中的端口信息找到对应的套接口结构。
套接口结构定义中有一个struct sockbuf so_rcv, so_snd,而sockbuf定义又如下所示:

struct sockbuf{
u_long sb_cc;
u_long sb_hiwat;
u_long sb_mbcnt;
u_long sb_mbmax;
long sb_lowat
struct mbuf *sb_mb;
struct selinfo sb_sel;
short sb_flags;
short sb_timeo;
}

因此第二个问题也是好回答的:将报文挂在与端口绑定的套接口接收队列上。
至于用户如何获取报文,当然是从套接口接收队列中取出了。
接下来我们要探讨一个更为复杂的问题:套接口、套接字描述符、端口这三者的关系该如何理解?

5.三方混战:套接字、端口与套接口

套接字描述符、套接口结构和端口是相对独立的三个概念,而且一般来说它们是一一对应的。一个套接字描述符在使用socket()后,就对应了一个特定的套接口结构,而在使用bind()之后,该套接口结构也就工作在特定的端口上了,等到connect()完成,对方的端点就也确定了。
我们再来把这三者的关系理清来说一遍:
应用进程通过套接字描述符从而找到套接口结构;
TCP实体通过端口号找到工作在其上的套接口结构;
套接字描述符与套接口结构的映射关系是通过socket()函数实现的;
端口号是在bind()函数中填写到套接口中的。
接下来是对脑力的考验,我们再来审视这三者所做出的不同排列组合(以下只列举看上去不正常的组合)有何现实的意义:
socket网络编程复习笔记(三):套接字描述符背后的秘密

(1)一个套接字描述符对应多个套接口?
若是在一个进程内发生,则标识符的意义被颠覆(同时指向两种不同地通信资源);若是不同进程的同一值描述符对应多个套接口,则是允许的,因为前面已经有所铺垫:在实际中,套接字标识符采用局部分配的方式。
(2)多个套接字描述符对应一个套接口?
这样可以提供通信资源的共享,但会在数据归属上出现混乱(比如recvbuf中的数据到底该提交给哪个应用进程?)。
(3)多个套接口对应一个端口?
只要两个端点之一不一样,就是两个不同的通信关系,并无冲突。实际上服务器就是通过在一个端口上同时有多个套接字活动来提供多用户同时接入功能的。
(4)一个套接口对应多个端口?
这一点比较难想出个所以然来,不过这种一点对多点的方式在特殊场合可能是有用的(比如想通过同一个recvbuf来接收不同服务器的回送消息)。

6.结论

一个全相关包含五个要素,以确定一次通信:A的端口号+A的IP地址+B的端口号+B的IP地址+协议。两个设备只要任一要素不同都应该被认为是不同的通信。
在套接口结构里,记录了全相关的全部要素因此通过两个套接字就能够确定一次通信。对于其中一方,一个套接字描述符对应于一个套接字结构,就能对应于一次通信,具体就表现在recv()和send()等函数中不再需要目的地址信息。