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多路复用技术,读者可以更好地理解和掌握网络编程中的并发处理,为构建高效稳定的网络应用奠定坚实的基础。

相关推荐
爱吃生蚝的于勒2 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~6 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
binishuaio11 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE13 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻17 分钟前
WPF中的依赖属性
开发语言·wpf
洋24026 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙27 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点28 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder1 小时前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发
苏三有春1 小时前
PyQt5实战——UTF-8编码器功能的实现(六)
开发语言·qt