28、IPv6 网络编程:从基础到应用

IPv6 网络编程:从基础到应用

1. IPv6 模块加载与测试

在系统的 modules.conf 文件中添加相应配置后,系统会在应用首次需要时加载 IPv6 模块,并保持加载状态直到重启。重启后,该模块会在需要时再次加载。

为验证系统的 IPv6 功能,我们可以使用 ping 工具进行测试。IPv4 本地主机地址 127.0.0.1 对应的 IPv6 地址是 0000:0000:0000:0000:0000:0000:0000:0001 ,通常简写为 ::1 。然而,直接使用 ping ::1 会提示 unknown host ::1 ,这是因为常见的网络工具默认仅支持 IPv4。要使用 IPv6,需使用其对应的 IPv6 版本工具,例如将 ping 替换为 ping6

以下是具体的测试步骤:

[user@host projects]$ ping ::1
ping: unknown host ::1
[user@host projects]$ ping6 –c 5 ::1
PING ::1(::1) 56 data bytes
64 bytes from ::1: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from ::1: icmp_seq=2 ttl=64 time=0.031 ms
64 bytes from ::1: icmp_seq=3 ttl=64 time=0.029 ms
64 bytes from ::1: icmp_seq=4 ttl=64 time=0.026 ms
64 bytes from ::1: icmp_seq=5 ttl=64 time=0.026 ms
--- ::1 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3996ms
rtt min/avg/max/mdev = 0.026/0.029/0.034/0.004 ms
[user@host projects]$

测试结果表明,系统已准备好在本地接口 ::1 上收发 IPv6 流量。

2. 迁移到 IPv6

在开发网络应用时,若要支持 IPv6,需考虑以下几个关键问题:
- 支持更大的地址空间 :在套接字地址结构中支持更大的 IPv6 地址空间。
- 地址转换 :以兼容 IPv4 和 IPv6 的方式处理名称到地址以及地址到名称的转换。
- 用户界面适配 :在用户界面属性(如文本框)中支持更大的 IPv6 地址。

不过,无需改变应用的逻辑流程, socket() bind() accept() listen() 等基础函数同时支持 IPv4 和 IPv6,可通过套接字地址结构来确定使用的地址空间。例如,可通过修改地址族来让 socket() 函数使用 IPv6:

/* create a streaming IPv4 socket   */
V4Socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
/* create a streaming IPv6 socket   */
V6Socket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);

IPv4 地址由 sockaddr_in 结构处理,而 IPv6 地址则由 sockaddr_in6 结构处理:

struct sockaddr_in6 {
    sa_family_t sin6_family;    /* AF_INET6 */
    in_port_t sin6_port;        /* Transport layer port # */
    uint32_t sin6_flowinfo;     /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id;     /* IPv6 scope-id */
};

切换到 PF_INET6 地址族和 sockaddr_in6 结构后,基本完成了向 IPv6 的迁移,但还需处理一些小问题,如使用 gethostbyname() gethostbyaddr() 函数时与 IPv6 的差异。

3. IPv6 服务器

以下是将简单的 TCP 服务器从 IPv4 迁移到 IPv6 的示例代码:

