深入理解TLI网络编程
1. 引言
在网络编程领域,套接字(socket)接口简单且流行,但存在设计缺陷,即它并非协议无关的。虽然套接字可用于多种协议,如UNIX IPC、TCP/IP、ISO/OSI和XNS等,但为某一协议编写的套接字程序若要使用其他协议,就必须修改源代码。而传输层接口(Transport Layer Interface,TLI)则试图解决这一问题。
2. TLI概述
TLI是一个函数库,允许两个程序通过传输提供者进行通信。传输提供者可以是设备驱动程序或其他提供通信支持的操作系统接口。例如,TCP/IP协议支持是一种传输提供者,Novell IPX协议支持则是另一种。TLI设计的关键在于,如果程序员小心避免采取任何依赖协议的操作,那么为TLI编写的单个程序可以在不更改源代码的情况下,在任意数量的不同传输提供者上运行。实际上,当添加新的传输提供者时,程序甚至无需重新编译。
TLI库最早在System V Release 3中引入,但AT&T在开发该接口时,没有在SVR3中包含传输提供者,这意味着如果不购买第三方产品,TLI就没有可通信的对象。因此,在包含TCP/IP传输提供者的SVR4发布之前,套接字一直是编写网络程序的唯一可行接口,TLI几乎被弃用。不过,对于支持或维护System V系统的人来说,TLI仍然值得学习。
在SVR3和SVR4之间,TLI库进行了许多改进,其中大部分更改涉及添加一种独立于网络的方法来处理主机和服务地址。这些更改被Sun和Silicon Graphics采用,并包含在Solaris 2.x和IRIX 5.x中。而Hewlett - Packard出于与早期版本向后兼容的原因,没有采用这些新功能。HP - UX 10.x中的TLI库更类似于最初随SVR3提供的TLI库。
所有使用TLI的程序在Solaris 2.x和IRIX 5.x上必须与
-lnsl
库链接,在HP - UX 10.x上必须与
-lnsl_s
库链接。
3. netbuf结构
由于TLI是协议无关的,因此各种TLI函数使用的数据结构相同,无论使用的网络协议是什么。然而,在传输提供者接口处,没有数据格式的标准,不同的传输提供者使用不同的格式。例如,对于主机地址的表示没有标准,TCP/IP使用32位值,而ISO/OSI使用160位值。
TLI函数需要处理这些不同的数据格式,但必须以不影响函数的方式进行。在套接字接口中,这是通过使用通用的
struct sockaddr
数据类型,并将依赖协议的数据结构(如
struct sockaddr_un
、
struct sockaddr_in
等)强制转换为该通用类型来处理的。在TLI中,这是通过
struct netbuf
结构来处理的,该结构在头文件
tiuser.h
中定义:
struct netbuf {
unsigned int maxlen;
unsigned int len;
char *buf;
}
该结构的
buf
元素包含数据(如网络地址等),
len
元素表示
buf
的字节长度。当TLI函数填充用户提供的
buf
时,
maxlen
元素表示缓冲区的大小,以防止函数溢出。
struct netbuf
结构在SVR4 TLI库中被广泛使用,但在HP - UX 10.x中不可用。
4. 网络选择
TLI的优势在于它能够在不同的传输提供者(网络协议)上无需更改即可工作。例如,一个需要虚拟电路连接的程序并不关心该连接是通过TCP/IP还是ISO/OSI建立的,只要能完成任务即可。当程序员使用套接字编写程序时,他必须决定要使用的协议,并相应地编写程序。而当程序员使用TLI编写程序时,她只需决定所需的服务类型(如虚拟电路、数据报等),程序就可以在任何提供该类型服务的传输提供者的系统上运行。
TLI中的网络选择功能由
/etc/netconfig
文件驱动,该文件的示例如下:
| NetID | Semantics | Flags | Proto Family | Proto | Network Device | Directory Lookup |
| — | — | — | — | — | — | — |
| udp | tpi_clts | v | inet | udp | /dev/udp | switch.so,tcpip.so |
| tcp | tpi_cots_ord | v | inet | tcp | /dev/tcp | switch.so,tcpip.so |
| rawip | tpi_raw | - | inet | - | /dev/rawip | switch.so,tcpip.so |
| ticlts | tpi_clts | v | loopback | - | /dev/ticlts | straddr.so |
| ticotsord | tpi_cots_ord | v | loopback | - | /dev/ticotsord | straddr.so |
| ticots | tpi_cots | v | loopback | - | /dev/ticots | straddr.so |
该文件为系统上安装的每个网络协议包含一个条目,每个条目有七个字段:
-
NetID
:网络的唯一名称。
-
Semantics
:描述网络提供的服务类型,目前有四个合法值:
-
tpi_clts
:无连接传输服务(数据报)。
-
tpi_cots
:面向连接的传输服务(虚拟电路)。
-
tpi_cots_ord
:具有有序释放的面向连接的传输服务。
-
tpi_raw
:网络协议的“原始”(低级)接口。
-
Flags
:目前仅定义了
v
标志,表示该条目对NETPATH例程可见,使用短横线可使网络对这些例程暂时(或永久)不可见。
-
Proto Family
:协议族的名称,例如所有Internet协议都归为“inet”。
-
Proto
:协议本身的名称,如果协议没有名称,可以使用短横线。
-
Network Device
:访问网络和协议时使用的设备的路径名。
-
Directory Lookup
:包含网络协议名称到地址转换函数的共享库的逗号分隔列表。
有两组函数用于读取
/etc/netconfig
文件,它们都使用
struct netconfig
结构来描述一个条目:
#include <netconfig.h>
struct netconfig {
char *nc_netid;
unsigned long nc_semantics;
unsigned long nc_flag;
char *nc_protofmly;
char *nc_proto;
char *nc_device;
unsigned long nc_nlookups;
char **nc_lookups;
};
该结构的
nc_netid
、
nc_protofmly
、
nc_proto
和
nc_device
元素分别包含网络标识符、协议族、协议名称和网络设备名称。
nc_lookups
元素包含名称到地址转换库的名称,
nc_nlookups
表示这些库的数量。
nc_semantics
字段包含
NC_TPI_CLTS
、
NC_TPI_COTS
、
NC_TPI_COTS_ORD
或
NC_TPI_RAW
之一,
nc_flag
元素将包含
NC_NOFLAG
或
NC_VISIBLE
。
下面是网络选择相关函数的介绍:
-
网络配置库
:
- 读取
/etc/netconfig
文件最简单的方法是一次读取一个条目,或者通过网络标识符查找特定条目。相关函数包含在网络配置库中:
#include <netconfig.h>
void *setnetconfig(void);
int endnetconfig(void *handlep);
struct netconfig *getnetconfig(void *handlep);
struct netconfig *getnetconfigent(const char *netid);
void freenetconfigent(struct netconfig *netconfigp);
void nc_perror(const char *msg);
char *nc_sperror(void);
-
setnetconfig函数打开或倒回/etc/netconfig文件,返回一个指向“句柄”的指针,该指针必须与其他一些函数一起使用。在调用getnetconfig之前必须调用setnetconfig,但在调用getnetconfigent之前不必调用。endnetconfig函数关闭网络配置数据库,handlep应该是调用setnetconfig返回的值。 -
getnetconfig函数接受一个参数handlep,它返回网络配置数据库中的下一个条目,当没有更多条目可读时返回NULL。getnetconfigent函数返回网络标识符等于netid的条目,如果未找到则返回NULL。 -
getnetconfig和getnetconfigent返回的内存是动态分配的,可以调用freenetconfigent函数来释放该内存。注意,调用endnetconfig也会释放这些函数分配的内存,在程序使用完这些信息之前不应调用。 -
nc_perror函数可在库中的其他函数返回错误时调用,它会在标准错误输出上打印msg字符串,后跟描述发生的错误的消息。nc_sperror函数将返回错误消息字符串而不打印它。
为了使TLI程序具有可移植性,可以反复调用
getnetconfig
来查找具有所需语义的任何网络。例如,一个数据报应用程序可能会这样调用:
void *handlep;
struct netconfig *ncp;
handlep = setnetconfig();
while ((ncp = getnetconfig(handlep)) != NULL) {
if (ncp->nc_semantics == NC_TPI_CLTS)
break;
}
if (ncp == NULL) {
fprintf(stderr, "cannot find acceptable transport provider.\n");
exit(1);
}
/* use the network described by ncp */
而使用
getnetconfigent
的程序在定义上不能跨不同的传输提供者移植,因为它请求的是特定的传输提供者。
下面是网络选择的mermaid流程图:
graph TD;
A[开始] --> B[调用setnetconfig];
B --> C{是否有更多条目};
C -- 是 --> D[调用getnetconfig];
D --> E{是否满足语义要求};
E -- 是 --> F[使用该网络];
E -- 否 --> C;
C -- 否 --> G[输出错误信息并退出];
- NETPATH库 :
-
NETPATH库提供了另一种读取
/etc/netconfig文件的方法,该方法允许用户对选择的网络表达一些控制(偏好)。用户可以将NETPATH环境变量设置为他愿意使用的网络标识符的冒号分隔列表,并按他偏好的顺序排列。例如,如果用户更喜欢TCP而不是ISO TP4,但更喜欢ISO TP0而不是UDP,她可以将NETPATH环境变量设置为:
NETPATH=tcp:iso_tp4:iso_tp0:udp
- NETPATH库中有三个函数:
#include <netconfig.h>
void *setnetpath(void);
int endnetpath(void *handlep);
struct netconfig *getnetpath(void *handlep);
-
setnetpath函数打开或倒回/etc/netconfig文件,并返回一个指向描述该文件的“句柄”的指针。在调用getnetpath之前必须调用它。endnetpath函数关闭文件并释放例程返回的所有分配资源。 -
getnetpath函数读取由handlep描述的网络配置文件,它不按顺序读取文件,而是返回NETPATH环境变量中包含的下一个有效网络标识符的条目。因此,无论网络在文件中列出的顺序如何,getnetpath总是按环境变量指定的顺序返回它们。getnetpath会默默地忽略NETPATH中包含的无效或不存在的网络标识符,当NETPATH条目用完时返回NULL。 -
如果未设置
NETPATH变量,则getnetpath返回“默认”网络列表,即网络配置文件中列为“可见”的网络,这些网络将按列出的顺序返回。 -
getnetpath函数的使用与getnetconfig基本相同,程序反复调用getnetpath,直到找到具有所需语义的网络。通过对NETPATH环境变量中的值进行排序,用户可以在存在多个具有相同语义的网络时对选择哪个网络施加一些控制。
4. HP - UX 10.x中的网络选择
HP - UX 10.x中的网络传输选择是在编译时进行的,而不是在运行时进行的。没有函数库可以让程序员根据服务类型要求选择网络,程序员必须确切知道她想要什么,并将网络设备的名称直接编码到她的程序中。因此,一个编写为使用TCP作为其面向连接的传输服务的程序如果要使用ISO TP4,则必须进行修改。
从技术角度来看,SVR4提供的解决方案更好,它更具可移植性,可以在具有不同网络服务的系统之间无修改地移动。但从实际角度来看,这可能并不重要。几乎所有连接到网络的系统都连接到TCP/IP网络,因此程序“默认”具有可移植性。对于那些使用其他网络传输的程序,它们可能本来就不打算在其本地环境之外进行移植。
深入理解TLI网络编程
5. 名称到地址的转换
在网络编程中,人们通常使用主机名来指代主机,但网络协议更倾向于使用地址。因此,和套接字接口一样,TLI也必须提供一种在主机和地址、端口名称和端口号之间进行转换的方法。相关函数定义在
netdir.h
头文件中:
#include <netdir.h>
int netdir_getbyname(const struct netconfig *config,
const struct nd_hostserv *service,
struct nd_addrlist **addrs);
int netdir_getbyaddr(const struct netconfig *config,
struct nd_hostservlist **service,
const struct netbuf *netaddr);
int netdir_options(const struct netconfig *netconfig,
const int opt, const int fd, char *argp);
void netdir_free(void *ptr, const int struct_type);
void netdir_perror(char *s);
char *netdir_sperror(void);
与套接字接口独立处理主机地址和服务(端口号)不同,TLI将它们视为一个整体。因此,一个地址是一个由(主机地址,端口号)组成的元组。
netdir_getbyname
函数用于查找
service
参数中给定的主机名和服务名。
service
是一个指向
struct nd_hostserv
类型的指针,该结构体定义如下:
struct nd_hostserv {
char *h_host;
char *h_serv;
};
-
h_host字段包含主机名。 -
h_serv字段包含服务名。对于没有名称的服务(例如,任意选择的端口号),h_serv应指向端口号的字符串表示。h_host元素可以包含一些特殊值,而不是主机名,具体如下: -
HOST_SELF:代表本地程序可以用来引用本地主机的地址,该地址在本地主机之外没有意义。 -
HOST_ANY:代表此传输提供者可访问的任何主机,相当于套接字接口中的INADDR_ANY值。 -
HOST_SELF_CONNECT:代表可用于连接到本地主机的主机地址。 -
HOST_BROADCAST:代表此传输提供者可到达的所有主机的地址,向该地址发送的网络请求将被发送到网络上的所有机器。
netdir_getbyname
函数会在
addrs
参数中返回主机和服务的所有有效地址列表。
addrs
是一个指向
struct nd_addrlist
类型数组的指针,该结构体定义如下:
struct nd_addrlist {
int n_cnt;
struct netbuf *n_addrs;
};
n_addrs
的每个元素包含一个地址,
n_cnt
元素指示有多少个地址。
netdir_getbyaddr
函数根据
netaddr
中给定的主机地址和端口号进行查找,并在
service
中返回主机和服务名的列表。
service
是一个指向
struct nd_hostservlist
类型数组的指针,该结构体定义如下:
struct nd_hostservlist {
int h_cnt;
struct nd_hostserv *h_hostservs;
};
netdir_getbyname
和
netdir_getbyaddr
函数在成功时返回零,失败时返回非零值。如果它们失败,可以使用
netdir_perror
和
netdir_sperror
函数来了解失败原因。
这些函数使用的内存可以通过调用
netdir_free
函数释放。第一个参数是指向内存的指针,第二个参数是一个常量,指示要释放的结构体类型,具体如下表所示:
| 常量 | 释放的结构体类型 |
| — | — |
|
ND_ADDR
|
struct netbuf
结构体 |
|
ND_ADDRLIST
|
struct nd_addrlist
结构体 |
|
ND_HOSTSERV
|
struct hostserv
结构体 |
|
ND_HOSTSERVLIST
|
struct nd_hostservlist
结构体 |
netdir_options
函数允许程序员在他选择的地址上设置或检查各种选项。
fd
参数是传输端点(稍后定义),
opt
参数指定选项,可能是以下值之一:
-
ND_SET_BROADCAST
:如果传输提供者支持广播,则将程序设置为发送广播数据包,
argp
参数将被忽略。
-
ND_SET_RESERVEDPORT
:如果传输提供者存在保留端口的概念,则允许调用者绑定一个保留端口。如果
argp
为
NULL
,将选择一个任意保留端口;如果
argp
指向一个
struct netbuf
结构体,将尝试绑定到它描述的保留端口。
下面是名称到地址转换的mermaid流程图:
graph TD;
A[开始] --> B{选择转换方式};
B -- 按名称转换 --> C[调用netdir_getbyname];
C --> D{是否成功};
D -- 是 --> E[获取地址列表];
D -- 否 --> F[使用netdir_perror或netdir_sperror获取错误信息];
B -- 按地址转换 --> G[调用netdir_getbyaddr];
G --> H{是否成功};
H -- 是 --> I[获取主机和服务名列表];
H -- 否 --> F;
E --> J[使用完地址列表后调用netdir_free释放内存];
I --> J;
6. 总结
TLI作为一种网络编程接口,旨在解决套接字接口协议相关性的问题,提供了协议无关的编程能力。通过使用
struct netbuf
结构处理不同协议的数据格式,利用
/etc/netconfig
文件和相关函数进行网络选择,以及提供名称到地址的转换功能,TLI为开发者提供了一种灵活且可移植的网络编程解决方案。
虽然TLI在实际应用中使用较少,但对于支持或维护System V系统的开发者来说,它仍然是一项重要的技术。此外,TLI的设计思想和实现方式也为我们理解网络编程中的协议无关性和可移植性提供了有益的参考。
在不同的操作系统中,TLI的实现可能会有所不同,如HP - UX 10.x在网络选择上采用编译时选择的方式,而SVR4则提供了更灵活的运行时选择机制。开发者在使用TLI时,需要根据具体的操作系统和需求选择合适的方法和函数。
通过深入学习TLI,开发者可以更好地掌握网络编程的核心概念和技术,提高程序的可移植性和灵活性,为开发高质量的网络应用程序打下坚实的基础。
超级会员免费看
7

被折叠的 条评论
为什么被折叠?



