MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 网络编程

2024-08-251.4k 阅读

C++ 网络编程基础

在 C++ 网络编程中,我们首先要了解网络通信的基本概念。网络通信是指不同计算机系统之间通过网络进行数据交换和共享的过程。在这个过程中,我们需要处理诸如 IP 地址、端口号、协议等关键要素。

IP 地址

IP 地址是网络中设备的唯一标识符,分为 IPv4 和 IPv6。IPv4 采用 32 位二进制数表示,通常以点分十进制形式呈现,例如 192.168.1.1。而 IPv6 则使用 128 位二进制数,以冒号十六进制表示,如 2001:0db8:85a3:0000:0000:8a2e:0370:7334。在 C++ 网络编程中,我们需要能够处理这两种类型的 IP 地址。

端口号

端口号用于标识应用程序在计算机中的特定进程。它是一个 16 位的无符号整数,范围从 0 到 65535。其中,0 到 1023 为系统保留端口,用于常见的网络服务,如 HTTP(80)、FTP(21)等。在网络编程中,我们需要为自己的应用程序选择合适的端口号。

协议

网络通信中常用的协议有传输控制协议(TCP)和用户数据报协议(UDP)。TCP 是一种面向连接的、可靠的协议,它保证数据的有序传输和完整性。UDP 则是无连接的、不可靠的协议,但具有传输速度快、开销小的特点。在 C++ 网络编程中,我们可以根据具体需求选择使用 TCP 或 UDP。

使用 Socket 进行网络编程

Socket 是网络编程的核心概念,它是应用层与传输层之间的接口。在 C++ 中,我们可以使用系统提供的 Socket 接口进行网络编程。

创建 Socket

在 Unix - like 系统中,我们使用 socket 函数来创建 Socket。该函数的原型如下:

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

int main() {
    // 创建 Socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }
    std::cout << "Socket created successfully" << std::endl;

    // 关闭 Socket
    close(sockfd);
    return 0;
}

在上述代码中,socket 函数的第一个参数 AF_INET 表示使用 IPv4 地址族,SOCK_STREAM 表示使用 TCP 协议,0 表示默认协议。如果 socket 函数调用成功,会返回一个非负的文件描述符,即我们创建的 Socket。

绑定 Socket

创建 Socket 后,我们需要将其绑定到一个特定的 IP 地址和端口号上。在 Unix - like 系统中,使用 bind 函数来完成此操作。bind 函数的原型如下:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    // 绑定 Socket
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }
    std::cout << "Bind successful" << std::endl;

    close(sockfd);
    return 0;
}

在这段代码中,我们首先创建了一个 sockaddr_in 结构体变量 servaddr,用于存储服务器的地址信息。sin_family 设置为 AF_INET 表示 IPv4 地址族,sin_port 使用 htons 函数将端口号 8080 转换为网络字节序,sin_addr.s_addr 设置为 INADDR_ANY 表示绑定到所有可用的网络接口。然后,通过 bind 函数将 Socket 绑定到指定的地址和端口。

监听 Socket

对于服务器端,在绑定 Socket 后,需要开始监听来自客户端的连接请求。在 Unix - like 系统中,使用 listen 函数来实现监听功能。listen 函数的原型如下:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }

    // 监听 Socket
    if (listen(sockfd, 5) < 0) {
        std::cerr << "Listen failed" << std::endl;
        close(sockfd);
        return -1;
    }
    std::cout << "Listening on port 8080" << std::endl;

    close(sockfd);
    return 0;
}

listen 函数的第一个参数是要监听的 Socket 文件描述符,第二个参数 backlog 表示等待连接队列的最大长度。这里设置为 5,表示最多可以有 5 个客户端连接在队列中等待处理。

接受连接

服务器在监听后,需要接受客户端的连接请求。在 Unix - like 系统中,使用 accept 函数来实现此功能。accept 函数的原型如下:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }

    if (listen(sockfd, 5) < 0) {
        std::cerr << "Listen failed" << std::endl;
        close(sockfd);
        return -1;
    }

    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    // 接受连接
    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        std::cerr << "Accept failed" << std::endl;
        close(sockfd);
        return -1;
    }
    std::cout << "Connection accepted" << std::endl;

    close(connfd);
    close(sockfd);
    return 0;
}

accept 函数的第一个参数是监听的 Socket 文件描述符,第二个参数 cliaddr 用于存储客户端的地址信息,第三个参数 lencliaddr 结构体的长度。accept 函数会阻塞等待客户端的连接请求,当有客户端连接时,会返回一个新的 Socket 文件描述符 connfd,用于与客户端进行通信。

