【计算机网络】基于UDP进行socket编程——实现服务端与客户端业务

发布于:2025-05-29 ⋅ 阅读:(12) ⋅ 点赞:(0)

🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹: 【Linux笔记】——网络基础
🔖流水不争,争的是滔滔不息


一、UDPsocket编程

UDP(User Datagram Protocol)是一种无连接的传输层协议,提供快速但不可靠的数据传输。与TCP不同,UDP不保证数据包的顺序、可靠性或重复性,适用于实时性要求高的场景(如视频流、游戏)。

UDPsocket编程的一些基本接口

创建套接字

int socket(int domain, int type, int protocol);

创建UDP套接字,第一个参数是选择ipv4还是ipv6,第二个参数是选择UDP还是TCP,第三个参数为0就可以。
ipv4写AF_INET,ipv6写 AF_INET6 。UDP写SOCK_DGRAM,TCP写SOCK_STREAM 。

套接字绑定本地地址和端口(服务端用)

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

第一个参数是套接字,第二个参数是指向 struct sockaddr 的指针,用于指定本地地址(需要强制类型转换),第三个参数是这个sockaddr_in的结构体。

向指定目标发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

第一个参数套接字,第二个参数是存储发送信息的buffer,第二个是这个buffer的大小,第三个标志位填0就行,第四个发送数据那端的struct sockaddr的指针,第五个是struct sockaddr的大小。

接收数据

 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数套接字,第二个参数接收buffer,第三个是这个buffer的大小,第四个是标准位写0就行,第五个是struct sockaddr的指针,第六个是struct sockaddr的大小的指针

关闭套接字

close()

和关闭文件描述符一样

封装sockaddr_in

#pragma once

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

using namespace std;

class InetAddr
{
public:
    InetAddr(struct sockaddr_in& Addr) //从网络中也就是客户端获取的信息
    :_Addr(Addr)
    {
        _ip=inet_ntoa(_Addr.sin_addr);
        _port=ntohs(_Addr.sin_port);
    }

    string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    string StringAddr() const
    {
       return _ip+":"+to_string(_port);
    }
    
    const struct sockaddr_in& NAddr()
    {
        return _Addr;
    }

    bool operator==(const InetAddr& Addr)
    {
        return Addr._ip==_ip && Addr._port==_port;
    }

    ~InetAddr()
    {

    }

private:
    struct sockaddr_in _Addr;
    string _ip;
    uint16_t _port;
};

每次写服务端或者是客户端的时候都要写struct sockaddr_in那一套太麻烦了,所以进行封装。比如说上面的这个构造,就是从客户端中传来的信息转化为主机地址。

注意啊,ip和端口号,网络到主机需要转换序列,主机到网络也需要转换序列。

二、单线程网络聊天室

一个网络聊天室需要客户端和服务端,客户端用户使用,然后把信息推送给服务端服务器接收到信息把信息路由给所有用户这时网络聊天室就形成了。所以客户端需要有发送的功能,服务器有接收的功能。

Udpserver.hpp服务端


#pragma once


#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"


using namespace std;
using namespace LogModule;
using func_t=function<void(int ,const string&,InetAddr&)>; 
class Udpserver
{
public:
    Udpserver(uint16_t port,func_t func)
    :_port(port)
    ,_sockfd(0)
    ,_isrunning(false)
    ,_func(func)
    {
       
    }

    void Init()
    {
        _sockfd =socket(AF_INET, SOCK_DGRAM,0);
        if(_sockfd <0)
        {
            LOG(LogLevel::FATAL)<<"创建套接字失败";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"创建套接字成功";
        
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));

        local.sin_family=AF_INET;
        local.sin_addr.s_addr=INADDR_ANY;
        local.sin_port=htons(_port);

        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"绑定成功";
    }

    void Start()
    {
        _isrunning=true;
        while(true)
        {
            char buffer[128];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
            
            if(n>0)
            {
                InetAddr client(peer);
                buffer[n]=0;
                _func(_sockfd,buffer,client);
            }
        }
    }
    ~Udpserver()
    {

    }
private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;

};

这里要引入一个结构体,这个结构体是存储本地ip地址和端口信息的结构体变量。
**struct sockaddr_in local; 是在进行 IPv4 网络编程时,定义的一个用于存储本地 IP 地址和端口信息的结构体变量,属于 IPv4 地址族的套接字地址结构体。**这是一个用于存储 IPv4 地址信息 的结构体,定义在头文件 <netinet/in.h> 中。它是给 bind()、connect()、sendto()、recvfrom() 等函数传参用的,通常你写服务器或客户端都要用到它。

