最近工作需要开始研究一下基于SCTP协议通信的socket编程,在Ubuntu系统下写了一些示例程序来理解SCTP的通信过程。
1. Ubuntu下支持SCTP通信协议编程
1.1. 安装支持库
实际上Linux内核(2.6以后)早就已经支持了SCTP协议栈了,但是如果需要编写程序基于SCTP通信进行编程,还需要安装如下几个库
$ sudo apt-get install libsctp-dev lksctp-tools
- libsctp-dev库提供了用户空间访问内核SCTP协议栈的功能和接口
- lksctp-tools库提供了一些C语言头文件 (netinet/sctp.h) ,可以作为头文件调用以访问一些SCTP的功能,此外还提供了一些SCTP的工具。
1.2. 验证SCTP通信
安装成功后,可以开启两个命令行窗口,来验证一下SCTP通信
- Server端
sctp_darn -H 0 -P 2500 -l
- Client端
sctp_darn -H 0 -P 2600 -h 127.0.0.1 -p 2500 -s
Ubuntu系统下可以通过sctp_darn命令来通过SCTP首发消息
- -H:指定一个本地地址
- -P:指定一个要绑定的端口
- -h:指定对端地址
- -p:指定对端的端口
- -l:监听对端发送的消息并打印
- -s:发送消息到对端
sctp_darn的源代码gitlab上有,链接参看后面的参考【REF】
2. 一对一的通信场景(实例)
上图就是一个最简单的一对一通信的场景示例图。
2. Server端程序
前面有说过SCTP也是基于连接的通信协议,服务端建立的过程遵循这样的过程来建立起链接(和其他socket一致):
- socket(创建)
- bind(绑定到端口)
- listen(设置为被动监听)
- accept(阻塞等待连接请求,optional) , 连接建立,可以进行消息收发
只有在一对一的通信场景下,才需要通过accept()函数来建立连接(经过4次握手);
在一对多的通信场景下,不需要通过这个函数来建立连接 - close(关闭socket)
2.1 创建socket
使用socket()
函数创建一个socket,返回一个指向新socket的文件描述符,
socket( AF_INET, SOCK_STREAM, IPPROTO_SCTP );
- 通信协议设置为SCTP: IPPROTO_SCTP;
- 通信语义可以设置成数据流传输:SOCK_STREAM,也可以设置成有序报文:SOCK_SEQPACKET
- 对于数据流传输,一对一的通信,就和TCP一样,需要通过系统调用
accept()
来建立连接,同时返回一个新的处于连接状态的socket; - 对于有序报文,一对多的方式通信,不要调用
accpet()
,同时也就不需要对每个连接创建新的socket。单个socket可以代表多个关联(association),每个连接都有各自的ID(association ID);
2.2 设置socket的属性
使用setsockopt来按照需求设置socket的一些属性。
例如,我们要使能每次接受消息时候返回sctp_sndrcvinfo信息,可以如下修改socket属性
struct sctp_event_subscribe evnts;
bzero(&evnts, sizeof(evnts));
evnts.sctp_data_io_event = 1;
setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts))
sctp_sndrcvinfo结构体中定义了一些字段,和SCTP通信相关,可以用来给服务端获取所需的信息,比如说指定新的用于发送消息的默认流。
2.3 绑定socket到端口
使用系统调用bind()
来进行这样的绑定,代码如下,
struct sockaddr_in servaddr;
bzero( (void *)&servaddr, sizeof(servaddr) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons(MY_PORT_NUM);
sctp_bindx( sock_fd, (struct sockaddr *)&servaddr, addr_count, SCTP_BINDX_ADD_ADDR)
- AF_INET代表使用IPv4进行通信;
- INADDR_ANY代表任意IP地址,如果当前主机作为server端,需要给socket绑定一个本地端口,用来监听client端发送的消息。这个时候应用程序可以不用知道当前的主机的IP地址(主机有可能有多个IP,也有可能有动态发生改变),这样就可以用INADDR_ANY来代表任意IP地址;
- 绑定的端口号自己指定;
2.4 设置socket为监听状态
使用系统调用listen()
来把socket设置为被动监听状态,参看如下代码
listen( sock_fd, LISTENQ);
- LISTENQ是一个自定义宏,可以自行设定socket的等待连接队列的长度
2.5 阻塞等待连接请求(一对一通信场景)
在一对一通信场景下,socket的通信语义设置为SOCK_STREAM;使用系统调用accept()
,返回一个新的处于连接状态的socket。
struct sockaddr_in client_addr;
conn_sockfd = accept(list_sockfd, (struct sockaddr*)&client_addr, &len);
accetp函数返回一个新的处于连接状态的socket。
2.6 阻塞等待socket上的数据接收
使用函数sctp_recvmsg
会阻塞等待socket上的接收数据,
int rz = sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),
&cliaddr, &len, &sri, &msg_flags);
2.7 关闭处于连接状态的socket
如果SCTP客户端关闭了socket,会先发送一个EOF通知给服务端。服务端会受到一条长度为0的消息,依据这个可以判断客户端发起了连接断开,这个时候服务端也会关闭对应的socket,
if (rz == 0)
{
printf("[Server]: EOF received!\n");
return;
}
...
close(conn_sockfd);
2.8 完整的服务端程序
//SCTP Server
#include
#include
#include
#include
#include
#include
#include
#include "common.h"
#include
#include
int Socket()
{
/* Create SCTP TCP-Style Socket */
return socket( AF_INET, SOCK_STREAM, IPPROTO_SCTP );
}
void Bind(int sock_fd)
{
struct sockaddr_in servaddr;
int addr_count = 1;
/* Accept connections from any interface */
bzero( (void *)&servaddr, sizeof(servaddr) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons(MY_PORT_NUM);
// replace bind with sctp_bindx
if(-1 == sctp_bindx( sock_fd, (struct sockaddr *)&servaddr, addr_count, SCTP_BINDX_ADD_ADDR))
{
printf("bind socket failed: %s \n", strerror(errno));
return;
}
}
int SetSocketOpt(int sock_fd)
{
struct sctp_event_subscribe evnts;
bzero(&evnts, sizeof(evnts));
evnts.sctp_data_io_event = 1;
if(0 != setsockopt(sock_fd, IPPROTO_SCTP, SCTP_EVENTS, &evnts, sizeof(evnts)))
{
printf("Error setsockopt(IPPROTO_SCTP): %s\n", strerror(errno));
return 0;
}
return 1;
}
void Listen(int sock_fd)
{
int listenQueLen = 5;
listen( sock_fd, listenQueLen);
}
int Accept(int list_sockfd)
{
int len = sizeof(struct sockaddr_in);
struct sockaddr_in client_addr;
int conn_sockfd;
printf("[Server]: Awaiting a new connection\n");
conn_sockfd = accept(list_sockfd, (struct sockaddr*)&client_addr, &len);
printf("[Server]: Connection Established\n");
return conn_sockfd;
}
void SctpRecevMsg(int sock_fd)
{
int msg_flags;
size_t rd_sz;
char readbuf[MAX_BUFFER+1];
struct sockaddr_in cliaddr;
struct sctp_sndrcvinfo sri;
socklen_t len;
len = sizeof(struct sockaddr_in);
while(1)
{
int rz = sctp_recvmsg(sock_fd, readbuf, sizeof(readbuf),
&cliaddr, &len, &sri, &msg_flags);
if (rz == 0)
{
printf("[Server]: EOF received!\n");
return;
}
else if(rz < 0)
{
printf("[Server]: Error sctp_recvmsg: %s\n", strerror(errno));
return;
}
printf("[Server]: Message(size = %d): '%s' received \n", rz , readbuf);
}
}
void Close(int sock_fd)
{
close(sock_fd);
printf("[Server]: Close connect socket!\n");
}
int main()
{
int list_sockfd, conn_sockfd;
list_sockfd = Socket();
Bind(list_sockfd);
SetSocketOpt(list_sockfd);
Listen(list_sockfd);
/* Server loop... */
while( 1 ) {
int conn_sockfd = Accept(list_sockfd);
SctpRecevMsg(conn_sockfd);
Close(conn_sockfd);
}
return 0;
}
3. Client端程序
客户端的建立连接的过程如下
- socket(创建)
- connect(发起到server端的连接,optional),连接成功后,可以进行消息收发
- close(关闭socket)
3.1 Socket建立
和Server端一样的建立方式
3.2 设置socket的属性
可以设置socket的输入输出流的最大个数,
struct sctp_initmsg initmsg;
initmsg.sinit_num_ostreams = 5;
initmsg.sinit_max_instreams = 5;
initmsg.sinit_max_attempts = 4;
setsockopt( sock_fd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(initmsg) );
3.3 发起到server端的连接
发起到server端地址的连接
struct sockaddr_in servaddr;
sctp_assoc_t assoc_id;
/* Specify the peer endpoint to which we'll connect */
bzero( (void *)&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MY_PORT_NUM);
servaddr.sin_addr.s_addr = inet_addr( "127.0.0.1" );
sctp_connectx(sock_fd, (struct sockaddr *)&servaddr, 1, &assoc_id)
3.4 发送消息到服务端
struct sctp_sndrcvinfo sinfo;
bzero(&sinfo, sizeof(sinfo));
sinfo.sinfo_stream = 1;
sctp_send(sock_fd, buf, strlen(buf), &sinfo, 0);
这里设置在流1上发送消息。
3.5 完整的客户端代码
//SCTP client
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "common.h"
#include
int Socket()
{
return socket( AF_INET, SOCK_STREAM, IPPROTO_SCTP );
}
void SetSocketOpt(int sock_fd)
{
struct sctp_initmsg initmsg;
struct sctp_event_subscribe events;
int opt = 1;
int len = sizeof(opt);
/* Specify that a maximum of 5 streams will be available per socket */
memset( &initmsg, 0, sizeof(initmsg) );
initmsg.sinit_num_ostreams = 5;
initmsg.sinit_max_instreams = 5;
initmsg.sinit_max_attempts = 4;
if(0 != setsockopt( sock_fd, IPPROTO_SCTP, SCTP_INITMSG,
&initmsg, sizeof(initmsg) ))
{
printf("[Client]: error setsocketopt IPPROTO_SCTP, %s\n", strerror(errno));
return;
}
/* Enable receipt of SCTP Snd/Rcv Data via sctp_recvmsg */
memset( (void *)&events, 0, sizeof(events) );
events.sctp_data_io_event = 1;
events.sctp_association_event = 1;
if(0 != setsockopt( sock_fd, SOL_SCTP, SCTP_EVENTS,
(const void *)&events, sizeof(events) ))
{
printf("[Client]: error setsocketopt SOL_SCTP, %s\n", strerror(errno));
return;
}
}
void Connect(int sock_fd)
{
struct sockaddr_in servaddr;
sctp_assoc_t assoc_id;
/* Specify the peer endpoint to which we'll connect */
bzero( (void *)&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MY_PORT_NUM);
servaddr.sin_addr.s_addr = inet_addr( "127.0.0.1" );
if(-1 != sctp_connectx(sock_fd, (struct sockaddr *)&servaddr, 1, &assoc_id))
{
printf("[Client]: Connection established, id = %x\n", assoc_id);
return;
}
}
void SendMsg(int sock_fd)
{
char buf[20];
struct sctp_sndrcvinfo sinfo;
printf("[Client]: Input message to send to server!\n");
if(NULL != gets(buf))
{
bzero(&sinfo, sizeof(sinfo));
sinfo.sinfo_stream = 1;
sctp_send(sock_fd, buf, strlen(buf), &sinfo, 0);
}
}
void Close(int sock_fd)
{
char buf[20];
printf("[Client]: Enter to close socket!\n");
if(NULL != gets(buf))
{
/* Close our socket and exit */
close(sock_fd);
printf("[Client]: Close socket!\n");
return;
}
}
int main()
{
int sock_fd;
/* Create an SCTP TCP-Style Socket */
sock_fd = Socket();
SetSocketOpt(sock_fd);
Connect(sock_fd);
SendMsg(sock_fd);
Close(sock_fd);
return 0;
}
实际的运行过程
- 启动server
server端打印,
[Server]: Awaiting a new connection...
- 启动Client端
Client端创建好socket后,发起连接请求,Server端收到请求,accept返回成功,打印日志
[Server]: Connection Established
Client端收到Server端的ACK,connect函数返回成功,打印日志
[Client]: Connection Established, id = 32
- Client端发送消息
Client阻塞等待用户输入的发送消息,用户输入"hello",并回车
[Client]: Input something to be sent...
hello
Server端会收到这条消息,
[Server]: Message(size = 5): 'hello' received
- Client端关闭socket
用户点击回车继续,
[Client]: Enter to close socket...
- Server端关闭socket
Server端会收到一条EOF消息
[Server]: EOF received!
然后Server退出循环,关闭socket,
[Server]: Close connect socket!
[REF]
sctp_darn源码