socket地址API

想学习socket地址API,要先理解主机字节序网络字节序

主机字节序和网络字节序

现代CPU的累加器一次至少装载一个整形数据。但是!内存中的排列顺序会影响他们被累加成的整数的值。也就是字节序的影响。

字节序分为大端字节序和小端字节序字节序。
大端字节序是指一个整数的高字节位存储在内存的低地址处,低字节位存储在高地址处。小端则相反

以下是一段检查用来检查机器的字节序的代码:

#include<iostream>
using namespace std;
void ByteOrder()
{
   
	union
	{
   
		short value;
		char union_bytes[sizeof(short)];	//每八位组成一个union_bytes
	}test;

	test.value = 0x0102;	//二进制: 0000 0001 0000 0010

	if((test.union_bytes[0] == 1) && test.union_bytes[1] == 2)
	{
   
		cout<< "big endian"<< endl;
	}
	else if((test.union_bytes[0] == 2) && test.union_bytes[1] == 1)
	{
   
		cout<< "little endian"<< endl;
	}
	else
	{
   
		cout<< "i also don't konw"<< endl;
	}
}

现代PC大多采用小端字节序,所以小端字节序又称主机字节序
但是!仍然有机器采用大端字节序。

解决方法:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道传送的数据是大端字节序,可以根据自身的字节序来决定是否转换。所以大端字节序又称网络字节序

字节序转换函数

Linux提供以下四个函数来完成字节序的转换。

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);	//长整型的主机字节序转化为网络字节序
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

别看这里函数这么多,但是也是有记忆方法的:

h 指代 host(主机),n 指代 net(网络),l 是 long,s 是 short
所以htonl就是host to network long

通用socket地址(一般不用)

socket 网络编程接口中用结构体sockaddr表示:

#include <bits/socket.h>
struct sockaddr
{
   
	sa_family_t sa_family;	//地址族类型
	char sa_data[14];		//存放socket地址
}

常见协议族:
AF_UNIX UNIX本地域协议族
AF_INET TCP/IPv4 协议族
AF_INET6 TCP/IPv6协议族

但是,随着技术的发展,14字节的sa_data无法容纳多数的协议族的地址值。所以,当下一般使用这个结构体:

#include <bits/socket.h>

struct sockaddr_storage
{
   
	sa_family_t sa_family;
	unsigned long int __ss_align;		//用于内存对齐
	char __ss_padding[128-sizeof(__ss_align)];
}

专用socket地址

通用虽然兼容性好,但是在设置与获取IP地址和端口号就需要执行繁琐的操作。所以就需要以下Linux提供的专门的地址结构体。

UNIX本地域协议族专用地址结构体:

#include <sys/un.h>

struct sockaddr_un
{
   
	sa_family_t sin_family;		//AF_UNIX
	char sun_path [108];		//文件路径名
}

TCP/IP 协议族则使用sockaddr_insockaddr_in6两个结构体,分别用于IPv4和IPv6:

struct sockaddr_in
{
   
	sa_family_t sin_family;		//AF_INET
	u_int16_t sin_port;			//端口号
	struct in_addr sin_addr;	//IPv4地址结构体
}

struct in_addr
{
   
	u_int32_t s_addr;	//IPv4地址
}
struct sockaddr_in6
{
   
	sa_family_t sin6_family;		//AF_INET6
	u_int16_t sin6_port;			//端口号
	u_int32_t sin6_flowinfo;		//流信息,一般设置为0
	struct in6_addr sin6_addr;		//IPv6地址结构体
	u_int32_t sin6_scope_id;		//scope ID,用于标识网络接口
}

struct in6_addr
{
   
	unsigned char sa_addr[16];		//IPv6地址
}

scope_id作用:
用于标识网络接口的,因为多个网络接口可以具有相同的本地链接IPv6地址。目的地址决定使用哪个接口,如果目的地址是本地链接地址,主机将无法确定要使用哪个接口,因为它们都具有相同的网络。来自stack overflow

IP地址转换函数

我们看到的IP地址通常是<stron>来表示的。但是对于计算机来说,它是不识别的。所以就需要把IP地址进行二进制的转换。</stron>

比如说我的云服务器地址就是点分十进制的:47.92.194.18

以下函数用于把点分十进制转换成二进制:

#include <arpa/inet.h>

in_addr_t inet_addr(const char* strptr);		//失败返回INADDDR_NONE
int inet_aton(const char* cp, struct in_addr* inp);		//这个函数将转换结果存储结构体中,而不是返回。成功时返回1,失败返回0

下面这个函数将二进制转换为点分十进制:

#include <arpa/inet.h>

char* inet_ntoa(struct in_addr in);		//该函数内存用一个静态变量存储转化结构,且不可重入

当然,下面这对更新函数也能完成和前面3个函数同样的功能,并同时适用IPv4和IPv6

#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);		//成功返回1,失败返回0并设置errno
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);		//成功返回指向目标存储单元的地址,失败则返回NULL并设置errno

cnt参数执行目标存储单元的大小,下面这个宏帮助我们指定大小:

#include <netinet/in.h>
#define INET_ADDRSTRLEN 16 //ipv4
#define INET6_ADDRSTRLEN 46 //ipv6

参考文献

[1]游双.Linux高性能服务器编程.机械工业出版社,2043.5.