iOS 流量监控分析,从零基础到精通,收藏这篇就够了!

作者 | 小鱼周凌宇,目前在饿了么物流iOS 组,主要工作内容蜂鸟骑手 app 和一些组内框架相关,业余喜欢日番,画画,
微博 | https://weibo.com/coderfish

由于骑手不能随时处在有 WIFI 的状态,流量变成了很敏感的问题,为了精确到每个 API 的流量,进行针对性的优化,开始在我们的 APM 中添加流量监控功能。
本文将记录自己做流量监控方面的总结。其中包括了非常多的踩坑经验,和现有一些方案的缺陷分析,对我来说是一个非常有意义的过程。

干货预警🤣请做好读大量代码的准备😂😂😂

一、资料收集

就目前来说,各家大厂基本都有自己的 APM(包括我们公司其实之前也有一套 APM,但是由于各个事业部的需求不同,尚不能完全满足物流平台的需要)但各家大厂目前开源的 APM 项目却不多,当然也可能是由于各家的业务场景差异比较大且对数据的后续处理不同。
所以本次在查阅资料阶段,没有太多的源码可选参考,但有不少文章。
以下是一些本次开发过程中参考的文章和开源库:

iOS-Monitor-Platform[1]
GodEye[2]
NetworkEye[3]
移动端性能监控方案Hertz[4]
使用NSURLProtocol注意的一些问题[5]
iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求[6]
获取NSURLResponse的HTTPVersion[7]

但以上这些资料对我们的需求都有不足之处:

1. Request 和 Response 记在同一条记录

在实际的网络请求中 Request 和 Response 不一定是成对的,如果网络断开、或者突然关闭进程,都会导致不成对现象,如果将 Request 和 Response 记录在同一条数据,将会对统计造成偏差

2. 上行流量记录不精准

主要的原因有三大类:

直接忽略了 Header 和 Line 部分
忽略了 Cookie 部分,实际上,臃肿的 Cookie 也是消耗流量的一部分
• body 部分的字节大小计算直接使用了 HTTPBody.length 不够准确

3. 下行流量记录不精准

主要原因有:

直接忽略了 Header 和 Status-Line 部分
• body 部分的字节大小计算直接使用了 expectedContentLength 不够准确
忽略了 gzip 压缩,在实际网络编程中,往往都使用了 gzip 来进行数据压缩,而系统提供的一些监听方法,返回的 NSData 实际是解压过的,如果直接统计字节数会造成大量偏差

后文将详细讲述。

二、需求

先简单罗列我们的需求:

• Request 基本信息记录
• 上行流量
• Reponse 基本信息记录
• 下行流量
• 数据归类:按照 host 和 path 归类,一条记录记载改 host/path 的 Request 记录数,Response 记录数,Reqeust 总流量(上行流量),Reponse 总流量(下行流量)

我们的侧重点是流量统计,为了方便分析 APP 使用中哪些 API 消耗流量多。所以对上行、下行流量都需要尽量准确记录。

最终的数据库表展示:

type 字段表示的是『该条记录是 Request 还是 Response』,几个 length 分别记录了流量的各个细节,包括:总字节数、Line 字节数、Header 字节数、Body 字节数。

最后的界面展示类似于:

三、分析现有资料

现在分析一下上面收集到的资料有哪些不足之处。

GodEye[2] | NetworkEye[3]:

NetworkEye 是 GodEye 的一部分,可以单独拆出来使用的网络监控库。
查阅两者的源码后发现,NetworkEye

  1. 仅仅记录了 Reponse 的流量

  2. 通过 expectedContentLength 记录是不准确的(后面将会说到)

  3. 仅仅记录了总和,这对我们来说是无意义的,不能分析出哪条 API 流量使用多

移动端性能监控方案Hertz[4]:

美团的文章中展示几个代码片段:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection  
{  
    [self.client URLProtocolDidFinishLoading:self];  
  
    self.data = nil;  
    if (connection.originalRequest) {  
        WMNetworkUsageDataInfo *info = [[WMNetworkUsageDataInfo alloc] init];  
        self.connectionEndTime = [[NSDate date] timeIntervalSince1970];  
        info.responseSize = self.responseDataLength;  
        info.requestSize = connection.originalRequest.HTTPBody.length;  
        info.contentType = [WMNetworkUsageURLProtocol getContentTypeByURL:connection.originalRequest.URL andMIMEType:self.MIMEType];  
    [[WMNetworkMeter sharedInstance] setLastDataInfo:info];  
    [[WMNetworkUsageManager sharedManager] recordNetworkUsageDataInfo:info];  
}  

在 connectionDidFinishLoading 中记录了整个网络请求结束的时间、 response 数据大小、request 数据大小以及一些其他数据。

总体来说是比较详细的,但是这里并没有给出 self.responseDataLength 的具体逻辑,另外 connection.originalRequest.HTTPBody.length 仅仅是 Request body 的大小。

iOS-Monitor-Platform[1]:

这篇文章比较详细的介绍了整个 APM 制作的过程,贴出了很多代码段,应该说非常详细也极具参考价值。

在流量部分,也分别针对了上行流量、下行流量进行了区分,但其中:

  1. 没有处理 gzip 压缩情况

  2. 对 Header 计算大小的方式是 Dictionary 转 NSData,然而实际上头部并不是 Json 格式(这块我觉得很迷,因为作者特意展示了 HTTP 报文组成)

四、动手自己做

HTTP 报文

为了更好的让大家了解 HTTP 流量计算的一些关键信息,首先要了解 HTTP 报文的组成。

再来随便抓个包具体看看:

iOS 下的网络监控

这块我采用的大家耳熟能详的 NSURLProtocol,NSURLProtocol 方式除了通过 CFNetwork 发出的网络请求,全部都可以拦截到。

Apple 文档[8]中对 NSURLProtocol 有非常详细的描述和使用介绍

An abstract class that handles the loading of protocol-specific URL data.

如果想更详细的了解 NSURLProtocol,也可以看大佐的_这篇文章_[9]

在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的 NSURLProtocol 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。

NSURLProtocol 是一个抽象类,需要做的第一步就是集成它,完成我们的自定义设置。

创建自己的 DMURLProtocol,为它添加几个属性并实现相关接口:

@interface DMURLProtocol() <NSURLConnectionDelegate, NSURLConnectionDataDelegate>  
  
@property (nonatomic, strong) NSURLConnection *connection;  
@property (nonatomic, strong) NSURLRequest *dm_request;  
@property (nonatomic, strong) NSURLResponse *dm_response;  
@property (nonatomic, strong) NSMutableData *dm_data;  
  
@end  

canInitWithRequest & canonicalRequestForRequest:

static NSString *const DMHTTP = @"LPDHTTP";  

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {  
    if (![request.URL.scheme isEqualToString:@"http"]) {  
        return NO;  
    }  
    // 拦截过的不再拦截  
    if ([NSURLProtocol propertyForKey:LPDHTTP inRequest:request] ) {  
        return NO;  
    }  
    return YES;  
}  

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {  
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];  
    [NSURLProtocol setProperty:@YES  
                        forKey:DMHTTP  
                     inRequest:mutableReqeust];  
    return [mutableReqeust copy];  
}  

