C++ 网络协议栈实现:从 TCP/IP 到应用层协议

好的,各位观众老爷们,今天咱们来聊聊怎么用 C++ 撸一个网络协议栈,从 TCP/IP 一路通关到应用层协议!别怕,这玩意儿听起来玄乎,其实拆开了揉碎了,也就那么回事儿。咱们争取用最接地气的方式,把这事儿给整明白。

第一章:打地基——TCP/IP 协议栈概览

想盖房子,先得打地基。网络协议栈也一样,得先了解一下 TCP/IP 这座大厦的结构。简单来说,TCP/IP 协议栈就像一个分工明确的团队,每层楼负责不同的任务。

  • 链路层 (Link Layer): 负责物理介质上的数据传输,比如以太网、Wi-Fi。它把数据帧扔到电缆里,或者无线电波里,让它在网络上跑起来。你可以把它想象成快递小哥,负责把包裹送到下一站。
  • 网络层 (Network Layer): IP 协议就是这层的扛把子。它负责数据包的路由,也就是决定数据包该往哪个方向走,才能最终到达目的地。这就像导航系统,告诉你该怎么走。
  • 传输层 (Transport Layer): TCP 和 UDP 在这层唱主角。TCP 提供可靠的、面向连接的传输,UDP 提供不可靠的、无连接的传输。TCP 就像一个靠谱的物流公司,保证包裹安全送达;UDP 就像一个随便的快递,只管扔过去,能不能收到就看天意了。
  • 应用层 (Application Layer): HTTP、SMTP、FTP 等协议都在这层。它们负责实现具体的应用功能,比如网页浏览、邮件发送、文件传输。这就像不同的应用商店,提供各种各样的 App。

用表格总结一下:

层级 协议 功能 比喻
链路层 Ethernet, Wi-Fi 物理介质上的数据传输,帧封装与解封装 快递小哥
网络层 IP 数据包路由,寻址 导航系统
传输层 TCP, UDP TCP: 可靠的、面向连接的传输;UDP: 不可靠的、无连接的传输 TCP: 靠谱物流;UDP: 随便快递
应用层 HTTP, SMTP, FTP 实现具体的应用功能,如网页浏览、邮件发送、文件传输 应用商店

第二章:撸起袖子写代码——底层 Socket 编程

