單線程bio
我們首先要解決的問題是:爲什麼要使用nio?
nio除了叫new io
之外,還叫做non-blocking io
,也就是非阻塞io,可見這一塊的重要性。
阻塞與非阻塞,我們主要講網絡編程。
傳統的BIO(Blocking IO):
寫一個server:
package nio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
byte[] contents = new byte[1024];
ServerSocket serverSocket = new ServerSocket(8088);
while (true){
System.out.println("--------------------split---------------------------");
System.out.println("waiting connection from client....");
Socket socket = serverSocket.accept();
System.out.println("connected!");
System.out.println("waiting data from client....");
InputStream inputStream = socket.getInputStream();
int read = inputStream.read(contents);
if(read>0){
System.out.println("data from client has been received!");
System.out.println("the data is: " + new String(contents));
}
}
}
}
這裏有兩個地方會阻塞:
Socket socket = serverSocket.accept();
只有當客戶端來連接,阻塞纔會解除。
InputStream inputStream = socket.getInputStream();
int read = inputStream.read(contents);
從客戶端讀數據也會阻塞。
我寫兩個客戶端(或者你一個客戶端run兩次也可以):
package nio;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",8088);
System.out.println("Client tries to connect server...");
Scanner scanner = new Scanner(System.in);
System.out.println("please write something...");
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
System.out.println("***data from client has sent!");
}
}
Client.java
package nio;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class Client2 {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8088);
System.out.println("Client2 tries to connect server...");
Scanner scanner = new Scanner(System.in);
System.out.println("please write something...");
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
System.out.println("***data from client has sent!");
}
}
Client2.java
開啓server,先用client連。
--------------------split---------------------------
waiting connection from client....
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
連上了。
但是卡在等待數據那裏了。
這時候我們開啓client2。
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
server沒有反應,表示沒有連上。
對於client2來說,是卡在accept那裏了。
如果此時client發送數據了:
Client tries to connect server...
please write something...
client
***data from client has sent!
Process finished with exit code 0
那麼client2就會連上:
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
data from client has been received!
the data is: client
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
這是一個問題。
一個客戶端不可能只發一條數據,它應該保持發送數據的狀態,所以我們在client上面加上while(true)
:
package nio;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8088);
System.out.println("Client tries to connect server...");
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("please write something...");
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
System.out.println("***data from client has sent!");
}
}
}
重啓server再次測試:
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
讓client連接上:
Client tries to connect server...
please write something...
hello
***data from client has sent!
please write something...
當client發送一條數據後,它還保持着發送數據的狀態。
--------------------split---------------------------
waiting connection from client....
connected!
waiting data from client....
data from client has been received!
the data is: hello
--------------------split---------------------------
waiting connection from client....
但是server接受到一條數據後就斷開與client的連接了,它又在等待一個新的連接,client此時再發數據server是接收不到的。
由此我們得出一個結論:單線程無法解決併發問題。
多線程bio
我們的思路是通過多線程解決。
一個client連過來,服務端就會產生一個socket,此時開一條線程將socket傳進去,以此與client通信。
package nio;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
int count = 0;
public static void main(String[] args) throws IOException {
Server server = new Server();
ServerSocket serverSocket = new ServerSocket(8088);
while (true) {
System.out.println("--------------------split---------------------------");
System.out.println("waiting connection from client....");
final Socket socket = serverSocket.accept();
server.count++;
Thread thread = new Thread(new IOWithClient(server.count,socket));
thread.start();
}
}
}
class IOWithClient implements Runnable{
int count;
Socket socket;
byte[] contents = new byte[1024];
public IOWithClient(int count, Socket socket) {
this.count = count;
this.socket = socket;
}
@Override
public void run() {
try {
System.out.println("client" + (count) + " connected!");
System.out.println("waiting data from client" + (count) + "....");
while (true) {
InputStream inputStream = socket.getInputStream();
int read = inputStream.read(contents);
if (read > 0) {
System.out.println("data from client"+(count)+" has been received!");
System.out.println("the data is: " + new String(contents));
}
}
} catch (IOException e) {
if(e.getMessage().equals("Connection reset")){
System.out.println("client" + count + " disconnected...");
}else{
e.printStackTrace();
}
}
}
}
我這裏把task拎出來結構會不會清晰一點。
這裏我用了count
來保存第幾個客戶端連過來了,目的是爲了控制檯打印的時候清晰一點。
這當然能夠解決所有的問題(併發的問題),但是它太消耗資源了。
new一個thread,可是很耗內存的。況且,成千上萬個thread,最終能有信息通信的又有幾個呢?
比如你上淘寶,淘寶爲每一個用戶都new一個thread來處理,真正發生數據交互的(比如買東西)會有多少?大多數人只是隨便逛逛。
就算是用線程池來重用線程,還是多線程。我們需要用一個單線程來解決問題!
這時候,nio就登場了。
(難道bio就沒用了嗎?如果客戶端和服務端的io很頻繁,100個連接有99個一直在傳輸數據,當然是可以用bio的)。
nio的模型
我們已經學過FileChannel
與ByteBuffer
了,在網絡通信中,數據的傳遞同樣是建立channel
。
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9090));
System.out.println("Client tries to connect server...");
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("please write something...");
String next = scanner.next();
byteBuffer.put(next.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
System.out.println("***data from client has sent!");
}
}
}
Client.java
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class Client2 {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9090));
System.out.println("Client2 tries to connect server...");
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("please write something...");
String next = scanner.next();
byteBuffer.put(next.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
byteBuffer.clear();
System.out.println("***data from client2 has sent!");
}
}
}
Client2.java
客戶端這裏我們通過SocketChannel去連接遠程機器。
連上以後會卡在scanner那裏。
然後是server部分:
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* non-blocking server
*/
public class TomcatServer {
static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) {
SocketChannel socketChannel = null;
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress(9090);
serverSocketChannel.bind(socketAddress);
serverSocketChannel.configureBlocking(false);
while(true){
//check if there's any information from the connected client
//if there is, print them.
//else,do nothing,and continue checking
Iterator<SocketChannel> iterator = channelList.iterator();
int read = 0;
while(iterator.hasNext()) {
try {
read = iterator.next().read(byteBuffer);
} catch (IOException e) {
System.out.println(e.getMessage());
iterator.remove();
System.out.println("the number of the clients are: " + channelList.size());
}
if (read > 0) {
String data = new String(byteBuffer.array(),0,read);
System.out.println("the data from client is : " + data);
byteBuffer.clear();
}
}
//this is non-block
//whether there's client trying to connect the server, following codes will be executed
socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
System.out.println("connected!");
//make socket non-block
//thus IO between server and client has no blocking.
socketChannel.configureBlocking(false);
//for further checking
channelList.add(socketChannel);
System.out.println(channelList.size() + " clients have connected the server.");
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(socketChannel != null){
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(serverSocketChannel != null){
try {
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
首先代碼裏面除了main線程沒有其他thread,所以是單線程的。
每來一個連接,都會建立一條channel
,所以我們用一個channelList
來存儲所有連上的客戶端。
serverSocketChannel.configureBlocking(false);
首先我們確保serverSocketChannel
的非阻塞,這樣一個client連上後,另一個也能連。換句話說,socketChannel = serverSocketChannel.accept();
不會阻塞。
然後進入while true
。
邏輯是這樣的:取出所有已經連上的channel
,一個個遍歷,裏面要是有信息,就打印出來,要是沒有就算了。
這裏的while true
保證服務端一直監聽客戶端消息的狀態。
我們可以想想,如果不這麼設計的話,如果是accept
之後再read
,這麼做會有問題的:
代碼實現是:
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* non-blocking server
*/
public class TomcatServer {
static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) {
SocketChannel socketChannel = null;
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress(9090);
serverSocketChannel.bind(socketAddress);
serverSocketChannel.configureBlocking(false);
while (true) {
socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
System.out.println("connected!");
//make socket non-block
//thus IO between server and client has no blocking.
socketChannel.configureBlocking(false);
//for further checking
channelList.add(socketChannel);
System.out.println(channelList.size() + " clients have connected the server.");
int read = socketChannel.read(byteBuffer);
if (read > 0) {
System.out.println("content from client is: " + new String(byteBuffer.array()));
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socketChannel != null) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (serverSocketChannel != null) {
try {
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
就算有一個客戶端連上了,也不能收到客戶端發來的消息,因爲一直while
循環,所以我們要把讀取客戶端消息的代碼提到前面去。
socketChannel.configureBlocking(false);
保證了與客戶端io的非阻塞,即read = next.read(byteBuffer);
是非阻塞的。
遺留的問題
以上,用單線程實現了併發,即是所謂的nio。然而,這依然是有問題的,因爲死循環裏面遍歷channelList
並檢查裏面是否有內容很消耗資源,我們希望,是不是能夠把這個遍歷的任務交給操作系統函數去做,這樣會不會更快?