【计算机网络】epoll

				#pragma once
				#include "NoCopy.hpp"
				#include "log.hpp"
				
				#include <sys/epoll.h>
				 
				#include <cstring>
				#include <cerrno> 
				
				class Epoller : public NoCopy
				{
				    static const int size = 128;
				public:
				    Epoller()
				    {
				        _epfd = epoll_create(size);
				        if(_epfd == -1){
				            lg(Fatal, "epoll_create error: %s", strerror(errno));
				        }
				        else{
				            lg(Info, "epoll_create success: %d", _epfd);
				        }
				    }
				
				    int EpollerWait(struct epoll_event revents[], int num)
				    {
				        int n = epoll_wait(_epfd, revents, num, _timeout);
				        return n;
				    }
				
				    int EpollerCtl(int oper, int sockfd, uint32_t event)
				    {   
				        int n = 0;
				        if(oper == EPOLL_CTL_DEL)
				        {
				            n = epoll_ctl(_epfd, oper, sockfd, nullptr);
				            if(n != 0){
				                lg(Error, "epoll_ctl delete error!");
				            }
				        }
				        else
				        {
				            // EPOLL_CTL_MOD 或者 EPOLL_CTL_ADD
				            // 设置进内核的红黑树中
				            struct epoll_event ev;
				            ev.events = event;
				            ev.data.fd = sockfd;    // 方便我们后面处理的时候知道是哪一个 fd 就绪了
				
				            n = epoll_ctl(_epfd, oper, sockfd, &ev);
				            if(n != 0){
				                lg(Error, "epoll_ctl error!");
				            }
				        }
				        return n;
				    }
				
				    ~Epoller()
				    {
				        if(_epfd >= 0){
				            close(_epfd);
				        }
				    }
				private:
				    int _epfd;
				    int _timeout = 1000;
				};

接下来编写 epollSever.hpp,如下:

				#pragma once
				
				#include <iostream>
				#include <memory>
				#include <sys/epoll.h>
				 
				#include "Socket.hpp"
				#include "log.hpp"
				#include "Epoller.hpp"
				#include "NoCopy.hpp"
				
				uint32_t EVENT_IN = (EPOLLIN);
				uint32_t EVENT_OUT = (EPOLLOUT);
				
				class EpollServer : public NoCopy
				{
				    static const int maxevents = 64;
				public:
				    EpollServer(uint16_t port)
				        :_port(port)
				        ,_listenSocket_ptr(new Sock())
				        ,_epoller_ptr(new Epoller())
				    {}
				
				    void Init()
				    {
				        _listenSocket_ptr->Socket();
				        _listenSocket_ptr->Bind(_port);
				        _listenSocket_ptr->Listen();
				
				        lg(Info, "create listen socket success: %d\n", _listenSocket_ptr->GetFd());
				    }
				
				    void Accepter()
				    {
				        std::string client_ip;
				        uint16_t client_port;
				        int sockfd = _listenSocket_ptr->Accept(&client_ip, &client_port);
				        if(sockfd > 0){
				            // 不能直接读取,而是将它添加到内核的红黑树中,让 epoll 关心即可
				            _epoller_ptr->EpollerCtl(EPOLL_CTL_ADD, sockfd, EVENT_IN);
				            lg(Info, "get a new link, client info@ %s:%d", client_ip.c_str(), client_port);
				        }
				        else{
				            return;
				        }
				    }
				
				    // for test
				    void Recver(int fd)
				    {
				        char buffer[1024];
				        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
				        if (n > 0)
				        {
				            buffer[n] = 0;
				            std::cout << "get a message: " << buffer << std::endl;
				
				            // write
				            std::string echo_str = "sever echo $ ";
				            echo_str += buffer;
				            write(fd, echo_str.c_str(), echo_str.size());
				        }
				        else if (n == 0)
				        {
				            lg(Info, "client quit, me too, close fd is: %d", fd);
				            // 先在内核红黑树中移除 fd,再关闭 fd
				            _epoller_ptr->EpollerCtl(EPOLL_CTL_DEL, fd, 0);
				            close(fd);
				        }
				        else
				        {
				            lg(Warning, "recv error, fd is: %d", fd);
				            _epoller_ptr->EpollerCtl(EPOLL_CTL_DEL, fd, 0);
				            close(fd);
				        }
				    }
				
				    void Dispatcher(struct epoll_event revs[], int num)
				    {
				        for(int i = 0; i < num; ++i){
				            uint32_t event = revs[i].events;
				            int fd = revs[i].data.fd;
				
				            if(event & EVENT_IN){
				                // 读事件就绪
				                if(fd == _listenSocket_ptr->GetFd()){
				                    // 获取到一个新连接,连接管理器
				                    Accepter();
				                }
				                else{
				                    // 其它 fd 上的普通读取事件就绪
				                    Recver(fd);
				                }
				            }
				            else if(event & EVENT_OUT){
				                // 写事件就绪
				                // ...
				            }
				            else{
				                // ...
				            }
				        }
				    }
				
				    void Start()
				    {
				        // 将 listenSocket 添加到 epoll 中
				        // 也就是将 listenSocket 和它所关心的事件添加到内核 epoll 模型中的红黑树中!
				        _epoller_ptr->EpollerCtl(EPOLL_CTL_ADD, _listenSocket_ptr->GetFd(), EVENT_IN);
				        
				        struct epoll_event revs[maxevents];
				        while(true)
				        {
				            // 其中 n 最大是 maxevents
				            int n = _epoller_ptr->EpollerWait(revs, maxevents);
				            if(n > 0)
				            {
				                // 有事件就绪,分派事件
				                lg(Debug, "event happend, fd is: %d", revs[0].data.fd);
				                Dispatcher(revs, n); 
				            }
				            else if(n == 0)
				            {
				                lg(Info, "time out...");
				            }
				            else 
				            {
				                lg(Error, "epoll wait error");
				            }
				        }
				    }
				
				    ~EpollServer()
				    {
				        _listenSocket_ptr->Close(); 
				    }
				
				private:
				    std::shared_ptr<Sock> _listenSocket_ptr;
				    std::shared_ptr<Epoller> _epoller_ptr;
				    uint16_t _port;
				};

