【Linux第二十章】socket

前言 🚀

socket 编程时,最容易出现的问题,不是函数不会背,而是明明每个接口都见过,却不知道它们为什么要这样设计 。比如为什么 UDPsendto/recvfrom,而 TCP 却是 listen/accept/connect 这一套;为什么网络里总要强调大端、小端和网络字节序;为什么服务端通常绑定 INADDR_ANY,而不是写死某个 IP;再比如 accept 为什么会返回一个新的文件描述符。

这些问题如果孤立来看,会觉得零散;但一旦放回"网络通信到底在解决什么问题"这条主线里,逻辑就会很顺。socket 不是凭空冒出来的一组 API,而是操作系统把网络协议栈能力暴露给应用程序的一种编程接口。

这篇文章就围绕这条主线,把 socket 编程里最核心的内容串起来:从 UDPTCP 的差异,到网络字节序、地址结构、常用接口,再到 TCP 建连与断连流程,以及服务端为什么通常要以守护进程方式运行。


一. socket 到底是什么 🧠

socket 本质上是操作系统提供给应用层的网络编程接口 。应用程序并不会直接操作 TCP/IP 协议细节,而是通过 socket 这套系统调用接口,把数据交给内核中的网络协议栈处理。

从资源管理的角度看,网络通信和文件读写在 Unix/Linux 世界里都遵循"一切皆文件 "的思路,因此网络对象最终也会表现为文件描述符 fd。这也是为什么我们创建套接字后,得到的也是一个整数类型的描述符。

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

这里的 sockfd 和普通文件 fd 一样,都由进程的文件描述符表统一管理。区别只在于:普通文件背后对应磁盘文件,而网络 fd 背后对应的是内核中的网络通信对象。


二. UDPTCP:不是谁更好,而是设计目标不同 🔍

UDPTCP 的差异,不是"谁高级谁低级",而是它们针对的通信目标不同 。如果业务更关注实时性和低开销,就更偏向 UDP;如果更关注可靠性、有序性和完整交付,就更偏向 TCP

2.1 核心区别总览

特性 TCP UDP
连接方式 面向连接 无连接
可靠性 高,保证传输 低,可能丢失、乱序、重复
数据边界 无,面向字节流 有,面向数据报
速度与开销 较慢,控制机制更多 较快,头部和控制开销小
典型场景 Web、文件传输、数据库连接 音视频、在线游戏、DNS、广播/多播

2.2 TCP 的特点

TCP 是面向连接的协议,通信前要先建立连接,通信结束后还要正确关闭连接。它为了保证可靠传输,需要维护序号、确认、重传、流量控制、拥塞控制等机制,因此开销更高,但也更稳妥。

对开发者来说,TCP 最关键的特征是:它看到的是一条连续的字节流,而不是一条一条完整消息。

这意味着你调用两次 send(),对方未必一定用两次 recv() 刚好一一对应地收回来。应用层如果需要"消息边界",必须自己设计协议,比如:

  • 固定长度报文
  • 特殊分隔符
  • 长度前缀 + 正文

2.3 UDP 的特点

UDP 不需要预先建立连接,直接就可以发数据,因此它更轻量,延迟也更低。

同时,UDP 保留发送边界,也就是说一次 sendto() 对应的是一个独立的数据报。接收端用 recvfrom() 时,拿到的是一个完整报文,而不是像 TCP 那样的一段字节流。

但这份"简单"是有代价的:UDP 不保证一定送达、不保证顺序、不保证不重复。如果业务要可靠性,就得自己在应用层补。

💡 避坑指南:
UDP 不等于"差",TCP 也不等于"绝对更好"。
可靠性优先选 TCP,实时性优先选 UDP


三. 网络字节序:为什么网络里统一要求大端 🧱

不同机器在内存中存储整数时,可能采用不同字节序。于是就有了大端和小端之分。

  • 大端:高位字节放在低地址
  • 小端:低位字节放在低地址

如果两台机器字节序不同,又直接把多字节整数原样发送到网络上,就会出现解析不一致的问题。所以网络规定:所有进入网络的多字节整数,都必须按网络字节序存放。

而网络字节序约定为大端序

3.1 常用字节序转换函数

cpp 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

它们的命名可以直接拆开理解:

  • h:host,主机字节序
  • n:network,网络字节序
  • l:long,32 位
  • s:short,16 位

例如端口号是 16 位整数,就应该使用 htons()IPv4 地址常按 32 位整数处理,则可用 htonl()

3.2 需要纠正的一个误区

有时会把"hton 可能有线程安全问题"记混。实际上,htons/htonl/ntohs/ntohl 只是纯转换函数,不依赖静态全局缓冲区,通常是线程安全的。

真正更容易引发线程安全争议的,是像 inet_ntoa() 这类返回静态缓冲区指针的旧接口。现代代码里更推荐使用 inet_ntop()inet_pton() 这一组接口。