好了,理论知识过了一遍,现在开始动真格的。咱们先从最底层的 Socket 编程入手,Socket 可以理解为应用程序和 TCP/IP 协议栈之间的接口。

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

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

    // 2. 绑定地址和端口
    sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有地址
    address.sin_port = htons(8080); // 监听 8080 端口

    if (bind(server_fd, (sockaddr*)&address, sizeof(address)) < 0) {
        std::cerr << "Bind failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    // 3. 监听连接
    if (listen(server_fd, 3) < 0) { // 允许最多 3 个连接排队
        std::cerr << "Listen failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    std::cout << "Server listening on port 8080..." << std::endl;

    // 4. 接受连接
    sockaddr_in client_address;
    socklen_t client_address_len = sizeof(client_address);
    int new_socket = accept(server_fd, (sockaddr*)&client_address, &client_address_len);
    if (new_socket < 0) {
        std::cerr << "Accept failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    // 5. 接收数据
    char buffer[1024] = {0};
    ssize_t bytes_received = recv(new_socket, buffer, sizeof(buffer), 0);
    if (bytes_received < 0) {
        std::cerr << "Receive failed!" << std::endl;
        close(new_socket);
        close(server_fd);
        return -1;
    }

    std::cout << "Received: " << buffer << std::endl;

    // 6. 发送数据
    const char* message = "Hello from server!";
    send(new_socket, message, strlen(message), 0);
    std::cout << "Message sent!" << std::endl;

    // 7. 关闭 Socket
    close(new_socket);
    close(server_fd);

    return 0;
}

这段代码实现了一个简单的 TCP 服务器。

  1. 创建 Socket: socket() 函数创建一个 Socket 文件描述符。AF_INET 表示使用 IPv4 协议,SOCK_STREAM 表示使用 TCP 协议。
  2. 绑定地址和端口: bind() 函数将 Socket 绑定到一个特定的 IP 地址和端口。INADDR_ANY 表示监听所有可用的 IP 地址。htons() 函数将端口号从主机字节序转换成网络字节序。
  3. 监听连接: listen() 函数开始监听指定端口上的连接请求。
  4. 接受连接: accept() 函数接受客户端的连接请求,并创建一个新的 Socket 文件描述符用于与客户端通信。
  5. 接收数据: recv() 函数从 Socket 接收数据。
  6. 发送数据: send() 函数通过 Socket 发送数据。
  7. 关闭 Socket: close() 函数关闭 Socket 文件描述符。

编译运行这段代码,一个简单的 TCP 服务器就跑起来了。你可以用 telnet 或者其他工具连接到这个服务器,发送一些数据,看看效果。

第三章:TCP 协议的精髓——三次握手和四次挥手

TCP 协议之所以可靠,很大程度上归功于它的连接管理机制。其中,最著名的就是三次握手和四次挥手。

  • 三次握手 (Three-way Handshake):

    1. 客户端发送一个 SYN (synchronize) 包给服务器,请求建立连接。
    2. 服务器收到 SYN 包后,发送一个 SYN-ACK (synchronize-acknowledge) 包给客户端,表示同意建立连接。
    3. 客户端收到 SYN-ACK 包后,发送一个 ACK (acknowledge) 包给服务器,确认连接建立。

    通过这三次握手,客户端和服务器就建立了一个可靠的 TCP 连接。

  • 四次挥手 (Four-way Handshake):

    1. 客户端发送一个 FIN (finish) 包给服务器,请求关闭连接。
    2. 服务器收到 FIN 包后,发送一个 ACK 包给客户端,表示收到关闭请求。
    3. 服务器处理完数据后,发送一个 FIN 包给客户端,表示同意关闭连接。
    4. 客户端收到 FIN 包后,发送一个 ACK 包给服务器,确认关闭连接。

    通过这四次挥手,客户端和服务器就安全地关闭了 TCP 连接。

为啥挥手要四次呢? 因为 TCP 是全双工的,客户端发送 FIN 仅仅表示客户端不再发送数据了,但是还可以接收数据。服务器收到 FIN 后,可能还有数据要发送,所以不能立即关闭连接,需要先发送 ACK,等所有数据发送完毕后,再发送 FIN。

第四章:应用层协议的构建——HTTP 协议为例

有了 TCP/IP 协议栈的基础,咱们就可以开始构建应用层协议了。这里以 HTTP 协议为例,HTTP 协议是 Web 应用的基础,用于客户端和服务器之间传输超文本。

一个简单的 HTTP 请求:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: My Browser

一个简单的 HTTP 响应:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024

<html>
  <body>
    <h1>Hello, World!</h1>
  </body>
</html>

HTTP 协议基于文本,易于理解和解析。咱们可以用 C++ 来解析 HTTP 请求和生成 HTTP 响应。

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <map>

// HTTP 请求类
class HttpRequest {
public:
    std::string method;
    std::string path;
    std::string version;
    std::map<std::string, std::string> headers;
    std::string body;

    HttpRequest(const std::string& request_string) {
        parse(request_string);
    }

private:
    void parse(const std::string& request_string) {
        std::stringstream ss(request_string);
        std::string line;

        // 解析请求行
        std::getline(ss, line);
        std::stringstream line_ss(line);
        line_ss >> method >> path >> version;

        // 解析头部
        while (std::getline(ss, line) && !line.empty() && line != "r") {
            size_t pos = line.find(":");
            if (pos != std::string::npos) {
                std::string key = line.substr(0, pos);
                std::string value = line.substr(pos + 1);
                // trim whitespace
                key.erase(0, key.find_first_not_of(" t"));
                key.erase(key.find_last_not_of(" t") + 1);
                value.erase(0, value.find_first_not_of(" t"));
                value.erase(value.find_last_not_of(" t") + 1);
                headers[key] = value;
            }
        }

        // 解析 body (简单起见,假设body在最后)
        std::stringstream body_ss;
        while(std::getline(ss, line)){
            body_ss << line << std::endl;
        }
        body = body_ss.str();
    }
};

// HTTP 响应类
class HttpResponse {
public:
    int status_code;
    std::string status_text;
    std::map<std::string, std::string> headers;
    std::string body;

    std::string to_string() const {
        std::stringstream ss;

        // 状态行
        ss << "HTTP/1.1 " << status_code << " " << status_text << "rn";

        // 头部
        for (const auto& header : headers) {
            ss << header.first << ": " << header.second << "rn";
        }

        ss << "rn";

        // Body
        ss << body;

        return ss.str();
    }
};

int main() {
    std::string request_string = "GET /index.html HTTP/1.1rn"
                                  "Host: www.example.comrn"
                                  "User-Agent: My Browserrn"
                                  "rn"
                                  "This is the body.";

    HttpRequest request(request_string);

    std::cout << "Method: " << request.method << std::endl;
    std::cout << "Path: " << request.path << std::endl;
    std::cout << "Version: " << request.version << std::endl;
    std::cout << "Host: " << request.headers["Host"] << std::endl;
    std::cout << "User-Agent: " << request.headers["User-Agent"] << std::endl;
    std::cout << "Body: " << request.body << std::endl;

    HttpResponse response;
    response.status_code = 200;
    response.status_text = "OK";
    response.headers["Content-Type"] = "text/html";
    response.body = "<html><body><h1>Hello, World!</h1></body></html>";

    std::cout << "nResponse:n" << response.to_string() << std::endl;

    return 0;
}

这段代码实现了简单的 HTTP 请求解析和响应生成。HttpRequest 类用于解析 HTTP 请求字符串,HttpResponse 类用于生成 HTTP 响应字符串。

第五章:进阶之路——多线程和异步 IO

上面的例子只是一个简单的单线程服务器,处理能力有限。在实际应用中,我们需要使用多线程或者异步 IO 来提高服务器的并发处理能力。

  • 多线程: 每个连接创建一个新的线程来处理,可以并发处理多个连接。但是线程创建和销毁会带来额外的开销,而且线程之间的同步也比较复杂。
  • 异步 IO: 使用非阻塞 IO 和事件循环机制,可以在一个线程中处理多个连接。异步 IO 可以提高服务器的并发处理能力,而且避免了线程切换的开销。

使用 epoll (Linux) 或 kqueue (BSD) 可以实现高效的异步 IO。这里给出一个使用 epoll 的简单例子:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <vector>

#define MAX_EVENTS 10

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

    // 2. 设置 Socket 为非阻塞
    int flags = fcntl(server_fd, F_GETFL, 0);
    if (flags == -1) {
        std::cerr << "fcntl(F_GETFL) failed!" << std::endl;
        close(server_fd);
        return -1;
    }
    if (fcntl(server_fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        std::cerr << "fcntl(F_SETFL) failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    // 3. 绑定地址和端口
    sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有地址
    address.sin_port = htons(8080); // 监听 8080 端口

    if (bind(server_fd, (sockaddr*)&address, sizeof(address)) < 0) {
        std::cerr << "Bind failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    // 4. 监听连接
    if (listen(server_fd, 3) < 0) { // 允许最多 3 个连接排队
        std::cerr << "Listen failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    std::cout << "Server listening on port 8080..." << std::endl;

    // 5. 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "epoll_create1 failed!" << std::endl;
        close(server_fd);
        return -1;
    }

    // 6. 将 server_fd 添加到 epoll 监听
    epoll_event event;
    event.data.fd = server_fd;
    event.events = EPOLLIN | EPOLLET; // 监听读事件,使用边缘触发
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
        std::cerr << "epoll_ctl failed!" << std::endl;
        close(epoll_fd);
        close(server_fd);
        return -1;
    }

    epoll_event events[MAX_EVENTS];

    while (true) {
        // 7. 等待事件发生
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // -1 表示无限等待
        if (num_events == -1) {
            std::cerr << "epoll_wait failed!" << std::endl;
            close(epoll_fd);
            close(server_fd);
            return -1;
        }

        for (int i = 0; i < num_events; ++i) {
            if (events[i].data.fd == server_fd) {
                // 8. 接受新连接
                sockaddr_in client_address;
                socklen_t client_address_len = sizeof(client_address);
                int new_socket = accept(server_fd, (sockaddr*)&client_address, &client_address_len);
                if (new_socket == -1) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // 没有新连接
                        continue;
                    } else {
                        std::cerr << "Accept failed!" << std::endl;
                        close(epoll_fd);
                        close(server_fd);
                        return -1;
                    }
                }

                // 设置新 Socket 为非阻塞
                flags = fcntl(new_socket, F_GETFL, 0);
                if (flags == -1) {
                    std::cerr << "fcntl(F_GETFL) failed!" << std::endl;
                    close(new_socket);
                    continue;
                }
                if (fcntl(new_socket, F_SETFL, flags | O_NONBLOCK) == -1) {
                    std::cerr << "fcntl(F_SETFL) failed!" << std::endl;
                    close(new_socket);
                    continue;
                }

                // 将新 Socket 添加到 epoll 监听
                event.data.fd = new_socket;
                event.events = EPOLLIN | EPOLLET;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) == -1) {
                    std::cerr << "epoll_ctl failed!" << std::endl;
                    close(new_socket);
                    continue;
                }

                std::cout << "New connection accepted." << std::endl;
            } else {
                // 9. 处理已连接 Socket 的数据
                int socket_fd = events[i].data.fd;
                char buffer[1024] = {0};
                ssize_t bytes_received = recv(socket_fd, buffer, sizeof(buffer), 0);
                if (bytes_received > 0) {
                    std::cout << "Received from socket " << socket_fd << ": " << buffer << std::endl;

                    // 回显数据
                    send(socket_fd, buffer, bytes_received, 0);
                } else if (bytes_received == 0) {
                    // 连接关闭
                    std::cout << "Socket " << socket_fd << " closed." << std::endl;
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, NULL);
                    close(socket_fd);
                } else {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // 没有数据可读
                    } else {
                        std::cerr << "Receive failed!" << std::endl;
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, NULL);
                        close(socket_fd);
                    }
                }
            }
        }
    }

    close(epoll_fd);
    close(server_fd);

    return 0;
}

