Java 中的 IO 與 socket 編程 [ 複習 ]

1、Unix IO 與 IPChtml

Unix IO:Open-Read or Write-Closejava

IPC:open socket - receive and send to socket - close socket數據庫

 

IPC 全稱是 InterProcess Communication。編程

當消息發出後,消息進入 SendQ隊列 一直等待 sending socket 處理,才真正發出(一直等待是阻塞的)。當消息到達時,消息進入RecvQ隊列 一直等待 receiving socket 處理(同前)。segmentfault

底層的 TCP 協議關聯的 RecvQ 或者 SendQ 隊列就是一個操做系統緩衝區。數組

 

TCP/UDP 基於 IPC。Udp 是一種 datagram communication protocol,是 connectionless protocol,意味着每次發送 datagrams 時,須要額外發送local socket descriptor 和 receiving socket's address.緩存

Tcp 是一種 stream communication protocol 你能夠經過直接構建 Tcp 通道創建鏈接,在通道中的 socket pairs 能夠互相訪問。服務器

 

2、Java 中的 IO 編程網絡

基於流的多線程

public static void simpleIo() throws IOException {
    InputStream input = new BufferedInputStream(new FileInputStream("/home/lg/Pictures/wallpapers/2047.png"));
    OutputStream out = new BufferedOutputStream(new FileOutputStream("dem.png"));
    byte[] buf = new byte[1024];
    while (input.read(buf) > 0) {
        out.write(buf);
    }
    out.flush();
}

基於字符的

public static void simpleIo() throws IOException {
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    PrintWriter out = new PrintWriter(System.out, true);
    String myLine;
    while ((myLine = input.readLine()) != null) {
        out.println(myLine);
    }
}

// new String(char[] chars, "utf-8") 字節序列到字符串的轉換

* PrintWriter 與 BufferedWriter 區別

 1. PrintWriter的print、println方法能夠接受任意類型的參數,而BufferedWriter的write方法只能接受字符、字符數組和字符串;

2. PrintWriter的println方法自動添加換行,BufferedWriter須要顯示調用newLine方法;

3. PrintWriter的方法不會拋異常,若關心異常,須要調用checkError方法看是否有異常發生;

4. PrintWriter構造方法可指定參數,實現自動刷新緩存(autoflush);

5. PrintWriter的構造方法更廣。 

 

3、Java 中的 socket 編程

服務端

public class Server {