四. IP 地址转换与地址结构:sockaddr 为什么像"多态" 🧩

4.1 点分十进制和二进制格式之间的转换

网络编程里,IP 地址既有人类易读的点分十进制形式,也有协议栈内部处理的二进制形式,因此需要专门的转换函数。

常见老接口有:

  • inet_aton
  • inet_addr
  • inet_ntoa

更推荐的现代接口是:

  • inet_pton
  • inet_ntop
cpp 复制代码
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

这组接口兼容性更好,也更适合写出可移植代码。

4.2 sockaddrsockaddr_in

socket 接口设计时,需要兼容多种地址族,比如:

  • AF_INETIPv4
  • AF_INET6IPv6
  • AF_UNIX:本地套接字

因此内核 API 采用了一个统一的通用地址类型 struct sockaddr,而具体到 IPv4 时,实际使用的是 struct sockaddr_in

cpp 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

传参时再强制转换为通用类型:

cpp 复制代码
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

这种写法本质上是一种"统一接口 + 具体实现"的设计思路,和面向对象里常说的多态思想很像:对外统一使用 sockaddr*,对内根据地址族解释成具体结构。


五. UDP 编程主线:无连接,但不是没有对象关系 💻

UDP 没有建连过程,因此服务端和客户端的代码路径都相对简单。

5.1 典型 UDP 服务端流程

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_port = htons(8888);
local.sin_addr.s_addr = htonl(INADDR_ANY);

bind(sockfd, (sockaddr*)&local, sizeof(local));

char buffer[1024];
sockaddr_in peer{};
socklen_t len = sizeof(peer);

ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                     (sockaddr*)&peer, &len);

sendto(sockfd, buffer, n, 0, (sockaddr*)&peer, len);

5.2 为什么 recvfrom() 要拿来源地址

因为 UDP 没有连接状态,服务端在收到报文之前,并不知道是谁发来的。recvfrom() 返回时除了把数据交给你,还会顺便把来源主机的 IP + port 填出来,后续你才能用 sendto() 回过去。

5.3 127.0.0.1 与本地回环

127.0.0.1 是本地回环地址,只能用于本机内部测试。数据不会真正离开当前主机,因此非常适合本地调试网络程序。

如果服务端绑定到 127.0.0.1,那它就只能接受本机发来的连接或报文,外部主机无法访问。

5.4 服务端为什么常绑定 INADDR_ANY

很多学习资料一开始会把服务端绑定到某个具体 IP,这样便于理解;但在真实部署里,更常见的做法是绑定任意地址 INADDR_ANY

cpp 复制代码
local.sin_addr.s_addr = htonl(INADDR_ANY);

原因很简单:一台机器可能有多个网卡、多个 IP,如果你把服务端写死绑定到其中某一个地址,可能会导致其他网卡来的报文无法被这个服务接收。

因此,大多数服务端程序更关注"监听哪个端口",而不是"只监听哪个单独 IP"。

💡 避坑指南:

本地测试常用 127.0.0.1,但真正对外服务时,通常更推荐绑定 INADDR_ANY
本地回环适合调试,任意地址更适合通用服务监听。


六. TCP 编程主线:先建连接,再通信 🗺️

UDP 不同,TCP 是面向连接的,所以服务端与客户端都要先完成连接建立。

6.1 TCP 服务端典型流程

cpp 复制代码
int listenfd = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = htonl(INADDR_ANY);

bind(listenfd, (sockaddr*)&local, sizeof(local));
listen(listenfd, 5);

sockaddr_in peer{};
socklen_t len = sizeof(peer);
int connfd = accept(listenfd, (sockaddr*)&peer, &len);

6.2 listenfdconnfd 为什么不是一个东西

这是 TCP 服务端最重要的接口语义之一。

  • listenfd:监听套接字,只负责"接收新的连接请求"
  • connfd:已连接套接字,只负责"和某个客户端进行具体数据通信"

也就是说,accept() 之所以返回一个新的文件描述符,就是因为监听和通信本来就是两种不同职责。监听 socket 要继续留着接待后续客户端,而当前这个客户端则交给新的已连接 socket 去处理。

6.3 客户端典型流程

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);

connect(sockfd, (sockaddr*)&server, sizeof(server));

连接建立成功后,双方就可以使用 read/writesend/recv 进行数据收发。


七. 面向字节流 vs 面向数据报:真正影响编程方式的核心差异 ⚠️

理解 TCPUDP,最关键的不只是"可靠 / 不可靠",而是要真正吃透:

  • TCP面向字节流
  • UDP面向数据报

7.1 TCP:没有天然消息边界

TCP 看来,发送方写进去的是一串连续字节,接收方读出来的也是连续字节。你发送三次,接收方可能一次读完;你发送一次,接收方也可能分多次才读全。

因此,TCP 应用层必须自己处理"包边界"问题,例如:

  • 先发长度,再发内容
  • 用特殊结束符做分隔
  • 定长报文

