前言

内容来源:本文章为阅读《Linux高性能服务器编程》第五章.Linux网络编程基础API所记录的部分笔记。
摘要:这篇文章围绕 Linux 网络编程基础 API 展开,介绍了 socket 地址(字节序、通用 / 专用地址结构、IP 转换)、socket 创建 / 命名 / 监听 / 接受 / 发起连接的核心函数(socket/bind/listen/accept/connect)、连接关闭的两种方式(close/shutdown),以及 TCP(recv/send、带外数据)和 UDP(recvfrom/sendto)的数据读写接口。

1.socket地址API

1.1 主机字节序和网络字节序

一、字节序的核心概念

  1. 定义:多字节数据(如 32 位整数)在内存中的存储顺序,分为两种:
    • 大端字节序:高位字节存在内存低地址,低位字节存在高地址;
    • 小端字节序:高位字节存在内存高地址,低位字节存在低地址。
  2. 影响:不同字节序的主机直接传递数据,会导致接收端解析错误。

二、主机字节序与网络字节序

  1. 主机字节序:当前机器的字节序(现代 PC 大多是小端);
  2. 网络字节序:网络传输的统一标准(强制为大端),解决不同主机字节序不兼容的问题。

三、字节序的适用场景

四、Linux 下的字节序转换函数
头文件:<netinet/in.h>
4 个核心函数(作用是主机字节序 ↔ 网络字节序):

函数名 含义(host ↔ network) 处理数据类型 常用场景
htonl 长整型(32 位)主机→网络 unsigned long 转换 IP 地址
htons 短整型(16 位)主机→网络 unsigned short 转换端口号
ntohl 长整型(32 位)网络→主机 unsigned long 解析 IP 地址
ntohs 短整型(16 位)网络→主机 unsigned short 解析端口号

1.2 通用socket地址

一、基础 Socket 地址结构:struct sockaddr
image.png
定义与成员
- 头文件:<bits/socket.h>
- 成员:
- sa_family:地址族(与协议族对应,如AF_INET对应 IPv4);
- sa_data[14]:存放 Socket 地址值,但仅 14 字节,空间不足。
二、协议族与地址族的关系

  1. 定义与特点
    • 头文件:<bits/socket.h>
    • 成员:
      • sa_family:地址族;
      • __ss_align:保证内存对齐;
      • __ss_padding:填充字段,总空间 128 字节(足够容纳所有协议族的地址)。
  2. 作用:提供足够大的空间存放任意协议族的地址,且内存对齐。

1.3 专用socket地址

一、专用 Socket 地址结构的设计原因
通用结构(sockaddr/sockaddr_storage)操作不便(比如设置 IP / 端口需要位操作),因此 Linux 为每个协议族设计了专用地址结构,字段更直观、易操作。
二、各协议族的专用地址结构

  1. UNIX 本地域协议族:struct sockaddr_un
    image.png
  1. TCP/IP 协议族:IPv4 专用struct sockaddr_in
    image.png
  1. TCP/IP 协议族:IPv6 专用struct sockaddr_in6
    image.png

1.4 IP地址转换函数

一、IP 地址转换的必要性
人们习惯用可读性字符串表示 IP(如 IPv4 的 “点分十进制”、IPv6 的 “十六进制”),但编程中需将其转为网络字节序的整数形式才能使用;记录日志时则需反向转换。
二、IPv4 专用转换函数(旧版)
image.png
头文件:<arpa/inet.h>,仅支持 IPv4:

函数名 功能 注意事项
inet_addr 点分十进制字符串 → 网络字节序整数 失败返回INADDR_NONE
inet_aton 点分十进制字符串 → 网络字节序整数(结果存入struct in_addr 成功返回 1,失败返回 0;比inet_addr更安全(避免INADDR_NONE的歧义)
inet_ntoa 网络字节序整数 → 点分十进制字符串 内部用静态变量存储结果,不可重入(多次调用会覆盖之前的结果)

三、inet_ntoa的不可重入性
由于inet_ntoa用静态变量存结果,多次调用会导致前一次的结果被覆盖:
运行

// 示例:两次调用inet_ntoa,szValue1和szValue2会指向同一个静态内存
char* szValue1 = inet_ntoa("1.2.3.4");
char* szValue2 = inet_ntoa("10.194.71.60");
// 最终szValue1和szValue2都会输出"10.194.71.60"

四、通用转换函数(新版,支持 IPv4/IPv6)
头文件:<arpa/inet.h>,同时支持 IPv4 和 IPv6:
image.png

函数名 功能 参数说明
inet_pton 字符串 IP → 网络字节序整数 - af:地址族(AF_INET=IPv4,AF_INET6=IPv6)
- src:字符串 IP
- dst:存储结果的内存
inet_ntop 网络字节序整数 → 字符串 IP - af:地址族
- src:整数形式的 IP
- dst:存储字符串的缓冲区
- cnt:缓冲区大小(可用宏INET_ADDRSTRLEN(IPv4,16 字节)/INET6_ADDRSTRLEN(IPv6,46 字节))

关键结论

2.创建socket

一、socket()的核心定位
在 UNIX/Linux 中,套接字(socket)是一种文件描述符,遵循 “一切皆文件” 的设计:可以像操作文件一样对 socket 执行读、写、控制、关闭等操作。
二、socket()函数的定义与头文件
运行

#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );
  1. domain:协议族(底层协议类型)
    作用:指定 socket 使用的协议族,决定了地址结构的类型。
    常见取值:
  1. type:服务类型(套接字类型)
    作用:指定 socket 的通信类型,决定了传输层协议的特性。
    核心取值:
  1. protocol:具体协议
    作用:在前两个参数确定的协议集合中,选择具体协议。

3.命名socket

一、bind()的核心作用

#include <sys/types.h>
#include <sys/socket.h>
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );
//例如:ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
  1. EACCES
    • 原因:绑定的地址是 “受保护地址”,普通用户无权限;
    • 典型场景:普通用户绑定0~1023的知名端口(如 80、443)。
  2. EADDRINUSE
    • 原因:绑定的地址正在被使用;
    • 典型场景:地址处于TIME_WAIT状态(服务器刚关闭,端口未释放)。

4.监听socket

一、listen()的核心作用
socketbind()命名后,需调用listen()将其转为被动监听状态,并创建监听队列来存放待处理的客户端连接 —— 只有调用listen()后,服务器才能通过accept()接收客户端连接。
二、listen()函数的定义与头文件

#include <sys/socket.h>
int listen( int sockfd, int backlog );
  1. 内核 2.2 之前backlog是 “半连接(SYN_RCVD)+ 完全连接(ESTABLISHED)” 的总数上限;
  2. 内核 2.2 之后backlog仅限制 “完全连接(ESTABLISHED)” 的数量;半连接的上限由内核参数/proc/sys/net/ipv4/tcp_max_syn_backlog控制。

5.接受连接

一、accept()的核心逻辑
accept()是从监听队列中取出一个已完成三次握手的连接,返回一个新的连接 socket(用于和客户端通信),同时通过addr参数获取客户端的远端地址。
image.png
二、核心特性