startLoading:

- (void)startLoading {  
    NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request];  
    self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];  
    self.dm_request = self.request;  
}  

didReceiveResponse:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {  
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];  
    self.dm_response = response;  
}  

didReceiveData:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {  
    [self.client URLProtocol:self didLoadData:data];  
    [self.dm_data appendData:data];  
}  

以上部分是为了在单次 HTTP 请求中记录各个所需要属性。

记录 Response 信息

前面的代码实现了在网络请求过程中为 dm_response 和 dm_data 赋值,那么在 stopLoading 方法中,就可以分析 dm_response 和 dm_data 对象,获取下行流量等相关信息。

需要说明的是,如果需要获得非常精准的流量,一般来说只有通过 Socket 层获取是最准确的,因为可以获取包括握手、挥手的数据大小。当然,我们的目的是为了分析 App 的耗流量 API,所以仅从应用层去分析也基本满足了我们的需要。

上文中说到了报文的组成,那么按照报文所需要的内容获取。

Status Line

非常遗憾的是 NSURLResponse 没有接口能直接获取报文中的 Status Line,甚至连 HTTP Version 等组成 Status Line 内容的接口也没有。

最后,我通过转换到 CFNetwork 相关类,才拿到了 Status Line 的数据,这其中可能涉及到了读取私有 API

这里我为 NSURLResponse 添加了一个扩展:NSURLResponse+DoggerMonitor,并为其添加 statusLineFromCF 方法

typedef CFHTTPMessageRef (*DMURLResponseGetHTTPResponse)(CFURLRef response);  
  
- (NSString *)statusLineFromCF {  
    NSURLResponse *response = self;  
    NSString *statusLine = @"";  
    // 获取CFURLResponseGetHTTPResponse的函数实现  
    NSString *funName = @"CFURLResponseGetHTTPResponse";  
    DMURLResponseGetHTTPResponse originURLResponseGetHTTPResponse =  
    dlsym(RTLD_DEFAULT, [funName UTF8String]);  
  
    SEL theSelector = NSSelectorFromString(@"_CFURLResponse");  
    if ([response respondsToSelector:theSelector] &&  
        NULL != originURLResponseGetHTTPResponse) {  
        // 获取NSURLResponse的_CFURLResponse  
        CFTypeRef cfResponse = CFBridgingRetain([response performSelector:theSelector]);  
        if (NULL != cfResponse) {  
            // 将CFURLResponseRef转化为CFHTTPMessageRef  
            CFHTTPMessageRef messageRef = originURLResponseGetHTTPResponse(cfResponse);  
            statusLine = (__bridge_transfer NSString *)CFHTTPMessageCopyResponseStatusLine(messageRef);  
            CFRelease(cfResponse);  
        }  
    }  
    return statusLine;  
}  

通过调用私有 API _CFURLResponse 获得 CFTypeRef 再转换成 CFHTTPMessageRef,获取 Status Line。

再将其转换成 NSData 计算字节大小:

- (NSUInteger)dm_getLineLength {  
    NSString *lineStr = @"";  
    if ([self isKindOfClass:[NSHTTPURLResponse class]]) {  
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self;  
        lineStr = [self statusLineFromCF];  
    }  
    NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding];  
    return lineData.length;  
}  

Header

通过 httpResponse.allHeaderFields 拿到 Header 字典,再拼接成报文的 key: value 格式,转换成 NSData 计算大小:

- (NSUInteger)dm_getHeadersLength {  
    NSUInteger headersLength = 0;  
    if ([self isKindOfClass:[NSHTTPURLResponse class]]) {  
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self;  
        NSDictionary<NSString *, NSString *> *headerFields = httpResponse.allHeaderFields;  
        NSString *headerStr = @"";  
        for (NSString *key in headerFields.allKeys) {  
            headerStr = [headerStr stringByAppendingString:key];  
            headerStr = [headerStr stringByAppendingString:@": "];  
            if ([headerFields objectForKey:key]) {  
                headerStr = [headerStr stringByAppendingString:headerFields[key]];  
            }  
            headerStr = [headerStr stringByAppendingString:@"\n"];  
        }  
        NSData *headerData = [headerStr dataUsingEncoding:NSUTF8StringEncoding];  
        headersLength = headerData.length;  
    }  
    return headersLength;  
}  

