Socket(套接字)是操作系统提供的标准网络编程 API,封装 TCP/IP 协议栈,实现跨主机 / 跨进程通信,遵循 “一切皆文件” 思想,以文件描述符(sockfd)操作。下面从核心概念、完整 API、TCP/UDP 流程、错误处理、示例全维度详解。
一、Socket 核心基础
1.1 核心三要素(唯一标识通信端点)
- 地址族(domain/AF_xxx):指定网络类型
AF_INET:IPv4(最常用)AF_INET6:IPv6AF_UNIX/AF_LOCAL:本地进程通信(Unix 域)
- 套接字类型(type/SOCK_xxx):指定传输语义
SOCK_STREAM:TCP,面向连接、可靠、字节流、有序无重复SOCK_DGRAM:UDP,无连接、不可靠、数据报、无序SOCK_RAW:原始套接字,直接操作 IP/ICMP 等网络层
- 协议(protocol):通常填
0(自动匹配默认:TCP 对应 IPPROTO_TCP,UDP 对应 IPPROTO_UDP)
1.2 地址结构体(通用 / IPv4)
// 通用地址(所有socket函数参数要求) struct sockaddr { sa_family_t sa_family; // 地址族 AF_xxx char sa_data[14]; // 地址数据(IP+端口) }; // IPv4专用(实际使用,强制转换为struct sockaddr*) struct sockaddr_in { sa_family_t sin_family; // AF_INET in_port_t sin_port; // 端口号(必须网络字节序,htons()) struct in_addr sin_addr;// IP地址(htonl()) char sin_zero[8]; // 填充,全0 }; // 常用宏:INADDR_ANY(绑定所有网卡IP,0.0.0.0)1.3 字节序转换(必须)
主机字节序(小端)↔网络字节序(大端):
htons():主机短整型→网络短整型(端口)htonl():主机长整型→网络长整型(IP)ntohs()、ntohl():反向转换
二、核心 Socket API(函数原型 + 参数 + 返回值 + 用法)
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
#include <sys/types.h> #include <sys/socket.h>2.1 socket () —— 创建套接字(入口)
int socket(int domain, int type, int protocol);
- 参数:对于IPv4, family参数指定为AF_INET;
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议;对于UDP协议,type参数指定为SOCK_DGRAM,表示面向数据报的传输协议; protocol参数的介绍从略,指定为0即可。- 返回:成功→非负整数
sockfd(文件描述符);失败→-1,设置errno- 作用:socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据;
2.2 bind () —— 绑定 IP + 端口(服务端必须)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端
口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- 参数:sockfd(socket 返回值)、addr(sockaddr_in,填 IP + 端口)、addrlen(sizeof (struct sockaddr_in))
- 返回:成功→0;失败→-1(常见错误:端口被占用 EADDRINUSE、权限不足 EACCES)
- 作用:bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们的程序中对myaddr参数是这样初始化的:
1. 将整个结构体清零;
2. 设置地址类型为AF_INET;
3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
4. 端口号为SERV_PORT, 我们定义为9999;
2.3 listen () —— TCP 服务端监听连接(仅 TCP)
int listen(int sockfd, int backlog);
- 参数:sockfd(已 bind 的监听 fd)、backlog(半连接 + 全连接队列总长度,受内核
net.core.somaxconn限制,默认 128)- 返回:成功→0;失败→-1
- 作用:将 socket 设为被动监听模式,等待客户端连接,内核维护连接队列
2.4 accept () —— TCP 服务端接受连接(仅 TCP)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:sockfd(监听 fd);addr是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给addr 传 NULL,表示不关心客户端的地址; addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
- 返回:成功→新的客户端 fd(专用于和该客户端通信);失败→-1(阻塞时无连接挂起,非阻塞返回 EAGAIN)
- 作用:从全连接队列取一个连接,创建新通信套接字,三次握手完成后, 服务器调用accept()接受连接;如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
2.5 connect () —— TCP 客户端发起连接(仅 TCP)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:sockfd(客户端 socket)、addr(服务端 IP + 端口)、addrlen(地址长度)
- 返回:成功→0;失败→-1(ECONNREFUSED:端口无监听、ETIMEDOUT:超时)
- 作用:触发 TCP三次握手,建立可靠连接,客户端需要调用connect()连接服务器;
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的
地址;
2.6 send () /write () —— TCP 发送数据(已连接)
ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t write(int sockfd, const void *buf, size_t len); // 等价send(flags=0)
- 参数:sockfd(已连接 fd)、buf(数据缓冲区)、len(发送长度)、flags(0 默认;MSG_OOB 带外数据;MSG_DONTWAIT 非阻塞)
- 返回:成功→实际发送字节数(可能 < len,需循环发送);失败→-1
- 注意:TCP 是流,无边界,需自定义分包(先发长度再发数据)
2.7 recv () /read () —— TCP 接收数据(已连接)
ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t read(int sockfd, void *buf, size_t len); // 等价recv(flags=0)
- 参数:sockfd、buf(接收缓冲区)、len(最大接收长度)、flags(0 默认;MSG_PEEK 偷看不清除;MSG_DONTWAIT 非阻塞)
- 返回:成功→接收字节数;0→对端关闭连接;失败→-1
2.8 sendto () —— UDP 发送(无连接,指定目标)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 参数:比 send 多目标地址 dest_addr(服务端 IP + 端口)
- 返回:成功→发送字节数;失败→-1
- 特点:无需 connect,每次指定目标,数据报独立
2.9 recvfrom () —— UDP 接收(获取源地址)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 参数:比 recv 多 src_addr(输出发送方 IP + 端口)、addrlen(长度)
- 返回:成功→接收字节数;失败→-1
- 特点:UDP 无连接,必须用此函数获取发送方地址
2.10 close () /closesocket () —— 关闭套接字
int close(int sockfd); // Linux/Unix int closesocket(SOCKET sockfd); // Windows(Winsock)
- 作用:释放文件描述符,关闭连接;TCP 会触发四次挥手;UDP 直接释放资源
- 注意:必须关闭,避免资源泄漏
2.11 setsockopt () /getsockopt () —— 设置 / 获取套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);- 常用选项:
SO_REUSEADDR:允许端口重用(解决 TIME_WAIT 问题)SO_RCVBUF/SO_SNDBUF:设置接收 / 发送缓冲区大小SO_KEEPALIVE:开启 TCP 保活TCP_NODELAY:禁用 Nagle 算法(降低延迟)
三、TCP vs UDP 完整调用流程
3.1 TCP 服务端流程(面向连接,可靠)
socket(AF_INET, SOCK_STREAM, 0)→ 创建监听 fdbind()→ 绑定 IP: 端口listen(backlog)→ 开启监听accept()→ 阻塞等待,获取客户端 fdsend()/recv()→ 与客户端收发数据close(客户端fd)→ 关闭单个连接close(监听fd)→ 停止服务
3.2 TCP 客户端流程
socket(AF_INET, SOCK_STREAM, 0)→ 创建客户端 fdconnect(服务端IP:端口)→ 发起三次握手send()/recv()→ 收发数据close()→ 关闭连接
3.3 UDP 流程(无连接,不可靠,简化)
- 服务端:socket→bind→recvfrom(循环接收,获取源地址)→sendto(回复)→close
- 客户端:socket→sendto(指定服务端地址)→recvfrom(接收)→close
- 特点:无 listen/accept/connect,直接收发数据报
四、错误处理与常见 errno
所有 Socket 函数失败均返回 - 1,需用perror()或strerror(errno)打印错误:
EADDRINUSE:端口已被占用ECONNREFUSED:连接被拒绝(服务端未监听)ETIMEDOUT:连接 / 接收超时EAGAIN/EWOULDBLOCK:非阻塞模式下无数据 / 无连接EPIPE:向已关闭的连接发送(需忽略 SIGPIPE 信号)
五、补充参考内容
地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP
地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是
void *addrptr。