UNIX网络编程卷一 学习笔记 第三十一章 流

在大多数源自SVR 4的内核中,X/Open传输接口(X/Open Transport Interface,XTI,是独立于套接字API的另一个网络编程API)和网络协议通常就像终端IO系统那样也使用流系统(STREAMS system)实现。

我们将使用传输提供者接口(Transport Provider Interface,TPI)开发一个简单的TCP客户程序,TPI是在基于流的系统上,XTI和套接字通常使用的传输层访问接口。

流由Dennis Ritchie设计,并于1986年随SVR 3首次广泛提供支持。POSIX规范将流定义为一个选项组(option group),意味着POSIX兼容系统可以不实现流,但如果实现,则必须符合POSIX规范。基本流函数包括getmsg、getpmsg、putmsg、putpmsg、fattach、所有流ioctl命令。XTI往往使用流实现。所有源自System V的系统都应提供流(SVR即System V Release),但各个4.x BSD版本不提供流。

流(STREAMS)这个名字尽管全是大写字母,但不是一个首字母缩写词,因此改用全小写字母可能更合理。我们需要区分本章讲解的流IO系统(streams IO system)和标准IO流(与标准IO库相关)。

流在进程和驱动程序之间提供全双工的连接:
在这里插入图片描述
驱动程序不必与某个硬件设备相关联,它可以是一个伪设备驱动程序(即软件驱动程序)。

流头(stream head)由一个内核例程构成,应用进程针对流描述符执行系统调用(如read、putmsg、ioctl等)时这些内核例程将被激活。

进程可以在流头和驱动程序之间动态增加或删除中间处理模块(processing module),这些模块对顺着一个流上行或下行的消息施行某种类型的过滤:
在这里插入图片描述
往一个流中可以推入(pushing)任意数量的模块,推入指的是每个新模块都被插入到流头的紧下方。

多路复选器(multiplexor)是一种特殊类型的伪设备驱动程序,它从多个源接受数据,例如,可在SVR 4上找到的TCP/IP协议族基于流的某个实现如下图所示,其中就有多路复选器:在这里插入图片描述
上图中:
1.在创建一个套接字时,套接字函数库把模块sockmod推入流中,向应用进程提供套接字API的是套接字函数库和sockmod流模块两者的组合。

2.创建一个XTI端点时,XTI函数库把模块timod推入流中,向应用进程提供XTI API的是XTI函数库和timod流模块两者的组合。XTI API的端点相当于套接字API的套接字。

本书早先版本详细叙述了XTI API,但它已不被广泛使用,甚至POSIX规范也不再涵盖它,因此就不讲述了。

3.为了针对XTI端点使用read和write访问网络数据,通常必须把模块tirdwr推入流中,推入该模块后该进程可能不会再使用XTI了,因此上图中我们没有显示XTI库。

4.各种各样的服务接口定义了网络消息在流中上行和下行交换的格式。最常见的3个服务接口为:
(1)传输提供者接口(TPI)定义了传输层提供者(如TCP和UDP)向它上方的模块提供的接口。

(2)网络提供者接口(NPI,Network Provider Interface)定义了网络层提供者(如IP)向它上方的模块提供的接口。

(3)数据链路提供者接口(DLPI)。

一个流中的每个部件(流头、所有处理模块、驱动程序)都包含至少一对队列,即一个写队列和一个读队列:
在这里插入图片描述
流消息可分为高优先级(high priority)、优先级带(priority band)、普通(normal)三类。优先级共有256带,在0~255之间取值,其中普通消息位于带0,流消息的优先级用于排队和流量控制,按约定高优先级消息不受流量控制影响。下图是一个给定队列中消息的出现顺序:
在这里插入图片描述
虽然流系统支持256个不同的优先级带,网络协议往往只用经加速数据的带1和代表普通数据的带0。

TPI不认为TCP带外数据是真正的经加速数据,事实上TCP的普通数据和带外数据都使用带0。只有那些让经加速数据先于普通数据发送的协议才使用带1发送经加速数据。

