基于 TLI 的网络编程详解
1. 面向连接的服务概述
面向连接的服务相较于无连接服务更为复杂,不过它并不比套接字接口复杂太多。在进行网络编程时,了解面向连接服务的工作原理至关重要。
2. 服务器端功能
要成为服务器,进程需要告知操作系统它希望接收连接,然后处理传入的连接请求。具体步骤如下:
-
等待连接
:与套接字接口不同,在 TLI 中,服务器通过循环调用
t_listen
函数来等待连接请求。
#include <tiuser.h>
int t_listen(int fd, struct t_call *call);
该函数会阻塞,直到由
fd
引用的传输端点收到连接请求。连接请求到达时,请求的描述信息会被放置在
call
中。
struct t_call
结构体定义如下:
struct t_call {
struct netbuf addr;
struct netbuf opt;
struct netbuf udata;
int sequence;
};
在调用
t_listen
之前,必须设置
addr
、
opt
和
udata
的
maxlen
字段。调用成功时,
t_listen
返回 0;失败时返回 -1,并将错误信息存储在
t_errno
(可能还有
errno
)中。
-
接受和拒绝连接
:通过
t_listen
接收到连接请求后,服务器可以选择接受或拒绝该请求。
-
接受请求
:调用
t_accept
函数。
#include <tiuser.h>
int t_accept(int fd, int resfd, struct t_call *call);
fd
参数指的是传输端点,
call
参数应指向
t_listen
返回的
struct t_call
结构体。如果
resfd
等于
fd
,则在请求到达的同一传输端点上接受连接;如果
resfd
不等于
fd
,则使用另一个绑定的端点接受连接。调用成功时返回 0,失败时返回 -1。
-
拒绝请求
:使用
t_snddis
函数。
#include <tiuser.h>
int t_snddis(int fd, struct t_call *call);
同样,
fd
是传输端点,
call
指向
t_listen
返回的结构体。调用成功返回 0,失败返回 -1。
3. 客户端功能
在传输数据之前,客户端程序必须连接到服务器,使用
t_connect
函数实现连接:
#include <tiuser.h>
int t_connect(int fd, struct t_call *sndcall,
struct t_call *rcvcall);
fd
指的是绑定的传输端点,
sndcall
和
rcvcall
指向
t_call
结构体。在
sndcall
中,
addr
是要连接的服务器地址,
opt
包含特定协议选项,
udata
可能包含随连接请求一起传输的数据。在
rcvcall
中,调用前必须设置
struct netbuf
结构体的
maxlen
字段。如果连接请求被服务器拒绝,
t_connect
会失败,
t_errno
会被设置为
TLOOK
,此时客户端应调用
t_rcvdis
函数:
#include <tiuser.h>
int t_rcvdis(int fd, struct t_discon *discon);
discon
指向
struct t_discon
结构体,该结构体包含拒绝原因。
4. 数据传输
连接建立后,客户端和服务器可以使用
t_snd
和
t_rcv
函数交换数据:
#include <tiuser.h>
int t_snd(int fd, char *buf, unsigned nbytes, int flags);
int t_rcv(int fd, char *buf, unsigned nbytes, int *flags);
在
t_snd
中,
buf
是要传输的数据,
nbytes
是要传输的字节数,
flags
参数指定发送选项:
-
T_EXPEDITED
:将数据作为加急(带外)数据发送。
-
T_MORE
:表示当前 TSDU 分多次
t_snd
调用发送。
在
t_rcv
中,
buf
是存储接收数据的缓冲区,
nbytes
指定缓冲区大小,
flags
指向一个标志字,会被修改以包含
t_snd
调用的标志。调用成功时,
t_snd
和
t_rcv
返回发送或接收的字节数;失败时返回 -1,并将错误信息存储在
t_errno
(可能还有
errno
)中。
5. 连接释放
如果连接支持有序释放,服务器和客户端必须协商有序释放连接,使用
t_sndrel
和
t_rcvrel
函数:
#include <tiuser.h>
int t_sndrel(int fd);
int t_rcvrel(int fd);
当客户端或服务器没有更多数据要发送时,应调用
t_sndrel
;收到通知后,应调用
t_rcvrel
确认接收。为了完全关闭双向连接,双方最终都应调用这两个函数。调用成功返回 0,失败返回 -1。
6. 示例代码
以下是使用 TLI 实现的客户端和服务器程序示例:
-
服务器示例(Example 15 - 3)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netconfig.h>
#include <tiuser.h>
#include <netdir.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#define PORTNUMBER 12345
extern int t_errno;
int
main(void)
{
int n, fd, flags;
struct t_call *callp;
struct netconfig *ncp;
struct nd_hostserv ndh;
struct nd_addrlist *nal;
struct t_bind *reqp, *retp;
char buf[1024], hostname[64];
/*
* Get our local host name.
*/
if (gethostname(hostname, sizeof(hostname)) < 0) {
perror("gethostname");
exit(1);
}
/*
* Select the TCP transport provider.
*/
if ((ncp = getnetconfigent("tcp")) == NULL) {
nc_perror("tcp");
exit(1);
}
/*
* Get a host and service address for our host. Since our
* port number is not registered in the services file, we
* send down the ASCII string representation of it.
*/
sprintf(buf, "%d", PORTNUMBER);
ndh.h_host = hostname;
ndh.h_serv = buf;
if (netdir_getbyname(ncp, &ndh, &nal) != 0) {
netdir_perror(hostname);
exit(1);
}
/*
* Create a transport endpoint.
*/
if ((fd = t_open(ncp->nc_device, O_RDWR, NULL)) < 0) {
t_error("t_open");
exit(1);
}
/*
* Bind the address to the transport endpoint.
*/
retp = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR);
reqp = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR);
if (reqp == NULL || retp == NULL) {
t_error("t_alloc");
exit(1);
}
memcpy(&reqp->addr, &nal->n_addrs[0], sizeof(struct netbuf));
reqp->qlen = 5;
if (t_bind(fd, reqp, retp) < 0) {
t_error("t_bind");
exit(1);
}
if (retp->addr.len != nal->n_addrs[0].len ||
memcmp(retp->addr.buf, nal->n_addrs[0].buf, retp->addr.len) != 0) {
fprintf(stderr, "did not bind requested address.\n");
exit(1);
}
/*
* Allocate a call structure.
*/
callp = (struct t_call *) t_alloc(fd, T_CALL, T_ALL);
if (callp == NULL) {
t_error("t_alloc");
exit(1);
}
/*
* Listen for a connection.
*/
if (t_listen(fd, callp) < 0) {
t_error("t_listen");
exit(1);
}
/*
* Accept a connect on the same file descriptor used for listeing.
*/
if (t_accept(fd, fd, callp) < 0) {
t_error("t_accept");
exit(1);
}
/*
* Read from the network until end-of-file and
* print what we get on the standard output.
*/
while ((n = t_rcv(fd, buf, sizeof(buf), &flags)) > 0)
write(1, buf, n);
/*
* Release the connection.
*/
t_rcvrel(fd);
t_sndrel(fd);
t_unbind(fd);
t_close(fd);
exit(0);
}
- 客户端示例(Example 15 - 4)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netconfig.h>
#include <tiuser.h>
#include <netdir.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#define PORTNUMBER 12345
extern int t_errno;
int
main(void)
{
int n, fd;
struct t_call *callp;
struct netconfig *ncp;
struct nd_hostserv ndh;
struct nd_addrlist *nal;
char buf[32], hostname[64];
/*
* Get our local host name.
*/
if (gethostname(hostname, sizeof(hostname)) < 0) {
perror("gethostname");
exit(1);
}
/*
* Select the TCP transport provider.
*/
if ((ncp = getnetconfigent("tcp")) == NULL) {
nc_perror("tcp");
exit(1);
}
/*
* Get a host and service address for our host. Since our
* port number is not registered in the services file, we
* send down the ASCII string representation of it.
*/
sprintf(buf, "%d", PORTNUMBER);
ndh.h_host = hostname;
ndh.h_serv = buf;
if (netdir_getbyname(ncp, &ndh, &nal) != 0) {
netdir_perror(hostname);
exit(1);
}
/*
* Create a transport endpoint.
*/
if ((fd = t_open(ncp->nc_device, O_RDWR, NULL)) < 0) {
t_error("t_open");
exit(1);
}
/*
* Bind an arbitrary address to the transport
* endpoint.
*/
if (t_bind(fd, NULL, NULL) < 0) {
t_error("t_bind");
exit(1);
}
/*
* Allocate a connection structure.
*/
callp = (struct t_call *) t_alloc(fd, T_CALL, 0);
if (callp == NULL) {
t_error("t_alloc");
exit(1);
}
/*
* Construct the connection request.
*/
memcpy(&callp->addr, &nal->n_addrs[0], sizeof(struct netbuf));
/*
* Connect to the server.
*/
if (t_connect(fd, callp, NULL) < 0) {
if (t_errno == TLOOK) {
if (t_rcvdis(fd, NULL) < 0) {
t_error("t_rcvdis");
exit(1);
}
}
else {
t_error("t_connect");
exit(1);
}
}
/*
* Read from standard input, and copy the
* data to the network.
*/
while ((n = read(0, buf, sizeof(buf))) > 0) {
if (t_snd(fd, buf, n, 0) < 0) {
t_error("t_snd");
exit(1);
}
}
/*
* Release the connection.
*/
t_sndrel(fd);
t_rcvrel(fd);
t_unbind(fd);
t_close(fd);
exit(0);
}
7. HP - UX 10.x 实现差异
在 HP - UX 10.x 中实现相同功能的程序有以下主要差异:
|差异点|描述|
| ---- | ---- |
|地址获取|使用
gethostbyname
函数获取主机地址,端口号已知,而不是使用
netdir_getbyname
。|
|设备名称|直接编译设备名称
/dev/inet_cots
,而不是使用
getnetconfigent
获取合适的网络设备名称。|
|地址结构|使用
struct sockaddr_in
结构处理网络地址,而不是独立于传输的
struct nd_addrlist
结构。|
以下是 HP - UX 10.x 中的服务器和客户端示例代码:
-
服务器示例(Example 15 - 5)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <tiuser.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#define PORTNUMBER 12345
extern int t_errno;
int
main(void)
{
int n, fd, flags;
struct t_call *callp;
struct t_bind *reqp, *retp;
struct sockaddr_in loc_addr;
char buf[1024], hostname[64];
/*
* Get our local host name.
*/
if (gethostname(hostname, sizeof(hostname)) < 0) {
perror("gethostname");
exit(1);
}
/*
* Create a host and service address for our host.
*/
memset((char *) &loc_addr, 0, sizeof(struct sockaddr_in));
loc_addr.sin_addr.s_addr = htonl(INADDR_ANY);
loc_addr.sin_port = htons(PORTNUMBER);
loc_addr.sin_family = AF_INET;
/*
* Create a transport endpoint.
*/
if ((fd = t_open("/dev/inet_cots", O_RDWR, NULL)) < 0) {
t_error("t_open");
exit(1);
}
/*
* Bind the address to the transport endpoint.
*/
retp = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR);
reqp = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR);
if (reqp == NULL || retp == NULL) {
t_error("t_alloc");
exit(1);
}
reqp->addr.maxlen = sizeof(struct sockaddr_in);
reqp->addr.len = sizeof(struct sockaddr_in);
reqp->addr.buf = (char *) &loc_addr;
reqp->qlen = 5;
if (t_bind(fd, reqp, retp) < 0) {
t_error("t_bind");
exit(1);
}
if (retp->addr.len != reqp->addr.len ||
memcmp(retp->addr.buf, reqp->addr.buf, retp->addr.len) != 0) {
fprintf(stderr, "did not bind requested address.\n");
exit(1);
}
/*
* Allocate a call structure.
*/
callp = (struct t_call *) t_alloc(fd, T_CALL, T_ALL);
if (callp == NULL) {
t_error("t_alloc");
exit(1);
}
/*
* Listen for a connection.
*/
if (t_listen(fd, callp) < 0) {
t_error("t_listen");
exit(1);
}
/*
* Accept a connect on the same file descriptor used for listeing.
*/
if (t_accept(fd, fd, callp) < 0) {
t_error("t_accept");
exit(1);
}
/*
* Read from the network until end-of-file and
* print what we get on the standard output.
*/
while ((n = t_rcv(fd, buf, sizeof(buf), &flags)) > 0)
write(1, buf, n);
/*
* Release the connection.
*/
t_rcvrel(fd);
t_sndrel(fd);
t_unbind(fd);
t_close(fd);
exit(0);
}
- 客户端示例(Example 15 - 6)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <tiuser.h>
#include <string.h>
#include <netdb.h>
#include <fcntl.h>
#include <stdio.h>
#define PORTNUMBER 12345
extern int t_errno;
int
main(void)
{
int n, fd;
struct hostent *hp;
struct t_call *callp;
char buf[32], hostname[64];
struct sockaddr_in rem_addr;
/*
* Get our local host name.
*/
if (gethostname(hostname, sizeof(hostname)) < 0) {
perror("gethostname");
exit(1);
}
/*
* Get the address of our host.
*/
if ((hp = gethostbyname(hostname)) == NULL) {
fprintf(stderr, "Cannot find address for %s\n", hostname);
exit(1);
}
/*
* Create a host and service address for our host.
*/
memset((char *) &rem_addr, 0, sizeof(struct sockaddr_in));
memcpy((char *) &rem_addr.sin_addr.s_addr, (char *) hp->h_addr,
hp->h_length);
rem_addr.sin_port = htons(PORTNUMBER);
rem_addr.sin_family = AF_INET;
/*
* Create a transport endpoint.
*/
if ((fd = t_open("/dev/inet_cots", O_RDWR, NULL)) < 0) {
t_error("t_open");
exit(1);
}
/*
* Bind an arbitrary address to the transport
* endpoint.
*/
if (t_bind(fd, NULL, NULL) < 0) {
t_error("t_bind");
exit(1);
}
/*
* Allocate a connection structure.
*/
callp = (struct t_call *) t_alloc(fd, T_CALL, T_ADDR);
if (callp == NULL) {
t_error("t_alloc");
exit(1);
}
/*
* Construct the connection request.
*/
callp->addr.maxlen = sizeof(struct sockaddr_in);
callp->addr.len = sizeof(struct sockaddr_in);
callp->addr.buf = (char *) &rem_addr;
callp->udata.len = 0;
callp->opt.len = 0;
/*
* Connect to the server.
*/
if (t_connect(fd, callp, NULL) < 0) {
if (t_errno == TLOOK) {
if (t_rcvdis(fd, NULL) < 0) {
t_error("t_rcvdis");
exit(1);
}
}
else {
t_error("t_connect");
exit(1);
}
}
/*
* Read from standard input, and copy the
* data to the network.
*/
while ((n = read(0, buf, sizeof(buf))) > 0) {
if (t_snd(fd, buf, n, 0) < 0) {
t_error("t_snd");
exit(1);
}
}
/*
* Release the connection.
*/
t_sndrel(fd);
t_rcvrel(fd);
t_unbind(fd);
t_close(fd);
exit(0);
}
8. 其他函数
TLI 还提供了一些其他可能有用的函数:
-
传输端点名称
:使用
t_getname
函数获取连接本地或远程端绑定的地址。
#include <tiuser.h>
int t_getname(int fd, struct netbuf *namep, int type);
type
参数可以取
LOCALNAME
或
REMOTENAME
,分别返回本地或远程传输端点绑定的地址。调用成功返回 0,失败返回 -1。
-
连接状态
:使用
t_getstate
函数获取传输端点的当前状态。
#include <tiuser.h>
int t_getstate(int fd);
该函数失败时返回 -1,成功时返回描述端点状态的常量:
|状态常量|描述|
| ---- | ---- |
|
T_UNBND
|传输端点未绑定地址。|
|
T_IDLE
|传输端点已绑定地址,但未连接任何内容。|
|
T_OUTCON
|端点上有一个传出连接请求待处理。|
|
T_INCON
|端点上有一个传入连接请求待处理。|
|
T_DATAXFER
|端点当前正在传输数据。|
|
T_OUTREL
|端点上已发送有序释放请求。|
|
T_INREL
|端点上已收到有序释放请求。|
由于调用
exec
后库状态会丢失,导致无法使用
t_getstate
函数,可以调用
t_sync
函数恢复库状态:
#include <tiuser.h>
int t_sync(int fd);
-
异步事件
:通信通道上可能发生一些异步事件,导致 TLI 函数返回错误。当函数返回错误时,应检查
t_errno。如果其值为TLOOK,则应调用t_look函数:
#include <tiuser.h>
int t_look(int fd);
综上所述,通过使用 TLI 提供的这些函数和机制,我们可以实现可靠的面向连接的网络编程,同时根据不同的操作系统环境进行适当的调整。无论是服务器端还是客户端的开发,都需要仔细处理每个步骤,确保网络连接的稳定和数据传输的准确。
基于 TLI 的网络编程详解(续)
9. 函数调用流程总结
为了更清晰地理解基于 TLI 的网络编程过程,下面以服务器和客户端的交互为例,总结函数调用的流程。
服务器端流程
:
1. 获取本地主机名。
2. 选择合适的传输协议(如 TCP)。
3. 获取主机和服务地址。
4. 创建传输端点。
5. 将地址绑定到传输端点。
6. 分配调用结构。
7. 循环调用
t_listen
等待连接请求。
8. 收到连接请求后,调用
t_accept
接受连接。
9. 使用
t_rcv
接收数据并处理。
10. 调用
t_rcvrel
和
t_sndrel
有序释放连接。
11. 解除绑定并关闭传输端点。
客户端流程
:
1. 获取本地主机名。
2. 选择合适的传输协议(如 TCP)。
3. 获取主机和服务地址。
4. 创建传输端点。
5. 绑定任意地址到传输端点。
6. 分配连接结构。
7. 构造连接请求。
8. 调用
t_connect
连接到服务器。
9. 如果连接被拒绝,调用
t_rcvdis
处理拒绝信息。
10. 使用
t_snd
发送数据。
11. 调用
t_sndrel
和
t_rcvrel
有序释放连接。
12. 解除绑定并关闭传输端点。
下面是这个流程的 mermaid 流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(服务器端流程):::process
A --> C(客户端流程):::process
B --> B1(获取本地主机名):::process
B1 --> B2(选择传输协议):::process
B2 --> B3(获取主机和服务地址):::process
B3 --> B4(创建传输端点):::process
B4 --> B5(绑定地址到端点):::process
B5 --> B6(分配调用结构):::process
B6 --> B7(循环 t_listen 等待连接):::process
B7 --> B8{收到连接请求?}:::decision
B8 -- 是 --> B9(t_accept 接受连接):::process
B9 --> B10(t_rcv 接收数据):::process
B10 --> B11(t_rcvrel 和 t_sndrel 释放连接):::process
B11 --> B12(解除绑定并关闭端点):::process
B8 -- 否 --> B7
C --> C1(获取本地主机名):::process
C1 --> C2(选择传输协议):::process
C2 --> C3(获取主机和服务地址):::process
C3 --> C4(创建传输端点):::process
C4 --> C5(绑定任意地址到端点):::process
C5 --> C6(分配连接结构):::process
C6 --> C7(构造连接请求):::process
C7 --> C8(t_connect 连接服务器):::process
C8 --> C9{连接成功?}:::decision
C9 -- 是 --> C10(t_snd 发送数据):::process
C10 --> C11(t_sndrel 和 t_rcvrel 释放连接):::process
C11 --> C12(解除绑定并关闭端点):::process
C9 -- 否 --> C13(t_rcvdis 处理拒绝信息):::process
C13 --> C8
B12 --> D([结束]):::startend
C12 --> D
10. 错误处理和调试建议
在基于 TLI 的网络编程中,错误处理至关重要。每个 TLI 函数在失败时都会设置
t_errno
(可能还有
errno
),我们可以根据这些错误码来定位问题。以下是一些常见错误及处理建议:
| 错误码 | 可能原因 | 处理建议 |
|---|---|---|
t_open
失败
| 传输设备不可用、权限不足等 | 检查设备路径是否正确,确保有足够的权限访问设备。 |
t_bind
失败
| 地址已被占用、地址格式错误等 | 检查地址是否已被其他进程使用,确保地址格式正确。 |
t_listen
失败
| 传输端点状态错误等 |
使用
t_getstate
检查传输端点的状态,确保其处于正确状态。
|
t_connect
失败
| 服务器未运行、地址错误、连接被拒绝等 |
检查服务器是否正常运行,确认地址和端口号是否正确。如果
t_errno
为
TLOOK
,调用
t_rcvdis
处理拒绝信息。
|
t_snd
或
t_rcv
失败
| 连接中断、缓冲区溢出等 | 检查连接是否仍然有效,确保缓冲区大小足够。 |
在调试过程中,我们可以在关键函数调用前后添加日志输出,记录函数的返回值和错误码,以便更好地跟踪程序的执行过程。例如:
if ((fd = t_open(ncp->nc_device, O_RDWR, NULL)) < 0) {
t_error("t_open");
fprintf(stderr, "t_open failed with t_errno: %d\n", t_errno);
exit(1);
}
11. 性能优化考虑
在进行基于 TLI 的网络编程时,性能优化也是一个重要的方面。以下是一些性能优化的建议:
- 缓冲区管理 :合理设置缓冲区大小,避免频繁的内存分配和释放。可以根据实际数据传输量来调整缓冲区大小,减少内存开销。
-
异步操作
:对于一些耗时的操作,如数据传输,可以考虑使用异步方式,避免阻塞主线程。可以结合
select、poll或epoll等函数实现异步 I/O。 - 连接复用 :尽量复用已建立的连接,避免频繁地创建和销毁连接。对于需要频繁通信的场景,保持连接的持久性可以减少连接建立和释放的开销。
- 批量传输 :将多个小的数据块合并成一个大的数据块进行传输,减少传输次数,提高传输效率。
12. 与其他网络编程接口的比较
TLI 是一种较为底层的网络编程接口,与其他常见的网络编程接口(如套接字接口)相比,有其自身的特点。
| 比较项 | TLI | 套接字接口 |
|---|---|---|
| 通用性 | 提供了与传输协议无关的接口,可支持多种传输协议 | 主要基于特定的协议族(如 TCP/IP) |
| 复杂性 | 相对复杂,需要处理更多的底层细节 | 相对简单,易于上手 |
| 性能 | 由于更接近底层,在某些情况下可能具有更好的性能 | 性能也不错,但可能在处理一些特殊需求时需要更多的额外工作 |
| 可移植性 | 在支持 TLI 的系统上具有较好的可移植性 | 在大多数操作系统上都有广泛的支持 |
选择使用 TLI 还是其他网络编程接口,需要根据具体的应用场景和需求来决定。如果需要对网络传输进行更精细的控制,或者需要支持多种传输协议,TLI 可能是一个不错的选择;如果追求简单易用和广泛的兼容性,套接字接口可能更合适。
13. 总结与展望
基于 TLI 的网络编程为我们提供了一种可靠的面向连接的网络通信方式。通过使用 TLI 提供的各种函数和机制,我们可以实现服务器和客户端之间的稳定连接和数据传输。在实际开发中,我们需要熟悉 TLI 的各个函数的使用方法,合理处理错误和异常情况,同时根据不同的操作系统环境进行适当的调整。
随着网络技术的不断发展,新的网络编程接口和框架不断涌现,但 TLI 作为一种经典的网络编程接口,仍然在一些特定的场景中发挥着重要的作用。未来,我们可以结合新的技术和方法,进一步优化基于 TLI 的网络编程,提高网络通信的性能和可靠性。同时,也可以探索 TLI 与其他新兴技术的结合,为网络应用的开发带来更多的可能性。
总之,掌握基于 TLI 的网络编程技术,对于深入理解网络通信原理和开发高效、稳定的网络应用具有重要的意义。希望本文能够帮助读者更好地理解和应用 TLI 进行网络编程。
超级会员免费看
40

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



