Socket 详解:应用程序如何进行网络通信

上一篇《TCP/IP 详解:理解互联网通信的基础》文章介绍了 TCP/IP。

我们知道:

应用层
   ↓
TCP / UDP
   ↓
IP
   ↓
网络

TCP/IP 负责完成网络通信,但应用程序并不会直接操作 TCP 报文或者 IP 数据包。

应用程序通常通过操作系统提供的 Socket API 来使用网络能力。

因此,如果 TCP/IP 是网络通信的基础,那么 Socket 就是:

应用程序进入网络世界的接口。

对于后端开发来说,理解 Socket 非常重要。

HTTP Server、MySQL、Redis、RPC、WebSocket,以及各种 TCP 长连接服务,底层都离不开 Socket。

1. Socket 到底是什么?

Socket 通常翻译为“套接字”。

它不是一个网络协议,而是一种网络通信接口和抽象。

应用程序可以通过 Socket:

创建通信端点
绑定地址
建立连接
发送数据
接收数据
关闭连接

例如 TCP 服务端的典型流程:

socket()
   ↓
bind()
   ↓
listen()
   ↓
accept()
   ↓
read() / write()
   ↓
close()

客户端则通常是:

socket()
   ↓
connect()
   ↓
read() / write()
   ↓
close()

因此可以把 Socket 理解成:

应用程序
    │
    │ Socket API
    ↓
操作系统网络协议栈
    ↓
TCP / UDP
    ↓
IP
    ↓
网卡

应用程序通过 Socket API 操作网络,而具体的数据传输由操作系统协议栈完成。


2. TCP Socket 是如何工作的?

以一个最基本的 TCP Server 为例。

假设服务器监听:

192.168.1.100:8080

客户端:

192.168.1.10

服务器大致执行:

socket()
   ↓
bind(192.168.1.100:8080)
   ↓
listen()
   ↓
accept()
   ↓
获得一个连接 Socket

客户端:

socket()
   ↓
connect(192.168.1.100:8080)

双方建立 TCP 连接之后,就可以:

send / write
        ↕
recv / read

进行数据交换。

可以抽象成:

客户端                         服务端

socket()                      socket()
   │                              │
   │                           bind()
   │                              │
   │                           listen()
   │                              │
connect() ───────────────────→ accept()
   │                              │
   └──────── TCP Connection ──────┘
                  │
             read / write
                  ↕
               数据通信

这里有一个非常重要的概念:

监听 Socket 和连接 Socket 不是同一个东西。

服务器调用:

listen()

之后得到的是监听端点。

当客户端连接进来:

accept()

会返回一个新的 Socket,用于处理这个具体的客户端连接。

因此服务器可以:

listen socket
      │
      ├── connection socket A
      ├── connection socket B
      ├── connection socket C
      └── connection socket D

一个监听端口可以对应大量并发 TCP 连接。


3. bind、listen、accept 分别做什么?

这三个 API 经常一起出现。

bind

bind() 将 Socket 与本地地址绑定。

例如:

192.168.1.100:8080

也就是告诉操作系统:

这个 Socket 使用哪个本地 IP 和端口。

对于服务器来说通常需要显式 bind()。


listen

listen() 将一个 TCP Socket 转换为监听状态。

也就是说:

我准备接受客户端发起的连接。

它只适用于面向连接的 Socket,例如 TCP。


accept

accept() 从已经建立/准备建立的连接队列中取出一个连接,并返回新的连接 Socket。

因此:

listen socket

负责:

接收新的连接。

而:

connected socket

负责:

和具体客户端交换数据。

这个区别非常重要。


4. connect 做了什么?

客户端通常调用:

connect()

连接服务器:

connect(server_ip, server_port)

例如:

connect(
    "192.168.1.100",
    8080
)

对于 TCP Socket 来说,connect() 会触发 TCP 连接建立过程。

也就是说:

connect()
   ↓
TCP 三次握手
   ↓
连接建立