在SVR 4之前的版本中没有优先级带的概念,只有普通消息和优先级消息,SVR 4实现了优先级带,并提供了getpmsg和putpmsg函数,较早的优先级消息于是被重命名为高优先级,常用的术语定义[ Rago 1993 ]称高优先级外的消息为普通优先级(normal priority)消息,然后把这些普通优先级消息细分到各个优先级带中。普通消息一词指处于带0的消息。

普通优先级消息和高优先级消息这两大类中分别约有12种和18种,从应用进程和getmsg、putmsg函数角度看,我们仅关注3种不同类型的消息:M_DATA、M_PROTO、M_PCPROTO(PC表示priority control,优先级控制,隐指高优先级消息)。下图说明了这三种消息类型是如何使用write和putmsg函数产生的:
在这里插入图片描述
沿着流上行和下行的数据由消息构成,且每个消息含有控制或数据,或两者都有。如果在流上使用read和write函数,那么所传送的仅仅是数据,为了让进程能读写数据和控制两部分信息,流系统增加了以下函数:
在这里插入图片描述
消息的控制和数据两部分各自由一个strbuf结构说明:
在这里插入图片描述
注意strbuf结构和XTI API所用的netbuf结构之间的相似性,它们由3个同名成员构成,但netbuf结构的两个长度成员是无符号整数,而strbuf结构的两个长度成员是普通整数。原因在于有些流函数使用值为-1的len或maxlen成员表示特殊的含义。

使用putmsg可以单纯发送控制信息或数据,也可同时发送两者。为了指示不发送控制信息,可把ctlptr参数指定为空指针,也可把ctlptr->len设为-1。同样的手段设置dataptr参数用于指示不发送数据信息。

如果不发送控制信息,putmsg函数将产生一个M_DATA消息,否则根据flags参数产生一个M_PROTO或M_PCPROTO消息,flags参数为0表示普通消息,为RS_HIPRI表示高优先级消息。

getmsg函数的最后一个参数是一个值-结果参数,如果调用时指定的flagsp参数指向的整数值为0,则返回的是流中第一个消息(既可能是普通消息,也可能是高优先级消息),如果该整数值为RS_HIPRI,那就等待一个高优先级消息到达流头,无论哪种情况,存放到flagsp参数指向的整数中的值根据所返回消息的类型或为0,或为RS_HIPRI。

假设传给getmsg函数的ctlptr和dataptr参数都是非空指针,如果没有控制信息待返回(也就是即将返回一个M_DATA消息),getmsg函数就在返回时把ctlptr->len设为-1作为指示,类似地,没有数据待返回时就把dataptr->len设为-1。

putmsg函数在成功时返回0,在出错时返回-1。但getmsg函数仅在整个消息完整返回给调用者时才返回0,如果控制缓冲区不足以容纳完整的控制信息,就返回非负的MORECTL,类似地,如果数据缓冲区太小,就返回MOREDATA,如果两个缓冲区都太小,就返回这两个标志的逻辑或。

对不同优先级带的支持随SVR 4被增加到流系统时,以下两个getmsg和putmsg函数的变体函数也被同时引入:
在这里插入图片描述
putpmsg函数的band参数必须在0~255之间(含),如果flags参数为MSG_BAND,就产生一个所指定优先级带的消息,把flags参数置为MSG_BAND且把band参数置为0等效于调用putmsg,如果flags参数为MSG_HIPRI,band参数就必须为0,所产生的的是一个高优先级消息(而putmsg函数使用的标志为RS_HIPRI)。

getpmsg的bandp和flagsp参数是值-结果参数,flagsp参数指向的整数可以取值MSG_HIPRI(用来读入一个高优先级消息)、MSG_BAND(用来读入一个优先级至少为bandp参数指向的整数值消息)、MSG_ANY(用来读入任一消息)。函数返回时,bandp参数指向的整数含有所读入消息的优先级带,flagsp参数指向的整数可能是MSG_HIPRI(如果读入的是一个高优先级消息)或MSG_BAND(如果读入的是其他类型消息)。

在流系统中我们会再次使用ioctl函数:
在这里插入图片描述
此处的ioctl函数的与第十七章中的相比,唯一的变化就是处理流时所包含的头文件不同。

大约30个ioctl请求影响流头,每个请求都以I_打头,它们的具体说明通常在streamio手册页面给出。

