0、前言
本文所有代碼可見 => 【gitee code demo】
本文涉及的主題:
1、BIO、NIO的業務實踐和缺陷
2、Redis IO多路復用:redis快的主要原因
3、epoll 架構
部分圖片 via 【epoll 原理分析】
1、BIO單線程版
1.1 業務代碼
client
client代碼相同 啟動多個即可
public class RedisClient1 {public static void main(String[] args) throws IOException {SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");Socket socket = new Socket("127.0.0.1", 6300);log("{} {}> 嘗試連接服務 {}", sdf.format(new Date()) ,socket.getLocalPort(), socket.getPort());OutputStream outputStream = socket.getOutputStream();while (true) {Scanner scanner = new Scanner(System.in);log("{} {}> ", sdf.format(new Date()) ,socket.getLocalPort());String string = scanner.nextLine();if (string.equalsIgnoreCase("quit")) {break;}socket.getOutputStream().write(string.getBytes());log("{} {}> 發送數據:{}", sdf.format(new Date()) ,socket.getLocalPort(), string);}outputStream.close();socket.close();}}
server
public class RedisServerBIO {public static void main(String[] args) throws IOException {SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");ServerSocket serverSocket = new ServerSocket(6300);while (true) {log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());Socket socket = serverSocket.accept();//阻塞1 ,等待客戶端連接log("{} {}> {} 連接到服務", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());InputStream inputStream = socket.getInputStream();int length = -1;byte[] bytes = new byte[1024];log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客戶端發送數據{log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));}inputStream.close();socket.close();}}
}
1.2 結果演示
現象:
1、client1 連接到server,client2嘗試連接被阻塞
2、client2 先發送的消息未被server接受,client1后發送的repeat消息被server接受
結論:
BIO會一直阻塞,單線程下只能處理一個socket連接
存在的問題:
多 client 訪問時效率低
2、BIO多線程版
2.1 業務代碼
public class RedisServerBIOMultiThread {public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(6300);SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");log("{} {}> ", sdf.format(new Date()), serverSocket.getLocalPort());while (true) {Socket socket = serverSocket.accept();//阻塞1 ,等待客戶端連接log("{} {}> {} 連接到服務", sdf.format(new Date()), socket.getLocalPort(), socket.getPort());new Thread(() -> {try {InputStream inputStream = socket.getInputStream();int length = -1;byte[] bytes = new byte[1024];while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客戶端發送數據{log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), serverSocket.getLocalPort(), socket.getPort(), new String(bytes, 0, length));}inputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();}}, Thread.currentThread().getName()).start();System.out.println(Thread.currentThread().getName());}}
}
2.1 結果演示
現象:
client1 、client2 都能正常連接到 server且正常發送、接受消息
結論:
BIO多線程提高處理能力,可以同時處理多個socket連接
存在的問題:
每個線程只能處理一個socket,當client數量大時,需要消耗大量線程資源
3、NIO
3.1 業務代碼
當一個客戶端與服務端進行連接,這個socket就會加入到一個容器中,隔一段時間遍歷一次,看這個socket的read()方法能否讀到數據,這樣一個線程就能處理多個客戶端的連接和讀取了
public class RedisServerNIO {static ArrayList<SocketChannel> socketList = new ArrayList<>();static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);public static void main(String[] args) throws IOException {ServerSocketChannel serverSocket = ServerSocketChannel.open();SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");serverSocket.bind(new InetSocketAddress("127.0.0.1", 6300));serverSocket.configureBlocking(false);//設置為非阻塞模式while (true) {for (SocketChannel element : socketList) {int read = element.read(byteBuffer);if (read > 0) {byteBuffer.flip();byte[] bytes = new byte[read];byteBuffer.get(bytes);//System.out.println(JSONUtil.toJsonStr(element));log("{} {}> 收到 {} 的消息:{}", sdf.format(new Date()), element.socket().getLocalPort(), element.socket().getPort(), new String(bytes, 0, read));byteBuffer.clear(); }}SocketChannel socketChannel = serverSocket.accept();if (socketChannel != null) {log("{} {}> {} 連接到服務", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketChannel.socket().getPort());socketChannel.configureBlocking(false);//設置為非阻塞模式socketList.add(socketChannel);log("{} {}> socket 數量: {} ", sdf.format(new Date()), socketChannel.socket().getLocalPort(), socketList.size());}}}
}
3.2 結果演示
現象:
1、client1 、client2 都能正常連接到 server且正常發送、接受消息
2、server 沒有創建額外線程
結論:
NIO 可以實現一個線程處理多個 socket 連接
存在的問題:
1、每次遍歷所有socket,有很多無用功
2、遍歷過程在用戶態,還需要將數據從內核態讀取到用戶態
4、IO多路復用
1、使用 epoll() 實現,多個網絡連接 socket 復用同一個線程
2、基于事件驅動機制,socket 中有數據會主動通知內核,并加入到就緒鏈表中,不需要遍歷所有 socket
3、減少了內核態和用戶態的切換
Redis IO多路復用實現
4.1 epoll_create()
創建內核中的fd容器
4.2 epoll_ctl()
epoll_ctl函數用于增加,刪除,修改epoll事件,epoll事件會存儲于內核epoll結構體紅黑樹中
4.3 epoll_wait
用于監聽套接字事件,可以通過設置超時時間timeout來控制監聽的行為為阻塞模式還是超時模式