JAVA網路程式設計基礎

小勇勇發表於2021-12-21

IO模型

IO請求的兩個階段(Linux)

  • IO呼叫階段:使用者程式向核心發起系統呼叫
  • IO執行階段:此時使用者進行等待IO請求處理完成返回,此階段分為兩步

    1. 等待資料就緒,並寫入核心緩衝區
    2. 資料從核心緩衝區 到 使用者態緩衝區

      • 核心態:執行作業系統程式,操作硬體
      • 使用者態:執行使用者程式

Linux五種IO模型

1.同步阻塞IO(BIO)

核心只能同時處理一個請求,分兩個階段(即上述的IO執行階段):

  1. 系統呼叫
  2. 資料從核心緩衝區讀取到使用者緩衝區

這個兩個操作都是阻塞的所以只有等這兩個操作都完成後才能處理其他IO

2.同步非阻塞IO(NIO)

程式的請求不會一直等待而是有專門的執行緒來輪詢這些IO程式是否存有資料,但是輪詢過程中會存在著系統呼叫導致的上下問切換,如果請求過多會存在嚴重的系統效能消耗

3.IO多路複用

多路是指多個資料通道,複用指的是一個或多個固定的執行緒來處理每一Socket連線, select poll epoll都是IO多路複用的實現,執行緒一次可以select多個資料通道的資料狀態,解決了NIO效能消耗過重的問題

-檔案描述符fd

檔案描述符(File descriptor)形式上是一個非負整數,是一個索引值,指向核心為每一個程式所維護的該程式所開啟檔案的記錄表.

- select

這個函式會監視3類檔案描述符,分別是writefds,readfds,exceptfds 呼叫select函式時會阻塞,直到select有以上3中描述符檔案就緒或者超時,一旦某個描述符就緒了,會通知程式進行相關的讀寫操作,由於select poll epoll都是同步IO,所以它們都需要在事件就緒後自己負責讀寫.也就是select會阻塞監聽相關事件,直到處理完讀寫操作或者超時後才會解除阻塞.select單個程式能夠監聽的檔案數量是有限的,linux一般預設是1024