在图31-3中,我们把TPI表示为传输层向它上方的模块提供的服务接口。在流环境中,套接字和XTI都使用TPI。在图31-3中,应用进程跟TCP和UDP交换TPI消息通过的是套接字函数库和sockmod的组合或XTI函数库和timod的组合。

TPI是一个基于消息的接口,它定义了在应用进程(如XTI函数库或套接字函数库)和传输层之间沿着流上行和下行交换的消息,包括消息的格式和每个消息执行的操作。例如,应用进程向提供者发送一个请求(如bind一个本地地址),提供者则发回一个响应(成功或出错)。一些事件在提供者异步地发生(对某个服务器来说,连接请求的到达),它们导致沿着流向上发送的消息或信号。

我们可以绕过XTI和套接字直接使用TPI,我们将改用TPI取代套接字重新编写我们的时间获取客户程序。用编程语言进行类比,使用套接字或XTI好比使用诸如C或Pascal等高级语言编程,而使用TPI好比使用汇编语言编程。我们不提倡在现实应用程序中使用TPI,但查看TPI如何工作并开发本例有助于我们更好地理解在流环境中套接字函数库和XTI函数库的工作原理。

以下是tpi_daytime.h头文件:

#include "unpxti.h"
// 头文件sys/steam.h中包含了所有TPI消息的结构定义
#include <sys/stream.h>
#include <sys/tihdr.h>void tpi_bind(int, const void *, size_t);
void tpi_connect(int, const void *, size_t);
ssize_t tpi_read(int, void *, size_t);
void tpi_close(int);

以下是时间获取客户程序的main函数:

#include "tpi_daytime.h"int main(int argc, char **argv) {int fd, n;char recvline[MAXLINE + 1];struct sockaddr_in myaddr, servaddr;if (argc != 2) {err_quit("usage: tpi_daytime <IPaddress>");}// 打开与传输提供者TCP对应的设备(通常为/dev/tcp)fd = Open(XTI_TCP, O_RDWR, 0);/* bind any local address */bzero(&myaddr, sizeof(myaddr));myaddr.sin_family = AF_INET;// 以INADDR_ANY和端口0填写一个网际网套接字地址结构,告知TCP捆绑任一本地地址和本地端点,由tpi_bind函数完成捆绑myaddr.sin_addr.s_addr = htonl(INADDR_ANY);myaddr.sin_port = htons(0);tpi_bind(fd, &myaddr, sizeof(struct sockaddr_in));/* fill in server's address */bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(13);    /* daytime server */Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);// tpi_connect函数建立与服务器的连接tpi_connect(fd, &servaddr, sizeof(struct sockaddr_in));for (; ; ) {if ((n = tpi_read(fd, recvline, MAXLINE)) <= 0) {if (n == 0) {break;} eles {err_sys("tpi_read error");}}recvline[n] = 0;    /* null terminate */fputs(recvline, stdout);}tpi_close(fd);exit(0);
}

以下是tpi_bind函数:

#include "tpi_daytime.h"void tpi_bind(int fd, const void *addr, size_t addrlen) {struct {// T_bind_req结构介绍在代码下面// 所有TPI请求都定义成以一个长整数类型字段开头的某个结构,如此处的T_bind_req结构,后跟一个缓冲区// TPI对该缓冲区中内容未做任何规定,由具体的提供者定义,TCP提供者期待该缓冲区中有一个sockaddr_in结构struct T_bind_req msg_hdr;char addr[128];} bind_req;struct {struct T_bind_ack msg_hdr;char addr[128];} bind_ack;struct strbuf ctlbuf;struct T_error_ack *error_ack;int flags;bind_req.msg_hdr.PRIM_type = T_BIND_REQ;bind_req.msg_hdr.ADDR_length = addrlen;    // addrlen对于网际网套接字地址结构为16字节// 设置套接字地址结构的偏移量bind_req.msg_hdr.ADDR_offset = sizeof(struct T_bind_req);// 由于我们是客户,因此将CONIND_number设为0bind_req.msg_hdr.CONIND_number = 0;// 我们直接用memcpy函数而非赋值运算等方式将sockaddr_in结构复制到bind_req结构中// 因此这难以保证sockaddr_in结构是适当对齐的memcpy(bind_req.addr, addr, addrlen);    /* sockaddr_in{} */ctlbuf.len = sizeof(struct T_bind_req) + addrlen;ctlbuf.buf = (char *)&bind_req;// TPI要求我们把刚构造的结构作为一个M_PROTO消息传给提供者// 于是我们将这个bind_req结构指定为控制信息,且指定没有数据信息,且指定标志为0Putmsg(fd, &ctlbuf, NULL, 0);ctlbuf.maxlen = sizeof(bind_ack);ctlbuf.len = 0;ctlbuf.buf = (char *)&bind_ack;// 对T_BIND_REQ请求的响应或者是T_BIND_ACK消息,或者是T_ERROR_ACK消息,这些确认消息是高优先级消息// 于是我们指定RS_HIPRI读入它,既然该应该是高优先级消息,它将绕过流中任意普通优先级消息// 可能的应答消息的结构介绍在代码下面// 两个应答消息都以PRIM_type成员打头,因此我们可以假设它是一个T_BIND_ACK消息读入应答// 查看类型值后再相应地处理该消息flags = RS_HIPRI;// 我们不期望提供者的任何数据,因此getmsg函数的第三个参数为空指针Getmsg(fd, &ctlbuf, NULL, &flags);// 在验证所返回的控制信息量至少是一个长整数的大小时,我们需要把sizeof的返回值强转为一个int// 因为sizeof返回的是一个无符号整型,而getmsg函数返回的strbuf.len可能是-1// 如果不进行转换,比较运算符一边是一个有符号值,一边是一个无符号值,C编译器会将有符号值转换为无符号值// -1转换为有符号值非常大,导致-1大于4(假设一个长整数占4个字节)if (ctlbuf.len < (int)sizeof(long)) {err_quit("bad length from getmsg");}switch (bind_ack.msg_hdr.PRIM_type) {// 如果应答是T_BIND_ACK消息,那么捆绑成功,直接返回,捆绑的实际地址由bind_ack结构的addr成员返回case T_BIND_ACK:return;// 如果应答是T_ERROR_ACKcase T_ERROR_ACK;// 验证所收到的是完整的消息if (ctlbuf.len < (int)sizeof(struct T_error_ack)) {err_quit("bad length for T_ERROR_ACK");}error_ack = (struct T_error_ack *)&bind_ack.msg_hdr;// 如果发生错误直接终止,不再返回到调用者err_quit("T_ERROR_ACK from bind (%d, %d)", error_ack->TLI_error, error_ack->UNIX_error);default:err_quit("unexpected message type: %d", bind_ackmsg_hdr.PRIM_type);}
}

以上函数中,T_bind_req结构在头文件sys/tihdr.h中定义如下:
在这里插入图片描述
以上函数中,对于T_BIND_REQ请求的响应或者是T_BIND_ACK消息,或者是T_ERROR_ACK消息,这两个应答消息的结构定义如下:
在这里插入图片描述
对于以上函数,我们尝试捆绑端口1,这需要超级用户权限,因为它是1024以内的端口,我们会得到以下输出:
在这里插入图片描述
该系统上EACCES的值为3。如果我们尝试捆绑一个1023以上,但被另一TCP端点使用中的端口,我们会得到以下输出:
在这里插入图片描述
该系统上EADDRBUSY的值为23,这个错误是TPI为了支持XTI而引入的,支持TLI的较早版本TPI在请求捆绑一个已使用的端口时将另行捆绑一个未使用的端口,这意味着捆绑众所周知端口的服务器不得不比较返回的地址(返回的地址出自t_bind函数的第3个指针参数返回的T_bind_ack消息)和请求的地址,如果不一致就放弃。

以下是tpi_connect函数,它建立与服务器的连接:

#include "tpi_daytime.h"void tpi_connect(int fd, const void *addr, size_t addrlen) {// 就像tpi_bind函数一样,此处也自定义一个名为conn_req的结构,它包含一个T_conn_req结构和用于存放协议地址的空间struct {// T_conn_req结构介绍在代码下面struct T_conn_req msg_hdr;char addr[128];} conn_req;struct {struct T_conn_con msg_hdr;char addr[128];} conn_con;struct strbuf ctlbuf;union T_primitives rcvbuf;struct T_error_ack *error_ack;struct T_discon_ind *discon_ind;int flags;conn_req.msg_hdr.PRIM_type = T_CONN_REQ;conn_req.msg_hdr.DEST_length = addrlen;conn_req.msg_hdr.DEST_offset = sizeof(struct T_conn_req);conn_req.msg_hdr.OPT_length = 0;conn_req.msg_hdr.OPT_offset = 0;memcpy(conn_req.addr, addr, addrlen);    /* sockaddr_in{} */ctlbuf.len = sizeof(struct T_conn_req) + addrlen;ctlbuf.buf = (char *)&conn_req;// 单纯指定控制信息调用putmsg,同时把标志指定为0,以顺着流下行发送一个M_PROTO消息Putmsg(fd, &ctlbuf, NULL, 0);ctlbuf.maxlen = sizeof(union T_primitives);ctlbuf.len = 0;ctlbuf.buf = (char *)&rcvbuf;flags = RS_HIPRI;// 调用getmsg期待接受T_OK_ACK消息(表示连接建立已启动,T_ok_ack结构介绍在代码下面)// 或可能接收到T_ERROR_ACK消息(上面已给出)// 既然不知道会接收到什么类型的消息,我们于是定义一个名为T_primitives的由所有可能的请求和应答组成的联合// 该联合用作控制消息的输入缓冲区Getmsg(fd, &ctlbuf, NULL, &flags);if (ctlbuf.len < (int)sizeof(long)) {err_quit("tpi_connect: bad length from getmsg");}switch (rcvbuf.type) {// 表示成功的T_OK_ACK消息只是告诉我们连接建立已启动,现在还要等待T_CONN_CON消息以获悉对端是否已接受该连接case T_OK_ACK:break;case T_ERROR_ACK:if (ctlbuf.len < (int)sizeof(struct T_error_ack)) {err_quit("tpi_connect: bad length for T_ERROR_ACK");}error_ack = (struct T_error_ack *)&rcvbuf;err_quit("tpi_connect: T_ERROR_ACK from conn (%d, %d)", error_ack->TLI_error, error_ack->UNIX_error);default:err_quit("tpi_connect: unexpected message type: %d", rcvbuf.type);}ctlbuf.maxlen = sizeof(conn_con);ctlbuf.len = 0;ctlbuf.buf = (char *)&conn_con;flags = 0;// 再次调用getmsg,但所期待的消息是一个M_PROTO消息而非M_PCPROTO消息,于是把标志设为0// 如果收到一个T_CONN_CON消息(T_conn_con结构介绍在代码下面),则连接建立完毕 // 如果连接未能建立(对端进程不在运行、超时等原因),会返回一个T_DISCON_IND消息(T_discon_ind结构介绍在代码下面)Getmsg(fd, &ctlbuf, NULL, &flags);if (ctlbuf.len < (int)sizeof(long)) {err_quit("tpi_connect2: bad length from getmsg");}switch(conn_con.msg_hdr.PRIM_type) {case T_CONN_CON:break;case T_DISCON_IND:if (ctlbuf.len < (int)sizeof(struct T_discon_ind)) {err_quit("tpi_connect2: bad length for T_DISCON_IND");}discon_ind = (struct T_discon_ind *)&conn_con.msg_hdr;err_quit("tpi_connect2: T_DISCON_IND from conn (%d)", discon_ind->DISCON_reason);default:err_quit("tpi_connect2: unexpected message type: %d", conn_con.msg_hdr.PRIM_type);}
}

TPI定义了一个T_conn_req结构,用于存放连接的协议地址和选项:
在这里插入图片描述
连接建立已启动时,返回的T_ok_ack结构如下:
在这里插入图片描述
连接成功建立时,返回的T_conn_con结构如下:
在这里插入图片描述
连接建立失败时,返回的T_conn_ind结构如下:
在这里插入图片描述
对于以上函数,我们先查看提供者返回的各种错误,如果我们指定连接到一个没有运行标准daytime服务器的主机:
在这里插入图片描述
错误146表示ECONNREFUSED。接着指定一个未接入因特网的IP地址:
在这里插入图片描述
错误145表示ETIMEDOUT,再次对该IP运行本程序,我们得到另一个错误:
在这里插入图片描述
错误148表示EHOSTUNREACH,这两个结果的区别在于,第一次没有ICMP主机不可达错误的返送,而第二次有。

