BIO(同步阻塞式IO)
由服务器提供ip port,客户端通过连接操作向服务端发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行相应的逻辑处理。处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通信模型。
该模型存在的问题是当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,导致系统线程急剧增加,系统性能严重下降。
改进方式:可以把new Thread 改成使用ThreadPool
package org.xxz.bio;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8765);
System.out.println("server start...");
// 这里是阻塞状态
Socket socket = serverSocket.accept();
new Thread(new ServerHandler(socket)).start();
serverSocket.close();
}
}
package org.xxz.bio;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream());
String line = in.readLine();
System.out.println(line);
out.println("服务端回写消息");
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
if (socket != null)
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package org.xxz.bio;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
PrintWriter out = null;
BufferedReader in = null;
try {
Socket socket = new Socket("localhost", 8080);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream());
out.println("客户端发送消息");
out.flush();
String line = in.readLine();
System.out.println(line);
socket.close();
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
if(out != null)
out.close();
if(in != null)
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
NIO(同步非阻塞IO)
NIO模型:服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
介绍几个重点的概念:
Buffer 缓冲区
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。
有这些常用的Buffer:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer 没有Boolean类型的Buffer哦!
Channel 通道
channel通道,可以比喻成水管,用于读、写和同时读写操作。
Channel分两类:
- SelectableChannel:用户网络读写
- FileChannel:用于文件操作
后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
Selector 多路复用器
Selector是Java NIO 编程的基础。
Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
package org.xxz.nio;
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.Iterator;
public class Server implements Runnable {
// 1.多路复用器(管理所有的通道)
private Selector selector;
// 2.建立缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
public Server(int port) {
try {
// 1.打开多路复用器
this.selector = Selector.open();
// 2.打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 3.设置服务器通道为非阻塞
ssc.configureBlocking(false);
// 4.绑定地址
ssc.bind(new InetSocketAddress(port));
// 5. 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port : " + port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true) {
try {
// 1.必须让多路复用器开始监听
this.selector.select();
// 2. 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
// 3.进行遍历
while(keys.hasNext()) {
// 4. 获取一个选择的元素
SelectionKey key = keys.next();
// 5.直接从容器中移除就可以了
keys.remove();
// 6. 如果是有效的
if(key.isValid()) {
// 7. 如果为阻塞状态
if(key.isAcceptable()) {
this.accept(key);
}
// 8.如果为可读状态
if(key.isReadable()) {
this.read(key);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void read(SelectionKey key) {
try {
// 1.清空缓冲区旧的数据
this.readBuf.clear();
// 2.获取之前注册的socket通道
SocketChannel sc = (SocketChannel) key.channel();
// 3.读取数据
int count = sc.read(this.readBuf);
// 4.如果没有数据
if(count == -1) {
key.channel().close();
key.cancel();
return ;
}
// 5.有数据则进行读取,读取之前需要进行复位方法(把position 和 limit进行复位)
this.readBuf.flip();
// 6.根据缓冲区的数据长度创建相应大小的byte数组,接受缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
// 7.接收缓冲区数据
this.readBuf.get(bytes);
// 8.打印结果
String body = new String(bytes).trim();
System.out.println("server : " + body);
// 9. 可以回写给客户端数据
} catch (Exception e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
// 1. 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 2. 执行阻塞方法
SocketChannel sc = ssc.accept();
System.out.println("accept " + sc);
// 3. 设置阻塞模式
sc.configureBlocking(false);
// 4.注册到多路复用器上,并设置读取标识
sc.register(this.selector, SelectionKey.OP_READ);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8765)).start();
}
}
package org.xxz.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
final static String HOST = "localhost";
final static int PORT = 8765;
public static void main(String[] args) {
// 创建连接地址
InetSocketAddress address = new InetSocketAddress(HOST, PORT);
// 声明连接通道
SocketChannel sc = null;
// 建立缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
// 打开通道
sc = SocketChannel.open();
// 进行连接
sc.connect(address);
while(true) {
// 定义一个字节数组,然后使用系统录入功能
byte[] bytes = new byte[1024];
System.in.read(bytes);
// 把数据放到缓冲区
buf.put(bytes);
// 对缓冲区进行复位
buf.flip();
// 写出数据
sc.write(buf);
// 清空缓冲区数据
buf.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(sc != null) {
try {
sc.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
AIO(异步非阻塞IO)
异步的套接字通道时真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO)。他不需要过多的Selector对注册的通道进行轮询即可实现异步读写,从而简化了NIO的编程模型。
package org.xxz.aio;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
// 线程池
private ExecutorService executorService;
// 线程组
private AsynchronousChannelGroup threadGroup;
// 服务器通道
public AsynchronousServerSocketChannel assc;
public Server(int port) {
try {
// 创建一个缓存线程池
executorService = Executors.newCachedThreadPool();
// 创建一个线程组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
// 创建一个服务器通道
assc = AsynchronousServerSocketChannel.open(threadGroup);
// 绑定端口
assc.bind(new InetSocketAddress(port));
System.out.println("server start, port : " + port);
// 进行阻塞
assc.accept(this, new ServerCompletionHandler());
// 一直阻塞 不让服务器停止
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Server(8765);
}
}
package org.xxz.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Server> {
@Override
public void completed(AsynchronousSocketChannel asc, Server attachment) {
// 当有下一个客户端接入的时候 直接调用Server的accept方法, 这样反复执行下去,保证多个客户端都可以阻塞
attachment.assc.accept(attachment, this);
read(asc);
}
@Override
public void failed(Throwable exc, Server attachment) {
exc.printStackTrace();
}
private void read(final AsynchronousSocketChannel asc) {
// 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
// 进行读取之后,重置标识位
attachment.flip();
// 获取读取的字节数
System.out.println("server receive data length is " + resultSize);
// 获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("server recevie data is " + resultData);
String response = "server response " + resultData;
write(asc, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
private void write(AsynchronousSocketChannel asc, String response) {
try {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(response.getBytes());
buf.flip();
asc.write(buf).get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
package org.xxz.aio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
public class Client implements Runnable {
private AsynchronousSocketChannel asc;
public Client() throws Exception {
asc = AsynchronousSocketChannel.open();
}
public void connect() {
asc.connect(new InetSocketAddress("localhost", 8765));
}
public void write(String req) {
try {
asc.write(ByteBuffer.wrap(req.getBytes())).get();
read();
} catch (Exception e) {
e.printStackTrace();
}
}
public void read() {
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
asc.read(buf).get();
buf.flip();
String data = new String(buf.array()).trim();
System.out.println(data);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
// 保证程序不停止
while(true) {
}
}
public static void main(String[] args) throws Exception {
Client c1 = new Client();
c1.connect();
Client c2 = new Client();
c2.connect();
Client c3 = new Client();
c3.connect();
new Thread(c1, "c1").start();
new Thread(c2, "c2").start();
new Thread(c3, "c3").start();
Thread.sleep(1000);
c1.write("c1 aaa");
c2.write("c2 bbbb");
c3.write("c3 ccccc");
}
}
在实际项目中一般不会用这些jdk自带的api来实现网络通信,但是这些基本的概念需要进行了解。
实际项目中一般使用netty 或者 mina来做网络通信。
请关注后续netty使用。