上一篇介绍的 TCP 三次握手,就是发生在这里。

应用程序调用的是:

connect()

操作系统网络协议栈负责完成:

SYN
SYN + ACK
ACK

因此:

Socket API 是应用程序接口,TCP 三次握手属于底层协议栈行为。


5. 一个 TCP Socket 连接由什么确定?

一个 TCP 连接可以用四元组表示:

源 IP
源端口
目标 IP
目标端口

例如:

192.168.1.10:52143
        ↓
192.168.1.100:8080

即:

Source IP      = 192.168.1.10
Source Port    = 52143
Destination IP = 192.168.1.100
Destination Port = 8080

这四个信息组合起来,可以唯一标识一条 TCP 连接。

因此,同一个服务器:

192.168.1.100:8080

可以同时服务:

192.168.1.10:52143
192.168.1.11:52144
192.168.1.12:52145
...

服务器并不是只能建立一个 TCP 连接。

这也是 TCP 能够支持大量并发连接的基础之一。


6. Socket 的阻塞与非阻塞

这是网络编程中非常重要的概念。

默认情况下,很多 Socket API 可以表现为阻塞式操作。

例如:

read()

如果当前没有数据:

read()
   ↓
等待
   ↓
数据到达
   ↓
返回

程序会阻塞在这里。

这种方式非常容易理解:

while (true) {
    $data = read($socket);

    process($data);
}

但如果同时存在:

10000 个连接

一个线程阻塞等待一个连接,就很难高效处理这么多连接。

于是就出现了:

非阻塞 IO + IO 多路复用。


7. 什么是 IO 多路复用?

IO 多路复用解决的问题可以简单描述为:

一个线程如何同时管理大量 Socket?

Linux 中常见的机制包括:

select
poll
epoll

以 epoll 为例。

程序不需要:

连接 A → 等
连接 B → 等
连接 C → 等
连接 D → 等

而是:

             epoll
               │
       ┌───────┼───────┐
       ↓       ↓       ↓
     Socket  Socket  Socket
       A       B       C

程序告诉内核:

帮我监控这些 Socket,哪个准备好了告诉我。

当某个 Socket 有数据:

Socket B
   ↓
可读事件
   ↓
epoll_wait()
   ↓
应用程序处理

于是一个线程就可以管理大量连接。

这也是现代高性能网络服务器非常重要的基础。


8. Reactor 与 Socket 有什么关系?

在高性能服务器中,经常会看到:

Reactor
Event Loop
Epoll
Non-blocking IO
Socket

它们并不是同一个概念。

可以理解为:

Socket
  ↓
提供网络通信端点

Epoll
  ↓
监控大量 Socket 的 IO 事件

Event Loop
  ↓
不断获取并处理事件

Reactor
  ↓
将 IO 事件分发给对应处理逻辑

例如:

             Event Loop
                  │
             epoll_wait()
                  │
       ┌──────────┼──────────┐
       ↓          ↓          ↓
   Socket A    Socket B    Socket C
       │          │          │
     read       read       write
       │          │          │
       ↓          ↓          ↓
    Handler    Handler    Handler

这也是:

  • Nginx

  • Node.js

  • Swoole

  • Hyperf

等高性能网络服务体系中经常涉及的核心思想。

具体实现虽然不同,但核心思想都是:

用事件驱动的方式高效管理大量网络连接。


9. TCP 为什么会出现粘包和拆包?

这是 Socket 编程最经典的问题之一。

假设客户端连续发送:

Hello
World

应用程序可能认为:

消息 1 = Hello
消息 2 = World

但 TCP 并不知道什么叫“消息”。

TCP 提供的是:

可靠、有序的字节流。

所以接收端可能读到:

HelloWorld

也可能:

Hello
World

也可能:

Hel
loW
orld

甚至:

HelloW
orld

这些都可能发生。

因此所谓“TCP 粘包/拆包”,更准确地说,是:

应用层消息边界与 TCP 字节流边界不一致。

