美文网首页iOS开发技巧面试
2021 iOS面试题大全---全方面剖析面试(二)

2021 iOS面试题大全---全方面剖析面试(二)

作者: ios南方 | 来源:发表于2021-07-24 15:14 被阅读0次

    九.iOS面试题-----多线程相关之NSOperation、NSOperationQueue、NSThread+runloop实现常驻线程、加锁

    • NSOperationQueue的优点
    • NSOperation和NSOperationQueue
    • NSThread+runloop实现常驻线程
    • 自旋锁与互斥锁

    一、NSOperationQueue的优点

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

    • 1、可以添加任务依赖,方便控制执行顺序
    • 2、可以设定操作执行的优先级
    • 3、任务执行状态控制:isReady,isExecuting,isFinished,isCancelled

    如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出
    如果重写了NSOperation的start方法,自行控制任务状态
    系统通过KVO的方式移除isFinished==YES的NSOperation

    • 4、可以设置最大并发量

    二、NSOperation和NSOperationQueue

    • 操作(Operation):

    执行操作的意思,换句话说就是你在线程中执行的那段代码。
    在 GCD 中是放在 block 中的。在 NSOperation 中,使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。

    • 操作队列(Operation Queues):

    这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
    操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
    NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

    iOS 多线程:『NSOperationNSOperationQueue』详尽总结 - 简书

    三、NSThread+runloop实现常驻线程

    NSThread在实际开发中比较常用到的场景就是去实现常驻线程。

    • 由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用,那么如何创建一个线程可以让它可以再次工作呢?也就是创建一个常驻线程。

    首先常驻线程既然是常驻,那么我们可以用GCD实现一个单例来保存NSThread

    + (NSThread *)shareThread {
    
        static NSThread *shareThread = nil;
    
        static dispatch_once_t oncePredicate;
    
        dispatch_once(&oncePredicate, ^{
    
            shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
    
            [shareThread setName:@"threadTest"];
    
            [shareThread start];
        });
    
        return shareThread;
    }
    
    

    这样创建的thread就不会销毁了吗?

    [self performSelector:@selector(test) onThread:[ViewController shareThread] withObject:nil waitUntilDone:NO];
    
    - (void)test
    {
        NSLog(@"test:%@", [NSThread currentThread]);
    }
    
    

    并没有打印,说明test方法没有被调用。
    那么可以用runloop来让线程常驻

    + (NSThread *)shareThread {
    
        static NSThread *shareThread = nil;
    
        static dispatch_once_t oncePredicate;
    
        dispatch_once(&oncePredicate, ^{
    
            shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest2) object:nil];
    
            [shareThread setName:@"threadTest"];
    
            [shareThread start];
        });
    
        return shareThread;
    }
    
    + (void)threadTest
    {
        @autoreleasepool {
    
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
            [runLoop run];
        }
    }
    
    

    这时候再去调用performSelector就有打印了。

    四、自旋锁与互斥锁

    image
    自旋锁:

    是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
    在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

    互斥锁:

    当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

    总结:

    自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
      互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

    优缺点:

    自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
      缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

    自旋锁:atomic、OSSpinLock、dispatch_semaphore_t
    互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock

    十.RunLoop数据结构、RunLoop的实现机制、RunLoop的Mode、RunLoop与NSTimer和线程

    RunLoop概念
    RunLoop的数据结构
    RunLoop的Mode
    RunLoop的实现机制
    RunLoop与NSTimer
    RunLoop和线程

    一、RunLoop概念

    RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。

    1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
    2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态

    为什么main函数不会退出?
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    
    

    UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)

    //无限循环代码模式(伪代码)
    int main(int argc, char * argv[]) {        
        BOOL running = YES;
        do {
            // 执行各种任务,处理各种事件
            // ......
        } while (running);
    
        return 0;
    }
    
    

    UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

    二、RunLoop的数据结构

    NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
    RunLoop 相关的主要涉及五个类:

    CFRunLoop:RunLoop对象
    CFRunLoopMode:运行模式
    CFRunLoopSource:输入源/事件源
    CFRunLoopTimer:定时源
    CFRunLoopObserver:观察者

    1、CFRunLoop

    pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成

    2、CFRunLoopMode

    由name、source0、source1、observers、timers构成

    3、CFRunLoopSource

    分为source0和source1两种

    • source0:
      即非基于port的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户态
    • source1:
      基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。
      具备唤醒线程的能力
    4、CFRunLoopTimer

    基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

    5、CFRunLoopObserver

    监听以下时间点:CFRunLoopActivity

    • kCFRunLoopEntry
      RunLoop准备启动
    • kCFRunLoopBeforeTimers
      RunLoop将要处理一些Timer相关事件
    • kCFRunLoopBeforeSources
      RunLoop将要处理一些Source事件
    • kCFRunLoopBeforeWaiting
      RunLoop将要进行休眠状态,即将由用户态切换到内核态
    • kCFRunLoopAfterWaiting
      RunLoop被唤醒,即从内核态切换到用户态后
    • kCFRunLoopExit
      RunLoop退出
    • kCFRunLoopAllActivities
      监听所有状态
    6、各数据结构之间的联系

    线程和RunLoop一一对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的

    image

    三、RunLoop的Mode

    关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

    image

    当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的

    总共是有五种CFRunLoopMode:

    • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行

    • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)

    • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用

    • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

    • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

    四、RunLoop的实现机制

    image

    这张图在网上流传比较广。
    对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。

    image

    RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
    即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop

    Mach消息发送机制
    大致逻辑为:
    1、通知观察者 RunLoop 即将启动。
    2、通知观察者即将要处理Timer事件。
    3、通知观察者即将要处理source0事件。
    4、处理source0事件。
    5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
    6、通知观察者线程即将进入休眠状态。
    7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

    • 一个基于 port 的Source1 的事件(图里应该是source0)。
    • 一个 Timer 到时间了。
    • RunLoop 自身的超时时间到了。
    • 被其他调用者手动唤醒。

    8、通知观察者线程将被唤醒。
    9、处理唤醒时收到的事件。

    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
    • 如果输入源启动,传递相应的消息。
    • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

    10、通知观察者RunLoop结束。

    五、RunLoop与NSTimer

    一个比较常见的问题:滑动tableView时,定时器还会生效吗?
    默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。
    怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
    所以我们需要把Timer同时添加到UITrackingRunLoopModekCFRunLoopDefaultMode上。
    那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    

    Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件

    六、RunLoop和线程

    • 线程和RunLoop是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
    • 自己创建的线程默认是没有开启RunLoop的
    1、怎么创建一个常驻线程?

    1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个RunLoop)
    1、向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)
    2、启动该RunLoop

       @autoreleasepool {
    
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
            [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
            [runLoop run];
    
        }
    
    
    2、输出下边代码的执行顺序
     NSLog(@"1");
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        NSLog(@"2");
    
        [self performSelector:@selector(test) withObject:nil afterDelay:10];
    
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    - (void)test
    {
    
        NSLog(@"5");
    }
    
    

    答案是1423,test方法并不会执行。
    原因是如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。
    那么我们改成:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            NSLog(@"2");
    
            [[NSRunLoop currentRunLoop] run];
    
            [self performSelector:@selector(test) withObject:nil afterDelay:10];
    
            NSLog(@"3");
        });
    
    

    然而test方法依然不执行。
    原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于其mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。
    所以我们自己启动RunLoop,一定要在添加item后

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            NSLog(@"2");
    
            [self performSelector:@selector(test) withObject:nil afterDelay:10];
    
            [[NSRunLoop currentRunLoop] run];
    
            NSLog(@"3");
        });
    
    
    3、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

    当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。
    我们就可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI

    [self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
    
    

    十一. iOS面试题-----网络相关之HTTP协议

    HTTP协议:超文本传输协议

    是一种详细规定了浏览器和万维网(WWW = World Wide Web)服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
    HTTP是基于TCP的应用层协议
    (OSI网络七层协议从上到下分别是 应用层表示层会话层传输层网络层数据链路层物理层

    • 请求/响应报文

    • 连接建立流程

    • HTTP的特点

      image

    一、请求报文和响应报文

    1、请求报文
    image

    如下:

    POST /somedir/page.html HTTP/1.1    
    //以上是请求行:方法字段、URL字段和HTTP版本字段
    Host: www.user.com
    Content-Type: application/x-www-form-urlencoded
    Connection: Keep-Alive
    User-agent: Mozilla/5.0.    
    Accept-lauguage: fr  
    //以上是首部行
    (此处必须有一空行)  //空行分割header和请求内容 
    name=world   请求体
    
    

    Host:指明了该对象所在的主机
    ConnectionKeep-Alive首部行用来表明该浏览器告诉服务器使用持续连接
    Content-Type: x-www-form-urlencoded首部行用来表明 HTTP会将请求参数用key1=val1&key2=val2的方式进行组织,并放到请求实体里面
    User-agent:首部行用来指明用户代理,即向服务器发送请求的浏览器类型
    Accept-lauguage:首部行表示用户想得到该对象的法语版本(如果服务器中有这样的对象的话),否则,服务器应发送它的默认版本

    2、响应报文
    image

    如下:

    HTTP/1.1 200 OK    
    //以上是状态行:协议版本字段、状态码、相应状态信息
    Connection:close
    Server:Apache/2.2.3(CentOS)
    Date: Sat, 31 Dec 2005 23:59:59 GMT
    Content-Type: text/html
    Content-Length: 122
    //以上是首部行
    (此处必须有一空行)  //空行分割header和实体主体
    (data data data data)//响应实体主体
    
    
    状态码及其相应的短语指示了请求的结果。

    一些常见的状态码和对应的短语:

    • 200 OK:请求成功,信息在返回的响应报文中
    • 301 Moved Permanently:请求的对象已经被永久转移了,新的URL定义在响应报文中的Location:首部行中。客户软件将自动获取新的URL
    • 400 Bad Request:一个通用差错代码,指示该请求不能被服务器理解
    • 404 Not Found:被请求的文件不在服务器上
    • 505 HTTP Version Not Supported:服务器不支持请求报文使用的HTTP协议版本
      <4开头的状态码通常是客户端的问题,5开头的则通常是服务端的问题>

    Connection:close首部行告诉客户,发送完报文后将关闭TCP连接。
    Date:指的不是对象创建或最后修改的时间,而是服务器从文件系统中检索到该对象,插入到响应报文,并发送该响应报文的时间。
    Server: 首部行指示该报文是由一台Apache Web服务器产生的,类似于HTTP请求报文里的User-agent
    Content-Length:首部行指示了被发送对象中的字节数
    Content-Type:首部行指示了实体体中的对象是HTML文本

    二、HTTP的请求方式

    GET、POST、PUT、DELETE、HEAD、OPTIONS
    1、GET和POST方式的区别
    从语法角度来看,最直观的区别就是
    • GET的请求参数一般以?分割拼接到URL后面,POST请求参数在Body里面
    • GET参数长度限制为2048个字符,POST一般是没限制的
    • GET请求由于参数裸露在URL中, 是不安全的,POST请求则是相对安全
      之所以说是相对安全,是因为,如果POST虽然参数非明文,但如果被抓包,GET和POST一样都是不安全的。(HTTPS该用还是得用)
    而从语义的角度来看:

    GET:获取资源是 安全的幂等的(只读的,纯粹的), 可缓存的
    POST:获取资源是 非安全的非幂等的不可缓存的

    • 这里的安全是指不应引起Server端的任何状态变化
      GET的语义就是获取数据,是不会引起服务器的状态变化的,即是安全的。(HEAD,OPTIONS也是安全的)
      而POST语义则是提交数据,是可能会引起服务器状态变化的,即是不安全的
    • 幂等:同一个请求方法执行多次和执行一次的效果完全相同
      显然GET请求是幂等而POST请求是非幂等的。
      这里用幂等形容GET还不够,因为GET不止是执行多次和执行一次的效果完全相同,而且是执行一次和执行零次的效果也是完全相同的。
    • 可缓存的
      请求是否可以被缓存。
      GET请求会主动进行Cache

    以上特性,并非并列,正是因为GET是幂等的只读的,即GET请求除了返回数据不会有其他副作用,所以GET才是安全的,从而可以直接由CDN缓存,大大减轻服务器的负担,也就是可缓存的
    而POST是非幂等的,即除了返回数据还会有其他副作用,所以POST是不安全的,必须交由web服务器处理,即是 不可缓存的

    GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

    在响应时,GET产生一个TCP数据包;POST产生两个TCP数据包:
    对于GET方式的请求,浏览器会把Header和实体主体一并发送出去,服务器响应200(返回数据);
    而对于POST,浏览器先发送Header,服务器响应100 Continue,浏览器再发送实体主体,服务器响应200 OK(返回数据)。

    2、GET 相对 POST 的优势是什么?

    1、最大的优势就是方便。GET 的URL可以直接手输,从而GET请求中的URL可以被存在书签里,或者历史记录里
    2、可以被缓存,大大减轻服务器的负担

    所以大多数情况下,还是用GET比较好。

    三、HTTP的特点

    无连接无状态
    HTTP的持久连接、Cookie/Session

    1、HTTP的无状态

    即协议对于事务处理没有记忆能力。
    每次的请求都是独立的,它的执行情况和结果与前面的请求和之后的请求时无直接关系的,它不会受前面的请求应答情况直接影响,也不会直接影响后面的请求应答情况
    也就是说服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
    标准的HTTP协议指的是不包括cookies,session,application的HTTP协议

    2、HTTP的持久连接
    image
    • 非持久连接:每个连接处理一个请求-响应事务。

    • 持久连接:每个连接可以处理多个请求-响应事务。

    持久连接情况下,服务器发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。
    HTTP/1.0 使用非持久连接。 HTTP/1.1 默认使用持久连接 <keep-alive>

    非持久连接的每个连接,TCP得在客户端和服务端分配TCP缓冲区,并维持TCP变量,会严重增加服务器负担。而且每个对象都有2个RTT(Round Trip Time,也就是一个数据包从发出去到回来的时间)的延迟,由于TCP的拥塞控制方案,每个对象都遭受TCP缓启动,因为每个TCP连接都起始于缓启动阶段

    HTTP持久连接怎么判断一个请求是否结束的?
    • Content-length:根据所接收字节数是否达到Content-length
    • chunked(分块传输):Transfer-Encoding。当选择分块传输时,响应头中可以不包含Content-Length,服务器会先回复一个不带数据的报文(只有响应行和响应头和\r\n),然后开始传输若干个数据块。当传输完若干个数据块后,需要再传输一个空的数据块,当客户端收到空的数据块时,则客户端知道数据接收完毕。

    十二.iOS面试题-----网络相关之HTTPS、对称加密、非对称加密

    一、HTTPS和HTTP的区别

    HTTPS协议 = HTTP协议 + SSL/TLS协议
    SSL的全称是Secure Sockets Layer,即安全套接层协议,是为网络通信提供安全及数据完整性的一种安全协议。TLS的全称是Transport Layer Security,即安全传输层协议。
    即HTTPS是安全的HTTP。

    二、HTTPS的连接建立流程

    HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。在传输的过程中会涉及到三个密钥:

    • 服务器端的公钥和私钥,用来进行非对称加密

    • 客户端生成的随机密钥,用来进行对称加密

      image

    如上图,HTTPS连接过程大致可分为八步:

    1、客户端访问HTTPS连接。

    客户端会把安全协议版本号、客户端支持的加密算法列表、随机数C发给服务端。

    2、服务端发送证书给客户端

    服务端接收密钥算法配件后,会和自己支持的加密算法列表进行比对,如果不符合,则断开连接。否则,服务端会在该算法列表中,选择一种对称算法(如AES)、一种公钥算法(如具有特定秘钥长度的RSA)和一种MAC算法发给客户端。
    服务器端有一个密钥对,即公钥私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。
    在发送加密算法的同时还会把数字证书随机数S发送给客户端

    3、客户端验证server证书

    会对server公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。

    4、客户端组装会话秘钥

    如果公钥合格,那么客户端会用服务器公钥来生成一个前主秘钥(Pre-Master Secret,PMS),并通过该前主秘钥和随机数C、S来组装成会话秘钥

    5、客户端将前主秘钥加密发送给服务端

    是通过服务端的公钥来对前主秘钥进行非对称加密,发送给服务端

    6、服务端通过私钥解密得到前主秘钥

    服务端接收到加密信息后,用私钥解密得到主秘钥。

    7、服务端组装会话秘钥

    服务端通过前主秘钥和随机数C、S来组装会话秘钥
    至此,服务端和客户端都已经知道了用于此次会话的主秘钥。

    8、数据传输

    客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。
    同理,服务端收到客户端发送来的密文,用服务端密钥对其进行对称解密,得到客户端发送的数据。

    总结:

    会话秘钥 = random S + random C + 前主秘钥

    • HTTPS连接建立过程使用非对称加密,而非对称加密是很耗时的一种加密方式
    • 后续通信过程使用对称加密,减少耗时所带来的性能损耗
    • 其中,对称加密加密的是实际的数据,非对称加密加密的是对称加密所需要的客户端的密钥。

    三、对称加密和非对称加密

    1、对称加密

    用同一套密钥来进行加密解密。
    对称加密通常有 DES,IDEA,3DES 加密算法。

    2、非对称加密

    用公钥和私钥来加解密的算法。
    公钥(Public Key)与私钥(Private Key)是通过一种算法得到的一个密钥对(即一个公钥和一个私钥),公钥是密钥对中公开的部分,私钥则是非公开的部分,私钥通常是保存在本地。

    • 公钥进行加密,就要用私钥进行解密;反之,用私钥加密,就要用公钥进行解密(数字签名)。

    • 由于私钥是保存在本地的,所以非对称加密相对与对称加密是安全的。
      非对称加密对称加密耗时(100倍以上),所以通常要结合对称加密来使用。

    常见的非对称加密算法有:RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)

    而为了确保客户端能够确认公钥就是想要访问的网站的公钥,引入了数字证书的概念,由于证书存在一级一级的签发过程,所以就出现了证书链,在证书链中的顶端的就是根CA。

    十三.iOS面试-----一个基于UDP的简单的聊天Demo(用C语言、python、GCDAsyncUdpSocket来实现UDP通信)

    <meta charset="utf-8">

    一、分别用C语言、python、GCDAsyncUdpSocket来实现UDP通信

    1、C语言方式
    • 首先初始化socket对象,Udp要用SOCK_DGRAM
    • 然后初始化sockaddr_in网络通信对象,如果作为服务端要绑定socket对象与通信链接,来接收消息
    • 然后开启一个循环,循环调用recvfrom来接收消息
    • 收到消息后,保存下发消息对象的地址,以便之后回复消息
    - (void)initCSocket
    {
    
        char receiveBuffer[1024];
    
        __uint32_t nSize = sizeof(struct sockaddr);
    
        if ((_listenfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
        {
    
            perror("socket() error. Failed to initiate a socket");
        }
    
        bzero(&_addr, sizeof(_addr));
    
        _addr.sin_family = AF_INET;
    
        _addr.sin_port = htons(_destPort);
    
        if(bind(_listenfd, (struct sockaddr *)&_addr, sizeof(_addr)) == -1)
        {
            perror("Bind() error.");
        }
    
        _addr.sin_addr.s_addr = inet_addr([_destHost UTF8String]);//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
    
        while(true){
    
            long strLen = recvfrom(_listenfd, receiveBuffer, sizeof(receiveBuffer), 0, (struct sockaddr *)&_addr, &nSize);
    
            NSString * message = [[NSString alloc] initWithBytes:receiveBuffer length:strLen encoding:NSUTF8StringEncoding];
    
            _destPort = ntohs(_addr.sin_port);
    
            _destHost = [[NSString alloc] initWithUTF8String:inet_ntoa(_addr.sin_addr)];
    
            NSLog(@"来自%@---%zd:%@",_destHost,_destPort,message);
        }
    }
    
    
    • 由于开启while循环来一直接收消息,所以为了避免阻塞主线程,这里要将initCSocket函数放在子线程中调用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
            [self initCSocket];
        });
    
    
    • 调用sendto方法来发送消息
    - (void)sendMessage:(NSString *)message
    {
        NSData *sendData = [message dataUsingEncoding:NSUTF8StringEncoding];
    
        sendto(_listenfd, [sendData bytes], [sendData length], 0, (struct sockaddr *)&_addr, sizeof(struct sockaddr));
    }
    
    
    2、GCDAsyncUdpSocket方式
    - (void)initGCDSocket
    {
        _receiveSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self
                                                       delegateQueue:dispatch_get_global_queue(0, 0)];
        NSError *error;
    
        // 绑定一个端口(可选),如果不绑定端口, 那么就会随机产生一个随机的唯一的端口
        // 端口数字范围(1024,2^16-1)
        [_receiveSocket bindToPort:test_port error:&error];
    
        if (error) {
            NSLog(@"服务器绑定失败");
        }
        // 开始接收对方发来的消息
        [_receiveSocket beginReceiving:nil];
    }
    
    
    • 在代理方法里获取到对方发过来的消息,记录下主机和端口,以便之后回复消息
    #pragma mark - GCDAsyncUdpSocketDelegate
    - (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)filterContext {
    
        NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
        _destPort = [GCDAsyncUdpSocket portFromAddress:address];
    
        _destHost = [GCDAsyncUdpSocket hostFromAddress:address];
    
        NSLog(@"来自%@---%zd:%@",_destHost,_destPort,message);
    }
    
    
    • 调用sendData:(NSData *)data toHost:(NSString *)host port:(uint16_t)port withTimeout:(NSTimeInterval)timeout tag:(long)tag方法来发送消息
    - (void)sendMessage:(NSString *)message
    {
        NSData *sendData = [message dataUsingEncoding:NSUTF8StringEncoding];
    
        [_receiveSocket sendData:sendData toHost:_destHost port:_destPort withTimeout:60 tag:500];
    }
    
    
    3、python方式

    python方式就比较简单了

    • 初始化socket,绑定端口
    socket = socket(AF_INET, SOCK_DGRAM)
    
    socket.bind(('', port))
    
    
    • 循环接收消息
    while True:
        message, address = socket.recvfrom(2048)
        print address,message
    
    
    • 发送消息
    socket.sendto(message, address)
    
    

    二、利用python实现Udp通信demo

    • 创建两个python文件,分别作为客户端和服务端,然后同时运行

    • 客户端

    from socket import *
    host = '127.0.0.1'
    port = 12000
    socket = socket(AF_INET, SOCK_DGRAM)
    while True:
        message = raw_input('input message ,print 0 to close :\n')
        socket.sendto(message, (host, port))
        if message == '0':
            socket.close()
            break
        receiveMessage, serverAddress = socket.recvfrom(2048)
        print receiveMessage,serverAddress
    
    
    • 服务端
    from socket import *
    port = 12000
    socket = socket(AF_INET, SOCK_DGRAM)
    socket.bind(('', port))
    print 'server is ready to receive'
    count = 0
    while True:
        message, address = socket.recvfrom(2048)
        print address,message
        count = count + 1
        if message == '0':
            socket.close()
            break
        else:
            message = raw_input('input message ,print 0 to close :\n')
            socket.sendto(message, address)
    
    
    • 客户端打印
    /usr/local/bin/python2.7 /Users/wangyong/Desktop/other/python/UDPClient.py
    input message ,print 0 to close :
    hello,服务端
    hello,客户端 ('10.208.61.53', 12000)
    input message ,print 0 to close :
    结束通信吧我们
    好的 ('10.208.61.53', 12000)
    input message ,print 0 to close :
    0
    
    Process finished with exit code 0
    
    
    • 服务端打印
    /usr/local/bin/python2.7 /Users/wangyong/Desktop/other/python/UDPServer.py
    server is ready to receive
    ('10.208.61.53', 53500) hello,服务端
    input message ,print 0 to close :
    hello,客户端
    ('10.208.61.53', 53500) 结束通信吧我们
    input message ,print 0 to close :
    好的
    ('10.208.61.53', 53500) 0
    
    Process finished with exit code 0
    
    

    三、iOS端基于UDP的简易聊天demo

    1、UdpManager

    Udp通信用C语言版和GCDAsyncUdpSocket都可以,封装在UdpManager

    • initSocketWithReceiveHandle:(dispatch_block_t)receiveHandle:初始化socket相关,receiveHandle是接收到消息后的回调
    • sendMessage:(NSString *)message:发送消息
    • messageArray:消息列表,包括接收到的和发送出去的消息
    + (void)initSocketWithReceiveHandle:(dispatch_block_t)receiveHandle;
    
    + (void)sendMessage:(NSString *)message;
    
    + (NSMutableArray *)messageArray;
    
    

    消息内容用MessageModel,其中role代表消息发送对象,为0即是接收到的消息,1为自己发送的消息

    @interface MessageModel:NSObject
    
    @property (nonatomic, copy) NSString *message;
    
    @property(nonatomic,assign) NSInteger role;
    
    @end
    
    
    2、ViewController

    控制器里调用UdpManager初始化socket

    [UdpManager initSocketWithReceiveHandle:^{
    
            dispatch_async(dispatch_get_main_queue(), ^{
    
                self.title = [NSString stringWithFormat:@"%@---%@",[[UdpManager shareManager] valueForKey:@"_destHost"],[[UdpManager shareManager] valueForKey:@"_destPort"]];
    
                [self reloadData];
            });
        }];
    
    

    在代理方法textFieldShouldReturn即点击键盘的发送按钮时发送编辑好的消息

    #pragma mark - UITextFieldDelegate
    - (BOOL)textFieldShouldReturn:(UITextField *)textField
    {
        if (self.textField.text.length == 0) return YES;
    
        [UdpManager sendMessage:self.textField.text];
    
        [self reloadData];
    
        self.textField.text = nil;
    
        return YES;
    }
    
    

    发送或者接收到新消息后都会将消息添加到messageArray里,并刷新页面

    - (void)sendMessage:(NSString *)message
    {
        NSData *sendData = [message dataUsingEncoding:NSUTF8StringEncoding];
    
        [self.messageArray addObject:[[MessageModel alloc] initWithMessage:message role:1]];
    
    #ifdef UseGCDUdpSocket
    
        // 该函数只是启动一次发送 它本身不进行数据的发送, 而是让后台的线程慢慢的发送 也就是说这个函数调用完成后,数据并没有立刻发送,异步发送
        [_receiveSocket sendData:sendData toHost:_destHost port:_destPort withTimeout:60 tag:500];
    
    #else
    
        sendto(_listenfd, [sendData bytes], [sendData length], 0, (struct sockaddr *)&_addr, sizeof(struct sockaddr));
    
    #endif
    }
    
    

    UI就不多做介绍了,控制器里只有一个显示接收和发送消息内容列表的tableView及一个编辑消息的输入框textField。大概就这些内容,只是个简易的demo,只实现了接收发送文字消息的功能,并没有做更多优化

    3、测试

    分别用模拟器和真机运行,或者可以配合刚才的python程序测试.
    test_host就直接用电脑ip即可

    image
    • 然后手机先发送消息到模拟器上,模拟器就可以根据记录下的手机的主机和端口回复消息了。这里手机连外网也是可以的

    • 效果图如下

      image

    十四.iOS面试题-----网络相关之UDP的特点、UDP的报文结构及差错检测

    TCP(Transmission Control Protocol 传输控制协议)和UDP(User Datagram Protocol 用户数据报协议)同属传输层协议

    一、UDP的特点

    UDP是是面向非连接的协议,传送数据不需要和服务器连接,只需要知道ip和监听端口,不需要链接没有目的的socket,只是将数据报投递出去,不管接收方是否成功接收到,是一种不可靠的传输。
    既然UDP是不可靠数据传输协议,那为什么那么多应用去选择UDP呢?

    1、关于何时、发送什么数据的应用层控制更加精细
    • 只要应用将数据传递给UDP,UDP就会将此数据打包进UDP报文段并立刻将其传递给网络层。
    • 而TCP则是有个拥塞控制机制,以确保数据能够安全传输,而不管可靠传输成功需要用多少时间。
    • 所以有些实时应用,比如微信视频、语音都是更希望数据能够及时发送,为此可以容忍一部分数据丢失,比较适合用UDP
    2、无需连接建立
    • 众所周知,TCP在数据传输前需要经过三次握手,UDP却不需要做任何的准备即可进行数据传输,因此UDP不会引入建立连接的时延。
    • 这也是DNS运行在UDP而不是TCP上的主要原因。
    • 而HTTP协议之所以使用TCP,是因为对于HTTP协议来说,可靠性是至关重要的。
    3、无连接状态
    • TCP需要维护连接状态。此连接状态包括接收和发送缓存、拥塞控制参数以及序号与确认号的参数。(后面如果有时间,会详细说下TCP的拥塞控制方案,对该方案来说,这些状态信息都是必要的)
    • 而UDP不需要维护连接状态,也不用跟踪这些参数
    4、分组首部开销小

    每个TCP报文段都有20字节的首部开销,而UDP仅有8字节的开销

    所以,如非必要,比如电子邮件,远程终端服务,web,以及文件传输,需要可靠地数据传输,会去采用TCP。其余的尤其是对实时性要求高的应用,比如实时视频会议,网络电话,一般都会选用UDP

    二、UDP的报文结构

    image

    应用层数据占用UDP报文段的数据字段。UDP首部只有4个字段,每个字段由2个字节组成,即UDP首部仅有8字节。

    • 端口号:可以使目的主机将应用数据交给运行在目的端系统中端相应进程,执行分用功能。

      image
    • 长度:该字段指示了在UDP报文段中的字节数(首部+数据)

    • 检验和:接收方使用检验和来检查在该报文段中是否出现了差错,即差错检测。

    三、UDP差错检测

    UDP检验和提供了差错检测功能。
    检验和相当于用于确定当UDP报文段从源到达目的地移动时,其中的比特是否发生了改变(比如,由于链路中的噪声干扰或存储在路由器中时的引入问题)。
    发送方的UDP对报文段中的所有16比特字对和进行反码运算,求和时遇到的任何溢出都被回卷。得到的结果被放在UDP报文段中的检验和字段。

    比如,假定有下面三个16比特的字:

    0110011001100000
    0101010101010101
    1000111100001100
    
    

    这些16比特字的前两个之和是:

    1011101110110101
    
    

    再将该和与第三个16比特字相加,得出:

    10100101011000001
    
    

    发现溢出了,该和就要被回卷,即把首位的1加到最后一位去,得出:

    0100101011000010
    
    

    然后对其进行反码运算,所谓反码运算,即是将所有的1换成0,0换成1

    1011010100111101
    
    

    这就是得出的检验和。而在接收方,全部的4个16比特字(包括检验和)加在一起。如果分组中没有引入差错,显然在接收处该和将是1111111111111111。而如果这些比特之一是0,那我们就知道该分组中出现了差错。
    UDP在端到端基础上在运输层提供差错检测,这就是在系统设计中被称颂的端到端原则

    而UDP虽然提供差错检测,但它对差错恢复无能为力。这就需要用到可靠数据传输--TCP了

    十五.iOS面试题-----网络相关之TCP、三次握手、四次挥手、代码实现

    一、TCP的特点和报文结构

    1、面向连接、可靠传输、面向字节流、全双工服务
    2、TCP的报文结构

    TCP报文段由首部字段和一个数据字段组成。
    数据字段包含一块应用数据。最大报文长度MSS(Maximum Segment Size)限制了报文段数据字段的最大长度。MSS选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。
    所以当TCP发送一个大文件(比如一张高清图片)时,通常是将该文件划分为MSS长度的若干块(最后一块除外,通常会小于MSS)。而实际交互式应用通常传送长度小于MSS的数据块。

    image

    如图,与UDP一样,首部包括源端口号目的端口号,用于多路复用/分解来自上层或送到上层应用的数据。TCP首部也同样包括检验和字段
    TCP首部还包含下列字段:

    • 32比特的序号字段Seq(sequence number field)和32比特的确认号字段Ack(acknowledge number field)
    • 16比特的接收窗口字段RW(receive window field),该字段用于流量控制,用于指示接收方愿意接收的字节数量。
    • 4比特的首部长度字段(header length field),该字段指示了以32比特的字为单位的TCP首部长度。由于TCP选项字段的原因,TCP首部长度是可变的。(通常,选项字段为空,所以TCP首部的典型长度就是20字节)
    • 可选和变长的选项字段(option field),该字段用于发送方和接收方协商最大报文段长度(MSS)时,或用作窗口调节因子时使用。
    • 6比特的标志字段(flag field)ACK比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被接收报文段的确认。RSTSYNFIN比特用于连接建立和拆除。
      PSH比特指示接收方应立即将数据交给上层。URG比特用于指示报文段里存在着被发送端的上层实体置为“紧急”的数据。紧急数据的最后一个字节由16比特的紧急数据指针字段指出。当紧急数据存在并给出指向紧急数据尾的指针的时候,TCP必须通知接收端的上层实体。在实践中,PSH、URG和紧急数据指针并没有使用。
    3、序号字段Seq和确认号字段Ack
    • 在TCP通讯中,无论是建立连接,数据传输,友好断开,强制断开,都离不开Seq值和Ack值,它们是TCP传输的可靠保证。

    序号Seq
    TCP把数据看成一个无结构的、有序的字节流。一个报文段的序号因此是该报文段的首字节的字节流编号。
    比如数据流由一个包含100000字节的文件组成,其MSS1000字节,数据流的首字节编号是0。该TCP将为该数据流构建100个报文段。给第一个报文段分配序号0,第二个则是1000,第三个是2000,以此类推。每一个序号被填入到相应TCP报文段首部的序号字段中。
    确认号Ack
    TCP是全双工服务的,因此主机A在向主机B发送数据的同时,也许也在接收主机B的数据。
    主机A填充进报文段的确认号是主机A期望从主机B收到的下一个字节的序号。

    在上个例子中,假如服务端已经接收包含字节0-999的报文段和包含字节2000-2999的报文段,但由于某种原因,还未收到包含字节1000-1999的报文段,那么将仍会等待字节1000(及其后的字节)。因此服务端发给客户端的下一个报文段将在确认号Ack字段中包含1000
    因为TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为累积确认

    二、三次握手

    -数据开始传输前,需要通过 三次握手来建立连接
    事实上我认为,这里称呼三步握手(three-way handshake)才更贴切些

    image

    第一步

    • 客户端的TCP首先向服务端的TCP发送一条特殊的TCP报文段。该报文段不包含应用层数据,该报文段首部中的一个标志位(SYN比特)被置为1,所以该报文段被称为SYN报文段。另外,客户会随机选择一个初始序号client_isn,并将该序号放置于该起始的TCP SYN报文段的序号字段中。
    • 客户端和服务端最开始都处于CLOSED状态,发送过该 SYN报文段后,客户端TCP进入SYN_SENT状态,等待服务端确认并将SYN比特置为1的报文段。

    第二步

    • 收到SYN报文段后,服务端会为该TCP连接分配TCP缓存和变量,服务端TCP会进入SYN_RCVD状态,等待客户端TCP发送确认报文段。
    • 并向该客户端TCP发送允许连接的报文段,该报文段同样不包含应用层数据。该报文段首部的SYN比特被置为1,确认号字段被置为client_isn+1。服务端还会选择自己的初始序号server_isn,放到报文段首部的序号段中。该连接被称为SYNACK报文段

    第三步

    • 收到SYNACK报文段后,客户端也要为该TCP连接分配缓存和变量,客户端TCP进入ESTABLISHED状态,在此状态,客户端就能发送和接收包含有效载荷数据的报文段了。
    • 并向服务端TCP发送一个报文段:这最后一个报文段对服务端的允许连接的报文表示了确认(将server_isn + 1放到报文段首部的确认字段中)。因为连接已经建立了,所以该SYN比特被置为0。这个阶段,可以在报文段负载中携带应用层数据。
    • 收到客户端该报文段后,服务端TCP也会进入ESTABLISHED状态,可以发送和接收包含有效载荷数据的报文段。

    三、四次挥手

    参与TCP连接的两个进程中的任何一个都能终止该连接,当连接结束后,主机中的资源(缓存和变量)会被释放。

    上边说到,SYN和FIN标志位分别对应着TCP连接的建立和拆除。

    image

    如图,

    第一步

    • 客户应用进程发出一个关闭连接的指令。会引起客户端TCP向服务端发送一个特殊的TCP报文段。该报文段即是将首部的一个标志位FIN比特置为1。
    • 同时,客户端进入FIN_WAIT_1状态,等待服务端的带有确认的TCP报文段。

    第二步

    • 收到该报文段后会向客户端发送一个确认报文段。
    • 服务端TCP进入CLOSE_WAIT状态,对应客户端的TIME_WAIT,表示被动关闭。
    • 客户端收到该报文段后,进入FIN_WAIT_2状态,等待服务端的FIN比特置为1的报文段。

    第三步

    • 服务端发送自己的终止报文段,同样是把报文段首部的标志位FIN比特置为1。
    • 服务端TCP进入LAST_ACK状态,等待服务端最后的确认报文段。

    第四步

    • 客户端收到服务端的终止报文段后,向服务端发送一个确认报文段。同时,客户端进入TIME_WAIT状态。
    • 假如ACK丢失,TIME_WAIT状态使TCP客户重传最后的确认报文,TIME_WAIT通常会等待2MSL(Maximum Segment Lifetime 最长报文段寿命)。经过等待后,连接就正式关闭,重新进入CLOSED状态,客户端所有资源将被释放。
    • 服务端收到该报文段后,同样也会关闭,重新进入CLOSED状态,释放所有服务端TCP资源。

    四、一些问题

    1、问:为什么建立连接只用三次握手,而断开连接却要四次挥手?
    • 首先,当客户端数据已发送完毕,且知道服务端也全部接收到了时,就会去断开连接即向服务端发送FIN
    • 服务端接收到客户端的FIN,为了表示接收到了,就会向客户端发送ACK
    • 但此时,服务端可能还在发送数据,并没有关闭TCP窗口的意思,所以服务端的FIN和ACK并不是同步发的,只有当数据发送完了,才会发送FIN
    • 答:服务端的FIN和ACK需要分开发,并不是像三次握手中那样,SYN可以和ACK同步发,所以就需要四次挥手
    2、在四次挥手中,客户端为什么在TIME_WAIT后必须等待2MSL时间呢?

    这个ACK报文段有可能丢失,因而使处在LAST_ACK端的服务端收不到对已发送的FIN报文段的ACK报文段,从而服务端会去不断重传FIN报文段。
    而客户端就能在2MSL时间内收到重传的FIN报文段。接着客户端重传一次确认,重新启动2MSL计时器。直至服务端收到后,客户端和服务端就都会进入CLOSED状态,关闭TCP连接。
    而如果客户端不等待2MSL时间,而是在发送完ACK确认后立即释放资源,关闭连接,那么就无法收到服务端重传的FIN报文段,因而也不会再发送一次ACK确认报文段,这样,服务端就无法正常进入CLOSED状态,资源就一直无法释放了。

    • 答:为了保证客户端发送的最后一个ACK报文段能够到达服务端。
    3、TCP在创建连接时,为什么需要三次握手而不是两次或四次?

    一个简单的例子:

    • 三次握手
      “喂,你听得到吗?”
      “我听得到呀,你听得到我吗?”
      “我能听到你,今天balabala……”
    • 两次握手
      “喂,你听得到吗?”
      “我听得到呀,你听得到我吗?”
      “喂,你听得到吗?”
      “……谁在说话?”
      “喂,你听得到吗?”
      “……”
    • 四次握手
      “喂,你听得到吗?”
      “我听得到呀”“你能听到我吗?”
      “……不想跟傻逼说话”

    之所以不用四次握手的原因很容易理解,就是浪费资源,服务端的SYNACK可以一起发,完全没必要分开两次。

    而如果是两次握手
    客户端发出的第一个连接请求SYN报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务端。本来这是一个早已失效的报文段。但服务端收到此失效的连接请求SYN报文段后,就误认为是客户端再次发出的一个新的连接请求SYN报文段。于是就向客户端发出ACK确认报文段,同意建立连接。假设不采用三次握手,那么只要服务端发出确认,新的连接就建立了。
    由于现在客户端并没有发出建立连接的SYN请求,因此不会理睬服务端的确认,也不会向服务端发送数据。但服务端却以为新的运输连接已经建立,并一直等待客户端发来数据。这样,服务端的很多资源就白白浪费掉了。

    事实上:TCP对有数据的TCP报文段必须确认的原则,所以,客户端对服务端的SYN报文段必须回复一个ACK报文段表示确认。并且,TCP不会为没有数据的ACK超时重传,那么当服务端没收到客户端的ACK确认报文段时,会超时重传自己的SYN报文段,一直到收到客户端的ACK为止。

    • 答:两次握手会可能导致已失效的连接请求报文段突然又传送到了服务端产生错误,四次握手又太浪费资源

    五、代码实现

    2019 iOS面试-----一个基于UDP的简单的聊天Demo(用C语言、python、GCDAsyncUdpSocket来实现UDP通信)
    参考UDP的代码,其实TCP在代码实现上也很相似,首先·socket·初始化时不再用·SOCK_DGRAM·,而是用·SOCK_STREAM·

    fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
    

    TCP服务端要多了一道监听、接受连接的过程:

    int listen_ret = listen(fd,5);
    int listen_socket = accept(_fd,(sockaddr *)&addr,&addr_len);
    
    

    UDP则是多了一道连接的过程:

    int ret = connect(_fd, (struct sockaddr *) &addr, sizeof(addr));
    
    

    然后就是在接收和发送数据时,不用再传主机和端口了。即由recvfromsendto改为recvsend

    send(_fd, [buffer bytes], [buffer length], 0);
    recv(_fd, receiveBuffer, sizeof(receiveBuffer), 0);
    
    

    python的客户端代码如下:

    from socket import *
    serverName = '127.0.0.1'
    serverPort = 12000
    clientSocket = socket(AF_INET,SOCK_STREAM)
    clientSocket.connect((serverName,serverPort))
    sentence = raw_input('Input lowercase:\n')
    clientSocket.send(sentence)
    modifiedSentence = clientSocket.recv(1029)
    print 'From server:\n',modifiedSentence
    clientSocket.close()
    
    

    服务端代码:

    from socket import *
    serverPort = 12000
    serverSocket = socket(AF_INET,SOCK_STREAM)
    serverSocket.bind(('',serverPort))
    serverSocket.listen(1)
    print 'server is ready to receive'
    connectionSocket,addr = serverSocket.accept()
    sentence = connectionSocket.recv(1029)
    capitalizeSentence = sentence.upper()
    print capitalizeSentence
    connectionSocket.send(capitalizeSentence)
    connectionSocket.close()
    

    十六.iOS面试题-----网络相关之TCP进阶:可靠数据传输、流量控制(滑动窗口)、拥塞控制

    一、可靠数据传输

    网络层服务(IP服务)是不可靠的。IP不保证数据报的交付,不保证数据报的按序交付,也不保证数据报中数据的完整性。

    TCP则是在IP服务上创建了一种可靠数据传输服务
    TCP的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、无间隔、无冗余、按序的数据流。即该字节流与连接的另一端发出的字节流是完全相同的。
    作为TCP接收方,有三个与发送和重传有关的主要事件

    1、从上层应用数据接收数据

    将数据封装到一个报文段中,并把报文段交付给IP。每个报文段都包含一个序号Seq,即该报文段第一个数据字节的字节流编号。如果定时器还没有为其他报文段而运行,则启动定时器(即不是每条报文段都会启动一个定时器,而是一共只启动一个定时器),定时器的过期间隔是TimeoutInterval
    是由EstimatedRTT和DevRTT计算得来的:TCP的往返时间的估计与超时

    2、超时

    TCP通过重传引起超时的报文段来响应超时事件,然后重启定时器。

    而发送端超时有两种情况:发送数据超时,接收端发送ACK超时。这两种情况都会导致发送端在TimeoutInterval内接收不到ACK确认报文段。

    • 1、如果是发送数据超时,直接重传即可。
    • 2、而如果是接收端发送ACK超时,这种情况接收端实际上已经接收到发送端的数据了。那么当发送端超时重传时,接收端会丢弃重传的数据,同时再次发送ACK。

    而如果在TimeoutInterval后接收到了ACK,会收下ACK,但不做任何处理

    • TCP不会为没有数据的ACK超时重传

    以下两种情况:

    • 1、如果在发送两条或多条数据报文段都超时,那么只会重传序号最小的那个,并重启定时器。只要其余报文段的ACK在新重启的定时器超时前到达,就不会重传。
    • 2、如果发送序号为100120的两条数据报文段,序号100的ACK丢失,但收到了序号120的ACK,由于累积确认机制,可以得出接收方已经接收到了序号100的报文段,这种情况也不会去重传。
    3、接收到ACK

    用TCP状态变量SendBase指最早未被确认的字节的序号。则SendBase - 1 指接收方已正确按序接收到的数据的最后一个字节的序号。
    当收到ACK确认报文段后,会将ACK的值Y与SendBase比较。TCP采用累计确认的方法,所以Y确认来字节编号在Y之前的所有字节都已经收到。如果YSendBase小,不用理会;而如果YSendBase大,则该ACK是在确认一个或多个先前未被确认的报文段,因此要更新SendBase变量,如果当前还有未被确认的报文段,TCP还要重启定时器。

    通过超时重传,能保证接收到的数据是无损坏、无冗余的数据流,但并不能保证按序。

    而通过TCP滑动窗口,能够有效保证接收数据有序

    二、流量控制

    TCP连接的双方主机都会为该TCP连接分配缓存和变量。当该TCP连接收到正确、按序的字节后,就将数据放入接收缓存。上层的应用进程会从该缓存中读取数据,但不必是数据一到达就立即读取,因为此时应用程序可能在做其他事务。而如果应用层读取数据相对缓慢,而发送方发送得太多、太快,发送的数据就会很容易地使该连接的接收缓存溢出。

    所以,TCP为应用程序提供了流量控制服务(flow-control service),以消除发送方使接收方缓存溢出的可能性。

    流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。

    作为全双工协议,TCP会话的双方都各自维护一个发送窗口和一个接收窗口(receive window)的变量来提供流量控制。而发送窗口的大小是由对方接收窗口来决定的,接收窗口用于给发送方一个指示--该接收方还有多少可用的缓存空间。

    image
    1、发送窗口

    发送方的发送缓存内的数据都可以被分为4类:

    1. 已发送,已收到ACK
    2. 已发送,未收到ACK
    3. 未发送,但允许发送
    4. 未发送,但不允许发送

    则2和3属于发送窗口

    • 发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界
    2、接收窗口

    接收方的缓存数据分为3类:
    1.已接收
    2.未接收但准备接收
    3.未接收而且不准备接收
    则2 属于接收窗口(这里的接收指接收数据并确认)

    • 接收窗口只有在前面所有的报文段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,会先接收下来,接收窗口不会移动,并不对后续字节发送ACK确认报文,以此确保发送端会对这些数据重传。

    我们定义以下变量:

    • LastByteRead:接收方应用程序读取的数据流的最后一个字节编号。可以得知,这是接收缓存的起点
    • LastByteRcvd:从网络中到达的并且已放入接收缓存中的数据流的最后一个自己的的编号。

    可以得知:LastByteRcvd - LastByteRead <= RcvBuffer(接收缓存大小)
    那么接收窗口rwnd =RcvBuffer - (LastByteRcvd - LastByteRead
    rwnd是随时间动态变化的,如果rwnd为0,则意味着接收缓存已经满了。
    接收端在回复给发送端的ACK中会包含该rwnd,发送端则会根据ACK中的接收窗口的值来控制发送窗口。

    有一个问题,如果当发送rwnd为0的ACK后,发送端停止发送数据。等待一段时间后,接收方应用程序读取了一部分数据,接收端可以继续接收数据,于是给发送端发送报文告诉发送端其接收窗口大小,但这个报文不幸丢失了,我们知道,不含数据的ACK是不会超时重传的,于是就出现发送端等待接收端的ACK通知||接收端等待发送端发送数据的死锁状态。

    为了处理这种问题,TCP引入了持续计时器(Persistence timer),当发送端收到对方的rwnd=0ACK通知时,就启用该计时器,时间到则发送一个1字节的探测报文,对方会在此时回应自身的接收窗口大小,如果结果仍未0,则重设持续计时器,继续等待。

    三、拥塞控制

    TCP除了可靠传输服务外,另一个关键部分就是拥塞控制
    TCP让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。
    可能有三个疑问:
    1、TCP发送方如何感知网络拥塞?
    2、TCP发送方如何限制其向连接发送流量的速率?
    3、发送方感知到网络拥塞时,采用何种算法来改变其发送速率?
    这就是TCP的拥塞控制机制
    前边说到,TCP连接的每一端都是由一个接收缓存、一个发送缓存和几个变量(LastByteReadLastByteRcvdrwnd等)组成。而运行在发送方的TCP拥塞控制机制会跟踪一个额外的变量,即拥塞窗口cwnd(congestion window)。它对一个TCP发送方能向网络中发送流量的速率进行了限制。
    发送方中未被确认的数据量不会超过cwndrwnd的最小值:min(rwnd,cwnd)

    1、TCP发送方如何感知网络拥塞?

    冗余ACK(duplicate ACK):就是再次确认某个报文段的ACK,而发送方先前已经收到对该报文段的确认。

    冗余ACK的产生原因:
    • 1.当接收端接收到失序报文段时,即该报文段序号大于下一个期望的、按序的报文段,检测到数据流中的间隔,即由报文段丢失,并不会对该报文段确认。TCP不使用否定确认,所以不能向发送方发送显式的否定确认,为了使接收方得知这一现象,会对上一个按序字节数据进行重复确认,这也就产生了一个冗余ACK
    • 2.因为发送方经常发送大量的报文段,如果其中一个报文段丢失,可能在定时器过期前,就会收到大量的冗余ACK。一旦收到3个冗余ACK(3个以下很可能是链路层的乱序引起的,无需处理),说明在这个已被确认3次的报文段之后的报文段已经丢失,TCP就会执行快速重传,即在该报文段的定时器过期之前重传丢失的报文段。
    将TCP发送方的丢包事件定义为:要么出现超时,要么收到来自接收方的3个冗余ACK

    当出现过度的拥塞时,路由器的缓存会溢出,导致一个数据报被丢弃。丢弃的数据报接着会引起发送方的丢包事件。那么此时,发送方就认为在发送方到接收方的路径上出现了网络拥塞

    2、TCP发送方如何限制其向连接发送流量的速率?
    • 当出现丢包事件时:应当降低TCP发送方的速率。
    • 当对先前未确认报文段的确认到达时,即接收到非冗余ACK时,应当增加发送方的速率。
    3、发送方感知到网络拥塞时,采用何种算法来改变其发送速率?

    TCP拥塞控制算法(TCP congestion control algorithm)
    包括三个主要部分:慢启动、拥塞避免、快速恢复,其中快速恢复并非是发送方必须的,慢启动和拥塞避免则是TCP强制要求的

    • 1、慢启动
      当一条TCP连接开始时,拥塞窗口cwnd的值通常置为一个MSS的较小值,这就使初始发送速率大约为MSS/RTT(RTT:往返时延,报文段从发出到对该报文段的确认被接收之间的时间量)。
      而对TCP发送方来说,可用带宽可能比MSS/RTT大得多,TCP发送方希望迅速找到可用带宽的数量。因此,在慢启动状态,cwnd以一个MSS的值开始并且每当收到一个非冗余ACK就增加一个MSS
    image

    如图,最初cwnd值为1MSS,发送一个报文段M1。收到M1的确认后,cwnd增加为2MSS,这时可以发送两个报文段M2M3。收到这两个报文段的确认后,cwnd则增加为4MSS,可以发送四个报文段,以此类推...

    因此,TCP虽然发送速率起始慢,但在慢启动阶段以指数增长。

    这种指数增长很显然不是无限制的,那么何时结束呢?
    如果出现丢包事件,TCP发送方将ssthresh(慢启动阈值)设置为cwnd/2

    • 发生由超时引起的丢包事件,并将cwnd重置为1MSS,重启慢启动

    • 当TCP发送方的cwnd值达到或超过ssthresh,再继续翻倍显然不合适。这时将结束慢启动转移到拥塞避免模式。

    • TCP发送方检测到3个冗余ACK,会结束慢启动,并快速重传,即在该报文段的定时器过期之前重传丢失的报文段。且进入快速恢复状态。

    • 2、拥塞避免
      一旦进入拥塞避免状态,cwnd的值大约是上次遇到拥塞时的值的一半,即距离拥塞并不遥远。因此,TCP无法每过一个RTT就将cwnd翻倍。而是每个RTT只增加1MSS,即每收到一个非冗余ACK,就将cwnd增加1/cwnd。即假如此时cwnd10MSS,则每收到一个非冗余ACK,cwnd就增加1/10MSS,在10个报文段都收到确认后,拥塞窗口的值就增加了1MSS

      image

      那么何时结束拥塞避免的线性增长(每RTT 1MSS)呢?
      和慢启动一样,如果出现丢包事件,TCP发送方将ssthresh(慢启动阈值)设置为cwnd/2(加法增大, 乘法减小)

    • 发生由超时引起的丢包事件,拥塞避免和慢启动处理的方式相同。即TCP发送方将ssthresh(慢启动阈值)设置为cwnd/2,并将cwnd重置为1MSS,重启慢启动

    • TCP发送方检测到3个冗余ACK,cwnd为原来的一半加上3MSS,进入快速恢复状态。

      image
    • 3、快速恢复

    快速恢复是由3个冗余ACK引起的。
    在快速恢复中,对引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余ACK,cwnd增加1个MSS。最终,当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥塞避免状态。
    如果出现超时,和之前一样,即TCP发送方将ssthresh(慢启动阈值)设置为cwnd/2,并将cwnd重置为1MSS,重启慢启动

    快速恢复并非是必须的。

    TCP的拥塞控制是:每个RTT内cwnd线性(加性增)增加1MSS,然后出现3个冗余ACK事件时cwnd减半(乘性减),因此TCP拥塞控制常被称为加性增,乘性减拥塞控制方式。

    十七.iOS面试题-----网络相关之Cookie和Session

    一、Cookie

    这里有说到,HTTP协议是无状态的,服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
    基于HTTP这种特点,就产生了cookie/session

    1、用户与服务器的交互:Cookie

    cookie主要是用来记录用户状态,区分用户,状态保存在客户端

    image
    • 1.首次访问amazon时,客户端发送一个HTTP请求到服务器端 。服务器端发送一个HTTP响应到客户端,其中包含Set-Cookie头部
    • 2.客户端发送一个HTTP请求到服务器端,其中包含Cookie头部。服务器端发送一个HTTP响应到客户端
    • 3.隔段时间再去访问时,客户端会直接发包含Cookie头部的HTTP请求。服务器端发送一个HTTP响应到客户端

    如图可知,cookie技术有4个组件:

    • 1.在HTTP响应报文中的一个cookie首部行
    • 2.在HTTP请求报文中的一个cookie首部行
    • 3.在用户端系统中保留一个cookie文件,并由用户的浏览器进行管理
    • 4.位于Web站点的一个后端数据库

    也就是说,cookie功能需要浏览器的支持。如果浏览器不支持cookie(如大部分手机中的浏览器)或者把cookie禁用了,cookie功能就会失效。

    2、cookie的修改和删除

    在修改cookie的时候,只需要新cookie覆盖旧cookie即可,在覆盖的时候,由于Cookie具有不可跨域名性,注意name、path、domain需与原cookie一致
    删除cookie也一样,设置cookie的过期时间expires为过去的一个时间点,或者maxAge = 0(Cookie的有效期,单位为秒)即可

    3、cookie的安全

    事实上,cookie的使用存在争议,因为它被认为是对用户隐私的一种侵害,而且cookie并不安全
    HTTP协议不仅是无状态的,而且是不安全的。使用HTTP协议的数据不经过任何加密就直接在网络上传播,有被截获的可能。使用HTTP协议传输很机密的内容是一种隐患。

    • 如果不希望Cookie在HTTP等非安全协议中传输,可以设置Cookie的secure属性为true。浏览器只会在HTTPS和SSL等安全协议中传输此类Cookie。
    • 此外,secure属性并不能对Cookie内容加密,因而不能保证绝对的安全性。如果需要高安全性,需要在程序中对Cookie内容加密、解密,以防泄密。
    • 也可以设置cookieHttpOnly,如果在cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS(跨站脚本攻击)攻击

    二、Session

    除了使用Cookie,Web应用程序中还经常使用Session来记录客户端状态。Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。

    Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上
    客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。

    image

    如图:

    • 当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为SessionId
    • 如果已包含则说明以前已经为此客户端创建过session,服务器就按照SessionId把这个session检索出来,使用(检索不到,会新建一个)
    • 如果客户端请求不包含SessionId,则为此客户端创建一个session并且生成一个与此session相关联的SessionIdSessionId的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个SessionId将被在本次响应中返回给客户端保存。
    • 保存这个SessionId的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。但cookie可以被人为的禁止,则必须有其他机制以便在cookie被禁止时仍然能够把SessionId传递回服务器。

    三、Cookie 和Session 的区别:

    1、cookie数据存放在客户的浏览器上,session数据放在服务器上。

    2、cookie相比session不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。

    3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie

    4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。而session存储在服务端,可以无限量存储

    5、所以:将登录信息等重要信息存放为session;其他信息如果需要保留,可以放在cookie

    十八.iOS面试题-----网络相关之IP协议、IP数据报分片、IPv4编址、网络地址转换(NAT)

    <meta charset="utf-8">

    之前有说到OSI七层协议中的应用层(HTTP协议)、传输层(TCP协议、UDP协议),在传输层之上就是网络层,网络层负责IP数据报的产生以及IP数据包在逻辑网络上的路由转发,网络层分为三个组件:

    image
    • 1、IP协议
    • 2、路由选择协议,它决定了数据报从源到目的地所流经的路径
    • 3、ICMP协议 (Internet Control Message Protocol, 因特网控制报文协议),报告数据报中的差错和对某些网路层信息请求进行响应对设施。
    网络层和传输层的区别
    • 网络层只是根据网络地址将源结点发出的数据包传送到目的结点(点到点),其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。
    即网络层提供了主机之间的逻辑通信
    • 而传输层则负责将数据可靠地传送到相应的端口(端到端),传输层提供了主机应用程序进程之间的端到端的服务。传输层利用网络层提供的服务,并通过传输层地址提供给高层用户传输数据的通信端口,使高层用户看到的只是在两个传输实体间的一条端到端的、可由用户控制和设定的、可靠的数据通路。
    即传输层为运行在不同主机上的进程之间提供了逻辑通信

    一、IP协议

    IP协议是TCP/IP核心协议。

    1、IP协议的数据报格式(IPv4)
    image
    • 版本号
      规定了数据报的IP协议版本(IPv4还是IPv6)。不同的IP版本使用不同的数据报格式 ,上图是IPv4的数据报格式
    • 首部长度
      大多数IP数据报不包含选项,所以一般IP数据报具有20字节的首部。
    • 服务类型
      使不同类型的IP数据报能相互区别开来。
    • 数据报长度
      整个IP数据报的长度利用首部长度和总长度就可以是算出IP数据报中数据内容的起始地址。该字段长度为16比特,所以IP数据报最长可达2^16=65535字节,而事实上,数据报很少有超过1500字节的
    • 标识、标志、片偏移字段
      这三个字段与IP分片有关。此外,IPv6不允许在路由器上对分组分片
    • 生存时间TTL
      用来确保数据报不会永远在网络中循环。设置该数据报可以经过的最多的路由器数。指定了数据报的生存时间,经过一个路由器,它的值就减1,当该字段为0时,数据报就被丢弃
    • 协议
      该字段只有在一个IP数据报到达其目的地才有用。该字段值指示了IP数据报的数据部分应交给哪个特定的传输层协议,比如,值为6表明要交给TCP,而值为17则表明要交给UDP

    • 首部检验和
      与UDP/TCP的检验和不同,这个字段只检验数据报的首部,不包括数据部分。

    • 选项字段
      是一个可变长字段,选项字段一直以4字节作为界限。这样就可以保证首部始终是4字节的整数倍。很少被用到
    • 源IP和目的IP
    记录源IP地址,目的IP地址
    
    
    • 数据

    二、IP数据报分片

    一个链路层帧能承载的最大数据量叫做最大传送单元(Maximun Transmission Unit,MTU),即链路层的MTU限制着IP数据报的长度。
    问题是在不同的链路上,可能使用不同的链路层协议,且每种协议可能具有不同的MTU。
    假定在某条链路上收到一个IP数据报,通过检查转发表确定出链路,且出链路的MTU比该IP数据报的长度要小,如何将这个过大的IP数据报压缩进链路层帧的有效载荷字段呢?

    解决方案就是分片:将IP数据报中的数据分片为两个或更多个较小的IP数据报,用单独的链路层帧封装这些较小的IP数据报,然后向出链路上发送这些帧,每个这些较小的数据都称为片(fragment)

    片在到达目的地传输层前需要重新组装。

    实际上,TCP和UDP都希望从网络层上收到完整的未分片的报文。IPv4的数据报重组工作是在端系统中,而不是在网络路由器中。
    当一台目的主机从相同源收到一系列数据时,需要确定这些数据报中的某些是否是一些原来较大的数据报中的片。而如果是片的话,需要进一步确定何时收到最后一片,并且如何将这些片拼接到一起以形成初始的数据报。从而就用到了前边说到的IPv4数据报首部中的标识、标志、片偏移 字段。

    • 1、当生成一个数据报时,发送主机在为该数据报设置源和目的地址的同时在贴上标识号,发送主机通常将为它发送的每个数据报标识号加1
    • 2、当某路由器需要对一个数据报分片时,形成的每个数据报(即片)具有初始数据报的源地址目的地址标识号
    • 3、当目的地从同一发送主机收到一系列数据报时,它能够检查数据报的标识号以确定哪些数据报实际上是同一较大数据报的片
    • 4、由于IP协议是不可靠服务,一个或者多个片可能永远到达不了目的地。为了让目的主机绝对相信它已收到初始数据报的最后一个片,最后一个片的标志比特被设为0,其余被设为1
    • 5、为了让目的主机确定是否丢失了一个片,并且能按照正确的顺序重新组装片,使用偏移字段指定该片应放在初始IP数据报的哪个位置>
      此外,如果有一个片或者多个片未能到达,则该不完整的数据报将会被丢弃且不会交给传输层。但如果传输层正使用着TCP,则TCP将通过让源以初始数据来重传数据。因为IP层没有超时重传机制,所以会重传整个数据报,而不是某个片

    三、IPv4编址

    1、IP地址

    一台主机通常只有一条链路连接到网络,当主机上的IP想发送一条数据报时,就在该链路上发送。主机与物理链路之间的边界叫做接口(interface)
    而路由器的任务是从链路上接收数据报并从某些其他链路转发出去,路由器必须有两条或更多链路与其连接,路由器与它的任意一条链路之间的边界也叫做接口。即一台路由器会有多个接口,每个接口有其链路。
    因为每台主机与路由器都能发送和接收IP数据报,IP要求每台主机和路由器接口都有自己的IP地址。因此,一个IP地址技术上是与一个接口相关联的,而不是与包括该接口的主机或路由器相关联的。

    2、子网

    每个IP地址(IPv4)长度为32比特(4字节),按点分十进制记法书写,即地址中的每个字节都用它的十进制形式书写,各字节间以点.隔开,比如193.32.122.30
    在因特网上的每台主机和路由器上的每个接口,必须有一个全球唯一的IP地址(NAT后的接口除外)。这些地址不能自由选择,一个接口的IP地址的一部分需要由其连接的子网来决定。

    image

    如图,一台路由器有三个接口,连接7台主机。左侧的三台主机以及连接它们的路由器的接口,都有一个形如223.1.1.x的IP地址。即在它们的IP地址中,最左侧的24比特是相同的。

    image

    互联左侧这三个主机接口与1个路由器接口的网络形成1个子网(subnet)(也被称为IP网络或直接称为网络)。IP编址为这个子网分配一个地址:223.1.1.0/24,其中的/24记法,有时称为子网掩码(network mask),指示了32比特中的最左侧24比特定义了子网地址。任何连接到该子网的主机都要求其地址具有223.1.1.x的形式。同样图中下侧和右侧也是子网,分别为223.1.3.0/24223.1.2.0/24

    image

    上图显示了3台通过点对点链路彼此互联的路由器,这里出现了6个子网。
    一个具有多个以太网段和点对点链路的组织将具有多个子网,在给定子网上的所有设备都具有相同的子网地址。
    虽然在理论上来说,不同子网可以有完全不同的子网地址。但上图可以看出,这6个子网在前16个比特是一致的,都是223.1

    3、无类别域间路由选择(CIDR)

    因特网的地址分配策略被称为无类别域间路由选择(CIDR)(也被称为无分类编址,以区分于分类编址)。对于子网寻址,32比特的IP地址被分为两部分,也是点分十进制数形式a.b.c.d/x,其中x指示了地址的第一部分中的比特数,又被称为该地址的前缀(prefix)
    一个组织通常被分配一块连续的地址,即具有相同前缀的地址。在该组织外的的路由器仅考虑前面的前缀比特x,这相当大地减少了在这些路由器中转发表的长度,形式为a.b.c.d/x单一表项足以将数据报转发到该组织内的任何目的地。

    image

    如图,200.23.16.0/20下有8个组织,分别是200.23.16.0/23200.23.30.0/23,每个组织有自己的子网。而外界不需要知道这8个组织。这种使用单个网络前缀通告多个网络的能力通常称为地址聚合,也称为路由聚合路由摘要

    4、分类编址

    在CIDR被采用之前,IP地址的网络部分被限制长度为8、16、24比特,也就是分类编址(classful addressing)。具有8、16、24比特子网地址的子网被称为A、BC类网络。
    一个C类(/24)子网既能容纳***2[图片上传失败...(image-448ab4-1627110540448)]

    • 2 = 254台主机(其中两个地址预留用于特殊用途),这对于很多组织来说都太小了。
      而一个B类(/16)子网可支持多达
      2[图片上传失败...(image-8feac9-1627110540448)]

    • 2 = 65534台主机,又太大了。
      在分类编址方法下,一个有2000台主机的组织通常被分给一个B类(/16)地址,那么剩下的6万多个地址就浪费掉了。这就会导致
      B类地址空间的迅速损耗以及所分配的地址空间的利用率低
      *。

    此外,255.255.255.255是IP广播地址,当一台主机发出一个目的地址为该地址的数据报时,该报文会交付给同一个网络中的所有主机。

    5、获取主机地址

    某组织一旦获得了一块地址,它就可为本组织内的主机与路由器逐一分配IP地址。系统管理员通常手工配置路由器中的IP地址。主机地址也能手动配置,但更多使用的是动态主机配置协议(DHCP)。DHCP允许主机自动获取IP地址。网络管理员可以配置DHCP,以使某给定主机每次与网络连接时能得到一个相同的IP地址,或者某主机将被分配一个临时的IP地址,该地址在每次与网络连接时也许是不同的。

    6、网络地址转换

    每个IP地址(IPv4)长度为32比特(4字节),因此总共有2[图片上传失败...(image-c49012-1627110540449)]

    个可能的IP地址,约为40亿个。在互联网越来越普及的当下,个人计算机及智能手机等越来越多,这些IP地址显然无法满足人们的需求。
    为了解决IP地址不足的问题,于是就有了网络地址转换(Network Address Translation, NAT),它的思想就是给一个局域网络分配一个IP地址就够了,对于这个网络内的主机,则分配私有地址,这些私有地址对外是不可见的,他们对外的通信都要借助那个唯一分配的IP地址。

    如果从广域网到达NAT路由器的所有数据报都有相同的目的IP地址,那么该路由器如何知道是发送给哪个内部主机的呢?其原理就是使用在NAT路由器上的一张NAT转换表,并在该表内包含了端口号及其IP地址。

    假设一台主机向广域网请求数据,NAT路由器收到该数据报,会为该数据报生成一个新的端口号替换掉源端口号,并将源IP替换为其广域网一侧接口的IP地址。当生成一个新的源端口号时,该端口号可以是任意一个当前未在NAT转换表中的源端口号(端口号字段是16比特,意味着NAT协议可以支持超过60000个并行使用路由器广域网一侧IP地址的连接),路由器中的NAT也在其NAT转换表中增加一表项。

    NAT路由器收到广域网返回的数据时,路由器使用目的IP地址与目的端口号从NAT转换表中检索出该主机使用的IP地址和目的端口号,改写该数据报的目的IP地址和目的端口号,并向该主机转发该数据报

    NAT虽然在近几年得到了很广泛的应用,但也被很多人反对。
    主要是:

    • 1、端口号是用来进程编址的,而不是主机编址的(NAT协议类似 NAT路由器将该家庭网络的主机都当做进程处理,并通过NAT转换表为其分配端口号)
    • 2、路由器通常仅应当处理高达第三层的分组
    • 3、违背端到端原则,即主机彼此之间应当相互直接对话,结点不应当介入修改IP地址与端口号。
    • 4、应当用IPv6来解决IP地址短缺问题

    但不管反对与否,NAT终究已成为当今因特网的一个重要组件

    十九.iOS面试题-----网络相关之IPv6、从IPv4到IPv6的迁移

    由于新的子网和IP结点以惊人的增长率连到因特网上,并被分配唯一的IP地址,32比特的IPv4地址空间即将用尽,为了解决这一问题,IPv6也应运而生。而事实上在20多年前,因特网工程任务组就开始致力于开发一种替代IPv4的协议,即IPv6

    一、IPv6数据报格式

    1、IPv6数据报格式
    image
    • 版本(4比特)
      该字段用于标识IP版本号,IPv6将该字段值设为6。而如果将该字段设为4并不能创建一个合法的IPv4数据报
    • 流量类型(8比特)
      类似于IPv4数据报中的服务类型(TOS)
    • 流标签(20比特)
      流标签字段是IPv6数据报中新增的一个字段,用来标识一条数据报的流类型,以便在网络层区分不同的报文。
    • 有效载荷长度(16比特)

    IPv6数据报中在40定长字节数据报首部后的字节数量,即除了IPv6的数据报首部以外的其他部分的总长度

    • 下一个首部(8比特)
      当IPv6没有扩展报头时,该字段的作用和IPv4的协议字段一样。当含有扩展报头时,该字段的值即为第一个扩展报头的类型
    • 跳限制(8比特)
      与IPv4报文中的TTL字段类似,转发数据报的每台路由器将对该字段的内容减1.如果跳限制计数到达0,则该数据报将被丢弃
    • 源地址和目的地址(各128比特)
      记录源IP地址,目的IP地址
    • 数据

    可以看出,在IPv4数据报中出现的几个字段在IPv6数据报中已不复存在:

    • 分片/重新组装
      IPv6不允许在中间路由器上进行分片和重新组装。这种操作只能在源与目的地上执行。如果路由器收到的IPv6数据报因太大不能转发出链路上的话,路由器会丢掉该数据报,并回一个“分组太大”的ICMP差错报文
    • 首部检验和
      因为运输层和数据链路层协议执行了检验操作,该项功能在网络层就没有必要了,从而更快速处理IP分组
    • 选项
      选项字段不再是标准IP首部的一部分了。但并没有消失,而是可能出现在IPv6首部中由“下一个首部”指出的位置上。即就像TCP或UDP协议首部能够是IP分组中的“下一个首部”,选项字段也能是“下一个首部”

    IPv6相对IPv4最重要的变化如下:

    • 扩大的地址容量
      IPv6将IP地址长度由32比特增加到128比特,这使得理论可存在的IP地址增加到2[图片上传失败...(image-65d1a1-1627111022615)]

      个,约340万亿亿亿亿个,这是一个非常大的数字,确保全世界再也不会用尽IP地址,甚至可以为地球上每一粒沙子都分配一个唯一的IP地址
      除了单播和多播地址外,IPv6没有广播这一说法,而是引入了一种称为任播地址的新型地址,这种地址可以使数据报交付给一组主机中的任意一个

    • 简化高效的40字节首部
      除去共32字节的源地址和目标地址外,首部其余字段只占了8字节

    • 流标签与优先级
      给属于特殊流的分组打上标签,这些特殊流是发送方要求进行特殊处理的流,如一种非默认服务质量或需要实时服务的流

    2、IPv6书写和表达方式

    表述和书写时,把长度为128比特的IPv6地址分成8个16位的二进制段、每一个16位的二进制段用4位的16进制数表示,段间用“:”(冒号)隔开(其书写方法和IPv4的十进制数加“.”不同)。

    例如:1000:0000:0000:0000:000A:000B:000C:000D就是每一个16位的二进制数的段用4位16进制数的段来表示、段间用“:”(冒号)隔开的一个IPv6地址;其中:各个4位16进制数的段中的高位0允许省略;因此,上面的IPv6地址也可以缩写成:1000:0:0:0:A:B:C:D

    为了更进一步简化,IPv6的地址规范中还规定,可以在一个IPv6地址中最多使用一次双冒号(::)来取代IPv6地址中紧密相连的多个全0的16进制数的段(因为如果允许在一个IPv6地址中使用一次以上的双冒号时将无法判断IPv6地址的长度,所以IPv6的地址规范中才规定:在一个IPv6地址中最多只能使用一次双冒号),这样上面的IPv6地址还可以缩写成:1000::A:B:C:D

    双冒号使用的地点可以在IPv6地址的前面、后面或者是中间;例如:对于1000:0:0:0:A:B:0:0这样的一个IPv6地址,可以写成1000::A:B:0:0,也可以写成1000:0:0:0:A:B::;但是不能写成1000::A:B::

    带有端口号的IPV6地址字符串形式,地址部分应当用“[]”括起来,在后面跟着‘:’带上端口号,如 [A01F::0]:8000

    二、从IPv4到IPv6的迁移

    基于IPv4的公共因特网如何迁移到IPv6呢?这是个非常现实的问题
    虽然IPv6使能系统可做成向后兼容,即能接收、发送和路由IPv4数据报,但已部署的IPv4使能系统却不能处理IPv6数据报

    1、双协议栈

    引入IPv6使能结点的最直接方式是双栈方法,即使用该方法的IPv6结点还有完整的IPv4实现,即IPv6/IPv4结点,具有接收和发送IPv4和IPv6两种数据报的能力。
    当与IPv4结点互操作时,IPv6/IPv4结点可使用IPv4数据报;当与IPv6结点互操作时,IPv6/IPv4结点又可使用IPv6数据报。

    IPv6/IPv4结点必须有IPv6与IPv4两种地址。此外,它们还必须能确定另一个结点是否是IPv6使能的或仅IPv4使能的。

    可以使用DNS来解决,若要解析的结点名字是IPv6使能的,则DNS会返回一个IPv6地址,否则返回一个IPv4地址。如果发出DNS请求的结点是仅IPv4使能的,则只返回一个IPv4地址。

    两个IPv6使能的结点不应相互发送IPv4数据报,而如果发送方或接收方任意一个仅为IPv4使能的,则必须使用IPv4数据报。
    这样就会有下面这种情况:

    image

    如图,假如结点A、B、E、F都是IPv6使能的结点,而结点C和D是仅IPv4使能的结点,那么当按A->B->C->D->E->F顺序发送数据报时,AB之间会发IPv6数据报,BC会发IPV4数据报, 由于IPv6数据报特定的字段在IPv4数据报中无对应的部分,这些字段将会丢失。因此,即使E和F之间能发IPv6数据报,从D到达E的IPv4数据报并未含有从A发出的初始IPv6数据报中的所有字段。

    2、隧道

    建隧道是另一种双栈方法,该方法能解决上述问题。
    假定两个IPv6结点要使用IPv6数据报进行交互,但是它们是经由中间IPv4路由器互联的。将两台IPv6路由器中间的IPv4路由器的集合成为一个隧道,如B->C->D->E

    image

    如图,借助于隧道,在隧道发送端的IPv6结点可将整个IPv6数据报放到一个IPv4数据报的数据字段中。于是,该IPv4数据报的地址设为指向隧道接收端的IPv6结点,再发送给隧道中的第一个结点。而隧道中的IPv4路由器在它们之间为该数据报提供路由,就像对待其他IPv4数据报一样,完全不知道该数据报自身就含有一个完整的IPv6数据报。而隧道接收端的IPv6结点最终收到该IPv4数据报,并确定该IPv4数据报中含有一个IPv6数据报,于是提取出该IPv6数据报,然后再为该IPv6数据报提供路由

    3、NAT-PT

    除了双栈和隧道方案外,还有一种NAT-PT(Network Address Translator - Protocol Translator)附带协议转换器的网络地址转换器方案

    传送门:
    iOS面试资料大全)

    相关文章

      网友评论

        本文标题:2021 iOS面试题大全---全方面剖析面试(二)

        本文链接:https://www.haomeiwen.com/subject/bttvmltx.html