欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网络编程(5)套接字地址结构、地址转换

程序员文章站 2022-04-12 16:18:09
...

在前面的示例演示代码、socket常用函数中,服务端需要将创建的socket进行bind到本地的struct sockadd_in结构对象上,在服务端accept和客户端connet时,以及sendto/recvfrom中需要传递struct sockaddr对象指针作为参数。

我们在使用过程中需要将地址的文本表达式转换为存放在套接字地址结构中的 二进制数据,需要调用地址转换函数,而这些函数都是与协议相关的。

因此,本节主要说明常用的套接字结构地址,以及地址结构的成员变量,以及相关的函数,另外还介绍用于获取或返回与某个套接字关联的本地/外地的协议地址。

1、套接字地址结构

对于不同的协议族,都定义了不同的套接字地址结构,都是以sockaddr_开头,对应每个协议族的唯一后缀尾。

1.1 IPv4套接字地址结构

typedef uint32_t in_addr_t;
struct in_addr  {
  in_addr_t s_addr;         /* IPv4 address,network byteordered  */
}

struct sockaddr_in {
  sa_family_t sin_family;   /* uint16_t  AF_INET */
  in_port_t sin_port;       /* uint16_t  Port number. network byteordered */
  struct in_addr sin_addr;  /* uint32_t Internet address.  */
	 
  /* Pad to size of `struct sockaddr'.  */
  unsigned char sin_zero[sizeof (struct sockaddr) -
	    sizeof (sa_family_t) - 
	    sizeof (in_port_t) - 
	    sizeof (struct in_addr)];   // 实际是8个字节
}; // 一共 16字节

1.2 IPv6套接字地址结构

struct sockaddr_in6
{
  sa_family_t sin6_family;   /* uint16_t  AF_INET6 */
  in_port_t sin6_port;	     /* uint16_t  Transport layer port # */
  uint32_t sin6_flowinfo;	 /* uint32_t  IPv6 flow information */
  struct in6_addr sin6_addr; /* 128bits   IPv6 address */
  uint32_t sin6_scope_id;	 /* uint32_t  IPv6 scope-id */
}; // 一共 28 字节

1.3 通用套接字地址结构

struct sockaddr {
  sa_family_t sin_family;  /* uint16_t  AF_INET */
  char sa_data[14];        /* 14 Bytes  Address data. */
} // 16字节

Socket的函数地址参数基本都是sockaddr结构,根据实际传入的第二个参数地址结构长度来决定解析哪一种套接字地址结构。

1.4 新的通用套接字地址结构

#define __ss_aligntype	unsigned long int
#define _SS_PADSIZE (_SS_SIZE - __SOCKADDR_COMMON_SIZE - sizeof(__ss_aligntype)) // 118
struct sockaddr_storage
{
  sa_family_t ss_family;	        /* 2 Bytes Address family, etc.  */
  char __ss_padding[_SS_PADSIZE];  /* 118 Bytes   */
  __ss_aligntype __ss_align;	   /* 8 Bytes Force desired alignment.  */
}; // 128 字节

sockaddr_storage类型满足对齐、空间足够大,内容透明,需要强制转换成适用于ss_family字段对应的地址类型套接字地址结构。

2、套接字地址相关函数

2.1 网络字节序

网络协议必须指定网络字节序,即大端字节序。大端字节序下,数据的高位数据保存在内存区的高地址上。
网络编程(5)套接字地址结构、地址转换
主机字节序和网络字节序的转换函数有

#include <netinet/in.h>
uint32_t ntohl (uint32_t __netlong);
uint16_t ntohs (uint16_t __netshort);	// 返回主机字节序
uint32_t htonl (uint32_t __hostlong);
uint16_t htons (uint16_t __hostshort); 	// 返回网络字节序

函数后缀虽然是l和s(long和short),但是long这里实际还是32位的值。在小端操作系统中会进行大小端转换,大端操作系统实际是一个空宏,不做转换。

2.2 字节操作

