大话网络通信

2023年3月30日

1、术语

并发 vs 并行

  • 并发和并行是相关的概念,但有一些小的区别。并发意味着两个或多个任务正在取得进展,即使它们可能不会同时执行。例如,这可以通过时间切片来实现,其中部分任务按顺序执行,并与其他任务的部分混合。另一方面,当执行的任务可以真正同时进行时,就会出现并行
    简单说启动一个线程在一个core上就是并行,启动两个线程在一个core上就是并发

异步 vs 同步

  • 如果调用者在方法返回值或引发异常之前无法取得进展,则认为方法调用是同步的。另一方面,异步调用允许调用者在有限的步骤之后继续进行,并且可以通过一些附加机制 (它可能是已注册的回调、Future 或消息)来通知方法的完成
    简单来说Java API层来说的,如下 :
ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future future = executorService.submit(new Callable() {
            @Override
            public Boolean call() throws Exception {
                System.out.println("执行业务逻辑");
                
                // 根据业务逻辑判断给定返回
                return true;
            }
        });

        future.get(); // 同步API,必须等到返回
        if(future.isDone()) {
            future.get();// 异步API,只有执行完,再get结果
        }  
  • 同步 API 可以使用阻塞来实现同步,但这不是必要的。CPU 密集型任务可能会产生类似 于阻塞的行为。一般来说,最好使用异步 API,因为它们保证系统能够进行

非阻塞 vs 阻塞

  • 如果一个线程的延迟可以无限期地延迟其他一些线程,这就是我们讨论的阻塞。一个很好的例子是,一个线程可以使用互斥来独占使用一个资源。如果一个线程无限期地占用资源(例如意外运行无限循环),则等待该资源的其他线程将无法进行。相反,非阻塞意味着没有线程能够无限期地延迟其他线程
  • 非阻塞操作优先于阻塞操作,因为当系统包含阻塞操作时,系统的总体进度并不能得到很好的保证

    死锁 vs 饥饿 vs 活锁

  • 当多个线程在等待对方达到某个特定的状态以便能够取得进展时,就会出现死锁。由于没有其他线程达到某种状态,所有受影响的子系统都无法继续运行。死锁与阻塞密切相关,因为线程能够无限期地延迟其他线程的进程
  • 在死锁的情况下,没有线程可以取得进展,相反,当有线程可以取得进展,但可能有一个或多个线程不能取得进展时,就会发生饥饿。典型的场景是一个调度算法,它总是选择高优先级的任务而不是低优先级的任务。如果传入的高优先级任务的数量一直足够多,那么低优先级任务将永远不会完成
  • 活锁类似于死锁,因为没有线程取得进展。不同之处在于,线程不会被冻结在等待他人进展的状态中,而是不断地改变自己的状态。一个示例场景是,两个线程有两个相同资源可用时。他们每一个都试图获得资源,但他们也会检查对方是否也需要资源。 如果资源是由另一个线程请求的,他们会尝试获取该资源的另一个实例。在不幸的情 况下,两个线程可能会在两种资源之间“反弹”,从不获取资源,但总是屈服于另一种资源

2、BIO vs NIO

BIO

serverSocket.accept(),这里会阻塞
socket.getInputStream.read(),也会阻塞

虽然可以使用了线程池,因为read()方法的阻塞,其实线程池也是不能复用的,说白了,就是需要一个客户端一个线程进行服务

思考:那BIO就没有使用场景了吗?
其实不是,BIO在建立长连接的流式传输场景还是很有用的,比如说HDSF,客户端向DataNode传输数据使用的就是建立一个BIO的管道,流式上传数据的。此时引入一个问题,那HDFS DataNode就不考虑到线程阻塞么?是这样的,其实要知道你不可能多个客户端上传文件都是针对某个DataNode(NameNode会进行选择DataNode),所以线程阻塞的压力是会分摊的。NIO还是擅长小数据量的RPC请求,能接受百万客户端的连接

NIO