TCP 不负责维护应用层消息边界。


10. 如何解决 TCP 粘包和拆包?

既然 TCP 不提供消息边界,那么应用层就必须自己定义协议。

常见方案有三种。

固定长度

例如规定每条消息:

1024 bytes

不足补齐。

接收端每次读取固定长度。

优点是简单。

缺点是空间利用率较低,而且消息长度受到限制。


分隔符

例如:

Hello\n
World\n

使用:

\n

作为消息结束标记。

接收端不断读取数据,直到找到分隔符。

这种方式在文本协议中比较常见。


长度字段

更通用的方式是:

┌────────────┬──────────────────┐
│ Length     │ Payload          │
│ 4 Bytes    │ N Bytes          │
└────────────┴──────────────────┘

例如:

00000005Hello

前 4 个字节表示 Payload 长度。

接收端:

读取长度
   ↓
读取指定长度
   ↓
得到完整消息

很多二进制协议都会采用类似设计。

例如:

Length + Request ID + Type + Payload

这也是 RPC、消息协议设计中非常常见的方式。


11. Socket 和 HTTP 有什么关系?

这是后端开发中最容易混淆的问题之一。

HTTP 是:

应用层协议。

Socket 是:

应用程序使用网络通信能力的接口。

例如传统 HTTP/1.1:

HTTP
 ↓
TCP
 ↓
IP
 ↓
Network

而应用程序通常通过:

Socket API

来使用 TCP。

因此:

HTTP
   ↓
Socket
   ↓
TCP
   ↓
IP

更准确地说,HTTP 本身并不等于 Socket。

可以把它理解为:

Socket = 通信接口
TCP    = 传输协议
HTTP   = 应用层通信协议

例如 PHP 程序可以直接使用 TCP Socket:

客户端
   │
   │ TCP Socket
   ↓
服务器

也可以在 TCP Socket 之上实现 HTTP:

客户端
   │
 HTTP
   ↓
TCP Socket
   ↓
TCP/IP

因此:

HTTP 可以运行在 TCP 之上,而 Socket 是应用程序使用 TCP 的接口之一。


12. Socket 和 WebSocket 又是什么关系?

WebSocket 的名字中虽然包含 Socket,但它和操作系统 Socket 不是一个概念。

WebSocket 是一种应用层通信协议。

典型关系是:

WebSocket
    ↓
TCP
    ↓
IP
    ↓
Network

浏览器中的 WebSocket API:

const socket = new WebSocket("wss://example.com/ws");

最终仍然需要通过底层网络连接完成通信。

因此:

操作系统 Socket
        ↓
TCP
        ↓
WebSocket

可以把 WebSocket 理解为:

建立在 TCP 之上的应用层实时双向通信协议。


13. PHP 中如何理解 Socket?

以 PHP 为例,可以直接使用 Socket 扩展提供的 API。

一个极简 TCP Server 的逻辑类似:

$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_bind($server, '0.0.0.0', 8080);

socket_listen($server);

while (true) {
    $client = socket_accept($server);

    $data = socket_read($client, 1024);

    socket_write($client, $data);

    socket_close($client);
}

它对应:

socket_create()
      ↓
创建 Socket

socket_bind()
      ↓
绑定 IP + Port

socket_listen()
      ↓
监听连接

socket_accept()
      ↓
接受客户端连接

socket_read()
      ↓
读取数据

socket_write()
      ↓
发送数据

socket_close()
      ↓
关闭连接

这个例子只是为了理解 Socket 的基本生命周期。

它并不是生产级网络服务器。

真正的高并发服务还需要考虑:

非阻塞 IO
IO 多路复用
事件循环
连接管理
读写缓冲区
协议解析
超时
心跳
异常处理
优雅关闭

这也是为什么现代 PHP 网络框架通常不会让业务代码直接管理大量底层 Socket。

例如 Swoole 会把大量网络 IO、事件循环和连接管理工作下沉到扩展层。


14. Socket 连接关闭