函数tpi_read从一个流中读入数据:

#include "tpi_daytime.h"ssize_t tpi_read(int fd, void *buf, size_t len) {struct strbuf ctlbuf;struct strbuf datbuf;union T_primitives rcvbuf;int flags;ctlbuf.maxlen = sizeof(union T_primitives);ctlbuf.buf = (char *)&rcvbuf;datbuf.maxlen = len;datbuf.buf = buf;datbuf.len = 0;flags = 0;// 同时读入控制信息和数据,用于返回数据的strbuf结构指向调用者指定的缓冲区,在读入时流上可能有4种情形:// 1.数据以一个M_DATA消息到来,这由返回的控制信息长度为-1指示//   消息会被拷贝到调用者指定的缓冲区,此时本函数返回getmsg函数返回的数据长度// 2.数据以一个T_DATA_IND消息到来,此时,控制消息是一个T_data_ind结构(此结构的介绍在代码下面)//   如果返回了此消息,我们就忽略MORE_flag成员(对于TCP这样的字节流协议,该成员不会被设置)//   此时本函数返回getmsg函数返回的数据长度// 3.到达一个T_DISCON_IND消息,表示收到一个断连请求,对于TCP提供者,本情形发生在某连接上收到RST后//   此简单的例子中,我们不处理该情形// 4.到达一个T_ORDREL_IND消息,表示TCP提供者已收取的所有分节均已被消费,且返回的是FIN//   T_ordrel_ind结构的介绍在代码下面,本函数此时返回0,以向调用者指示已在连接上遇到EOF//   这是TCP的有序释放(orderly release),即三次挥手Getmsg(fd, &ctlbuf, &datbuf, &flags);if (ctlbuf.len >= (int)sizeof(long)) {if (rcvbuf.type == T_DATA_IND) {return datbuf.len;} else if (rcvbuf.type == T_ORDREL_IND) {return 0;} else {err_quit("tpi_read: unexpected type %d", rcvbuf.type);}} else if (ctlbuf.len == -1) {return datbuf.len;} else {err_quit("tpi_read: bad length from getmsg");}
}

T_data_ind结构如下:
在这里插入图片描述
当收取FIN时,getmsg函数会返回一个T_ordrel_ind结构:
在这里插入图片描述
tpi_close函数:

#include "tpi_daytime.h"// 本函数相当于XTI的t_sndrel函数
void tpi_close(int fd) {struct T_ordrel_req ordrel_req;struct strbuf ctlbuf;ordrel_req.PRIM_type = T_ORDREL_REQ;ctlbuf.len = sizeof(struct T_ordrel_req);ctlbuf.buf = (char *)&ordrel_req;// 构造一个T_ordrel_req结构(此结构的介绍在代码下面)并调用putmsg将其作为一个M_PROTO消息发送出去Putmsg(fd, &ctlbuf, NULL, 0);Close(fd);
}

用于主动结束TCP连接的T_ordrel_req结构:
在这里插入图片描述
tpi_close函数中,我们调用putmsg沿着流下行发送一个顺序释放请求,然后立即关闭该流,如果关闭流期间,我们的有序释放请求被流子系统丢弃,此时流关闭时的默认处理就是有序释放,这对TCP来说没问题。

本例子展示了TPI的风格,应用进程沿着流下行向提供者发送消息(请求),提供者则沿着流上行发回消息(应答)。一些消息交换是简单的请求-应答情形(如捆绑一个本地地址),另外一些消息交换则需要耗费一段时间(如建立一个连接),并允许我们在等待应答期间做其他事而非空等。本例选择编写使用TPI的TCP客户程序而非服务器程序是为了简单,编写使用TPI的服务器程序并合理地处理连接要困难得多。

从XTI到TPI的函数映射比较接近,而从套接字从TPI的映射不那么接近,但这两个函数库都处理了TPI所需的大量细节,从而简化了应用程序的编写。

