【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)

此为牛客Linux C++课程和黑马Linux系统编程笔记。

1. 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分。
【Linux网络编程学习】预备知识(网络字节序、IP地址转换函数、sockaddr数据结构)
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如:发送主机中地址0-1存储16位的源端口号,这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,则地址0存储0xe8,地址1存储0x03,接收方接收到的端口号就成了59395(0xe803)而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);  // 32位,host转为net,用于主机向网络发送IP时的转换
uint16_t htons(uint16_t hostshort); // 16位,host转为net,用于主机向网络发送IP时的转换
uint32_t ntohl(uint32_t netlong);   // 32位,net转为host,用于主机向网络发送端口号时的转换
uint16_t ntohs(uint16_t netshort);  // 16位,net转为host,用于主机向网络发送端口号时的转换

h表示host,n表示network,l表示32位长整数(IP地址为32位),s表示16位短整数(端口号位16位)。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2. IP地址转换函数

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用
十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录
日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);

af:地址族: AF_INET(表示ipv4) AF_INET6(表示ipv6)
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面

#include <arpa/inet.h>
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

af:地址族: AF_INET(表示ipv4) AF_INET6(表示ipv6)
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

3. socket 地址

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
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:

#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
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;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
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 */
};
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。

上一篇:std::bind与socket bind冲突


下一篇:C语言socket UDP广播的收发