2. socket套接字

87 阅读6分钟

socket 到底是什么?

image.png 这张图表达的是网络编程中,客户端和服务器工作的核心逻辑。

先从右侧的服务器开始,客户端发起连接请求前,服务器必须初始化。右侧的图显示的是服务器端初始化的过程,首先初始化 socket,之后服务器需要执行 bind 函数,将服务能力绑定在一个众所周知的地址和端口上,紧接着服务器执行 listen 操作,将原先的 socket 转化为服务端的 socket,服务端最后阻塞在 accept 上等待客户端请求的到来。

此时服务器准备就绪。客户端需要先初始化 socket,再执行 connect 向服务器的地址和端口发起连接请求。

三次握手完成,客户端和服务器端建立连接,就进入了数据传输过程。

客户端进程向OS内核发起 write 字节流写操作,内核协议栈将字节流通过网络设备传输到服务器,服务器从内核得到信息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成后服务器再将得到的结果以同样的方式写给客户端。可以看到,一旦连接建立,数据的传输就不再是单向的,而是双向的,这也是 TCP 的一个显著特性

当客户端完成和服务器的交互后,需要和服务器断开连接时就会执行 close 函数,OS内核此时会通过原先的连接链路向服务器端发送一个 FIN 包,服务器收到后执行被动关闭,这时候整个链路处于半关闭状态,此后,服务器也会执行 close 函数,整个链路才会真正关闭。半关闭的状态下,发起 close 请求的一方在没有收到对方 FIN 包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。

请注意,以上所有的操作,都是通过 socket 来完成的。无论是客户端的 connect,还是服务端的 accept,或者 read/write 操作等,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。

接下来是端口号,最多是 16bit,即最大支持 2 的 16 次方,65536,。所谓保留端口就是大家约定俗成的,已经被对应服务广为使用的端口,比如 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 套接字地址格式

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 个字节,其中流控信息和域 IP 先不用管,这两个字段,一个在 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 套接字地址结构的长度是固定的,而本地地址结构的长度是可变的。

image.png