Socket编程--UDP

一、V1版本-Echo Server

网络通信中必须包含的三个头文件:

描述网络套接字的结构体:

填充字段,保证结构体符合指定长度

为什么需要16位地址类型AFinet:第一次传入AFinet是为了告诉操作系统我要创建网络套接字,而第二次传入是为了让未来Socket接口函数能够识别出我是用于网络通信的。afinet:

宏中的##功能:拼接a##1,拼接后即为:a1

EchoServer.hpp

cpp 复制代码
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstring>
#include <cstdint>    // uint16_t 类型依赖,原代码用到uint16_t必须加
#include <cstdlib>    // exit() 函数依赖,Init中exit退出程序
#include <sys/types.h>// socket/recvfrom/sendto 底层类型前置依赖

// Linux网络系统调用头文件
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h> // sockaddr_in、INADDR_ANY 定义核心头文件,缺失会报INADDR_ANY未定义

#include "Mutex.hpp"
#include "Logger.hpp"

const static int default_fd = -1;
const static uint16_t default_port = 8080; 

enum {
    SUCCESS = 0,
    SOCKET_ERR,
    BIND_ERR,      // 补充bind错误码
    USAGE_ERR      // 补充用法错误码
};

class UdpServer {
public:
    UdpServer(uint16_t port = default_port)
        : _port(port), 
          _sockfd(default_fd) 
    {
    }

    ~UdpServer() {
        if (_sockfd != default_fd)
            close(_sockfd);
    }

    void Init() {
        // 第一步:创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

        // 第二步:填充网络信息并bind
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);                    // 主机序 -> 网络序
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); 
        //字符串 IP -> 4字节 + 网络序,由于服务器开发时不建议显示bind IP,所以不使用这种方式来绑定IP,而是采用下面的方式

        local.sin_addr.s_addr = INADDR_ANY;//最佳实践:任意IP地址bind
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0) {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success, ip: " << _ip << ", port: " << _port;
    }

        void Start()
    {
        // 传递的是字符串,echo server
        char inbuffer[1024];
        while(true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 用户发来的数据
            // 2. 用户的socket信息
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct     sockaddr *)&peer, &len);
            if(n > 0)
            {
                // 用户的IP + Port信息,是recvfrom从网络中获取到的数据,网络序列(大端) -> 转换成为主机序列
                // peer.sin_port; // peer client port
                // peer.sin_addr; // peer client i
                // 把网络序端口转为主机序,得到客户端真实端口号
                uint16_t client_port = ntohs(peer.sin_port);

                // 将sockaddr_in里4字节网络序IP数值,转换成人类可读的点分十进制IP字符串
                std::string client_ip = inet_ntoa(peer.sin_addr); 

                // 拼接格式化客户端地址,格式为 [ip:端口]
                std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]";
                // 用户发来的数据
                inbuffer[n] = 0;
                LOG(LogLevel::DEBUG) << "cilent say# " << inbuffer;
                std::string echo_string = "server echo# ";
                echo_string += inbuffer;
                // h to n
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct             sockaddr*)&peer, len);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
        }
    }    

private:
    int _sockfd;
    uint16_t _port;
    //std::string _ip;
    //由于使用的是任意IP,不需要绑定固定IP,可以把std::string _ip去掉了,只需要固定端口号即可
};
#endif

上面的代码中,服务端在进行网络通信时,需要先创建套接字,填充本机 IP 与端口地址信息,再调用 bind 函数完成地址绑定;很多人会产生疑问,客户端通信时是否也需要专属的套接字地址(IP + 端口)?

答案是肯定的!但是客户端在绑定时不能显示地bind IP和端口号。原因是,服务端的端口必须明确且固定,这样各个客户端发起通信时,才能精准找到服务器上对应的服务进程;而客户端端口没有固定要求,只要本机内端口唯一不冲突就行。

如果我们强行给客户端显式绑定固定端口,后续客户端去连接不同服务端的时候,很容易出现端口占用冲突问题,一旦该端口被其他程序占用,客户端就无法正常收发数据了。

那么客户端如何设置自己的IP和端口呢?

