Linux应用开发-17-套接字
IPC (进程间通信)有System V以及POSIX IPC两大体系,所有进程都运行在同一台 Linux 机器上。它们通过共享这台机器的内核资源(如内存、文件系统)来进行通信。
-共有四大类:-管道 (Pipes):类似水管”的数据流。-匿名管道 (Anonymous Pipes):pipe() 函数。只能用于有“血缘关系”的进程(即 fork() 之后的父子进程)。随进程结束而消失。-命名管道 (FIFO):mkfifo() 函数。允许任何知道该管道文件路径的、互不相关的进程间通信。永久存在于文件系统,直到被 rm 或 unlink() 删除。-System V IPC:由内核维护的、持久性的 IPC 对象,使用全局唯一的key_t 键值(通常由 ftok() 生成)。-消息队列 (msgget, msgsnd, msgrcv, msgctl):传输一个个带“类型”的数据块。允许接收方按消息类型(msgtyp)选择性地接收,而不是严格的先进先出。-共享内存 (shmget, shmat, shmdt, shmctl):将同一块物理内存,映射到多个进程各自的虚拟地址空间中。数据无需拷贝,A 进程写入,B 进程立即可见。不提供任何同步,必须配合“信号量”使用。-信号量 (semget, semop, semctl):同步与互斥(“P/V 操作”)。-POSIX IPC:更现代、更简洁、API 更友好的 IPC 接口。-POSIX 信号量 (sem_t)-命名信号量 (sem_open):一个文件系统路径,用于不相关的进程间同步。-无名信号量 (sem_init):内存中的一个 sem_t 变量。(pshared=0): 用于同一进程的线程间同步(轻量、高效)。(pshared=1): 用于相关的进程间同步(必须放置在共享内存中)。-POSIX 互斥锁 (pthread_mutex_t):专用于线程间的“互斥”。提供了“归属权”(谁上锁,谁解锁)和“递归锁”等更安全的特性。-信号 (Signals):signal(), sigaction(), kill(), raise(), alarm(),不用于传输数据,只用于发送一个“事件发生了”的通知。
套接字(Socket)目的是进行“跨机器通信”。
所需头文件:
#include <sys/socket.h> // 包含了 socket, bind, connect, listen, accept, send, recv, sendto, getsockopt, setsockopt 等
#include <netinet/in.h> // 包含了 struct sockaddr_in, IPPROTO_TCP, IPPROTO_UDP
#include <unistd.h> // 包含了 read, write, close
#include <sys/ioctl.h> // 包含了 ioctl
#include <arpa/inet.h> // 包含了 inet_addr, inet_ntoa (用于IP地址转换)
核心地址结构体:struct sockaddr_in
#include <netinet/in.h>
struct sockaddr_in {sa_family_t sin_family; // (1) 地址族 (必须是 AF_INET)in_port_t sin_port; // (2) 端口号 (必须用 htons())struct in_addr sin_addr; // (3) IP 地址
};struct in_addr {uint32_t s_addr; // (4) IP 地址 (必须用 inet_addr() 或 htonl())
};
使用示例:
struct sockaddr_in server_addr;// (1) AF_INET ipv4
server_addr.sin_family = AF_INET;// (2) 端口号
// 必须用 htons() (Host to Network Short) 来转换“字节序”
server_addr.sin_port = htons(8080);// (3) & (4) IP 地址
// 必须用 inet_addr() 来转换, 它也会处理字节序
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//(老式函数)
inet_aton("127.0.0.1", &serv_addr.sin_addr);//(新式函数)
//注意:inet_aton() 拿到这个地址后,会把转换好的二进制 IP 直接写入到 serv_addr.sin_addr 结构体中(它会自动处理内部的 .s_addr)。
// (如果是服务器 bind(),IP 通常设为“任意 IP”)
// server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
核心生命周期函数 (TCP 流程)
socket()
//通信的第一步,创建套接字
int socket(int domain, int type, int protocol);
/*
domain: 协议族。最常用的是 AF_INET (用于 IPv4) 或 AF_INET6 (用于 IPv6)。AF_UNIX用于本机内部进程通信(使用文件路径)。
type: 通信类型。SOCK_STREAM:流式套接字,用于 TCP (可靠、面向连接)。SOCK_DGRAM:数据报套接字,用于 UDP (不可靠、无连接)。
protocol: 协议。通常设为 0,让内核根据 type 自动选择。
*/
bind()
//将一个地址(IP 地址 + 端口号)绑定到 socket() 创建的文件描述符上。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
sockfd: socket() 返回的文件描述符。
addr: struct sockaddr_in 结构体指针,必须提前填好您要绑定的IP地址(sin_addr)和端口(sin_port)。创建一个 struct sockaddr_in server_addr变量,(const struct sockaddr *)&server_addr,地址强制类型转换后传入
addrlen: addr 结构体的大小。sizeof(struct sockaddr_in)。
*/
listen()
//使一个已 bind() 的套接字进入“被动监听”状态,准备接收客户端连接,服务器(Server)专用。
int listen(int sockfd, int backlog);
/*
sockfd: 已 bind() 的文件描述符。
backlog: “全连接队列”的最大长度。这是指内核已经完成了三次握手、但您的 accept() 还未来得及处理的连接的最大数量。
*/
connect()
//(客户端) 向服务器发起连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
sockfd: 客户端自己 socket() 创建的文件描述符。
addr: 服务器的地址(struct sockaddr_in),包含服务器的 IP 和 端口。
客户端服务器端三次握手四次挥手,此函数会阻塞(暂停),直到TCP三次握手完成(连接成功)或失败(超时、服务器拒绝)。
*/
accept()
//(服务器端) 从 listen() 的队列中接受一个已完成的客户端连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
sockfd: 那个正在 listen() 的监听套接字。
addr / addrlen: (可选)输出参数,用于获取连接进来的那个客户端的 IP 和 端口信息。
返回值 (最关键):它会返回一个全新的文件描述符(new_fd)。new_fd:用于与这个特定的客户端进行 read/write 通信。sockfd:原始的监听套接字保持不变,继续在 listen() 状态,等待下一个客户端连接。
如果队列为空,accept() 会阻塞(暂停),直到有新客户端 connect() 进来。
*/
close()
//关闭一个套接字文件描述符。
int close(int fd);//<unistd.h>
数据传输函数 (TCP 和 UDP)
read() / write() (通用 I/O)
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
/*
fd: 套接字文件描述符。
buf: 指向“数据缓冲区”(您用来存放/发送数据的数组)的指针。
count: 您希望读取/写入的最大字节数。
在 TCP (或 AF_UNIX ) 中,一旦连接建立(connect() 和 accept() 之后)
套接字 fd 就等同于一个文件描述符。您可以使用最通用的 read 和 write 来收发数据。
用于TCP 客户端和服务器。
*/
recv() / send() (Socket 专用)
//与 read/write 功能基本相同,但专为套接字设计。
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 完全一样。MSG_PEEK:(用于 recv) 查看数据,但不将其从内核缓冲区中移除。MSG_OOB:发送或接收“带外数据”(Out-of-Band)。
*/
sendto()/recvfrom() (UDP 专用)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
//因为 UDP 是无连接的,在每一次 sendto() 时,都明确指定目标地址。
//dest_addr指向您填充好的目标(接收方)地址结构体。(const struct sockaddr *)&receiver_addrssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
//它不仅接收数据(buf),还会把发送方的地址信息填入 src_addr,以便您知道该回复给谁。
配置与控制函数 (高级)
getsockopt() / setsockopt()
//获取或设置套接字的底层选项,用于高级调优。
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
/*
level: 选项所在的协议层。最常用的是 SOL_SOCKET (套接字层)。
optname: 选项名称。SO_REUSEADDR: (常用) 允许服务器立即重启,并重新绑定到刚释放的端口上,无需等待 TIME_WAIT 状态结束。SO_KEEPALIVE: (TCP) 启用 TCP 心跳保活机制。SO_RCVTIMEO / SO_SNDTIMEO: 设置接收/发送超时。
void *optval (选项值):setsockopt: (输入) 指向您准备好的新值的指针。getsockopt: (输出) 指向一块“空”内存,用于接收内核返回的当前值。
*/
//常用端口复用
int optval = 1; // 1 表示“开启”
// 允许服务器立即重启并绑定同一个端口
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
ioctl()/ioctlsocket()
//ioctlsocket 是 Windows (Winsock) 中的函数名。在 Linux 上,标准函数是 ioctl
int ioctl(int fd, unsigned long request, ...);
/*
unsigned long request (请求命令):想执行的具体操作,FIONBIO (设置非阻塞模式)。
大多功能已被 getsockopt/setsockopt 取代。但仍有一些特定用途:
FIONBIO: (常用) 将套接字设置为非阻塞 (Non-Blocking) 模式。(但更推荐用 fcntl(fd, F_SETFL, O_NONBLOCK) 来实现)。
SIOCGIFCONF: 获取所有网络接口的配置信息。
*/
TCP 客户端
- 调用 socket() 函数创建一个套接字描述符。
- 调用connect() 函数连接到指定服务器中,端口号为服务器监听的端口号。
- 调用 write() 函数发送数据。
- 调用 close() 函数终止连接。
/* * tcp_client.c* 1. 启动并连接到 "127.0.0.1:8080"* 2. 发送一条消息* 3. 接收服务器的“回声”* 4. 打印回声并退出*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024int main() {int sock_fd; // 客户端的套接字struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE];const char *message = "Hello, Server! (来自客户端)";// --- 1. socket() ---// 创建客户端套接字sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd < 0) {perror("socket 创建失败");exit(1);}printf("客户端:[1] Socket 创建成功。\n");// --- 2. 准备服务器地址结构体 ---memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT); // 必须与服务器的端口一致// 将 "127.0.0.1" (本地回环地址) 转换为网络格式// 连接在本机上运行的服务器if (inet_aton("127.0.0.1", &serv_addr.sin_addr) == 0) {fprintf(stderr, "无效的服务器 IP 地址\n");exit(1);}// --- 3. connect() ---// 主动向服务器发起连接if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("connect 连接失败");exit(1);}printf("客户端:[2] 已成功连接到服务器 (127.0.0.1:%d)。\n", PORT);// --- 4. write() ---// 发送消息给服务器if (write(sock_fd, message, strlen(message)) < 0) {perror("write 失败");exit(1);}printf("客户端:[3] 消息已发送: '%s'\n", message);// --- 5. read() ---// 等待并接收服务器的“回声”memset(buffer, 0, BUFFER_SIZE);ssize_t n = read(sock_fd, buffer, BUFFER_SIZE - 1);if (n < 0) {perror("read 失败");} else if (n == 0) {printf("客户端:[4] 服务器关闭了连接。\n");} else {// (注意:read 不会自动添加 \0,但我们前面 memset 过了)printf("客户端:[4] 收到服务器回声: '%s'\n", buffer);}// --- 6. close() ---// 关闭连接close(sock_fd);printf("客户端:[5] 连接已关闭。\n");return 0;
}
TCP 服务器
- 调用 socket() 函数创建一个套接字描述符。
- 调用 bind() 函数绑定监听的端口号。
- 调用 listen() 函数让服务器进入监听状态。
- 调用 accept() 函数处理来自客户端的连接请求。
- 调用 read() 函数接收客户端发送的数据。
- 调用 close() 函数终止连接。
/* * tcp_server.c* 1. 启动并监听 8080 端口* 2. 接受一个客户端连接* 3. 读取客户端发来的消息* 4. 将收到的消息原样发回 (回声)* 5. 断开连接,并等待下一个客户端*/
#include <stdio.h> // 用于 printf, perror
#include <stdlib.h> // 用于 exit
#include <string.h> // 用于 memset, strlen
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 核心 socket 函数
#include <sys/types.h> // 兼容性所需
#include <netinet/in.h> // 包含 struct sockaddr_in, INADDR_ANY, htons
#include <arpa/inet.h> // 包含 inet_ntoa (IP地址转换)// 约定一个端口号
#define PORT 8080
// 缓冲区大小
#define BUFFER_SIZE 1024 int main() {int listen_fd, conn_fd; // listen_fd: 监听套接字; conn_fd: 连接套接字struct sockaddr_in serv_addr, client_addr;socklen_t client_len;char buffer[BUFFER_SIZE];ssize_t n; // 用于 read/write 的返回值// --- 1. socket() ---// 创建一个 "监听套接字" (listen_fd)// AF_INET = IPv4; SOCK_STREAM = TCPlisten_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd < 0) {perror("socket 创建失败");exit(1);}printf("服务器:[1] Socket 创建成功。\n");// --- (可选) setsockopt() ---// 为了解决 "Address already in use" (地址已占用) 错误// 允许服务器在重启后立即重新绑定到同一个端口int opt = 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {perror("setsockopt 失败");}// --- 2. 准备地址结构体 ---memset(&serv_addr, 0, sizeof(serv_addr)); // 清空结构体serv_addr.sin_family = AF_INET;// INADDR_ANY 表示绑定到本机的所有可用 IP 地址 (包括 127.0.0.1)serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(PORT); // 转换端口号为“网络字节序”// --- 3. bind() ---// 将套接字与地址和端口绑定if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("bind 绑定失败");exit(1);}printf("服务器:[2] Bind 成功。\n");// --- 4. listen() ---// 开始监听,backlog 队列大小设为 5if (listen(listen_fd, 5) < 0) {perror("listen 失败");exit(1);}printf("服务器:[3] 正在监听 %d 端口...\n\n", PORT);// --- 5. accept() (无限循环) ---// 这是一个典型的服务器循环while (1) {client_len = sizeof(client_addr);// accept() 会阻塞(暂停),直到一个客户端连接进来// 它会返回一个新的 "连接套接字" (conn_fd)conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);if (conn_fd < 0) {perror("accept 失败");continue; // 继续等待下一个连接}// 成功连接,打印客户端的 IP (inet_ntoa 用于转换)printf("服务器:[4] 客户端 %s 已连接!\n", inet_ntoa(client_addr.sin_addr));// --- 6. read() ---// 从 conn_fd (不是 listen_fd) 读取数据memset(buffer, 0, BUFFER_SIZE);n = read(conn_fd, buffer, BUFFER_SIZE - 1);if (n < 0) {perror("read 失败");} else if (n == 0) {printf("服务器:[5] 客户端关闭了连接。\n");} else {printf("服务器:[5] 收到消息: '%s'\n", buffer);// --- 7. write() (回声) ---// 将收到的数据原样写回给客户端if (write(conn_fd, buffer, n) < 0) {perror("write 失败");} else {printf("服务器:[6] 已发送回声。\n");}}// --- 8. close() ---// 关闭这个“连接套接字”,准备 accept 下一个close(conn_fd);printf("服务器:[7] 已断开与客户端的连接。等待下一个连接...\n\n");}// (这段代码实际上不会执行到,因为上面是无限循环)close(listen_fd);return 0;
}
