1. socket 到底是什么?
在网络编程,socket,中文翻译为套接字,有的时候也叫套接口。它的寓意是通过插口接入的方式,快速完成网络连接和数据收发。可以把它从想象成现实世界的电源插槽,或者早起上网需要的网络插槽,所以 socket也可以看做是对物理世界的直接映射。
先上一张图,可以看看:
这张图是网络编程中,客户端和服务端工作的核心逻辑。先从右侧服务端开始看,因为在客户端发起连接请求之前,服务端必须先初始化好。首先初始化socket,之后服务器执行 bind() 绑定,将自己的服务能力绑定在一个众所周知的地址和端口上。紧接着,执行 listen() ,将原先的 socket转化为服务端的socket,服务端最后阻塞在 accept() 等待客户端请求的到来。
服务端准备就绪后,客户端需要先初始化 socket,再执行 connect() 向服务端的地址和端口发起连接请求,这里的地址必须是客户端预先知晓的。这个过程就是 TCP三次握手,具体的后面会详细阐述。
三次握手成功后,客户端和服务端之间就可以进行数据传输。具体来说,客户端向内核发起write字节流写操作,内核协议栈将字节流从网络设备发送到服务端,服务端从内核得到信息,将字节流从内核读到进程中,并开始业务逻辑的处理,完成之后,服务端再将得到的结果以同样的方式写回到客户端。可以看到,一旦连接建立,数据的传输就不再是单向传输,而是双向的,这也是 TCP的一个显著特性。
当两边交互完之后,比如执行一次 telnet操作,或者一次 HTTP请求,客户端需要和服务器断开连接,就会执行close函数。系统内核此时通过原先的连接链路,向服务端发送一个 FIN包,服务端收到之后被动关闭,整个链路处于半关闭状态,服务器也会执行close(),整个链路才会真正关闭。半关闭状态下,发起close请求的一方,在收到对方 FIN包之前,都会认为链接是正常的,而全关闭情况下,双方都感知连接是关闭状态。
记住,以上所讲这些,socket是我们建立连接 和传输数据的唯一途径。
2. 套接字地址格式
通用套接字地址结构
/* 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
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一样,都有一个 16bit 的sin_family字段,这里就是AF_INET。接下来是端口号,可支持寻址的端口号有 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的字段,最多支持 约 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 bytes,其中流控信息 和 ID先不管,前者在 glibc官网根本没出现,后者是当前未使用字段。这里的地址族显然应该是 AF_INET6,端口和 IPV4一样,主要是地址从 32 bit 提高到 128 bit,完全解决了 寻址数字不够的情况。
以上无论是ipv4 还是 ipv6的地址格式,都是因特网套接字格式。还有一种本地套接字格式,用来本地进程间通信,AF_LOCAL。本地socket本质上是访问本地文件系统,自然不需要端口号。
struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 */
};
下面展示出几种套接字地址格式的比较,ipv4 和 ipv6 套接字地址结构长度是固定的,本地地址结构是可变的。
从这一篇开始,关于网络编程的温故而知新。