欢迎光临
我们一直在努力

《UNIX 网络编程-卷1》阅读笔记11: SCTP客户/服务器程序例子

作者: andylin02
学习章节: 第十章 SCTP客户/服务器程序例子
关键词: SCTP, 一到多套接字, 回射服务器, 流号递增, sctp_get_no_strms, 流序列号(SSN), sctp_opt_info, 关联ID, 头端阻塞


一、章节概述

第十章是对第九章理论的实战验证。第九章详细介绍了SCTP的基本套接字API,本章则运用这些API编写一个完整的一到多式SCTP回射客户/服务器程序。

回射程序的核心步骤如下:

  • 客户从标准输入读入一行文本,格式为[#]text(方括号中的数字表示要在这个流号上发送该文本消息)
  • 服务器接收后,将接收消息的流号加1,然后在新流号上发回
  • 客户读取回射行并打印,同时列出**流号、流序列号(SSN)**和新流号
  • 这个简单的程序背后,蕴含了SCTP多流特性的核心价值:解决头端阻塞问题。流序列号在每个流内独立递增,不同流间的数据即使乱序到达也不相互影响。

    二、核心函数详解

    2.1 sctp_opt_info——获取SCTP选项的跨平台方案

    由于getsockopt无法可靠传递关联ID,SCTP提供了专用函数sctp_opt_info。getsockopt在SCTP中不可靠的原因在于:一到多套接字管理多个关联,而getsockopt无法指定要查询哪个关联。该函数可获取RTO参数、关联参数、状态信息等多种SCTP选项。

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/sctp.h>

    int sctp_opt_info(int sd, sctp_assoc_t id, int opt, void *arg, socklen_t *size);
    // 返回值:成功返回0,失败返回-1

    参数详解:

    参数说明
    sd 套接字描述符
    id 一到多式套接字中指定关联ID;一到一式忽略该参数
    opt 要获取的选项(SCTP_RTOINFO、SCTP_STATUS、SCTP_PEER_ADDR_PARAMS等)
    arg 选项特定结构缓冲区
    size 值-结果参数,输入缓冲区大小,返回实际写入大小

    支持的选项类型:

    选项用途
    SCTP_RTOINFO 获取RTO(重传超时)可调参数
    SCTP_ASSOCINFO 获取关联特定参数(最大重传次数、对端地址数、窗口大小)
    SCTP_STATUS 获取关联状态信息(外出流数等)
    SCTP_PEER_ADDR_PARAMS 获取对端地址参数(心跳间隔、重传次数、关联ID)

    应用示例:通过SCTP_PEER_ADDR_PARAMS选项将对端地址翻译为关联ID:

    struct sctp_paddrparams sp;
    bzero(&sp, sizeof(sp));
    memcpy(&sp.spp_address, sa, salen);
    sctp_opt_info(sock_fd, 0, SCTP_PEER_ADDR_PARAMS, &sp, &siz);
    return sp.spp_assoc_id; // 返回关联ID

    💡 关键理解:sctp_opt_info为那些不能使用getsockopt的系统提供了替代方案。对于一到多式套接字,必须指定关联ID来标识查询哪个关联。

    2.2 sctp_peeloff——剥离关联到独立套接字

    SCTP_PEELOFF分支出一个现有关联,生成一个一到一风格的新套接字,非常适合将高流量数据关联剥离出来单独处理。设计思想是:大多数短期请求仍由一到多套接字高效处理,长期高流量会话则剥离出来,避免阻塞其他关联。

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/sctp.h>

    int sctp_peeloff(int sd, sctp_assoc_t assoc_id);
    // 返回值:成功返回新的套接字描述符,失败返回-1

    剥离后的行为变化:

    变化项说明
    套接字类型 新套接字是一到一风格
    关联管理 该关联从原套接字移出,原套接字不再接收此关联的数据
    并发处理 新套接字可派发到专用线程/进程处理
    关联ID重用 关联关闭后ID可被重用

    三、完整源代码分析

    3.1 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]);

    // 创建一到多式SCTP套接字
    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));

    // 预订数据I/O事件,以获取sctp_sndrcvinfo结构
    bzero(&events, sizeof(events));
    events.sctp_data_io_event = 1; // 仅预订此事件,获取消息到达所在的流号
    Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events));

    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++;
    // 达到最大流号时归零
    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_EVENTS选项预订sctp_data_io_event,允许服务器从sri结构中获取消息到达所在的流号。

    3.2 SCTP回射客户端

    #include "unp.h"

    int main(int argc, char **argv)
    {
    int sock_fd;
    struct sockaddr_in servaddr;
    struct sctp_event_subscribe events;
    int rd_sz;
    char sendline[MAXLINE], recvline[MAXLINE];
    struct sctp_sndrcvinfo sri;
    int stream_no, msg_flags;

    if (argc != 2)
    err_quit("usage: sctpcli <IPaddress>");

    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(&events, sizeof(events));
    events.sctp_data_io_event = 1;
    Setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events));

    // 隐式关联建立:第一个sendto触发四路握手
    while (Fgets(sendline, MAXLINE, stdin) != NULL) {
    // 解析[#]格式,提取流号
    if (sscanf(sendline, "[%d]%s", &stream_no, sendline) != 2)
    err_quit("Invalid format: use [#]text");

    // 在指定流号上发送
    Sctp_sendmsg(sock_fd, sendline, strlen(sendline),
    (SA *)&servaddr, sizeof(servaddr),
    0, 0, stream_no, 0, 0);

    // 接收回射(包含sri信息)
    rd_sz = Sctp_recvmsg(sock_fd, recvline, sizeof(recvline),
    NULL, NULL, &sri, &msg_flags);
    recvline[rd_sz] = 0;
    printf("(strm=%d, ssn=%d): %s",
    sri.sinfo_stream, sri.sinfo_ssn, recvline);
    }

    Close(sock_fd);
    return 0;
    }

    💡 关键理解:流序列号(SSN)在特定流内唯一,不同流间可以重复,每条进入流的新消息都会获得一个递增的流序列号。

    四、关键图表

    4.1 SCTP服务器程序流程图

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ SCTP一到多式回射服务器流程图 │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ │
    │ ┌──────────────┐ │
    │ │ socket() │ ← AF_INET + SOCK_SEQPACKET + IPPROTO_SCTP │
    │ │ (创建套接字) │ 创建一到多式SCTP套接字 │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ bind() │ ← 绑定通配地址(INADDR_ANY) + 知名端口 │
    │ │ (绑定地址) │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ SCTP_EVENTS │ ← 预订sctp_data_io_event,获取sctp_sndrcvinfo │
    │ │ (预订事件) │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ listen() │ ← 转换为被动套接字 │
    │ │ (监听) │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ 无限循环 │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ sctp_recvmsg │ ← 接收消息,获取流号(存在sri.sinfo_stream) │
    │ │ (接收消息) │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ 流号递增处理 │ ← sri.sinfo_stream++ │
    │ │ (可选) │ 若超过最大流号,调用sctp_get_no_strms获取上限后归零 │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ sctp_sendmsg │ ← 在新流号上发回消息 │
    │ │ (回射数据) │ │
    │ └──────────────┘ │
    │ │ │
    │ └──────────────→ 循环继续等待下一条消息 │
    │ │
    └─────────────────────────────────────────────────────────────────────────────┘

    4.2 流号递增与SSN示意图

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ 流号递增与流序列号(SSN)示意图 │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ │
    │ 客户端流0 服务器回射到流1 │
    │ ┌─────────────┐ ┌─────────────┐ │
    │ │ 消息A(SSN=5)├───────────→│ 消息A(SSN=5)│ ← SSN保持 │
    │ └─────────────┘ └─────────────┘ │
    │ │
    │ 客户端流1 服务器回射到流2 │
    │ ┌─────────────┐ ┌─────────────┐ │
    │ │ 消息B(SSN=8)├───────────→│ 消息B(SSN=8)│ │
    │ └─────────────┘ └─────────────┘ │
    │ │
    │ 流号递增规律: │
    │ ┌───────────────────────────────────────────────────────────────────┐ │
    │ │ 输入流号 → 输出流号(输入流号 + 1) │ │
    │ │ 若达到最大流号(MAXSTREAMS)→ 归零 │ │
    │ └───────────────────────────────────────────────────────────────────┘ │
    │ │
    │ SSN特性:每个流独立维护自己的SSN计数器;流内SSN单调递增;不同流的SSN独立 │
    │ │
    │ 多流如何解决头端阻塞? │
    │ ┌───────────────────────────────────────────────────────────────────┐ │
    │ │ TCP单流:流1消息1丢失 → 流1消息2/3/4全部阻塞等待重传 │ │
    │ │ SCTP多流:流1消息1丢失 → 仅流1阻塞,流2/3/4正常递送 │ │
    │ └───────────────────────────────────────────────────────────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────────────────────┘

    4.3 sctp_get_no_strms函数实现

    该函数通过SCTP_STATUS套接字选项获取关联中商定的流数目:

    #include "unp.h"

    int sctp_get_no_strms(int sock_fd, struct sockaddr *to, socklen_t tolen)
    {
    int retsz;
    struct sctp_status status;

    retsz = sizeof(status);
    bzero(&status, sizeof(status));
    // 将对端地址转换为关联ID
    status.sstat_assoc_id = sctp_address_to_associd(sock_fd, to, tolen);
    Getsockopt(sock_fd, IPPROTO_SCTP, SCTP_STATUS, &status, &retsz);
    return (status.sstat_outstrms); // 返回外出流数目
    }

    五、SCTP通知与关联管理

    5.1 SCTP_ASSOC_CHANGE通知

    当关联状态发生变化时,应用程序会收到此通知。

    sac_state值含义
    SCTP_COMM_UP 新关联建立
    SCTP_COMM_LOST 连接中断(可能由于ABORT分节)
    SCTP_RESTART 关联重新启动
    SCTP_SHUTDOWN_COMPLETE 优雅关闭完成

    所有通知均提供sac_assoc_id字段,用于标识受影响的关联。

    5.2 关联终止方式对比

    方式方法行为
    优雅关闭 调用shutdown() 发送SHUTDOWN分节,等待对方确认
    立即终止 发送MSG_ABORT标志 立即发送ABORT分节,数据可能丢失
    关联结束 SCTP_EOF标志 优雅关闭当前关联

    六、本章练习题精解

    习题10.1

    问题:在dg_echo函数中,如果发送的数据报长度超过了接收缓冲区的大小,会发生什么?

    答案:数据报将被截断,超出部分被丢弃。应用程序只能收到缓冲区大小以内的数据,无法知道数据是否完整。

    习题10.2

    问题:为什么UDP服务器不需要调用listen和accept?

    答案:因为UDP是无连接的协议,不存在“建立连接”的概念。服务器只需创建UDP套接字并绑定端口,即可通过recvfrom接收任何客户发送的数据报。

    习题10.3

    问题:UDP套接字调用connect后,对发送和接收行为有什么影响?

    答案:三个变化:

  • 不能再使用sendto指定目的地址(或必须用NULL和0),改用write/send
  • 不需要使用recvfrom获取发送者地址,改用read/recv,且只能接收来自connect指定地址的数据报
  • 异步错误(如ICMP端口不可达)会返回给进程
  • 习题10.4

    问题:UDP客户端调用connect后,如何实现与多个不同服务器的通信?

    答案:可以多次调用connect。切换到新服务器只需再次调用connect,传入新服务器地址。要恢复到未连接状态,将connect第二个参数的sin_family设为AF_UNSPEC。

    习题10.5

    问题:为什么UDP没有发送缓冲区,而TCP有?

    答案:TCP需要可靠传输,发送数据必须保留在缓冲区中,直到收到ACK确认后才能删除。UDP是不可靠传输,发送后不需要保留数据副本,因此不需要实质的发送缓冲区。UDP写成功返回只表示数据已传递到链路层输出队列。

    七、本章小结

    7.1 核心知识点回顾

    知识点关键要点
    一到多套接字 使用SOCK_SEQPACKET,单套接字管理多个关联
    流号递增 输入流号+1,达到上限后归零
    sctp_opt_info 获取SCTP选项的跨平台方案,一到多套接字必须指定关联ID
    sctp_peeloff 将关联剥离为一到一套接字,便于分线程处理
    sctp_get_no_strms 通过SCTP_STATUS获取商定的最大流数
    SCTP通知 8种可预订事件,关联变化通过SCTP_ASSOC_CHANGE报告
    头端阻塞解决 多流使单个流的丢包不影响其他流的数据递送

    7.2 本章思维导图

    第十章 SCTP客户/服务器程序例子
    ├── 程序功能
    │ ├── 客户输入[#]text → 服务器在新流号上回射
    │ └── 客户打印流号和SSN
    ├── 服务器实现
    │ ├── socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP)
    │ ├── bind → listen → 循环
    │ ├── sctp_recvmsg获取流号和sri
    │ ├── 流号递增(可选)→ sctp_get_no_strms
    │ └── sctp_sendmsg回射
    ├── 客户端实现
    │ ├── 解析[#]提取流号
    │ ├── sctp_sendmsg发送
    │ ├── sctp_recvmsg接收
    │ └── 打印流号和SSN
    ├── 关键函数
    │ ├── sctp_opt_info(跨平台获取选项,需关联ID)
    │ ├── sctp_peeloff(剥离关联)
    │ ├── sctp_get_no_strms(获取最大流数)
    │ └── sctp_address_to_associd(地址→关联ID)
    └── SCTP优势总结
    ├── 多流 → 解决头端阻塞
    ├── 多宿 → 路径冗余
    └── 消息边界 → 保留记录边界

    八、下一章预告

    📌 下一篇:《UNIX网络编程》读书笔记(十一):第十一章 名字与地址转换

    第十一章将详细讲解:

  • 域名系统(DNS)基础:域名到IP地址的映射机制
  • gethostbyname和gethostbyaddr:经典主机名解析函数(已过时)
  • getaddrinfo函数:协议无关的名称到地址转换,支持IPv4/IPv6
  • getnameinfo函数:地址到名称的逆转换
  • 可重入性问题:经典函数的线程安全性问题和解决方案
  • 服务名与端口号转换:getservbyname和getservbyport
  • 学习目标:学完第十一章后,你将能够——

    • 编写协议无关的网络程序
    • 使用getaddrinfo正确处理IPv4和IPv6
    • 理解名称解析函数的可重入性及其影响
    • 通过服务名(如"http")而非硬编码端口号编写可移植程序

    敬请期待!

    参考资料

  • W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  • UNIX网络编程卷一 学习笔记 第十章 SCTP客户/服务器程序例子,CSDN,https://blog.csdn.net/tus00000/article/details/129591073/
  • UNIX网络编程卷一 学习笔记 第十章 SCTP客户/服务器程序例子,博客园,https://www.cnblogs.com/gblog6/p/17518071.html
  • sctp_opt_info(3) — Linux manual page,https://michaelkerrisk.com/linux/man-pages/man3/sctp_opt_info.3.html
  • sctp_peeloff(3) — HP-UX man page,https://www.unix.com/man-page/hpux/3/sctp_peeloff/
  • sctp_getnostrm.c — unpv13e源代码,Gitee,https://gitee.com/q723937936/unpv13e/blob/master/sctp/sctp_getnostrm.c
  • SCTP_OPT_INFO(3SOCKET),Oracle,https://docs.oracle.com/cd/E19253-01/819-7052/sockets-199/index.html
  • 23.8 Finding an Association ID Given an IP Address,UNP卷1在线版,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch23lev1sec8.html

  • 本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
    👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

    赞(0)
    未经允许不得转载:171主机测评 » 《UNIX 网络编程-卷1》阅读笔记11: SCTP客户/服务器程序例子
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址