前言
無論是軟件還是硬件的本質都是要解決IO問題(輸入、輸出),再說回網絡編程本質上都是基于TCP/UP的開發,socket是在此基礎上做的擴展與封裝,而Netty又是對socket做的封裝。本文旨在通過相關案例對socket進行探討。
一、基本知識
交互方式
- 連接三次握手:1.客戶-服務 (請求)2.服務-客戶(同意)3.客戶-服務(連接)
- 斷開四次握手:1.客戶-服務(請求斷開)2.服務-客戶(接受請求)3.服務-客戶(斷開) 4.客戶-服務(斷開完成)
Java類
? ? ? ? ServerSocket? 服務類
? ? ? ? accept(()? 開啟連接
? ? ? ? close() 停止服務
? ? ? ? Socket 客戶端類
? ? ? ? getInputStream()? 輸入內存流(接收)
? ? ? ? getOutputStream()? 輸出內存流(發布)
二、基于線程阻塞式socket(BIO)開發示例
實現
Server代碼
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class SockerServer {public static void main(String[] args) throws IOException {ServerSocket serverSocket= new ServerSocket(1200);while (true){Socket socket = serverSocket.accept();System.out.println("有新的客戶端連接了:"+socket.getInetAddress());new Thread(new ClientHandler(socket)).start();}}
}class ClientHandler implements Runnable {private Socket socket;public ClientHandler(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {BufferedReader inStreamReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter outStreamWriter = new PrintWriter(socket.getOutputStream(), true);outStreamWriter.println("請輸入內容:");String message;while ((message = inStreamReader.readLine()) != null) {System.out.println("收到客戶端消息: " + message);if ("bye".equalsIgnoreCase(message)) {outStreamWriter.println("服務器:連接已關閉,再見!");break; // 結束連接}// 回顯客戶端消息outStreamWriter.println("服務器回顯:" + message);}} catch (IOException e) {throw new RuntimeException(e);}}
}
Client 代碼
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;public class SocketClient {public static void main(String[] args) throws Exception {Socket socket = new Socket("127.0.0.1",1200);BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader console = new BufferedReader(new InputStreamReader(System.in));System.out.println("已連接到服務器!");System.out.println(in.readLine()); // 讀取服務器歡迎消息String userInput;System.out.println("請輸入消息(輸入 exit 斷開連接):");while ((userInput = console.readLine()) != null) {out.println(userInput); // 發送消息給服務器String serverResponse = in.readLine(); // 接收服務器響應System.out.println(serverResponse);if ("exit".equalsIgnoreCase(userInput)) {System.out.println("連接已關閉。");break;}}}
}
效果
缺點
雖然這個已經實現通信,由于它是基于線程控制通信的,換言之每個客戶端連接后都會創建一個線程,客戶端數量與線程增長成正比就意味著會吃更多的內存,這顯然是不合理的。(如果你足夠細心會有一些程序要隔一段時間需要重新啟動一下否則會卡死,這或許是設計之初犯類似的錯誤),這就是BIO的致命缺點;
三、基于單線程socket(NIO)
前言
基于上面的問題,我們通過改造實現非隔斷性Socket實現,而非多線程方式;這可以極大的提高交互效率與并發問題。
實現
服務端
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NioServer {public static void main(String[] args) throws IOException {// 打開服務端Socket通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 綁定端口1200serverSocketChannel.socket().bind(new java.net.InetSocketAddress(1200));// 設置為非阻塞模式serverSocketChannel.configureBlocking(false);// 創建Selector選擇器Selector selector = Selector.open();// 將ServerSocketChannel注冊到Selector,監聽連接事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("系統運行中.......");while (true){// 阻塞直到有事件發生selector.select();// 獲取所有發生的事件鍵Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()){SelectionKey key = iterator.next();if (key.isAcceptable()){ // 如果是客戶端連接事件// 處理客戶端連接ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 獲取觸發事件的通道SocketChannel sc = server.accept(); // 接受客戶端連接sc.configureBlocking(false); // 設置為非阻塞模式sc.register(selector, SelectionKey.OP_READ); // 注冊讀取事件System.out.println("有新的客戶端連接了:"+sc.getRemoteAddress());}else if (key.isReadable()){ // 如果是客戶端讀取事件// 處理客戶端讀取請求SocketChannel sc = (SocketChannel) key.channel(); // 獲取觸發事件的通道ByteBuffer buffer = ByteBuffer.allocate(1024); // 創建緩沖區int len = sc.read(buffer); // 讀取數據if (len > 0){ // 如果有數據String msg = new String(buffer.array(), 0, len); // 解析消息System.out.println("收到客戶端"+sc.getRemoteAddress()+"的消息:"+msg);// 將收到的消息回傳給客戶端ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); // 包裝響應數據sc.write(outBuffer); // 發送響應}else if (len == -1){ // 如果客戶端斷開連接System.out.println("客戶端"+sc.getRemoteAddress()+"斷開了連接");sc.close(); // 關閉通道}}iterator.remove(); // 移除當前事件鍵}}}
}
客戶端
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;public class NioClient {public static void main(String[] args) throws Exception{// 打開SocketChannel并連接到服務器SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress("localhost", 1200));// 創建控制臺輸入流,用于讀取用戶輸入BufferedReader console = new BufferedReader(new InputStreamReader(System.in));System.out.println("已連接到服務器!");// 初始化PrintWriter對象,用于向服務器發送數據PrintWriter writer = new PrintWriter(socketChannel.socket().getOutputStream(), true);String message;// 循環讀取用戶輸入并發送給服務器while ((message = console.readLine()) != null) {if ("exit".equalsIgnoreCase(message)) {System.out.println("即將退出連接...");break;}// 向服務器發送消息writer.println(message);// 讀取服務器返回的響應數據BufferedReader serverResponse = new BufferedReader(new InputStreamReader(socketChannel.socket().getInputStream()));String response;// 打印服務器返回的每一行數據while ((response = serverResponse.readLine()) != null) {System.out.println("服務器響應: " + response);}}// 關閉SocketChannel連接socketChannel.close();System.out.println("客戶端已退出");}
}
效果
缺點
綜上所述,NIO雖好,上面的代碼僅做到了實現,但是進行性能調優、異常處理等等需要更寫更多的代碼;作為程序員目的是用好輪子而非造輪子,那么Netty這個網絡通信工具以它的高性能、高并發、易配置的特點脫穎而出! 下篇我將進一步進行介紹。
下一篇《Spring Boot 從Socket 到Netty網絡編程(下):Netty基本開發與改進【心跳、粘包與拆包、閑置連接】》