Body

对于 Body 的计算,上文看到有些文章里采用的 expectedContentLength 或者去 NSURLResponse 对象的 allHeaderFields 中获取 Content-Length 值,其实都不够准确。

首先 API 文档中对 expectedContentLength 也有介绍是不准确的:

其次,HTTP 1.1 标准里也有介绍 Content-Length 字段不一定是每个 Response 都带有的,最重要的是,Content-Length 只是表示 Body 部分的大小。

我的方式是,在前面代码中有写到,在 didReceiveData 中对 dm_data 进行了赋值

didReceiveData:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {  
    [self.client URLProtocol:self didLoadData:data];  
    [self.dm_data appendData:data];  
}  

那么在 stopLoading 方法中,就可以拿到本次网络请求接收到的数据。
但需要注意对 gzip 情况进行区别分析。我们知道 HTTP 请求中,客户端在发送请求的时候会带上 Accept-Encoding,这个字段的值将会告知服务器客户端能够理解的内容压缩算法。而服务器进行相应时,会在 Response 中添加 Content-Encoding 告知客户端选中的压缩算法。

所以,我们在 stopLoading 中获取 Content-Encoding,如果使用了 gzip,则模拟一次 gzip 压缩,再计算字节大小:

- (void)stopLoading {  
    [self.connection cancel];  
  
    DMNetworkTrafficLog *model = [[DMNetworkTrafficLog alloc] init];  
    model.path = self.request.URL.path;  
    model.host = self.request.URL.host;  
    model.type = DMNetworkTrafficDataTypeResponse;  
    model.lineLength = [self.dm_response dm_getLineLength];  
    model.headerLength = [self.dm_response dm_getHeadersLength];  
    if ([self.dm_response isKindOfClass:[NSHTTPURLResponse class]]) {  
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)self.dm_response;  
        NSData *data = self.dm_data;  
        if ([[httpResponse.allHeaderFields objectForKey:@"Content-Encoding"] isEqualToString:@"gzip"]) {  
            // 模拟压缩  
            data = [self.dm_data gzippedData];  
        }  
        model.bodyLength = data.length;  
    }  
    model.length = model.lineLength + model.headerLength + model.bodyLength;  
    [model settingOccurTime];  
    [[DMDataManager defaultDB] addNetworkTrafficLog:model];  
}  

这里 gzippedData 参考_这个库的内容_[9]

[[DMDataManager defaultDB] addNetworkTrafficLog:model]; 是调用持久化层的代码将数据落库。

记录 Resquest 信息

Line

很遗憾,对于NSURLRequest 我没有像 NSURLReponse 一样幸运的找到私有接口将其转换成 CFNetwork 相关数据,但是我们很清楚 HTTP 请求报文 Line 部分的组成,所以我们可以添加一个方法,获取一个经验 Line。

同样为 NSURLReques 添加一个扩展:NSURLRequest+DoggerMonitor

- (NSUInteger)dgm_getLineLength {  
    NSString *lineStr = [NSString stringWithFormat:@"%@ %@ %@\n", self.HTTPMethod, self.URL.path, @"HTTP/1.1"];  
    NSData *lineData = [lineStr dataUsingEncoding:NSUTF8StringEncoding];  
    return lineData.length;  
}  

Header

Header 这里有一个非常大的坑。

request.allHTTPHeaderFields 拿到的头部数据是有很多缺失的,这块跟业内朋友交流的时候,发现很多人都没有留意到这个问题。

缺失的部分不仅仅是上面一篇文章中说到的 Cookie。

如果通过 Charles 抓包,可以看到,会缺失包括但不仅限于以下字段:

  1. Accept

  2. Connection

  3. Host

这个问题非常的迷,同时由于无法转换到 CFNetwork 层,所以一直拿不到准确的 Header 数据。

最后,我在 so 上也找到了两个相关问题,供大家参考

NSUrlRequest: where an app can find the default headers for HTTP request?[10]

NSMutableURLRequest, cant access all request headers sent out from within my iPhone program[11]

两个问题的回答基本表明了,如果你是通过 CFNetwork 来发起请求的,才可以拿到完整的 Header 数据。

所以这块只能拿到大部分的 Header,但是基本上缺失的都固定是那几个字段,对我们流量统计的精确度影响不是很大。

那么主要就针对 cookie 部分进行补全:

- (NSUInteger)dgm_getHeadersLengthWithCookie {  
    NSUInteger headersLength = 0;  
  
    NSDictionary<NSString *, NSString *> *headerFields = self.allHTTPHeaderFields;  
    NSDictionary<NSString *, NSString *> *cookiesHeader = [self dgm_getCookies];  
  
    // 添加 cookie 信息  
    if (cookiesHeader.count) {  
        NSMutableDictionary *headerFieldsWithCookies = [NSMutableDictionary dictionaryWithDictionary:headerFields];  
        [headerFieldsWithCookies addEntriesFromDictionary:cookiesHeader];  
        headerFields = [headerFieldsWithCookies copy];  
    }  
    NSLog(@"%@", headerFields);  
    NSString *headerStr = @"";  
  
    for (NSString *key in headerFields.allKeys) {  
        headerStr = [headerStr stringByAppendingString:key];  
        headerStr = [headerStr stringByAppendingString:@": "];  
        if ([headerFields objectForKey:key]) {  
            headerStr = [headerStr stringByAppendingString:headerFields[key]];  
        }  
        headerStr = [headerStr stringByAppendingString:@"\n"];  
    }  
    NSData *headerData = [headerStr dataUsingEncoding:NSUTF8StringEncoding];  
    headersLength = headerData.length;  
    return headersLength;  
}  

