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 的过渡,为未来的网络应用开发奠定基础。
超级会员免费看
813

被折叠的 条评论
为什么被折叠?



