Unix网络编程(4):基本UDP/SCTP套接字编程
1. 基本UDP
套接字编程
1.1 recvfrom
和sendto
函数
$recvfrom$ 和 $sendto$ 类似于标准 $read$ 和 $write$ 函数,不过需要三个额外的参数。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);
// 成功则返回读或写的字节数,出错返回-1
$sendto$ 的 $to$ 参数指向一个含有数据报接收者的协议地址的套接字地址结构,其大小由 $addrlen$ 指定。$recvfrom$ 的 $from$ 参数指向一个将由该参数在返回时填写数据报发送者的协议地址的套接字地址结构,其大小由 $addrlen$ 返回给调用者。写一个长度为 $0$ 的数据报是可行的。在UDP
情况下,这回形成一个只包含一个IP
首部和一个 $8$ 字节UDP
首部而没有数据的IP
数据报。这也意味着对于数据报协议,$recvfrom$ 返回 $0$ 值是可以接受的,并不像TCP
一样代表对端连接关闭。如果 $recvfrom$ 的 $from$ 参数是一个空指针,那么相应的长度参数 $addrlen$ 也必须是一个空指针。
UDP
输出操作成功仅仅表示在接口输出队列中具有存放所形成IP
数据报的空间,而错误会在之后返回,称为异步错误 ( $asynchronous$ $error$ )。一个基本规则是:对于一个UDP
套接字,由它引发的异步错误并不返回给它,除非它已连接。考虑在单个UDP
套接字上接连发送 $3$ 个数据报给 $3$ 个不同的服务器的一个UDP
客户,其中有 $2$ 个数据报被正确递送,但是第三个主机没有运行服务器,于是返回一个ICMP
端口不可达错误。发送这 $3$ 个数据报的客户需要知道引发该错误的数据报的目的地址以区分究竟是哪一个数据报引发错误,如果 $recvfrom$ 仅仅设置 $errno$ ,则无法返回出错数据报的IP
地址的端口。因此,仅在进程已将其UDP
套接字连接到一个对端之后,这些异步错误才会返回给进程。
1.2 connect
函数
我们可以给UDP
套接字调用 $connect$ ,然而这样做的结果与TCP
连接大相径庭:没有三次握手,而是更类似于指定对端地址。内核只是检查是否存在立即可知的错误,记录对端的IP
地址和端口号,然后立即返回到调用进程。对于已连接的UDP
套接字,与默认未连接的UDP
套接字相比,有以下变化:
- 不能给输出操作指定目的
IP
地址和端口号。也就是说,不使用 $sendto$ ,而是改为使用 $write$ 或 $send$ 。写到已连接UDP
套接字上的任何内容都会自动发送到由 $connect$ 指定的协议地址; - 不必使用 $recvfrom$ 获取数据报,而是使用 $read$ 、$recv$ 或 $recvmsg$ 。在一个已连接
UDP
套接字上,由内核为输入操作返回的数据报只有那写来自 $connect$ 所指定协议地址的数据报。目的地为这个已连接UDP
套接字的本地协议地址,发出地不是该套接字早先连接到的协议地址的数据报,不会被投递到该套接字。这就限制了一个已连接UDP
套接字只能与一个对端交换数据; - 由已连接
UDP
套接字引发的异步错误会返回给它们所在的进程,未连接UDP
套接字不会接收任何异步错误。
拥有一个已连接UDP
套接字的进程可出于以下两个目的之一再次调用 $connect$ :
- 指定新的
IP
地址和端口号; - 断开套接字,再次调用 $connect$ 时把套接字地址结构的地址族成员设置为 $AF_-UNSPEC$ ,可能会返回 $EANNOSUPPORT$ 错误,但是没有关系。
已连接UDP
套接字还可以用来确定用于某个特定目的地的外出接口,这是由 $connect$ 函数应用到UDP
套接字时的一个副作用造成的:内核选择本地IP
地址 ( 假设其进程未曾调用 $bind$ 进行显式指派 )。这个IP
地址通过为目的IP
地址搜索路由表得到外出接口,然后选用该接口的主IP
地址而选定。当UDP
套接字连接完成后,调用 $getsockname$ 即可得到本地IP
地址和端口号。
2. 基本SCTP
套接字编程
2.1 接口模型
SCTP
套接字分为一到一套接字和一到多套接字。一到一套接字对应一个单独的SCTP
关联,这种映射类似于TCP
套接字和TCP
连接的对应关系。对于一到多套接字,一个给定套接字上可以同时有多个活跃的SCTP
关联,这种映射类似于绑定了某个特定端口的UDP
套接字能够从若干个同时在发送数据的远程UDP
端点接收彼此交错的数据报。
2.1.1 一到一形式
开发一到一形式的目的是方便将现有的TCP
应用程序移植到SCTP
上。以下是这两者之间的差异:
- 任何
TCP
套接字选项必须转换成等效的SCTP
套接字选项; SCTP
保存消息边界,因而应用层消息边界并非必须。举例来说,基于TCP
的某个应用协议可能先执行一个双字节的 $write$ 系统调用,给出消息的长度 $x$ ,再调用一个 $x$ 字节的系统调用,写出消息数据本身。改用SCTP
,接收端SCTP
将收到两个独立的消息;- 有些
TCP
应用进程使用半关闭来告知对端去往它的数据流已经结束。将这样的应用程序移植到SCTP
需要额外重写应用层协议,让应用进程在应用数据流中告知对端该传输数据流已经结束; - $send$ 函数能够以普通方式使用,使用 $sendto$ 或 $sendmsg$ 函数时,指定的任何地址都被认为是对目的地址的重写。
2.1.2 一到多形式
一到多形式给应用程序开发人员提供这样的能力:编写的服务器程序无需管理大量的套接字描述符。单个套接字描述符将代表多个关联,就像一个UDP
套接字能从多个客户接收消息那样。在一到多套接字上,用于标识单个关联的是一个关联标识 ( $association$ $identifier$ )。关联标识是一个类型为 $sctp_-assoc_-t$ 的值,通常是一个整数。它是一个不透明的值,应用进程不应该使用不是由内核先前给予的任何关联标识。一到多套接字的用户要注意以下几点:
- 当一个客户关闭其关联时,其服务器也将自动关闭同一个关联,服务器主机内核中不再有该关联的状态;
- 可用于致使在四次握手的第三个或第四个分组中捎带用户数据的唯一办法就是使用一到多形式;
- 对于一个与它还没有关联存在的
IP
地址,任何以它为目的地的 $sendto$ 、$sendmsg$ 或 $sctp_-sendmsg$ 将导致对主动打开的尝试,如果成功的话将建立一个与该地址的关联; - 用户必须使用 $sendto$ 、$sendmsg$ 或 $sctp_-sendmsg$ 这 $3$ 个分组发送函数,而不能使用 $send$ 或 $write$ 这 $2$ 个分组发送函数,除非已经使用 $sctp_-peeloff$ 函数从一个一到多套接字剥离出一个一到一套接字;
- 任何时候调用其中任何一个分组发送函数时,所用的目的地址是由系统在关联建立阶段选定的主目的地址,除非调用者在所提供的 $sctp_-sndrcvinfo$ 结构中设置了 $MSG_-ADDR_-OVER$ 标志。为了提供这个结构,调用者必须使用伴随辅助数据的 $sendmsg$ 或 $sctp_-sendmsg$ 函数;
- 关联事件可能被启用,因此要是应用进程不希望收到这些事件,就得使用 $SCTP_-EVENTS$ 套接字选项显式禁止它们。默认情况下启用的唯一事件是 $sctp_-data_-io_-event$ ,它给 $recvmsg$ 和 $sctp_-recvmsg$ 调用提供辅助数据。
2.2 函数
#include <netinet/sctp.h>
// 成功返回0, 出错返回-1
int sctp_bindx(int sockfd, const struct sockaddr *addrs, int addrcnt, int flags);
SCTP
服务器可能希望绑定与所在主机系统相关IP
地址的一个子集。$sctp_-bindx$ 函数允许SCTP
套接字捆绑一个特定地址子集。$flags$ 参数指定两种行为:$SCTP_-BINDX_-ADD_-ADDR$ 为往套接字中添加地址,$SCTP_-BINDX_-REM_-ADDR$ 为从套接字中移除地址。
#include <netinet/sctp.h>
// 成功返回0,出错返回-1
int sctp_connectx(int sockfd, const struct sockaddr *addrs, int addrcnt);
$sctp_-connectx$ 函数用于连接到一个多宿对端主机。
#include <netinet/sctp.h>
// 成功返回对端地址数,出错返回-1
int sctp_getpaddrs(int sockfd, sctp_assoc_t id, struct sockaddr **addrs);
$getpeername$ 不是为支持多宿概念的传输协议设计的,当用于SCTP
时仅返回主目地址。如果需要知道对端的所有地址,应该使用 $sctp_-getpaddrs$ 。
#include <netinet/sctp.h>
void sctp_freepaddrs(struct sockaddr *addrs);
$sctp_-freepaddrs$ 函数释放由 $sctp_-getpaddrs$ 函数分配的资源。
#include <netinet/sctp.h>
// 成功返回本端地址数,出错返回-1
int sctp_getladdrs(int sockfd, sctp_assoc_t id, struct sockaddr **addrs);
$sctp_-getladdrs$ 函数用于获取属于某个关联的本地地址。
#include <netinet/sctp.h>
void sctp_freeladdrs(struct sockaddr *addrs);
$sctp_-freeladdrs$ 函数释放由 $sctp_-getladdrs$ 函数分配的资源。
#include <netinet/sctp.h>
// 成功返回所写字节数,出错返回-1
ssize_t sctp_sendmsg(int sockfd, const void *msg, size_t msgsz,
const struct sockaddr *to, socklen_t tolen, uint32_t ppid,
uint32_t flags, uint16_t stream, uint32_t timetolive, uint32_t context);
$sctp_-sendmsg$ 的使用者以指定更多参数为代价简化了发送方法。$ppid$ 参数指定将随数据块传递的净荷协议标识符,$flags$ 参数将传递给SCTP
栈,用以标识任何SCTP
选项。调用者可以在 $stream$ 指定一个SCTP
流号,可以在 $lifetime$ 参数中以毫秒为单位指定消息的生命期,其中 $0$ 表示无限生命期。$context$ 参数用于指定可能有的用户上下文。
#include <netinet/sctp.h>
// 成功返回所读字节数,出错返回-1
ssize_t sctp_recvmsg(int sockfd, void *msg, size_t msgz, struct sockaddr *from,
socklen_t *fromlen, struct sctp_sndrcvinfo *sinfo, int msg_flags);
与$sctp_-sendmsg$ 一样,$sctp_-recvmsg$ 也为SCTP
高级特性提供一个更方便用户的接口。使用本函数不仅能获取对端的地址,也能获取通常伴随 $recvmsg$ 函数调用返回的 $msg_-flags$ 参数。本函数也允许获取已读入消息缓冲区中的伴随所接收消息的 $sctp_-sndrcvinfo$ 结构。$msg_-flags$ 参数中存放可能有的消息标志。
#include <netinet/sctp.h>
// 成功返回0,出错返回-1
int sctp_opt_info(int sockfd, sctp_assoc_t assoc_id, int opt,
void *arg, socklen_t *siz);
$sctp_-opt_-info$ 函数是为无法为SCTP
使用 $getsockopt$ 函数的那写实现提供的。$assoc_-id$ 参数给出可能存在的关联标识。$opt$ 参数是SCTP
套接字选项。$arg$ 给出套接字选项参数。
#include <netinet/sctp.h>
// 成功返回一个新的套接字描述符,出错返回-1
int sctp_peeloff(int sockfd, sctp_assoc_t id);
$sctp_-peeloff$ 从一个一到多套接字中抽取一个关联,构成单独一个一到一套接字。
2.3 shutdown
函数
由于SCTP
设计成不提供半关闭状态,所以 $shutdown$ 的行为不同于TCP
。当相互通信的两个SCTP
端点中任意一个发起关联终止序列时,这两个端点都得把已排队的任何数据发送掉,然后关闭关联。关联主动打开的发起端点改用 $shutdown$ 而不是 $close$ 的可能原因是:同一个端点可用于连接到一个新的对端端点。SCTP
允许一个端点调用 $shutdown$ ,$shutdown$ 结束之后,这个端点就可以重用原套接字连接到一个新的对端。注意,如果这个端点没有等到SCTP
关联终止序列结束,新的连接就会失败。