- (NSDictionary<NSString *, NSString *> *)dgm_getCookies {  
    NSDictionary<NSString *, NSString *> *cookiesHeader;  
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];  
    NSArray<NSHTTPCookie *> *cookies = [cookieStorage cookiesForURL:self.URL];  
    if (cookies.count) {  
        cookiesHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];  
    }  
    return cookiesHeader;  
}  

body

最后是 body 部分,这里也有个坑。通过 NSURLConnection 发出的网络请求 resquest.HTTPBody 拿到的是 nil。

需要转而通过 HTTPBodyStream 读取 stream 来获取 request 的 Body 大小。

- (NSUInteger)dgm_getBodyLength {  
    NSDictionary<NSString *, NSString *> *headerFields = self.allHTTPHeaderFields;  
    NSUInteger bodyLength = [self.HTTPBody length];  
  
    if ([headerFields objectForKey:@"Content-Encoding"]) {  
        NSData *bodyData;  
        if (self.HTTPBody == nil) {  
            uint8_t d[1024] = {0};  
            NSInputStream *stream = self.HTTPBodyStream;  
            NSMutableData *data = [[NSMutableData alloc] init];  
            [stream open];  
            while ([stream hasBytesAvailable]) {  
                NSInteger len = [stream read:d maxLength:1024];  
                if (len > 0 && stream.streamError == nil) {  
                    [data appendBytes:(void *)d length:len];  
                }  
            }  
            bodyData = [data copy];  
            [stream close];  
        } else {  
            bodyData = self.HTTPBody;  
        }  
        bodyLength = [[bodyData gzippedData] length];  
    }  
  
    return bodyLength;  
}  

落库

最后在 DMURLProtocol 的 - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response; 方法中对 resquest 调用报文各个部分大小方法后落库:

-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {  
    if (response != nil) {  
        self.dm_response = response;  
        [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];  
    }  
  
    DMNetworkTrafficLog *model = [[DMNetworkTrafficLog alloc] init];  
    model.path = request.URL.path;  
    model.host = request.URL.host;  
    model.type = DMNetworkTrafficDataTypeRequest;  
    model.lineLength = [connection.currentRequest dgm_getLineLength];  
    model.headerLength = [connection.currentRequest dgm_getHeadersLengthWithCookie];  
    model.bodyLength = [connection.currentRequest dgm_getBodyLength];  
    model.length = model.lineLength + model.headerLength + model.bodyLength;  
    [model settingOccurTime];  
    [[DMDataManager defaultDB] addNetworkTrafficLog:model];  
    return request;  
}  

针对 NSURLSession 的处理

直接使用 DMURLProtocol 并 registerClass 并不能完整的拦截所有网络请求,因为通过 NSURLSession 的 sharedSession 发出的请求是无法被 NSURLProtocol 代理的。

我们需要让 [NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 的属性中也设置我们的 DMURLProtocol,这里通过 swizzle,置换 protocalClasses 的 get 方法:

编写一个 DMURLSessionConfiguration

#import <Foundation/Foundation.h>  
  
@interface DMURLSessionConfiguration : NSObject  
  
@property (nonatomic,assign) BOOL isSwizzle;  
+ (DMURLSessionConfiguration *)defaultConfiguration;  
- (void)load;  
- (void)unload;  
  
@end  

#import "DMURLSessionConfiguration.h"  
#import <objc/runtime.h>  
#import "DMURLProtocol.h"  
#import "DMNetworkTrafficManager.h"  
  
@implementation DMURLSessionConfiguration  
  
+ (DMURLSessionConfiguration *)defaultConfiguration {  
    static DMURLSessionConfiguration *staticConfiguration;  
    static dispatch_once_t onceToken;  
    dispatch_once(&onceToken, ^{  
        staticConfiguration=[[DMURLSessionConfiguration alloc] init];  
    });  
    return staticConfiguration;  
      
}  
  
- (instancetype)init {  
    self = [super init];  
    if (self) {  
        self.isSwizzle = NO;  
    }  
    return self;  
}  
  
- (void)load {  
    self.isSwizzle = YES;  
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");  
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];  
      
}  
  
- (void)unload {  
    self.isSwizzle=NO;  
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");  
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];  
}  
  
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {  
    Method originalMethod = class_getInstanceMethod(original, selector);  
    Method stubMethod = class_getInstanceMethod(stub, selector);  
    if (!originalMethod || !stubMethod) {  
        [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];  
    }  
    method_exchangeImplementations(originalMethod, stubMethod);  
}  
  
- (NSArray *)protocolClasses {  
    // DMNetworkTrafficManager 中的 protocolClasses 可以给使用者设置自定义的 protocolClasses  
    return [DMNetworkTrafficManager manager].protocolClasses;  
}  
  
@end  

这样,我们写好了方法置换,在执行过该类单例的 load 方法后,[NSURLSessionConfiguration defaultSessionConfiguration].protocolClasses 拿到的将会是我们设置好的 protocolClasses。

如此,我们再为 DMURLProtocol 添加 start 和 stop 方法,用于启动网络监控和停止网络监控:

+ (void)start {  
    DMURLSessionConfiguration *sessionConfiguration = [DMURLSessionConfiguration defaultConfiguration];  
    for (id protocolClass in [DMNetworkTrafficManager manager].protocolClasses) {  
        [NSURLProtocol registerClass:protocolClass];  
    }  
    if (![sessionConfiguration isSwizzle]) {  
        // 设置交换  
        [sessionConfiguration load];  
    }  
}  
  
+ (void)end {  
    DMURLSessionConfiguration *sessionConfiguration = [DMURLSessionConfiguration defaultConfiguration];  
    [NSURLProtocol unregisterClass:[DMURLProtocol class]];  
    if ([sessionConfiguration isSwizzle]) {  
        // 取消交换  
        [sessionConfiguration unload];  
    }  
}  

到此,基本完成了整个网络流量监控。

再提供一个 Manger 方便使用者调用:

#import <Foundation/Foundation.h>  
  
@class DMNetworkLog;  
@interface DMNetworkTrafficManager : NSObject  
  
/** 所有 NSURLProtocol 对外设置接口,可以防止其他外来监控 NSURLProtocol */  
@property (nonatomic, strong) NSArray *protocolClasses;  
  
  
/** 单例 */  
+ (DMNetworkTrafficManager *)manager;  
  
/** 通过 protocolClasses 启动流量监控模块 */  
+ (void)startWithProtocolClasses:(NSArray *)protocolClasses;  
/** 仅以 DMURLProtocol 启动流量监控模块 */  
+ (void)start;  
/** 停止流量监控 */  
+ (void)end;  
  
@end  

#import "DMNetworkTrafficManager.h"  
#import "DMURLProtocol.h"  
  
@interface DMNetworkTrafficManager ()  
  
@end  
  
@implementation DMNetworkTrafficManager  
  
#pragma mark - Public  
  
+ (DMNetworkTrafficManager *)manager {  
    static DMNetworkTrafficManager *manager;  
    static dispatch_once_t onceToken;  
    dispatch_once(&onceToken, ^{  
        manager=[[DMNetworkTrafficManager alloc] init];  
    });  
    return manager;  
}  
  
+ (void)startWithProtocolClasses:(NSArray *)protocolClasses {  
    [self manager].protocolClasses = protocolClasses;  
    [DMURLProtocol start];  
}  
  
+ (void)start {  
    [self manager].protocolClasses = @[[DMURLProtocol class]];  
    [DMURLProtocol start];  
}  
  
+ (void)end {  
    [DMURLProtocol end];  
}  
  
@end  

五、代码

本文中贴出了比较多的代码,为了便于大家整体观看,可以到 这里[12] 来阅读。

由于其中包含了一些数据操作的内容不需要关心,所以我直接省略了,虽然没有 Demo,但我相信大家都是能理解整个监控结构的。

六、Other

如果你的 APP 从 iOS 9 支持,可以使用 NetworkExtension,通过 NetworkExtension 可以通过 VPN 的形式接管整个网络请求,省掉了上面所有的烦恼。

参考

[1]https://github.com/aozhimin/iOS-Monitor-Platform
[2]https://github.com/zixun/GodEye
[3]https://github.com/zixun/NetworkEye
[4]https://tech.meituan.com/2016/12/19/hertz.html
[5]http://liujinlongxa.com/2016/12/20/使用NSURLProtocol注意的一些问题/
[6]https://github.com/Draveness/analyze/blob/master/contents/OHHTTPStubs/iOS%20开发中使用%20NSURLProtocol%20拦截%20HTTP%20请求.md
[7]https://www.jianshu.com/p/49e7282e888d
[8]https://developer.apple.com/documentation/foundation/nsurlprotocol?language=occ
[9]https://github.com/nicklockwood/GZIP
[10]https://stackoverflow.com/questions/5695914/nsurlrequest-where-an-app-can-find-the-default-headers-for-http-request
[11]https://stackoverflow.com/questions/21694886/nsmutableurlrequest-cant-access-all-request-headers-sent-out-from-within-my-iph
[12]https://github.com/summertian4/iOS-ObjectiveC/tree/master/NetworkTraffic


黑客/网络安全学习路线

对于从来没有接触过黑客/网络安全的同学,目前网络安全、信息安全也是计算机大学生毕业薪资相对较高的学科。

大白也帮大家准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

这也是耗费了大白近四个月的时间,吐血整理,文章非常非常长,觉得有用的话,希望粉丝朋友帮忙点个**「分享」「收藏」「在看」「赞」**

网络安全/渗透测试法律法规必知必会****

今天大白就帮想学黑客/网络安全技术的朋友们入门必须先了解法律法律。

【网络安全零基础入门必知必会】网络安全行业分析报告(01)

【网络安全零基础入门必知必会】什么是黑客、白客、红客、极客、脚本小子?(02)

【网络安全零基础入门必知必会】网络安全市场分类(03)

【网络安全零基础入门必知必会】常见的网站攻击方式(04)

【网络安全零基础入门必知必会】网络安全专业术语全面解析(05)

【网络安全入门必知必会】《中华人民共和国网络安全法》(06)

【网络安全零基础入门必知必会】《计算机信息系统安全保护条例》(07)

【网络安全零基础入门必知必会】《中国计算机信息网络国际联网管理暂行规定》(08)

【网络安全零基础入门必知必会】《计算机信息网络国际互联网安全保护管理办法》(09)

【网络安全零基础入门必知必会】《互联网信息服务管理办法》(10)

【网络安全零基础入门必知必会】《计算机信息系统安全专用产品检测和销售许可证管理办法》(11)

