1. 为什么还要聊TLI这个“老古董”
第一次在《UNIX网络编程》卷1里翻到TLI那一章的时候,我的反应跟大多数人一样:这玩意儿还有人用?书架上那本砖头厚的书,讲套接字的部分被翻得起了毛边,TLI那几章却干净得像刚印出来。后来接了一个维护老系统的活儿,代码里全是t_open、t_bind、t_connect这些调用,我才意识到——不是这东西没用,是我没碰到用它的场景。
TLI,全称Transport Layer Interface,是AT&T在System V Release 3里搞出来的一套传输层编程接口。它跟BSD套接字是同一层次的东西,都是让应用程序跟传输协议打交道,但设计哲学完全不同。套接字走的是“协议无关”路线,你把地址结构、协议族往里一塞,剩下的交给内核;TLI走的是“传输提供者”路线,你得先打开一个传输端点,把它绑到一个传输提供者上,然后通过一套标准的服务原语来通信。
说白了,套接字像去一家综合医院挂号,你说挂哪个科,医院给你安排;TLI像去一家专科诊所,你得先找到这家诊所,然后按它的流程走。两种方式都能看病,但流程和体验不一样。
这篇文章适合谁看?如果你正在维护一套跑在System V上的老系统,或者你在研究UNIX网络编程的历史演进,再或者你单纯想搞明白“为什么会有两套传输层接口”这个问题,那这篇内容应该能帮你省下不少翻文档的时间。我会从设计思路、核心数据结构、实操流程、常见坑这几个角度,把TLI这套东西拆开揉碎讲清楚。
提示:TLI在Linux上的实现叫XTI(X/Open Transport Interface),两者API几乎一样,只是头文件和库的引用方式略有差别。本文以TLI为主,XTI的差异会单独标注。
2. TLI的设计思路与核心概念拆解
2.1 传输提供者模型:TLI的世界观
要理解TLI,先得接受它的世界观:网络通信不是内核“内置”的功能,而是由一个个“传输提供者”提供的服务。传输提供者可以是一个内核模块,也可以是一个用户态进程,甚至可以是硬件。应用程序通过TLI跟传输提供者打交道,传输提供者负责实际的协议处理。
这个模型的好处是解耦。套接字把协议栈焊死在内核里,想加一个新协议得改内核;TLI把协议实现抽出来,只要符合TLI规范,谁都能当传输提供者。坏处也很明显——多了一层抽象,性能开销上去了,而且传输提供者的质量参差不齐,调试起来更麻烦。
TLI里最核心的概念是传输端点(transport endpoint)。你可以把它理解成套接字里的“套接字描述符”,但它比套接字描述符承载了更多信息。一个传输端点代表一个通信的端点,它绑定到一个传输提供者上,有自己的状态机,有自己的地址。
传输端点的状态机是TLI区别于套接字的一个重要特征。套接字的状态相对简单,TLI的端点状态有十几种,从T_UNBND(未绑定)到T_IDLE(空闲)到T_DATAXFER(数据传输中),每个状态允许的操作不一样。你如果在错误的状态下调用了某个函数,会直接返回错误,不会像套接字那样“凑合能用”。
2.2 服务原语:TLI的“动词”
TLI的操作是通过一组服务原语来完成的。这些原语的名字都以t_开头,看起来挺整齐,但用起来得记清楚每个原语在哪个状态下有效。
| 原语 | 作用 | 有效状态 | 对应套接字函数 |
|---|---|---|---|
t_open | 打开传输端点 | 任意 | socket |
t_bind | 绑定地址 | T_UNBND | bind |
t_connect | 建立连接 | T_IDLE | connect |
t_listen | 监听连接 | T_IDLE | listen |
t_accept | 接受连接 | T_LISTEN | accept |
t_snd | 发送数据 | T_DATAXFER | send |
t_rcv | 接收数据 | T_DATAXFER | recv |
t_snddis | 发送断开 | T_DATAXFER | shutdown |
t_close | 关闭端点 | 任意 | close |
这张表只是最常用的几个,实际TLI原语有二十多个。你会发现一个规律:TLI的原语命名更“动作化”,t_snd、t_rcv、t_snddis,一看就知道是干什么的。套接字的send、recv、shutdown其实也差不多,但TLI把“断开”分成了t_snddis(发送断开)和t_rcvdis(接收断开),粒度更细。
2.3 地址结构:struct t_bind和struct netbuf
TLI的地址表示跟套接字很不一样。套接字用sockaddr系列结构,TLI用struct netbuf。netbuf不直接存地址,它存的是指向地址缓冲区的指针和长度:
struct netbuf { unsigned int maxlen; unsigned int len; char *buf; };这个设计的好处是地址格式完全由传输提供者决定,TLI本身不关心地址长什么样。坏处是你得自己管理缓冲区,maxlen和len搞混了就会出问题。
绑定地址的时候用struct t_bind:
struct t_bind { struct netbuf addr; unsigned int qlen; };addr是地址,qlen是连接队列长度,只在监听的时候有意义。如果你不想指定具体地址,可以把addr.len设为0,传输提供者会给你分配一个。
注意:
netbuf里的buf指针必须指向有效的内存,而且maxlen要设成缓冲区实际大小。我见过有人把maxlen设成0,结果t_bind返回成功但地址根本没绑上,后面t_connect直接失败,排查了半天。
3. TLI实操:从打开端点到数据传输
3.1 打开传输端点:t_open的正确姿势
t_open是TLI编程的第一步,原型如下:
int t_open(const char *path, int oflag, struct t_info *info);path是传输提供者的设备文件路径,比如/dev/tcp、/dev/udp。这个路径因系统而异,得查系统文档。oflag跟open的flag类似,常用O_RDWR。info是个输出参数,返回传输提供者的能力信息:
struct t_info { long addr; // 地址最大长度 long options; // 选项缓冲区最大长度 long tsdu; // 传输服务数据单元最大长度 long etsdu; // 加速传输服务数据单元最大长度 long connect; // 连接建立时最大数据长度 long discon; // 断开时最大数据长度 long servtype; // 服务类型 };servtype有三个可能的值:T_COTS(面向连接)、T_COTS_ORD(面向连接且保证顺序)、T_CLTS(无连接)。这个信息很重要,决定了你后面能用哪些原语。比如T_CLTS就不能用t_connect和t_listen。
int fd; struct t_info info; fd = t_open("/dev/tcp", O_RDWR, &info); if (fd < 0) { t_error("t_open failed"); exit(1); } printf("servtype: %ld\n", info.servtype);t_error是TLI提供的错误打印函数,比perror更懂TLI的错误码。这个函数在调试的时候特别好用,建议每个TLI程序都带上。
3.2 绑定地址:t_bind的细节
打开端点之后,下一步是绑定地址。服务端和客户端的绑定策略不一样。
服务端通常要绑一个“众所周知”的地址,让客户端能找到。比如TCP服务端绑端口80:
struct t_bind bind_req; struct t_bind bind_ret; struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(80); sin.sin_addr.s_addr = htonl(INADDR_ANY); bind_req.addr.maxlen = sizeof(sin); bind_req.addr.len = sizeof(sin); bind_req.addr.buf = (char *)&sin; bind_req.qlen = 5; if (t_bind(fd, &bind_req, &bind_ret) < 0) { t_error("t_bind failed"); exit(1); }bind_ret返回实际绑定的地址。如果你在bind_req里把addr.len设为0,传输提供者会分配一个地址,通过bind_ret返回。客户端通常这么干,让系统自动分配端口。
qlen只在服务端监听时有意义,表示连接队列的最大长度。设太小了,并发连接一多就会拒绝新连接;设太大了,占内存。一般设5到10就够了,跟套接字的listenbacklog类似。
实操心得:
t_bind的bind_ret参数不能传NULL。有些实现允许传NULL,但System V的某些版本会直接段错误。我习惯总是传一个有效的struct t_bind,哪怕不关心返回值。
3.3 建立连接:t_connect与t_listen/t_accept
客户端用t_connect发起连接:
struct t_call call; struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(80); inet_pton(AF_INET, "192.168.1.1", &sin.sin_addr); call.addr.maxlen = sizeof(sin); call.addr.len = sizeof(sin); call.addr.buf = (char *)&sin; call.opt.maxlen = 0; call.opt.len = 0; call.opt.buf = NULL; call.udata.maxlen = 0; call.udata.len = 0; call.udata.buf = NULL; call.sequence = 0; if (t_connect(fd, &call, NULL) < 0) { t_error("t_connect failed"); exit(1); }struct t_call是TLI里表示连接请求的结构,包含地址、选项、用户数据三部分。sequence用于区分多个未完成的连接请求,同步模式下用0就行。
服务端用t_listen接收连接请求:
struct t_call call; call.addr.maxlen = sizeof(struct sockaddr_in); call.addr.buf = malloc(call.addr.maxlen); call.opt.maxlen = 0; call.opt.buf = NULL; call.udata.maxlen = 0; call.udata.buf = NULL; if (t_listen(fd, &call) < 0) { t_error("t_listen failed"); exit(1); }t_listen会阻塞直到有连接请求到达。收到请求后,call里包含了客户端的地址。然后你需要用t_accept接受连接:
int new_fd; new_fd = t_open("/dev/tcp", O_RDWR, NULL); if (new_fd < 0) { t_error("t_open for accept failed"); exit(1); } if (t_accept(fd, new_fd, &call) < 0) { t_error("t_accept failed"); exit(1); }这里有个关键点:t_accept的第一个参数是监听端点,第二个参数是新的端点,用于跟客户端通信。这个新端点必须先用t_open打开,但不能绑定地址。t_accept会把连接请求“转移”到新端点上。
注意:
t_accept之后,监听端点继续处于监听状态,可以接受更多连接。新端点进入T_DATAXFER状态,可以开始收发数据。这个流程跟套接字的accept很像,但多了一个显式打开新端点的步骤。
3.4 数据传输:t_snd和t_rcv
连接建立之后,就可以用t_snd和t_rcv收发数据了:
char sendbuf[] = "hello, tli"; if (t_snd(fd, sendbuf, strlen(sendbuf), 0) < 0) { t_error("t_snd failed"); exit(1); } char recvbuf[1024]; int n; n = t_rcv(fd, recvbuf, sizeof(recvbuf), 0); if (n < 0) { t_error("t_rcv failed"); exit(1); } printf("received %d bytes: %.*s\n", n, n, recvbuf);t_snd和t_rcv的flags参数跟套接字的send/recv类似,但TLI的flags定义不一样。常用的有T_MORE(表示还有后续数据)、T_EXPEDITED(加速数据)、T_PUSH(立即发送)。T_MORE这个flag特别重要,它跟TLI的消息边界概念有关。
TLI是面向消息的,不是面向字节流的。t_snd一次调用发送的数据构成一个消息,接收端t_rcv一次调用会尽量返回一个完整消息。如果消息太大,接收缓冲区装不下,t_rcv会返回部分数据并设置T_MORE标志,告诉你还有后续数据。这个行为跟TCP的字节流语义不一样,用惯了套接字的人容易在这里踩坑。
实操心得:如果你用TLI写TCP程序,想模拟字节流语义,可以在每次
t_snd时都带上T_MORE标志,接收端循环t_rcv直到没有T_MORE。但这样效率不高,更好的做法是直接按消息边界处理,把每次t_snd的数据当成一个完整消息。
4. 常见问题与排查技巧实录
4.1 状态机错误:TLI最让人头疼的地方
TLI的状态机比套接字严格得多,在错误的状态下调函数会直接返回TSTATECHK错误。我整理了一个常见状态错误速查表:
| 错误码 | 含义 | 常见原因 | 解决方法 |
|---|---|---|---|
TSTATECHK | 状态检查失败 | 在错误状态调用了原语 | 检查端点当前状态,确认原语是否有效 |
TBADADDR | 地址格式错误 | netbuf的len或maxlen不对 | 检查地址缓冲区大小和实际长度 |
TBADOPT | 选项格式错误 | 选项缓冲区设置不当 | 不用的选项把len和maxlen设为0 |
TACCES | 权限不足 | 绑定了特权端口但无权限 | 换端口或用root运行 |
TNOADDR | 地址不可用 | 绑定的地址已被占用 | 换地址或设addr.len=0让系统分配 |
TOUTSTATE | 状态超出范围 | 端点状态与操作不匹配 | 用t_getstate查看当前状态 |
t_getstate是排查状态问题的利器,返回端点的当前状态。我习惯在关键操作前后都打印一下状态,虽然有点啰嗦,但能省下大量调试时间。
int state = t_getstate(fd); printf("current state: %d\n", state);状态值对应的宏定义在<tiuser.h>里,T_UNBND是1,T_IDLE是2,T_DATAXFER是6,等等。打印出来是数字,得对着头文件看。
4.2 阻塞与非阻塞:t_open的O_NONBLOCK
TLI默认是阻塞模式,t_listen、t_connect、t_rcv都会阻塞。如果你想用非阻塞模式,可以在t_open时加O_NONBLOCK标志:
fd = t_open("/dev/tcp", O_RDWR | O_NONBLOCK, &info);非阻塞模式下,没有数据可读时t_rcv返回-1并设置errno为EAGAIN。t_connect在非阻塞模式下会立即返回,连接完成后再通过t_rcv或t_snd通知你。这个行为跟套接字的非阻塞connect类似,但TLI没有select那样的多路复用机制,得用poll或者t_look。
t_look是TLI特有的函数,返回端点上当前挂起的事件类型:
int event = t_look(fd); if (event == T_DATA) { // 有数据可读 } else if (event == T_DISCONNECT) { // 有断开请求 }t_look不会阻塞,适合在轮询循环里用。但它只能告诉你“有事件”,不能告诉你“有几个字节可读”,所以还是得配合t_rcv。
4.3 断开连接的两种方式
TLI断开连接有两种方式:优雅断开和强制断开。
优雅断开用t_snddis:
t_snddis(fd, NULL);这会发送一个断开请求给对端,对端t_rcv会返回0或者T_DISCONNECT事件。t_snddis的第二个参数可以带用户数据,但大多数传输提供者不支持,传NULL就行。
强制断开用t_close:
t_close(fd);t_close直接关闭端点,不发送断开请求。对端可能收不到任何通知,直到下一次t_snd或t_rcv才发现连接断了。这个行为跟套接字的close类似,但TLI的t_close更“粗暴”一些。
注意:
t_snddis之后端点进入T_IDLE状态,可以重新t_connect或t_listen。t_close之后端点就没了,得重新t_open。如果你在循环里反复建立连接,用t_snddis比t_close效率高。
4.4 内存管理:netbuf的坑
netbuf里的buf指针需要你自己分配和释放。TLI不会帮你管理内存,忘了释放就是内存泄漏,释放早了就是野指针。
我踩过的一个坑:在t_listen之后,call.addr.buf指向的是TLI内部缓冲区,你不能直接释放它,也不能在下次t_listen之后继续用。正确的做法是每次t_listen之前重新分配call.addr.buf,或者用t_alloc分配:
call.addr.buf = t_alloc(fd, T_ADDR, T_ALL);t_alloc是TLI提供的内存分配函数,会根据传输提供者的能力分配合适大小的缓冲区。用t_alloc分配的内存要用t_free释放:
t_free(call.addr.buf, T_ADDR);t_alloc的第二个参数指定分配类型:T_ADDR(地址)、T_OPT(选项)、T_UDATA(用户数据)、T_ALL(全部)。第三个参数指定是用于发送还是接收:T_ALL表示两者都适用。
实操心得:
t_alloc分配的内存块大小可能比你实际需要的大,但不要自己调整maxlen。TLI内部会检查maxlen是否跟分配时一致,不一致会返回TBADADDR。老老实实用t_alloc返回的maxlen就行。
5. TLI与套接字的对比:什么时候用哪个
5.1 功能对比
| 特性 | TLI | 套接字 |
|---|---|---|
| 设计哲学 | 传输提供者模型 | 协议无关模型 |
| 地址表示 | netbuf(指针+长度) | sockaddr(固定结构) |
| 状态机 | 严格,十几种状态 | 宽松,状态较少 |
| 消息边界 | 保留消息边界 | 字节流(TCP) |
| 多路复用 | t_look+poll | select/poll/epoll |
| 非阻塞 | O_NONBLOCK | O_NONBLOCK |
| 内存管理 | 手动,t_alloc/t_free | 内核管理 |
| 可移植性 | System V系 | BSD系,更广泛 |
从功能上看,套接字在大多数场景下更方便。select/epoll的多路复用机制成熟稳定,地址结构简单直接,内存管理由内核负责。TLI的优势在于它的传输提供者模型——如果你需要实现自定义协议,或者需要跟非IP协议打交道,TLI的抽象层能帮你省不少事。
5.2 性能对比
性能上,TLI和套接字没有本质差距。两者最终都是通过系统调用跟内核通信,开销主要在内核态和用户态的切换上。TLI多了一层传输提供者的抽象,理论上会多一点点开销,但实际测试中差异在误差范围内。
真正影响性能的是使用方式。TLI的消息边界语义意味着你不能像TCP那样随便发随便收,得按消息来。如果你用TLI模拟字节流,每次t_snd都带T_MORE,接收端循环t_rcv,性能会比直接用套接字差一些。
5.3 选型建议
如果你在维护老系统,代码里已经用了TLI,那就继续用TLI,别折腾着改成套接字。改造成本高,收益低,还容易引入新bug。
如果你在写新代码,除非有特殊需求(比如要跟某个只提供TLI接口的传输提供者打交道),否则直接用套接字。套接字的生态更成熟,文档更丰富,遇到问题更容易找到答案。
如果你在学习UNIX网络编程的历史,TLI值得了解。它代表了另一种设计思路,理解了TLI能帮你更深刻地理解套接字的设计取舍。但了解归了解,实际项目里还是套接字更实用。
提示:Linux上的XTI跟TLI几乎一样,但头文件是
<xti.h>而不是<tiuser.h>,库是-lxti而不是-lnsl。如果你在Linux上编译TLI代码,把#include <tiuser.h>改成#include <xti.h>,链接时加-lxti就行。
6. 一个完整的TLI回显服务端示例
6.1 代码结构
下面是一个完整的TLI回显服务端,监听TCP端口,收到数据后原样返回。这个示例涵盖了t_open、t_bind、t_listen、t_accept、t_rcv、t_snd、t_snddis、t_close的完整流程。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <tiuser.h> #include <netinet/in.h> #include <arpa/inet.h> #define BUFSIZE 1024 int main(int argc, char *argv[]) { int listen_fd, conn_fd; struct t_info info; struct t_bind bind_req, bind_ret; struct t_call call; struct sockaddr_in sin; char buf[BUFSIZE]; int n; // 1. 打开传输端点 listen_fd = t_open("/dev/tcp", O_RDWR, &info); if (listen_fd < 0) { t_error("t_open failed"); exit(1); } // 2. 绑定地址 memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(8888); sin.sin_addr.s_addr = htonl(INADDR_ANY); bind_req.addr.maxlen = sizeof(sin); bind_req.addr.len = sizeof(sin); bind_req.addr.buf = (char *)&sin; bind_req.qlen = 5; if (t_bind(listen_fd, &bind_req, &bind_ret) < 0) { t_error("t_bind failed"); exit(1); } printf("listening on port 8888\n"); // 3. 主循环 while (1) { // 分配地址缓冲区 call.addr.maxlen = sizeof(struct sockaddr_in); call.addr.buf = malloc(call.addr.maxlen); call.opt.maxlen = 0; call.opt.buf = NULL; call.udata.maxlen = 0; call.udata.buf = NULL; // 4. 监听连接 if (t_listen(listen_fd, &call) < 0) { t_error("t_listen failed"); free(call.addr.buf); continue; } // 5. 打开新端点用于通信 conn_fd = t_open("/dev/tcp", O_RDWR, NULL); if (conn_fd < 0) { t_error("t_open for accept failed"); free(call.addr.buf); continue; } // 6. 接受连接 if (t_accept(listen_fd, conn_fd, &call) < 0) { t_error("t_accept failed"); t_close(conn_fd); free(call.addr.buf); continue; } free(call.addr.buf); printf("connection accepted\n"); // 7. 回显循环 while (1) { n = t_rcv(conn_fd, buf, sizeof(buf), 0); if (n < 0) { t_error("t_rcv failed"); break; } if (n == 0) { printf("client disconnected\n"); break; } if (t_snd(conn_fd, buf, n, 0) < 0) { t_error("t_snd failed"); break; } } // 8. 关闭连接端点 t_snddis(conn_fd, NULL); t_close(conn_fd); } // 9. 关闭监听端点 t_close(listen_fd); return 0; }6.2 编译与运行
编译这个程序需要链接TLI库。在System V系统上:
cc -o echo_server echo_server.c -lnsl在Linux上(用XTI):
cc -o echo_server echo_server.c -lxti运行:
./echo_server然后用telnet或者nc连接测试:
nc localhost 8888输入什么就返回什么,说明回显服务正常。
6.3 代码中的关键点
这个示例里有几个地方值得单独说。
第一,call.addr.buf的分配和释放。每次t_listen之前都重新malloc,t_accept之后free。如果你在循环外分配一次,t_listen可能会覆盖里面的内容,导致地址信息错乱。
第二,conn_fd的打开时机。必须在t_accept之前打开,而且不能绑定地址。t_accept会把连接请求“转移”到这个新端点上,如果新端点已经绑定了地址,t_accept会返回TBADADDR。
第三,t_snddis和t_close的配合。先t_snddis发送断开请求,再t_close关闭端点。如果直接t_close,客户端可能收不到断开通知,会一直等在那里。
第四,错误处理。每个TLI调用都检查返回值,出错就打印错误信息。t_error会输出详细的错误描述,比perror有用得多。生产环境里你可能需要更精细的错误处理,比如重试、记录日志、通知监控系统,但作为示例,这样已经够了。
实操心得:这个示例是单线程的,一次只能处理一个连接。如果你需要并发处理多个连接,可以用
fork或者多线程。但TLI的端点不能跨进程共享,fork之后子进程得自己t_open新端点。多线程的话,每个线程管理自己的端点,别共享。
7. 调试TLI程序的几个实用技巧
7.1 用t_getstate跟踪状态变化
TLI的状态机是调试的最大障碍。我习惯在关键操作前后都调用t_getstate,把状态打印出来。虽然输出有点多,但能清楚地看到端点状态的变化过程,定位问题特别快。
#define PRINT_STATE(fd, msg) \ printf("[%s] state=%d\n", msg, t_getstate(fd)) PRINT_STATE(fd, "before t_connect"); t_connect(fd, &call, NULL); PRINT_STATE(fd, "after t_connect");状态值对应的宏定义在<tiuser.h>里,你可以写个函数把状态值转成字符串:
const char *state_name(int state) { switch (state) { case T_UNBND: return "T_UNBND"; case T_IDLE: return "T_IDLE"; case T_OUTCON: return "T_OUTCON"; case T_INCON: return "T_INCON"; case T_DATAXFER: return "T_DATAXFER"; case T_OUTREL: return "T_OUTREL"; case T_INREL: return "T_INREL"; default: return "UNKNOWN"; } }7.2 用t_look做非阻塞轮询
t_look返回端点上当前挂起的事件,不会阻塞。在非阻塞模式下,你可以用t_look配合poll实现多路复用:
struct pollfd pfd; pfd.fd = fd; pfd.events = POLLIN; if (poll(&pfd, 1, -1) > 0) { int event = t_look(fd); if (event == T_DATA) { n = t_rcv(fd, buf, sizeof(buf), 0); // 处理数据 } else if (event == T_DISCONNECT) { // 处理断开 } }t_look的返回值有T_DATA、T_DISCONNECT、T_ORDREL、T_CONNECT等。T_ORDREL表示对端优雅关闭,T_DISCONNECT表示对端强制关闭。这两个事件的处理方式不一样,T_ORDREL可以继续发送剩余数据,T_DISCONNECT则不能。
7.3 用strace跟踪系统调用
TLI的函数最终都会变成系统调用。用strace跟踪一下,能看到底层发生了什么:
strace -e trace=network ./echo_server你会看到t_open变成了open,t_bind变成了bind,t_connect变成了connect。这个技巧在排查“为什么TLI调用失败了”的时候特别有用,因为你能看到内核返回的错误码,比TLI的错误码更底层。
注意:不同系统的TLI实现不一样,
strace的输出也会有差异。System V的TLI可能走的是ioctl而不是标准的socket系统调用。别被输出吓到,抓住关键的错误码就行。
8. 写在最后
TLI这套东西,说它过时吧,确实过时了,新项目基本没人用。但说它没用吧,也不对,维护老系统的时候绕不开,而且它的一些设计思路——比如传输提供者模型、严格的状态机、消息边界语义——在今天的某些框架里还能看到影子。
我个人的体会是,学TLI最大的收获不是会用它的API,而是理解了“传输层接口”这件事本身可以有多种设计方式。套接字不是唯一的选择,也不是天然正确的选择。TLI的很多设计决策,放在它诞生的那个年代,是有道理的。只是后来BSD套接字赢了,不是因为套接字更好,而是因为BSD赢了。
如果你正在跟TLI打交道,希望这篇内容能帮你少踩几个坑。如果你只是好奇,那当个故事看也挺好。技术这东西,多了解一种思路,总没坏处。