1. 概述
- 继续开始Netty框架之旅,本文仍然还没有进入到Netty框架使用中。
- 在那之前,我们一起来看看Java的Socket编程,Netty是基于NIO实现的,而原生的NIO是什么样的呢。
- 这篇文章中,我们将会看到2种编程模式的特点以及优劣性的对比。
2. BIO实现客户端与服务端通信代码
这里将实现一个客户端和一个服务端,客户端发起一次请求,服务端接收请求并返回处理结果。
下文将根据这段代码说明BIO存在的问题。
- 首先是服务端代码:
/**
* @author GrainRain
* @date 2020/05/19 20:10
*
* Description BIOServer
**/
public class BIOServer {
public static final int PORT = 8090;
public static void main(String[] args){
ServerSocket server = null;
try {
server = new ServerSocket(PORT);
System.out.println("the server is started");
while (true) {
//每获取一个连接,将socket交给handler处理,此处发生阻塞
Socket socket = server.accept();
//此处可以采用线程池处理
new Thread(new BIOMessageHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(server != null) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
server = null;
}
}
}
}
}
- 服务端消息处理器(本质上是一个线程)
/**
* @author GrainRain
* @date 2020/05/19 20:26
**/
public class BIOMessageHandler implements Runnable {
private Socket socket;
public BIOMessageHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
in = socket.getInputStream();
byte[] bytes = new byte[2 << 20];
StringBuilder builder = new StringBuilder();
int len = -1;
while ((len = in.read(bytes)) != -1){
builder.append(new String(bytes,0,len));
}
System.out.println("receive message,the data is >>" + builder.toString());
out = socket.getOutputStream();
out.write("receive success, now time is"+ System.currentTimeMillis()).getBytes());
}catch (Exception e){
e.printStackTrace();
}finally {
close(in,out);
}
}
public void close(AutoCloseable ... autoCloseables){
if(autoCloseables==null){
return;
}
for (int i = 0; i < autoCloseables.length; i++) {
AutoCloseable autoCloseable = autoCloseables[i];
try {
autoCloseable.close();
} catch (Exception e) {
e.printStackTrace();
}finally {
autoCloseable = null;
}
}
}
}
- 从以上代码可以看出,每获取一个连接,我们都为其分配一个线程进行处理。就算是在空闲状态,也就是说客户端只是单纯的连接但没有进行数据通信,那么这个线程也并不能被释放,始终占用着系统资源。当大量请求涌入时,过多的线程将导致系统资源耗竭。
- 当然,我们可以通过线程池使得一组线程处理多个请求,实现一种伪异步模型。尽管这样不会导致系统线程过多导致资源耗竭。但本质上,由于同步阻塞模型的限制,读写过程都是处于阻塞状态,当客户端网络很慢时,将会导致线程资源持续的占用,如果线程池正在执行的都是这样的劣质慢任务,那么大量的等待线程被挂起而得不到处理,最终产生超时,此时给外部的感觉是服务已经宕掉了。有句话说的很对,当服务的性能依赖于大量良莠不齐的客户端,性能无法得到保障。
- 但是,代码确实非常的简单便于理解。对于并发并不大的服务而言,还是值得使用的。
- 客户端代码
- 客户端就更为简单了,实现一个发送数据的功能。
/**
* @author GrainRain
* @date 2020/05/19 20:32
* Description BIO客户端
**/
public class BIOClient {
public static final String HOST = "127.0.0.1";
public static final int PORT = 8090;
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
OutputStream out = null;
try{
socket = new Socket(HOST,PORT);
out = socket.getOutputStream();
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.write("I'm client".getBytes());
//通知已完成写出
socket.shutdownOutput();
String back = in.readLine();
//通知已完成读入数据
socket.shutdownInput();
System.out.println("the client receive message >> " + back);
}catch(Exception e){
e.printStackTrace();
}finally {
}
}
public void close(AutoCloseable ... autoCloseables){
if(autoCloseables==null){
return;
}
for (int i = 0; i < autoCloseables.length; i++) {
AutoCloseable autoCloseable = autoCloseables[i];
try {
autoCloseable.close();
} catch (Exception e) {
e.printStackTrace();
}finally {
autoCloseable = null;
}
}
}
}
3. NIO实现客户端与服务端通信代码
-
Java从1.7开始,对NIO的API进行了重大升级,实现了真正意义上的NIO支持。
-
Java NIO引入了3大的组件,包括buffer(缓冲区)、Channel(通道)、Selector(多路复用选择器)。通过这几大组件的分工协作,能够实现多路复用的非阻塞通信模型。
几大组件的详细介绍在此不做过多说明,读者可自行百度或翻阅源码。 -
以下是基于这几大组件实现的NIO模式的客户端与服务端代码。功能实现上上,仍然同前文的同步阻塞模型(BIO)一样。
- 首先是服务端代码,首先是运行一个多路复用器线程,进行事件监听。
/**
* @author GrainRain
* @date 2020/05/19 21:38
**/
public class NIOServer {
public static final int PORT = 8090;
public static void main(String[] args) {
new Thread(new MessageSelector(PORT)).start();
}
}
上面这段非常简单,就是启动一个多路复用选择器,通过多路复用选择器轮询监听Channel,将活跃的事件进行处理。
2. 以下是多路复用选择器线程,这部分非常关键。
/**
1. @author GrainRain
2. @date 2020/05/19 21:41
**/
public class MessageSelector implements Runnable{
private int port;
/**
* 多路复用选择器
* 通过selector实现一个线程处理多个请求
*/
private Selector selector;
private boolean isKeepAlive;
private ServerSocketChannel serverSocketChannel;
/**
* 消息处理器
*/
private NIOMessageHandler nioMessageHandler;
public MessageSelector(int port){
this.port = port;
nioMessageHandler = NIOMessageHandler.getInstance();
}
@Override
public void run() {
registrySelector();
this.isKeepAlive(true);
while (isKeepAlive){
try {
//轮询
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.stream().forEach(selectionKey -> {
try {
//将轮询到的活跃请求交给handler处理
this.selectKeyHandler(selectionKey);
} catch (Exception e) {
if (selectionKey != null) {
selectionKey.cancel();
if (selectionKey.channel() != null) {
try {
selectionKey.channel().close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
});
}catch (Throwable e){
e.printStackTrace();
}
}
}
public void registrySelector(){
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
//注册服务端serverSocketChannel的accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (Exception e){
e.printStackTrace();
System.exit(1);
}
}
public boolean isKeepAlive(boolean isKeep){
this.isKeepAlive = isKeep;
return isKeep;
}
public void selectKeyHandler(SelectionKey selectionKey) throws IOException {
//新连接请求,获取连接,注册读事件
if (selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
//设为非阻塞
socketChannel.configureBlocking(false);
//注册读事件
socketChannel.register(selector,SelectionKey.OP_READ);
}
//如果是读事件,则交给另一组线程池进行业务逻辑处理
if(selectionKey.isReadable()){
SocketChannel socketChannel = null;
try {
socketChannel = (SocketChannel) selectionKey.channel();
nioMessageHandler.socketHandle(socketChannel);
}catch (Exception e){
socketChannel.close();
selectionKey.cancel();
}finally {
socketChannel = null;
}
}
}
}
- 以上过程启动了一个selector进程,用于监听所有注册的Channel,当通道中相应事件被激活时,则对应该事件进行处理。
- 可以看出,此处实现了一个线程处理多个请求,对于一些慢任务逻辑,可以直接转交给一个专门的线程池进行处理。
在这种模型下,由于缓冲区等组件的引入,并不会因为客户端通信问题而导致服务端的线程被阻塞。整个过程是非阻塞的,服务端只需要从触发事件的socket获取buffer中的数据进行处理即可。
由此,真正实现了异步通信的要求,服务端性能不再受客户端制约。 - 以上就是NIO的核心代码,而数据处理器代码和客户端代码相对简便,此处客户端仅给出本测试案例的实现,并不是NIO应有的实现模式。
- 数据处理器
/**
* @author GrainRain
* @date 2020/05/19 22:12
**/
public class NIOMessageHandler {
private static NIOMessageHandler nioMessageHandler = new NIOMessageHandler();
private ExecutorService threadPool;
private NIOMessageHandler(){
//初始化线程池
threadPool = new ThreadPoolExecutor(2,
5,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1024));
}
public static NIOMessageHandler getInstance(){
return nioMessageHandler;
}
public void socketHandle(SocketChannel socketChannel){
if(socketChannel==null){
return;
}
threadPool.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//数据读取
int read = socketChannel.read(byteBuffer);
if (read > 0) {
Buffer buffer = byteBuffer.flip();
byte[] bytes = new byte[buffer.remaining()];
byteBuffer.get(bytes);
String msg = new String(bytes);
System.out.println("the server receive message >>" + msg);
String sendMsg = "hello, I'm server!!!";
//写出数据
socketChannel.write(ByteBuffer.wrap(sendMsg.getBytes()));
}
return null;
}
});
}
}
- 客户端代码实现
客户端就只是单纯的连接服务端,发送数据和接收数据,并没有做更多的考虑。
/**
* @author GrainRain
* @date 2020/05/19 22:28
**/
public class NIOClient {
public static final int PORT = 8090;
public static final int LOCAL_PORT = 8081;
public static final String IP = "127.0.0.1";
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
socketChannel.bind(new InetSocketAddress(LOCAL_PORT));
//设为非阻塞
socketChannel.configureBlocking(false);
//连接服务端
socketChannel.connect(new InetSocketAddress(IP,PORT));
while (!socketChannel.isConnected()){
socketChannel.finishConnect();
}
//发送数据
socketChannel.write(ByteBuffer.wrap("hello server, I'm client".getBytes()));
Thread.sleep(100);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取数据
socketChannel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
socketChannel = null;
}
}
}
}
4. 总结
- 以上是有关Java BIO与NIO实现客户端与服务端案例,并介绍了两种模式的优缺点。
- 可以看出NIO虽好,但是代码也确实复杂很多,开发难度较大,而且有许多需要考虑的问题。而Netty的出现将这些麻烦的事情都给处理好了,提供了简单易用的API供我们使用。
- 下一篇将开始正式进入Netty,看看Netty为我们做了哪些工作。希望各位大佬能给出您宝贵的意见,共同交流学习。