C++网络编程

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.5k人参与

大家好,我是⑩。以下是学习卡哥的网络编程。

本次将围绕网络编程展开。对于C++开发者而言,网络编程是不可或缺的重要技能,它为我们构建高性能网络应用和分布式系统提供了关键支撑。

一:在C++使用TCP socket进行网络通信的核心步骤如下:

服务端

  1. 创建 socket:调用socket()创建流式套接字(TCP)。
  2. 绑定地址:通过bind()将 socket 与 IP 地址和端口绑定。
  3. 监听连接:使用listen()开启监听,设置最大连接队列。
  4. 接受连接:调用accept()阻塞等待客户端连接,返回新的 socket 用于通信。
  5. 数据收发:使用send()recv()进行数据传输。
  6. 关闭 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;
}

客户端

  1. 创建 socket:同服务器端。
  2. 连接服务器:通过connect()向服务器发起连接请求。
  3. 数据收发:同服务器端。
  4. 关闭 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_RCVTIMEOSO_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操作(如readwriteacceptconnect等)会阻塞调用线程,直到操作完成或发生错误。

如: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++ 服务器处理多客户端连接的核心方案有以下三类:

  1. 多进程 / 多线程模型
    • 多进程:每个客户端连接 fork 一个子进程(Unix/Linux)
    • 多线程:每个客户端连接创建一个新线程(跨平台)
    • 优点:编程简单,隔离性好
    • 缺点:资源消耗大(线程 / 进程上下文切换开销)
  1. I/O 多路复用模型
    • select/poll:单线程轮询多个 socket(select 有 FD 数量限制)
    • epoll(Linux):事件驱动,高效处理大量连接(LT/ET 模式)
    • 优点:资源利用率高,适合高并发
    • 缺点:编程复杂度高
  1. 异步 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协议;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值