1. 什么是 socket

现在的网络编程几乎都是 socket 编程,不理解 socket 本质,很多知识是无法串联起来的,今天我们就一起揭开 socket 的神秘面纱,探究一下 socket 到底是什么。

1.初识 socket

首先我们以 TCP 编程为例整体感受一下 socket 的存在,在 TCP 通信编程的过程中,我们的编程思路如下:

在这里插入图片描述
只要按顺序实现了这些关于 socket 的调用,我们就能完成客户端和服务端之间的通信,这大概就是我们对 socket 的第一印象了。

那 socket 具体指的是什么呢?
其实 socket 是在应用层和传输层之间的一个抽象层,它把复杂的 TCP/IP 协议抽象为了几个简单的接口供应用层调用,就能实现网络中通信,画个图来具体感受一下整体网络架构:

在这里插入图片描述
如图所示,socket 是由操作系统内核进行实现的,然后暴露接口给应用层使用,作为应用层编程很难感知到 socket 真正的实现逻辑,但想要理解网络 IO 就必须揭开 socket 的神秘面纱。

好的,对 socket 有了一个整体的认知以后,我们看一下百度百科给出的官方定义:

套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。

本节小结

  1. socket 是在应用层和传输层之间的一个抽象层,为应用层提供了一套用于双端通信的接口
  2. socket 实现的是端对端的通信,进行的是双端的数据交换

2.socket 起源和本质

阅读本节内容可以回答的问题:假设端口号共 65535 个都可以使用,一个客户端(固定IP)能与一个服务端(固定IP+端口)最多建立多少个 socket?

2.1 socket 起源和本质

socket 起源于 Unix,提到 Unix 就不得不提到有名的哲学思想:“一切皆文件”。
socket 也遵从 Unix 一切皆文件的思想,是 Unix/Linux 中的一种特殊文件资源,以 “打开-读/写-关闭” 模式的实现。

那些关于 socket 封装好的接口其实就是对文件进行操作(读写,打开,关闭),服务端和客户端各自维护一个这样的文件,在双端建立连接后,内核会帮我们建立 socket 文件,可以通过向自己的文件写入和读取数据的方式与对方进行通信。双端的通信过程就如下图所示:

在这里插入图片描述
两端的通信更具体一点的话就是两个进程之间的通信,我们知道两个进程如果需要进行通信最基本的一个前提是:能够唯一的标识一个进程

在本地进程通讯中我们可以使用 PID(进程ID) 来唯一标识一个进程,但 PID 只在本地唯一,网络中两个进程的 PID 冲突概率很大,而我们知道 IP 层的 ip 地址可以唯一标识主机,而 TCP 层协议中的端口号可以唯一标识主机的一个进程,这样我们可以利用 “ip 地址+协议端口号” 唯一标识网络中的一个进程。

因此,两个进程的唯一标识就组成了一个四元组:ip:port - ip:port,这就是 socket 文件的唯一标识,(四元组中有一项不同,则 socket 不同),而对于我们应用层程序来说 socket 就是一个文件描述符,缩写为:FD。我们简单的再画一张图理解一下 socket 建立的过程。

在这里插入图片描述

2.2 socket 初体验

这里通过一个简单的例子带大家实际感受一下 socket 的存在(linux 环境 TCP 连接):

  1. 使用 netstat -natp 第一次查看 TCP 连接情况(作为对比使用)
  2. 抓包:tcpdump -nn -i eth0 port 80 (可以抓到数据包的传输过程,比如:三次握手)
  3. 与百度建立连接 nc www.baidu.com 80,抓包数据会出现三次握手数据包,表示连接建立完成
  4. 使用 netstat -natp 第二次查看 TCP 连接情况 (存在 nc 的连接)
  5. 发送请求 GET / HTTP/1.0 可以请求到百度的数据,接收到数据后,断开连接(数据包四次挥手)
  6. 前后两次查看 TCP 连接情况得到下图,对比发现多了一条 TCP 连接由 nc 持有,这个就是一个 socket 通信在 linux 的表现形式

在这里插入图片描述
7. 使用 lsof -op 1904 可以具体查询到 FD(socket)= 3u 的表现形式如下:

COMMAND   PID  USER   FD   TYPE     DEVICE    OFFSET   NODE  NAME
nc       1904  root   3u   IPv4      39179       0t0    TCP  node01:37732->111.206.208.133:http (ESTABLISHED)

本节小结

  1. socket起源于 unix, 本质是操作系统的一种特殊文件资源,以 “打开-读/写-关闭” 模式实现,对应用层暴露的接口都是对该文件资源的操作
  2. socket 以四元组 ip:port - ip:port 为唯一标识,四元组只要有一个不一样,就可以建立一个 socket;因此一个客户端(固定ip)与一个服务端(固定ip+端口)建立 socket 的最大个数,取决于客户端可用端口个数

3. socket 实战

阅读本节可回答的问题:服务端执行到了 listen 接口,但没有调用 accept 接口,能与客户端完成三次握手建立连接吗?客户端能给此时的服务端发送数据吗?

3.1 流程讲解

这里以 TCP 为例进行实战,真实感受一下 socket 的使用过程。提到 TCP 就必须得聊一聊“三次握手”的概念了:

在这里插入图片描述

  1. 服务器 listen 时,tcp 状态由 CLOSE 变为 LISTEN,并计算了全/半连接队列的长度,还申请了相关内存并初始化。这里会建立一个 “监听 socket”,用于监听客户端连接:
    a. 全连接队列:又称 Accept 队列(底层结构:双向链表,先进先出),包含了服务端所有完成了三次握手,但是还未被应用调用 accept 取走的连接队列。此时的 socket 处于 ESTABLISHED 状态。如果 accept 阻塞不被调用,那么很快全连接就可以被占满,此时再来客户端连接将超时,server 会舍弃 client 发送过来的 SYN,客户端一直重试,最终超时断开。
    b. 半连接队列:又称 SNY 队列(底层结构:Hash表,易于查找),服务端收到客户端的 SYN 包,回复 SYN+ACK 但是还没有收到客户端 ACK 情况下,会将连接信息放入半连接队列。
  2. 客户端 connect 时,把本地 socket 状态设置成了 TCP_SYN_SENT,选则一个可用的端口,发出 SYN 握手请求并启动重传定时器。
  3. 服务器响应客户端发出的 syn 时,会判断下接收队列是否满了,满的话会丢弃该请求。否则发出 syn ack,申请 request_sock 添加到半连接队列中,同时启动定时器。
  4. 客户端响应 syn ack 时,清除了 connect 时设置的重传定时器,把当前 socket 状态设置为 ESTABLISHED,开启保活计时器后发出第三次握手的 ack 确认。
  5. 服务器响应 ack 时,把对应半连接对象删除,创建了新的 sock 后加入到全连接队列中,最后将新连接状态设置为 ESTABLISHED。
  6. accept 从已经建立好的全连接队列中取出一个 socket(FD) 返回给用户进程持有,利用该 socket 资源进行通信使用。

3.1.2 传输数据
socket 内部结构中存在一个“接收缓冲区” 和 “发送缓冲区”(底层是个链表)

在这里插入图片描述
调用 send() 系统调用可以实现发送数据到一个 socket,linux 中 man send 可以查看 send 接口的作用和用法:

在这里插入图片描述
调用 recv() 系统调用可以实现从一个 socket 接收数据:man recv

在这里插入图片描述
这里不具体讲具体接口的使用,后续有时间写新文章补充。

3.2 linux + java 实战

这里使用 java 实现多线程的 BIO 模型进行实战,代码很简单,截图中对重要的地方进行了解读,值得一提的地方就是程序中多了一行阻塞:System.in.read(); 只有每次从终端读取一行数据才能继续往下执行,否则将阻塞在 accept() 函数之前,使得 accept 得不到调用。

  1. 启动服务端程序,建立 “监听 socket” (服务端特有socket类型),程序阻塞在 accept 之前
    在这里插入图片描述
  2. netstat -natp 查看服务端已经启动监听 socket,监听 9090 端口,可以匹配任意 ip + port 的客户端连接
    在这里插入图片描述
  3. 客户端发起连接 nc localhost 9090,可以通过 tcpdump -nn -i eth0 port 9090 抓包发现,已经完成三次握手,成功建立连接,此时的连接没有被服务端进程持有(还没有调用 accept 函数)
    在这里插入图片描述
  4. netstat -natp 查看客户端 socket 连接
    a. 属于客户端的 socket 已经建立,且被 nc 进程持有,可以随时使用(本地启动的客户端)
    b. 可以发现还有一个 “-” 持有的 socket 是服务端与客户端建立的连接(内核已经完成了与客户端的TCP三次握手,并建立连接,但没有任何进程持有,所以用 “-” 代替)-- 暂时称之为 socket-a
    在这里插入图片描述
  5. 客户端发送数据 hello,此时服务端仍然阻塞在 accept 之前:
    在这里插入图片描述
  6. socket-a 的 Recv-Q 已经有数据进来:hello(刚好6个字节),所以客户端此时可以给服务端发送数据,且每一个 socket 申请的资源里面会缓存发送的数据;普通 socket 中存储的是传输的数据,那“监听 socket” 里面存储的是什么呢?
    在这里插入图片描述
  7. Listen socket 监听连接:用于监听 9090 端口,存储客户段的连接,监听连接的 Recv-Q 是用于存储还未使用(未 accept)的客户端连接,也就是上文提到的全连接队列;当服务端程序不再阻塞(敲回车), 就可以通过 accept 函数读取里面的 socket 资源进行与客户端的通信,此时 socket-a 就有进程持有它了,就是我们启动的服务端程序的进程 1933
    在这里插入图片描述
    为了看的更加明显,使用 ss -na 找到我们的 socket,可以看到 Recv-Q 值为1,就是 socket-a,而 Send-Q 表示的是全连接队列总长度(max)
    在这里插入图片描述
    为了演示 Recv-Q 中存储的数据是什么,我们按上边步骤重来一遍,这次启动两个客户端对服务端进行连接,服务端依然阻塞在 accept 之前,可以看到 Recv-Q 值为2,有两个 “-” 持有的 socket
    在这里插入图片描述
    在这里插入图片描述
  8. 服务端(回车)解除 accept 阻塞,之前建立的 socket 就会被服务端持有和使用,Recv-Q 会被清空
    在这里插入图片描述
    在这里插入图片描述

本节小结

  1. 只要启动了监听的服务端,创建了 “监听socket”,当客户端进行连接时,内核会帮程序完成三次握手,此过程在内核中完成,和 accept 阻塞不阻塞无关
  2. 三次握手后,内核会创建 socket 资源,资源中有缓冲区,用于缓存客户端发送的数据,当服务端调用了 accept,可以持有该 socket 资源, 可以处理已缓存的数据
  3. 监听 socket 缓冲区中存储的是客户端的连接,分为半连接队列和全连接队列,完成三次握手且未被 accept 的连接会存储在全连接队列,已回复 SYN ACK 还未收到 ACK 的连接会存储在半连接队列中

以上就是本篇文章的全部内容了,希望对你们理解 socket 有所帮助。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值