目录
[1.1 Socket的本质](#1.1 Socket的本质)
[.2 地址族与协议族](#.2 地址族与协议族)
[1.3 套接字类型](#1.3 套接字类型)
[2.1 服务器端流程](#2.1 服务器端流程)
[2.2 客户端流程](#2.2 客户端流程)
[2.3 一个完整的TCP回显服务器](#2.3 一个完整的TCP回显服务器)
[3.1 服务器端](#3.1 服务器端)
[3.2 客户端](#3.2 客户端)
[5.1 select](#5.1 select)
[5.2 poll](#5.2 poll)
[5.3 epoll(Linux特有)](#5.3 epoll(Linux特有))
引言
Socket(套接字)是Linux网络编程的基石,它提供了一种统一的接口,让应用程序能够通过网络与远端进程通信,也能在同一台主机内进行进程间通信。正如Linux的哲学"一切皆文件",Socket 本质上也是一种特殊的文件描述符。通过它,进程间可以像读写普通文件一样进行数据收发,操作系统则负责将数据封装为TCP段或UDP数据报,透明地穿越协议栈送达对端。
一、Socket基础:地址族与类型
1.1 Socket的本质
在Linux中,socket() 系统调用返回一个文件描述符,这个描述符关联着内核中的一套数据结构,包括发送缓冲区、接收缓冲区以及协议状态信息。应用层通过这个文件描述符进行操作,而内核负责处理传输层以下的复杂逻辑。
.2 地址族与协议族
创建Socket时需要指定地址族(Address Family),常见的包括:
AF_INET:IPv4网络协议,用于跨主机通信;
AF_INET6:IPv6网络协议,解决IPv4地址枯竭问题;
AF_UNIX:Unix域套接字,用于本机进程间通信,效率高于网络套接字。
1.3 套接字类型
Socket的类型决定了通信的语义:
SOCK_STREAM:流式套接字,基于TCP协议,提供可靠、有序、面向连接的双向字节流。适用于需要数据完整性的场景,如HTTP、FTP。
SOCK_DGRAM:数据报套接字,基于UDP协议,提供无连接、不可靠的消息传输。适用于视频流、DNS查询等对实时性要求高但能容忍部分丢包的场景。
SOCK_RAW:原始套接字,允许直接访问网络层及以下的数据包。常用于网络嗅探工具(如tcpdump)或实现自定义协议。
cpp
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建一个IPv4的TCP套接字,第三个参数protocol通常为0,由系统自动选择
二、TCP编程模型详解
TCP是面向连接的协议,通信前必须建立连接。其编程模型类似于电话通信过程:安装电话机(socket)、分配号码(bind)、等待来电(listen)、接听电话(accept)。
2.1 服务器端流程
第一步:创建套接字
cpp
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
第二步:绑定地址
通过bind()将Socket与特定的IP地址和端口号关联。服务器通常需要显式绑定,以便客户端能够找到它
cpp
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网络接口
servaddr.sin_port = htons(8080); // 端口号,需转换为网络字节序
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
第三步:监听连接
listen()将套接字设置为被动模式,准备接受连接请求。backlog参数指定了内核中已完成连接队列的最大长度
cpp
listen(listenfd, 5); // 允许最多5个连接排队等待
第四步:接受连接
accept()从已完成连接队列中取出一个连接。若队列为空,默认会阻塞。返回一个新的文件描述符connfd,用于与客户端通信;而原来的listenfd继续监听新连接。
cpp
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
// 此后用connfd进行读写,listenfd依然存在
第五步:数据交换
建立连接后,使用read()/write()或send()/recv()进行数据传输。
cpp
char buf[1024];
int n = read(connfd, buf, sizeof(buf));
write(connfd, buf, n); // 简单的回显
第六步:关闭连接
cpp
close(connfd);
close(listenfd);
2.2 客户端流程
客户端的步骤相对简单:
socket()创建套接字;
connect()向服务器发起连接请求,此调用会触发TCP三次握手;连接成功后,通过
send()/recv()或write()/read()进行通信;
close()关闭连接。
cpp
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
servaddr.sin_port = htons(8080);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, "Hello", 5);
char buf[1024];
read(sockfd, buf, sizeof(buf));
close(sockfd);
2.3 一个完整的TCP回显服务器
下面给出一个可运行的TCP回显服务器示例,它不断接受客户端连接,将收到的消息原样返回:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int listenfd, connfd;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen = sizeof(cliaddr);
char buffer[BUFFER_SIZE];
// 1. 创建socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 绑定地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(listenfd);
exit(EXIT_FAILURE);
}
// 3. 监听
if (listen(listenfd, 5) < 0) {
perror("listen failed");
close(listenfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受连接并处理
while (1) {
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd < 0) {
perror("accept failed");
continue;
}
printf("New client connected\n");
// 5. 读取并回显
ssize_t n = read(connfd, buffer, BUFFER_SIZE - 1);
while (n > 0) {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
write(connfd, buffer, n);
n = read(connfd, buffer, BUFFER_SIZE - 1);
}
if (n == 0) {
printf("Client disconnected\n");
} else if (n < 0) {
perror("read error");
}
close(connfd);
}
close(listenfd);
return 0;
}
三、UDP编程模型
UDP是无连接的,不需要三次握手。服务器和客户端之间更像"寄信"------知道地址即可发送。
3.1 服务器端
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
struct sockaddr_in cliaddr;
char buf[1024];
socklen_t len = sizeof(cliaddr);
// 循环接收数据
int n = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr *)&cliaddr, &len);
// 可以向该客户端回复
sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, len);
3.2 客户端
UDP客户端可以不调用bind(),操作系统会自动分配临时端口.
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
servaddr.sin_port = htons(8080);
char *msg = "Hello UDP";
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr));
char buf[1024];
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
UDP的特点 :recvfrom()和sendto()每次操作的是完整的数据报。若缓冲区小于数据报长度,多余数据会被截断丢弃。UDP不保证可靠性、顺序性,但在实时音视频、DNS等场景中凭借低延迟优势被广泛使用。
五、I/O多路复用:突破阻塞限制
上述示例中,一个进程同一时间只能处理一个客户端。要同时处理多个连接,需要I/O多路复用技术。
5.1 select
select()允许程序同时监控多个文件描述符的读、写或异常状态。它是POSIX标准的一部分,可移植性最好,但有以下限制:
文件描述符数量受
FD_SETSIZE(通常1024)限制;每次调用都需要将fd集合从用户态拷贝到内核态,效率随fd数量线性下降。
cpp
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenfd, &readfds);
int maxfd = listenfd;
while (1) {
fd_set tmpfds = readfds;
select(maxfd + 1, &tmpfds, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &tmpfds)) {
// 有新连接
int connfd = accept(listenfd, ...);
FD_SET(connfd, &readfds);
if (connfd > maxfd) maxfd = connfd;
}
// 检查其他connfd是否有数据可读...
}
5.2 poll
poll()克服了select()的文件描述符数量限制,使用动态数组管理。但在大量文件描述符时,遍历整个数组仍存在O(n)的性能开销。
5.3 epoll(Linux特有)
epoll是Linux特有的I/O多路复用机制,专为高并发场景设计。它通过以下特性实现高效:
事件驱动 :使用
epoll_ctl()注册感兴趣的事件,内核通过回调机制在事件发生时将其加入就绪列表;零拷贝就绪列表 :
epoll_wait()直接返回就绪的文件描述符,无需遍历全部;支持边缘触发(ET)和水平触发(LT) :ET模式只在状态变化时通知一次,效率更高;LT模式是默认模式,行为类似
poll()。
cpp
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listenfd) {
int connfd = accept(listenfd, ...);
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else {
// 处理已连接套接字的数据
}
}
}