单线程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
并检查里面是否有内容很消耗资源,我们希望,是不是能够把这个遍历的任务交给操作系统函数去做,这样会不会更快?