gitbook/网络编程实战/docs/113607.md
2022-09-03 22:05:03 +08:00

220 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 03丨套接字和地址:像电话和电话号码一样理解它们
在网络编程中,我们经常会提到socket这个词,它的中文翻译为套接字,有的时候也叫做套接口。
socket这个英文单词的原意是“插口”“插槽”, 在网络编程中,它的寓意是可以通过插口接入的方式,快速完成网络连接和数据收发。你可以把它想象成现实世界的电源插口,或者是早期上网需要的网络插槽,所以socket也可以看做是对物理世界的直接映射。
其实计算机程序设计是一门和英文有着紧密联系的学科,很多专有名词使用英文原词比翻译成中文更容易让大家接受。为了方便,在专栏里我们一般会直接使用英文,如果需要翻译就一律用“套接字”这个翻译。
## socket到底是什么?
在网络编程中,到底应该怎么理解socket呢?我在这里先呈上这么一张图,你可以先看看。
![](https://static001.geekbang.org/resource/image/0b/64/0ba3f3d04b1466262c02d6f24ee76a64.jpg)
这张图表达的其实是网络编程中,客户端和服务器工作的核心逻辑。
我们先从右侧的服务器端开始看,因为在客户端发起连接请求之前,服务器端必须初始化好。右侧的图显示的是服务器端初始化的过程,首先初始化socket,之后服务器端需要执行bind函数,将自己的服务能力绑定在一个众所周知的地址和端口上,紧接着,服务器端执行listen操作,将原先的socket转化为服务端的socket,服务端最后阻塞在accept上等待客户端请求的到来。
此时,服务器端已经准备就绪。客户端需要先初始化socket,再执行connect向服务器端的地址和端口发起连接请求,这里的地址和端口必须是客户端预先知晓的。这个过程,就是著名的**TCP三次握手**(Three-way Handshake)。下一篇文章,我会详细讲到TCP三次握手的原理。
一旦三次握手完成,客户端和服务器端建立连接,就进入了数据传输过程。
具体来说,客户端进程向操作系统内核发起write字节流写操作,内核协议栈将字节流通过网络设备传输到服务器端,服务器端从内核得到信息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务器端再将得到的结果以同样的方式写给客户端。可以看到,**一旦连接建立,数据的传输就不再是单向的,而是双向的,这也是TCP的一个显著特性**。
当客户端完成和服务器端的交互后,比如执行一次Telnet操作,或者一次HTTP请求,需要和服务器端断开连接时,就会执行close函数,操作系统内核此时会通过原先的连接链路向服务器端发送一个FIN包,服务器收到之后执行被动关闭,这时候整个链路处于半关闭状态,此后,服务器端也会执行close函数,整个链路才会真正关闭。半关闭的状态下,发起close请求的一方在没有收到对方FIN包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。
请你牢牢记住文章开头的那幅图,它是贯穿整个专栏的核心图之一。
讲这幅图的真正用意在于引入socket的概念,请注意,以上所有的操作,都是通过socket来完成的。无论是客户端的connect,还是服务端的accept,或者read/write操作等,**socket是我们用来建立连接,传输数据的唯一途径**。
### 更好地理解socket:一个更直观的解释
你可以把整个TCP的网络交互和数据传输想象成打电话,顺着这个思路想象,socket就好像是我们手里的电话机,connect就好比拿着电话机拨号,而服务器端的bind就好比是去电信公司开户,将电话号码和我们家里的电话机绑定,这样别人就可以用这个号码找到你,listen就好似人们在家里听到了响铃,accept就好比是被叫的一方拿起电话开始应答。至此,三次握手就完成了,连接建立完毕。
接下来,拨打电话的人开始说话:“你好。”这时就进入了write,接收电话的人听到的过程可以想象成read(听到并读出数据),并且开始应答,双方就进入了read/write的数据传输过程。
最后,拨打电话的人完成了此次交流,挂上电话,对应的操作可以理解为close,接听电话的人知道对方已挂机,也挂上电话,也是一次close。
在整个电话交流过程中,电话是我们可以和外面通信的设备,对应到网络编程的世界里,socket也是我们可以和外界进行网络通信的途径。
### socket的发展历史
通过上面的讲解和这个打电话的类比,你现在清楚socket到底是什么了吧?那socket最开始是怎么被提出来的呢?接下来就很有必要一起来简单追溯一下它的历史了。
socket是加州大学伯克利分校的研究人员在20世纪80年代早期提出的,所以也被叫做伯克利套接字。伯克利的研究者们设想用socket的概念,屏蔽掉底层协议栈的差别。第一版实现socket的就是TCP/IP协议,最早是在BSD 4.2 Unix内核上实现了socket。很快大家就发现这么一个概念带来了网络编程的便利,于是有更多人也接触到了socket的概念。Linux作为Unix系统的一个开源实现,很早就从头开发实现了TCP/IP协议,伴随着socket的成功,Windows也引入了socket的概念。于是在今天的世界里,socket成为网络互联互通的标准。
## 套接字地址格式
在使用套接字时,首先要解决通信双方寻址的问题。我们需要套接字的地址建立连接,就像打电话时首先需要查找电话簿,找到你想要联系的那个人,你才可以建立连接,开始交流。接下来,我们重点讨论套接字的地址格式。
### 通用套接字地址格式
下面先看一下套接字的**通用**地址结构:
```
/* POSIX.1g 规范规定了地址族为2字节的值. */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址 */
struct sockaddr{
sa_family_t sa_family; /* 地址族. 16-bit*/
char sa_data[14]; /* 具体的地址值 112-bit */
};
```
在这个结构体里,第一个字段是地址族,它表示使用什么样的方式对地址进行解释和保存,好比电话簿里的手机格式,或者是固话格式,这两种格式的长度和含义都是不同的。地址族在glibc里的定义非常多,常用的有以下几种:
* AF\_LOCAL:表示的是本地地址,对应的是Unix套接字,这种情况一般用于本地socket通信,很多情况下也可以写成AF\_UNIX、AF\_FILE;
* AF\_INET:因特网使用的IPv4地址;
* AF\_INET6:因特网使用的IPv6地址。
这里的AF\_表示的含义是Address Family,但是很多情况下,我们也会看到以PF\_表示的宏,比如PF\_INET、PF\_INET6等,实际上PF\_的意思是Protocol Family,也就是协议族的意思。我们用AF\_xxx这样的值来初始化socket地址,用PF\_xxx这样的值来初始化socket。我们在<sys/socket.h>头文件中可以清晰地看到,这两个值本身就是一一对应的。
```
/* 各种地址族的宏定义 */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
```
sockaddr是一个通用的地址结构,通用的意思是适用于多种地址族。为什么定义这么一个通用地址结构呢,这个放在后面讲。
### IPv4套接字格式地址
接下来,看一下常用的IPv4地址族的结构:
```
/* IPV4套接字地址,32bit值. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/* 描述IPV4的套接字地址格式 */
struct sockaddr_in
{
sa_family_t sin_family; /* 16-bit */
in_port_t sin_port; /* 端口号 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */
/* 这里仅仅用作占位符,不做实际用处 */
unsigned char sin_zero[8];
};
```
我们对这个结构体稍作解读,首先可以发现和sockaddr一样,都有一个16-bit的sin\_family字段,对于IPv4来说这个值就是AF\_INET。
接下来是端口号,我们可以看到端口号最多是16-bit,也就是说最大支持2的16次方,这个数字是65536,所以我们应该知道支持寻址的端口号最多就是65535。关于端口,我在前面的章节也提到过,这里重点阐述一下保留端口。所谓保留端口就是大家约定俗成的,已经被对应服务广为使用的端口,比如ftp的21端口,ssh的22端口,http的80端口等。一般而言,大于5000的端口可以作为我们自己应用程序的端口使用。
下面是glibc定义的保留端口。
```
/* Standard well-known ports. */
enum
{
IPPORT_ECHO = 7, /* Echo service. */
IPPORT_DISCARD = 9, /* Discard transmissions service. */
IPPORT_SYSTAT = 11, /* System status service. */
IPPORT_DAYTIME = 13, /* Time of day service. */
IPPORT_NETSTAT = 15, /* Network status service. */
IPPORT_FTP = 21, /* File Transfer Protocol. */
IPPORT_TELNET = 23, /* Telnet protocol. */
IPPORT_SMTP = 25, /* Simple Mail Transfer Protocol. */
IPPORT_TIMESERVER = 37, /* Timeserver service. */
IPPORT_NAMESERVER = 42, /* Domain Name Service. */
IPPORT_WHOIS = 43, /* Internet Whois service. */
IPPORT_MTP = 57,
IPPORT_TFTP = 69, /* Trivial File Transfer Protocol. */
IPPORT_RJE = 77,
IPPORT_FINGER = 79, /* Finger service. */
IPPORT_TTYLINK = 87,
IPPORT_SUPDUP = 95, /* SUPDUP protocol. */
IPPORT_EXECSERVER = 512, /* execd service. */
IPPORT_LOGINSERVER = 513, /* rlogind service. */
IPPORT_CMDSERVER = 514,
IPPORT_EFSSERVER = 520,
/* UDP ports. */
IPPORT_BIFFUDP = 512,
IPPORT_WHOSERVER = 513,
IPPORT_ROUTESERVER = 520,
/* Ports less than this value are reserved for privileged processes. */
IPPORT_RESERVED = 1024,
/* Ports greater this value are reserved for (non-privileged) servers. */
IPPORT_USERRESERVED = 5000
```
实际的IPv4地址是一个32-bit的字段,可以想象最多支持的地址数就是2的32次方,大约是42亿,应该说这个数字在设计之初还是非常巨大的,无奈互联网蓬勃发展,全球接入的设备越来越多,这个数字渐渐显得不太够用了,于是大家所熟知的IPv6就隆重登场了。
### IPv6套接字地址格式
我们再看看IPv6的地址结构:
```
struct sockaddr_in6
{
sa_family_t sin6_family; /* 16-bit */
in_port_t sin6_port; /* 传输端口号 # 16-bit */
uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
struct in6_addr sin6_addr; /* IPv6地址128-bit */
uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
};
```
整个结构体长度是28个字节,其中流控信息和域ID先不用管,这两个字段,一个在glibc的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是AF\_INET6,端口同IPv4地址一样,关键的地址从32位升级到128位,这个数字就大到恐怖了,完全解决了寻址数字不够的问题。
请注意,以上无论IPv4还是IPv6的地址格式都是因特网套接字的格式,还有一种本地套接字格式,用来作为本地进程间的通信, 也就是前面提到的AF\_LOCAL。
```
struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 */
};
```
### 几种套接字地址格式比较
这几种地址的比较见下图,IPv4和IPv6套接字地址结构的长度是固定的,而本地地址结构的长度是可变的。
![](https://static001.geekbang.org/resource/image/ed/58/ed49b0f1b658e82cb07a6e1e81f36b58.png)
## 总结
这一讲我们重点讲述了什么是套接字,以及对应的套接字地址格式。套接字作为网络编程的基础,概念异常重要。套接字的设计为我们打开了网络编程的大门,实际上,正是因为BSD套接字如此成功,各大Unix厂商(包括开源的Linux)以及Windows平台才会很快照搬了过来。在下一讲中,我们将开始创建并使用套接字,建立连接,进一步开始我们的网络编程之旅。
## 思考题
最后给你留两道思考题吧,你可以想一想IPv4、IPv6、本地套接字格式以及通用地址套接字,它们有什么共性呢?如果你是BSD套接字的设计者,你为什么要这样设计呢?
第二道题是,为什么本地套接字格式不需要端口号,而IPv4和IPv6套接字格式却需要端口号呢?
我在评论区期待你的思考与见解,如果你觉得这篇文章对你有所帮助,欢迎点击“请朋友读”,把这篇文章分享给你朋友或同事。