多字节的操作函数有两组,不对数据作解释,也不假设数据是以空字符结束的C字符串。以空字符结尾的C字符串是头文件<string.h>中以str(表示字符串)开头的函数处理的。

#include <string.h>
void *memset(void *src, void *dest, int c, size_t len);
void *memcopy(void *dest, const void *src, size_t nbytes); // 重叠时使用memmove
int   memcmp(const void *ptr1, const void *ptr2, size_t nbytes);  

名字以b(表示字节)开头的函数起源于4.2 BSD,以mem(表示内存)开头的函数在支持ANSI C函数库的系统都支持。

#include <strings.h>
void bzero(void *dest, size_t nbytes);
void bcopy(void *src, void *dest, size_t nbytes);// 重叠正常
int  bcmp(void *ptr1, void *ptr2, size_t nbytes);  

2.3 地址转换

介绍在ASCII字符串格式(点分二进制数串)与网络字节序的二进制数据之间转换网络地址

(1)inet_aton、inet_addr和inet_ntoa

点分十进制数串(如“192.168.3.123”)与长度为32位的网络字节序IPv4地址转换。

#include <arpa/inet.h>
int net_aton(const char *strptr, struct in_addr *addrptr);  //转换成功返回1,否则返回0
in_addr_t inet_addr(const char *strptr);  //字符串有效返回32位IPv4地址,否则返回INADDR_NONE 
char *inet_ntoa(struct in_addr addrptr);  //返回点分十进制数串的字符指针

函数inet_addr()失败返回0xffffffff,实际是”255.255.255.255”的广播地址,有些手册出错时返回-1,已经废弃,使用新版的inet_aton函数;inet_ntoa函数仅支持IPv4地址转换,且是不可重入(有其他多个函数体重公用一个全局变量,多线程会有问题),建议使用新版的inet_ntop函数。

(2)inet_pton、inet_ntop

这两个函数是随IPv6出现的,也支持IPv4地址转换,并且都是可重入的。函数名中p和n分别表示表达presentation和数值numeric。地址的表达格式是ASCII字符串,数值格式是存放在套接地址结构中的二进制值。

#include <arpa/inet.h>
int         net_pton(int family, const char *srcptr, void *addrptr);  // 转换成功返回1,无效字符串返回0,出错返回-1
const char *inet_ntop(int family, const void *addrptr, char *srcptr, size_t len);	//返回结果指针,出错返回NULL

注意第一个函数net_pton()的返回结果:转换成功返回1,无效字符串返回0,出错返回-1
两个函数的参数family可以是AF_INET、AF_INET6。第二个函数的len参数是目标存储单元的大小,为避免超过缓冲区长度,定义了两个宏

#include <arpa/inet.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

在IPv4地址转换中,我们可以替换老代码

// 老代码
foo.sin_addr.s_addr = inet_addr(cp);  
// 新代码  
itet_pton(AF_INET, cp, &foo.sin_addr);
// 老代码
char *ptr = inet_ntoa(foo.sin_addr);  
// 新代码     
char str[INET_ADDRLEN];  
ptr = inet_ntop(AF_INET, &foo.sin_addr, str, INET_ADDRLEN);

2.4 地址操作

套接字的地址处理主要在两个地方,第一个是构建struct sockaddr_in和struct sockaddr_in6地址结构用于bind()或connect()或sendto(),第二个是用在recvfrom()以获取struct sockaddr_in或struct sockaddr_in6。

(1)构建地址结构

struct sockaddr_in和struct sockaddr_in6的构建,分别如下

struct sockaddr_in addr_in4;
// 初始化内存(若ip和ort都赋值非零值,可去掉)
//memset(&addr_in4,0,sizeof(addr_in4));
// 协议地址族
addr_in4.sin_family = AF_INET; // IPv4
// 地址赋值
const char ip4_addr[] = "192.168.3.100";
addr_in4.sin_addr.s_addr = INADDR_ANY;            //INADDR_ANY=0, 内核选择
addr_in4.sin_addr.s_addr = inet_addr(ip4_addr);   //废弃,仅IPv4
inet_aton(ip4_addr, &addr_in4.sin_addr);          // 仅IPv4
inet_pton(AF_INET, ip4_addr, &addr_in4.sin_addr); // 通用IPv4/6,建议
// 端口赋值
addr_in4.sin_port = 0;          // 内核选择
ddr_in4.sin_port = htons(8080); //网络序