我们上面两个模块都用到了 NoCopy 这个类,也就是禁止拷贝,代码如下:

				#pragma once
				
				class NoCopy
				{
				public:
				    NoCopy(){}
				    NoCopy(const NoCopy&) = delete;
				    const NoCopy& operator=(const NoCopy&) = delete;
				};

5. epoll 的工作模式

(1)水平触发 Level Triggered 工作模式(LT 模式)

epoll 默认所处的工作模式就是 LT 模式。例如我们上面所写的简单的 epoll 服务器,每次有新的连接到来时,如果我们不处理它,epoll 会每次都通知我们有连接到来了。这种一旦有新的连接到来,或者有新的数据到来,上层如果不取走,底层就会一直通知上层,让上层把数据尽快取走,这种模式就叫做 LT 模式。就像示波器中的高电平,一直有效。

(2)边缘触发 Edge Triggered 工作模式(ET 模式)

ET 模式指的是,数据或者连接,从无到有,从有到多,变化的时候,才会通知我们一次。正是因为 ET 模式有这种特点,才会倒逼程序员每次通知都必须把本轮数据全部取走,怎么保证数据全部取走呢?所以就需要循环读取,直到读取出错!但是我们使用 read() 或者 recv() 在缓冲区中读取数据的时候,当缓冲区的数据没有了,因为它们的读取方式默认是阻塞的,所以此时就会阻塞,服务器就会被挂起!所以我们在 ET 模式下,所有的 fd 必须是要设置为非阻塞的!

(3)LT 和 ET 的对比
  • selectpoll 其实也是工作在 LT 模式下;epoll 既可以支持 LT,也可以支持 ET
  • 普遍地我们认为,ET 的工作模式比 LT 的工作模式通知效率更高,因为通知一次就可以倒逼上层把全部数据读取走。同时也看得出来 ET 模式的 IO 效率也更高,这也就意味着,TCP 会向对方通告一个更大的窗口,从而从概率上让对方一次给自己发送更多的数据!
  • 所谓的 LT 模式和 ET 模式,本质就是向就绪队列中放入多个或者一个就绪的事件
  • 但是 ET 模式就一定比 LT 模式的效率高吗?不一定!因为 LT 也可以将所有的 fd 设置为非阻塞,然后循环读取,也就是当通知一次的时候,就把数据全部取走了,就和 ET 一样了!所以谁的效率高不一定,要看具体的实现。

二、Reactor

1. 概念

我们在上面编写的 epoll 服务器的代码中,在其他普通的 fd 读取事件就绪时,也就是在 Recver() 中,读取是有问题的,因为我们不能区分每次读取上来的数据是一个完整的报文。另外还有其它各种问题,所以我们要对上面的代码使用 Reactor 的设计模式作修改。

