大家好,我是⑩。以下是学习卡哥的网络编程。
本次将围绕网络编程展开。对于C++开发者而言,网络编程是不可或缺的重要技能,它为我们构建高性能网络应用和分布式系统提供了关键支撑。
一:在C++使用TCP socket进行网络通信的核心步骤如下:
服务端
- 创建 socket:调用
socket()创建流式套接字(TCP)。 - 绑定地址:通过
bind()将 socket 与 IP 地址和端口绑定。 - 监听连接:使用
listen()开启监听,设置最大连接队列。 - 接受连接:调用
accept()阻塞等待客户端连接,返回新的 socket 用于通信。 - 数据收发:使用
send()和recv()进行数据传输。 - 关闭 socket:通信结束后关闭连接。
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 1. 创建socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return -1;
}
// 2. 绑定地址
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口
server_addr.sin_port = htons(8080); // 端口号(网络字节序)
if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
std::cerr << "Failed to bind" << std::endl;
close(server_fd);
return -1;
}
// 3. 监听连接
if (listen(server_fd, 3) == -1) {
std::cerr << "Failed to listen" << std::endl;
close(server_fd);
return -1;
}
std::cout << "Server listening on port 8080..." << std::endl;
// 4. 接受连接
sockaddr_in client_addr{};
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
std::cerr << "Failed to accept connection" << std::endl;
close(server_fd);
return -1;
}
std::cout << "Client connected: " << inet_ntoa(client_addr.sin_addr) << std::endl;
// 5. 数据收发
char buffer[1024] = {0};
int valread = recv(client_fd, buffer, 1024, 0);
if (valread > 0) {
std::cout << "Received: " << buffer << std::endl;
send(client_fd, "Hello from server!", strlen("Hello from server!"), 0);
}
// 6. 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
客户端
- 创建 socket:同服务器端。
- 连接服务器:通过
connect()向服务器发起连接请求。 - 数据收发:同服务器端。
- 关闭 socket:通信结束后关闭连接。
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 1. 创建socket
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return -1;
}
// 2. 连接服务器
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
// 将IPv4地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
close(client_fd);
return -1;
}
if (connect(client_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
std::cerr << "Connection failed" << std::endl;
close(client_fd);
return -1;
}
// 3. 数据收发
const char* message = "Hello from client!";
send(client_fd, message, strlen(message), 0);
char buffer[1024] = {0};
int valread = recv(client_fd, buffer, 1024, 0);
if (valread > 0) {
std::cout << "Received from server: " << buffer << std::endl;
}
// 4. 关闭连接
close(client_fd);
return 0;
}
1.关键函数解析
socket():创建套接字
int socket(int domain, int type, int protocol);
// domain: AF_INET (IPv4) 或 AF_INET6 (IPv6)
// type: SOCK_STREAM (TCP) 或 SOCK_DGRAM (UDP)
// protocol: 通常为0,表示自动选择
bind():绑定地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:由socket()函数返回的套接字描述符。
//addr:指向包含本地地址信息的sockaddr结构体。
//addrlen:addr结构体的长度。
listen():监听连接请求
int listen(int sockfd, int backlog);
//sockfd:由socket()函数返回的套接字描述符。
//backlog:指定等待连接队列的最大长度。
accept():接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//sockfd:由socket()函数返回的套接字描述符。
//addr:指向存储客户端地址信息的sockaddr结构体。
//addrlen:addr结构体的长度。
connect():客户端连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:由socket()函数返回的套接字描述符。
//addr:指向包含服务器地址信息的sockaddr结构体。
//addrlen:addr结构体的长度
send()/recv():数据传输
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//sockfd:由socket()函数返回的套接字描述符。
//buf:指向发送或接收数据的缓冲区。
//len:缓冲区的长度。
//flags:传输控制标志
2. 错误处理与优化
- 非阻塞 I/O:使用
fcntl()设置 socket 为非阻塞模式,避免线程阻塞。 - 多线程处理:每个客户端连接分配独立线程,提高并发能力。
- 超时设置:通过
setsockopt()设置SO_RCVTIMEO和SO_SNDTIMEO避免永久阻塞。 - 地址复用:设置
SO_REUSEADDR标志允许快速重启服务器。
3. 常见面试陷阱
Q1:TCP 和 UDP 的主要区别是什么?
- A1:TCP 提供可靠、面向连接的通信;UDP 无连接、不可靠但效率更高。
Q2:为什么服务器需要两个 socket(监听 socket 和通信 socket)?
A2:监听 socket 用于接受连接请求,保持监听状态;通信 socket 用于与客户端实际通信,可创建多个。
Q3:如何优雅地关闭 TCP 连接?
A3:使用shutdown()而非直接close(),可指定关闭发送或接收方向,避免数据丢失。
shutdown(sockfd, SHUT_RDWR); // 关闭读写
二:解释一下socket编程中的阻塞模式和非阻塞模式,以及它们之间的区别?
1.阻塞模式
默认行为 在阻塞模式下,Socket I/O操作(如read,write,accept,connect等)会阻塞调用线程,直到操作完成或发生错误。
如:read()会一直等待,直到接收到数据。write()会等待,直到数据被写入缓冲区。
优点:就是简单,易于理解,但是缺点就是造成线程阻塞影响效率。 那有没有一种非阻塞的行为不需要一直等待,就是如果我需要read()但是没有数据我想做其他的,或者因为read()线程阻塞,导致我无法从终端写入数据进行发送,这个也是同步问题,解决的办法采用IO多路复用(虽然不需要在read()上面阻塞但是阻塞会在io多路复用中进行)。
2.非阻塞模式
修改行为 : 在非阻塞模式下,Socket I/O操作不好阻塞调用线程。如果操作无法立即完成,函数会立即返回,并返回错误代码(通常是EAGAIN或EWOULDBLOCK)。 如:read()返回0或错误码,表示暂时无数据可读,write()返回表示写入的字节数,如果缓冲区满则返回错误。 非阻塞模式通常于IO多路复用结合使用如:select、poll、epoll。
如何切换阻塞和非阻塞模式
通过设置Socket的属性可以切换模式:
1.使用系统调用fcntl:
#include <fcntl.h>
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); // 设置非阻塞模式
2.使用ioctl:
#include <sys/ioctl.h>
int nonblocking = 1;
ioctl(sockfd, FIONBIO, &nonblocking); // 设置非阻塞模式
三:如何编写一个C++服务器,能够同时处理多个客户端的连接请求?
C++ 服务器处理多客户端连接的核心方案有以下三类:
- 多进程 / 多线程模型:
-
- 多进程:每个客户端连接 fork 一个子进程(Unix/Linux)
- 多线程:每个客户端连接创建一个新线程(跨平台)
- 优点:编程简单,隔离性好
- 缺点:资源消耗大(线程 / 进程上下文切换开销)
- I/O 多路复用模型:
-
- select/poll:单线程轮询多个 socket(select 有 FD 数量限制)
- epoll(Linux):事件驱动,高效处理大量连接(LT/ET 模式)
- 优点:资源利用率高,适合高并发
- 缺点:编程复杂度高
- 异步 I/O 模型:
-
- Windows IOCP / Linux aio:内核直接通知 I/O 完成
- 优点:线程利用率最大化
- 缺点:平台依赖,调试困难
1. 多线程服务器实现
#include <thread>
#include <vector>
#include <atomic>
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
std::atomic<bool> server_running(true);
void handle_client(int client_fd) {
char buffer[1024];
while (server_running) {
int bytes_received = recv(client_fd, buffer, 1024, 0);
if (bytes_received <= 0) break;
// 处理数据...
send(client_fd, buffer, bytes_received, 0);
}
close(client_fd);
}
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定和监听...
std::vector<std::thread> threads;
while (server_running) {
int client_fd = accept(server_fd, nullptr, nullptr);
threads.emplace_back(handle_client, client_fd);
threads.back().detach(); // 分离线程,自动回收资源
}
close(server_fd);
return 0;
}
2. select 多路复用实现
2.1.初始化阶段
- 创建服务器套接字
- 绑定地址和端口(代码中省略)
- 开始监听(代码中省略)
2.2 事件循环阶段
while (true) {
// 1. 设置监控集合
FD_ZERO + FD_SET(server) + FD_SET(all clients)
// 2. 等待事件
select(...)
// 3. 处理新连接
if (server_fd ready) accept() and add to client_fds
// 4. 处理客户端数据
for each client:
if (client_fd ready):
recv() data
if (recv failed): remove client
else: echo data back
}
2.3select 工作机制
- 阻塞等待:直到有文件描述符就绪
- 就绪条件:客户端套接字:有数据到达或连接关闭,服务器套接字:有新连接请求。
#include <sys/select.h>
#include <vector>
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);//创建TCP套接字
//AF_INET:IPv4 地址族 SOCK_STREAM:流式套接字(TCP) 0:默认协议
// 绑定和监听
fd_set readfds;//声明文件描述符集合,用于 select 监控可读事件
std::vector<int> client_fds;//创建 vector 存储所有客户端文件描述符
int max_fd = server_fd;//初始化最大文件描述符为服务器套接字
while (true) {
FD_ZERO(&readfds);//清空文字描述符
FD_SET(server_fd, &readfds);//将服务器套接字加入监控集合,用于检测新连接
for (int fd : client_fds) FD_SET(fd, &readfds);//遍历所有客户端套接字,将每个都加入监控集合
//调用selcet等待I/O事件
//max_fd + 1:监控的文件描述符范围(0 到 max_fd)
//&readfds:可读事件集合
//nullptr:可写事件集合(不监控)
//nullptr:异常事件集合(不监控)
//nullptr:超时时间(无限等待)
select(max_fd + 1, &readfds, nullptr, nullptr, nullptr);
if (FD_ISSET(server_fd, &readfds)) {//检查服务器套接字是否有新连接到达
int client_fd = accept(server_fd, nullptr, nullptr);//接受新客户端连接
client_fds.push_back(client_fd);//将新客户端文件描述符加入 vector
max_fd = std::max(max_fd, client_fd);//更新最大文件描述符
}
//开始遍历所有客户端连接(使用迭代器以便删除)
for (auto it = client_fds.begin(); it != client_fds.end();) {
if (FD_ISSET(*it, &readfds)) {//检查当前客户端是否有数据可读
char buffer[1024];
int bytes = recv(*it, buffer, 1024, 0);//从客户端接收数据
//*it:当前客户端文件描述符
//buffer:数据缓冲区
//1024:缓冲区大小
//0:默认标志
if (bytes <= 0) {
close(*it);
it = client_fds.erase(it);//从 vector 中移除客户端文件描述符,并更新迭代器
} else {
send(*it, buffer, bytes, 0);//将接收到的数据原样发回客户端
++it;
}
} else {
++it;
}
}
}
}
3.epoll 多路复用实现(Linux)
#include <sys/epoll.h>
#include <vector>
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAX_EVENTS 10 // epoll_wait一次返回的最大事件数
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);// 创建服务器socket
// 绑定和监听...
int epoll_fd = epoll_create1(0);// 创建epoll实例,参数0表示使用默认方式
epoll_event ev, events[MAX_EVENTS];//定义epoll事件结构和事件数组:
ev.events = EPOLLIN;// 监听可读事件(即有新连接)
ev.data.fd = server_fd;// 设置关联的文件描述符为服务器socket
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); // 将服务器socket加入epoll监听
while (true) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 阻塞等待事件发生,-1表示无限等待
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == server_fd) {//如果是服务器socket事件,表示有新连接:
int client_fd = accept(server_fd, nullptr, nullptr);//// 接受新连接
ev.events = EPOLLIN | EPOLLET; // 设置事件为可读和边缘触发模式
ev.data.fd = client_fd;// // 设置关联的文件描述符为客户端socket
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);// // 将客户端socket加入epoll监听
} else {//如果是客户端socket事件,处理数据
char buffer[1024];
int bytes = recv(events[i].data.fd, buffer, 1024, 0);
if (bytes <= 0) {//如果读取失败或连接关闭,关闭客户端socket:
close(events[i].data.fd);
} else {//否则,回显数据:
send(events[i].data.fd, buffer, bytes, 0);
}
}
}
}
}
4.多线程+epoll混合模型
// 主Reactor线程:接受连接并分发给Worker线程
void main_reactor(int server_fd) {
int epoll_fd = epoll_create1(0);
// 注册server_fd到epoll...
while (true) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == server_fd) {
int client_fd = accept(server_fd, nullptr, nullptr);
dispatch_to_worker(client_fd); // 分发给Worker线程
}
}
}
}
// Worker线程池:每个线程维护一个epoll实例处理I/O
void worker_thread() {
int epoll_fd = epoll_create1(0);
// 注册分配的client_fd到epoll...
while (true) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
// 处理I/O事件...
}
}
1. 核心机制对比
|
方案 |
优点 |
缺点 |
适用场景 |
|
多线程 / 进程 |
编程简单,隔离性好 |
资源消耗大,扩展性差 |
连接数少,计算密集型 |
|
select/poll |
跨平台支持 |
FD 数量受限(select),轮询效率低 |
中小规模并发 |
|
epoll(Linux) |
事件驱动,无 FD 限制 |
Linux 专属,编程复杂度高 |
大规模高并发(如 Web 服务器) |
|
异步 I/O(IOCP) |
线程利用率最大化 |
平台依赖,调试困难 |
特定平台高性能需求 |
2. epoll 关键特性
- 水平触发(LT):默认模式,只要 socket 有数据可读,就会触发事件
- 边缘触发(ET):仅在数据到来时触发一次,要求一次性读完所有数据
- 高效机制:使用红黑树管理 FD,事件链表通知就绪 FD,O (1) 时间复杂度
3. 常见面试陷阱
- Q1:为什么 select 有 1024 个 FD 的限制? A1:历史上由
fd_set的实现(位图)决定,可通过修改内核参数调整,但效率仍低于 epoll。 - Q2:epoll 的 ET 模式为什么要求非阻塞 socket? A2:ET 模式下若数据未读完,不会再次触发事件,使用阻塞 socket 可能导致线程永久阻塞。
- Q3:如何处理多线程服务器中的竞态条件? A3:使用互斥锁(
std::mutex)保护共享资源,或采用无锁数据结构(如原子操作)。
【总结】
- 选择策略:
-
- 小规模连接:多线程 / 进程(简单)
- 中等规模并发:select/poll(跨平台)
- 大规模高并发:epoll(Linux 高性能)
- 极致性能:异步 I/O + 线程池(特定场景)
- 关键优化:
-
- 使用线程池减少线程创建开销
- 采用边缘触发模式提高 epoll 效率
- 分离 I/O 操作与业务逻辑(Reactor 模式)
- 使用零拷贝技术(如
splice())减少数据拷贝
- 现代实践:
-
- 使用 Boost.Asio、libevent 等成熟库封装底层差异
- 结合协程(如 C++20 的 coroutine)简化异步编程模型
- 考虑 io_uring(Linux 5.1+)进一步提升 I/O 性能
-
四:在C++网络编程中,如何处理粘包和拆包问题?
对于粘包和拆包问题,常见的解决方案有四种:
- 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
- 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
- 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。
1280

被折叠的 条评论
为什么被折叠?