时间阶段 服务器行为(应用层) 客户端行为(应用层) 内核行为(三次握手核心)
阶段 1:服务器准备 调用socket()创建监听 socket → bind()绑定地址 → listen()进入监听状态(LISTEN) 服务器内核初始化 “半连接队列”“完全连接队列”,等待客户端连接请求
阶段 2:客户端发起连接 服务器处于sleep(20)(应用层暂停,内核仍在工作) 调用telnet(本质是connect() 1. 客户端内核发SYN 包给服务器 → 客户端进入SYN_SENT状态;

2. 服务器内核收到 SYN 包 → 发SYN+ACK 包给客户端 → 服务器该连接进入SYN_RCVD状态(加入半连接队列);

3. 客户端内核收到 SYN+ACK 包 → 发ACK 包给服务器 → 客户端进入ESTABLISHED状态;

✅ 三次握手完成!
阶段 3:连接入队 服务器仍在sleep(20) 客户端可能断网 / 退出(应用层操作) 服务器内核将这个 “完成三次握手的连接” 从半连接队列移到完全连接队列 → 连接状态变为ESTABLISHED
阶段 4:服务器取连接 服务器sleep结束 → 调用accept() 客户端已断连 / 退出(应用层)

细节:三次握手的触发点

  1. 三次握手和accept()的关系

6.发起连接

客户端通过connect()主动向服务器发起 TCP 连接请求,是客户端建立网络通信的关键调用(对应服务器的listen()/accept())。
image.png

sockfd在connect和accept区别

对比维度 connect中的sockfd(客户端) accept中的sockfd(服务器)
所属端 客户端的套接字 服务器的套接字
职责 代表 “客户端与服务器的这条连接”,用于和服务器通信 代表 “服务器与某一个客户端的这条连接”,用于和该客户端通信
来源 客户端调用socket()直接创建(无需bind/listen) 服务器调用accept()后新返回的套接字(不是监听 socket)
与监听 socket 的关系 无(客户端没有监听 socket) 由服务器的监听 socket(listen()后的 socket)触发生成
数量 客户端通常只有 1 个(对应一条连接) 服务器会有多个(每accept一个客户端连接,就生成一个新的sockfd)

7.关闭连接

一、close:通用的文件描述符关闭(适用于 socket,但有局限性)

#include <unistd.h>
int close(int fd);
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
维度 close shutdown
依赖引用计数 是(计数归 0 才关) 否(直接操作连接)
支持半关闭 否(只能同时关读写) 是(可单独关读 / 写)
适用场景 单进程、简单关闭 多进程、需要灵活控制读写的场景

四、注意客户端和服务器的区别
image.png
五、注意监听socket和连接socket区别
image.png
举个直观例子:

8.数据读写

8.1 TCP数据读写

一、TCP 数据读写:recv/send

  1. 通用接口兼容
    对 socket 的读写可以直接用read/write(因为 UNIX “一切皆文件”),但socket提供了更灵活的专用接口recv/send
  2. recv/send的定义
    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    • flags参数是核心:提供额外的读写控制(如非阻塞、带外数据等),默认填0时效果等同于read/write
  3. recv的返回值含义
    • 正数:实际读取的字节数(可能小于len,需循环读取);
    • 0:对方关闭连接;
    • -1:出错(需检查errno)。
      image.png

二、示例:带外数据的发送与接收

  1. 客户端代码(testoobsend)逻辑:
  1. 服务器代码(testoobrecv)逻辑:
  1. 实验结果与关键结论:
    服务器输出:
got 5 bytes of normal data '123ab'
got 1 bytes of oob data 'c'
got 3 bytes of normal data '123'

8.2 UDP数据读写

一、UDP 数据读写的专用系统调用
UDP 是无连接的协议,因此读写数据时需要明确指定通信对方的地址,对应的系统调用是:

#include <sys/socket.h>
// 读取UDP数据(同时获取发送端地址)
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, 
                 struct sockaddr* src_addr, socklen_t* addrlen);
// 发送UDP数据(指定接收端地址)
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, 
               const struct sockaddr* dest_addr, socklen_t addrlen);

二、核心参数与逻辑(适配 UDP 的无连接特性)

  1. recvfrom(读)
    • 因为 UDP 无连接,每次读数据都需要通过src_addr获取发送端的 socket 地址(IP + 端口);
    • addrlen需先初始化(传入src_addr的内存大小),内核会返回实际地址长度。
  2. sendto(写)
    • 因为 UDP 无连接,每次写数据都需要通过dest_addr指定接收端的 socket 地址(IP + 端口);
    • addrlendest_addr的地址长度
      三、与 TCP 读写接口的关联
  1. UDP 的无连接特性,决定了其读写必须通过recvfrom/sendto明确处理通信地址;
  2. recvfrom/sendto是通用接口,既支持 UDP(无连接),也支持 TCP(面向连接);
  3. 核心区别:UDP 用这两个接口处理 “动态地址”,TCP 用它们时可以忽略地址参数。