C++后端开发--网络编程基础

目录

一、网络编程基础概念

[1.1 网络协议](#1.1 网络协议)

[1.2 IP地址和端口号](#1.2 IP地址和端口号)

[1.3 Socket](#1.3 Socket)

[1.4 TCP协议的三次握手和四次挥手](#1.4 TCP协议的三次握手和四次挥手)

TCP的三次握手

TCP的四次挥手

整个流程更通俗易懂

[TCP 三次握手流程图](#TCP 三次握手流程图)

[TCP 四次挥手流程图](#TCP 四次挥手流程图)

[1.5 详细介绍一下http协议](#1.5 详细介绍一下http协议)

HTTP协议的主要特点

HTTP请求

HTTP请求方法

HTTP响应

常见的HTTP状态码

[HTTP/1.1 vs HTTP/2 vs HTTP/3](#HTTP/1.1 vs HTTP/2 vs HTTP/3)

HTTP/1.1

HTTP/2

HTTP/3

HTTP安全

二、Socket编程

[2.1 创建Socket](#2.1 创建Socket)

[2.2 绑定地址和端口](#2.2 绑定地址和端口)

[2.3 监听和接受连接](#2.3 监听和接受连接)

[2.4 发送和接收数据](#2.4 发送和接收数据)

[2.5 关闭Socket](#2.5 关闭Socket)

三、简单的服务器示例

四、结语

[五、深入探讨 select 和 epoll,以及多客户端编程](#五、深入探讨 select 和 epoll,以及多客户端编程)

[5.1 select 的工作原理及使用](#5.1 select 的工作原理及使用)

[5.1.1 select 基本概念](#5.1.1 select 基本概念)

[5.1.2 select 函数原型](#5.1.2 select 函数原型)

[5.1.3 使用 select 的步骤](#5.1.3 使用 select 的步骤)

[5.1.4 select 示例](#5.1.4 select 示例)

[5.2 epoll 的工作原理及使用](#5.2 epoll 的工作原理及使用)

[5.2.1 epoll 基本概念](#5.2.1 epoll 基本概念)

[5.2.2 epoll 函数](#5.2.2 epoll 函数)

[5.2.3 使用 epoll 的步骤](#5.2.3 使用 epoll 的步骤)

[5.2.4 epoll 示例](#5.2.4 epoll 示例)

[5.3、select 和 epoll 的比较](#5.3、select 和 epoll 的比较)

[5.3.1 select 的优缺点](#5.3.1 select 的优缺点)

[5.3.2 epoll 的优缺点](#5.3.2 epoll 的优缺点)

5.4、结语


网络编程是后端开发中不可或缺的一部分,尤其是在构建需要与其他系统或设备通信的应用程序时。本文将从基础概念开始,逐步深入网络编程的各个方面,帮助读者建立全面的网络编程知识体系。

一、网络编程基础概念

1.1 网络协议

网络协议是计算机网络中进行数据交换的规则。常见的网络协议包括:

  • TCP/IP:传输控制协议/互联网协议,是互联网的核心协议。TCP提供可靠的、面向连接的通信,而IP负责数据包的路由和传输。
  • UDP:用户数据报协议,是一种无连接的协议,适用于实时应用,如视频流和在线游戏。
  • HTTP/HTTPS:超文本传输协议/安全超文本传输协议,用于浏览器和服务器之间的通信。
  • FTP:文件传输协议,用于文件的上传和下载。

1.2 IP地址和端口号

  • IP地址 :标识网络中的每个设备,如192.168.1.1
  • 端口号 :用于区分同一设备上的不同服务,范围为065535

1.3 Socket

Socket是网络编程的基石,提供了应用层与TCP/IP协议栈通信的接口。主要类型有:

  • 流式套接字(Stream Socket):基于TCP,提供可靠的数据传输。
  • 数据报套接字(Datagram Socket):基于UDP,适用于无连接的数据传输。

1.4 TCP协议的三次握手和四次挥手

TCP的三次握手

三次握手(Three-way Handshake)是TCP建立连接的过程,确保双方能够正确接收和发送数据。三次握手的步骤如下:

  1. 第一次握手(SYN)

    • 客户端向服务器发送一个SYN(Synchronize)报文,表示请求建立连接。

    • 报文头中的SYN标志位被置为1,同时生成一个初始序列号(Sequence Number),假设为x。

      客户端 -> 服务器:SYN=1, Seq=x

  2. 第二次握手(SYN-ACK)

    • 服务器收到客户端的SYN报文后,确认连接请求,并向客户端发送一个SYN-ACK(Synchronize-Acknowledgment)报文。

    • 报文头中的SYN和ACK标志位都被置为1,ACK号为x+1(确认已收到客户端的SYN),并生成一个自己的初始序列号(假设为y)。

      服务器 -> 客户端:SYN=1, ACK=1, Seq=y, Ack=x+1

  3. 第三次握手(ACK)

    • 客户端收到服务器的SYN-ACK报文后,向服务器发送一个ACK(Acknowledgment)报文,表示确认连接建立。

    • 报文头中的ACK标志位被置为1,ACK号为y+1(确认已收到服务器的SYN),序列号为x+1。

      客户端 -> 服务器:ACK=1, Seq=x+1, Ack=y+1

完成三次握手后,客户端和服务器之间的TCP连接正式建立,可以开始数据传输。

TCP的四次挥手

四次挥手(Four-way Handshake)是TCP断开连接的过程,确保双方都能正常关闭连接。四次挥手的步骤如下:

  1. 第一次挥手(FIN)

    • 一方(通常是客户端)向另一方发送一个FIN(Finish)报文,表示希望关闭连接。

    • 报文头中的FIN标志位被置为1,序列号为u。

      客户端 -> 服务器:FIN=1, Seq=u

  2. 第二次挥手(ACK)

    • 另一方(通常是服务器)收到FIN报文后,向发送方发送一个ACK报文,确认已经收到关闭连接的请求。

    • 报文头中的ACK标志位被置为1,ACK号为u+1。

      服务器 -> 客户端:ACK=1, Seq=v, Ack=u+1

  3. 第三次挥手(FIN)

    • 服务器也向客户端发送一个FIN报文,表示同意关闭连接。

    • 报文头中的FIN标志位被置为1,序列号为w。

      服务器 -> 客户端:FIN=1, Seq=w

  4. 第四次挥手(ACK)

    • 客户端收到服务器的FIN报文后,向服务器发送一个ACK报文,确认已经收到关闭连接的请求。

    • 报文头中的ACK标志位被置为1,ACK号为w+1。

      客户端 -> 服务器:ACK=1, Seq=u+1, Ack=w+1

完成四次挥手后,客户端和服务器之间的TCP连接正式关闭。

通过三次握手和四次挥手机制,TCP协议能够确保可靠地建立和关闭连接,使得数据传输变得可靠和有序。这是网络编程中非常重要的部分,理解和掌握这些概念对于实现高效和稳定的网络通信至关重要。

整个流程更通俗易懂
TCP 三次握手流程图
客户端                              服务器
  |                                    |
  | --------- SYN, Seq=x ------------> |
  |                                    |
  | <------ SYN, ACK, Seq=y, Ack=x+1 --|
  |                                    |
  | --------- ACK, Seq=x+1, Ack=y+1 -->|
  |                                    |
  • 解释:
  1. 第一次握手:客户端发送SYN

    • 客户端向服务器发送一个SYN(同步)包,表明客户端想要建立连接,并且包含一个初始序列号(Seq=x)。
  2. 第二次握手:服务器回应SYN-ACK

    • 服务器收到SYN包后,回应一个SYN-ACK包,表示同意连接。这个包包含服务器的初始序列号(Seq=y)和对客户端SYN包的确认(Ack=x+1)。
  3. 第三次握手:客户端发送ACK

    • 客户端收到SYN-ACK包后,回应一个ACK包,确认服务器的SYN包(Ack=y+1),此时连接建立,双方可以开始传输数据。
TCP 四次挥手流程图
客户端                              服务器
  |                                    |
  | --------- FIN, Seq=u ------------> |
  |                                    |
  | <--------- ACK, Seq=v, Ack=u+1 ----|
  |                                    |
  | <--------- FIN, Seq=w ------------ |
  |                                    |
  | --------- ACK, Seq=u+1, Ack=w+1 -->|
  |                                    |
  • 解释:
  1. 第一次挥手:客户端发送FIN

    • 客户端发送一个FIN(终止)包,表示客户端要关闭连接,并且包含当前序列号(Seq=u)。
  2. 第二次挥手:服务器回应ACK

    • 服务器收到FIN包后,回应一个ACK包,确认客户端的FIN包(Ack=u+1),此时客户端到服务器的连接关闭,但服务器到客户端的连接仍然存在。
  3. 第三次挥手:服务器发送FIN

    • 服务器也发送一个FIN包,表示服务器也要关闭连接,并且包含当前序列号(Seq=w)。
  4. 第四次挥手:客户端回应ACK

    • 客户端收到服务器的FIN包后,回应一个ACK包,确认服务器的FIN包(Ack=w+1),此时整个连接正式关闭。

这些流程图展示了TCP协议在建立和关闭连接时的具体步骤,帮助理解TCP三次握手和四次挥手的机制。这样,你可以更直观地看到每一步是如何进行的,以及每一步的作用。

1.5 详细介绍一下http协议

HTTP(HyperText Transfer Protocol,超文本传输协议)是用于分布式、协作和超媒体信息系统的应用层协议,是万维网数据通信的基础。HTTP起初由蒂姆·伯纳斯-李(Tim Berners-Lee)为万维网设计,现由互联网工程任务组(IETF)和万维网联盟(W3C)共同维护。HTTP协议定义了浏览器(客户端)与Web服务器之间的通信规则。

HTTP协议的主要特点
  1. 简单快速

    • 客户端向服务器请求服务时,只需传送请求方法和路径。
    • 由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  2. 灵活

    • HTTP允许传输任意类型的数据对象。通过Content-Type头,可以表示具体的数据类型。
  3. 无连接

    • 无连接的意思是限制每次连接只处理一个请求。服务器处理完客户端的请求并收到客户端的应答后,就断开连接。但这种方式能节省传输时间。
  4. 无状态

    • HTTP协议是无状态协议,即对事务处理没有记忆能力。每次请求都是独立的,服务器不保留任何会话信息。
HTTP请求

一个HTTP请求由以下部分组成:

  1. 请求行:包括请求方法、请求URL和HTTP版本。

    • 示例:GET /index.html HTTP/1.1
  2. 请求头:包括各种头部信息,用于客户端向服务器传递附加信息。

    • 示例:

      Host: www.example.com
      User-Agent: Mozilla/5.0
      Accept: text/html
      
  3. 空行:用于分隔请求头和请求体。

  4. 请求体:包含客户端发送给服务器的数据(仅在POST、PUT等请求方法中使用)。

HTTP请求方法

常见的HTTP请求方法包括:

  • GET:请求获取指定资源。常用于请求数据。
  • POST:向指定资源提交数据。常用于提交表单或上传文件。
  • PUT:上传指定资源的最新内容。
  • DELETE:删除指定资源。
  • HEAD:类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头。
  • OPTIONS:返回服务器支持的HTTP请求方法。
  • PATCH:对资源进行部分修改。
HTTP响应

一个HTTP响应由以下部分组成:

  1. 状态行:包括HTTP版本、状态码和状态描述。

    • 示例:HTTP/1.1 200 OK
  2. 响应头:包括各种头部信息,用于服务器向客户端传递附加信息。

    • 示例:

      Content-Type: text/html
      Content-Length: 1234
      
  3. 空行:用于分隔响应头和响应体。

  4. 响应体:包含服务器返回给客户端的数据。

常见的HTTP状态码
  • 1xx(信息性状态码):表示请求已被接收,继续处理。

    • 100 Continue
    • 101 Switching Protocols
  • 2xx(成功状态码):表示请求已成功被服务器接收、理解并接受。

    • 200 OK
    • 201 Created
  • 3xx(重定向状态码):表示需要客户端采取进一步的操作以完成请求。

    • 301 Moved Permanently
    • 302 Found
    • 304 Not Modified
  • 4xx(客户端错误状态码):表示请求包含错误或无法完成。

    • 400 Bad Request
    • 401 Unauthorized
    • 403 Forbidden
    • 404 Not Found
  • 5xx(服务器错误状态码):表示服务器在处理请求的过程中发生了错误。

    • 500 Internal Server Error
    • 502 Bad Gateway
    • 503 Service Unavailable
HTTP/1.1 vs HTTP/2 vs HTTP/3
HTTP/1.1
  • 长连接:默认使用长连接,允许多个请求/响应在同一连接上进行,减少了连接的建立和关闭次数,提高了传输效率。
  • 管道化:允许客户端在收到HTTP响应之前发送多个HTTP请求,但服务器仍然会按顺序响应。
HTTP/2
  • 二进制分帧:使用二进制格式传输数据,更高效和更容易解析。
  • 多路复用:允许同时通过单一的HTTP/2连接发送多个请求和响应。
  • 头部压缩:使用HPACK算法对头部信息进行压缩,减少带宽占用。
  • 服务器推送:服务器可以主动向客户端推送资源,而不需要客户端明确请求。
HTTP/3
  • 基于QUIC:HTTP/3基于QUIC协议,使用UDP传输,减少了连接建立的延迟。
  • 更快的连接建立:由于QUIC在单个数据包内完成握手,大大减少了连接建立时间。
  • 改进的多路复用:避免了HTTP/2中的队头阻塞问题,更高效。
HTTP安全

HTTP本身是明文传输的,容易被窃听和篡改。为了提高安全性,通常使用HTTPS(HTTP Secure),即HTTP over TLS(或SSL),加密HTTP通信,保证数据的机密性和完整性。

  • HTTPS:通过使用TLS(传输层安全性)来加密数据传输,确保数据在传输过程中不被窃听和篡改。
  • SSL/TLS:安全套接字层/传输层安全性,是一种加密协议,用于保护互联网通信安全。

总结来说,HTTP协议是万维网的基石,通过明确的请求和响应机制,客户端和服务器可以高效地进行通信。随着技术的发展,HTTP协议也在不断演进,以提高性能、安全性和用户体验。

二、Socket编程

2.1 创建Socket

在C++中,可以使用socket()函数创建Socket。语法如下:

int socket(int domain, int type, int protocol);
  • domain:地址族,如AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:Socket类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
  • protocol:协议,一般设为0,由系统自动选择。

示例:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

2.2 绑定地址和端口

使用bind()函数将Socket绑定到特定的IP地址和端口。语法如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

示例:

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(8080);

if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
    perror("socket bind failed");
    exit(EXIT_FAILURE);
}

2.3 监听和接受连接

使用listen()函数使Socket进入监听状态,并使用accept()函数接受客户端连接。

int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

示例:

if (listen(sockfd, 5) != 0) {
    perror("Listen failed");
    exit(EXIT_FAILURE);
}

int connfd = accept(sockfd, (struct sockaddr *)&cli, &len);
if (connfd < 0) {
    perror("server accept failed");
    exit(EXIT_FAILURE);
}

2.4 发送和接收数据

使用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);

示例:

char buffer[1024] = {0};
recv(connfd, buffer, sizeof(buffer), 0);
send(connfd, "Hello from server", strlen("Hello from server"), 0);

2.5 关闭Socket

使用close()函数关闭Socket。

close(sockfd);

三、简单的服务器示例

以下是一个简单的C++服务器示例,使用TCP协议,监听8080端口,并向每个连接的客户端发送一条欢迎消息。

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int sockfd, connfd;
    struct sockaddr_in servaddr, cli;

    // 创建Socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        std::cerr << "Socket creation failed\n";
        exit(EXIT_FAILURE);
    }

    // 绑定IP和端口
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);

    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
        std::cerr << "Socket bind failed\n";
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(sockfd, 5) != 0) {
        std::cerr << "Listen failed\n";
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    socklen_t len = sizeof(cli);
    connfd = accept(sockfd, (struct sockaddr *)&cli, &len);
    if (connfd < 0) {
        std::cerr << "Server accept failed\n";
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 发送欢迎消息
    const char *message = "Hello from server";
    send(connfd, message, strlen(message), 0);

    // 关闭连接
    close(connfd);
    close(sockfd);

    return 0;
}

四、结语

网络编程是后端开发中的重要技能,掌握基础概念和Socket编程方法是深入学习和实践的第一步。本文介绍了网络协议、IP地址、端口号以及Socket编程的基本操作。希望通过这篇文章,读者能对网络编程有一个初步的了解,并能够编写简单的网络应用程序。

在后续的文章中,我们将深入探讨高级网络编程技术,包括多线程服务器、异步I/O、网络安全等内容,敬请期待。

五、深入探讨 selectepoll,以及多客户端编程

在网络编程中,处理多个客户端连接是一项常见且重要的任务。为了有效地管理多个连接,操作系统提供了多种I/O多路复用机制,如selectpollepoll。本文将详细讲解selectepoll,并展示如何使用它们实现多客户端编程。

5.1 select 的工作原理及使用

5.1.1 select 基本概念

select 是一种I/O多路复用机制,用于监视多个文件描述符(如Socket)上的事件,如数据可读、可写或异常状态。当任何一个文件描述符上的事件发生时,select 返回,程序可以对这些事件进行处理。

5.1.2 select 函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:所有文件描述符中最大值加一。
  • readfds:可读事件的文件描述符集合。
  • writefds:可写事件的文件描述符集合。
  • exceptfds:异常事件的文件描述符集合。
  • timeout:超时时间,NULL表示永不超时。
5.1.3 使用 select 的步骤
  1. 初始化文件描述符集合。
  2. 将需要监视的文件描述符添加到集合中。
  3. 调用 select 函数,等待事件发生。
  4. 遍历文件描述符集合,处理已发生的事件。
5.1.4 select 示例

下面是一个使用 select 实现的简单多客户端服务器示例:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>

#define PORT 8080
#define MAX_CLIENTS 30

int main() {
    int server_fd, new_socket, client_socket[MAX_CLIENTS], max_clients = MAX_CLIENTS, activity, i, valread, sd;
    int max_sd;
    struct sockaddr_in address;
    char buffer[1025];
    fd_set readfds;

    // 初始化所有客户端socket为0
    for (i = 0; i < max_clients; i++) {
        client_socket[i] = 0;
    }

    // 创建服务器socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    int addrlen = sizeof(address);
    std::cout << "Listening on port " << PORT << std::endl;

    while (true) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);

        // 添加服务器socket到集合
        FD_SET(server_fd, &readfds);
        max_sd = server_fd;

        // 添加客户端socket到集合
        for (i = 0; i < max_clients; i++) {
            sd = client_socket[i];
            if (sd > 0)
                FD_SET(sd, &readfds);
            if (sd > max_sd)
                max_sd = sd;
        }

        // 等待活动事件
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            std::cerr << "select error" << std::endl;
        }

        // 处理新连接
        if (FD_ISSET(server_fd, &readfds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            std::cout << "New connection, socket fd is " << new_socket << ", ip is: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;

            // 添加新socket到客户端数组
            for (i = 0; i < max_clients; i++) {
                if (client_socket[i] == 0) {
                    client_socket[i] = new_socket;
                    std::cout << "Adding to list of sockets as " << i << std::endl;
                    break;
                }
            }
        }

        // 处理已连接的客户端的IO操作
        for (i = 0; i < max_clients; i++) {
            sd = client_socket[i];
            if (FD_ISSET(sd, &readfds)) {
                if ((valread = read(sd, buffer, 1024)) == 0) {
                    // 某客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    std::cout << "Host disconnected, ip: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
                    close(sd);
                    client_socket[i] = 0;
                } else {
                    buffer[valread] = '\0';
                    std::cout << "Received message: " << buffer << std::endl;
                    send(sd, buffer, strlen(buffer), 0);
                }
            }
        }
    }

    return 0;
}

5.2 epoll 的工作原理及使用

5.2.1 epoll 基本概念

epoll 是Linux特有的一种I/O多路复用机制,提供比 selectpoll 更高效的事件通知。epoll 使用事件驱动模型,通过内核维护一个事件表,减少用户空间和内核空间之间的拷贝开销。

5.2.2 epoll 函数

epoll 主要包含以下三个函数:

  1. epoll_create1 :创建一个 epoll 实例。
  2. epoll_ctl :控制 epoll 实例,添加、删除或修改事件。
  3. epoll_wait:等待事件发生,并返回已触发的事件。
5.2.3 使用 epoll 的步骤
  1. 创建 epoll 实例。
  2. 使用 epoll_ctl 函数将文件描述符添加到 epoll 实例中。
  3. 调用 epoll_wait 函数等待事件发生。
  4. 处理已触发的事件。
5.2.4 epoll 示例

下面是一个使用 epoll 实现的简单多客户端服务器示例:

#include <iostream>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>

#define PORT 8080
#define MAX_EVENTS 10
#define MAX_CLIENTS 30

int main() {
    int server_fd, new_socket, epoll_fd, event_count, i, valread;
    struct sockaddr_in address;
    struct epoll_event ev, events[MAX_EVENTS];
    char buffer[1025];

    // 创建服务器socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket为非阻塞
    fcntl(server_fd, F_SETFL, O_NONBLOCK);

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 添加服务器socket到epoll实例
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        perror("epoll_ctl: server_fd");
        close(server_fd);
        close(epoll_fd);
        exit(EXIT_FAILURE);
    }

    std::cout << "Listening on port " << PORT << std::endl;

    while (true) {
        // 等待事件发生
        event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if

 (event_count == -1) {
            perror("epoll_wait");
            close(server_fd);
            close(epoll_fd);
            exit(EXIT_FAILURE);
        }

        for (i = 0; i < event_count; i++) {
            if (events[i].data.fd == server_fd) {
                // 处理新连接
                new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
                if (new_socket == -1) {
                    perror("accept");
                    continue;
                }
                std::cout << "New connection, socket fd is " << new_socket << ", ip is: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;

                // 设置新socket为非阻塞
                fcntl(new_socket, F_SETFL, O_NONBLOCK);

                // 添加新socket到epoll实例
                ev.events = EPOLLIN;
                ev.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {
                    perror("epoll_ctl: new_socket");
                    close(new_socket);
                }
            } else {
                // 处理客户端IO操作
                valread = read(events[i].data.fd, buffer, 1024);
                if (valread == -1) {
                    perror("read");
                    close(events[i].data.fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                } else if (valread == 0) {
                    // 客户端断开连接
                    getpeername(events[i].data.fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    std::cout << "Host disconnected, ip: " << inet_ntoa(address.sin_addr) << ", port: " << ntohs(address.sin_port) << std::endl;
                    close(events[i].data.fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                } else {
                    buffer[valread] = '\0';
                    std::cout << "Received message: " << buffer << std::endl;
                    send(events[i].data.fd, buffer, strlen(buffer), 0);
                }
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
}

5.3、selectepoll 的比较

5.3.1 select 的优缺点

优点:

  • 跨平台,几乎在所有操作系统上都支持。
  • 简单易用,适合小规模应用。

缺点:

  • 每次调用 select 都需要重新设置文件描述符集合,效率较低。
  • 支持的文件描述符数量有限(一般为1024)。
5.3.2 epoll 的优缺点

优点:

  • 高效,支持大量并发连接,适用于高并发场景。
  • 事件驱动模型,减少了无效的系统调用。

缺点:

  • 仅支持Linux,跨平台性较差。
  • 相对复杂,需要更多的代码来管理事件。

5.4、结语

上面详细介绍了 selectepoll 的工作原理及其使用方法,并通过示例展示了如何实现多客户端编程。对于小规模的网络应用,可以选择简单易用的 select;而对于高并发、高性能的应用,epoll 则是更好的选择。

通过学习和实践这些I/O多路复用技术,读者可以更好地理解和掌握网络编程中的并发处理,为构建高效稳定的网络应用奠定坚实的基础。

相关推荐
喵叔哟17 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生23 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow37 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
网络安全-杰克44 分钟前
网络安全概论
网络·web安全·php
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
怀澈1221 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
请叫我欧皇i1 小时前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落1 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++