IPv4和IPv6的地址ASCII表示
IPV4:
定义的最大长度
#define INET_ADDRSTRLEN 16
ddd.ddd.ddd.ddd’\0’
例如111.111.111.111
43个ASCII码表示的十进制数加上3个冒号。
最大43+3+1=16字节。
IPv6:
#define INET6_ADDRSTRLEN 46
内嵌IPv4地址法,为了兼容IPv4和IPv6
HHHH.HHHH.HHHH.HHHH.HHHH.HHHH.ddd.ddd.ddd.ddd
64个ASCII码表示的16进制数加上一族IPv4地址,加上6个冒号。
最大64+16+6=46字节
函数总结
字节排序函数
(1)大端和小端
对于一个16位整数,他有两个字节。内存通常一个单元存储一个字节,因此存储这个数有两种存储方式。对于16位整数
10101010 01010101
最高有效位 最低有效位
MSB LSB
小端:低序字节存储在起始地
大端:高序字节存储在起始地址
不同主机采用不同的排序方式。给定系统所用的字节序称为主机字节序。
(2)如何查看本机字节序
#include<stdio.h>
int main(){
union{
short s;
char c[2];
}un;
un.s=0x0102; //高字节序0x01,低字节序0x02.
if(un.c[0]==1&&un.c[1]==2)
printf("大端");
else if(un.c[0]==2&&un.c[1]==1)
printf("小端");
else
printf("未知字节序");
return 0;
}
这里使用一个共用体union来测试字节序。对于字节数组来说,系统会创建一个从低内存到高内存的连续内存。对于本例来说,定义了一个short型变量0x0102。改数值的高字节序为0x01,低字节序为0x02。
它存储在共用体中,和一个两位的字节数组共用内存。对于char c[2],c[0]对应内存地址A,c[1]就是内存地址A+1。查看c[0]和c[1]中储存的数据,即可直到本机字节序。
(3)字节排序函数
主机字节序和网络字节序不同,因此需要用到排序函数来进行转换。使用这些函数时,我们不需要去直到字节序的真正值(到底是大端还是小端),只需要针对场景调用适当函数,来进行主机和网络之间的数值转换。一般在需要将IP地址填入套接字地质结构中时,要进行主机和网络的转换。
#include<netinet/in.h>
/*返回网络字节序的值*/
uint16_t htons(uint16_t host16bitvalue); //host to net
uint32_t htonl(uint32_t host32bitvalue);
/*返回主机字节序多的值*/
uint16_t ntons(uint16_t net16bitvalue); //net to host
uint16_t ntons(uint32_t net32bitvalue);
命名规则:h代表host,n代表net,s代表short(用于16位),l代表long(用于32位)。
字节操纵函数
在网络通信中,我们处理的字节可能标准不同。有些字节(例如IP地址)可能包含值为0的字节(’\0’),却不是一般意义上的C字符串(C字符串总是以’\0’结尾,定义在string.h中)。字节操纵函数可以统一处理这些字节,用来处理套接字地址结构。
(1)bzero函数
b开头函数源于4.2BSD。
#include<strings.h>
void bzero(void *dest,size_t nbytes);
void bcopy(const void *src,void *dest,szie_t nbytes); //将指定数目的字节*src从源字节串移到目标串*dest
int bcmp(const void *ptr1,const void *ptr2,size_t nbytes); //比较任意两个字节串,相同返回0,否则1
关键字const,表示所限定的指针所指向的内容不会被函数更改。保证使用这两个函数不会改变原字符串。
bzero函数将目标字节串中指定数目的字节置为0。一般在创建一个套接字地质结构之前,调用该函数来进行初始化为0的操作。
struct sockaddr_in servaddr;
//……
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13);
(2)men_系函数
men(内存)开头的函数源与ANSI C标准。
#include<string.h>
void *memset(void *dest,int c,size_t len); //将目标字节串指定数目的字节置为c
void *mencpy(void *dest,const void *src,size_t nbytes); //类似copy
int mencmp(const void *ptr1,const void *ptr2,size_t nbytes); //类似bcmp
地址转换函数
地址转换函数的作用是:在ASCII字符串和网络字节序的二进制值(存放在套接字地址结构中的值)之间进行转换。
(1)用于IPv4的地址转换函数
主要是inet_aton函数和inet_ntoa函数。
a代表ASCII码点分十进制数,n代表net网际32位IPv4地址addr_in
(a)inet_aton函数
用于将点分十进制数串(例如192.168.0.1)转换为相应的32位网络字节序二进制IPv4地址。
aton的意思就是ASCII to net:
ASCII -----> addr_in
函数原型
#include<arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
参数:
const char *strptr:ASCII码的ip地址。
struct in_addr *addrptr:32位IPv4地址结构
返回值:
字符串有效返回1,否则返回0
作用:
将strptr指向的点分十进制数串ip地址,转换为struct in_addr类型的地址(用于套接字地质结构中)。
(b)inet_ntoa函数
该函数的作用是ntoa,net to ASCII
in_addr ----> ASCII
函数原型:
#include<arpa/inet.h>
char *inet_ntoa(struct in_addr inaddr);
参数:
struct in_addr结构的32位IPv4地址。
返回值:
指向ASCII码的点分十进制数串的指针。
(c)inet_addr函数(已被废弃)
ASCII -----> in_addr_t
函数原型:
#include<arpa/inet.h>
in_addr_t inet_addr(const char *strptr);
参数:
const char *strptr:ASCII码的IP地址。
返回值:
in_addr_t:uint32整数,不是in_addr结构。
作用:
作用相同。该函数存在的问题是:不能处理255.255.255.255。因为该数值在这个函数中代表处理出错
(2)适用于IPv4和IPv6
主要是inet_pton函数和inet_ntop函数。p代表表达(presentation),n代表数值(numeric)。这两个函数既可以用于IPv4,也可以用于IPv6,用参数int family来区分。
(a)inet_pton函数
函数原型:
#include<arpa/inet.h>
int inet_pton(int family,const char *strptr,void *addrptr);
参数:
int family:可以是AF_INET,也可以时AF_INET6
const char *strptr:ASCII码IP地址
void *addrptr:一个指针,指向IP地址转换后的二进制结果。
返回值:
如果对于指定的family,输入的字符串是无效的,则返回0;成功转换则返回1;若出错则返回-1.
(b)inet_ntop函数
函数原型
#include<arpa/inet.h>
const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len);
参数:
int family:可以是AF_INET,也可以是AF_INET6
size_t len:len参数是最后转换后字符串需要的存储单元的大小。对于这个大小,不同的协议族有不同的定义。在<netinet/in.h>中有如下定义:
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
返回值:
如果len太小,不足以容纳最后的格式效果,则返回一个空指针,否则返回一个指向转换结果的指针。
包裹函数:sock_ntop函数(书中用的)
(1)包裹函数的意义
系统函数的缺陷是,必须与协议绑定。
例如对于inet_ntop函数,实现numeric to presentation
const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len);
对于IPv4:
struct sockaddr_in addr;
inet_ntop(AF_INET,&addr.sin_addr,str,sizeof(str));
对于IPv6:
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6,&addr6.sin_addr,str,sizeof(str));
为了获得addrptr这个参数,必须与固定的地址族绑定。
为了解除绑定,使得IPv4和IPv6的移植性更好,考虑使用包裹函数来包裹这些需要特定地址族的函数。
在函数中加入额外的switch模块,用case区分。
(2)sock_ntop包裹inet_ntop
对于套接字地址结构,我们有可用的通用套接字地址结构,因此可以用通用套接字地址结构作为输入参数,并在函数内部再调用特定的方法(switch)来得到二进制IP地址。
函数原型:
#include<unp.h>
char *sock_ntop(const struct sockaddr *sockaddr,socklen_t addrlen);
参数:
sockaddr指向一个长度为addrlen的套接字地址结构。
返回值:
本函数的返回值是一个指向sockaddr的静态缓冲区的一个指针。
说明:
本函数使用sockaddr自己的静态缓冲区来保存结果。对于IPv4和IPv6来说,转换结果是ASCII形式地址加上分号,分号后加5位位端口号和’\0’。IPv6需要额外两边的方括号。因此
IPv4为ddd.ddd.ddd.ddd:ppppp。所需最大缓存空间为:16+6=22字节
IPv6为[HHHH.HHHH.HHHH.HHHH.HHHH.HHHH.ddd.ddd.ddd.ddd:ppppp]。最大:46+6+2=54字节
(3)源码
char * sock_ntop(const struct sockaddr *sa, socklen_t salen){
char portstr[8];
static char str[128];
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return(NULL);
if (ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return(str);
}
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
str[0] = '[';
if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return(NULL);
if (ntohs(sin6->sin6_port) != 0) {
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return(str);
}
return (str + 1);
}
//……还有其他省略