使用ip addr查看本机的ipv6地址
网络编程(5)套接字地址结构、地址转换

struct sockaddr_in6 addr_in6;
memset(&addr_in6, 0, sizeof(addr_in6));
// 协议地址族
addr_in6.sin6_family = AF_INET6; // IPv6
// 地址赋值
const char ip6_addr[] = "fe80::30bb:634b:434:bca4";
addr_in6.sin6_addr = IN6ADDR_ANY_INIT; // 内核选择
addr_in6.sin6_addr.s6_addr16[0] = htons(std::stoi("fe80",0,16));
addr_in6.sin6_addr.s6_addr16[1] = 0;
addr_in6.sin6_addr.s6_addr16[2] = 0;
addr_in6.sin6_addr.s6_addr16[3] = 0;//手动赋值
addr_in6.sin6_addr.s6_addr16[4] = htons(std::stoi("30bb",0,16));
addr_in6.sin6_addr.s6_addr16[5] = htons(std::stoi("634b",0,16));
addr_in6.sin6_addr.s6_addr16[6] = htons(std::stoi("434",0,16));
addr_in6.sin6_addr.s6_addr16[7] = htons(std::stoi("bca4",0,16)); 
inet_pton(AF_INET6, ip6_addr, &addr_in6.sin6_addr);  // 建议
// 端口赋值
addr_in6.sin6_port = 0;           // 内核选择
addr_in6.sin6_port = htons(8080); //网络序

(2)获取地址结构

这里给出服务端recvform代码的部分示例,是修改之前udp简单通信代码,如下

struct sockaddr_storage storage; 
socklen_t socklen;  // 区别是ipv4,还是ipv6

int len = ::recvfrom(socket_fd, buf, sizeof(buf), 0, (struct sockaddr *)&storage, &socklen);

if (len < 0){
  printf("%s: recv failed. err %s \n", __func__, strerror(errno));  
}
else{
  // 获取客户端的ip、和port
  char ip[INET6_ADDRSTRLEN];   
  int port;
  // 根据socklen长度值,确认
  if(socklen == sizeof(sockaddr_in))  { //Ipv4   
    sockaddr_in  clientaddr = *(sockaddr_in*)&storage;
    inet_ntop(AF_INET, &clientaddr.sin_addr, ip, socklen);      
    port = ntohs(clientaddr.sin_port);     
    printf("%s: client [%s:%d] recv %2d: %s\n", __func__, ip, port, len, buf);
  }
  else if(socklen == sizeof(sockaddr_in6))  { //Ipv6   
    sockaddr_in6 clientaddr = *(sockaddr_in6*)&storage;
    inet_ntop(AF_INET6, &clientaddr.sin6_addr, ip, socklen);      
    port = ntohs(clientaddr.sin6_port);
	printf("%s: client [%s:%d] recv %2d: %s\n", __func__, ip, port, len, buf);
  }
}

结果示例
网络编程(5)套接字地址结构、地址转换

3、套接字关联的本地/外地地址

在网络通信中,TCP服务端可能需要知道当前建立连接的客户端地址,或者UDP/TCP客户端未调用bind函数绑定本地地址、或者bind时仅指定ip或port中的一个,需要知道内核分配的地址信息,就需要用到如下两个函数:

#include <sys/socket.h>
int getpeername (int sockfd, struct sockaddr *localaddr, socklen_t * addrlen);
int getsockname (int sockfd, struct sockaddr *peeraddr, socklen_t * addrlen);  // 成功返回0,出错返回-1

这两个函数的使用方式,参看后面的TCP套接字编程。