网络知识

网络编程:Socket 是如何创建的?

转载:网络编程:Socket 是如何创建的?

说起网络通信,就不得不提到 Socket,不管使用的是 Java 语言,还是 C/C++,Go,PHP,只要你跟网络编程打交道,基本上离不开 Socket。那么 Socket 到底是什么? 它又是如何被创建的? 这篇文章,我们就来讲清楚。

在正式分析 Socket 之前,我们先铺垫下 Linux 操作系统下的内核态和用户态,以便更好地理解 Socket。

一、Linux用户态和内核态 


Linux 设计的初衷是为了安全,给不同的操作分配不同的“权限”,因此将权限分为 2 个等级:内核态(内核空间)和用户态(用户空间)。

  • 用户态:提供应用程序运行的空间,一般我们编写的业务代码就是运行在空间。
  • 内核态:提供给操作系统内核使用,比如:cpu 可以访问内存、外围设备等。

为什么要划分用户态和内核态?

简单来说:

  • 禁止用户程序和底层硬件平台直接交互。
  • 禁止用户程序直接访问任意内存地址空间。

用户态和内核态主要可以通过 3 种方式来进行转换:

  1. 系统调用:调用系统的库函数或者 shell 调用;
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常;
  3. 外设中断:当外设完成用户的请求时,会向 CPU 发送中断信号;

交互抽象如下图:

在了解完 Linux的用户态和内核态之后,我们开始分析 Socket 及其创建过程。

二、什么是Socket 


Socket:中文翻译有很多,所有的翻译整理如下:

业内,很多人把 Socket 翻译成”套接字“,或许这个中文名词可以很生动形象的传达 Socket 所要表达的意思,但是我查阅了很多资料,一直没有找到为什么翻译成”套接字“的有力文档说明,所以个人还是喜欢直接用英文 Socket 来表达,不过这个不影响整体对 Socket 的理解。

在 Linux 操作系统中, Socket 是用来替代传输层以上协议实体的标准接口,它负责实现传输层以上所有的功能,可以说 Socket 是 TCP/IP 协议栈对外的窗口。Socket 是介于应用层与传输层中间的软件抽象层,它是一组接口。

三、Socket的数据结构 


在 Linux 内核中,Socket 的数据结构由 struct socket 与 struct sock 2 部分组成。

struct socket

每个 Socket 在内核中都唯一对应一个 struct socket 结构,其结构如下:

struct sock

struct sock 是 Socket 在网络中的最小描述,它包含了内核管理 Socket 最重要的信息集合,其结构如下:

在 Linux 中 Socket 存在的方式是:文件,对应文件描述符。

当 Socket 连接建立后,用户进程就可以使用常规文件操作访问 Socket,每个 Socket 都分配了一个 VFS inode,inode 结构如下:

四、Socket如何创建 


上文我们讲解了 Socket 的定义以及数据结构,接下来就要分析基于 TCP 协议的 Socket 是如何创建的,一般来说,创建 Socket 需要经过下面 6 个步骤:

  1. 创建Socket;
  2. 将Socket与地址绑定,设置Socket选项;
  3. 建立Socket之间的连接;
  4. 监听Socket;
  5. 接收、发送数据;
  6. 关闭、释放Socket;

因为 Socket 是双通道的,所以我们从服务端和客户端两个部分来说明基于TCP协议的 Socket 的创建过程。

服务端创建 Socket

  1. 调用 socket()函数,创建一个 socket,该 Socket 成为主动 Socket(Active Socket);
  2. 调用 bind()函数,给第 1 步的主动 Socket 绑定一个 ip 和 port;
  3. 调用 listen()函数,将主动 Socket 转成监听 Socket,开始监听客户端的连接请求;
  4. 调用 accept()函数,从已完成连接的队列中拿出一个连接进行处理,如果还没有完成,就要阻塞等待有连接完成;
  5. 若服务端 accept()函数获取到了一个已连接 Socket(Connected Socket),则服务端可以往已连接 Socket 读数据或者写数据;

说明:在 Linux 内核中,会为每个 Socket 维护两个队列:一个是已经建立了连接的队列(三次握手已经完毕),处于 established 状态;一个是还没有完全建立连接的 (未完成三次握手),处于 syn_rcvd 的状态。

客户端创建 Socket

  1. 调用 socket()函数,创建一个 socket,该 Socket 成为主动 Socket(Active Socket);
  2. 当服务端调用 accept()时,客户端可以调用 connect()向服务器发起连接请求,内核会给客户端分配一个临时的端口,一旦握手成功,服务端的 accept()就会返回另一个 Socket;
  3. 客户端可以向 已连接 Socket 读数据或者写数据;

基于 TCP 协议的 Socket 创建过程可以描述成如下图:

UDP 和 TCP 有些差异,UDP 是面向无连接协议,因此不需要三次握手,也就是不需要调用 listen 和 connect,只需要 IP 和端口号,因而需要调用 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。

基于 UDP 协议的 Socket 创建过程可以描述成如下图:

五、Socket类型 


Socket 类型有4种,说明如下:

  1. 主动 Socket(Active Socket):通过系统库函数 socket()生成的就是主动 Socket,主要是用于客户端主动向服务器发送连接;
  2. 被动 Socket(Passive Socket):通过调用系统库函数 listen(),可以将主动 Socket 标记为被动 Socket,用于被动监听客户端的请求连接,也叫监听 Socket。被动 Socket 是服务端独有的,将伴随服务端的整个生命周期。
  3. 监听 Socket(Listened Socket):同被动 socket(Passive Socket)一样。
  4. 已连接 Socket(Connected Socket):通过系统库函数 accept()获取的已建立连接的 Socket,该 Socket 是用于客户端和服务端数据读写的通道,已连接 Socket 是服务器独有的,生命周期为 客户端和服务端的维持的连接时长,当断开连接,生命周期结束。

六、Socket Java实例 


讲述了 Socket 这么多的理论知识,最后我们看下 Socket 在应用层语言是怎么使用的,这里以 Java 为例。

Server端源码

Java 源码 ServerSocket 类中封装了多个构造器,源码调试可以看下,构造器最终都指向 bind(SocketAddress endpoint, int backlog)方法,该方法里面包含重要两个步骤 bind()和 listen(),俩方法又指向 PlainSocketImpl 类中的的 native socketBind()和 native socketListen(),native 方法其实最终操作系统的 bind()和 listen()函数对应。

所以通过上面的 Java 源码可以看出,Java 的 ServerSocket 类只是对操作系统的函数做了一层简单的包装,下面我们再看看 Java 对客户端的代码实现:

Client端源码

Java 源码对客户端的包装和服务端很类似,最后都指向 native connect()方法和操作系统的 connect()函数对应。

通过对 java.net 包中 ServerSocket 类和 Socket 类的源码分析,我们看出 Java 只是在 jdk 里面做了一层使用封装,最终都是指向 native 的方法,和操作系统的函数绑定,这个流程也再次和上面基于 TCP 协议的 Socket 程序函数调用过程图 吻合。