1. 程式人生 > >傳統 BIO 程式設計

傳統 BIO 程式設計

BIO 通 信

  •  Java I/O 進化之路》中已經介紹了 Java I/O 程式設計的歷史過程,為了更好的掌握 NIO 程式設計,現在先了解原始的 BIO 程式設計,TCP 就是典型的 BIO 程式設計。
  • 網路程式設計的基本模型時 Client/Server 模型,也就是兩個程序之間相互通訊其中服務端提供位置資訊(繫結的 IP 地址與監聽的埠),客戶端通過連線操作向伺服器監聽地址發起連線騎請求,通過三次握手連線,如果連線建立成功,雙方就可以通過網路套接字(Socket) 進行通訊。
  • 傳統的同步阻塞模型(BIO)開發中,ServerSocket 負責繫結 IP 地址,啟動監聽埠,Socket 負責發起連線操作。連線成功之後,雙方通過輸入和輸出流進行同步阻塞式通訊。

  • BIO 通訊模型的服務端通常由一個獨立的 Accpetor(接受者)執行緒負責監聽客戶端的連線,它接收到客戶端連線請求後為每個客戶端建立一個新的執行緒進行單獨處理,處理完成後,通過輸出流返回給客戶端,執行緒銷燬,這就是典型的一請求一應答通訊模型。
  • BIO 模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的執行緒個數和客戶端併發訪問數呈1:1正比關係,由於執行緒是 Java 虛擬機器非常寶貴的系統資源,當執行緒數膨脹之後,系統的效能將急驟下降,隨著併發訪問量的繼續增大,系統會發生執行緒堆疊溢位、建立新執行緒失敗等問題,並最終導致程序宕機或者僵死,不能對外提供服務。

BIO 編 碼

  • 這裡以一個例子進行說明,客戶端往伺服器傳送資料,伺服器回覆資料。

·服務端·

package com.lct.bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 時間伺服器
 */
public class TimeServer {
    public static void main(String[] args) {
        tcpAccept();
    }

    public static void tcpAccept() {
        ServerSocket serverSocket = null;
        try {
            /**Tcp 伺服器監聽埠,ip 預設為本機地址*/
            serverSocket = new ServerSocket(8080);
            /**迴圈監聽客戶端的連線請求
             * accept 方法會一直阻塞,直到 客戶端連線成功,主執行緒才繼續往後執行*/
            Socket socket = null;
            while (true) {
                System.out.println("等待客戶端連線..........");
                socket = serverSocket.accept();
                System.out.println("客戶端連線成功..........");
                /**
                 * 為每一個客戶端連線都新開執行緒進行處理
                 */
                new Thread(new TimeServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**發生意外時,關閉服務端*/
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 對每個客戶端連線新開執行緒單獨處理:
package com.lct.bio;

import java.io.*;
import java.net.Socket;
import java.util.Date;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 為每個 TCP 客戶端新開執行緒進行處理
 */
public class TimeServerHandler implements Runnable {
    private Socket socket = null;

    /**
     * 將每個 TCP 連線的 Socket 通過構造器傳入
     *
     * @param socket
     */
    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        DataInputStream dataInputStream = null;
        DataOutputStream dataOutputStream = null;
        try {
            /**讀客戶端資料*/
            InputStream inputStream = socket.getInputStream();
            dataInputStream = new DataInputStream(inputStream);
            String message = dataInputStream.readUTF();
            System.out.println(Thread.currentThread().getName() + " 收到客戶端訊息:" + message);
            /**往客戶端寫資料*/
            OutputStream outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeUTF(new Date().toString());
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**操作完成,關閉流*/
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            /**操作完成,關閉連線,執行緒自動銷燬*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

·客戶端·

package com.lct.bio;

import java.io.*;
import java.net.Socket;

/**
 * Created by Administrator on 2018/10/14 0014.
 * 時間 客戶端
 */
public class TtimeClient {
    public static void main(String[] args) {
        /**
         * 3個執行緒模擬3個客戶端
         */
        for (int i = 0; i < 3; i++) {
            new Thread() {
                @Override
                public void run() {
                    tcpSendMessage();
                }
            }.start();
        }
    }

    /**
     * Tcp 客戶端連線伺服器併發送訊息
     */
    public static void tcpSendMessage() {
        Socket socket = null;
        DataOutputStream dataOutputStream = null;
        DataInputStream dataInputStream = null;
        try {
            /**
             * Socket(String host, int port):
             *      host)被連線的伺服器 IP 地址
             *      port)被連線的伺服器監聽的埠
             * Socket(InetAddress address, int port)
             *      address)用於設定 ip 地址的物件
             * 此時如果 TCP 伺服器未開放,或者其它原因導致連線失敗,則丟擲異常:
             * java.net.ConnectException: Connection refused: connect
             */
            socket = new Socket("127.0.0.1", 8080);
            System.out.println("連線成功.........." + Thread.currentThread().getName());
        
             /**往服務端寫資料*/
            OutputStream outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeUTF("我是長城" + Thread.currentThread().getName());
            dataOutputStream.flush();

            /**讀服務端資料*/
            InputStream inputStream = socket.getInputStream();
            dataInputStream = new DataInputStream(inputStream);
            String message = dataInputStream.readUTF();
            System.out.println("收到伺服器訊息:" + message);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            /**關閉流,釋放資源*/
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            /** 操作完畢關閉 socket*/
            if (socket != null && !socket.isClosed()) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

服務端輸出:

等待客戶端連線..........
客戶端連線成功..........
等待客戶端連線..........
客戶端連線成功..........
等待客戶端連線..........
客戶端連線成功..........
等待客戶端連線..........
Thread-0 收到客戶端訊息:我是長城Thread-2
Thread-1 收到客戶端訊息:我是長城Thread-0
Thread-2 收到客戶端訊息:我是長城Thread-1

客戶端輸出:

連線成功..........Thread-1
連線成功..........Thread-2
連線成功..........Thread-0
收到伺服器訊息:Sun Oct 14 18:38:53 CST 2018
收到伺服器訊息:Sun Oct 14 18:38:53 CST 2018
收到伺服器訊息:Sun Oct 14 18:38:53 CST 2018

總 結

  • BIO 阻塞式程式設計如上所示說明完畢,BIO 主要的問題在於每當有一個新的客戶端請求連線時,伺服器端必須新建執行緒進行處理,一個執行緒只能處理一個客戶端連線。
  • 在高效能伺服器應用領域,往往需要面向成千上萬個客戶端的併發連線,這種模型顯然無法滿足需求。為了改進這種一個執行緒一個連線的模型,後來又演進出了一種通過執行緒池或者訊息佇列實現一個或者多個執行緒處理 N 個客戶端的模型,由於它的底層通訊機制仍然使用同步阻塞 I/O ,所以被稱為 “偽非同步”。

···········下一篇《 偽非同步 I/O 程式設計