【网络安全零基础入门必知必会】《通信网络安全防护管理办法》(12)

【网络安全零基础入门必知必会】《中华人民共和国国家安全法》(13)

【网络安全零基础入门必知必会】《中华人民共和国数据安全法》(14)

【网络安全零基础入门必知必会】《中华人民共和国个人信息保护法》(15)

【网络安全零基础入门必知必会】《网络产品安全漏洞管理规定》(16)

网络安全/渗透测试linux入门必知必会

【网络安全零基础入门必知必会】什么是Linux?Linux系统的组成与版本?什么是命令(01)

【网络安全零基础入门必知必会】VMware下载安装,使用VMware新建虚拟机,远程管理工具(02)

【网络安全零基础入门必知必会】VMware常用操作指南(非常详细)零基础入门到精通,收藏这一篇就够了(03)

【网络安全零基础入门必知必会】CentOS7安装流程步骤教程(非常详细)零基入门到精通,收藏这一篇就够了(04)

【网络安全零基础入门必知必会】Linux系统目录结构详细介绍(05)

【网络安全零基础入门必知必会】Linux 命令大全(非常详细)零基础入门到精通,收藏这一篇就够了(06)

【网络安全零基础入门必知必会】linux安全加固(非常详细)零基础入门到精通,收藏这一篇就够了(07)

网络安全/渗透测试****计算机网络入门必知必会****

【网络安全零基础入门必知必会】TCP/IP协议深入解析(非常详细)零基础入门到精通,收藏这一篇就够了(01)

【网络安全零基础入门必知必会】什么是HTTP数据包&Http数据包分析(非常详细)零基础入门到精通,收藏这一篇就够了(02)

【网络安全零基础入门必知必会】计算机网络—子网划分、子网掩码和网关(非常详细)零基础入门到精通,收藏这一篇就够了(03)

网络安全/渗透测试入门之HTML入门必知必会

【网络安全零基础入门必知必会】什么是HTML&HTML基本结构&HTML基本使用(非常详细)零基础入门到精通,收藏这一篇就够了1

【网络安全零基础入门必知必会】VScode、PhpStorm的安装使用、Php的环境配置,零基础入门到精通,收藏这一篇就够了2

【网络安全零基础入门必知必会】HTML之编写登录和文件上传(非常详细)零基础入门到精通,收藏这一篇就够了3

网络安全/渗透测试入门之Javascript入门必知必会

【网络安全零基础入门必知必会】Javascript语法基础(非常详细)零基础入门到精通,收藏这一篇就够了(01)

【网络安全零基础入门必知必会】Javascript实现Post请求、Ajax请求、输出数据到页面、实现前进后退、文件上传(02)

网络安全/渗透测试入门之Shell入门必知必会

【网络安全零基础入门必知必会】Shell编程基础入门(非常详细)零基础入门到精通,收藏这一篇就够了(第七章)

网络安全/渗透测试入门之PHP入门必知必会

【网络安全零基础入门】PHP环境搭建、安装Apache、安装与配置MySQL(非常详细)零基础入门到精通,收藏这一篇就够(01)

【网络安全零基础入门】PHP基础语法(非常详细)零基础入门到精通,收藏这一篇就够了(02)

【网络安全零基础入门必知必会】PHP+Bootstrap实现表单校验功能、PHP+MYSQL实现简单的用户注册登录功能(03)

网络安全/渗透测试入门之MySQL入门必知必会

【网络安全零基础入门必知必会】MySQL数据库基础知识/安装(非常详细)零基础入门到精通,收藏这一篇就够了(01)

【网络安全零基础入门必知必会】SQL语言入门(非常详细)零基础入门到精通,收藏这一篇就够了(02)

【网络安全零基础入门必知必会】MySQL函数使用大全(非常详细)零基础入门到精通,收藏这一篇就够了(03)

【网络安全零基础入门必知必会】MySQL多表查询语法(非常详细)零基础入门到精通,收藏这一篇就够了(04)

****网络安全/渗透测试入门之Python入门必知必会

【网络安全零基础入门必知必会】之Python+Pycharm安装保姆级教程,Python环境配置使用指南,收藏这一篇就够了【1】

【网络安全零基础入门必知必会】之Python编程入门教程(非常详细)零基础入门到精通,收藏这一篇就够了(2)

python开发之手写第一个python程序

python开发笔记之变量

python基础语法特征

python开发数据类型

python开发笔记之程序交互

python入门教程之python开发学习笔记基本数据类型

python入门教程之python开发笔记之格式化输出

python入门教程之python开发笔记基本运算符

python入门教程python开发基本流程控制if … else

python入门教程之python开发笔记流程控制之循环

python入门之Pycharm开发工具的使用

python入门教程之python字符编码转换

python入门之python开发字符编码

python入门之python开发基本数据类型数字

python入门python开发基本数据类型字符串

python入门python开发基本数据类型列表

python入门python开发基本数据类型

python入门教程之python开发可变和不可变数据类型和hash

python入门教程python开发字典数据类型

python入门之python开发笔记基本数据类型集合

python开发之collections模块

python开发笔记之三元运算

【网络安全零基础入门必知必会】之10个python爬虫入门实例(非常详细)零基础入门到精通,收藏这一篇就够了(3)

****网络安全/渗透测试入门之SQL注入入门必知必会

【网络安全渗透测试零基础入门必知必会】之初识SQL注入(非常详细)零基础入门到精通,收藏这一篇就够了(1)

【网络安全渗透测试零基础入门必知必会】之SQL手工注入基础语法&工具介绍(2)

