写点什么

Android C++ 系列:Linux Socket 编程(一)预备知识

作者:轻口味
  • 2021 年 12 月 13 日
  • 本文字数:2293 字

    阅读完需:约 8 分钟

Android C++系列:Linux Socket编程(一)预备知识

1. 网络字节序

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


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


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


#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
复制代码


h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2. IP 地址转换函数

早期:


#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp);char *inet_ntoa(struct in_addr in);只能处理IPv4的ip地址 不可重入函数 注意参数是struct in_addr
复制代码


最新:


#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);支持IPv4和IPv6 可重入函数
复制代码


其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr, 因此函数接口是 void *addrptr。

3. sockaddr 数据结构

strcut sockaddr 很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 sockaddr 结 构体,为了向前兼容,现在 sockaddr 退化成了(void *)的作用,传递一个地址给函数,至 于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转 化为所需的地址类型



struct sockaddr {   sa_family_t sa_family;/* address family, AF_xxx */  char sa_data[14]; /* 14 bytes of protocol address */};struct sockaddr_in {   __kernel_sa_family_t sin_family; /* Address family */  __be16 sin_port; /* Port number */  struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];};
/* Internet address. */ struct in_addr { __be32 s_addr; };struct sockaddr_in6 { unsigned short int sin6_family;/* AF_INET6 */ __be16 sin6_port; /* Transport layer port # */ __be32 sin6_flowinfo;/* IPv6 address */ struct in6_addr sin6_addr; /* IPv6 address */ __u32 sin6_scope_id; /*scope id (new in RFC2553) */};
struct in6_addr { union { __u8 u6_addr8[16]; __be16 u6_addr16[8]; __be32 u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32

#define UNIX_PATH_MAX 108struct sockaddr_un { __kernel_sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
复制代码


Pv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包 括 16 位端口号和 32 位 IP 地址,IPv6 地址用 sockaddr_in6 结构体表示,包括 16 位端口号、128 位 IP 地址和一些控制字段。UNIX Domain Socket 的地址格式定义在 sys/un.h 中,用 sock- addr_un 结构体表示。各种 socket 地址结构体的开头都是相同的,前 16 位表示整个结构 体的长度(并不是所有 UNIX 的实现都有长度字段,如 Linux 就没有),后 16 位表示地址类 型。IPv4、IPv6 和 Unix Domain Socket 的地址类型分别定义为常数 AF_INET、AF_INET6、AF_UNIX。 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构 体,就可以根据地址类型字段确定结构体中的内容。因此,socket API 可以接受各种类型的 sockaddr 结构体指针做参数,例如 bind、accept、connect 等函数,这些函数的参数应该设 计成 void *类型以便接受各种类型的指针,但是 sock API 的实现早于 ANSI C 标准化,那时还 没有 void *类型,因此这些函数的参数都用 struct sockaddr *类型表示,在传递参数之前 要强制类型转换一下,例如:


struct sockaddr_in servaddr;/* initialize servaddr */bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
复制代码

4. 总结

本文介绍了网络字节序概念以及字节序转换 C 函数、IP 地址转换函数、sockaddr 数据结构等。

发布于: 7 小时前阅读数: 7
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android、音视频、AI相关领域从业者。 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Android C++系列:Linux Socket编程(一)预备知识