我们比较一下使用TPI完成网络操作与套接字实现于内核中的系统上完成同样操作所需的系统调用个数,TPI情形捆绑一个本地地址需要2个系统调用,而内核套接字情形只需要1个;TPI情形在一个阻塞式描述符上建立一个连接需要3个系统调用,而内核套接字情形只需要1个。

XTI一般使用流来实现,为访问流子系统而提供的4个新函数时getmsg、getpmsg、putmsg、putpmsg,已有的ioctl函数也被流子系统频繁使用。

TPI是从上层进入传输层的SVR 4流接口。XTI和套接字均使用TPI。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/70366.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nginx__基础入门篇

目录: Nginx的优势 HTTP协议详解 Nginx部署-Yum Nginx配置文件 Nginx编译参数 Nginx基本配置 Nginx日志Log Nginx WEB模块 Nginx 访问限制 Nginx 访问控制 Nginx的优势 Nginx (engine x) 是一个高性能的HTTP(解决C10k的问题)和反向代理服务器&#xff0c;也是一个IM…

leetcode 205. 同构字符串

2023.9.6 本题维护两个映射表map&#xff0c;若发现无法对应则返回false。 代码如下&#xff1a; class Solution { public:bool isIsomorphic(string s, string t) {unordered_map<char,char> m1;unordered_map<char,char> m2;for(int i0; i<s.size(); i){//相…

ctfshow vip题目限免

源码泄露 右键查看源代码即可得到flag 前台JS绕过 解法一 URL前面加上view-source: 解法二 题目描述说禁用js&#xff0c;那就禁用就行 然后和前面一样右键查看就行 协议头信息泄露 提示抓包&#xff0c;那就抓包吧 抓包直接就在返回包获得flag了 robots后台泄露 robot…

java获取jenkins发布版本信息

一.需求&#xff1a; 系统cicd发布时首页需要展示jenkins发布的版本和优化内容 二.思路: 1.jenkins创建用户和秘钥 2.找到对应构建任务信息的api 3.RestTemplate发起http请求 三.实现&#xff1a; 1.创建用户和token 2.查找jenkins API 创建 Job POST http://localhost…

Linux查端口占用的几种方式

在Linux中&#xff0c;你可以使用以下几种方式来查看端口的占用情况。 一、使用netstat命令 #安装netstat yum -y install net-tools #检测端口占用 netstat -npl | grep 端口# 几种常规用法 netstat -ntlp //查看当前所有tcp端口 netstat -ntulp | grep 80 //查看所有80端…

layui引入百度地图

<script type"text/javascript" src"//api.map.baidu.com/api?typewebgl&v1.0&ak你的ak"></script> <script src"https://code.bdstatic.com/npm/jquery1.12.4/dist/jquery.min.js"></script> <script src&…

看涨期权计算例题(期权案例计算)

看涨期权又称认购期权&#xff0c;买进期权&#xff0c;买方期权&#xff0c;买权&#xff0c;延买期权&#xff0c;或“敲进”&#xff0c;是指期权的购买者拥有在期权合约有效期内按执行价格买进一定数量标的物的权利&#xff0c;下文为大家科普看涨期权计算例题&#xff08;…

LLVM 与代码混淆技术

项目源码 什么是 LLVM LLVM 计划启动于2000年&#xff0c;开始由美国 UIUC 大学的 Chris Lattner 博士主持开展&#xff0c;后来 Apple 也加入其中。最初的目的是开发一套提供中间代码和编译基础设施的虚拟系统。 LLVM 命名最早源自于底层虚拟机&#xff08;Low Level Virtu…

ComfyUI 安装

背景&#xff1a; stable diffussion XL最先适配&#xff0c;专业性强的SD操作界面 安装步骤&#xff1a; git clone GitHub - comfyanonymous/ComfyUI: A powerful and modular stable diffusion GUI with a graph/nodes interface. 1、pip install torch torchvision torc…

window11,C盘瘦身减肥技巧

前言 本文主要说明windows11的C盘空间优化技巧&#xff0c;也叫减肥瘦身&#xff0c;目标&#xff1a;20G以内。绝大部分内容&#xff0c;也适用于window10和windows7&#xff0c;只是操作系统不同&#xff0c;操作方式略有不同。不关注C盘空间的读者&#xff0c;可以忽略此文…