NIO中有三个重要组件 : Buffer(ByteBuffer主要使用)、Channel(双向通道,可读可写)和Selector(多路复用选择器)

  1. Buffer
    常用的就是 ByteBuffer,缓冲池,可以作为channel写的单位,也可以接受channel读取的返回里面重要的属性 :position、capacity、flip、limit和hasRemain

    每个channel都需要记录可能切分的消息,因为ByteBuffer不能被多个channel使用,因此需要为每个channel维护一个独立的ByteBuffer。ByteBuffer不能太大,比如一个ByteBuffer 1M的话,需要支持百万连接要1TB内存,因此需要设计大小可变的ByteBuffer
    1、首先分配一个较小的buffer,比如4k,如果发现不够的话,再分配8kb的buffer,将4kb buffer内容拷贝到8kb buffer,有点是消息连续容易处理,缺点是数据拷贝耗费性能
    2、多个数组组成buffer,一个数组不够,把多出来的内容写入新的数组,缺点不连续解析复杂,有点避免了拷贝引起的性能损耗

  2. FileChannel
    FileChannel在同一个JVM中是线程安全的,多个线程写也没有问题,但是在不同的JVM中同时写一个文件就会有问题需要的是 FileLock对文件进行加锁,有独占锁和共享锁
    channel.lock(0, Integer.MAX_VALUE, true),可以锁一定的区间

    RandomAccessFile 可以支持读写文件,Channel 本身是支持读写的,只是看源头是不是支持读写,比如说FileInputStream流获取的channel只能支持读,RandomAccessFile获取的流支持读写

    channel.force(true); 强制将os cache数据刷入到磁盘上

    from.transferTo(position,count,dest);从from channel写到dest channel,从position 开始写,写了count长度
    比如说从本地文件向网络中进行传输
    to.transferFrom(src,position,count); 比如说从网络中写到本地文件,from就是从外界到,src读取,写到to中

    transferTo & transferFrom 底层使用的是零拷贝,零拷贝简单来说其实不走应用层数据复制,但是也是有数据复制的,是在Linux内核层

  3. Selector & SocketChannel
    服务端 ServerSocketChannel
    是通过 ServerSocketChannel和Selector来获取多个连接,每个连接是一个SocketChannel
    将 ServerSocketChannel 注册到 Selector上,如果有连接,selector的select阻塞方法会有事件,生成SelectionKey,每个SelectionKey其实对应一个SocketChannel

    Selectionkey是可以attach对象的,也可以通过 Selectionkey 通过attachment进行对象的获取,这很重要,一般会创建一个对象并和SelectioKey进行关联

    照样是bind,监听OP_ACCEPT,进行isAcceptable、read和write事件(write事件是一次没有写完毕,继续要写)

    客户端 SocketChannel
    是进行connect,监听OP_CONNECT事件,isConnectable,read和write事件

    要注意,SelectionKey,每次迭代是需要删除的,否则重复请求,但是已经处理,就会有问题

    写数据的时候一定要注意,最好不要 while(buffer.hasRemaining()) 一直写,这样会阻塞网络带宽的,影响读取
    写一部分数据,然后关注SelectionKey.OP_WRITE事件,不断selector.select()继续写,写完毕取消写事件的关注

    socketChannel.write(buffer); 写一下,也不一定会把buffer中都写完毕
    if(buffer.hasRemaining()) {
        selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
        selectionKey.attach(buffer);
    }

3、零拷贝

传统IO问题
比如说要将本地磁盘文件往网络中写,磁盘 -> 内核缓冲区 -> 用户缓冲区 -> socket缓冲区 -> 网卡

读磁盘数据 : 用户态 -> 内核态
内核数据写到用户缓冲区 : 内核态 -> 用户态
网卡写数据 : 用户态 -> 内核态

4次数据复制,3次内核切换

通过DirectByteBuffer,MappedByteBuffer,为什么快?
因为他使用direct buffer的方式读写文件内容,称为内存映射。这种方式直接调用系统底层的缓存,没有JVM和系统之间的复制操作,所以效率大大的提升
将堆外内存映射到JVM内存中直接访问
减少一次数据拷贝,用户态与内核态的切换次数没有减少

Linux2.4
Java调用transferTo,要从Java程序的用户态到内核态,磁盘 -> 内核缓冲区 -> 网卡,一次内核切换,两次数据复制

4、Socket参数

SocketChannel参数
SO_RCVBUF和SO_SNDBUF : Socket参数,TCP数据接收缓冲区大小,发送和接受缓冲区,128kb或者256kb
CONNECT_TIMEOUT_MILLIS : 用户在客户端建立连接时,如果在指定毫秒内无法建立连接,会抛出timeout异常
TCP_NODELAY TCP参数,立即发送数据,默认值为Ture(关闭nagle算法)
SO_KEEPALIVE Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性(2个小时)
SO_REUSEADDR : 其实就是比如说ServerSocketChannel连接关闭了,此时跟其他客户端的连接都处于一个timeout状态,重启Netty Server,如果设置了
SO_REUSEADDR 为 true,则会让ServerSocketChannel重新地址端口绑定,否则失败

ServerSocketChannel参数
SO_BACKLOG Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128
TCP三次握手是在ACCEPT之前发生的
1、第一次握手,client发送SYN到server,状态修改为SYN_SEND,server收到,状态修改为SYN_REVD,并将请求放入sync queu队列
2、第二次握手,server回复 SYN + ACK 给client,client收到,状态修改为ESTABLISHED,并发送给ACK给server
3、第三次握手,server收到ack,状态修改为 ESTABLISHED,将请求从sync queue放入accept queue

所以现在出现了半连接队列和全连接队列
在Centos Linux下对应着 /proc/sys/net/ipv4/tcp_max_syn_backlog(512),/proc/sys/net/core/somaxconn(128)

SO_BACKLOG 设置的是全连接

TCP SYNC FLOOD恶意DOS攻击方式就是建立大量的半连接状态的请求,然后丢弃

服务器托管,北京服务器托管,服务器租用 http://www.hhisp.net

hackdl

咨询热线/微信 13051898268