UNP总结 Chapter 22~25 高级UDP套接字编程、高级SCTP 套接字编程、带外数据、信号驱动I/O

|▌冷眼眸甩不掉的悲伤 提交于 2020-02-05 10:54:25

一、高级UDP套接字编程

1.接收标志、目的IP地址和接口索引

作为recvmsg的一个例子,我们将要写一个名为recvfrom_flags的函数,它与recvfrom类似,但他还返回:

  • 返回的msg_flags值
  • 收到的数据报的目的地址(通过设置IP_RECVDSTADDR套接口选项)
  • 接收数据报接口的索引(通过设置IP_RECIF套机口选项)

相关详细代码 见UNP P463

 

2.何时用UDP代替TCP

  • 使用广播或者多播时候,因为UDP支持广播或多播
  • 类似实时音频应用的程序应使用UDP
  • 对于简单的请求-应答应用程序应使用UDP
  • 对于海量数据传输(例如文件传输)不应该使用UDP

 

3.给UDP应用增加可靠性

如果我们想要在请求-应答式应用程序中使用UDP,那么我们必须对我们的客户增加两个特性:

  • 超时和重传以处理丢失的数据报
  • 序列号,这样客户可以验证一个应答是对应相应的请求的

这两个特性是多数使用简单的请求-应答范例的现有UDP应用程序的一部分:例如DNS解析器,SNMP代理,TFTP和RPC。

加入序列号比较简单。客户给每个请求附加一个序列号,并且服务器必须在应答中给客户返回这个号,这样可以让客户验证给定的应答是对应所发请求的应答。

老式的处理超时和重传的方法是发送一个请求后等待N秒。如果没有收到应答,则重传并再等待另外N秒。这种情况发生一定次数后放弃。这是一种线性重传定时器。这种方法的问题是数据报在一个互联网上往返的时间会从LAN上的远远不到一秒变到WAN上的许多秒。影响往返时间(RTT)的因素是距离、网速、拥塞。我们必须采用一个将实际测得的RTT以及RTT随时间的变化考虑在内的超时和重传算法。

 

“重传二义性问题”,当重传定时器超时时,三种可能的情形:

1). 请求丢失了

2). 应答丢失了

3). RTO太小

 

 

4.并发UDP服务器

当使用TCP时,能够简化服务器并发性的根源在于每个客户连接都是唯一的,也就是说TCP套接口对于每个连接多是唯一的。然而对于UDP,我们必须处理两个不同类型的服务器。

1). 第一种是简单的UDP服务器,它读入一个客户请求,发送应答,接着与这个客户就无关了。在这种情形里,读客户请求的服务器可以fork一个子进程去处理请求。“请求”(也就是数据报的内容和保存在客户协议地址中的套接口地址结构)通过从fork得来的内存映像传递给子进程。子进程接着直接给客户发送它的应答。

2). 第二种是与客户交换多个数据报的UDP服务器。问题是客户只知道服务器的端口是服务器众所周知的端口。客户发送请求的第一个数据报到这个端口,但是服务器又怎么能区分这是那个客户的后继数据报还是新的请求呢?这种问题的典型解决方法是让服务器给每个客户创建一个新的套接字,bind一个临时端口,然后使用该套接字并发送对该客户的所有应答。

 

下图给出inet激发UDP并发服务器所涉及步骤:

 

 

5.IPv6分组信息

IPv6允许应用程序对外出的数据报指定最多四条信息:

  • 源IPv6地址
  • 外出接口索引
  • 外出跳限
  • 下一跳地址

这些信息是作为辅助数据使用sendmsg发送的。对于收到的分组可以返回三条类似的信息,他们是作为辅助数据由recvmsg返回的:

  • 目的IPv6地址
  • 到达接口索引
  • 到达跳限

 

 

 

二、高级SCTP 套接字编程

涉及的主要内容

1.部分传递

2.无序的数据

3.捆绑地址子集

4.确定对端和本端地址信息

5.心搏和地址不可达

6.关联剥离

7.定时控制

 

 

 

 

三、带外数据

1.概述

带外数据被认为具有比普遍数据更高的优先级。带外数据并不要求在客户和服务器之间在使用一个连接,而是被映射到已有的连接中

 