客户端默认走随机端口这套逻辑,全部交给操作系统分配,不用我们手动操作。拿 UDP 客户端来说,程序第一次调用sendto发数据的时候,系统会自动完成隐式bind,随机挑一个本机空闲端口,同时绑定本机可用 IP,自动生成属于这个客户端的 IP + 端口。

EchoClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 打印使用提示
static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " server_ip server_port" << std::endl;
}

// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    // 参数校验:必须传入服务端IP、端口两个参数,总参数数量为3
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 从命令行读取目标服务端IP、端口
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建UDP socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    // 2. 填充目标服务端地址信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    while(true)
    {
        std::string message;
        // 1. 获取用户输入
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
    
        // 2. client 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct         sockaddr*)&server, sizeof(server));
        if(n > 0)
        {
            // recvfrom 接收服务端回包
            char inbuffer[1024] = {0};
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&temp, &len);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer << std::endl;
            }
        }
    }
    
        return 0;
    }

EchoServerMain.cc

cpp 复制代码
#include "EchoServer.hpp"
#include <memory>
#include <iostream>   // std::cerr、std::endl 依赖
#include <string>     // std::string、std::stoi 依赖
#include <cstdlib>    // exit() 函数依赖
#include <cstdint>    // uint16_t 类型依赖

// 打印程序使用帮助
static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

// 程序运行格式:./server_udp 端口号
int main(int argc, char *argv[])
{
    // 参数校验:必须传入1个端口参数,总参数数量为2
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 开启控制台日志输出策略(自定义日志库宏)
    ENABLE_CONSOLE_LOG_STRATEGY();

    // 把命令行传入的端口字符串转为数字端口
    uint16_t server_port = std::stoi(argv[1]);

    // 使用智能指针创建UDP服务端对象,传入指定监听端口
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
    // 初始化socket、bind端口
    usvr->Init();
    // 启动循环收发echo数据
    usvr->Start();

    return 0;
}

如果测试时绑定本地环回 IP 127.0.0.1、端口 8080,流量只会在本机内核协议栈内部流转,报文不会出网卡、不会发到外部物理网络,只能本机内部进程互相访问,局域网、公网其他设备都无法连接这个服务。

服务端开发则有一套最佳实践,云服务器千万不能显式 bind 公网 IP,日常写服务代码也不建议手动指定固定IP去 bind。一旦显式绑定指定 IP,系统只会接收目标地址和该绑定 IP 完全匹配的报文,云厂商公网 IP 经过NAT转发后流量目的地址实际是内网IP,外部请求根本进不来。但禁止显式绑定公网 IP 不代表公网 IP 不会参与通信,外部用户访问服务走的依旧是公网 IP,稳妥的写法直接bind 0.0.0.0(INADDR_ANY),监听本机全部网卡地址就能兼容所有访问场景。

所以服务端开发时最佳实践是随机bind任意IP地址,而客户端开发最佳实践是随机bind任意端口号。

二、V2版本-Dict Server

前面的网络通信服务只能完成IO功能,也就是只能收到或发送信息,下面通过封装一层业务软件层来实现一个简单的英译汉英文字典

Dict.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"

static const std::string default_dict = "./dict.txt";
static const std::string sep = ": ";
using namespace NS_LOG_MODULE;

class Dict
{
public:
    Dict(const std::string &dict_path = default_dict) : _dict_path(dict_path)
    {
        LoadDict();
    }
    ~Dict()
    {
    }
    void LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::FATAL) << " open " << _dict_path << " error";
            exit(1);
        }
        std::string line;
        while (std::getline(in, line))
        {
            LOG(LogLevel::FATAL) << "load: " << line << " success";
            // apple: 苹果
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "Format: " << line << " error";
                continue;
            }

            std::string k = line.substr(0, pos); // [)
            std::string v = line.substr(pos + sep.size());
            _dict.insert(std::make_pair(k, v));
        }

        in.close();
        LOG(LogLevel::INFO) << "load done....";
    }
    std::string Translate(std::string word)
    {
        auto iter = _dict.find(word);
        if(iter != _dict.end())
        {
            return iter->second;
        }
        else
        {
            return "None";
        }
    }

