114-给 udp 增加可靠性(二)

这一节的目标是完成一个最简单的尽量可靠 UDP 程序,实现的功能如下:

  1. 客户端给服务端发送数据,服务端收到数据后立即将数据原样发送回去,进行确认。
  2. 客户端收到服务端的确认后,检查是不是自己刚刚发送出去的数据,如果是表明服务端收到,继续发送一下个。
  3. 如果客户端在约定的时间(比如 1s 内)没有收到服务端的确认,则重传一次。
  4. 如果客户端重传 3 次服务端还没有确认,放弃重传,发送下一个数据。

上面这些功能非常简单,只是实现起来稍微有些麻烦,但是相比那些开源的 RUDP (Reliable UDP) 库,这已经简单的不能再简单了。当然,你可以在此基础上加入一些其它的特性,比如自己计算 RTO (超时重传时间)。

本小节程序们于文件夹:/unp/program/advcudp/retransmission
git 托管地址:https://gitee.com/ivan_allen/unp

1. 功能预览

为了先让同学们有个直观感受,先来看看这个程序完成后的样子,见图 1。


这里写图片描述
图1 一个简单的 RUDP 程序

图 1 中左侧是客户端程序(在主机 flower 上,后面简称 C 端),右侧是服务器程序(在主机 mars,后面简称 S 端)上。C 端程序从标准输入读取数据,并发送给 S 端,S 端收到数据后立即回射给 C 端。S 端程序有个特性,它会随机丢弃收到的数据(模拟网络环境差),并不会应答 C 端。

图 1 左侧,当我在输入 “This” 并由 C 端发送出去后,C 端等了 1 秒后没有收到 S 的回复,于是又重新发了一次。后面又有一次在发送 “udp” 的时候,C 端又重传了 2 次,才发送成功。

相对于图 2 右侧,同时可以看到 S 端丢弃了 “This”, “is”, “udp” 这些报文,并在屏幕上打印 “discard from 114.244.52.202(seq = 7, timestampe = 1000)” 类似的字样。

2. 传输协议设计

实现的要点在于客户端收到确认后,如何才能知道服务器确认的是哪一包数据呢?基于此,需要给数据打上序列号,设计的包格式如下:

|    HEADER    |    MSG    |

传输的消息由 header + msg 两部分构成,这个 header 是一个结构体,大小固定为 8 字节。msg 是真正要发送的消息(字节数据)。header 定义如下:

struct echo_head {
    int seq; // 序号             
    int ts; // 时间戳,基准时间是 C 端报文发出去那一刻开始为 0 时刻。        
};

3. 程序实现

3.1 客户端

void doClient(int sockfd) {
    int ret, len, nr, nw;
    struct sockaddr_in servaddr;
    char outbuf[4096];
    char inbuf[4096];

    ret = resolve(g_option.hostname, g_option.port, &servaddr); // 解析主机名,填充 sock addr
    if (ret < 0) ERR_EXIT("resolve");

    while(1) {
        nr = iread(STDIN_FILENO, outbuf, 4096); // read from stdin
        if (nr < 0) {
            ERR_EXIT("iread");
        }
        else if (nr == 0) break;

        // 重点是这个函数,重传机制实现就在这里头,后面会介绍。
        // 它的功能就是将数据发送出去,如果 1 秒内没收到确认,就重传。重传三次后就不再重传。
        nr = send_recv(sockfd, outbuf, nr, inbuf, 4096, (struct sockaddr*)&servaddr, sizeof(servaddr));
        if (nr < 0) {
            ERR_EXIT("send_recv");
        }

        // 将收到的数据打印到屏幕。
        iwrite(STDOUT_FILENO, "recv:", strlen("recv:"));
        nw = iwrite(STDOUT_FILENO, inbuf, nr);
        if (nr < 0) {
            ERR_EXIT("iwrite");
        }
    }
}

3.2 服务端实现

void doServer(int sockfd) {
    char buf[4096];
    int nr, nw;
    struct sockaddr_in cliaddr;
    socklen_t len;
    /*
    struct echo_head {
      int seq; // 序号             
      int ts; // 时间戳            
    };
    */
    struct echo_head *hdr;
    char *data;

    while(1) {
        // 接收客户端的数据
        nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &len);
        if (nr < 0) {
            if (errno == EINTR) continue;
            ERR_EXIT("recvfrom");
        }
        hdr = (struct echo_head*)buf;
        data = buf + sizeof(struct echo_head);

        // 随机丢弃
        if (rand() % 100 < 50) {
            WARNING("discard from %s(seq = %d, timestamp = %d):\n", inet_ntoa(cliaddr.sin_addr),
                    hdr->seq, hdr->ts);
            continue;
        }

        LOG("from %s(seq = %d, timestamp = %d):\n", inet_ntoa(cliaddr.sin_addr),
                hdr->seq, hdr->ts);
        iwrite(STDOUT_FILENO, data, nr - sizeof(struct echo_head));

        // 将数据原样发送回去
        nw = sendto(sockfd, buf, nr, 0, (struct sockaddr*)&cliaddr, len);
        if (nw < 0) {
            if (errno == EINTR) continue;
            ERR_EXIT("sentdo");
        }
    }
}

