<audio id="audio" title="03丨套接字和地址：像电话和电话号码一样理解它们" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/63/1f/6315819f6bff64a0a17b3a8d8783021f.mp3"></audio>

在网络编程中，我们经常会提到socket这个词，它的中文翻译为套接字，有的时候也叫做套接口。

socket这个英文单词的原意是“插口”“插槽”， 在网络编程中，它的寓意是可以通过插口接入的方式，快速完成网络连接和数据收发。你可以把它想象成现实世界的电源插口，或者是早期上网需要的网络插槽，所以socket也可以看做是对物理世界的直接映射。

其实计算机程序设计是一门和英文有着紧密联系的学科，很多专有名词使用英文原词比翻译成中文更容易让大家接受。为了方便，在专栏里我们一般会直接使用英文，如果需要翻译就一律用“套接字”这个翻译。

## socket到底是什么？

在网络编程中，到底应该怎么理解socket呢？我在这里先呈上这么一张图，你可以先看看。

<img src="https://static001.geekbang.org/resource/image/0b/64/0ba3f3d04b1466262c02d6f24ee76a64.jpg" alt=""><br>
这张图表达的其实是网络编程中，客户端和服务器工作的核心逻辑。

我们先从右侧的服务器端开始看，因为在客户端发起连接请求之前，服务器端必须初始化好。右侧的图显示的是服务器端初始化的过程，首先初始化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。我们在&lt;sys/socket.h&gt;头文件中可以清晰地看到，这两个值本身就是一一对应的。

```
/* 各种地址族的宏定义  */
#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套接字地址结构的长度是固定的，而本地地址结构的长度是可变的。

<img src="https://static001.geekbang.org/resource/image/ed/58/ed49b0f1b658e82cb07a6e1e81f36b58.png" alt="">

## 总结

这一讲我们重点讲述了什么是套接字，以及对应的套接字地址格式。套接字作为网络编程的基础，概念异常重要。套接字的设计为我们打开了网络编程的大门，实际上，正是因为BSD套接字如此成功，各大Unix厂商（包括开源的Linux）以及Windows平台才会很快照搬了过来。在下一讲中，我们将开始创建并使用套接字，建立连接，进一步开始我们的网络编程之旅。

## 思考题

最后给你留两道思考题吧，你可以想一想IPv4、IPv6、本地套接字格式以及通用地址套接字，它们有什么共性呢？如果你是BSD套接字的设计者，你为什么要这样设计呢？

第二道题是，为什么本地套接字格式不需要端口号，而IPv4和IPv6套接字格式却需要端口号呢？

我在评论区期待你的思考与见解，如果你觉得这篇文章对你有所帮助，欢迎点击“请朋友读”，把这篇文章分享给你朋友或同事。