所谓的 Reactor 是一种设计模式,翻译过来称为反应堆模式。用于处理事件驱动的系统中的并发操作。它提供了一种结构化的方式来处理输入事件,并将其分发给相应的处理程序。Reactor 模式通常用于网络编程中,特别是在服务器端应用程序中。

要进行正确的 IO 处理,就应该有如下的理解:在应用层一定存在大量的连接,每一个连接在应用层都叫做文件描述符。而在读取每一个文件描述符上的数据的时候,可能根本就没有读取完,此时我们就需要把该文件描述符上的数据临时保存起来。所以我们在写服务器的时候,我们要保证每一个文件描述符及其连接及其缓冲区,都是独立的!

2. 实现

(1)Epoller.hpp

Epoller.hpp 是对 epoll 的系统调用的封装,代码如下:

			#pragma once
			#include "NoCopy.hpp"
			#include "log.hpp"
			
			#include <sys/epoll.h>
			   
			#include <cstring>
			#include <cerrno> 
			
			class Epoller : public NoCopy
			{
			    static const int size = 128;
			public:
			    Epoller()
			    {
			        _epfd = epoll_create(size);
			        if(_epfd == -1){
			            lg(Fatal, "epoll_create error: %s", strerror(errno));
			        }
			        else{
			            lg(Info, "epoll_create success: %d", _epfd);
			        }
			    }
			
			    int EpollerWait(struct epoll_event revents[], int num, int timeout)
			    {
			        int n = epoll_wait(_epfd, revents, num, timeout);
			        return n;
			    }
			
			    int EpollerCtl(int oper, int sockfd, uint32_t event)
			    {   
			        int n = 0;
			        if(oper == EPOLL_CTL_DEL)
			        {
			            n = epoll_ctl(_epfd, oper, sockfd, nullptr);
			            if(n != 0){
			                lg(Error, "epoll_ctl delete error! sockfd: %d", sockfd);
			            }
			        }
			        else
			        {
			            // EPOLL_CTL_MOD 或者 EPOLL_CTL_ADD
			            // 设置进内核的红黑树中
			            struct epoll_event ev;
			            ev.events = event;
			            ev.data.fd = sockfd;    // 方便我们后面处理的时候知道是哪一个 fd 就绪了
			
			            n = epoll_ctl(_epfd, oper, sockfd, &ev);
			            if(n != 0){
			                lg(Error, "epoll_ctl error!");
			            }
			        }
			        return n;
			    }
			
			    ~Epoller()
			    {
			        if(_epfd >= 0){
			            close(_epfd);
			        }
			    }
			private:
			    int _epfd;
			    int _timeout = 1000;
			};

(2)TcpServer.hpp