#include <stdio.h>      
#include <sys/types.h>
#include <sys/socket.h>   
#include <netdb.h>
const char APRESSMESSAGE[] = "APRESS - For Professionals, By Professionals!\n";
int main(int argc, char *argv[]) {
    int simpleSocket = 0;
    int simplePort = 0;
    int returnStatus = 0;
    struct sockaddr_in6 simpleServer;
    if (2 != argc) {
        fprintf(stderr, "Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    simpleSocket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
    if (simpleSocket == -1) {
        fprintf(stderr, "Could not create a socket!\n");
        exit(1);
    }
    else {
        fprintf(stderr, "Socket created!\n");
    }
    simplePort = atoi(argv[1]);
    bzero(&simpleServer, sizeof(simpleServer)); 
    simpleServer.sin_family = PF_INET6;
    simpleServer.sin_port = htons(simplePort);
    inet_pton(PF_INET6, "::1", &(simpleServer.sin6_addr));
    returnStatus = bind(simpleSocket, (struct sockaddr *)&simpleServer, sizeof(simpleServer));
    if (returnStatus == 0) {
        fprintf(stderr, "Bind completed!\n");
    }
    else {
        fprintf(stderr, "Could not bind to address!\n");
        close(simpleSocket);
        exit(1);
    }
    returnStatus = listen(simpleSocket, 5);
    if (returnStatus == -1) {
        fprintf(stderr, "Cannot listen on socket!\n");
        close(simpleSocket);
        exit(1);
    }
    while (1) {
        struct sockaddr_in6 clientName = { 0 };
        int simpleChildSocket = 0;
        int clientNameLength = sizeof(clientName);
        simpleChildSocket = accept(simpleSocket, (struct sockaddr *)&clientName, &clientNameLength);
        if (simpleChildSocket == -1) {
            fprintf(stderr, "Cannot accept connections!\n");
            close(simpleSocket);
            exit(1);
        }
        write(simpleChildSocket, APRESSMESSAGE, strlen(APRESSMESSAGE));
        close(simpleChildSocket);
    }
    close(simpleSocket);
    return 0;
}

运行该 IPv6 服务器的步骤如下:
1. 编译代码: cc -o ip6Server ip6Server.c
2. 运行服务器,指定端口号: ./ip6Server 9999
3. 若要停止服务器,使用 Ctrl+C 组合键。

4. IPv6 客户端

同样,简单的 TCP 客户端也需要迁移到 IPv6 以匹配服务器。以下是迁移后的客户端代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int main(int argc, char *argv[]) {
    int simpleSocket = 0;
    int simplePort = 0;
    int returnStatus = 0;
    char buffer[256] = "";
    struct sockaddr_in6 simpleServer;
    if (3 != argc) {
        fprintf(stderr, "Usage: %s <server> <port>\n", argv[0]);
        exit(1);
    }
    simpleSocket = socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP);
    if (simpleSocket == -1) {
        fprintf(stderr, "Could not create a socket!\n");
        exit(1);
    }
    else {
        fprintf(stderr, "Socket created!\n");
    }
    simplePort = atoi(argv[2]);
    bzero(&simpleServer, sizeof(simpleServer));
    simpleServer.sin_family = PF_INET6;
    inet_pton(PF_INET6, argv[1], &(simpleServer.sin6_addr));
    simpleServer.sin_port = htons(simplePort);
    returnStatus = connect(simpleSocket, (struct sockaddr *)&simpleServer, sizeof(simpleServer));
    if (returnStatus == 0) {
        fprintf(stderr, "Connect successful!\n");
    }
    else {
        fprintf(stderr, "Could not connect to address!\n");
        close(simpleSocket);
        exit(1);
    }
    returnStatus = read(simpleSocket, buffer, sizeof(buffer));
    if ( returnStatus > 0 ) {
        printf("%d: %s", returnStatus, buffer);
    } else {
        fprintf(stderr, "Return Status = %d \n", returnStatus);
    }
    close(simpleSocket);
    return 0;
}

运行该 IPv6 客户端的步骤如下:
1. 编译代码: cc -o ip6Client ip6Client.c
2. 运行客户端,指定服务器地址和端口号: ./ip6Client ::1 9999

5. 地址转换函数

在 IPv6 网络编程中,许多传统的地址转换函数已被整合为两个新的函数: getnameinfo() getaddrinfo()

  • getnameinfo() :用于将地址转换为名称,它整合了 gethostbyaddr() getservbyport() 的功能。函数原型如下:
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *serv, size_t servlen, int flags);

调用时,主机名或服务名可以用 NULL 替代,但必须提供其中一个。 flags 参数可用于修改函数返回值。

  • getaddrinfo() :用于将名称转换为地址,是 getnameinfo() 的反向操作,整合了 getservbyname() gethostbyname() 等多个函数的功能。函数原型如下:
int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

该函数是线程安全的,调用成功后会创建一个或多个套接字地址结构,可用于 connect() bind() 函数。此外,它还有两个辅助函数: freeaddrinfo() 用于释放 addrinfo 结构, gai_strerror() 用于报告错误。

以下是这两个函数的关系流程图:

graph LR
    A[地址] -->|getnameinfo()| B[名称]
    B -->|getaddrinfo()| A

综上所述,通过上述步骤和方法,我们可以实现从 IPv4 到 IPv6 的迁移,并利用新的地址转换函数进行高效的网络编程。在实际应用中,开发者还需考虑系统的 IPv6 支持情况、用户界面的适配以及使用可移植的地址结构等问题。

IPv6 网络编程:从基础到应用

6. 可移植性结构 sockaddr_storage

在将应用程序从 IPv4 迁移到 IPv6 时,若想同时支持两种协议,单纯将 sockaddr_in 替换为 sockaddr_in6 会将应用拆分为 IPv4 和 IPv6 两个版本,并非最优解。此时可使用专门为可移植性设计的 sockaddr_storage 结构。

/* Structure large enough to hold any socket address (with the historical
exception of AF_UNIX).  We reserve 128 bytes.  */
#if ULONG_MAX > 0xffffffff
# define __ss_aligntype __uint64_t
#else
# define __ss_aligntype __uint32_t
#endif
#define _SS_SIZE        128
#define _SS_PADSIZE     (_SS_SIZE - (2 * sizeof (__ss_aligntype)))
struct sockaddr_storage
{
    __SOCKADDR_COMMON (ss_);    /* Address family, etc.  */
    __ss_aligntype __ss_align;  /* Force desired alignment.  */
    char __ss_padding[_SS_PADSIZE];
};

