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


一、章节概述

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

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

  1. 客户从标准输入读入一行文本,格式为[#]text(方括号中的数字表示要在这个流号上发送该文本消息)
  2. 服务器接收后,将接收消息的流号加1,然后在新流号上发回
  3. 客户读取回射行并打印,同时列出**流号、流序列号(SSN)**和新流号

这个简单的程序背后,蕴含了SCTP多流特性的核心价值:解决头端阻塞问题。流序列号在每个流内独立递增,不同流间的数据即使乱序到达也不相互影响。

二、核心函数详解

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

由于getsockopt无法可靠传递关联ID,SCTP提供了专用函数sctp_opt_infogetsockopt在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服务器不需要调用listenaccept

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

习题10.3

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

答案:三个变化:

  1. 不能再使用sendto指定目的地址(或必须用NULL和0),改用write/send
  2. 不需要使用recvfrom获取发送者地址,改用read/recv,且只能接收来自connect指定地址的数据报
  3. 异步错误(如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网络编程》读书笔记(十一):第十一章 名字与地址转换

第十一章将详细讲解:

  1. 域名系统(DNS)基础:域名到IP地址的映射机制
  2. gethostbyname和gethostbyaddr:经典主机名解析函数(已过时)
  3. getaddrinfo函数:协议无关的名称到地址转换,支持IPv4/IPv6
  4. getnameinfo函数:地址到名称的逆转换
  5. 可重入性问题:经典函数的线程安全性问题和解决方案
  6. 服务名与端口号转换getservbynamegetservbyport

学习目标:学完第十一章后,你将能够——

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

敬请期待!

参考资料

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

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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