    public static void main(String[] args) {
        {
            ServerSocket myServer = null;
            Socket acceptSocket = null;
            BufferedReader input = null;
            PrintWriter output = null;
            try {
                myServer = new ServerSocket(4000);

                while (true) {
                    
                    acceptSocket = myServer.accept();
                    
                    input = new BufferedReader(new InputStreamReader(acceptSocket.getInputStream()));
                    output = new PrintWriter(acceptSocket.getOutputStream(), true);

                    String rLine;

                    while ((rLine = input.readLine()) != null) {
                        System.out.println("服務器接受到一條消息:\n" + rLine + "\n---------------\n\n");

                        if (rLine.equals("hello server")) {
                            System.out.println("而後打了個招呼\n---------------\n\n");
                            output.println("hello client");
                        } else {
                            System.out.println("而後嘲諷了一波\n---------------\n\n");
                            output.println("233333");
                        }
                    }
                }

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

客戶端

public class Client {

    private static Socket myClient;

    //靜態初始化時處理異常
    static {
        try {
            myClient = new Socket("localhost", 4000);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void sendMessage() {
        try {
            PrintWriter output = new PrintWriter(myClient.getOutputStream(), true);
            BufferedReader readConsole = new BufferedReader(new InputStreamReader(System.in));

            String cline;

            while ((cline = readConsole.readLine()) != null) {
                output.println(cline);
                System.out.println("消息發送成功!\n---------------\n\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void receiveMessage() {

        try {
            BufferedReader input = new BufferedReader(new InputStreamReader(myClient.getInputStream()));
            // 這裏省略掉了 OutputStream(System.out), 直接使用打印替代

            String rLine;
            while ((rLine = input.readLine()) != null) {
                System.out.println("客戶端收到來自服務器的消息:\n" + rLine + "\n---------------\n\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(Client::sendMessage).start();
        new Thread(Client::receiveMessage).start();
    }

}

 

說明:

客戶端使用了兩個線程:一個用於發送消息,另外一個用於接受消息。服務端只考慮了一個 socket 鏈接,所以並無用多線程。實際上,服務端一般須要同時處理多個 socket 鏈接,所以可考慮多線程。創建一個線程池,對於每一個 socket 鏈接,分派一個線程去處理。

 

4、NIO 與 socket 編程

1. 處理流程對比

普通的 BIO,消息要等待處理,須要先放入操做系統的 Socket 緩衝區(阻塞的SendQ與RecvQ隊列),這樣一直等待,直到 sending socket 或 receiving socket 就位後,才進行處理。

而 NIO,是 消息進入到操做系統的 Socket 緩衝區,直接複製到 Buffer (ByteBuffer.allocate)中,抑或直接進入與系統底層關聯的緩衝區(ByteBuffer.allocateDirector)。不用一直等待 sending socket 或 receiving socket 就續就進行處理,是非阻塞的。

須要說明的是等待就緒的阻塞是不使用CPU的,是在「空等」;而真正的讀寫操做的阻塞是使用CPU的,真正在"幹活",並且這個過程很是快,屬於memory copy,帶寬一般在1GB/s級別以上,能夠理解爲基本不耗時。

 

下圖是幾種常見I/O模型的對比:(圖片來自:UNIX網絡編程 -- I/O複用:select和poll函數

 

(摘自 Linux IO模式及 select、poll、epoll詳解 )

  • BIO,同步阻塞IO,在IO執行的兩個階段都被block了。若是鏈接少,他的延遲是最低的,由於一個線程只處理一個鏈接,適用於少鏈接且延遲低的場景,好比說數據庫鏈接。
  • NIO,同步非阻塞IO,用戶進程須要不斷的主動詢問kernel數據好了沒有。
  • 多路複用IO,就是咱們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。注意與 nio 的關係:在IO multiplexing Model中,實際中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
  • 信號驅動IO,這種IO模型主要用在嵌入式開發,不參與討論。
  • 異步IO,用戶進程發起read操做以後,馬上就能夠開始去作其它的事,當數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

 

2.線程模型對比

NIO一個重要的特色是:socket主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操做是同步阻塞的(消耗CPU但性能很是高)。

回憶BIO模型,之因此須要多線程,是由於在進行I/O操做的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能"傻等",即便經過各類估算,算出來操做系統沒有能力進行讀寫,也無法在socket.read()和socket.write()函數中返回,這兩個函數沒法進行有效的中斷。因此除了多開線程另起爐竈,沒有好的辦法利用CPU。

NIO的讀寫函數能夠馬上返回,這就給了咱們不開線程利用CPU的最好機會:若是一個鏈接不能讀寫(socket.read()返回0或者socket.write()返回0),咱們能夠把這件事記下來,記錄的方式一般是在Selector上註冊標記位,而後切換到其它就緒的鏈接(channel)繼續進行讀寫。

 

3.事件模型

NIO的主要事件有幾個:讀就緒、寫就緒、有新鏈接到來。Selector 主要圍繞這幾個事件作分發。註冊全部感興趣的事件處理器,單線程輪詢選擇就緒事件,執行事件處理器。 

 服務端

public class Server {

    private static Selector selector;

    static {
        try {
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        sscRegister();
    }

    private static void sscRegister() {

        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();

            ssc.configureBlocking(false);
            ssc.socket().bind(new InetSocketAddress("localhost", 8000));

            ssc.register(selector, SelectionKey.OP_ACCEPT);

            dispatcher(selector);

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

    private static void dispatcher(Selector selector) {
        try {

            while (true) {

                int select = selector.select();

                if (select == 0) {
                    System.out.println("返回 0 表示 SelectedKeys 未更新");
                    continue;
                }

                Iterator<SelectionKey> selectedIterator = selector.selectedKeys().iterator();
                while (selectedIterator.hasNext()) {
                    SelectionKey key = selectedIterator.next();

                    selectedIterator.remove(); // 只是從 SelectedKeys 中移除,而並非從整個選擇器的鍵集中移除。

                    if (!key.isValid()) {
                        continue;
                    }
                    if (key.isConnectable()) {
                        System.out.println(233);
                    }
                    if (key.isAcceptable()) {
                        accept(key);
                    }
                    if (key.isReadable()) {
                        read(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void accept(SelectionKey key) {

        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        try {
            SocketChannel socketChannel;
            if ((socketChannel = serverSocketChannel.accept()) != null) { // 服務端非阻塞時,直接返回 null

                socketChannel.configureBlocking(false);

                socketChannel.register(selector, SelectionKey.OP_READ);
            }


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

    private static void read(SelectionKey key) {

        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try {
            if (channel.read(buffer) > 0) {
                System.out.println("客戶端:" + new String(buffer.array()));
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            buffer.clear();
        }

    }
}

  

客戶端

public class Client {

    private static void clientSendMsg() {
        try {
            SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", 8000));

            System.out.println("已向服務器發出請求!");

            String msg = "hello" + Thread.currentThread().getName();
            ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());

            client.write(buffer);
            buffer.flip();
            Thread.sleep(5);

            client.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(Client::clientSendMsg, "Thread-A").start();
    }

}

 

  

參考文章


 https://tech.meituan.com/nio.html?utm_source=tool.lu

https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html

https://www.bysocket.com/?p=615 (大小端模式)

相關文章
相關標籤/搜索