sockaddr_storage 结构使用 128 字节,足够容纳任何套接字地址。使用该结构可隐藏地址空间是 IPv4 还是 IPv6 的细节,同时允许应用使用任意一种协议。结合 getnameinfo() getaddrinfo() 这两个与 IPv4 兼容的函数,可创建同时支持 IPv4 和 IPv6 的应用程序。

7. 支持双协议的客户端示例

以下是对 IPv6 客户端进行修改,使其能处理 IPv4 或 IPv6 连接的示例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int main(int argc, char *argv[]) {
    int simpleSocket = 0;
    int simplePort = 0;
    int returnStatus = 0;
    char buffer[256] = "";
    struct addrinfo hints, *res;
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if (3 != argc) {
        fprintf(stderr, "Usage: %s <server> <port>\n", argv[0]);
        exit(1);
    }
    returnStatus = getaddrinfo(argv[1], argv[2], &hints, &res);
    if(returnStatus) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(returnStatus));
        exit(1);
    }
    simpleSocket = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (simpleSocket == -1) {
        fprintf(stderr, "Could not create a socket!\n");
        exit(1);
    }
    else {
        fprintf(stderr, "Socket created!\n");
    }
    returnStatus = connect(simpleSocket, res->ai_addr, res->ai_addrlen);
    if (returnStatus == 0) {
        fprintf(stderr, "Connect successful!\n");
    }
    else {
        fprintf(stderr, "Could not connect to address!\n");
        close(simpleSocket);
        exit(1);
    }
    returnStatus = read(simpleSocket, buffer, sizeof(buffer));
    if ( returnStatus > 0 ) {
        printf("%d: %s", returnStatus, buffer);
    } else {
        fprintf(stderr, "Return Status = %d \n", returnStatus);
    }
    close(simpleSocket);
    freeaddrinfo(res);
    return 0;
}

具体操作步骤如下:
1. 声明 addrinfo 结构 :使用 PF_UNSPEC 设置 ai_family ,表示地址族未指定。 hints 结构用于设置协议偏好, res 结构用于存储主机上所有可能的地址链表。
2. 检查参数 :确保命令行参数包含服务器地址和端口号。
3. 填充 addrinfo 结构 :使用 getaddrinfo() 函数,传入服务器 IP 地址、端口号以及 hints res 结构的指针。
4. 创建套接字 :使用 res 结构中的 ai_family ai_socktype ai_protocol 值创建套接字。
5. 连接服务器 :使用 res 结构中的 ai_addr ai_addrlen 进行连接。
6. 读取服务器消息 :使用 read() 函数读取服务器发送的消息。
7. 清理资源 :关闭套接字并使用 freeaddrinfo() 释放 addrinfo 结构。

8. 测试双协议客户端

可以使用 IPv4 服务器和 IPv6 服务器对这个多地址客户端进行测试。例如,启动 Chapter 2 中的 IPv4 服务器在端口 8888,启动上述的 IPv6 服务器在端口 9999,然后使用新的多地址客户端进行测试:

[user@host projects]$ cc -o multiClient multiClient.c
[user@host projects]$./multiClient 127.0.0.1 8888
Socket created!
Connect successful!
46: APRESS - For Professionals, By Professionals!
[user@host projects]$ ./multiClient ::1 9999
Socket created!
Connect successful!
46: APRESS - For Professionals, By Professionals!

通过上述测试可以看出,使用同一个客户端版本和 addrinfo 结构,能够连接到 IPv4 服务器或 IPv6 服务器。

9. IPv6 总结

IPv6 作为下一代 IP 寻址方案,使用 128 位地址,是当前标准的四倍,提供了近乎无限的增长空间,还具备改进的路由、认证和隐私功能,以及扩展头部信息以满足未来需求的能力,并且与 IPv4 地址向后兼容。

将现有的 IPv4 应用程序迁移到 IPv6 并不困难,但开发者在迁移时应牢记以下关键点:
| 关键点 | 具体说明 |
| ---- | ---- |
| 系统支持 | 运行应用程序的每个系统都需要支持 IPv6,并且必须启用 IPv6 支持。 |
| 用户界面适配 | 应用程序的用户界面必须能够容纳 IPv6 可能的更大地址值。 |
| 地址转换 | 使用 getaddrinfo() getnameinfo() 可以为 IPv4 和 IPv6 提供名称和地址转换,而无需针对特定协议。 |
| 可移植性结构 | 如果应用程序同时使用 IPv4 和 IPv6,应同时支持两者,并使用可移植的结构来存储地址和端口信息,如 sockaddr_storage 结构。 |

综上所述,掌握 IPv6 网络编程技术对于开发者来说至关重要,通过合理运用相关技术和工具,能够顺利实现从 IPv4 到 IPv6 的过渡,为未来的网络应用开发奠定基础。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值