type
status
date
slug
summary
tags
category
icon
password
这一章,首先我们来谈一谈 IP 地址和端口这两个概念,因为这两个概念在网络编程中至关重要。然后,我们再聊一聊套接字应用程序接口 (Sockets API) 是如何存储和处理 IP 地址以及端口的。
1. IPv4 和 IPv6
在美好的旧时代,有一个神奇的路由系统,叫做”The Internet Protocol Version 4”,简称 IPv4。它的地址由 4 个字节组成,通常以”点分十进制“的形式书写,如:
192.168.1.92
。你可能经常见到这样的 IP 地址。事实上,目前几乎所有的网站用的还是 IPv4。
一切都很美好,直到有一天,一个名叫 Vint Cerf 的人告诉大家:我们的 IPv4 地址即将耗尽!
于是,IPv6 诞生了。IPv6 地址由 16 个字节组成 (128位),这样我们就有 个地址,永远地解决了地址枯竭问题。IPv6 地址到底多到什么程度呢?有了 IPv6,我们就可以为地球上的每一粒沙子分配一个地址,甚至多个地址…
IPv6以十六进制的形式书写,每两个字节以冒号 (:) 隔开,如:
2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551
它还有一些简写形式。比如,IPv6 地址中有很多 0,那么我们可以将它们压缩在两个冒号之间;并且每个字节对中的前置 0 也可以省略。例如,下面每对地址都是等价的:
其中,
::1
是回环地址,它表示”当前这台机器“。在 IPv4 中,回环地址是127.0.0.1
。IPv6 还提供了 IPv4 兼容的模式。例如,如果要将IPv4地址
192.0.2.33
表示为 IPv6 地址,那么需要写成::ffff:192.0.2.33
。1.1 子网
为了方便管理,我们将 IP 地址分成了两部分:前半部分是网络号,后半部分是主机号。比如,IPv4 地址
192.0.2.12
,前三个字节是网络部分,最后一个字节是主机部分。在早期的时候,子网是分类别的,可以分为 A, B, C, D, E 五类(了解即可):

A 类地址非常少,但是每个 A 类地址可以拥有很多台主机(1600 多万);相反,我们有很多个 C 类地址,然而每个 C 类地址只能拥有 250 多台主机。
我们可以用子网掩码 (netmask) 来表示一个 IP 地址的网络部分。子网掩码的前面部分都是 1,后面部分都是 0,比如
255.255.255.0
。将子网掩码和 IP 地址按位与,就可以获得 IP 地址的网络部分。比如,子网掩码为255.255.255.0
,IP 地址为192.0.2.12
,192.0.2.12 AND 255.255.255.0 = 192.0.2.0
,故网络部分为192.0.2.0
。 事实证明,将 IPv4 地址这样分类,太简单粗暴了。A 类地址非常少,而 C 类地址支持的主机数目又极其有限。为了修正这一点,我们允许子网掩码可以是任意的长度,而不仅仅是 8 位,16 位,或者 24 位。所以子网掩码可以是
255.255.255.252
,网络部分有 30 位,主机部分只有 2 位。但是
255.255.255.252
这种写法太麻烦了,并且还很不直观,我们没办法一眼看出网络部分有多少位。所以,我们有了一种更简洁的写法:在 IP 地址后面添加一个斜杠 (/),后面跟一个表示网络部分位数的数字。如,192.0.2.12/30
。IPv6 地址也支持这种写法,如:
2001:db8::/32
和2001:db8:5413:4028::9db9/64
。1.2 端口号
IP 地址是给 IP 协议(网络层)使用的,用来定位网络上的主机。但是光有 IP 地址是不够的,传输层协议 (TCP 和 UDP) 还需要使用另一种地址——端口号来定位网络服务。端口号是一个 16 位的整数,在 C 语言中,我们可以用
short
类型表示。打个比方,IP 地址就像小区的街道地址,端口号就像房间号。
为何网络通信一定需要端口号呢?那是因为一台机器可以提供多种服务,比如既可以接收邮件,也可以提供 HTTP 服务。设想一下,如果只有 IP 地址,那怎么区分这两种服务呢?
互联网上知名的服务都有自己的端口号,我们可以在 iana 上查看。如果使用的是类 Unix 系统,还可以在
/etc/services
文件中查看。HTTP 服务的端口是 80,telnet 是 23,SMTP 是 25 等等。低于 1024 的端口被视为特殊端口,通常需要特殊的系统权限才能使用。端口号,我们就说到这儿。
2. 字节序
如果一个数据占多个字节,那么我们就需要考虑它在主机中和在网络中是如何存储的。我们有两种字节序,分别称为大端法和小端法。
举个例子,两字节的数字
b34f
,如果它按b3
, 4f
的顺序存储,这种存储方式叫大端法 (most significant byte first);相反,如果它是按4f
, b3
的顺序存储,这种存储方式就叫小端法 (least significant byte first)。Intel 架构的主机采用的是小端法,PowerPC 架构的主机一般采用大端法。
不同的主机可能采用不同的方式存储数据,这就给网络编程带来了极大的困难。
第一个问题是:网络中的数据该以何种方式存储呢?我们人为地规定网络中的数据以大端法存储,并且称之为网络字节序 (Network Byte Order)。主机存储数据的方式,我们称之为主机字节序 (Host Byte Order),它可能是大端法,也可能是小端法。
当我们构建数据包的时候,往往会往数据包中填充一些数据结构,这些数据结构必须以大端法的方式存储 (否则另一端就不能够正确解析)。所以,第二个问题就是数据如何在网络字节序和主机字节序之间转换?
好消息,我们有现成的函数来做这些繁琐的转换(
man byteorder
)。原则上,我们在发送数据之前,都要将数据转换成网络字节序;接收到数据之后,又要将数据转换回主机字节序。但是,幸运的是,通常一些函数的内部已经帮我们做了这些转换。
3. 套接字地址结构
终于,我们可以谈一谈套接字地址了。套接字地址既包含 IP 地址,也包含端口号,我们可以简单地认为它是 (address, port) 对 (回想一下,套接字位于应用层和传输层之间)。IPv4 和 IPv6 有不同的套接字地址结构。
首先,我们来看一看 IPv4 套接字的地址结构(
man 7 ip
):接下来,我们再看一看 IPv6 套接字的地址结构(
man 7 ipv6
):显然,IPv4 和 IPv6 的地址结构有很大的不同,这就给 API 的设计带来了很大的困难。比如,
bind()
函数(绑定地址用的),参数的类型是设为struct sockaddr_in
,还是struct sockaddr_in6
呢?显然都不可以!于是,我们设计了一个通用地址结构struct sockaddr
:struct sockaddr
类似接口,而struct sockaddr_in
和struct sockaddr_in6
类似具体的实现类。各个套接字地址结构,如下图所示:

最后,我们来看另一个通用地址结构
struct sockaddr_storage
:有些函数,比如
accept()
,它可以接收远端的地址。于是,我们专门设计了struct sockaddr_storage
用来接收远端地址。它足够大,既可以接收 IPv4 的地址,也可以接收 IPv6 的地址(甚至还可以接收本地套接字地址)。4. inet_pton()
和 inet_ntop()
inet_pton()
和inet_ntop()
,这两个函数可以将 IP 地址在文本形式和二进制形式之间进行转换(p for presentation or printable if you like, n for network)。首先,我们来看一下
inet_pton()
:参数
af
: 地址族。AF_INET
表示 IPv4 地址,AF_INET6
表示 IPv6 地址。src
: 文本形式的 IP 地址。比如“10.12.110.57”,或“2001:db8:63b3:1::3490”。dst
: 二进制形式的 IP 地址要写入的地址,并且是以网络字节序写入的。Example
The old-school way
以前做这种转换的函数是
inet_addr()
和inet_aton()
。不过它们现在已经过时了,并且它们不支持 IPv6。我们再来看一下
inet_ntop()
这个函数,它可以将 IP 地址从二进制形式转换成文本形式。参数
af
: 地址族。AF_INET
表示 IPv4 地址,AF_INET6
表示 IPv6 地址。src
: 指向二进制的 IP 地址。dst
: 指向一个字符数组,用来存储文本形式的 IP 地址。size
: 字符数组的长度。Example
Okok,相信你对各种套接字地址结构已经有了一个大致的印象了(不需要记下来),而且会将 IP 地址在二进制形式和文本形式之间进行转换了。
The old-school way
以前做这种转换的函数是
inet_ntoa()
。不过现在它已经过时了,并且它不支持 IPv6 。接下来,请集中注意力,因为我们要介绍一个很重要的函数。虽然它参数很多(不要被吓到),但用起来很简单,它就是
getaddrinfo()
!(观众:欢呼声,尖叫声!
getaddrinfo()
:闪亮登场,并向观众弯腰致谢…)5. getaddrinfo()
getaddrinfo()
函数会动态申请一些内存空间,并构建一条链表。该链表的结点是一个addrinfo
结构体, addrinfo
结构体里面包含一个套接字地址.该套接字地址能够匹配node
和service
,并且满足hints
里面设定的限制条件。参数
node
: 网络上的结点(主机)。可以是域名,如 “www.baidu.com”;也可以是具体的 IP 地址,如 "10.12.110.57"。service
: 网络服务(端口)。可以是服务名,如 “http”;也可以是端口号,如 “9527”。hints
: addrinfo
结构体。我们可以在hints
里面设置一些返回结果必须满足的限制条件。res
: 指向链表的第一个结点。Example
6. 综合练习
最后,我们一起来做一个综合练习题,它可以把我们这一章学过的知识都运用起来。这个程序是这样的:用户在命令行指定一个域名,程序显示该域名所有的 IP 地址和端口。
运行结果
大家应该都用过 IP 地址查询工具,showip 小程序其实就是一个简易的 IP 地址查询工具!
归纳总结
呼!这一章,我们讲了很多的内容:
- 和套接字地址相关的一些概念,比如,IP 地址,端口号,字节序。
- 各种套接字地址结构:
- IPv4 地址:
struct sockaddr_in
- IPv6 地址:
struct sockaddr_in6
- 通用地址
struct sockaddr
一般是作为接口参数使用。 struct sockaddr_storage
一般是用来接收远端地址的。
- 以及一个很重要的函数
getaddrinfo()
,它可以根据node
和service
获取一系列套接字地址。
参考文章
- TCP/IP Illustrated, Volume 1: The Protocols
- UNIX Network Programming, Volume 1: The Sockets Networking API, 3rd Edition
欢迎您在底部评论区留言,大家一起交流学习~
- 作者:Thomas He
- 链接:https://notion-next-lovat-ten.vercel.app/article/networkprogramming/2
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章