【网络安全渗透测试零基础入门必知必会】之SQL注入实战(非常详细)零基础入门到精通,收藏这一篇就够了(3)

【网络安全渗透测试零基础入门必知必会】之SQLmap安装&实战(非常详细)零基础入门到精通,收藏这一篇就够了(4)

【网络安全渗透测试零基础入门必知必会】之SQL防御(非常详细)零基础入门到精通,收藏这一篇就够了(4)

****网络安全/渗透测试入门之XSS攻击入门必知必会

【网络安全渗透测试零基础入门必知必会】之XSS攻击基本概念和原理介绍(非常详细)零基础入门到精通,收藏这一篇就够了(1)

网络安全渗透测试零基础入门必知必会】之XSS攻击获取用户cookie和用户密码(实战演示)零基础入门到精通收藏这一篇就够了(2)

【网络安全渗透测试零基础入门必知必会】之XSS攻击获取键盘记录(实战演示)零基础入门到精通收藏这一篇就够了(3)

【网络安全渗透测试零基础入门必知必会】之xss-platform平台的入门搭建(非常详细)零基础入门到精通,收藏这一篇就够了4

【网络安全渗透测试入门】之XSS漏洞检测、利用和防御机制XSS游戏(非常详细)零基础入门到精通,收藏这一篇就够了5

****网络安全/渗透测试入门文件上传攻击与防御入门必知必会

【网络安全渗透测试零基础入门必知必会】之什么是文件包含漏洞&分类(非常详细)零基础入门到精通,收藏这一篇就够了1

【网络安全渗透测试零基础入门必知必会】之cve实际漏洞案例解析(非常详细)零基础入门到精通, 收藏这一篇就够了2

【网络安全渗透测试零基础入门必知必会】之PHP伪协议精讲(文件包含漏洞)零基础入门到精通,收藏这一篇就够了3

【网络安全渗透测试零基础入门必知必会】之如何搭建 DVWA 靶场保姆级教程(非常详细)零基础入门到精通,收藏这一篇就够了4

【网络安全渗透测试零基础入门必知必会】之Web漏洞-文件包含漏洞超详细全解(附实例)5

【网络安全渗透测试零基础入门必知必会】之文件上传漏洞修复方案6

****网络安全/渗透测试入门CSRF渗透与防御必知必会

【网络安全渗透测试零基础入门必知必会】之CSRF漏洞概述和原理(非常详细)零基础入门到精通, 收藏这一篇就够了1

【网络安全渗透测试零基础入门必知必会】之CSRF攻击的危害&分类(非常详细)零基础入门到精通, 收藏这一篇就够了2

【网络安全渗透测试零基础入门必知必会】之XSS与CSRF的区别(非常详细)零基础入门到精通, 收藏这一篇就够了3

【网络安全渗透测试零基础入门必知必会】之CSRF漏洞挖掘与自动化工具(非常详细)零基础入门到精通,收藏这一篇就够了4

【网络安全渗透测试零基础入门必知必会】之CSRF请求伪造&Referer同源&置空&配合XSS&Token值校验&复用删除5

****网络安全/渗透测试入门SSRF渗透与防御必知必会

【网络安全渗透测试零基础入门必知必会】之SSRF漏洞概述及原理(非常详细)零基础入门到精通,收藏这一篇就够了 1

【网络安全渗透测试零基础入门必知必会】之SSRF相关函数和协议(非常详细)零基础入门到精通,收藏这一篇就够了2

【网络安全渗透测试零基础入门必知必会】之SSRF漏洞原理攻击与防御(非常详细)零基础入门到精通,收藏这一篇就够了3**
**

****网络安全/渗透测试入门XXE渗透与防御必知必会

【网络安全渗透测试零基础入门必知必会】之XML外部实体注入(非常详细)零基础入门到精通,收藏这一篇就够了1

网络安全渗透测试零基础入门必知必会】之XXE的攻击与危害(非常详细)零基础入门到精通,收藏这一篇就够了2

【网络安全渗透测试零基础入门必知必会】之XXE漏洞漏洞及利用方法解析(非常详细)零基础入门到精通,收藏这一篇就够了3

【网络安全渗透测试零基础入门必知必会】之微信XXE安全漏洞处理(非常详细)零基础入门到精通,收藏这一篇就够了4

****网络安全/渗透测试入门远程代码执行渗透与防御必知必会

【网络安全渗透测试零基础入门必知必会】之远程代码执行原理介绍(非常详细)零基础入门到精通,收藏这一篇就够了1

【网络安全零基础入门必知必会】之CVE-2021-4034漏洞原理解析(非常详细)零基础入门到精通,收藏这一篇就够了2

【网络安全零基础入门必知必会】之PHP远程命令执行与代码执行原理利用与常见绕过总结3

【网络安全零基础入门必知必会】之WEB安全渗透测试-pikachu&DVWA靶场搭建教程,零基础入门到精通,收藏这一篇就够了4

****网络安全/渗透测试入门反序列化渗透与防御必知必会

【网络安全零基础入门必知必会】之什么是PHP对象反序列化操作(非常详细)零基础入门到精通,收藏这一篇就够了1

【网络安全零基础渗透测试入门必知必会】之php反序列化漏洞原理解析、如何防御此漏洞?如何利用此漏洞?2

【网络安全渗透测试零基础入门必知必会】之Java 反序列化漏洞(非常详细)零基础入门到精通,收藏这一篇就够了3

【网络安全渗透测试零基础入门必知必会】之Java反序列化漏洞及实例解析(非常详细)零基础入门到精通,收藏这一篇就够了4