3.3 send_recv 函数实现

/*
 * sockfd: 套接字描述符
 * outbuf: 发送缓冲区
 * outlen: 发送缓冲区大小
 * inbuf: 接收缓冲区
 * inlen: 接收缓冲区大小
 * dstaddr: 目标套接字地址
 * addrlen: 套接字地址长度
 * return: 返回 0 成功,小于 0 失败
 */
int send_recv(int sockfd, char* outbuf, int outlen,
    char* inbuf, int inlen, struct sockaddr* dstaddr, socklen_t addrlen) {

  // 初始化。
  int nr, nw, maxfd, ret, rtt_count;
  int64_t base;
  struct msghdr msgsend, msgrecv;
  struct iovec iovsend[2];
  struct iovec iovrecv[2];
  fd_set rfds;
  sigset_t sigmask, emptyset;


  FD_ZERO(&rfds);
  maxfd = sockfd;

  // 构造信号集。sigmask 集只包含 SIGALRM 信号。emptyset 什么也没有。
  sigemptyset(&sigmask);
  sigemptyset(&emptyset);
  sigaddset(&sigmask, SIGALRM);
  // 阻塞 SIGALRM 信号
  ret = sigprocmask(SIG_BLOCK, &sigmask, NULL);
  if (ret < 0) ERR_EXIT("sigprocmask");

  registSignal(SIGALRM, handler);

  // 构造 iovec 如果不记得 iovec 是干嘛的,回去补习。网络编程第 70 篇有详细介绍。
  iovsend[0].iov_base = &sendhdr;
  iovsend[0].iov_len = sizeof(struct echo_head);
  iovsend[1].iov_base = outbuf;
  iovsend[1].iov_len = outlen;

  msgsend.msg_name = dstaddr;
  msgsend.msg_namelen = addrlen;
  msgsend.msg_iov = iovsend;
  msgsend.msg_iovlen = 2;
  msgsend.msg_control = 0;
  msgsend.msg_controllen = 0;
  msgsend.msg_flags = 0;

  iovrecv[0].iov_base = &recvhdr;
  iovrecv[0].iov_len = sizeof(struct echo_head);
  iovrecv[1].iov_base = inbuf;
  iovrecv[1].iov_len = inlen;

  msgrecv.msg_name = NULL;
  msgrecv.msg_namelen = 0;
  msgrecv.msg_iov = iovrecv;
  msgrecv.msg_iovlen = 2;
  msgrecv.msg_control = 0;
  msgrecv.msg_controllen = 0;
  msgrecv.msg_flags = 0;

  // 用于统计重传次数
  rtt_count = 0;
  ++sendhdr.seq;
  base = now();
sendagain:
  sendhdr.ts = (now() - base)/ 1000;
  LOG("send, seq = %d, ts = %d\n", sendhdr.seq, sendhdr.ts);
  nw = sendmsg(sockfd, &msgsend, 0);
  if (nw < 0) ERR_EXIT("sendmsg");

  // 这里使用 SIGALRM 信号中断来模拟超时。
  // 当然你完全可以使用 pselect 自带的超时来完成此功能。就当这次的作业吧。
  alarm(1); // 设置超时时间
  do {
    FD_SET(sockfd, &rfds);
    ret = pselect(maxfd + 1, &rfds, NULL, NULL, NULL, &emptyset);
    if (ret < 0) {
      // 超时重传
      if (errno == EINTR) {
        ++rtt_count;
        // 重传三次不成功宣告失败。
        if (rtt_count < 3) {
          WARNING("send again: ");
          goto sendagain;
        }
        else {
          errno = EHOSTUNREACH;
          alarm(0); // 清理定时器 
          return -1;
        }
      }
      ERR_EXIT("pselect");
    }
    nr = recvmsg(sockfd, &msgrecv, 0);
    if (nr < 0) ERR_EXIT("recvmsg");

    // 接收到的数据长度不够 or 接收的报文乱序,则重新接收
  } while(nr < sizeof(struct echo_head) || sendhdr.seq != recvhdr.seq);

  // 清理定时器
  alarm(0);

  return nr - sizeof(struct echo_head);
}

4. 总结

知道 RUDP 的基本原理,并能够实现一个最简单的 RUDP 程序。

我们实现的 RUDP 实在是太简单了,它没有自己去动态计算 RTT、RTO 这些值,你完全可以自由的去扩展它。除此之外,还可以改进的地方有重传策略,比如可以模仿 TCP 的快速重传策略。

还有其它的特性,比如拥塞控制,流量控制,这些可能都是需要你去考虑的。如果你完整的实现了这些内容,其实又是在走 tcp 的路,为啥不直接使用 tcp 呢?话又说回来,RUDP 可定制度更高,弹性更好,很多时候,需要采取折中方案的时候,这是个不错的选择。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值