「Linux 网络编程」Socket 构建 TCP 本地通信
0 前言
在 Linux 网络编程中,TCP 本地通信是最基础的技术之一。本文将会详细介绍如何使用 Linux socket API 构建完整的 TCP 本地通信。
1 TCP 本地通信基础
1.1 TCP 协议的特点
- 面向连接:通信前需要建立连接
- 可靠传输:保证数据顺序和完整性
- 流式传输:数据作为字节流传输
- 全双工:双方可以同时发送和接收数据
TCP 收发数据是无消息边界的:TCP 把数据看作一连串无结构的字节流,发送方调用 send()
多次发送的数据,在接收方可能被合并成一次 recv()
接收,也可能一次发送的数据被拆分成多次接收。
「Linux 网络编程」计算机网络核心知识 6.3 TCP协议
1.2 本地回环地址
使用 本地回环地址(127.0.0.1
)进行本地通信:
- 无需网络:不依赖物理网络设备
- 高效安全:数据在系统内部传输,速度快且安全
- 开发便利:轻松实现单机测试和开发
「Linux 网络编程」计算机网络核心知识 5.2.5 本地回环地址
1.3 什么是 Socket 套接字
Socket 套接字是网络通信的端点,是不同主机上的应用程序之间进行双向数据传输的通道接口。
可以把 Socket 套接字想象成一个虚拟的“电话插座”。可以用一个打电话的过程来类比网络通信:
- 电话系统 (网络):整个互联网或局域网就像是一个庞大的电话网络系统。
- IP 地址 (电话号码):每台联网的计算机都有一个唯一的IP地址,这就像家里的 电话号码。别人通过这个号码才能找到你。
- 端口号 (分机号):一台计算机上可能同时运行着多个网络程序(比如浏览器、微信、游戏)。端口号就像公司总机下的 分机号,例如:
80
端口是给网页服务(HTTP)的21
端口是给文件传输(FTP)的
- 套接字 (完整的电话连接):一个套接字由 IP 地址 + 端口号 来唯一标识。就像是 “电话号码 + 分机号” 的组合。
- 服务端:像一个呼叫中心,它先创建一个套接字(安装一个总机),绑定自己的IP和端口号(公布总机号码),然后等待来电。
- 客户端:像一位客户,它创建一个套接字(拿起话筒),拨打服务器的IP和端口号(拨打总机号码+分机号)。
- 连接 (通话建立):当客户端和服务器端的套接字成功连接后,就像电话接通了。双方可以通过这个“电话线路”(套接字连接)进行双向的数据收发(对话)。
套接字在 TCP 通信中的工作步骤:
-
服务器端监听
- 服务器程序创建一个套接字。
- 将套接字与自己的 IP地址 和某个 端口号 绑定。
- 开始监听这个端口,等待客户端的连接请求。
-
客户端连接
- 客户端程序创建一个套接字。
- 它知道服务器的 IP地址 和 端口号。
- 客户端向服务器发起连接请求。
-
建立连接
- 服务器接受客户端的连接请求。建立一个稳定的、双向的通信通道。操作系统会为此连接 创建一个新的套接字 来专门处理这个客户端。
-
数据传输
- 连接建立后,双方就可以通过各自的套接字使用
send()
(发送)和recv()
(接收)等函数来读写数据。(就好像电话两头说的话)
- 连接建立后,双方就可以通过各自的套接字使用
-
关闭连接
- 通信结束后,任何一方都可以关闭套接字,断开连接。(就像挂断电话一样)
套接字原理:
在通信过程中,套接字一定是成对出现的。一个文件描述符指向一个套接字(套接字内部由内核借助两个缓冲区实现读写)
1.4 TCP 本地通信流程
2 网络字节序、IP 转换及 sockaddr 结构
2.1 网络字节序
2.1.1 什么是字节序
字节序(Byte Order)指的是多字节数据在内存中的存储顺序。在计算机系统中,主要有两种字节序:
1 |
|
- 大端序:就像写数字一样,从左到右,高位在前(如:1234 读作"一千两百三十四")
- 小端序:就像有些国家的日期写法,日/月/年,低位在前
2.1.2 为什么需要网络字节序
- 不同架构的 CPU 使用不同的字节序
- Intel x86 系列使用小端字节序
- 某些处理器(如 PowerPC)使用大端字节序
- 网络设备可能使用不同的字节序
TCP/IP 协议栈规定使用 大端字节序 作为网络字节序,所有在网络中传输的多字节数据都必须使用网络字节序。
2.1.3 字节序转换函数
Linux 提供了一组完整的字节序转换函数:
1 |
|
h
= host(主机)n
= network(网络)l
= long(32位)s
= short(16位)
2.1.4 代码示例
1 |
|
输出结果:
1 |
|
2.2 IP 地址转换
2.2.1 为什么需要 IP 地址转换
在网络编程中,IP 地址有三种常见表示形式:
- 字符串形式:人类可读的格式,如 “192.168.1.1”
- 32 位整数形式:计算机内部使用的二进制格式
- in_addr 结构体:socket API 使用的格式
2.2.2 转换函数:inet_pton 和 inet_ntop
1 |
|
参数详解:
参数 | 说明 | 常用值 |
---|---|---|
af |
地址族 | AF_INET (IPv4), AF_INET6 (IPv6) |
src |
源地址 | 字符串或二进制IP指针 |
dst |
目标地址 | 二进制或字符串IP指针 |
size |
缓冲区大小 | INET_ADDRSTRLEN (IPv4), INET6_ADDRSTRLEN (IPv6) |
返回值:
inet_pton
:成功返回1
,无效输入返回0
,错误返回-1
inet_ntop
:成功返回目标字符串指针,错误返回 NULL
2.2.3 代码示例
1 |
|
1 |
|
以 IPv4 地址转换 192.168.1.100
为例,首先将 4 个部分分别转换为 二进制,例如 192 -> 11000000
、168 -> 10101000
、1 -> 00000001
、100 -> 01100100
,组合起来得到网络字节序(大端序) 0xc0a80164
。 在小端机器上,当我们用%X打印整个32位数时,字节顺序是反的,所以显示为 0x6401a8c0
。
2.3 sockaddr 结构
2.3.1 为什么需要 sockaddr 结构
- 提供统一的地址结构接口
- 支持多种协议族(IPv4、IPv6、Unix域套接字等)
- 保持 API 的向后兼容性
2.3.2 通用地址结构:sockaddr
1 |
|
- 大小固定为 16 字节
- 前 2 字节标识地址族类型
- 后 14 字节存储具体的地址信息
2.3.3 IPv4 专用结构:sockaddr_in
1 |
|
2.3.4 IPv6专用结构:sockaddr_in6
1 |
|
3 TCP 服务端构建
3.1 服务端构建流程
3.2 服务端构建相关函数
3.2.1 socket(): 创建通信端点
1 |
|
参数说明:
domain
:协议族,AF_INET
(IPv4) 或AF_INET6
(IPv6)type
:套接字类型,SOCK_STREAM
(TCP) 或SOCK_DGRAM
(UDP)protocol
:通常设为 0,由系统自动选择
代码示例:
1 |
|
3.2.2 bind(): 绑定地址和端口
1 |
|
地址结构体设置:
1 |
|
绑定示例:
1 |
|
3.2.3 listen(): 设置监听上限
1 |
|
参数说明:
backlog
:连接队列的最大长度,表示可以排队等待接受的连接数。
示例:
1 |
|
3.2.4 accept(): 接受客户端连接
1 |
|
接受连接示例:
1 |
|
3.2.5 recv() 和 send(): 数据收发函数
1 |
|
4 TCP 客户端构建
4.1 客户端构建流程
4.2 客户端构建相关函数
4.2.1 connect(): 连接服务器
默认情况下,客户端不需要指定自己的端口和IP,系统会为其分配一个可用的临时端口和合适的本地IP地址。
在客户端代码中,我们通常不调用 bind()
,而是直接调用 connect()
,这时内核会为套接字自动分配一个本地IP和临时端口。
1 |
|
设置服务器地址:
1 |
|
连接示例:
1 |
|
5 完整 TCP 本地通信示例
5.1 TCP 服务端完整代码
1 |
|
5.2 TCP 客户端完整代码
1 |
|
5.3 程序测试
编译程序
1 |
|
运行程序
1 |
|
输出结果示例
1 |
|
6 TCP 的数据无边界性
TCP 的 数据无边界性 意味着发送端多次发送的数据可能在接收端一次接收,或者一次发送的数据可能被分多次接收。
6.1 数据无边界性服务端代码
1 |
|
6.2 数据无边界性客户端代码
1 |
|
6.3 数据无边界性测试
1 |
|
TCP 无边界性可能出现的情况:
-
粘包现象:多个小消息可能在一次接收中到达
客户端发送 5 条独立消息,服务端可能 1-2 次就接收完
-
拆包现象:大消息可能被分割成多次接收
客户端发送 3000 字节,服务端可能分 3-4 次接收
-
接收次数不确定性:相同的发送模式可能产生不同的接收模式
输出结果如下:
- 服务端输出:
1 |
|
- 客户端输出:
1 |
|
在本地回环(lo)接口上,由于网络延迟几乎为零,内核优化会倾向于立即发送数据。所以在上述实际运行结果中并没有遇到粘包的情况。