这段代码使用 epoll 实现了一个简单的异步 IO 服务器。

  1. 创建 Socket: 和之前一样,创建 Socket 文件描述符。
  2. 设置 Socket 为非阻塞: 使用 fcntl() 函数将 Socket 设置为非阻塞模式。
  3. 绑定地址和端口: 和之前一样,绑定地址和端口。
  4. 监听连接: 和之前一样,监听连接。
  5. 创建 epoll 实例: epoll_create1() 函数创建一个 epoll 实例。
  6. 将 server_fd 添加到 epoll 监听: epoll_ctl() 函数将 server_fd 添加到 epoll 监听,监听读事件(EPOLLIN)。EPOLLET 表示使用边缘触发模式。
  7. 等待事件发生: epoll_wait() 函数等待事件发生。
  8. 处理新连接: 如果 epoll_wait() 返回的事件是 server_fd 上的事件,表示有新的连接请求。使用 accept() 函数接受连接,并将新的 Socket 文件描述符添加到 epoll 监听。
  9. 处理已连接 Socket 的数据: 如果 epoll_wait() 返回的事件是已连接 Socket 上的事件,表示有数据可读。使用 recv() 函数接收数据,并进行处理。

第六章:总结与展望

好了,各位观众老爷们,今天咱们就聊到这里。从 TCP/IP 协议栈的概览,到 Socket 编程,再到应用层协议的构建,以及多线程和异步 IO 的进阶,希望大家对 C++ 网络协议栈的实现有了一个更清晰的认识。

当然,这只是一个入门级别的教程。实际的网络协议栈实现要复杂得多,涉及到更多的细节和优化。但是,只要掌握了基本原理,就可以逐步深入,构建出高性能、高可靠的网络应用。

希望大家能够多多实践,不断学习,早日成为网络编程高手! 感谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注