7.2 UDP:天然保留一次发送的一条报文

UDP 则不同,一次 sendto() 发出去的就是一个完整数据报,对方一次 recvfrom() 拿到的也是一整个报文。消息边界天然存在,因此协议设计更直接。


八. socket 常用接口如何串成一条主线 🧩

8.1 常见 API 关系图

UDP
TCP
socket
bind
协议类型
recvfrom / sendto
listen
accept
read/write 或 send/recv

8.2 各接口的职责

  • socket():创建套接字对象
  • bind():给 socket 绑定本地地址和端口
  • listen():把 TCP socket 变成监听状态
  • accept():取出一个已完成握手的新连接
  • connect():客户端主动发起连接
  • sendto()/recvfrom():无连接数据收发
  • send()/recv()read()/write():有连接数据收发

一旦把这些接口按"协议差异 + 生命周期阶段"来理解,就不会再把它们当成一堆零散函数去死记硬背。


九. 服务端为什么通常要做成守护进程 🧱

网络服务一般不适合以前台交互进程的方式运行。真正的服务端程序通常需要:

  • 长时间驻留后台
  • 脱离终端控制
  • 避免因用户退出 shell 而中断
  • 能稳定处理日志、连接和任务

因此,服务端程序常常会被设计为守护进程(daemon)。

9.1 守护进程的关键特征

守护进程通常运行在后台,脱离控制终端,生命周期也更长,适合承担网络服务、日志服务、定时任务等工作。

很多资料会说"守护进程一定是孤儿进程",这属于教学语境里的简化表达。更准确地说:

  • 经典守护化过程通常会让父进程退出,从而让子进程被 init/systemd 接管;
  • 守护进程的本质是脱离终端、独立会话、长期后台运行,而不是单纯抓住"孤儿"这个标签。

9.2 /dev/null 的作用

守护化过程中,常常会把标准输入、标准输出、标准错误重定向到 /dev/null,从而避免后台进程继续占用终端。写入 /dev/null 的数据会被直接丢弃,读取它则通常立即返回文件结束。


十. 三次握手与四次挥手:把连接生命周期看完整 🗺️

TCP 之所以可靠,一个重要原因就是它把"连接建立"和"连接关闭"都设计成了明确的协议过程。

10.1 三次握手

客户端和服务端在真正通信之前,需要先完成三次握手,目的是:

  1. 确认双方收发能力正常
  2. 同步初始序列号
  3. 明确连接建立的双方状态

简化过程如下:
服务端 客户端 服务端 客户端 SYN SYN + ACK ACK

握手完成之后,客户端与服务端才进入 ESTABLISHED 状态,正式开始数据传输。

10.2 四次挥手

断开连接时,通常需要四次挥手。原因在于:TCP 是全双工的,双方的发送方向都需要独立关闭。
服务端 客户端 服务端 客户端 FIN ACK FIN ACK

所以"四次"并不是机械规定,而是由双向关闭、分步确认这个协议设计决定的。


十一. 面试高频:UDPTCP 真正该怎么选 📌

11.1 什么时候优先选 UDP

当业务更看重低延迟、实时性、发送简单直接时,通常优先考虑 UDP,例如:

  • 实时音视频
  • 在线游戏状态同步
  • 广播与多播
  • DNS 这类短消息查询

11.2 什么时候优先选 TCP

当业务更看重可靠交付、有序传输和完整性时,通常优先考虑 TCP,例如:

  • 网页访问
  • 数据库连接
  • 文件传输
  • 绝大多数业务系统接口调用

11.3 一句话总结选型原则

实时性优先选 UDP,可靠性优先选 TCP


总结 📝

socket 编程并不是在背几个系统调用,而是在学习应用程序如何借助操作系统的网络协议栈完成通信。从这个角度出发,很多细节都会自然串起来:

  • socket 得到的是网络文件描述符,因为网络对象也由内核统一管理;
  • UDPTCP 的差异,源于连接语义、边界语义和可靠性目标不同;
  • 网络字节序是为了解决异构机器之间的多字节整数解析一致性问题;
  • sockaddr 统一了多种地址类型的接口形式;
  • TCP 服务端之所以有 listenfdconnfd,是因为监听连接和处理连接本身就是两个职责;
  • 真正部署网络服务时,通常还要进一步考虑后台运行、日志和守护化问题。

学到这里,socket 这部分最重要的框架其实已经建立起来了:地址怎么表示,协议怎么选,接口怎么串,连接怎么建立,数据怎么收发,服务怎么长期运行。

后面继续深入 socket,无论是多进程版服务器、多线程版服务器,还是 select/poll/epoll 这类高并发模型,本质上都只是沿着这条主线继续展开。

相关推荐
HLC++11 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室13 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy15 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕16 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫17 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生17 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1119 小时前
linux基础IO
linux·运维·服务器
BelongPanda20 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯21 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983421 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器