UNIX网络编程卷一 学习笔记 第十章 SCTP客户/服务器程序例子

编写一个一到多式SCTP回射客户/服务器程序,执行如下步骤:
1.客户从标准输入读入一行文本,并发送给服务器,该文本行遵循[#]text格式,方括号中的数字表示要在这个流号上发送该文本消息。

2.服务器从网络接收这个文本消息,并将接收消息的流号加1,然后把这个文本消息从新的流号上发回。

3.客户读到回射的行,然后将其打印在标准输出上,并列出流号、流序列号(用于标识一个数据块在它所在流中的序号)和新流号。
在这里插入图片描述
上图中我们在客户与服务器之间画了两个代表单向流的箭头,但整个关联是全双工的。

本例中我们使用一到多式接口。基于TCP的回射客户/服务器程序略作修改就能运行于SCTP上:把socket函数的第三个参数由IPPROTO_TCP改为IPPROTO_SCTP。但这样简单地改动无法发挥SCTP提供的除多宿外的特性,而一到多式接口允许使用SCTP的所有特性。

以下是SCTP回射服务器代码:

#include "unp.h"

int main(int argc, char **argv) {
    int sock_fd, msg_flags;
    char readbuf[BUFFSIZE];
    struct sockaddr_in servaddr, cliaddr;
    struct sctp_sndrcvinfo sri;
    struct sctp_event_subscribe events;
    int stream_increment = 1;    // 是否每次把流号增加1
    socklen_t len;
    size_t rd_sz;

    if (argc == 2) {
        stream_increment = atoi(argv[1]);
    }
    sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP):
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    // 对多宿主机而言,捆绑通配地址意味着一个远程端点能与任何本地可路由地址建立关联
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(sock_fd, (SA *)&servaddr, sizeof(servaddr));

    bzero(&evnts, sizeof(evnts));
    // 仅预定sctp_data_io_event,从而允许服务器查看sctp_sndrcvinfo结构
    // 服务器可从该结构确定消息到达所在的流号
    evnts.sctp_data_io_event = 1;
    Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));

    Listen(sock_fd, LISTENQ);
    for (; ;) {
        len = sizeof(struct sockaddr_in);
		rd_sz = Sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf), (SA *)&cliaddr, &len, &sri, &msg_flags);
		// 如果需要增长流号
		if (stream_increment) {
		    sri.sinfo_stream++;
		    // 如果增长到最大流号,重置流号为0
		    // sctp_get_no_strms函数通过SCTP_STATUS套接字选项找出商定的流数目
		    if (sri.sinfo_stream >= sctp_get_no_strms(sock_fd, (SA *)&cliaddr, len)) {
		        sri.sinfo_stream = 0;
		    }
		}
		Sctp_sendmsg(sock_fd, readbuf, rd_sz, (SA *)&cliaddr, len, sri.sinfo_ppid, sri.sinfo_flags, sri.sinfo_stream, 0, 0);
    }
}

以上程序运行到用户以外部信号杀掉服务器进程为止。

以下是SCTP客户程序的main函数:

#include "unp.h"

int main(int argc, char **argv) {
    int sock_fd;
    struct sockaddr_in servaddr;
    struct sctp_event_subscribe evnts;
    int echo_to_all = 0;

    if (argc < 2) {
        err_quit("Missing host argument - use '%s host [echo]'\n", argv[0]);
    }
    if (argc > 2) {
        printf("Echoint messages to all streams\n");
		echo_to_all = 1;
    }

    sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    bzero(&evnts, sizeof(evnts));
    evnts.sctp_data_io_event = 1;
    Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts));
    if (echo_to_all == 0) {
        sctpstr_cli(stdin, sock_fd, (SA *)&servaddr, sizeof(servaddr));
    } else {
        sctpstr_cli_echoall(stdin, sock_fd, (SA *)&servaddr, sizeof(servaddr));
    }

    Close(sock_fd);
    return 0;
}

客户的sctpstr_cli函数:

void sctpstr_cli(FILE *fp, int sock_fd, struct sockaddr *to, socklen_t tolen) {
    struct sockaddr_in peeraddr;
    struct sctp_sndrcvinfo sri;
    char sendline[MAXLINE], recvline[MAXLINE];
    socklen_t len;
    int out_sz, rd_sz;
    int msg_flags;

    bzero(&sri, sizeof(sri));
    // 循环读入用户输入并处理,直到客户键入终端EOF字符Control-D
    while (fgets(sendline, MAXLINE, fp) != NULL) {
        // 检查客户输入是否符合[#]text格式
        if (sendline[0] != '[') {
		    printf("Error, line must be of the form '[streamnum]text\n");
		    continue;
		}
		// 把客户输入的流号转换成sri结构的sinfo_stream字段
		sri.sinfo_stream(strtol(&sendline[1], NULL, 0));
		out_sz = strlen(sendline);
		Sctp_sendmsg(sock_fd, sendline, out_sz, to, tolen, 0, 0, sri.sinfo_stream, 0, 0);
	
		len = sizeof(peeraddr);
		rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline), (SA *)&peeraddr, &len, &sri, &msg_flags);
		printf("From str: %d seq:%d (assoc:0x%x):", sri.sinfo_stream, sri.sinfo_ssn, (u_int)sri.sinfo_assoc_id);
		printf("%.*s", rd_sz, recvline);
    }
}

运行SCTP回射程序:
在这里插入图片描述
以上程序中,如果客户的sctp_sendmsg调用返回错误,那么就不会发送任何消息,客户进程于是阻塞在sctp_recvmsg调用中,等待永远不会回来的响应,解决办法是检查这个函数的返回值,如果消息发送出错,那就不再接收。

如果客户的sctp_recvmsg调用返回错误,那么不会收到响应消息,客户进程接着循环发送消息,可能导致建立新的关联,解决办法是检查这个函数的返回值,根据情况报告错误或关闭套接字,从而让服务器也收到一个错误,或若错误是暂时的则重新尝试sctp_recvmsg调用。

如果服务器在收到一个请求后退出,客户将被永远挂起,等待决不会到来的消息,客户检测这种情况的方法之一是开启关联事件,这样当服务器退出时客户将收到一个消息,告知客户该关联已不复存在。方法二是客户启动一个定时器,一段时间收不到响应就取消关联。

SCTP中的流不同于TCP的字节流,它是关联内部具有先后顺序的一个消息序列。这种以流而不是以关联为单位进行消息排序的做法用于避免仅使用单个TCP字节流导致的头端阻塞现象。

头端阻塞发生在一个TCP分节丢失,导致其后续分节到达接收端的时候接收端认为到达的分节是失序的。这些后续分节将被接收端一直缓存直到丢失的分节被发送端重传并到达接收端为止。这些后续分节的延迟递送确保接收应用进程能按顺序得到由发送进程发送的数据,但也有不好之处,假如在单个TCP连接上发送语义上独立的消息,如服务器可能发送3幅不同的图像供web浏览器显示,为了营造这几幅图像在用户屏幕上并行显示的效果,服务器先发送第一幅图片的一个断片,再发送第二幅图像的一个断片,然后再发送第三幅图像的一个断片,服务器重复此过程,直到3幅图像全部成功发送到浏览器。如果第一幅图像的某个断片内容的TCP分节丢失了,客户将缓存不按序到达所有数据,直到丢失的分节被重传并成功到达,这样不仅延缓了第一幅图像的递送,也延缓了第二幅和第三幅图像数据的递送:
在这里插入图片描述
尽管可为每幅图像创建一个TCP连接(HTTP客户通常这么做)避免了头端阻塞问题,但每个连接不得不独立发现RTT和可用带宽,这将导致拥塞网络上较低的整体利用率。

理想情况下,只有第一幅图像的后续断片会被延缓,而按顺序到达的第二幅和第三幅图像的各个断片将被立即递送给用户。

SCTP的多流特性能尽可能减少头端阻塞:
在这里插入图片描述
以下是sctpstr_cli_echoall函数,展示了SCTP如何把头端阻塞减到最小:

#define SCTP_MAXLINE 800

void sctpstr_cli_echoall(FILE *fp, int sock_fd, struct sockaddr *to, socklen_t tolen) {
    struct sockaddr_in peeraddr;
    struct sctp_sndrcvinfo sri;
    char sendline[SCTP_MAXLINE], recvline[SCTP_MAXLINE];
    socklen_t len;
    int rd_sz, i, strsz;
    int msg_flags;

    bzero(sendline, sizeof(sendline));
    bzero(&sri, sizeof(sri));
    // 每次最多读800字节,原因是我们想让每个SCTP块处于单个分组中
    // 更好的方法是通过SCTP_MAXSEG套接字选项确定适合一个SCTP块的大小
    while (fgets(sendline, SCTP_MAXLINE - 9, fp) != NULL) {
        strsz = strlen(sendline);
		if (sendline[strsz - 1] == '\n') {
		    sendline[strsz - 1] = '\0';
		    --strsz;
		}
		// 客户只是把消息发送到固定数目的流中,如果对端向下商定流数,则客户发送的一些消息可能会失败
		for (i = 0; i < SERV_MAX_SCTP_STRM; ++i) {
		    // 在消息末尾加上发送的流号,从而在后面接收时查看响应消息到达的顺序
		    snprintf(sendline + strsz, sizeof(sendline) - strsz, ".msg.%d", i);
		    Sctp_sendmsg(sock_fd, sendline, sizeof(sendline), to, tolen, 0, 0, i, 0, 0);
		}
		for (i = 0; i < SERV_MAX_SCTP_STRM; ++i) {
		    len = sizeof(peeraddr);
		    rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline), (SA *)&peeraddr, &len, &sri, &msg_flags);
		    printf("From str:%d seq:%d (assoc:0x%x):", sri.sinfo_stream, sri.sinfo_ssn, (u_int)sri.sinfo_assoc_id);
		    printf("%.*s\n", rd_sz, recvline);
		}
    }
}

以上函数类似于sctpstr_cli函数,但客户不再需要指出每个文本消息的流号,本函数把用户输入的文本发送到SERV_MAX_SCTP_STRM个流中,发送完消息后,客户等待来自服务器的所有响应到达。运行服务器时,我们传递一个额外的命令行参数,使服务器在接收到消息的流上给出响应。

以上程序每次发送800字节的数据,而Nagle算法会在较小的数据传送大小前提下导致延迟,如果发送的数据较小,会暂缓发送后面的数据以等待来自对端的SACK,如果发送的数据量较小,最好禁止Nagle算法。

如果服务器的发送或接收缓冲区过小,以上程序可能会失败,由于客户只有在发送完所有消息后才会读消息,因此如果服务器要发送的消息全放到发送缓冲区放不下(此时客户的接收缓冲区已满),且服务器的接收缓冲区也放不下当前接收的消息时,会发生死锁,此时客户会等服务器接收所有消息,服务器在等客户读取消息。

我们在两个不同的FreeBSD主机上执行客户和服务器程序,这两个主机由一个可配置的路由器分开,路由器能配置成插入延迟和丢失,我们先在路由器不丢失的前提下执行。
在这里插入图片描述
我们以一个额外参数0启动服务器,使服务器不增长应答所用流号。接着启动客户,通过命令行传入服务器主机的地址和一个额外参数,使客户把消息发送到每个流。
在这里插入图片描述
在没有丢失的前提下,客户的收到的响应消息按它们发送的顺序到达,之后把路由器参数改为两个方向的分组丢失率均为10%:
在这里插入图片描述
让客户往每个流中发送两条消息,就能验证同一个流内消息会保持顺序,以下是客户程序的改动:

for (i = 0; i < SERV_MAX_SCTP_STRM; ++i) {
    // 在每个消息后还增添了一个消息序号,以便标识同一个流内的两个消息
    snprintf(sendline + strsz, sizeof(sendline) - strsz, ".msg.%d 1", i);
    Sctp_sendmsg(sock_fd, sendline, sizeof(sendline), to, tolen, 0, 0, i, 0, 0);
    snprintf(sendline + strsz, sizeof(sendline) - strsz, ".msg.%d 2", i);
    Sctp_sendmsg(sock_fd, sendline, sizeof(sendline), to, tolen, 0, 0, i, 0, 0);
}
for (i = 0; i < SERV_MAX_SCTP_STRM * 2; ++i) {
    len = sizeof(peeraddr);

运行改动过的代码:
在这里插入图片描述
可见,消息存在丢失现象,但只有一个流内的消息才因此延缓。SCTP流可以说是一个既能避免头端阻塞又能在相关的消息之间保持顺序的有效机制。

我们以上例子使用的是外出流数目的默认值,对于FreeBSD上的SCTP的KAME实现而言,这个默认值为10,如果客户和服务器想用多于10个的流,我们可以把服务器修改为允许在关联启动阶段增加端点请求的流数目,这个改动必须针对尚未建立关联的套接字进行,已建立关联的流数不会受到影响:

if (argc == 2) {
    stream_increment = atoi(argv[1]);
}
sock_fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
// 清零sctp_initmsg结构
bzero(&initm, sizeof(initm));
// SERV_MORE_STRMS_SCTP是服务器期望请求的流数目
initm.sinit_num_ostreams = SERV_MORE_STRMS_SCTP;
Socketopt(sock_fd, IPPROTO_SCTP, SCTP_INITMSG, &initm, sizeof(initm));

也可使用sendmsg函数并提供辅助数据以请求不同于默认设置的流参数,但这种类型的辅助数据仅适用于一到多式套接字,因为辅助数据必须随消息一起发送,而一到一式套接字使用write函数,没有参数来发送辅助数据。

以上例子中,我们依赖于客户关闭套接字来终止关联,但客户并不总是愿意关闭套接字,服务器也可能不愿意在发送了应答消息后继续保持关联开放,此时我们有终止关联的另外两个机制,对于一到多式接口,这两个方法都适用,一个是优雅的,一个是破坏性的。

如果服务器希望在发送完一个消息后终止一个关联,则可以在与该消息对应的sctp_sndrcvinfo结构的sinfo_flags字段中设置MSG_EOF字段,该标志使所发送的消息被客户确认后,相应关联也被终止。另一个方法是把MSG_ABORT标志应用于sinfo_flags字段,该标志会以ABORT块立即终止关联,SCTP的ABORT块类似TCP的RST分节,能无延迟地中止任何关联,尚未发送的数据被丢弃。SCTP没有类似TCP的TIME_WAIT状态,因为SCTP使用了验证标签(SCTP数据包中的一个字段,用作关联标识符),当SCTP建立连接时,双方会交换INIT数据块,其中包含了各自的验证标签,之后每个发送的数据块都要带上接收方的验证标签,防止旧连接或恶意连接的干扰。

修改服务器程序,使其在应答同时优雅地终止关联:

Sctp_sendmsg(sock_fd, readbuf, rd_sz, (SA *)&cliaddr, len, sri.sinfo_ppid, (sri.sinfo_flags | MSG_EOF), sri.sinfo_stream, 0, 0);

修改客户程序,使其在调用close前abort关联:

strcpy(byemsg, "goodbye");
Sctp_sendmsg(sock_fd, byemsg, strlen(byemsg), (SA *)&servaddr, sizeof(servaddr), 0, MSG_ABORT, 0, 0, 0);
// 即使关联已经中止,仍需关闭套接字描述符以释放与之关联的系统资源
Close(sock_fd);

客户以MSG_ABORT调用sctp_sendmsg函数,该标志导致发送一个ABORT块,从而立即终止当前关联,这个ABORT块中包含用户发起的错误(即ABORT块)的起因代码,上层原因字段的消息为goodbye。

posted @   epiphanyy  阅读(59)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2022-03-22 JAVA关于数组
2020-03-22 C++ Primer 学习笔记 第十二章 动态内存
点击右上角即可分享
微信分享提示