一、Socket
socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
socket地址的是结构体sockaddr,定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family; //sa就是sockaddr的简称
char sa_data[14]; //一共14个字节
};
typedef unsigned short int sa_family_t; //2哥字节
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族如下所示:
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; //地址的类型
unsigned long int __ss_align; //用来做内存对齐的
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6。
struct sockaddr_in
{
sa_family_t sin_family; //地址族的类型AF-INET
in_port_t sin_port; //short类型,端口号,2个字节
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
//剩余填充的部分
}
struct in_addr
{
in_addr_t s_addr; //4个字节的int类型的数据 ip地址
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
二、IP地址转换
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
上面的三个函数比较旧了,是早期的转换函数。
下面的函数比较新,也能够完成前面3个函数相同的功能:
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
#include <stdio.h>
#include <arpa/inet.h>
int main() {`在这里插入代码片`
// 创建一个ip字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
unsigned int num = 0;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)# //num已经是网络字节序了 大端字节序的形式
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3)); //输出 192 168 1 4
// 将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, 16);
printf("str : %s\n", str);
printf("ip : %s\n", str);
printf("%d\n", ip == str);
return 0;
}
通过下面的代码,我们可以确认完整的网络字节序是什么样子的。
#include <stdio.h>
#include <arpa/inet.h>
int main() {
char buf[] = "192.168.1.4";
unsigned int num = 0;
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)#
// 十六进制打印完整的网络字节序
printf("完整的网络字节序: 0x");
for (int i = 0; i < sizeof(num); i++) {
printf("%02X", p[i]); //%02X表示以两位十六进制数格式化输出,不足两位时前面补0
}
printf("\n");
// 十六进制打印每个字节的值
printf("逐字节打印: ");
for (int i = 0; i < sizeof(num); i++) {
printf("%02X ", p[i]);
}
printf("\n");
return 0;
}
// 二进制打印完整的网络字节序
printf("二进制完整的网络字节序: ");
for (int i = 0; i < sizeof(num); i++) {
print_binary(p[i]);
if (i < sizeof(num) - 1) {
printf(" "); // 在字节之间添加空格
}
}
printf("\n");
输出:
完整的网络字节序: 0xC0A80104
逐字节打印: C0 A8 01 04
二进制完整的网络字节序: 11000000 10101000 00000001 00000100