BIO的缺點:
public static void main(String[] args) throws FileNotFoundException {
try {
ServerSocket server = new ServerSocket(5555);
while (true) {
Socket socket = server.accept(); //這是阻塞的,沒有客戶端到來,一直停在這裏
System.out.println("客戶端連接來了");
byte[] bytes = new byte[1024];
//這是阻塞的,客戶端連接過來後,若客戶端不發數據,一直停在這裏
socket.getInputStream().read(bytes);
//直到有客戶端發送數據過來,才能走到這裏
System.out.println("收到客戶端的消息:" + new String(bytes));
//在這個處理過程中,server一直在處理這一個客戶端的請求,如果有第二個客戶端過來,server也處理不了
}
} catch (IOException e) {
e.printStackTrace();
}
}
針對上面,服務端必須等這個客戶端完全處理完成才能處理下一個客戶端的請求,我們做一次升級:
public static void main(String[] args) throws FileNotFoundException {
try {
ServerSocket server = new ServerSocket(5555);
while (true) {
final Socket socket = server.accept(); //這是阻塞的,沒有客戶端到來,一直停在這裏
System.out.println("客戶端連接來了");
//啓動一個線程去執行後續的讀寫,server可以繼續處理其他客戶端的請求
new Thread(new Runnable() {
public void run() {
while (true) {
try {
byte[] bytes = new byte[1024];
socket.getInputStream().read(bytes);
System.out.println("收到客戶端的消息:" + new String(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
這樣做可以讓server繼續處理其他客戶端請求,但每個socket都要new Thread,太過於浪費,當然如果進一步優化可以使用線程池,但這些都是治標不治本,性能較低。
所以有了NIO:
package nio;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.net.InetSocketAddress;
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.Date;
import java.util.Iterator;
import java.util.Set;
public class ServerSocketChannels implements Runnable {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
private volatile boolean stop;
public ServerSocketChannels(int port){
try {
//創建多路複用器selector,工廠方法
selector = Selector.open();
//創建ServerSocketChannel,工廠方法
serverSocketChannel = ServerSocketChannel.open();
//綁定ip和端口號,默認的IP=127.0.0.1,對連接的請求最大隊列長度設置爲backlog=1024,如果隊列滿時收到連接請求,則拒絕連接
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
//設置非阻塞方式
serverSocketChannel.configureBlocking(false);
//註冊serverSocketChannel到selector多路服用器上面,監聽accrpt請求
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("the time is start port = " + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
this.stop = true;
}
@Override
public void run() {
//如果server沒有停止
while(!stop){
try {
//selector.select()會一直阻塞到有一個通道在你註冊的事件上就緒了
//selector.select(1000)會阻塞到1s後然後接着執行,相當於1s輪詢檢查
selector.select(1000);
//找到所有準備接續的key
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try {
//處理準備就緒的key
handle(key);
}catch (Exception e){
if(key != null){
//請求取消此鍵的通道到其選擇器的註冊
key.cancel();
//關閉這個通道
if(key.channel() != null){
key.channel().close();
}
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void handle(SelectionKey key) throws IOException {
//如果key是有效的
if(key.isValid()){
//監聽到有新客戶端的接入請求
//完成TCP的三次握手,建立物理鏈路層
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = (SocketChannel) ssc.accept();
//設置客戶端鏈路爲非阻塞模式
sc.configureBlocking(false);
//將新接入的客戶端註冊到多路複用器Selector上
sc.register(selector, SelectionKey.OP_READ);
}
//監聽到客戶端的讀請求
if(key.isReadable()){
//獲得通道對象
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//從channel讀數據到緩衝區
int readBytes = sc.read(readBuffer);
if (readBytes > 0){
//Flips this buffer. The limit is set to the current position and then
// the position is set to zero,就是表示要從起始位置開始讀取數據
readBuffer.flip();
//eturns the number of elements between the current position and the limit.
// 要讀取的字節長度
byte[] bytes = new byte[readBuffer.remaining()];
//將緩衝區的數據讀到bytes數組
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("the time server receive order: " + body);
String currenttime = "query time order".equals(body) ? new Date(System.currentTimeMillis()).toString(): "bad order";
doWrite(sc, currenttime);
}else if(readBytes < 0){
key.channel();
sc.close();
}
}
}
}
public static void doWrite(SocketChannel channel, String response) throws IOException {
if(!StringUtils.isEmpty(response)){
byte [] bytes = response.getBytes();
//分配一個bytes的length長度的ByteBuffer
ByteBuffer write = ByteBuffer.allocate(bytes.length);
//將返回數據寫入緩衝區
write.put(bytes);
write.flip();
//將緩衝數據寫入渠道,返回給客戶端
channel.write(write);
}
}
}
NIO操作過於複雜、繁瑣,進而產生了Netty
單線程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup);
多線程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(2);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup);
主從單線程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
主從多線程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(2);
EventLoopGroup workerGroup = new NioEventLoopGroup(10);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup);
Netty爲什麼優於NIO:
netty底層基於jdk的NIO,我們爲什麼不直接基於jdk的nio或者其他nio框架?
- 1.使用jdk自帶的nio需要了解太多的概念,編程複雜
- 2.netty底層IO模型隨意切換,而這一切只需要做微小的改動
- 3.netty自帶的拆包解包,異常檢測等機制讓你從nio的繁重細節中脫離出來,讓你只需要關心業務邏輯
- 4.netty解決了jdk的很多包括空輪訓在內的bug
- 5.netty底層對線程,selector做了很多細小的優化,精心設計的reactor線程做到非常高效的併發處理
- 6.自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手
- 7.netty社區活躍,遇到問題隨時郵件列表或者issue
- 8.netty已經歷各大rpc框架,消息中間件,分佈式通信中間件線上的廣泛驗證,健壯性無比強大