《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
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)