TcpServer.hpp 是处理 IO 的服务器,代码如下:

			#pragma once
			#include <iostream>
			#include <string>
			#include <unordered_map>
			#include <memory>
			#include <functional>
			
			#include <cerrno>  
			
			#include "log.hpp"
			#include "NoCopy.hpp"
			#include "Epoller.hpp"
			#include "Socket.hpp"
			#include "Comm.hpp"
			
			// 设置 ET 模式
			uint32_t EVENT_IN = (EPOLLIN | EPOLLET);
			uint32_t EVENT_OUT = (EPOLLOUT | EPOLLET);
			const static int g_buffer_size = 128;
			
			class Connection;
			class TcpServer;
			
			using func_t = std::function<void(std::weak_ptr<Connection>)>;
			using except_t = std::function<void(std::weak_ptr<Connection>)>;
			
			// 管理每一个连接
			class Connection 
			{
			public:
			    Connection(int sockfd)
			        :_sockfd(sockfd)
			    {}
			
			    void SetHandler(func_t recv_cb, func_t send_cb, except_t except_cb)
			    {
			        _recv_cb = recv_cb;
			        _send_cb = send_cb;
			        _except_cb = except_cb;
			    }
			
			    ~Connection()
			    {}
			private:
			    int _sockfd;
			
			    // 充当缓冲区
			    std::string _inbuffer; 
			    std::string _outbuffer;
			public:
			    // 回指指针
			    // std::shared_ptr<TcpServer> _tcpServer_ptr;
			    std::weak_ptr<TcpServer> _tcpServer_ptr;
			
			    // 回调方法
			    func_t _recv_cb;
			    func_t _send_cb;
			    except_t _except_cb;
			
			    std::string _ip;
			    uint16_t _port;
			
			    int Sockfd()
			    {
			        return _sockfd;
			    }
			
			    void AppendInBuffer(const std::string& info)
			    {
			        _inbuffer += info;
			    }
			
			    void AppendOutBuffer(const std::string& info)
			    {
			        _outbuffer += info;
			    }
			
			    std::string& Inbuffer()
			    {
			        return _inbuffer;
			    }
			
			    std::string& Outbuffer()
			    {
			        return _outbuffer;
			    }
			
			    void SetWeakPtr(std::weak_ptr<TcpServer> tcpServer_ptr)
			    {
			        _tcpServer_ptr = tcpServer_ptr;
			    }
			};
			
			
			// enable_shared_from_this 可以提供返回当前对象的 this 对应的 shared_ptr
			class TcpServer : public std::enable_shared_from_this<TcpServer>, public NoCopy
			{
			    static const int num = 64;
			public:
			    TcpServer(uint16_t port, func_t OnMessage)
			        :_port(port)
			        ,_quit(true)
			        ,_OnMessage(OnMessage)
			        ,_epoller_ptr(new Epoller())
			        ,_listenSock_ptr(new Sock())
			    {}
			
			    void Init()
			    {
			        _listenSock_ptr->Socket();
			        // 将 fd 设置为非阻塞
			        SetNonBlockOrDie(_listenSock_ptr->GetFd());
			        _listenSock_ptr->Bind(_port);
			        _listenSock_ptr->Listen();
			        lg(Info, "create listen socket success: %d\n", _listenSock_ptr->GetFd());
			
			        AddConnection(_listenSock_ptr->GetFd(), EVENT_IN, \
			            std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
			    }
			
			    void AddConnection(int sockfd, uint32_t event, func_t recv_cb, func_t send_cb, except_t except_cb,\
			        const std::string& ip = "0.0.0.0", uint16_t port = 0)
			    {
			        // 1. 给所有的套接字建立一个 connection 对象
			        std::shared_ptr<Connection> new_connection(new Connection(sockfd));
			
			        // shared_from_this(): 返回当前对象的 shared_ptr
			        new_connection->SetWeakPtr(shared_from_this()); 
			        
			        new_connection->SetHandler(recv_cb, send_cb, except_cb);
			        new_connection->_ip = ip;
			        new_connection->_port = port;
			
			        // 2. 将套接字和 Connection 添加到 unordered_map 中
			        _connections.insert(std::make_pair(sockfd, new_connection));
			
			        // 3. 将 listen 套接字或其它事件添加到 epoll 模型中
			        _epoller_ptr->EpollerCtl(EPOLL_CTL_ADD, sockfd, event);
			
			        lg(Debug, "add a new connection success, sockfd is: %d", sockfd);
			    }
			
			    // listen 套接字的连接管理器,即有事件就绪的时候,就是有连接到来,就需要处理新连接
			    void Accepter(std::weak_ptr<Connection> conn)
			    {
			        auto connection = conn.lock();
			        // 不断检测是否还有新连接,直到读取出错
			        while(true){
			            struct sockaddr_in peer;
			            socklen_t len = sizeof(peer);
			            int sockfd = ::accept(connection->Sockfd(), (sockaddr*)&peer, &len);
			
			            // 获取到新连接设置为非阻塞,然后构建 Connection 对象放入哈希表中和内核红黑树中
			            if(sockfd > 0){
			                uint16_t peer_port = ntohs(peer.sin_port);
			                char ipbuffer[128];
			                inet_ntop(AF_INET, &peer.sin_addr, ipbuffer, sizeof(ipbuffer));
			                lg(Debug, "get a new client, get info-> [%s: %d], sockfd: %d", ipbuffer, peer_port, sockfd);
			
			                SetNonBlockOrDie(sockfd);
			                AddConnection(sockfd, EVENT_IN, \
			                    std::bind(&TcpServer::Recver, this, std::placeholders::_1),\
			                    std::bind(&TcpServer::Sender, this, std::placeholders::_1),\
			                    std::bind(&TcpServer::Excepter, this, std::placeholders::_1),\
			                    ipbuffer, peer_port);
			            }
			            else{
			                if(errno == EWOULDBLOCK){   // 读取完毕
			                    break;
			                }
			                else if(errno == EINTR){ // 信号原因中断
			                    continue;
			                }
			                else{
			                    break;
			                }
			            }
			        }
			    }
			
			    // 普通事件的事件管理器
			    // 对于服务器而言只需要进行IO,不需要关心是否读完和报文的格式
			    void Recver(std::weak_ptr<Connection> conn)
			    {
			        if(conn.expired()) return;
			        auto connection = conn.lock();
			
			        int sockfd = connection->Sockfd();
			        while(true){
			            char buffer[g_buffer_size];
			            memset(buffer, 0, sizeof(buffer));
			            ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);   // 非阻塞读取
			            if(n > 0){
			                connection->AppendInBuffer(buffer);
			            }
			            else if(n == 0){
			                lg(Info, "sockfd: %d, client info %s: %d quit...", sockfd, connection->_ip.c_str(), connection->_port);
			                connection->_except_cb(connection);
			            }
			            else{
			                if(errno == EWOULDBLOCK){
			                    break;
			                }
			                else if(errno == EINTR){
			                    continue;
			                }
			                else{
			                    lg(Warning, "sockfd: %d, client info %s: %d recv error...", sockfd, connection->_ip.c_str(), connection->_port);
			                    connection->_except_cb(connection);
			                    return;
			                }
			            }
			        }
			        // 交给上层处理,读取到的数据都在 connection 中
			        // 1. 检测
			        // 2. 如果有完整报文,就处理
			        _OnMessage(connection);
			    }
			
			    void Sender(std::weak_ptr<Connection> conn)
			    {
			        if(conn.expired()) return;
			        auto connection = conn.lock();
			        
			        auto& outbuffer = connection->Outbuffer();
			        while(true){
			            ssize_t n = send(connection->Sockfd(), outbuffer.c_str(), outbuffer.size(), 0);
			            if(n > 0){
			                outbuffer.erase(0, n);
			                if(outbuffer.empty()){
			                    break;
			                }
			            }
			            else if(n == 0){
			                return;
			            }
			            else{
			                if(errno == EWOULDBLOCK){
			                    break;
			                }
			                else if(errno == EINTR){
			                    continue;
			                }
			                else{
			                    lg(Warning, "sockfd: %d, client info %s: %d send error...", connection->Sockfd(), connection->_ip.c_str(), connection->_port);
			                    connection->_except_cb(connection);
			                    return;
			                }
			            }
			        }
			        // 没发完
			        if(!outbuffer.empty()){
			            // 开始对写事件关心
			            EnableEvent(connection->Sockfd(), true, true);
			        }
			        else{
			            // 关闭对写事件关心
			            EnableEvent(connection->Sockfd(), true, false);
			        }
			    }
			
			    void Excepter(std::weak_ptr<Connection> connection)
			    {
			        if(connection.expired()) return;
			        auto conn = connection.lock();
			
			        lg(Debug, "Excepter hander sockfd: %d, client info %s: %d excepter handler", \
			            conn->Sockfd(), conn->_ip.c_str(), conn->_port);
			
			        // 1. 移除对特定 fd 的关心
			        // EnableEvent(connection->Sockfd(), false, false);
			        _epoller_ptr->EpollerCtl(EPOLL_CTL_DEL, conn->Sockfd(), 0);
			        // 2. 关闭异常的 fd
			        lg(Debug, "close %d done...\n", conn->Sockfd());
			        close(conn->Sockfd());
			        // 3. 从 _connections 中移除 fd 和 Connection 的映射关系
			        lg(Debug, "remove %d from _connections...\n", conn->Sockfd());
			        _connections.erase(conn->Sockfd());
			    }
			
			    void EnableEvent(int sockfd, bool readAble, bool writeAble)
			    {
			        uint32_t events = 0;
			        events |= ((readAble ? EPOLLIN : 0) | (writeAble ? EPOLLOUT : 0) | EPOLLET);
			        _epoller_ptr->EpollerCtl(EPOLL_CTL_MOD, sockfd, events);
			    }
			
			    bool IsConnectionExist(int fd)
			    {
			        auto iter = _connections.find(fd);
			        if(iter == _connections.end()){
			            return false;
			        }
			        else{
			            return true;
			        }
			    }
			
			    void Dispatcher(int timeout)
			    {
			        int n = _epoller_ptr->EpollerWait(revs, num, timeout);
			        for(int i = 0; i < n; i++){
			            uint32_t event = revs[i].events;
			            int sockfd = revs[i].data.fd;
			
			            // 一旦事件异常,统一把异常转换为读写问题
			            if(event & EPOLLERR){
			                event |= (EPOLLIN | EPOLLOUT);
			            }
			            if(event & EPOLLHUP){
			                event |= (EPOLLIN | EPOLLOUT);
			            }
			            
			            // 只需要处理读写
			            if((event & EPOLLIN) && IsConnectionExist(sockfd)){
			                if(_connections[sockfd]->_recv_cb){
			                    _connections[sockfd]->_recv_cb(_connections[sockfd]);
			                }
			            }
			            if((event & EPOLLOUT) && IsConnectionExist(sockfd)){
			                if(_connections[sockfd]->_send_cb){
			                    _connections[sockfd]->_send_cb(_connections[sockfd]);
			                }
			            }
			        }
			    }
			
			    void Loop()
			    {
			        _quit = false;
			        
			        // AddConnection();
			        while(!_quit)
			        {
			            // 事件派发
			            // Dispatcher(3000);
			            Dispatcher(-1);
			            PrintConnection();
			        }
			        _quit = true;
			    }
			
			    void PrintConnection()
			    {
			        std::cout << "_connections fd list: ";
			        for(auto& connection : _connections){
			            std::cout << connection.second->Sockfd() << ", ";
			            std::cout << "inbuffer: " << connection.second->Inbuffer() << " ";
			        }
			        std::cout << std::endl;
			    }
			
			    ~TcpServer()
			    {}
			private:
			    std::shared_ptr<Epoller> _epoller_ptr;
			    std::shared_ptr<Sock> _listenSock_ptr;   
			    uint16_t _port;
			    bool _quit;
			
			    struct epoll_event revs[num];
			
			    // fd 到对应连接到映射,_connections 就是当前服务器管理的所有连接
			    std::unordered_map<int, std::shared_ptr<Connection>> _connections;
			
			    // 让上层处理信息
			    func_t _OnMessage;
			};

