JAVA Socket TCP 網絡編程基礎

TCP 網絡編程

 

一、 JAVA-IO分類:

BIO,NIO,AIO (NIO 2.0)。

BIO 同步阻塞IO (blocking I/O): 服務器處理客戶端的連接請求業務需要開啓一個線程來進行業務,這樣連接的資源越多服務器承載的消耗會變大。

NIO 同步非阻塞 (non-blocking I/O): 同步非阻塞,客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時會得到需要處理的客戶端對其進行處理。這種方式就大大減少了過多線程資源的開銷。

AIO 異步非阻塞 (Asynchronous I/O) : 異步非阻塞,客戶端連接或請求時,服務器的某些狀態改變後通知對應處理方法。例如客戶端連接服務器時,服務器知道客戶端連接則通知處理accept方法,客戶端發送給服務器數據,服務器知道有某客戶端的數據則通知處理read方法。

 

NIO 實際上使用一個線程開始輪訓選擇有狀態的客戶端,發現某客戶端有狀態變化後挨個處理,這種處理方式業務實現上一般不要有過長的業務堵塞(例如 較長IO操作)。

AIO 實際上是有狀態的客戶端,調用一個線程來處理這部分業務。處理完之後如果還需要繼續監聽這個狀態,則還需要重新註冊。

 

以下代碼可以copy到編輯器中調試,使用。

二、 BIO JAVA 實現

這裏我實現一個簡單的發送字符串的例子,服務器接受客戶端發送的字符串,每一段完整的內容直接都使用\n進行分割。

客戶端發送兩條消息: 1234567890\n34567899\n

因爲消息發送出來的時候,會出現兩種情況:

    1. 兩個數據包粘在一起需要拆包。

    2. 一個數據包第一次獲取並不完整,分爲多次獲取才取完。

 

1. 服務器實現

BioServer.class

public class BioServer {

    public static void main(String[] args) throws Exception {
        // 創建一個服務器, 端口號 11111, 最大客戶端連接: 10000
        ServerSocket server = new ServerSocket(11111, 10000);

        while (true) {
            // 等待接受客戶端的連接,如果有客戶端連接則會返回Socket對象,沒有則阻塞。
            Socket client = server.accept();

            // 這裏需要開啓一個線程來保存與這個客戶端的連接,然後繼續接受下一個客戶端。
			
            Thread thread = new Thread(new BioServerReaderHandler(client) ) ;
			
            thread.start(); // 啓動線程
        }

    }
}

 

BioServerReaderHandler.class

public class BioServerReaderHandler implements Runnable {
    private Socket socket;