【网络安全渗透测试零基础入门必知必会】之CTF题目解析Java代码审计中的反序列化漏洞,以及其他漏洞的组合利用5

网络安全/渗透测试**入门逻辑漏洞必知必会**

【网络安全渗透测试零基础入门必知必会】之一文带你0基础挖到逻辑漏洞(非常详细)零基础入门到精通,收藏这一篇就够了

网络安全/渗透测试入门暴力猜解与防御必知必会

【网络安全渗透测试零基础入门必知必会】之密码安全概述(非常详细)零基础入门到精通,收藏这一篇就够了1

【网络安全渗透测试零基础入门必知必会】之什么样的密码是不安全的?(非常详细)零基础入门到精通,收藏这一篇就够了2

【网络安全渗透测试零基础入门必知必会】之密码猜解思路(非常详细)零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之利用Python暴力破解邻居家WiFi密码、压缩包密码,收藏这一篇就够了4

【网络安全渗透测试零基础入门必知必会】之BurpSuite密码爆破实例演示,零基础入门到精通,收藏这一篇就够了5

【网络安全渗透测试零基础入门必知必会】之Hydra密码爆破工具使用教程图文教程,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之暴力破解medusa,零基础入门到精通,收藏这一篇就够了7

【网络安全渗透测试零基础入门必知必会】之Metasploit抓取密码,零基础入门到精通,收藏这一篇就够了8

Wfuzz:功能强大的web漏洞挖掘工具

****网络安全/渗透测试入门掌握Redis未授权访问漏洞必知必会

【网络安全渗透测试零基础入门必知必会】之Redis未授权访问漏洞,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Redis服务器被攻击后该如何安全加固,零基础入门到精通,收藏这一篇就够了**
**

网络安全/渗透测试入门掌握**ARP渗透与防御关必知必会**

【网络安全渗透测试零基础入门必知必会】之ARP攻击原理解析,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之ARP流量分析,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之ARP防御策略与实践指南,零基础入门到精通,收藏这一篇就够了

网络安全/渗透测试入门掌握系统权限提升渗透与防御关****必知必会

【网络安全渗透测试零基础入门必知必会】之Windows提权常用命令,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Windows权限提升实战,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之linux 提权(非常详细)零基础入门到精通,收藏这一篇就够了

网络安全/渗透测试入门掌握Dos与DDos渗透与防御相关****必知必会

【网络安全渗透测试零基础入门必知必会】之DoS与DDoS攻击原理(非常详细)零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Syn-Flood攻击原理解析(非常详细)零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之IP源地址欺骗与dos攻击,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之SNMP放大攻击原理及实战演示,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之NTP放大攻击原理,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之什么是CC攻击?CC攻击怎么防御?,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之如何防御DDOS的攻击?零基础入门到精通,收藏这一篇就够了

网络安全/渗透测试入门掌握无线网络安全渗透与防御相****必知必会

【网络安全渗透测试零基础入门必知必会】之Aircrack-ng详细使用安装教程,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之aircrack-ng破解wifi密码(非常详细)零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之WEB渗透近源攻击,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之无线渗透|Wi-Fi渗透思路,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之渗透WEP新思路Hirte原理解析,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之WPS的漏洞原理解析,零基础入门到精通,收藏这一篇就够了

网络安全/渗透测试入门掌握木马免杀问题与防御********必知必会

【网络安全渗透测试零基础入门必知必会】之Metasploit – 木马生成原理和方法,零基础入门到精通,收藏这篇就够了

【网络安全渗透测试零基础入门必知必会】之MSF使用教程永恒之蓝漏洞扫描与利用,收藏这一篇就够了

网络安全/渗透测试入门掌握Vulnhub靶场实战********必知必会

【网络安全渗透测试零基础入门必知必会】之Vulnhub靶机Prime使用指南,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Vulnhub靶场Breach1.0解析,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之vulnhub靶场之DC-9,零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Vulnhub靶机Kioptrix level-4 多种姿势渗透详解,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之Vulnhub靶场PWNOS: 2.0 多种渗透方法,收藏这一篇就够了

网络安全/渗透测试入门掌握社会工程学必知必会

【网络安全渗透测试零基础入门必知必会】之什么是社会工程学?定义、类型、攻击技术,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之社会工程学之香农-韦弗模式,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之社工学smcr通信模型,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之社会工程学之社工步骤整理(附相应工具下载)收藏这一篇就够了

网络安全/渗透测试入门掌握********渗透测试工具使用******必知必会**

2024版最新Kali Linux操作系统安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

【网络安全渗透测试零基础入门必知必会】之渗透测试工具大全之Nmap安装使用命令指南,零基础入门到精通,收藏这一篇就够了

2024版最新AWVS安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新burpsuite安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新owasp_zap安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新Sqlmap安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新Metasploit安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新Nessus下载安装激活使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

2024版最新Wireshark安装使用教程(非常详细)零基础入门到精通,收藏这一篇就够了

觉得有用的话,希望粉丝朋友帮大白点个**「分享」「收藏」「在看」「赞」**

黑客/网络安全学习包

资料目录

  1. 成长路线图&学习规划

  2. 配套视频教程

  3. SRC&黑客文籍

  4. 护网行动资料

  5. 黑客必读书单

  6. 面试题合集

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

1.成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

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


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

2.视频教程

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

3.SRC&黑客文籍

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

黑客资料由于是敏感资源,这里不能直接展示哦!

4.护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

5.黑客必读书单

**

**

6.面试题合集

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。

更多内容为防止和谐,可以扫描获取~

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值