(3)Calculator.hpp

Calculator.hpp 是上层处理业务的具体处理方法,代码如下:

			#pragma once
			#include <string>
			#include <iostream>
			#include "Protocol.hpp"
			
			enum
			{
			    DIV_ERR = 1,
			    MOD_ERR = 2,
			    OP_ERR = 3
			};
			
			
			// 上层业务
			class Calculator
			{
			public:
			    Calculator()
			    {}
			
			    Response CalculatorHelper(const Request &req)
			    {
			        Response resp(0, 0);
			        switch (req._op)
			        {
			        case '+':
			            resp._result = req._x + req._y;



还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!


王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。


对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!


【完整版领取方式在文末!!】


***93道网络安全面试题***


![](https://img-blog.csdnimg.cn/img_convert/6679c89ccd849f9504c48bb02882ef8d.png)








![](https://img-blog.csdnimg.cn/img_convert/07ce1a919614bde78921fb2f8ddf0c2f.png)





![](https://img-blog.csdnimg.cn/img_convert/44238619c3ba2d672b5b8dc4a529b01d.png)





内容实在太多,不一一截图了


### 黑客学习资源推荐


最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!


对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


😝朋友们如果有需要的话,可以联系领取~

#### 1️⃣零基础入门


##### ① 学习路线


对于从来没有接触过网络安全的同学,我们帮你准备了详细的**学习成长路线图**。可以说是**最科学最系统的学习路线**,大家跟着这个大的方向学习准没问题。


![image](https://img-blog.csdnimg.cn/img_convert/acb3c4714e29498573a58a3c79c775da.gif#pic_center)


##### ② 路线对应学习视频


同时每个成长路线对应的板块都有配套的视频提供:


![image-20231025112050764](https://img-blog.csdnimg.cn/874ad4fd3dbe4f6bb3bff17885655014.png#pic_center)


#### 2️⃣视频配套工具&国内外网安书籍、文档


##### ① 工具


![](https://img-blog.csdnimg.cn/img_convert/d3f08d9a26927e48b1332a38401b3369.png#pic_center)


##### ② 视频


![image1](https://img-blog.csdnimg.cn/img_convert/f18acc028dc224b7ace77f2e260ba222.png#pic_center)


##### ③ 书籍


![image2](https://img-blog.csdnimg.cn/img_convert/769b7e13b39771b3a6e4397753dab12e.png#pic_center)

资源较为敏感,未展示全面,需要的最下面获取

![在这里插入图片描述](https://img-blog.csdnimg.cn/e4f9ac066e8c485f8407a99619f9c5b5.png#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/111f5462e7df433b981dc2430bb9ad39.png#pic_center)


##### ② 简历模板


![在这里插入图片描述](https://img-blog.csdnimg.cn/504b8be96bfa4dfb8befc2af49aabfa2.png#pic_center)

 **因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆**

  



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值