IPV4与IPV6 兼容的socket编程
套接字Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。
生成套接字,主要有3个参数:通信目的IP地址、使用的协议,使用的端口号。通过将这3个参数结合起来,应用层就可以和传输层(或网络层)通过套接字接口,区分来自不同应用程序进程或网络连接的通信。
Socket通讯流程图:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write(); 之后close(clientsocket);
7、关闭网络连接; close(socket)
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
不需要listen()
4、数据交互,用函数recvfrom(); sendto()
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
函数connect();* 可选
5、数据交互,用函数sendto(); recvfrom();
如果调用了connect就直接可以用send和recv了,这时最后两个参数会自动用connect建立时的地址信息填充
6、关闭网络连接;
几个Socket 地址结构相关结构体
注:在操作系统(内核)不同情况下,首两个字节可能是length+family
#ifdef ISC_PLATFORM_HA VESALEN
ntp_u_int8_t ss_len; /* address length */
ntp_u_int8_t ss_family; /* address family */
#else
short ss_family; /* address family */
#endif
struct in_addr {
u_int32_t s_addr; /* 4bytes, IPv4 address */
};
=4
struct in6_addr {
u_int8_t s6_addr[16]; /* IPv6 address */
}
=16
一般socket函数都是传入的这个类型指针
struct sockaddr {
sa_family_t sa_family; /* unsigned short address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */
};
2+14=16
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
2+2+4+8=16
struct sockaddr_in6{
sa_family_t sin6_family; //地址簇类型,为AF_INET6
in_port_t sin6_port; //16 位端口号,网络字节序
uint32_t sin6_flowinfo; //32 位流标签
struct in6_addr sin6_addr; //128 位IP 地址
/*uint32_t sin6_scope_id;*/(新版本可能会多4字节scope id, RFC2553)
}
2+2+4+16=24
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX,本地进程通讯*/
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
=110
例:
my_addr.sun_family = AF_UNIX;
strncpy(my_addr.sun_path, MY_SOCK_PATH, sizeof(my_addr.sun_path) - 1);
sockaddr_storage能提供严格的结构对齐
sockaddr_storage能容纳系统支持的更大的地址结构,容器
在使用sockaddr_storage定义的变量时,尽量强制转换成struct sockaddr, struct sockaddr_in, struct sockaddr_in6后操作
struct __kernel_sockaddr_storage {
unsigned short ss_family; /* address family */
/* Following field(s) are implementation specific */
char __data[_K_SS_MAXSIZE - sizeof(unsigned short)];
/* space to achieve desired size, */
/* _SS_MAXSIZE value minus size of ss_family */
} __attribute__ ((aligned(_K_SS_ALIGNSIZE))); /* force desired alignment */
#define sockaddr_storage __kernel_sockaddr_storage
=128
不同于sockaddr_storag的一个IPV4,IPV6兼容结构体(来源于busybox)
typedef struct len_and_sockaddr {
socklen_t len;
union {
struct sockaddr sa;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
};
} len_and_sockaddr;
用struct sockaddr_storage的好处,看:
取得res后声明一个struct sockaddr_storage变量以兼容IPV4和IPV6 //….getaddrinfo后建立socket
struct sockaddr_storage local_addr;
memset(&local_addr, 0, sizeof(struct sockaddr_storage));
if(res->ai_family == AF_INET6)
{
struct sockaddr_in6 *local_addr6 = (struct sockaddr_in6 *)&local_addr;
local_addr6->sin6_family = AF_INET6;
local_addr6->sin6_port = htons(udp_local_port);
local_addr6->sin6_addr = in6addr_any;
}
else if(res->ai_family == AF_INET)
{
struct sockaddr_in *local_addr4 = (struct sockaddr_in *)&local_addr;
local_addr4->sin_family = AF_INET;
local_addr4->sin_port = htons(udp_local_port);
local_addr4->sin_addr.s_addr = htonl(INADDR_ANY);
}
if(bind(usd, (struct sockaddr *)&local_addr, sizeof(local_addr)) == -1)
{
perror("bind");
fprintf(stderr, "could not bind to local udp port %d\n", udp_local_port);
exit(1);
}
freeaddrinfo(res);
//….send recv等数据交互
一些重要函数
主机字节序到网络字节序间的转换函数
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
例:本机字节序与网络字节序
short int port=0x1234; //本地存储, windows为小端系统,所以存34 12 sockaddr_in.sin_port = htons(port); //网络传输,TCP包头里的数据顺序
IPV4字符串地址和网络序ip地址的转换函数
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
in_addr_t inet_addr(const char *cp);
IPV6,IPV4兼容的字符串地址和网络序ip地址的转换函数
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
两个地址长度的宏
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
IPV4主机名和地址的转换
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
IPV6,IPV4兼容的主机名和地址的转换函数
struct hostent *gethostbyname2(const char *name, int af); //扩展版本,也支持IPV6
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res); //调用时机:在bind或connect之后就可以释放该内存
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
getaddrinfo函数原型
addrinfo结构
首先了解下:
1.通配地址
IPV4:0.0.0.0
IPV6:::
https://www.sodocs.net/doc/2f6878260.html,stat指令查询监听,绑定等的IP和port信息
作为服务器端时:
struct addrinfo hints, *res=NULL;
memset(&hints,0,sizeof(hints));
hints.ai_family=AF_UNSPEC;
//hints.ai_family=AF_INET6;
hints.ai_socktype=SOCK_DGRAM;
//hints.ai_protocol=IPPROTO_UDP;
hints.ai_flags=AI_PASSIVE;
rc=getaddrinfo(NULL,"123",&hints,&res);
socket=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
bind (socket,res->ai_addr,res->ai_addrlen);
hints可看作是过滤条件
res返回一个地址链表,一般来说用第一个bind成功就可以了
可以看到sa_addr的sa_data在端口后面是全0的,相当于是通配地址
因此其相当于下面不调用getaddrinfo方法的简化版本:
usd = socket(AF_INET6, SOCK_DGRAM, 0);
struct sockaddr_storage local_addr;
memset(&local_addr, 0, sizeof(struct sockaddr_storage));
struct sockaddr_in6 *local_addr6 = (struct sockaddr_in6 *)&local_addr;
local_addr6->sin6_family = AF_INET6;
local_addr6->sin6_port = htons(123);
local_addr6->sin6_addr = in6addr_any;
if(bind(usd, (struct sockaddr *)&local_addr, sizeof(local_addr)) == -1)
……
提示:用IPV6建立服务器端的话, 即使客户端仍用IPV4的socket连接也可以正常通讯, IPV4的地址会被转换成这种地址::ffff:192.168.27.25
而旧的IPV4时用以下代码:
usd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(123);
servaddr.sin_addr.s_addr = htons(INADDR_ANY);
if (bind(servfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
……
服务器端从IPV4移植到IPV6要做些什么?
可以调用getaddrinfo得到AF_INET6的通配地址,也可以直接将sockaddr_in结构体更改为sockaddr_in6结构体并相应初始化即可
客户器端如何同时兼容IPV4和IPV6?
按如上方法就可同时兼容IPV4和IPV6的连接请求
作为客户器端时:
memset(&hints,0,sizeof(hints));
hints.ai_family=AF_UNSPEC;
hints.ai_socktype=SOCK_DGRAM;
//hints.ai_protocol=IPPROTO_UDP;
hints.ai_flags=AI_CANONNAME;
rc=getaddrinfo("https://www.sodocs.net/doc/2f6878260.html,","ntp",&hints,&res);
socketfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);
connect(sockfd,res->ai_addr , res->ai_addrlen);
如图res返回两个ai_addr,一个sa_family是0x2为AF_INET,另一个为0x17 AF_INET6;
说明该主机既有AAAA(或A6)记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA 记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回
dig AAAA https://www.sodocs.net/doc/2f6878260.html,
客户器端从IPV4移植到IPV6要做些什么?
如果服务器端具有IPV4地址则基本不需改动,因为IPV6的服务器对IPV4客户端是兼容的,如果是域名且是IPV6的地址则必须调用getaddrinfo得到addrinfo的可用IP地址。
客户器端如何同时兼容IPV4和IPV6?
指定getaddrinfo的family为AF_UNSPEC,会返回可用的IPV4和IPV6 IP地址链表。用返回的family、socktype、protocol建立socket,用返回的IP地址进行连接请求
实例:
修改ntpclient.c以支持IPV6的NTP服务器,还添加了一个参数[-v ipversion],具体请直接查看代码,
旧的ntpclient源码:
附录:
IPV4包头20字节
IPV6包头40字节