客户端连接

在客户端,我们需要使用 connect 函数来连接到服务器。connect 函数的原型如下:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

    // 连接到服务器
    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Connect failed" << std::endl;
        close(sockfd);
        return -1;
    }
    std::cout << "Connected to server" << std::endl;

    close(sockfd);
    return 0;
}

在这段代码中,我们创建了一个客户端 Socket,并设置服务器的地址信息。然后使用 connect 函数连接到服务器,connect 函数的第一个参数是客户端 Socket 文件描述符,第二个参数是服务器的地址信息,第三个参数是地址信息的长度。如果连接成功,程序会输出连接成功的信息。

数据传输

在建立连接后,我们就可以进行数据传输了。在 TCP 编程中,常用 sendrecv 函数进行数据的发送和接收。

发送数据

在服务器端和客户端都可以使用 send 函数发送数据。send 函数的原型如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }

    if (listen(sockfd, 5) < 0) {
        std::cerr << "Listen failed" << std::endl;
        close(sockfd);
        return -1;
    }

    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        std::cerr << "Accept failed" << std::endl;
        close(sockfd);
        return -1;
    }

    const char *msg = "Hello, client!";
    // 发送数据
    ssize_t bytes_sent = send(connfd, msg, strlen(msg), 0);
    if (bytes_sent < 0) {
        std::cerr << "Send failed" << std::endl;
    } else {
        std::cout << "Data sent successfully: " << bytes_sent << " bytes" << std::endl;
    }

    close(connfd);
    close(sockfd);
    return 0;
}

在上述服务器端代码中,我们在接受客户端连接后,使用 send 函数向客户端发送一条消息。send 函数的第一个参数是连接的 Socket 文件描述符,第二个参数是要发送的数据缓冲区,第三个参数是数据的长度,第四个参数通常设置为 0。

接收数据

在服务器端和客户端都可以使用 recv 函数接收数据。recv 函数的原型如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Connect failed" << std::endl;
        close(sockfd);
        return -1;
    }

    char buffer[1024];
    // 接收数据
    ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_received < 0) {
        std::cerr << "Recv failed" << std::endl;
    } else {
        buffer[bytes_received] = '\0';
        std::cout << "Received data: " << buffer << std::endl;
    }

    close(sockfd);
    return 0;
}

在上述客户端代码中,我们在连接到服务器后,使用 recv 函数接收服务器发送的数据。recv 函数的第一个参数是连接的 Socket 文件描述符,第二个参数是接收数据的缓冲区,第三个参数是缓冲区的大小减 1(为了给字符串结束符 '\0' 留出空间),第四个参数通常设置为 0。

UDP 网络编程

UDP 网络编程与 TCP 有所不同,它不需要建立连接,直接进行数据的发送和接收。

创建 UDP Socket

创建 UDP Socket 的方式与 TCP 类似,只是在 socket 函数的第二个参数使用 SOCK_DGRAM 表示 UDP 协议。

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

int main() {
    // 创建 UDP Socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }
    std::cout << "UDP Socket created successfully" << std::endl;

    close(sockfd);
    return 0;
}

发送 UDP 数据

在 UDP 编程中,使用 sendto 函数发送数据,因为 UDP 不需要连接,所以需要指定目标地址。sendto 函数的原型如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

    const char *msg = "Hello, UDP server!";
    // 发送 UDP 数据
    ssize_t bytes_sent = sendto(sockfd, msg, strlen(msg), 0, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    if (bytes_sent < 0) {
        std::cerr << "Sendto failed" << std::endl;
    } else {
        std::cout << "UDP data sent successfully: " << bytes_sent << " bytes" << std::endl;
    }

    close(sockfd);
    return 0;
}

在上述代码中,sendto 函数的第一个参数是 UDP Socket 文件描述符,第二个参数是要发送的数据缓冲区,第三个参数是数据长度,第四个参数通常为 0,第五个参数是目标地址,第六个参数是目标地址的长度。

接收 UDP 数据

在 UDP 编程中,使用 recvfrom 函数接收数据,同时可以获取发送方的地址。recvfrom 函数的原型如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }

    char buffer[1024];
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    // 接收 UDP 数据
    ssize_t bytes_received = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&cliaddr, &len);
    if (bytes_received < 0) {
        std::cerr << "Recvfrom failed" << std::endl;
    } else {
        buffer[bytes_received] = '\0';
        char cli_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &(cliaddr.sin_addr), cli_ip, INET_ADDRSTRLEN);
        std::cout << "Received UDP data from " << cli_ip << ":" << ntohs(cliaddr.sin_port) << ": " << buffer << std::endl;
    }

    close(sockfd);
    return 0;
}

