目录
- [Socket 是什么?它是如何诞生的?](#Socket 是什么?它是如何诞生的?)
- [Socket API 详解](#Socket API 详解)
- [socket() --- 创建套接字](#socket() — 创建套接字)
- [domain(协议族 / 地址族)](#domain(协议族 / 地址族))
- type(套接字类型)
- protocol(协议类型)
- 返回值
- [bind() --- 绑定地址与端口](#bind() — 绑定地址与端口)
- [listen() --- 开始监听](#listen() — 开始监听)
- [accept() --- 接受连接](#accept() — 接受连接)
- [connect() --- 发起连接](#connect() — 发起连接)
- [send() --- 发送数据](#send() — 发送数据)
- [recv() --- 接收数据](#recv() — 接收数据)
- [close() --- 关闭连接](#close() — 关闭连接)
- [socket() --- 创建套接字](#socket() — 创建套接字)
Socket 是什么?它是如何诞生的?
在现代网络编程中,Socket(套接字) 是一个核心概念。要理解 Socket,我们需要从操作系统内核说起。
操作系统的内核协议栈(如 Linux 内核中的 TCP/IP 模块) 是 UDP/TCP 规则的真正落地执行者 ------ 它通过成千上万行 C 语言代码将滑动窗口、拥塞控制等算法实体化。
为了方便用户态程序安全地触发这些内核功能,操作系统封装出了 Socket(套接字)编程接口(API)。简单来说,Socket 就是应用程序与网络协议栈之间的「桥梁」,让程序员能够以文件描述符的形式操作网络连接。
Socket API 详解
在 Linux 系统中,使用 Socket 编程需要包含以下头文件:
c
#include <sys/socket.h>
下面将逐一介绍 Socket API 中最核心的函数。
socket() --- 创建套接字
socket() 函数用于创建一个新的套接字,无论是服务端还是客户端都需要调用此函数。
c
int socket(int domain, int type, int protocol);
domain(协议族 / 地址族)
| 常量 | 含义 |
|---|---|
AF_INET |
IPv4 互联网协议族 (最常用),地址为 32 位,如 192.168.1.1。 |
AF_INET6 |
IPv6 互联网协议族,地址为 128 位。 |
这两个是实际开发中最常用的选项,记住即可。
type(套接字类型)
| 常量 | 含义 | 典型协议 |
|---|---|---|
SOCK_STREAM |
流式套接字,面向连接、可靠、有序的字节流传输。 | TCP |
SOCK_DGRAM |
数据报套接字,无连接、不可靠、固定最大长度的报文传输。 | UDP |
这两个是实际开发中最常用的选项,记住即可。
💡 Linux 特有标志(可选):
从 Linux 2.6.27 开始,type 参数可以与以下标志按位或(|)组合使用:
SOCK_NONBLOCK:创建非阻塞套接字,省去后续fcntl()调用。SOCK_CLOEXEC:设置FD_CLOEXEC标志,在执行exec()时自动关闭该套接字。
protocol(协议类型)
通常情况下,给定 domain 和 type 后只有一种协议支持,此时填 0 即可,系统会自动选择默认协议。
返回值
| 结果 | 返回值 | 说明 |
|---|---|---|
| 成功 | 非负整数 | 套接字文件描述符(sockfd)。 |
| 失败 | -1 |
设置全局变量 errno 以指示具体错误原因。 |
bind() --- 绑定地址与端口
bind() 函数将套接字文件描述符绑定到指定的 IP 地址和端口上,为服务端的后续操作做准备。
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解
sockfd:socket()函数返回的文件描述符。addr:指向协议地址结构的指针。这是一个通用指针类型struct sockaddr *,实际上需要传入具体协议族对应的地址结构体,并在使用时强制转换为(struct sockaddr *)。
| 协议族 | 实际使用的结构体 | 关键成员赋值 |
|---|---|---|
| IPv4 | struct sockaddr_in |
sin_family = AF_INET; sin_port = htons(端口); sin_addr.s_addr = INADDR_ANY; 或 inet_addr("IP") |
| IPv6 | struct sockaddr_in6 |
sin6_family = AF_INET6; sin6_port = htons(端口); sin6_addr = in6addr_any; |
INADDR_ANY表示监听所有网络接口,无论客户端连接哪个 IP(内网或公网),服务器都能接收到,一般推荐使用。
addrlen:地址结构体的大小,通常使用sizeof获取即可。
返回值
| 结果 | 返回值 | 说明 |
|---|---|---|
| 成功 | 0 |
绑定成功。 |
| 失败 | -1 |
设置全局变量 errno 以指示错误原因。 |
listen() --- 开始监听
listen() 函数将套接字设置为被动监听状态,准备接受客户端的连接请求。
c
int listen(int sockfd, int backlog);
参数详解
sockfd:socket()函数返回并已bind()的文件描述符。backlog(等待队列的最大长度):当服务器忙于处理现有连接时,新客户端发起的connect()请求不会被丢弃,而是会在这个队列里排队等待accept()处理。
不要设得太大(浪费内存),也不要太小(高并发下客户端会连接失败)。一般情况下设置为 1024 即可。
返回值
| 结果 | 返回值 | 说明 |
|---|---|---|
| 成功 | 0 |
进入监听状态。 |
| 失败 | -1 |
设置全局变量 errno。 |
accept() --- 接受连接
accept() 函数从已完成连接的队列中取出一个客户端连接,返回一个专门用于与该客户端通信的新套接字。
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数详解
sockfd:已调用listen()的监听套接字。addr(输出参数 ):传入一个struct sockaddr_in(IPv4)或struct sockaddr_in6(IPv6)结构体的指针,用于存放发起连接请求的客户端 IP 地址和端口号。如果不关心客户端地址信息,可以直接填NULL。addrlen:这是一个值-结果 参数,⚠️ 极其重要,容易出错 !- 传入时 :必须初始化为
addr结构体的大小(如sizeof(struct sockaddr_in))。 - 返回时:系统会修改此值为客户端地址的实际大小。
- 传入时 :必须初始化为
返回值
| 结果 | 返回值 | 说明 |
|---|---|---|
| 成功 | 新的非负整数文件描述符 | 连接套接字,专门用于和该客户端通信。 |
| 失败 | -1 |
设置全局变量 errno。 |
connect() --- 发起连接
connect() 函数用于客户端向服务器发起连接请求(完成 TCP 三次握手)。
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数详解
sockfd:socket()函数返回的客户端套接字描述符(主动套接字)。addr:指向目标服务器的协议地址结构体(如struct sockaddr_in),包含服务器的 IP 地址 和端口号 。使用时需要强制转换为(struct sockaddr *)。addrlen:地址结构体的大小,即sizeof(结构体)。
返回值
| 结果 | 返回值 | 说明 |
|---|---|---|
| 成功 | 0 |
三次握手已成功完成。 |
| 失败 | -1 |
设置全局变量 errno。 |
send() --- 发送数据
send() 函数用于通过已连接的套接字发送数据。
c
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数详解
sockfd:通常指connect()(客户端)或accept()(服务端)返回的已连接套接字。buf:指向存放待发送数据的内存区域指针(通常是字符数组)。len:要发送的数据长度(字节数)。flags:控制发送行为,大多数情况下填0即可(默认阻塞模式)。在高并发场景下可填MSG_DONTWAIT实现非阻塞发送,也可以使用fcntl()修改套接字为非阻塞模式。
返回值(必须严格判断)
| 返回值 | 含义 | 后续操作 |
|---|---|---|
> 0 |
成功发送了 n 个字节。注意:n 可能小于 len(部分发送)。 |
需要循环调用,将剩余数据继续发送。 |
== 0 |
(极少见)没有发送任何数据,通常表示连接已关闭。 | 关闭套接字。 |
-1 |
发生错误。查看 errno。 |
根据错误码重试或报错。 |
recv() --- 接收数据
recv() 函数用于从已连接的套接字接收数据。
c
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数详解
sockfd:通常指accept()返回的连接套接字 (conn_fd),用于与特定客户端通信。buf:指向存放接收数据的内存区域指针(通常是字符数组)。len:缓冲区最多能容纳的字节数。flags:控制接收行为,大多数情况下填0即可(默认阻塞模式)。在高并发场景下可填MSG_DONTWAIT实现非阻塞接收,也可以使用fcntl()修改套接字为非阻塞模式。
返回值(极其重要,必须判断)
返回值类型是 ssize_t(有符号整型),三种情况:
| 返回值 | 含义 | 后续操作 |
|---|---|---|
> 0 |
成功读取到 n 个字节。注意:n 可能小于 len(剩余数据还在路上)。 |
处理这 n 个字节,继续循环读取直到收完整条消息。 |
== 0 |
连接已关闭(对端调用了 close()),这是「优雅关闭」信号。 |
立即关闭本端 conn_fd,不要再读。 |
-1 |
发生错误。查看 errno 获取原因。 |
根据错误码处理(如重试或退出)。 |
close() --- 关闭连接
通信完成后要关闭套接字,避免资源泄漏。
c
#include <unistd.h>
int close(int fd);
fd:要关闭的套接字文件描述符。- 返回值 :成功返回
0,失败返回-1并设置errno。
💡 提示:本文介绍了 Socket 编程中最核心的九个函数。掌握它们的用法后,你就可以开始编写自己的网络程序了。建议在实际编码中多练习,加深对这些 API 的理解。