TCP Socket 最终需要关闭。

典型过程:

close()
   ↓
TCP 连接关闭

TCP 的正常关闭通常涉及四次挥手:

客户端                         服务端

FIN -------------------------->

      <----------------------- ACK

      <----------------------- FIN

ACK -------------------------->

实际关闭过程还涉及 TCP 状态转换,例如:

FIN_WAIT_1
FIN_WAIT_2
TIME_WAIT
CLOSE_WAIT
LAST_ACK

因此在生产环境中看到大量:

TIME_WAIT
CLOSE_WAIT

时,不能简单认为:

“TCP 连接太多了。”

两者产生的原因完全不同。

例如:

TIME_WAIT

通常与主动关闭连接的一方有关。

而:

CLOSE_WAIT

表示对端已经关闭连接,但本地应用程序还没有完成关闭。

如果 CLOSE_WAIT 长时间大量堆积,通常应该重点检查应用程序是否正确关闭 Socket。


15. Socket 编程真正需要关注什么?

从简单的:

socket()
bind()
listen()
accept()
read()
write()
close()

到生产级网络服务,中间还有大量工程问题。

一个可靠的 Socket 服务通常需要解决:

连接管理
    ↓
超时管理
    ↓
读写缓冲
    ↓
消息边界
    ↓
粘包拆包
    ↓
心跳
    ↓
断线重连
    ↓
异常处理
    ↓
限流
    ↓
连接数控制
    ↓
事件循环
    ↓
高并发

这也是 Socket 与普通文件 IO 最大的区别之一:

网络连接是不稳定的外部资源,服务端必须持续面对延迟、断开、半连接、超时和异常。


16. 总结

Socket 本身不是 TCP,也不是 HTTP。

它们处于不同的层次:

┌─────────────────────────┐
│ HTTP / WebSocket        │ 应用层协议
├─────────────────────────┤
│ Socket API              │ 应用程序通信接口
├─────────────────────────┤
│ TCP / UDP               │ 传输层
├─────────────────────────┤
│ IP                      │ 网际层
├─────────────────────────┤
│ Ethernet / Wi-Fi        │ 链路层
└─────────────────────────┘

对于后端开发者,可以记住这几个核心关系:

IP
→ 负责主机寻址和数据包转发

TCP
→ 提供可靠、有序的字节流传输

Socket
→ 应用程序使用网络通信能力的接口

HTTP
→ Web 应用层通信协议

WebSocket
→ 基于 TCP 的实时双向通信协议

TCP 的核心是字节流,所以 Socket 编程必须自己解决应用层消息边界。

而当连接数量从几十、几百增长到几万甚至更多时,传统的:

一个连接
    ↓
一个线程
    ↓
阻塞等待

就会逐渐遇到资源和调度方面的瓶颈。

这也是:

非阻塞 IO
     ↓
IO 多路复用
     ↓
Epoll
     ↓
Event Loop
     ↓
Reactor

这些技术出现的重要背景。

掌握这些概念之后,再来看 HTTP 就会非常自然:

浏览器
   ↓
HTTP
   ↓
TCP Socket
   ↓
TCP
   ↓
IP
   ↓
网络

另一篇 应用层 HTTP 详解 文章:

《HTTP 详解:一次请求背后发生了什么?》

参考资料

  1. RFC 9293 — Transmission Control Protocol (TCP) TCP 当前核心规范,定义 TCP 的连接、可靠传输、状态机、序列号、确认机制等。 RFC 9293 — TCP

  2. RFC 1122 — Requirements for Internet Hosts - Communication Layers 对 Internet 协议体系以及 TCP、UDP、IP 等协议的职责进行了规范性描述。 RFC 1122

  3. Linux socket(7) — Linux Manual Page Linux Socket 接口、Socket 类型以及相关行为的官方手册。 socket(7) — Linux manual page

  4. Linux epoll(7) — Linux Manual Page Linux epoll IO 多路复用机制的官方手册。 epoll(7) — Linux manual page