动态

详情 返回 返回

大話網絡通信 - 动态 详情

1、術語

併發 vs 並行

  • 併發和並行是相關的概念,但有一些小的區別。併發意味着兩個或多個任務正在取得進展,即使它們可能不會同時執行。例如,這可以通過時間切片來實現,其中部分任務按順序執行,並與其他任務的部分混合。另一方面,當執行的任務可以真正同時進行時,就會出現並行
    簡單説啓動一個線程在一個core上就是並行,啓動兩個線程在一個core上就是併發

異步 vs 同步

  • 如果調用者在方法返回值或引發異常之前無法取得進展,則認為方法調用是同步的。另一方面,異步調用允許調用者在有限的步驟之後繼續進行,並且可以通過一些附加機制 (它可能是已註冊的回調、Future 或消息)來通知方法的完成
    簡單來説Java API層來説的,如下 :
ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
            @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攻擊方式就是建立大量的半連接狀態的請求,然後丟棄

如感興趣,點贊加關注哦!

user avatar mannayang 头像 u_13529088 头像 lu_lu 头像 chaochenyinshi 头像 tuhooo 头像 dengjijie 头像 yangrd 头像 itxiaoma 头像 xiaoxiansheng_5e75673e1ae30 头像 daimajiangxin 头像 wxweven 头像 dreamlu 头像
点赞 24 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.