网络编程知识扫盲
Socket 网络编程全解
Socket(套接字)是操作系统提供的网络通信编程接口,它把复杂的 TCP/IP 协议族封装成一组函数调用,让应用层开发者无需关心底层协议细节,就能实现跨主机的进程通信。
Linux 下 socket 本质是一个特殊的文件描述符,我们可以像操作普通文件一样,通过 read/write 接口在两个 socket 之间收发数据。
常见套接字类型:
- 流式套接字
SOCK_STREAM:对应 TCP 协议,面向连接、可靠、有序、字节流传输 - 数据报套接字
SOCK_DGRAM:对应 UDP 协议,无连接、不可靠、报文边界明确 - 原始套接字
SOCK_RAW:跳过传输层,直接操作 IP 层报文,用于自定义协议开发
一、TCP Socket 编程(面向连接)
TCP 是面向连接的可靠传输协议,通信分为「服务端被动监听」和「客户端主动连接」两个角色,流程固定,是网络编程的核心。
1. 完整通信流程
服务端流程
- 创建套接字:调用
socket(),生成监听用的文件描述符 - 绑定地址:调用
bind(),给套接字绑定固定的 IP 地址 + 端口号 - 开启监听:调用
listen(),将套接字转为被动监听状态,设置连接等待队列长度 - 接受连接:调用
accept(),阻塞等待客户端连接;成功后返回全新的通信套接字,专门用于和该客户端收发数据 - 数据交互:通过
read()/write()或recv()/send()收发数据 - 关闭连接:调用
close(),关闭通信套接字和监听套接字
客户端流程
- 创建套接字:调用
socket() - 发起连接:调用
connect(),向服务端 IP + 端口发起 TCP 连接(对应三次握手) - 数据交互:连接建立后,通过
read()/write()收发数据 - 关闭连接:调用
close(),断开连接(对应四次挥手)
2. 核心 API 详解
(1)socket () - 创建套接字
c
运行
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址族,常用AF_INET(IPv4)、AF_INET6(IPv6)type:套接字类型,TCP 用SOCK_STREAM,UDP 用SOCK_DGRAMprotocol:协议,一般填 0,自动匹配对应类型的默认协议- 返回值:成功返回文件描述符(≥0),失败返回 -1
(2)bind () - 绑定 IP 与端口
c
运行
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket 返回的文件描述符addr:地址结构体指针,实际使用sockaddr_in(IPv4),调用时强转为sockaddr*addrlen:地址结构体的长度
IPv4 地址结构体:
c
运行
struct sockaddr_in {
sa_family_t sin_family; // 地址族,填 AF_INET
in_port_t sin_port; // 端口号,必须使用网络字节序
struct in_addr sin_addr; // IP 地址结构体
};
(3)listen () - 开启监听
c
运行
int listen(int sockfd, int backlog);
backlog:已完成三次握手的连接等待队列最大长度,Linux 默认上限通常为 128- 作用:将主动套接字转为被动监听套接字,让操作系统开始接收发往该端口的连接请求
(4)accept () - 接受客户端连接
c
运行
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 阻塞等待客户端连接,连接成功返回新的通信套接字
- 原
sockfd是监听套接字,继续负责接收新连接;后续和客户端通信只用返回的新 fd addr会被填充为客户端的 IP 和端口信息
(5)connect () - 客户端发起连接
c
运行
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 向指定的服务端地址发起 TCP 连接,对应三次握手过程
- 连接成功返回 0,失败返回 -1
(6)数据收发
c
运行
// 通用读写,和文件操作一致
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
// 网络专用,支持更多控制标志
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);
3. 关键概念:网络字节序
TCP/IP 协议规定所有网络数据必须使用大端字节序(高字节存低地址),而主流 PC 主机大多是小端字节序。因此端口号、IP 地址在填入 socket 结构体前,必须做字节序转换。
常用转换函数:
htons(port):主机字节序 → 网络字节序(short 类型,用于端口)ntohs(port):网络字节序 → 主机字节序inet_pton(AF_INET, "127.0.0.1", &addr):点分十进制 IP → 网络字节序整数inet_ntoa(addr):网络字节序整数 → 点分十进制字符串
4. 完整代码示例(回声服务器)
服务端 server.c
c
运行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main() {
// 1. 创建监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) { perror("socket"); exit(1); }
// 设置端口复用,避免重启时地址占用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 配置服务端地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有IP
// 3. 绑定地址
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind"); exit(1);
}
// 4. 开启监听
if (listen(listen_fd, 128) == -1) { perror("listen"); exit(1); }
printf("服务端已启动,监听端口%d...\n", PORT);
// 5. 接受客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) { perror("accept"); exit(1); }
printf("客户端连接成功,IP:%s\n", inet_ntoa(client_addr.sin_addr));
// 6. 循环收发数据(回声)
char buf[BUF_SIZE];
while (1) {
memset(buf, 0, BUF_SIZE);
int len = recv(conn_fd, buf, BUF_SIZE, 0);
if (len <= 0) { printf("客户端断开连接\n"); break; }
printf("收到:%s", buf);
send(conn_fd, buf, len, 0); // 原封不动发回
}
// 7. 关闭套接字
close(conn_fd);
close(listen_fd);
return 0;
}
客户端 client.c
c
运行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main() {
// 1. 创建套接字
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) { perror("socket"); exit(1); }
// 2. 配置服务端地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 3. 连接服务端
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect"); exit(1);
}
printf("连接服务端成功\n");
// 4. 循环发送数据
char buf[BUF_SIZE];
while (1) {
fgets(buf, BUF_SIZE, stdin);
send(sock_fd, buf, strlen(buf), 0);
memset(buf, 0, BUF_SIZE);
recv(sock_fd, buf, BUF_SIZE, 0);
printf("服务端回声:%s", buf);
}
close(sock_fd);
return 0;
}
编译运行:
bash
gcc server.c -o server && gcc client.c -o client
./server # 先启动服务端
./client # 再启动客户端
二、UDP Socket 编程(无连接)
UDP 是无连接协议,不需要建立连接,直接通过 IP + 端口发送报文,没有可靠保证,但延迟低、速度快,适合直播、实时游戏等场景。
1. 整体流程
- 服务端:
socket()→bind()→ 循环recvfrom()/sendto()收发 - 客户端:
socket()→ 直接sendto()/recvfrom()收发(不需要 connect、listen、accept)
2. 核心收发函数
c
运行
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
src_addr:调用后会填充发送方的地址信息dest_addr:每次发送都需要指定接收方的完整地址
三、进阶核心知识点
1. 阻塞 vs 非阻塞
- 阻塞模式(默认):
accept、recv等函数没有数据时会一直挂起等待,线程卡住 - 非阻塞模式:调用后立即返回,没数据时返回 -1 并设置
errno为EAGAIN,需要轮询检查 - 设置方式:通过
fcntl()函数设置O_NONBLOCK标志
2. IO 多路复用(高性能网络核心)
当需要同时处理上千个连接时,多线程模式开销过大,此时使用 IO 多路复用:用一个线程监控大量文件描述符,哪个有事件就处理哪个。
select:跨平台通用,连接数上限 1024,性能随连接数线性下降poll:Linux 下无连接数上限,但仍需遍历所有描述符检测epoll:Linux 专属,事件驱动,性能极高,是 Nginx、Redis 等高性能服务的核心技术
3. 常见坑点与解决方案
-
端口占用 / TIME_WAIT:服务端重启时提示地址已被占用,是因为 TCP 四次挥手后主动关闭方会进入 TIME_WAIT 状态,端口保留 2MSL 时间。 解决:设置端口复用选项
c
运行
int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); -
TCP 粘包问题:TCP 是字节流协议,没有报文边界,多次发送的数据可能被合并接收,也可能被拆分。 解决:固定包长、包尾加特殊分隔符、包头 + 包体格式(先发包长度,再发包体)
4. 协议流程与 socket 调用对应
- 客户端调用
connect()→ 发起 SYN,完成三次握手后 connect 返回 - 服务端
listen()后,三次握手完成的连接进入 backlog 等待队列 - 服务端调用
accept()→ 从队列中取出一个已完成连接 - 任意一端调用
close()→ 发起 FIN,进入四次挥手流程
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)