2.TCP带外数据

TCP没有真正的带外数据,而是提供了一个我们要讨论的紧急模式(urgent mode)。假设一个进程已向一个TCP套接口写入了N字节数据,并且这些数据被TCP放入套接口发送缓冲区等待发送给对方。下图展示了这种状态,并且标记了从1到N的数据字节。

进程现在使用send函数和MSG_OOB标志发送一个包含ASCII字符a的带外数据字节:

send(fd, "a", 1, MSG_OOB);

TCP将数据放置在套机口发送缓冲区的下一个可用位置,并设置这个连接的TCP紧急指针(urgent pointer)为下一个可用位置。下图中展示了这种状态,并且标记带外字节为“OOB”

 

3.sockatmark函数

每当接收到带外数据时,就有一个相关联的带外标记。这是发送进程发送带外字节时在发送方普通数据流中的位置。接收进程读套接口时通过调用sockatmark函数确定是否在带外标记上。

#include <sys/socket.h>
 
int sockatmark(int sockfd) ;
//返回值:如果在带外标记上为1, 不在标记上为0, 出错为-1 

 

下面给出了使用的SIOCATMARK ioctl完成本函数的一个实现

#include    "unp.h"

int
sockatmark(int fd)
{
    int     flag;

    if (ioctl(fd, SIOCATMARK, &flag) < 0)
        return (-1);
    return (flag != 0);
}

不管接收进程在线(SO_OOBINLINE套接口选项)或是带外(MSG_OOB标志)接收带外数据,带外标记都能使用。

 

实例:

发送程序

#include    "unp.h"

int
main(int argc, char **argv)
{
    int     sockfd;

    if (argc != 3)
        err_quit("usage: tcpsend04 <host> <port#>");

    sockfd = Tcp_connect(argv[1], argv[2]);

    Write(sockfd, "123", 3);
    printf("wrote 3 bytes of normal data\n");

    Send(sockfd, "4", 1, MSG_OOB);
    printf("wrote 1 byte of OOB data\n");

    Write(sockfd, "5", 1);
    printf("wrote 1 byte of normal data\n");

    exit(0);
}

 

调用sockatmark接受程序

#include    "unp.h"

int
main(int argc, char **argv)
{
    int     listenfd, connfd, n, on = 1;
    char    buff[100];

    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], NULL);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], NULL);
    else
        err_quit("usage: tcprecv04 [ <host> ] <port#>");

    Setsockopt(listenfd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on));

    connfd = Accept(listenfd, NULL, NULL);
    sleep(5);

    for ( ; ; ) {
        if (Sockatmark(connfd))
            printf("at OOB mark\n");

        if ( (n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {
            printf("received EOF\n");
            exit(0);
        }
        buff[n] = 0;            /* null terminate */
        printf("read %d bytes: %s\n", n, buff);
    }
}

 

 

 

 

四、信号驱动I/O

1.概述

信号驱动是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。需要注意的是这里描述的信号驱动不是真正的异步I/O。

注意第16章描述的非阻塞I/O同样不是异步I/O。对于非阻塞I/O,内核一旦启动,I/O操作就不像异步I/O那样立即返回到进程,而是等到I/O操作完成或遇到错误;内核立即返回的唯一条件是I/O操作的完成不得不把进程投入睡眠,这种情况下内核不启动I/O操作

 

2.套接字的信号驱动式I/O

针对一个套接字使用信号驱动I/O(SIGIO) 要求进程执行以下三个步骤:

1). 给SIGIO信号建立信号处理程序

2). 设置套接口属主,通常使用fcntl的F_SETOWN命令

3). 激活套接口的信号驱动I/O,通常使用fcntl的F_SETFL命令打开O_ASYNC标志

 

3.UDP套接字的SIGIO信号

UDP上使用信号驱动I/O是简单的。当下述事件发生时产生SIGIO信号:

  • 数据报到达套接字
  • 套接口上发生异步错误

因此当捕获到SIGIO信号时,调用recvfrom或者读入到达的数据报或者获取发生的异步错误。

 

 

4.TCP套接字的SIGIO信号

不幸的是,信号驱动I/O对TCP套接字几乎是没用的,问题在于是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。

