IO
什麼是 IO,IO 是Input、Output的簡稱,即輸入輸出,服務端與客戶端互動的過程也是一種 IO。
BIO
全稱 Blocking IO,即阻塞 IO,單個執行緒在處理單個請求時,如果當前請求沒有下一步操作,當前執行緒會被卡住,如果有另一個請求進來,當前執行緒是無法響應新來的請求的。
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8000);
while (true){
// accept 會阻塞
Socket accept = serverSocket.accept();
System.out.println("客戶端連線成功");
byte[] bytes = new byte[1024];
// read 也會阻塞
int read = accept.getInputStream().read(bytes);
if (read != -1) {
System.out.println("收到訊息:" + new String(bytes));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
缺點:一個執行緒只能處理一個請求,無法應對高併發場景
NIO
全稱 New IO,又稱 Non-Blocking IO,即非阻塞的 IO 模型。伺服器再處理 accept
和 read
等操作時,並不會阻塞當前執行緒,而是繼續執行下面的程式碼。
public static List<SocketChannel> CHANNEL_LIST = new ArrayList<>();
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
// 配置 socket 為非阻塞
serverSocketChannel.configureBlocking(false);
while (true) {
// 接受客戶端請求
SocketChannel accept = serverSocketChannel.accept();
if (accept != null) {
System.out.println("連線成功");
// 設定客戶端 socket 位非阻塞 ==> 讀操作是從這個 socket 裡面讀取
accept.configureBlocking(false);
// 把建立好連線的 socket 放入 List 中
CHANNEL_LIST.add(accept);
}
// 遍歷 List
for (SocketChannel socketChannel : CHANNEL_LIST) {
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("收到訊息:" + new String(byteBuffer.array()));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
改進:現在一個執行緒可以處理多個請求
缺點:1. 伺服器空轉,當沒有accept
或read
操作時,浪費 CPU 資源。
2. 已建立連線的 Socket List 無法快速定位當前傳送資料的 socket
Selector
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
// 設定為非阻塞
serverSocketChannel.configureBlocking(false);
// 建立 Selector ==> epoll_create
Selector selector = Selector.open();
// 將 server socket 註冊 ACCEPT 事件到 Seletor 中 ==> epoll_ctl
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 獲取事件 ==> epoll_wait 如果此時這個 Selector 監聽的 socket 沒有事件發生,則會掛起
// 處理方式類似 阻塞佇列中的 poll,佇列中沒資料時會 park 當前執行緒,來了資料會 unpark 當前執行緒
// 阻塞佇列中的資料寫入是其他執行緒操作的,而 epoll 中的事件寫入是系統層面進行的
selector.select();
// 獲取有事件產生的 Keys
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍歷 Keys
if (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果當前事件是連線事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel();
// 建立連線
SocketChannel accept = serverChannel.accept();
// 配置通道為非阻塞
accept.configureBlocking(false);
// 註冊當前通道到 selector 中,事件為 read
accept.register(selector, SelectionKey.OP_READ);
SocketAddress remoteAddress = accept.getRemoteAddress();
System.out.println("客戶端" + remoteAddress + "連線成功");
} else
// 如果事件是讀取事件(即客戶端傳送了資料)
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 讀取資料
int read = channel.read(byteBuffer);
if (read > 0) {
System.out.println("收到訊息:" + new String(byteBuffer.array()));
}
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
EpollSelectorImpl
linux 核心函式
- epoll_create:建立一個epoll的控制程式碼
- epoll_ctl:向epoll中註冊事件
- epoll_wait:返回 epoll 中已註冊的事件
EPollSelectorProvider.openSelector()
EPollSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
long pipeFds = IOUtil.makePipe(false);
fd0 = (int) (pipeFds >>> 32);
fd1 = (int) pipeFds;
// 建立一個裝 socket 控制程式碼的陣列
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
fdToKey = new HashMap<>();
}
void initInterrupt(int fd0, int fd1) {
outgoingInterruptFD = fd1;
incomingInterruptFD = fd0;
//將管道的讀取端註冊
epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
}
EpollSelector.implRegister
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
int fd = Integer.valueOf(ch.getFDVal());
fdToKey.put(fd, ski);
// fd 為當前 socket
pollWrapper.add(fd);
keys.add(ski);
}
EpollSelector.doSelect()
protected int doSelect(long timeout) throws IOException {
if (closed)
throw new ClosedSelectorException();
processDeregisterQueue();
try {
begin();
// 從 socket 陣列中取出事件 這一步如果沒有事件更新就會掛起
pollWrapper.poll(timeout);
} finally {
end();
}
processDeregisterQueue();
int numKeysUpdated = updateSelectedKeys();
if (pollWrapper.interrupted()) {
// Clear the wakeup pipe
pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
synchronized (interruptLock) {
pollWrapper.clearInterrupted();
IOUtil.drain(fd0);
interruptTriggered = false;
}
}
return numKeysUpdated;
}
int poll(long timeout) throws IOException {
//更新epoll事件,實際呼叫`epollCtl`加入到epollfd中
updateRegistrations();
//獲取已就緒的檔案控制程式碼
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
//如是喚醒檔案控制程式碼,則跳過,設定interrupted=true
for (int i=0; i<updated; i++) {
if (getDescriptor(i) == incomingInterruptFD) {
interruptedIndex = i;
interrupted = true;
break;
}
}
return updated;
}
private void updateRegistrations() {
synchronized (updateLock) {
int j = 0;
while (j < updateCount) {
int fd = updateDescriptors[j];
short events = getUpdateEvents(fd);
boolean isRegistered = registered.get(fd);
int opcode = 0;
if (events != KILLED) {
//已經註冊過
if (isRegistered) {
//修改或刪除
opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
} else {
//新增
opcode = (events != 0) ? EPOLL_CTL_ADD : 0;
}
if (opcode != 0) {
epollCtl(epfd, opcode, fd, events);
if (opcode == EPOLL_CTL_ADD) {
//增加到registered快取是否已註冊
registered.set(fd);
} else if (opcode == EPOLL_CTL_DEL) {
registered.clear(fd);
}
}
}
j++;
}
updateCount = 0;
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結