2023年数维杯数学建模A题河流-地下水系统水体污染研求解全过程文档及程序

2023年数维杯数学建模 A题 河流-地下水系统水体污染研 原题再现&#xff1a; 河流对地下水有着直接地影响&#xff0c;当河流补给地下水时&#xff0c;河流一旦被污染&#xff0c;容易导致地下水以及紧依河流分布的傍河水源地将受到不同程度的污染&#xff0c;这将严重影响工…

sentinel加密狗使用及规则配置

Sentinel加密狗是一种硬件加密设备&#xff0c;用于保护软件应用程序免受未经授权的访问和复制。它可以提供软件许可管理、访问控制和数据保护等功能。下面是Sentinel加密狗的使用及规则配置的相关介绍。 Sentinel加密狗的使用 插入加密狗&#xff1a;将Sentinel加密狗插入计算…

基于语雀编辑器的在线文档编辑与查看

概述 语雀是一个非常优秀的文档和知识库工具&#xff0c;其编辑器更是非常好用&#xff0c;虽无开源版本&#xff0c;但有编译好的可以使用。本文基于语雀编辑器实现在线文档的编辑与文章的预览。 实现效果 实现 参考语雀编辑器官方文档&#xff0c;其实现需要引入以下文件&…

Google Earth Engine 的缺点和限制

随着 Google Earth Engine 在地球科学和数据计算领域越来越流行&#xff0c;网上有很多介绍Google Earth Engine 的文章及 Google Earth Engine的追随者。Google Earth Engine确实是一款伟大的产品&#xff0c;我们应该为其点赞。但由于已经有太多人在热捧了&#xff0c;我这里…

数据库设计:防止MySQL字段名与关键字相撞,保护数据完整性!

MySQL是一款广泛应用的关系型数据库管理系统&#xff0c;对于数据库设计而言&#xff0c;字段名的选择是至关重要的一环。不小心选择了和MySQL关键字相同的字段名可能导致严重的数据完整性问题。下面将深入探讨如何防止MySQL字段名与关键字相撞&#xff0c;以保护数据的完整性。…

报错: “Data is Null. This method or property cannot be called,解决方法

在进行导入的时候报错 System.Data.SqlTypes.SqlNullValueException: "Data is Null. This method or property cannot be called on Null values."是一个在使用DataReader.GetString(i)方法时出现的异常情况。当对应字段的值为Null时&#xff0c;这个方法会抛出异常…

用户案例 | 蜀海供应链基于 Apache DolphinScheduler 的数据表血缘探索与跨大版本升级经验

导读 蜀海供应链是集销售、研发、采购、生产、品保、仓储、运输、信息、金融为一体的餐饮供应链服务企业。2021年初&#xff0c;蜀海信息技术中心大数据技术研发团队开始测试用DolphinScheduler作为数据中台和各业务产品项目的任务调度系统工具。本文主要分享了蜀海供应链在海…

【Node.js】—基本知识点总结

【Node.js】—基本知识总结 一、命令行常用操作 二、Node.js注意点 Node.js中不能使用BOM和DOM操作 总结 三、Buffer buffer是一个类似于数组的对象&#xff0c;用于表示固定长度的字节序列buffer的本质是一段内存空间&#xff0c;专门用来处理二进制数据 特点&#xff1a;…

postgresql-子查询

postgresql-子查询 子查询简介派生表IN 操作符ALL 操作符ANY 操作符关联子查询横向子查询EXISTS 操作符 子查询简介 子查询&#xff08;Subquery&#xff09;是指嵌套在其他 SELECT、INSERT、UPDATE 以及 DELETE 语句中的 查询语句 子查询的作用与多表连接查询有点类似&#x…

基于Python机器学习、深度学习提升气象、海洋、水文应用教程

详情点击链接&#xff1a;基于Python机器学习、深度学习提升气象、海洋、水文应用教程 前沿 Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;能够在不同操作系统和平台使用&#xff0c;简洁的语法和解释性语言使其成为理想的脚本语言。除了标准…