    public BioServerReaderHandler(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();
            byte[] buff = new byte[1024];
            ByteArrayOutputStream cache = new ByteArrayOutputStream(1024);
            while (!socket.isClosed() && !socket.isInputShutdown()) {
                try {
                    int len = in.read(buff);
                    // 讀到 \n 則輸出
                    for (int i = 0; i < len; i++) {
                        byte b = buff[i];
                        
                        // 通過 \n 用來分割每個數據包, 能夠完整拆除一個包則直接輸出
                        
                        if (b == '\n') {
                            
                            byte[] lineBytes = cache.toByteArray();
                            String text = new String(lineBytes, Charset.forName("UTF-8"));

                            // 客戶端關閉客戶端
                            if (text.trim().equals("quit")) return; 
                            // 輸出內容
                            System.out.println(text);
                            // 返回應答
                            out.write("已經收到一條消息\n".getBytes(Charset.forName("UTF-8")));
                            // 清空緩存
                            cache = new ByteArrayOutputStream(1024);
                        } else {
                            
                            // 不滿足條件則放入緩存
                            cache.write(b);
                        }
                        
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e1) {
            e1.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (Exception e) {
    
            }
        }
    }

}

2. 客戶端實現

這裏寫一個類似 telnet 的客戶端, 通過控制檯輸入獲取一行內容發送給服務器,服務器回傳的信息顯示到控制檯上。

發送服務器的消息和接受服務器回覆的消息都是 \n 分割。

如下就實現了一個簡單的客戶端,控制檯的消息發送給服務器,服務器回覆的消息再顯示到控制檯。

BioClient.class

public class BioClient {
    public static void main(String[] args) throws Exception {
        // 這裏寫一個類似 telnet 的客戶端
        Socket client = new Socket("127.0.0.1", 11111);

        // 這裏開啓一個線程異步讀取服務器返回的消息
        Thread thread = new Thread(new BioClientReaderHandler(client));
        thread.start();
        
        // 獲得向服務器輸出的流對象
        OutputStream out = client.getOutputStream();
        Scanner in = new Scanner(System.in);
        
        // 監聽控制檯寫入的內容,發送給服務器
        while (!client.isClosed() && !client.isOutputShutdown()) {
            // 從控制檯中讀取一行數據  加上 \n 分割符 發送給服務器
            String line  = in.nextLine();
            
            byte[] lineBytes = (line  + "\n").getBytes(Charset.forName("UTF-8"));
            out.write(lineBytes);
            // 退出
            if (line.trim().equals("quit")) {
                break;
            }
        }
        
        client.close();
    }
}

 

BioClientReaderHandler.class

public class BioClientReaderHandler implements Runnable {
    private Socket socket;

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

    @Override
    public void run() {
        try {
            InputStream in = socket.getInputStream();
            byte[] buff = new byte[1024];
            ByteArrayOutputStream cache = new ByteArrayOutputStream(1024);
            while (!socket.isClosed() && !socket.isInputShutdown()) {

                try {
                    int len = in.read(buff);
                    // 讀到 \n 則輸出
                    for (int i = 0; i < len; i++) {
                        // 通過 \n 用來分割每個數據包, 能夠完整拆除一個包則直接輸出
                        if (buff[i] == '\n') {
                            byte[] lineBytes = cache.toByteArray();
                            String text = new String(lineBytes, Charset.forName("UTF-8"));
                            // 輸出內容
                            System.out.println(text);
                            cache = new ByteArrayOutputStream(1024);
                        } else {
                            // 不滿足條件則放入緩存
                            cache.write(buff[i]);
                        }
                    }
                } catch (Exception e) {

                }

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

測試,啓動BioServer,然後啓動BioClient 連接服務器或 直接使用 telnet 連接服務器 。

BioClient: 

BioServer 


三、NIO JAVA 實現。

這裏也同樣實現一個字符串消息傳輸,但是此時我會直接在最開始的時候告知可以讀取的內容長度是多少。

這裏我定義一個消息結構:

message {

    int length; // int 佔 4byte,  下面我這麼代表 [][][][]

    String msg; // 不固定長度的內容,長度通過length得到。

}

這樣我發送每個消息的時候在消息頭開始攜帶這個消息的長度,按照固定長度分割每個消息包。

一連串消息格式 : [][][][]內容......[][][][]內容......[][][][]內容......[][][][]內容......

[][][][] 代表4子節

這裏會用到這些功能: 

Selector 多路選擇器

SocketChannel 客戶端的sokcet通道

ServerSocketChannel 服務器端的socket通道

ByteBuffer 1. 讀取或回覆socket數據,2. 獲取的數據包不完整時、先加入緩存。

點擊可以查看: ByteBuffer使用


NIO 服務器端

NioServer.class .

功能: 收到客戶端發來的消息包,拆包結構後輸出內容, 並回復客戶端。 接受 quit 關閉客戶端

個人感覺Nio的ByteBuffer 理解上不太好用。

Selector 你可以理解爲一個集合,將需要監聽狀態的對象放裏面,通過循環一個一個判斷狀態,滿足狀態的取出。

具體看代碼, 每行代碼都給一定解釋

public class NioServer {
    
    public static void main(String[] args) throws Exception {
        // 多路複用選擇
        Selector selector = Selector.open();
        // 開啓服務器Socket通道
        ServerSocketChannel server = ServerSocketChannel.open();
        // 監聽 11111 端口、設置允許 10000 連接等待
        server.bind(new InetSocketAddress(11111), 10000);
        // 非阻塞、所以這裏要配置一下,false爲非阻塞
        server.configureBlocking(false);
        // 註冊選擇器,OP_ACCEPT 的狀態,如果有這個則會被輪訓選擇出來
        server.register(selector, SelectionKey.OP_ACCEPT);
        
        
        
        while (server.isOpen()) {
            // 設置空輪訓的間隔
            selector.select(3000);
            // selectedKeys 選擇有狀態的需要進行操作的項
            Iterator<SelectionKey> selectKeys = selector.selectedKeys().iterator();
            
            while (selectKeys.hasNext()) {
                // 獲取等待操作的項
                SelectionKey selectionKey = selectKeys.next();
                // 操作完成消費掉當前項。
                selectKeys.remove();
                
                if (selectionKey.isValid()) {
                    // 有客戶端的連接需要被接受
                    if (selectionKey.isAcceptable()) {
                        ServerSocketChannel ser = (ServerSocketChannel)selectionKey.channel();
                        // 接收到客戶端了
                        SocketChannel client = ser.accept();
                        // 將客戶端設置爲非阻塞
                        client.configureBlocking(false);
                        // 註冊選擇器, OP_READ 的狀態如果存在時候會被輪訓選擇出來,ByteBuffer.allocate(10240) 用來緩存不完整的消息包
                        client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(10240));
                    } 
                    // 有未讀完的數據
                    if (selectionKey.isReadable()) {
                        SocketChannel client = (SocketChannel)selectionKey.channel();
                        // 其實到這一步就完成了簡單的NIO服務器,read 用來讀,write用來回復。
//                        client.read(dst)
//                        client.write(src)
                        
                        // 下面演示用 ByteBuffer 和 SocketChannel 合用的拆包過程
                        // ByteBuffer 寫的數據需要調用 flip() 轉化讀的狀態,
                        // 他是將 limit賦值position,position=0,可讀長度就爲remaining() =  limit - position。
                        // []代表每個byte,[-]代表有值。
                        // 寫的時候 position 記錄寫的位置。limit 代表最大可寫位置
                        // [-][-][-][-][-][-][position][][][][][][][]limit   limit - position 是剩餘可寫,寫的時候position會向右++
                        // 讀的時候 position 記錄讀的位置。limit 代表最大可讀位置
                        // [position][-][-][-][-][-][limit][][][][][][][]capacity    flip()後  limit - position 是剩餘可讀  讀的時候position會向右++
                        
                        // 開始讀取客戶端的操作,讀取內容後輸出。
                        // attachment 是一個可以在當前選擇Key中傳遞的參數,方便存儲一些必要操作的內容。
                        ByteBuffer cache = (ByteBuffer)selectionKey.attachment();
                        // 用一個臨時變量緩存取接受讀的數據。
                        ByteBuffer readBuff = ByteBuffer.allocate(1024);
                        int len = client.read(readBuff); // 將讀的數據寫入 ByteBuffer
                        if (len == -1) {
                            selectionKey.cancel();
                            client.close();
                            continue;
                        }
                        readBuff.flip(); // 轉化爲讀
                        
                        // 放不下了, 增加一點容量(讀取到的長度大於cache可寫空間)
                        if (len > cache.remaining()) {
                            cache = ByteBuffer.allocate(cache.capacity() + len).put(cache);
                            selectionKey.attach(cache);
                        }
                        
                        // 將讀讀到的數據先放入緩存,在後面在做拆包處理。
                        cache.put(readBuff); // 寫
                        cache.flip(); // 轉換爲讀 此時 limit = position,  position = 0
                        
                        // 獲得可讀空間大於 4 字節, 因爲定義的數據結構是 前4字節記錄的body長度
                        if (cache.remaining() < 4) {
                            // 還將position,limit 位置 原爲寫時的狀態
                            cache.position(cache.limit());
                            cache.limit(cache.capacity());
                            continue;
                            
                        }
                        int length = cache.getInt(); // 獲取內容的總長度
                        if (length < cache.remaining()) { // 判斷當前內容可讀長度 不滿足 數據內容長度,則繼續還原
                            // 還將position,limit 位置 原爲寫時的狀態
                            cache.position(cache.limit());
                            cache.limit(cache.capacity());
                            continue;
                        }
                        // 讀取成功
                        byte[] body = new byte[length];
                        cache.get(body);
                        String text = new String(body, Charset.forName("UTF-8"));
                        cache.compact(); // 已讀部分移除、又轉化爲可寫的情況。
                        if (text.equals("quit")) {
                            selectionKey.cancel(); // 從多路選擇上移除自己
                            client.close();                // 關閉客戶端
                            System.out.println("再見!"); // 再見!
                            continue;
                        }
                        
                        System.out.println("客戶端發來的內容: " + text);
                        
                        // 認真的回覆客戶端 我已經成功接收到!
                        byte[] reply = "我已經成功接收到!".getBytes(Charset.forName("UTF-8"));
                        ByteBuffer replyBuff = ByteBuffer.allocate(4 + reply.length).putInt(reply.length).put(reply);
                        replyBuff.flip();
                        client.write(replyBuff);
                    }
                    
                }
            }
        }
    }
    
}

NIO 客戶端:

客戶端,從控制的輸入內容,發送給服務器。並異步接受服務器的回覆。發送 quit 關閉客戶端

NioClient.class


public class NioClient {
    
    
    public static void main(String[] args) throws Exception {
        // 多路選擇器 (這個如果你開了好幾個客戶端,都可以註冊到同一個Selector)
        Selector selector = Selector.open();
        // 客戶端開啓了
        SocketChannel client = SocketChannel.open();
        // 連接到你想要連接的
        client.connect(new InetSocketAddress("127.0.0.1", 11111));
        // 跟服務器一樣 需要設置爲非阻塞
        client.configureBlocking(false);
        // 註冊到 Selector, 輪訓時 OP_READ 狀態則會被選中
        SelectionKey selectionKey = client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(10240));
        // 開始輪訓,這裏我們使用一個線程進行輪訓。
        Thread thread = new Thread(new NioSelectorLoopHandler(selector));
        thread.start();
        
        // 下面從控制檯輸入內容發送到服務器中
        Scanner in = new Scanner(System.in);
        
        while (true) {
            String line = in.nextLine();
            byte[] body = line.getBytes(Charset.forName("UTF-8"));
            int length = body.length;
            
            ByteBuffer byteBuffer = ByteBuffer.allocate(4 + length);
            byteBuffer.putInt(length);
            byteBuffer.put(body);
            byteBuffer.flip();
            // 發送信息給服務器
            client.write(byteBuffer);
            
            // 關閉了這個客戶端
            if (line.equals("quit")) {
                selectionKey.cancel();
                client.close();
                break;
            }
            
        }
        
        selector.close();
    }
    
}

NioSelectorLoopHandler.class 多路選擇器輪訓,開啓的一個線程進行輪訓。

public class NioSelectorLoopHandler implements Runnable {
    private Selector selector;
    
    public NioSelectorLoopHandler(Selector selector) {
        this.selector = selector;
    }

    @Override
    public void run() {
        while (selector.isOpen()) {
            try {
                selector.select(3000);
                Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
                while (selectionKeys.hasNext()) {
                    SelectionKey selectionKey = selectionKeys.next();
                    selectionKeys.remove();
                    if (selectionKey.isValid()) {
                        
                        if (selectionKey.isReadable()) {
                            
                            SocketChannel client = (SocketChannel)selectionKey.channel();
                            ByteBuffer cache = (ByteBuffer)selectionKey.attachment();
                            
                            // 用一個臨時變量緩存取接受讀的數據。
                            ByteBuffer readBuff = ByteBuffer.allocate(1024);
                            int len = client.read(readBuff); // 將讀的數據寫入 ByteBuffer
                            readBuff.flip(); // 轉化爲讀
                            
                            // 放不下了, 增加一點容量(讀取到的長度大於cache可寫空間)
                            if (len > cache.remaining()) {
                                cache = ByteBuffer.allocate(cache.capacity() + len).put(cache);
                                selectionKey.attach(cache);
                            }
                            
                            // 將讀讀到的數據先放入緩存,在後面在做拆包處理。
                            cache.put(readBuff); // 寫
                            cache.flip(); // 轉換爲讀 此時 limit = position,  position = 0
                            
                            // 獲得可讀空間大於 4 字節, 因爲定義的數據結構是 前4字節記錄的body長度
                            if (cache.remaining() < 4) {
                                // 還將position,limit 位置 原爲寫時的狀態
                                cache.position(cache.limit());
                                cache.limit(cache.capacity());
                                continue;
                                
                            }
                            int length = cache.getInt(); // 獲取內容的總長度
                            if (length < cache.remaining()) { // 判斷當前內容可讀長度 不滿足 數據內容長度,則繼續還原
                                // 還將position,limit 位置 原爲寫時的狀態
                                cache.position(cache.limit());
                                cache.limit(cache.capacity());
                                continue;
                            }
                            // 讀取成功
                            byte[] body = new byte[length];
                            cache.get(body);
                            String text = new String(body, Charset.forName("UTF-8"));
                            cache.compact(); // 已讀部分移除、又轉化爲可寫的情況。
                            
                            // 輸出
                            System.out.println(text);
                            
                        }    
                        
                    }
                }
                
            } catch (Exception e) {
                //e.printStackTrace();
            }
            
            
        }

    }

}

現在啓動 NioServer, 在啓動NioClient 連接服務器,

測試結果:

NioClient : 

NioServer:


四、AIO (NIO 2.0) 非阻塞異步IO

 

這裏實現異步IO的服務端,消息的結構依舊是 \n 分割即可。這樣可以直接使用telnet做客戶端測試。

消息:     好多內容....\n又是好多內容.....\n

這裏需要用到: 

AsynchronousChannelGroup 異步通知處理的的線程池。

AsynchronousServerSocketChannel 異步IO服務Socket通道。

ByteBuffer 1. 接受消息或寫消息,2. 緩存未處理的消息。

異步iO的使用,需要一次執行一次註冊。例如accept 狀態會通知指定的處理方法,處理完需要再次重新註冊。

AIO 服務器

AioServer.class 可以直接複製去直接調試.

public class AioServer {
    
    
    public static void main(String[] args) throws Exception {
        // 創建了一個線程池來執行 後續的處理器
        ExecutorService executor = Executors.newFixedThreadPool(100);
        // 第一個參數線程池,第二個參數,啓動時直接開啓這麼多個線程
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(executor, 10);
        // 創建服務
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group);
        // 綁定 11111, 可以允許接受 10000待處理的客戶端。
        server.bind(new InetSocketAddress(11111), 10000);

        // 第一個參數是允許傳遞全局的對象。第二個參數是註冊 accept,有這個狀態是通過哪個處理器執行。
        server.accept(null, new AioAcceptHandler(server));    
    }
    
}

 

AioAcceptHandler.class 用於異步接受客戶端連接


/**
 * 自己實現一個處理器
 *
 */
public class AioAcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
    AsynchronousServerSocketChannel server;

    public AioAcceptHandler(AsynchronousServerSocketChannel server) {
        this.server = server;
    }

    @Override
    public void completed(AsynchronousSocketChannel client, Object attachment) {
        // attachment 是上次註冊傳遞過來的對象。
        // 當已經觸發accept後,需要重新註冊自己, 這樣可以繼續接受新的客戶端連接
        server.accept(attachment, this);
        
        // 下面要註冊服務器接受客戶端的消息處理器
        ByteBuffer msg = ByteBuffer.allocate(1024); // 讀取的數據會填充這個對象
        ByteBuffer cache = ByteBuffer.allocate(10240); // 這個對象用於緩存未完成處理的數據
        // 這裏註冊自己實現的讀取處理器, 有讀的內容就會進去處理了
        client.read(msg, cache, new AioReaderHandler(client, msg));
    }

    @Override
    public void failed(Throwable exc, Object attachment) {
        exc.fillInStackTrace();
    }

 

AioReaderHandler.class 用於異步接受處理客戶端的消息

/**
 * 自己實現一個客戶端讀取處理器
 */
public class AioReaderHandler implements CompletionHandler<Integer, ByteBuffer>{
    private AsynchronousSocketChannel client;
    private ByteBuffer msg; // 讀的數據會填充這個
    
    
    public AioReaderHandler(AsynchronousSocketChannel client, ByteBuffer msg) {
        this.client = client;
        this.msg = msg; // 讀的數據會填充這個
    }

    @Override
    public void completed(Integer result, ByteBuffer cache) {
        // 其實這裏就已經OK了
        // msg.flip(), msg.get(byte[]) 讀取數據
        // client.write(ByteBuffer) 回覆
        // client.read(msg, cache, this); 重新註冊 第一個參數是下次要讀的內容,第二個參數是全局攜帶的對象
        
        // 下面是 對消息包進行拆包的示例,假設每個消息包都使用\n分割。
        
        
        ByteBuffer msg = this.msg;
        try {
            if (result == -1) {
                client.close();
                return; // 關閉了不再讀
            }
            
            // 1 獲得客戶端發來的數據
            // 轉讀狀態,msg.remaining() 就是可讀內容了長度 
            msg.flip();
            byte[] data = new byte[msg.remaining()];
            msg.get(data); // 讀完了。
            msg.clear(); // 清空 msg
            
            // 2 判斷是否有 \n 分割符,如果存在就把緩存裏的值和當前內容輸出。
            
            // 客戶端接收到的消息集合
            List<String> results= new ArrayList<String>();
            // 讀取每個\n分割的內容,分割成功的內容放入 results集合內
            for (int i = 0; i < data.length; i++) {
                if (data[i] == '\n') {
                    cache.flip(); // 讀
                    // 讀所有
                    byte[] body = new byte[cache.remaining()];
                    cache.get(body);
                    // 得到最終讀內容 將內容加入 結果集合,在後面進行處理結果
                    String text = new String(body, Charset.forName("UTF-8"));
                    results.add(text); // 拆開一個完整的消息添加到集合
                    // 已經全部輸出了,清空緩存
                    cache.clear();
                } else {
                    // 空間不足就擴大空間繼續緩存
                    if (cache.remaining() == 0) {
                        cache = ByteBuffer.allocate(cache.capacity() + 1024).put(cache);
                    }
                    // 不滿足條件加入緩存
                    cache.put(data[i]);
                }
            }

            // 3 結果處理,處理接受到的每一段消息。
            // results 是當前解析的消息包集合 每個消息\n分割解析後的結果
            // 輸出讀取到的內容,每次輸出都回復給客戶端 "成功收到消息! \n"
            for (String text : results) {
                if (text.trim().equals("quit")) {
                    System.out.println("再見!");
                    client.close();
                    return;  // 關閉了不再讀
                }
                
                System.out.println("去讀到客戶端消息: " + text);
                byte[] reply = "成功收到消息!\n".getBytes(Charset.forName("UTF-8"));
                ByteBuffer replyBuffer = ByteBuffer.allocate(reply.length).put(reply);
                replyBuffer.flip();
                client.write(replyBuffer);
            }
            // 執行完讀取後,繼續註冊,進行下一次讀取。
            client.read(msg, cache, this);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        exc.printStackTrace();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

測試結果:

客戶端:

服務端: 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章