在上述代码中,recvfrom 函数的第一个参数是 UDP Socket 文件描述符,第二个参数是接收数据的缓冲区,第三个参数是缓冲区大小减 1,第四个参数通常为 0,第五个参数用于存储发送方的地址,第六个参数是地址长度。接收到数据后,我们可以通过 inet_ntop 函数将 IP 地址转换为字符串形式,并输出发送方的地址和数据。

多线程网络编程

在网络编程中,为了提高并发处理能力,常常会使用多线程技术。

线程基础

在 C++ 中,我们可以使用 <thread> 库来创建和管理线程。以下是一个简单的线程示例:

#include <iostream>
#include <thread>

void thread_function() {
    std::cout << "This is a thread" << std::endl;
}

int main() {
    std::thread t(thread_function);
    std::cout << "Main thread" << std::endl;
    t.join();
    return 0;
}

在上述代码中,我们创建了一个线程 t,并将 thread_function 作为线程执行的函数。join 函数用于等待线程结束。

多线程网络编程示例

在网络编程中,我们可以为每个客户端连接创建一个新的线程来处理数据,这样可以同时处理多个客户端的请求。以下是一个简单的多线程服务器示例:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <thread>
#include <string.h>

void handle_client(int connfd) {
    char buffer[1024];
    ssize_t bytes_received = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_received < 0) {
        std::cerr << "Recv failed in thread" << std::endl;
    } else {
        buffer[bytes_received] = '\0';
        std::cout << "Received data in thread: " << buffer << std::endl;

        const char *msg = "Message from server to client in thread";
        ssize_t bytes_sent = send(connfd, msg, strlen(msg), 0);
        if (bytes_sent < 0) {
            std::cerr << "Send failed in thread" << std::endl;
        }
    }
    close(connfd);
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Socket creation failed" << std::endl;
        return -1;
    }

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

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "Bind failed" << std::endl;
        close(sockfd);
        return -1;
    }

    if (listen(sockfd, 5) < 0) {
        std::cerr << "Listen failed" << std::endl;
        close(sockfd);
        return -1;
    }

    while (true) {
        struct sockaddr_in cliaddr;
        socklen_t len = sizeof(cliaddr);
        int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
        if (connfd < 0) {
            std::cerr << "Accept failed" << std::endl;
            continue;
        }

        std::thread t(handle_client, connfd);
        t.detach();
    }

    close(sockfd);
    return 0;
}

在上述代码中,服务器在接受客户端连接后,创建一个新的线程 t 来处理该客户端的通信。handle_client 函数负责接收客户端数据并发送响应数据。detach 函数用于将线程与主线程分离,使主线程不会等待该线程结束。

网络编程中的错误处理

在网络编程中,错误处理是非常重要的。常见的错误包括 Socket 创建失败、绑定失败、连接失败等。

错误码获取

在 Unix - like 系统中,当系统调用失败时,会设置全局变量 errno 来表示错误码。我们可以通过 perror 函数来输出错误信息。例如:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return -1;
    }
    std::cout << "Socket created successfully" << std::endl;

    close(sockfd);
    return 0;
}

在上述代码中,如果 socket 函数调用失败,perror 函数会输出错误信息,格式为:错误提示信息: 具体错误描述。

常见错误处理

在网络编程中,对于绑定失败、连接失败等错误,我们需要根据具体情况进行处理。例如,在绑定失败时,我们可以尝试更换端口号重新绑定:

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

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return -1;
    }

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

    int bind_retries = 3;
    while (bind_retries > 0) {
        if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) == 0) {
            std::cout << "Bind successful" << std::endl;
            break;
        } else {
            perror("Bind failed");
            servaddr.sin_port = htons(servaddr.sin_port + 1);
            bind_retries--;
        }
    }

    if (bind_retries == 0) {
        std::cerr << "Failed to bind after multiple retries" << std::endl;
        close(sockfd);
        return -1;
    }

    close(sockfd);
    return 0;
}

在上述代码中,当绑定失败时,我们尝试将端口号加 1 并重新绑定,最多尝试 3 次。如果仍然失败,则输出错误信息并退出程序。

通过以上对 C++ 网络编程的详细介绍,包括基础概念、Socket 编程、UDP 编程、多线程编程以及错误处理等方面,相信读者对 C++ 网络编程有了较为深入的理解和掌握。在实际应用中,可以根据具体需求选择合适的网络编程方式,并灵活运用各种技术来实现高效、稳定的网络应用程序。