void Init()
    {
        _sockfd =socket(AF_INET, SOCK_DGRAM,0);
        if(_sockfd <0)
        {
            LOG(LogLevel::FATAL)<<"创建套接字失败";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"创建套接字成功";
        
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));

        local.sin_family=AF_INET;
        local.sin_addr.s_addr=INADDR_ANY;
        local.sin_port=htons(_port);

        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"绑定失败";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"绑定成功";
    }

初始化,创建套接字,绑定本地的地址和端口号,这是socket编程必须写的。在bind之前创建了套接字地址结构体,用来存储本地服务器的ip和端口号,注意这个ip我们设置成了INADDR_ANY,因为这里是服务器不要设置成固定的ip。写成INADDR_ANY保证ip是动态的。


void Start()
    {
        _isrunning=true;
        while(true)
        {
            char buffer[128];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
            
            if(n>0)
            {
                InetAddr client(peer);
                buffer[n]=0;
                _func(_sockfd,buffer,client);
            }
        }
    }

服务端接收信息,这里的socket_in peer 是客户端传来的套接字地址结构体。用recvfrom接收信息如果能够接收到信息,返回值大于0,执行这个回调函数。回调到udpserver.cc。

#include "Udpserver.hpp"
#include "Route.hpp"
#include <memory>

using namespace std;
using namespace LogModule;

int main(int argc, char *argv[])
{
    uint16_t port = stoi(argv[1]);
    Enable_Console_Log_Strategy(); // 启用控制台输出

    Route r; // 服务器路由

    unique_ptr<Udpserver> usvr = make_unique<Udpserver>(port,
        [&r](int _sockfd, const string &messages, InetAddr &peer)
        { r.MessageRoute(_sockfd, messages, peer); });

    usvr->Init();
    usvr->Start();
    return 0;
}

Udpserver 作为底层网络接收模块,不关心具体如何处理数据,它只负责接收,然后通过你传入的 回调函数,把接收到的数据“上传”给上层(这里是 Route::MessageRoute())去决定如何处理 —— 这就是典型的 解耦设计。回调到这里进行路由,路由的对象已经实例化好了,到这里直接在lambda表达式中直接对路由对象的函数进行调用就可以了。下面是路由的方法。

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Udpserver.hpp"

using namespace std;
using namespace LogModule;
using namespace MutexModule;
class Route
{
public:
    Route()
    {
    }

    bool Exist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (user == peer)
            {
                return true;
            }
        }
        return false;
    }

    void Adduser(InetAddr &peer)
    {
    
        LOG(LogLevel::INFO) << "新增了一个在线用户" <<peer.StringAddr();
        _online_user.push_back(peer);
    }

    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除了一个在线用户" <<peer.StringAddr();
                _online_user.erase(iter);
                break;
            }
        }
    }
    void MessageRoute(int sockfd, const std::string &message, InetAddr &peer) // 路由功能
    {
        LockGuard lockguard(_mutex);  // 加锁
        if (!Exist(peer))
        {
            Adduser(peer);
        }

        string send_messages = peer.StringAddr() + "#" + message; // 发过来的信息

        for (auto &user : _online_user)
        {
            sendto(sockfd, send_messages.c_str(), send_messages.size(), 0, (struct sockaddr *)&(user.NAddr()), sizeof(user.NAddr()));
        }

        // 这个用户一定已经在线了
        if (message == "QUIT")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();
            DeleteUser(peer);
        }
    }

    ~Route()
    {
    }

private:
    vector<InetAddr> _online_user;
    Mutex _mutex;
};

路由服务,就是把服务器收到了信息,分发给所有客户端的用户。加锁线程安全,如果这个用户不存在就在存储用户的数组中新加进去,遍历整个数组把信息都发回去。如果这个用户已经在线了就删除这个用户。

本项目采用回调机制实现业务逻辑与底层网络模块的解耦。Udpserver 仅负责网络收发,而具体的处理逻辑通过 lambda 回调函数注册在 main() 中,实现了业务的灵活注入和高扩展性。


Udpclient.cc

客户端要有发送信息的能力,也有有收到应答的能力,服务器有路由功能客户端要接收路由的信息。

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace std;
using namespace LogModule;
using namespace ThreadModule;

int _sockfd = 0;
string server_ip;
uint16_t server_port = 0;
pthread_t id;

void Recv()
{
    while (true)
    {
        char buffer[128];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int n = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            cout << buffer << endl;
        }
    }
}

void Send()
{

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));

    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    server.sin_port = htons(server_port);
    while (true)
    {
        string input;
        cout << "请输入" << endl;
        getline(cin, input);

        int n = sendto(_sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "客户端发送信息失败";
        }
        if (input == "QUIT")
        {
            pthread_cancel(id);
            break;
        }
    }
}

