Unix下基于SCTP socket的通信:一对一场景

最近工作需要开始研究一下基于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通信

  1. Server端
sctp_darn -H 0 -P 2500 -l
  1. 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. 一对一的通信场景(实例)

Unix下基于SCTP socket的通信:一对一场景_第1张图片

上图就是一个最简单的一对一通信的场景示例图。

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 );
  1. 通信协议设置为SCTP: IPPROTO_SCTP;
  2. 通信语义可以设置成数据流传输: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)
  1. AF_INET代表使用IPv4进行通信;
  2. INADDR_ANY代表任意IP地址,如果当前主机作为server端,需要给socket绑定一个本地端口,用来监听client端发送的消息。这个时候应用程序可以不用知道当前的主机的IP地址(主机有可能有多个IP,也有可能有动态发生改变),这样就可以用INADDR_ANY来代表任意IP地址;
  3. 绑定的端口号自己指定;

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;
}

实际的运行过程

  1. 启动server
    server端打印,
[Server]: Awaiting a new connection...
  1. 启动Client端
    Client端创建好socket后,发起连接请求,Server端收到请求,accept返回成功,打印日志
[Server]: Connection Established

Client端收到Server端的ACK,connect函数返回成功,打印日志

[Client]: Connection Established, id = 32
  1. Client端发送消息

Client阻塞等待用户输入的发送消息,用户输入"hello",并回车

[Client]: Input something to be sent...
hello

Server端会收到这条消息,

[Server]: Message(size = 5): 'hello' received
  1. Client端关闭socket

用户点击回车继续,

[Client]: Enter to close socket...
  1. Server端关闭socket

Server端会收到一条EOF消息

[Server]: EOF received!

然后Server退出循环,关闭socket,

[Server]: Close connect socket!

[REF]

sctp_darn源码

你可能感兴趣的:(Unix下基于SCTP socket的通信:一对一场景)