考虑一个回射服务器,接收一段 TCP 字节流,或一个 UDP 数据报,然后返回同样的字节流或数据报。

在弱端系统模型(weak end system model)中,IP 层接受目的地址为本机任一接口的分组,而不管到达的具体接口。但在强端系统模型(strong end system model)中,IP 层仅接收目的地址与到达接口一致的分组。

主机通常都是弱端系统模型,但是,有时候我们需要获知哪个数据报究竟是来自哪个接口,这时我们可以给每个接口分别 bind 一个套接字。而遍历接口当然要用 getifaddrs 函数。

#include "all.h"

#define MAX 1024

static void my_echo(int fd, struct sockaddr *cliaddr, socklen_t clilen)
{
    int n;
    char buf[MAX];
    socklen_t len;
    struct sockaddr_storage cli;
    
    n = MAX;
    memcpy(&cli, cliaddr, clilen);  /* copy client addr info */
    len = clilen;
    while (1) {
        memset(buf, 0, n);
        n = recvfrom(fd, buf, MAX, 0, (struct sockaddr *) &cli, &len);
        printf("sockfd %d from %s\n", fd,
                sock_ntop((struct sockaddr *) &cli));
        sendto(fd, buf, n, 0, (struct sockaddr *) &cli, len);
    }
}

int main()
{
    int fd, n;
    const int on = 1;
    sa_family_t family;
    struct ifaddrs *ifaddr, *ifa;
    struct sockaddr_in6 *sin6;
    struct sockaddr_in *sin, wildaddr;

    if (getifaddrs(&ifaddr) < 0)
        error(EXIT_FAILURE, errno, "getifaddrs error");
    if (ifaddr->ifa_addr == NULL)
        error(EXIT_FAILURE, errno, "getifaddrs error");

    ifa = ifaddr;
    do {
        if (ifa->ifa_addr == NULL)
            continue;

        family = ifa->ifa_addr->sa_family;
        if (family == AF_INET6) {
            fd = socket(AF_INET6, SOCK_DGRAM, 0);
            sin6 = (struct sockaddr_in6 *) ifa->ifa_addr;
            sin6->sin6_port = htons(33333);
            n = bind(fd, ifa->ifa_addr, sizeof(struct sockaddr_in6));
        } else if (family == AF_INET) {
            fd = socket(AF_INET, SOCK_DGRAM, 0);
            sin = (struct sockaddr_in *) ifa->ifa_addr;
            sin->sin_port = htons(33333);
            n = bind(fd, ifa->ifa_addr, sizeof(struct sockaddr_in));
        } else {      /* maybe AF_PACKET */
            continue;
        }
        if (n < 0) {
            error(0, errno, "bind error: %s", sock_ntop(ifa->ifa_addr));
            continue;
        }
        printf("bind success: %s on %s\n",
                sock_ntop(ifa->ifa_addr), ifa->ifa_name);
        if (fork() == 0) {
            my_echo(fd, ifa->ifa_addr,family == AF_INET ?
                                      sizeof(struct sockaddr_in) :
                                      sizeof(struct sockaddr_in6));
            exit(0);  /* never executed */
        }
    } while ((ifa = ifa->ifa_next) != NULL);
    freeifaddrs(ifaddr);

    /* For INADDR_ANY, but i think this will never work */
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    n = bind(fd, (struct sockaddr *) &wildaddr, sizeof(struct sockaddr_in));
    wildaddr.sin_family = AF_INET;
    wildaddr.sin_port = htons(33333);
    wildaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (n < 0)
        error(EXIT_FAILURE, errno, "bind error");
    printf("bind success: %s\n", sock_ntop((struct sockaddr *) &wildaddr));

    if (fork() == 0) {
        my_echo(fd, (struct sockaddr *) &wildaddr, sizeof(wildaddr));
        exit(0);  /* never executed */
    }
    /* make program always in the foreground */
    while (1)
        sleep(100);

    exit(0);
}

必要的头文件我都放在 all.h 里面了,sock_ntop 是一个类似 inet_ntop 的函数,程序使用 33333 端口。跳过所有非 AF_INET 和 AF_INET6 的接口,以及通配地址,比如 AF_PACKET,在 bind 成功之后,使用 fork() 直接在该套接字上建立一个服务器,并把接口地址和大小传给 my_echo 函数。

程序启动:

$ ./myecho
bind success: 127.0.0.1:33333 on lo
bind success: 192.168.1.116:33333 on enp8s0
bind success: 192.168.1.115:33333 on wlp9s0
bind success: [::1]:33333 on lo
bind success: [fe80::4d2c:d940:2064:7647]:33333 on enp8s0
bind success: [fe80::e733:949:93bc:ea49]:33333 on wlp9s0
bind success: 0.0.0.0:33333

可以看到,列出了所有三个接口的六个地址和一个通配地址,lo 是 loopback,enp8s0 是我的有线连接,wlp9s0 是我的无线连接。

然后用我自己写的 sock 和 sock6 程序,分别向几个地址发送 UDP 数据报:

$ sock udp localhost
$ sock udp 192.168.1.116
$ sock udp 192.168.1.115
$ sock6 udp ip6-localhost
$ sock6 udp fe80::4d2c:d940:2064:7647
$ sock6 udp fe80::e733:949:93bc:ea49

可以看到,数据报到达了不同的 sockfd 和接口。

sockfd 3 from 127.0.0.1:46805
sockfd 4 from 192.168.1.116:46882
sockfd 5 from 192.168.1.115:53062
sockfd 6 from [::1]:42833
sockfd 7 from [fe80::4d2c:d940:2064:7647]:42061
sockfd 8 from [fe80::e733:949:93bc:ea49]:54049

而使用通配地址 0.0.0.0 作为目标地址的话,则会:

sockfd 3 from 127.0.0.1:58218

因为我是从本机发送到本机的数据报,一定会传到 loopback 的。通配地址应该永远不会出现在目的地址中。