int select(int n,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

- poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

poll使用一個pollfd的結構體來傳導需要監聽的事件和要發生的事件,另外poll監聽的檔案描述符個數是沒有限制的

- epoll

​ 不需要輪詢,時間複雜度為O(1)

epoll_create 建立一個白板 存放fd_events
epoll_ctl 用於向核心註冊新的描述符或者是改變某個檔案描述符的狀態。已註冊的描述符在核心中會被維護在一棵紅黑樹上
epoll_wait 通過回撥函式核心會將 I/O 準備好的描述符加入到一個連結串列中管理,程式呼叫 epoll_wait() 便可以得到事件完成的描述符

​ 兩種觸發模式:
​ LT:水平觸發
​ 當 epoll_wait() 檢測到描述符事件到達時,將此事件通知程式,程式可以不立即處理該事件,下次呼叫 epoll_wait() 會再次通知程式。是預設的一種模式,並且同時支援 BlockingNo-Blocking
​ ET:邊緣觸發
​ 和 LT 模式不同的是,通知之後程式必須立即處理事件。
​ 下次再呼叫 epoll_wait() 時不會再得到事件到達的通知。很大程度上減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。只支援 No-Blocking,以避免由於一個檔案控制程式碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。

4.訊號驅動模型

訊號驅動模型並不常用,是一種半非同步IO.當資料準備就緒後,核心會傳送一個SIGIO訊息給應用程式,程式然後開始讀寫訊息.

5.非同步IO

系統呼叫會被立即返回結果,然後讀取寫訊息由非同步完成.

BIO

BIO - Block-IO 阻塞同步的通訊方式

BIO的問題:

阻塞\同步,BIO很依賴於網路,網速不好阻塞時間會很長;每次請求都由程式執行並返回,這是同步的缺陷

BIO的工作流程:

  • 服務端啟動
  • 阻塞等待客戶端連線
  • 客戶端連線
  • 監聽客戶端內容
  • 客戶端斷開
  • 回到第一步

BioServer

public class BioServer {
    public static void main(String[] args) {
        try {
            // 服務端繫結埠
            ServerSocket server  = new ServerSocket(9000);
            while (true) {
                // 建立一個Socket接收連線 - 當沒有時阻塞
                Socket socket = server.accept();
                // 獲取輸入流
                InputStream inputStream = socket.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                String message;
                while (null != (message = reader.readLine())) {
                    System.out.println(message);
                }
                inputStream.close();
                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

BioClient

public class BioClient {
    public static void main(String[] args) {
        try {
            // 建立socket
            Socket socket  = new Socket("localhost",9000);
            // 獲取Socket輸出流
            OutputStream  outputStream = socket.getOutputStream();
            // 輸出流
            outputStream.write("hello socket".getBytes());
            // 關閉
            outputStream.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

多執行緒解決BIO阻塞問

  • 解決了的問題:多個執行緒處理當一個客戶端遲遲不退出時,其他執行緒依然可以處理其它客戶端傳送過來的請求.避免了一個請求阻塞導致其他客戶端請求一直等待的問題
  • 仍然存在問題:加入服務端給定固定執行緒數是10,有10個客戶端建立了連線 但是沒有一個人傳送訊息 那麼10個執行緒將全部阻塞,或者有些客戶端遲遲沒有操作會造成不必要的資源佔用.

多執行緒BioServer程式碼

public class BioServer {
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);
    public static void main(String[] args) {
        ServerSocket serverSocket;
        try {
            serverSocket  = new ServerSocket(9000);
            while (true){
                //new Thread(new BioHandler(serverSocket.accept()){}).start();
                executorService.execute(new BioHandler(serverSocket.accept()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class BioHandler implements Runnable {
    private Socket  socket;

    public BioHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            InputStream input  = socket.getInputStream();
            BufferedReader reader  = new BufferedReader(new InputStreamReader(input));
            String m;
            while (null != (m = reader.readLine())){
                System.out.println(m);
            }
            input.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO

java 1.4版本引入,給予緩衝區面 \ 向通道的io操作
bionio
面向流面向緩衝區(buffer)
阻塞io非阻塞io
同步同步
Selector(選擇器)

緩衝區(Buffer)

緩衝區介紹

緩衝區是一個特定資料型別的容器,有java.nio包定義,所有的緩衝區都是Buffer抽象類的子類

Buffer主要用於和NIO通道進行通訊,資料從通道讀入到緩衝區,再從緩衝區讀取到通道

Buffer就像是一個資料可以儲存多個型別相同的資料

子類

ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer

基本屬性

1.容量(capacity):表示緩衝區的最大容量 一旦建立不能修改
2.限制(limit):第一個不可讀的索引,即位於limit後面的資料不可讀
3.位置(position):下一個要讀取或寫入資料的索引
4.flip:將此時的position設為limit,position置為0 ,一般是從inputChannel將資料讀入到buffer 然後將buffer flip後 為了從buffer中讀取資料outputChannel
5.標記(mark)和恢復(reset):標記是一個索引,通過Buffer.mark()指定一個特定的位置,使用reset方法可以恢復到這個位置

public class BufferSample {
    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println("capacity:" + buffer.capacity());
        System.out.println("limit:" + buffer.limit(10));
        System.out.println("position:" +  buffer.position());
        /**
         * 結果:
         * capacity:1024
         * limit:java.nio.HeapByteBuffer[pos=0 lim=10 cap=1024]
         * position:0
         */

        System.out.println("==============================");
        String str = "hello";
        buffer.put(str.getBytes());
        System.out.println("position:" +  buffer.position());
        /**
         * 結果:
         * position:5
         */

        System.out.println("==============================");
        System.out.println("pos 和 limit之間元素的個數:" + buffer.remaining());
        buffer.mark();
        buffer.put("oo".getBytes());
        System.out.println("reset前position:" +  buffer.position());
        buffer.reset();
        System.out.println("reset後position:" +  buffer.position());
        /**
         * 結果:
         * pos 和 limit之間元素的個數:5
         * reset前position:7
         * reset後position:5
         */

        System.out.println("==============================");
        buffer.rewind();
        System.out.println("position:" + buffer.position());
        /**
         * 結果:
         * position:0
         */

        System.out.println("==============================");
        byte[] dst = new byte[3];
        buffer.get(dst);
        System.out.println(new String(dst));
        System.out.println("position:" + buffer.position());
        /**
         * 結果:
         * hel
         * position:3
         */

        System.out.println("==============================");
        //將此時的position轉為limit,並將position置為0 - 一般flip以後就是開始讀取緩衝區類
        buffer.flip();
        System.out.println("capacity:" + buffer.capacity());
        System.out.println("limit:" + buffer.limit());
        System.out.println("position:" +  buffer.position());
        byte[] b = new byte[buffer.limit()];
        buffer.get(b,0,2);
        System.out.println(new String(b,0,2));
        /**
         * 結果:
         * capacity:1024
         * limit:3
         * position:0
         * he
         */
    }
}

直接/非直接緩衝區

  • 直接緩衝區:程式直接操作物理對映檔案
  • 非直接緩衝區:jvm - 作業系統 - 實體記憶體

通道(Channel)

Channel:類似於流,但是Channel不能直接訪問資料,只能與緩衝區進行互動

通道主體實現類

1.FileChannel:用於讀取 寫入 對映和操作檔案的通道
2.DataGramChannel:通過UDP讀取網路中的資料通道
3.SocketChannel:通過Tcp讀寫通道的資料
4.ServerSocketChannel:可以監聽新進入的Tcp連線,對每一個新連線建立一個SocketChannel

提供getChannel()方法的類

1.FileInputStream
2.FileOutputStream
3.RandomAccessFile
4.Socket
5.ServerSocket
6.DataGramSocket

通道直接傳輸

1.transferFrom()
2.transferTo()

public class ChannelSimple {
    /**
     * 利用通道完成檔案複製(非直接緩衝區)
     */
    public static void FileNoDirectBufferTest(){
        try {
            //建立輸入輸出流
            FileInputStream inputStream = new FileInputStream("../test.txt");
            FileOutputStream outputStream = new FileOutputStream("../test2.txt");
            //根據流獲取通道
            FileChannel inputChannel = inputStream.getChannel();
            FileChannel outputChannel = outputStream.getChannel();
            //建立緩衝區
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //從通道讀取資料到緩衝區
            while (-1 != inputChannel.read(byteBuffer)){
                //limit - position,position - 0
                byteBuffer.flip();
                //將緩衝區中的資料寫出
                outputChannel.write(byteBuffer);
                byteBuffer.clear();
            }
            outputChannel.close();
            inputChannel.close();
            outputStream.close();
            inputStream.close();
        } catch (IOException  e) {
            e.printStackTrace();
        }
    }

    /**
     * 利用直接緩衝區完成檔案複製(記憶體對映檔案)
     * @throws IOException
     */
    public static void FileMpDirectBufferTest() throws IOException{
        //建立通道
        FileChannel inputChannel = FileChannel.open(Paths.get("../test.txt"), StandardOpenOption.READ);
        FileChannel outputChannel = FileChannel.open(Paths.get("../test2.txt"),StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);

        //記憶體對映檔案
        MappedByteBuffer inputBuffer = inputChannel.map(FileChannel.MapMode.READ_ONLY,0,inputChannel.size());
        MappedByteBuffer outputBuffer =  outputChannel.map(FileChannel.MapMode.READ_WRITE,0,inputChannel.size());

        //直接對緩衝區進行資料讀寫操作
        byte [] dst = new byte[inputBuffer.limit()];
        inputBuffer.get(dst);
        outputBuffer.put(dst);

        outputChannel.close();
        inputChannel.close();
    }

    /**
     * 利用直接緩衝區複製
     * @throws IOException
     */
    public static void FileDirectBufferTest() throws IOException {
        //建立通道
        FileChannel inputChannel = FileChannel.open(Paths.get("../test.txt"), StandardOpenOption.READ);
        FileChannel outputChannel = FileChannel.open(Paths.get("../test2.txt"),StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);

        //inputChannel.transferTo(0,inputChannel.size(),outputChannel);
        //等同 上面的註釋
        outputChannel.transferFrom(inputChannel,0,inputChannel.size());
        outputChannel.close();
        inputChannel.close();
    }
}

分散讀取和聚集寫入

  • 分散讀取(Scatter):將一個Channel 中的資料分散儲存到多個Buffer
  • 聚集寫入(Gather):將多個Buffer中的資料寫入同一個Channel
public class ScatterAndGather {
    public static void main(String[] args) {
        try {
            //建立輸入輸出流
            FileInputStream inputStream = new FileInputStream("../test.txt");
            FileOutputStream outputStream = new FileOutputStream("../test2.txt");
            //根據流獲取通道
            FileChannel inputChannel = inputStream.getChannel();
            FileChannel outputChannel = outputStream.getChannel();
            //建立緩衝區
            ByteBuffer byteBuffer1 = ByteBuffer.allocate((int)inputChannel.size()/2);
            ByteBuffer byteBuffer2 = ByteBuffer.allocate((int)inputChannel.size()/2);

            ByteBuffer[] byteBuffers = new ByteBuffer[]{byteBuffer1,byteBuffer2};
            //從通道讀取資料到緩衝區 - 分散寫入
            while (-1 != inputChannel.read(byteBuffers)){
                for (ByteBuffer buffer:byteBuffers){
                    //limit - position,position - 0
                    buffer.flip();
                }
                //聚集寫出
                for (ByteBuffer buffer:byteBuffers) {
                    //將緩衝區中的資料寫出
                    outputChannel.write(buffer);
                    buffer.clear();
                }
            }
            outputChannel.close();
            inputChannel.close();
            outputStream.close();
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

選擇器(Selector)

Selector一般被稱為選擇器,也被稱為多路複用器.用於檢查一個或多個通道是否處於可讀\可寫,如此可以實現一個執行緒管理多個Channel

使用Selector帶來的好處

使用更少的執行緒來處理Channel,可以防止上下文切換帶來的效能消耗

可以多路複用的Channel

可以被選擇(多路複用)的Channel都繼承自SelectableChannel

                         SelectableChannel
                               ||
                    AbstractSelectableChannel
                  ||           ||            ||
      DataGramChannel    SocketChannel     ServerSocketChannel

所以FileChannel不適應與Selector,即不能切換為非阻塞模式

Selector使用基本步驟

1.建立Selector: Selector selector = Selector.open();
2.設定為非阻塞為:

`channel.configureBlocking(false);`

3.註冊ChannelSelector:

/**
* 引數-1:要註冊到的多路複用器
* 引數-2:是一個"interest集合",即要監聽事件的集合(有以下四種)
* OP_CONNECT 連線    
* OP_ACEEPT  接收
* OP_READ    讀
* OP_WRITE   寫
*/
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
如果要監聽多種事件如下:
SelectionKey key = channel.register(selector,SelectionKey.OP_CONNECT | SelectionKey.OP_READ); 

4.然後就 連線就緒 | 接收就緒 | 讀就緒 | 寫就緒

Selector主要方法

方法描述
Set<SelectKey> keys()返回所有SelectionKey集合,代表 註冊在這個Selector上Channel
Set<SelectKey> selectedKeys()返回已選擇了的(即有io操作的)SelectionKey
int select()監控所有註冊了的Channel,如果有需要 io的操作時會將對應的selectKey加入到 selectedKeys集合中,返回的則是被選擇 (有io操作的)Channel數量,這個操作時阻 塞的即只有被選擇的Channel數量>=1才 返回
int select(timeout)有超時時長,一直沒有io操作的Channel出現, 到達timeout出現的時間後將自動返回
int selectNow()無阻塞 立即返回
Selector wakeUp()使正在select()立即返回
void close()關閉

SelectionKey主要方法

SelectionKey表示ChannelSelector之間的關係,ChannelSelector註冊就會產生一個SelectionKey

方法描述
int interestOps()感興趣事件的集合 boolean isInterested = interestSet & SelectionKey.OP_CONNECT ...
int readyOps()獲取通道準備好就緒的操作
SelectableChannel channel()獲取註冊通道
Selector selector()獲取選擇器
boolean isConnectable()檢測Channel中是否有連線事件就緒
boolean isAcceptable()檢測Channel中是否有接收事件就緒
boolean isReadaable()檢測Channel中是否有讀事件就緒
boolean isWriteable()檢測Channel中是否有寫事件就緒
Object attach()將一個物件附著到SelectionKey上, 主要是一些用於標識的資訊
Object attachment()獲取註冊資訊 也可以在Channel註冊的時候附著資訊 SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
void cancel()請求取消此鍵的通道到其選擇器的註冊

NioServer

public class NioServer {
    public static void main(String[] args) throws IOException {
        Integer flag = 0;
        //建立服務端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //繫結埠
        serverSocketChannel.bind(new InetSocketAddress(9021));
        //建立選擇器
        Selector selector = Selector.open();
        //註冊 接收
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //有一個事件時就操作
        while (selector.select() > 0) {
            //獲取事件集合
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //如果是接收就緒
                if (selectionKey.isAcceptable()) {
                    //獲取客戶端連線
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //切換成非阻塞
                    socketChannel.configureBlocking(false);
                    //註冊在多路複用器上 讀
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    //讀事件
                } else if (selectionKey.isReadable()) {
                    //獲取客戶端連線
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //設定快取
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    while (-1 != (len = socketChannel.read(byteBuffer))) {
                        flag = 0;
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(),0,len));
                        byteBuffer.clear();
                    }
                    flag++;
                    //判斷此時是否有io事件,陷入空輪詢 - 連續空輪詢100次
                    //請求取消此鍵的通道在其選擇器的註冊,也就是 selector.select();的數量 -1
                    if(flag == 100){
                        selectionKey.cancel();
                        socketChannel.close();
                    }
                }
            }
            iterator.remove();
        }
    }
}

NioClient

package com.yuan.nio.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NioClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost",9021));

            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put("hello".getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);

            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

相關文章