int main(int argc, char *argv[])
{
    server_ip = argv[1];
    server_port = stoi(argv[2]);
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "创建套接字失败";
    }
    LOG(LogLevel::INFO) << "创建套接字成功";

    // 2. 创建线程
    Thread recver(Recv);
    Thread sender(Send);

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

    recver.Join();
    sender.Join();

    return 0;
}

这里设计的是单线程的,客户端创建的时候,创建两个线程一个收一个发。收方法,就还是老一套recvfrom。发方法还是sendto。

单线程版网络聊天室源码

在这里插入图片描述

三、多线程网络聊天室

Udpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "ThreadPool.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace std;
using namespace LogModule;

using func_t =function<void (int sockfd,const string&,InetAddr&)>;

const int defaultfd = -1;

class Udpserver
{
public:
    Udpserver(uint16_t port,func_t func)
    :_port(port)
    ,_sockfd(defaultfd)
    ,_isrunning(false)
    ,_func(func)
    {

    }

    void Inet() //初始化
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket false";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"socket success";

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));

        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;

        int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local)); //绑定
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"bind false";
            exit(2);
        }
        LOG(LogLevel::INFO)<<"bind success";
    }

    void Start()
    {
        _isrunning=true;
        while (true)
        {
            char buffer[1024]; //存收到的信息
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//收
            if(n>0)
            {
                buffer[n]=0;
                InetAddr cli(peer);
                _func(_sockfd,buffer,cli);

            }
        }
        
    }

    ~Udpserver()
    {

    }
private:
    bool _isrunning;
    int _sockfd;
    uint16_t _port;
    func_t _func;
};

服务器与单线程版本一模一样。

Udpserver.cc

#include "Udpserver.hpp"
#include "Route.hpp"
#include <memory>

using namespace std;
using namespace ThreadPoolModule;

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

int main(int argc,char* argv[])
{
    Enable_Console_Log_Strategy(); // 启用控制台输出
    uint16_t port=stoi(argv[1]);

    Route r;
    auto tp = ThreadPool<task_t>::GetInstance();

    // unique_ptr<Udpserver> usvr = make_unique<Udpserver>(port, 
    //     [&r](int _sockfd, const string &messages, InetAddr &peer)
    //     { r.MessageRoute(_sockfd, messages, peer); });

    unique_ptr<Udpserver> u= make_unique<Udpserver>(port,
        [&r,&tp](int _sockfd, const string &messages, InetAddr &peer){
            task_t t=bind(&Route::MessageRoute,&r,_sockfd,messages,peer);
            tp->Enqueue(t);
    });
    u->Inet();
    u->Start();
    return 0;
}

我们做的是一个多线程 UDP 网络聊天室服务端。为了提升并发能力,我们引入了一个线程池(ThreadPool)。网络部分(接收数据)使用的是 Udpserver,它在接收到消息后通过 回调机制 把业务逻辑“丢”出去。回调函数用的是 lambda 表达式,其中 bind 了 Route::MessageRoute() 和收到的参数,形成一个任务。这个任务再被投递到 线程池 中执行,实现了网络收发和逻辑处理解耦 + 多线程并发处理。

Udpclient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace std;
using namespace LogModule;
using namespace ThreadModule;

int sockfd = 0;
string server_ip;
uint16_t server_port = 0;
pthread_t id;

void Recv()
{
    while(true)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        ssize_t n=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n>0)
        {
            buffer[n]=0;
            cout<<buffer<<endl;
        }
    }
}

void Send()
{
    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());

    const std::string online = "inline";
    sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));

    while(true)
    {
        string input;
        cout<<"请输入"<<endl;
        getline(cin,input);
        ssize_t n=sendto(sockfd,input.c_str(),input.size(),0,(struct sockaddr*)&server,sizeof(server));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "客户端发送信息失败";
        }
        if (input == "QUIT")
        {
            pthread_cancel(id);
            break;
        }
    }
    
}

int main(int argc,char* argv[])
{
    server_ip=argv[1];
    server_port=stoi(argv[2]);

    sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        LOG(LogLevel::FATAL)<<"socket false";
    }
    LOG(LogLevel::INFO)<<"socket success";

    Thread recver(Recv);
    Thread sender(Send);

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

    id = recver.Id();
    
    recver.Join();
    sender.Join();

    return 0;
}

基本也与单线程版本一样。

注意:
这里引入线程池是服务器高并发接收和处理客户端消息,比如多个客户端同时发消息,需要并发处理。客户端还是需要分别创建了两个线程分别是收线程和发线程。即使服务端用了线程池,客户端也要至少两个线程:一个发一个收,这样聊天室才能像样地用起来。客户端的多线程不是为了并发处理请求,是为了让用户能“边说边听”。