private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
};

dict.txt:

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

三、V3版本-Chat Server

在网络通信模块中,UDPServer会接收到客户端发送的所有信息,也会收到所有的客户端。如果要将这些信息和客户端管理起来,就必须先描述,在组织!用链表,vector这类数据结构来对这些收到的信息和客户端进行管理。

在上面的V1版本中,整个服务器都是单进程的,工作方式也是串行进行的。而由于UDP Server会以回调的方式调用上层逻辑,所以在V1版本-Echo Server的基础上,我们可以设计一种转发逻辑,形成一个新的数据转发的任务------route_message(),将客户端收到的消息,转发给所有的在线用户。为了完成信息转发,我们可以将这些转发任务放入线程池中,经过线程池处理之后,由线程池将数据转发给所有人。虽然还是单进程对收到的数据进行收发处理,但是经过任务封装和线程池之后,我们就能并发地读和写,多线程处理收到的任务。这也就是V3版本聊天服务器Chat Server的核心思想。

由于需要使用到线程池,同时需要引入线程池,条件变量的hpp文件:Thread.hpp、Threadpool.hpp、Cond.hpp(在前面的博客中有)

下面是V3版本Chat Server的核心代码:

ChatClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"

using namespace NS_THREAD_MODULE;

int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;

static void Usage(const std::string &proc)
{
    std::cout << "Usage:\n\t";
    std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
    std::cout << "Please Set Your Nick Name# ";
    std::getline(std::cin, nickname);
    std::string online_message = nickname + " online!";
    ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
                       (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
    (void)n;
}

void RecvMessage()
{
    while (true)
    {
        // recvfrom
        char inbuffer[1024] = {0};
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            inbuffer[m] = 0;
            std::cerr << inbuffer << std::endl; // 2
        }
    }
}

void SendMessage()
{
    InetAddr serveraddr(server_port, server_ip);
    Online(serveraddr);

    while (true)
    {
        std::string message;
        // 1. 获取用户输入
        std::cout << "Please Enter# "; // 1
        std::getline(std::cin, message);

        message = nickname + "# " + message;

        // 2. clinet 发送数据给 server,首次发送即自动bind
        ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
                           (struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
        (void)n;
    }
}

// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    Thread recver(RecvMessage);
    Thread sender(SendMessage);

    recver.Start();
    sender.Start();

    recver.Join();
    sender.Join();
    return 0;
}

ChatServerMain.cc

cpp 复制代码
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp"   // 任务
#include "UdpServer.hpp" // 获取事件
#include <memory>

static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

// ./server_udp port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY();
    uint16_t server_port = std::stoi(argv[1]);
    
    // 线程池模块
    auto thread_pool = ThreadPool<task_t>::Instance();

    // 路由模块
    Route r;

    // 网络模块
    UdpServer usvr(server_port);
    usvr.Init();

    usvr.RegisterService(
        [&r](const InetAddr &addr){
            r.CheckUser(addr);
        },
        [&r, thread_pool](int sockfd, std::string msg){
            auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
            thread_pool->Enqueue(t);
            // thread_pool->Enqueue([&r, &sockfd, &msg](){
            //     r.Broadcast(sockfd, msg);
            // });
        }
    );

    usvr.Start();
    return 0;
}

InetAddr.hpp

cpp 复制代码
#pragma once

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


// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
    {
        _ip = inet_ntoa(_address.sin_addr);
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);                  // h->n
        _address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        _len = sizeof(_address);
    }
    bool operator == (const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    InetAddr()
    {}
    struct sockaddr_in *GetNetAddress()
    {
        return &_address;
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {}
private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};

UpdServer.hpp

cpp 复制代码
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP

#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

const static int default_fd = -1;
const static int default_port = 8888;

using handler_addr_t = std::function<void (const InetAddr &)>;
using handler_msg_t = std::function<void (int sokcfd, std::string msg)>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(uint16_t port = default_port)
        : _port(port),
          _sockfd(default_fd)
    {
    }
    ~UdpServer()
    {
        close(_sockfd);
    }
    void Init()
    {
        // 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

        InetAddr local(_port);

        // 第三步:bind socket 信息
        int n = bind(_sockfd, (struct sockaddr *)(local.GetNetAddress()), local.Len());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
    }
    void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
    {
        _handler_addr = handler_addr;
        _handler_msg = handler_msg;
    }
    void Start()
    {
        // 传递的是字符串,echo server
        char inbuffer[1024];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 用户发来的数据
            // 2. 用户的socket信息
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                inbuffer[n] = 0;
                // 1. 检测新用户
                InetAddr clientaddress(peer);
                std::string tips = clientaddress.ToString();
                std::string message = tips + inbuffer;
                LOG(LogLevel::DEBUG) << message;
                _handler_addr(clientaddress);
                // 2. 转发消息

                // nickname# message -> [ip:port]-nickname# message

                _handler_msg(_sockfd, message);
            }
            else
            {
                LOG(LogLevel::ERROR) << "recvfrom error";
            }
            // sleep(3);
        }
    }

private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
    uint16_t _port;  // 用户设置好的,server port必须是固定的!
    handler_addr_t _handler_addr;
    handler_msg_t _handler_msg;
};

#endif

UserManager.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"

using namespace NS_LOG_MODULE;

class UserManager
{
public:
    UserManager(){}
    void AddUser(const InetAddr &addr)
    {
        if(SearchUser(addr))
            return;
        _users.push_back(addr);
    }
    void DelUser(const InetAddr &addr)
    {
        for(auto iter = _users.begin(); iter != _users.end(); iter++)
        {
            if(*iter == addr)
            {
                _users.erase(iter);
                break;
            }
        }
    }
    bool SearchUser(const InetAddr &addr)
    {
        for(auto &user : _users)
        {
            if(user == addr)
            {
                return true;
            }
        }
        return false;
    }
    bool ModUser(const InetAddr &addr)
    {
        DelUser(addr);
        AddUser(addr);
        return true;
    }
    std::vector<InetAddr> &Users()
    {
        return _users;
    }
    ~UserManager(){}
private:
    std::vector<InetAddr> _users;
};

Route.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include "Mutex.hpp"
#include "UserManager.hpp"

class Route
{
public:
    Route():_uma(std::make_unique<UserManager>())
    {}
    void CheckUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->AddUser(addr);
    }
    void OfflineUser(const InetAddr &addr)
    {
        LockGuard lockguard(_lock);
        _uma->DelUser(addr);
    }
    void Broadcast(int sockfd, std::string message)
    {
        LockGuard lockguard(_lock);
        auto &users = _uma->Users();
        for(auto &user : users)
        {
            sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
        }
    }
    
    ~Route()
    {}
private:
    std::unique_ptr<UserManager> _uma;
    Mutex _lock;
};
相关推荐
KKKlucifer4 小时前
拨开接口黑盒迷雾:运营商第三方合作接口安全审计与准入管控落地实践
网络·人工智能·安全
青瓦梦滋5 小时前
传输层UDP/TCP协议
linux·网络·c++·网络协议·tcp/ip·udp
江畔柳前堤5 小时前
LLM 训练核心机制深度解析:Warmup、Cosine Decay 与 Perplexity 的完整知识体系
网络·人工智能·深度学习·算法·机器学习·语音识别
小绫网络安全5 小时前
【每日一讲】学习网络安全的一些注意事项
网络·安全·web安全
星恒讯工业路由器6 小时前
5G LAN + Wi-Fi 6融合组网:工业路由器如何为柔性产线“剪掉辫子”
网络·5g·工业路由器·工业物联网·5g la·柔性产线·5g工业路由
SamChan908 小时前
PDF翻译API的JWT签名鉴权与速率限制设计:安全实战指南
网络·安全·pdf
xiaoxiangsiyan8 小时前
HCIA 网络技术基础核心模块详解
运维·网络·网络协议·tcp/ip·运维开发·网络基础
AGCSDNl9 小时前
葛姓的来源
网络
漫路在线11 小时前
无境红日靶场五 wp
网络·安全·web安全
Sylvia33.11 小时前
从轮询到推送:足球数据API架构演进与火星数据技术拆解
java·服务器·网络·python·websocket·架构