java实践11之网络IO BIO和NIO(上)
java 网络IO也是java基础知识体系中很重要的一部分,java目前提供的网络编程模型有3种BIO、NIO、AIO。关于他们概念上的东西,阻塞、非阻塞、同步、异步这些概念就不说了,这个很多文章中已经分享了,下面主要分享一下使用方式,和我对他们的理解。(下面没有AIO的内容,这个没实际用过,没有深入研究过)
1 BIO:
BIO:BIO是一种同步阻塞网络处理模型。当客户端有连接请求时服务器端就需要启动一个线程进行处理。
BIO的使用demo
下面看一下他们的基本demo。
服务端:
public class BioServer extends Thread {
public static int port = 8080;
private ServerSocket serverSocket;
public BioServer(int port) {
try {
serverSocket = new ServerSocket(port);
} catch (Exception e) {
e.printStackTrace();
}
}
public void run() {
for (;;) {
try {
System.out.println("等待远程连接,监控端口" + serverSocket.getLocalPort() + "...");
Socket server = serverSocket.accept();
DataInputStream in = new DataInputStream(server.getInputStream());
String[] strs = in.readUTF().split(",");
System.out.println(strs[2]+"来了,说:" + strs[0] );
DataOutputStream out = new DataOutputStream(server.getOutputStream());
System.out.println(strs[2]+"好个粑粑");
out.writeUTF(strs[2]+"好个粑粑");
server.close();
} catch (Exception s) {
System.out.println("Socket timed out!");
break;
}
}
}
public static void main(String[] args) {
int port = BioServer.port;
Thread t = new BioServer(port);
t.run();
}
}
客户端
public class BioClient extends Thread {
public static void main(String[] args) {
// for(int i=0;i<100;i++){
Thread t1 = new BioClient("t"+0);
t1.start();
// }
}
private String name;
public BioClient(String name) {
this.name = name;
}
@Override
public void run() {
String host = "127.0.0.1";
int port = BioServer.port;
try {
System.out.println(name+"连接到主机:" + host + " ,端口号:" + port);
Socket client = new Socket(host, port);
// System.out.println("远程主机地址:" + client.getRemoteSocketAddress());
OutputStream outToServer = client.getOutputStream();
DataOutputStream out = new DataOutputStream(outToServer);
out.writeUTF("你好啊兄弟,我是,"+name);
InputStream inFromServer = client.getInputStream();
DataInputStream in = new DataInputStream(inFromServer);
System.out.println(name+"获取服务器响应: " + in.readUTF());
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
由于accept read write都是同步阻塞操作,这是无法改变,当多个客户端请求时,一个请求没处理完,则下一个请求一直在阻塞状态,所以一般不会这么用,会使用线程来进行异步处理,加快处理速度。
优化版使用线程池实现异步处理
由于串行执行请求,速度较慢,所以需要加入线程来异步处理,但是线程过多会导致线程切换、创建、销毁,会对系统造成极大的负担,所以一般使用线程池来优化服务端。
优化BIO处理模型
修改服务端代码:
public class BioServer extends Thread {
public static int port = 8080;
private ServerSocket serverSocket;
public BioServer(int port) {
try {
serverSocket = new ServerSocket(port);
} catch (Exception e) {
e.printStackTrace();
}
}
//使用线程池来优化BIO处理模型
ExecutorService es=Executors.newFixedThreadPool(10);
public void run() {
for (;;) {
try {
System.out.println("等待远程连接,监控端口" + serverSocket.getLocalPort() + "...");
Socket socket = serverSocket.accept();
//请求到了后放入线程池许处理
es.submit(new Weiyibu(socket));
} catch (Exception s) {
System.out.println("Socket timed out!");
break;
}
}
}
public static void main(String[] args) {
int port = BioServer.port;
Thread t = new BioServer(port);
t.run();
}
}
class Weiyibu implements Runnable {
Socket socket;
public Weiyibu(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
DataInputStream in = new DataInputStream(socket.getInputStream());
String[] strs = in.readUTF().split(",");
System.out.println(strs[2] + "来了,说:" + strs[0]);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
System.out.println(strs[2] + "好个粑粑");
out.writeUTF(strs[2] + "好个粑粑");
socket.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
在一般的系统中,此种方式,用的比较多,因为并发不是很大,支持异步处理请求,并且使用了线程池,避免了线程过多对系统造成的负担。
那么使用BIO+线程池不就异步,没问题了么?不是的,针对BIO的阻塞,我的理解是虽然在BIO中使用了线程,但是他的底层仍然是阻塞的。那么他具体提现在哪呢?
针对上面的模型,增加client3客户端和client4客户端,线程池为2。当线程池执行提交的client1和client2的请求时,如果速度很慢,那么client3和client4会一直在线程池中等待。
通过刚才的例子可以看到,虽然我们针对BIO增加了线程池,来异步处理,但是仍然没改变accept、write、read阻塞的本质。
为何已经异步处理了还说BIO是阻塞的?
BIO的阻塞不是说我们的程序是阻塞的,而是系统层面做的阻塞。我们可以从程序层面做多线程做异步,但是底层的阻塞是我们无法修改的。
例如:去餐厅吃饭。餐厅为每一个包房(client)分配一个服务人员(socket),服务人员对包房人员点餐完成后,请求后厨(java server)去做菜。由于包房-服务人员-后厨是绑定的,在后厨做饭的过程中,服务人员是阻塞的,一直在等菜完毕 返回给包房后,才能进行下一个的处理。 BIO的阻塞就体现在这里,服务人员等待菜品,是阻塞的。它是系统层面的,我们正常的优化javaserver方式,相当于优化后厨处理,但是无法修改 服务员等菜的操作,即系统层阻塞操作。
BIO带来的问题
从上面的描述中,我们可以看到虽然BIO可以使用线程池来加快处理速度,但是还有下列问题。
1、处理read、wirte、accept时是阻塞的,他是操作系统层面的,我们一般是无法修改的。
2、资源利用不合理。BIO阻塞主要体现在client、socket和java处理程序的线程是绑定的,当我们程序处理慢时,那么这一条线用户、socket、java处理线程会阻塞其他线程,没有更合理的利用资源。
3、BIO是单通道的。在实际使用中一次请求需要2个通道。由于读和写是互斥的,所以他们完全可以合并为1个双向通道。
大家是否还有疑问,只做读取,那么是否只需要1个通道就行了,为什么是2个?我理解为,读写是系统层面的定义,就算我们只读取数据,在实际请求中,比如只连接不做读写,那么也需要3次握手 ,即有读还有写,所以还是需要2个通道。
针对BIO带来的问题,下面我们来看看NIO是如何来优化处理这些问题的。
2 NIO
NIO是一种新的同步非阻塞IO模型,客户端发送的请求时,都会先放到表中,并增加监视功能,监视器会轮训表中的读写,有就绪的,则进行下一步处理。
下面是我对NIO处理流程的概念理解:
1、首先服务启动时操作系统会有开启请求处理线程监控网卡等硬件,看是否有请求到达,同步开启监视线程,监控请求存放表中是否有请求。
2、当请求处理线程收到请求后,会为请求创建channel通道,并放入表中。
3、同时监视线程请求表,当有就绪的channel则会通知javaserver来处理对应的事件。
4、这时,javaserver可以分配给对应的handler来处理对应的事件(可以使用同步或者线程池异步来处理对应的事件)。
下面是对应的demo
Server端口
public class NioServer {
public static int port = 8080;
// 创建一个selector
Selector selector;
public NioServer() {
try {
// 创建server
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定一个端口
serverSocketChannel.socket().bind(new InetSocketAddress(port));
// 开启一个选择器 根据操作系统不同 使用不同的选择器:
// 比如linux用的是epoll
selector = Selector.open();
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel 注册到选择器中 关心Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
// TODO: handle exception
}
}
public void start() throws Exception {
// 循环等待客户端连接
while (true) {
System.out.println("server:等待远程连接,监控端口" + port + "...");
// 阻塞等待关心的channel事件发生
selector.select();
// 如果有事件发生select>0 获取到相关事件的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
// 获取发生事件的key
SelectionKey selectionKey = iterator.next();
iterator.remove();
// 如果是连接请求事件
if (selectionKey.isAcceptable() && selectionKey.isValid()) {
this.accept(selectionKey);
continue;
// 服务端关心的可读,意味着有数据从client传来了,根据不同的需要进行读取,然后返回
}
if (selectionKey.isReadable() && selectionKey.isValid()) {
this.read(selectionKey);
continue;
}
// 实际上服务端不在意这个,这个写入应该是client端关心的
if (selectionKey.isWritable() && selectionKey.isValid()) {
this.write(selectionKey);
continue;
}
// 手动将selectionKey从集合中移除 防止重复操作
}
}
}
// 处理服务器写事件
private void write(SelectionKey selectionKey) throws Exception {
// 有channel可写,取出可写的channel
SocketChannel sc = (SocketChannel) selectionKey.channel();
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date())
+"开始处理客户端"+sc.getRemoteAddress()+"写请求");
Thread.sleep(1000);
// 设计非阻塞
sc.configureBlocking(false);
sc.write(ByteBuffer.wrap("wirete secsess".getBytes()));
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "写成功");
// 重新将channel注册到选择器上,设计为监听
sc.register(selector, SelectionKey.OP_READ);
}
// 处理读事件
private void read(SelectionKey selectionKey) throws Exception {
// 通过key反向获取对应的channel进行读取
SocketChannel sc = (SocketChannel) selectionKey.channel();
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date())
+"开始处理客户端"+sc.getRemoteAddress()+"读请求");
Thread.sleep(1000);
// try {
// 获取该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int len = sc.read(buffer);
if (len < 1) {
// 读不到东西抱异常了
selectionKey.cancel();
sc.close();
return;
}
String msg = new String(buffer.array(), 0, buffer.position());
buffer.clear();
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "收到客户端发送的数据:" + msg);
buffer = ByteBuffer.wrap("三花淡奶没了来个海克斯科技".getBytes());
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "返回:三花淡奶没了来个海克斯科技");
sc.write(buffer);
// } catch (Exception e) {
// System.out.println("没数据 关闭了");
// readSocketChannel.close();
// }
}
// 处理接收状态的通道
private void accept(SelectionKey selectionKey) throws Exception {
ServerSocketChannel serverSocketChannelAccept = (ServerSocketChannel) selectionKey.channel();
// 给客户端生成一个SocketChannel 非阻塞
SocketChannel sc = serverSocketChannelAccept.accept();
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date())
+"开始处理客户端"+sc.getRemoteAddress()+"连接请求");
Thread.sleep(1000);
// 设置客户端为非阻塞的
sc.configureBlocking(false);
// 将与客户端连接的socketChannel也注册到selector中 同时给 Channel关联一个buffer
sc.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
ByteBuffer buff = ByteBuffer.wrap("connect secsess".getBytes());
Thread.sleep(1000);
System.out.println("server:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "接到请求与客户端连接成功");
sc.write(buff);
}
public static void main(String[] args) throws Exception {
new NioServer().start();
}
}
client端:
public class NioClient {
public static void main(String[] args) throws Exception {
NioClient client = new NioClient();
client.client();
}
Selector selector;
SocketChannel socketChannel;
public NioClient() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
// 设置异步连接
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
socketChannel.connect(new InetSocketAddress("127.0.0.1", NioServer.port));
System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "请求连接服务端");
} catch (Exception e) {
// TODO: handle exception
}
}
public void client() throws Exception {
// 判断连接
while (true) {
selector.select();
Iterator<SelectionKey> setKeys = selector.selectedKeys().iterator();
while (setKeys.hasNext()) {
SelectionKey setKey = setKeys.next();
setKeys.remove();
// 如果与服务端连上了 回先走这里,因为上边是异步
if (setKey.isConnectable()) {
connect(setKey);
// continue;
}
if (setKey.isReadable()) {
read(setKey);
// continue;
}
if (setKey.isValid() && setKey.isWritable()) {
write(setKey);
// continue;
}
if (isExit) {
System.out.println("客户端完毕 退出");
return;
}
}
}
}
private void write(SelectionKey setKey) throws Exception {
Thread.sleep(1000);
SocketChannel client = (SocketChannel) setKey.channel();
System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "向客户端请求三花淡奶");
ByteBuffer buff = ByteBuffer.wrap("三花淡奶".getBytes());
client.write(buff);
client.register(selector, SelectionKey.OP_READ);
}
boolean isExit = false;
private void read(SelectionKey setKey) throws Exception {
Thread.sleep(1000);
SocketChannel client = (SocketChannel) setKey.channel();
ByteBuffer bf = ByteBuffer.allocateDirect(1024);// 创建缓冲区大小
client.read(bf);
byte[] bytes = new byte[bf.position()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = bf.get(i);
}
String msg = new String(bytes);
System.out.println("client:" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "接收服务端返回的数据" + msg);
if (msg.equals("三花淡奶没了来个海克斯科技")) {
socketChannel.close();
setKey.cancel();
isExit = true;
// selector.close();
return;
}
client.register(selector, SelectionKey.OP_WRITE);
}
private void connect(SelectionKey setKey) throws Exception {
// 获取客户端通道
SocketChannel client = (SocketChannel) setKey.channel();
if (client.isConnectionPending()) {
client.finishConnect();
client.register(selector, SelectionKey.OP_READ);
}
// System.out.println(new String(buffer.array()));
}
}
在上面的例子中,server端,为什么select还是阻塞的,为什么说NIO是非阻塞的,它的非阻塞提现在哪?
篇幅有限,请看下章java实践12之网络IO BIO和NIO(下)