下列条件均可在TCP套接口上产生SIGIO信号(假设信号驱动I/O是使能的):

  • 在监听套接口上有一个连接请求已经完成
  • 发起了一个连接拆除请求
  • 一个连接拆除请求已经完成
  • 一个连接的一半已经关闭
  • 数据到达了套接字
  • 数据已从套接字上发出(即输出缓冲区有空闲时间)
  • 发生了一个异步错误

 

5.SIGIO的UDP回射服务器程序

1)全局声明

#include    "unp.h"

static int sockfd;

#define QSIZE     8             /* size of input queue */
#define MAXDG  4096             /* max datagram size */

typedef struct {
    void   *dg_data;            /* ptr to actual datagram */
    size_t  dg_len;             /* length of datagram */
    struct sockaddr *dg_sa;     /* ptr to sockaddr{} w/client's address */
    socklen_t dg_salen;         /* length of sockaddr{} */
} DG;
static DG dg[QSIZE];            /* queue of datagrams to process */
static long cntread[QSIZE + 1]; /* diagnostic counter */

static int iget;                /* next one for main loop to process */
static int iput;                /* next one for signal handler to read into */
static int nqueue;              /* # on queue for main loop to process */
static socklen_t clilen;        /* max length of sockaddr{} */

static void sig_io(int);
static void sig_hup(int);

 

2)dg_echo函数:服务器主处理循环

void
dg_echo(int sockfd_arg, SA *pcliaddr, socklen_t clilen_arg)
{
    int     i;
    const int on = 1;
    sigset_t zeromask, newmask, oldmask;

    sockfd = sockfd_arg;
    clilen = clilen_arg;

    for (i = 0; i < QSIZE; i++) {    /* init queue of buffers */
        dg[i].dg_data = Malloc(MAXDG);
        dg[i].dg_sa = Malloc(clilen);
        dg[i].dg_salen = clilen;
    }
    iget = iput = nqueue = 0;

    Signal(SIGHUP, sig_hup);
    Signal(SIGIO, sig_io);
    Fcntl(sockfd, F_SETOWN, getpid());
    Ioctl(sockfd, FIOASYNC, &on);
    Ioctl(sockfd, FIONBIO, &on);

    Sigemptyset(&zeromask);     /* init three signal sets */
    Sigemptyset(&oldmask);
    Sigemptyset(&newmask);
    Sigaddset(&newmask, SIGIO); /* signal we want to block */

    Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    for ( ; ; ) {
        while (nqueue == 0)
            sigsuspend(&zeromask); /* wait for datagram to process */

            /* unblock SIGIO */
        Sigprocmask(SIG_SETMASK, &oldmask, NULL);

        Sendto(sockfd, dg[iget].dg_data, dg[iget].dg_len, 0,
               dg[iget].dg_sa, dg[iget].dg_salen);

        if (++iget >= QSIZE)
            iget = 0;

            /* block SIGIO */
        Sigprocmask(SIG_BLOCK, &newmask, &oldmask);
        nqueue--;
    }
}

 

3)SIGIO处理函数

static void
sig_io(int signo)
{
    ssize_t len;
    int     nread;
    DG     *ptr;

    for (nread = 0;;) {
        if (nqueue >= QSIZE)
            err_quit("receive overflow");

        ptr = &dg[iput];
        ptr->dg_salen = clilen;
        len = recvfrom(sockfd, ptr->dg_data, MAXDG, 0,
                       ptr->dg_sa, &ptr->dg_salen);
        if (len < 0) {
            if (errno == EWOULDBLOCK)
                break;          /* all done; no more queued to read */
            else
                err_sys("recvfrom error");
         }
         ptr->dg_len = len;

         nread++;
         nqueue++;
         if (++iput >= QSIZE)
             iput = 0;

    }
    cntread[nread]++;            /* histogram of # datagrams read per signal */
}

 

4)SIGHUP信号处理函数

static void
sig_hup(int signo)
{
    int     i;

    for (i = 0; i <= QSIZE; i++)
        printf("cntread[%d] = %ld\n", i, cntread[i]);
}

 

最后一句重点总结:信号驱动I/O就是让内核在套接字上发生“某事”时使用SIGIO信号通知进程

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!