在这里插入图片描述
在这里插入图片描述
多线程网络聊天室源码

四、网络字典

Udpserver.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Dict.hpp"
using namespace LogModule;
using namespace std;

const int num = -1;
using func_t = function<string(const string&,InetAddr&)>;//

class Udpserver
{
public:
    Udpserver(uint16_t port,func_t func)
        : _sockfd(num)
        , _port(port)
        , _isrunning(false)
        ,_func(func)
    {
    }

    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "套接字创建失败";
        }
        LOG(LogLevel::INFO) << "套接字创建成功";

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "绑定失败";
        }
        LOG(LogLevel::INFO) << "绑定成功";
    }

    void Start()
    {
        _isrunning=true;
        while(true)
        {
            char buffer[128];
            struct sockaddr_in peer;
            memset(&peer,0,sizeof(peer));
            socklen_t len=sizeof(peer);
            //收客户端传来的信息
            ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
            if(n>0)
            {
                buffer[n]=0;
                InetAddr client(peer);
                string result= _func(buffer,client);

                //这里设计为收完就发
                sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
            }
            
        }
    }

    ~Udpserver()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    func_t _func;
};

都是相同的操作,注意一下收到客户端传来的信息,回调去翻译,然后做出应答翻译完发回去。

Dict.hpp翻译模块

#pragma once
#include <iostream>
#include <string>
#include <map>
#include <fstream>
#include "Udpserver.hpp"
#include "InetAddr.hpp"

const string dpath = "./dictionary.txt";
const string sep = ": ";

using namespace LogModule;
using namespace std;

class Dict
{
public:
    Dict(string path=dpath)
    :_dict_path(path)
    {
    }

    bool LoadDict()
    {  
        ifstream in(_dict_path);
        if(!in.is_open())
        {
            LOG(LogLevel::WARNINC)<<"字典打开失败";
            return false;
        }
        string line;
        while(getline(in,line))
        {
            auto pos=line.find(sep);
            if(pos==string::npos)
            {
                LOG(LogLevel::WARNINC)<<"解析"<<line<<"失败";
                continue;
            }

            string english=line.substr(0,pos);
            string chinese=line.substr(pos + sep.size());
            if(english.empty() || chinese .empty())
            {
                LOG(LogLevel::WARNINC) << "没有有效内容: " << line;
                continue;
            }
            _dict.insert(make_pair(english,chinese));

        }
        in.close();
        return true;
    }

    string Translate(const string& word,InetAddr& client)
    {
        auto iter=_dict.find(word);
        if(iter == _dict.end())
        {
            LOG(LogLevel::DEBUG)<<"进入翻译模块"<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<word<<"->None";
            return "None";
        }
        LOG(LogLevel::DEBUG)<<"进入翻译模块"<<"["<<client.Ip()<<":"<<client.Port()<<"]"<<word<<"->iter->sencond";
        return iter->second;
    }

    ~Dict()
    {
    }

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

用文件流的方式打开英汉文本,判断是否打开成功,从文件流中逐行读取,找到中文和英文中间的:,如果没有找到:解释失败。截取一行中的中文和英文,把中文和英文插入map中。上面说的可以算是初始化字典。下面Translate是真正的翻译模块,在字典中查找要查的单词,判断是否有这个单词,有单词返回map中的sencod不就是对应的翻译了吗。

Udpserver.cc

#include "Udpserver.hpp"
#include "Dict.hpp"
#include <memory>
int main(int argc,char* argv[])
{
    Enable_Console_Log_Strategy(); // 启用控制台输出
    uint16_t port=stoi(argv[1]);
    Dict dict;
    dict.LoadDict();
    unique_ptr<Udpserver> u=make_unique<Udpserver>(port,
        [&dict](const string& word,InetAddr& cli)->string
    {
        return dict.Translate(word,cli);
    });

    u->Init();
    u->Start();
    return 0;
}

创建dict对象,LoadDict初始化字典,这里是udpserver.hpp中回调函数到这里进行翻译,lambda表达式这里来执行翻译的操作。

在这里插入图片描述
udp网络字典源码


通过上面的单线程网络聊天室、多线程网络聊天室、网络字典、我们发现都用到了回调函数,来对不同部分进行模块化,解耦合。网络设计本身就是基于之前我们说过分层模型设计的,网络回调机制与协议分层理念是一脉相承的 —— 都在追求模块化、解耦、职责单一。回调机制本质上是 事件驱动 + 分层解耦 的产物。


网站公告

今日签到

点亮在社区的每一天
去签到