简介
==
- 随着IO类库不断完善,我们基于Java的网络编程学习成本越来越低。之前网络开发都是底层语言编写的,像C语言,C 。其实通过Java也可以编写。Java的io类基于tcp连接提供的socket就是我们实现网络开发的桥段。但是因为是同步阻塞式,所以效率上来说就慢了很多。在Java7之后提供了异步阻塞式io。为我们拉开了一个新天地。
BIO
===
- 什么是网络编程?所谓的网络编程其实就是C/S模型。大家都知道Java是开发B/S模型的。C/S实际是client和server端的开发。说白就是两个进程相互通信。client通过tcp连接server然后交互数据
- BIO 即同步阻塞是编程。在JDK1.4之前我们网络连接都是采用的BIO模式,服务端通过ServerSocket(port)构建服务端。然后服务端通过accept方法阻塞式等待客户端的连接。客户端通过Socket(ip,port)构建客户端。然后通过PrintWriter传递消息。
- 默认情况下BIO模式是一个客户端对应一个线程。这样对于内存的消耗是严重的。慢慢的这种方式也就被抛弃了。
server
- 为了缓解线程压力。这里构造线程池。初始大小5个
private static final ThreadLocal<ExecutorService> executorService = new ThreadLocal<ExecutorService>() {
@Override
protected ExecutorService initialValue() {
return Executors.newFixedThreadPool(5);
}
};
- 然后是构建ServerSocket。然后就是一直在阻塞式等待客户端的连接。什么叫阻塞式等待。就是一直在等待客户端连接。没有新的客户端连接就不继续执行。当客户端连接后。accept就会返回当前客户端的连接对象。然后我们将他进行封装后放到线程池中。线程池中有可用资源就会将可用线程加载这个客户端对象。
public static void start() throws IOException {
try {
// 通过构造函数创建ServerSocket
server = new ServerSocket(HostConstant.PORT);
System.out.println("服务器已启动,端口号:" HostConstant.PORT);
while (true) {
// 真正处理的还是Socket
Socket socket = server.accept();// 阻塞方法
// 把客户端请求打包成一个任务,放到线程池执行
executorService.get().execute(new ServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
server.close();
}
}
}
代码语言:java复制public class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
String message;
String result;
// 通过输入流读取客户端传输的数据
while ((message = br.readLine()) != null) {
System.out.println("server receive data:" message);
result = response(message);
// 将业务结果通过输出流返回给客户端
pw.println(result);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
// 返回给客户端的应答
public static String response(String msg) {
return "Hello," msg ",Now is " new java.util.Date(System.currentTimeMillis()).toString();
}
}
##client
代码语言:java复制public class BIOClient {
public void startConnect() {
try {
Socket socket = new Socket(HostConstant.IP, HostConstant.PORT);
new ReadMsg(socket).start();
PrintWriter pw = null;
// 写数据到服务端
pw = new PrintWriter(socket.getOutputStream());
pw.println(UUID.randomUUID());
pw.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class ReadMsg extends Thread {
Socket socket;
public ReadMsg(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
String line = null;
// 通过输入流读取服务端传输的数据
while ((line = br.readLine()) != null) {
System.out.printf("%sn", line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Integer time = 6;
for (Integer i = 0; i < time; i ) {
new BIOClient().startConnect();
}
}
}
缺点
--
- 上面服务端和客户端实际上是伪异步。表面上看起来不会因为客户端的增加导致内存溢出。但是因为实际上还是accpet同步阻塞等待。所以在连接性能上还是不好。
- 我们在ServerHanndler中是读取客户端传输的数据通过BufferedReader.readLine这个方法。我们跟踪下去发现实际调用的是InputStream.read这个方法。
/**
* Reads the next byte of data from the input stream. The value byte is
* returned as an <code>int</code> in the range <code>0</code> to
* <code>255</code>. If no byte is available because the end of the stream
* has been reached, the value <code>-1</code> is returned. This method
* blocks until input data is available, the end of the stream is detected,
* or an exception is thrown.
*
* <p> A subclass must provide an implementation of this method.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
*/
public abstract int read() throws IOException;
- 我们在翻阅他们的源码说明,会发现上面注释大意为:当没有字节的时候意味着已经结束了我们会返回-1。这个方法一直阻塞知道获取到字节或者是结束或者是抛出异常。我们之前也说了服务端等待连接的时候是阻塞式等待。这会造成客户端连接的一些问题。但是客户端连接上以后开始通信了。服务端获取客户端的消息也是采用阻塞式等待的。这会直接造成交互等待从而造成交互拥堵。换句话说客户端A发送了100条消息。服务端会一条一条处理。像食堂打菜排队一样。
- 同样的道理OutputStream也是阻塞式的。这里有兴趣的读者可以自行翻阅源码查看。这也说明我们连接是阻塞的。通信也是阻塞的。这就是BIO暴露的缺点。
- 因为阻塞,会造成后期客户端的接入无法成功,会一直等待。造成连接超时。
NIO
===
NIO=Non Block IO .即非阻塞式编程。
- 在BIO中我们通过ServerSocket、Socket实现服务端和客户端的开发。在NIO中对应提供了ServerSocketChannel、SocketChannel两个类实现通信。这两个类支持阻塞式和非阻塞式模式。阻塞模式这里不做介绍造成的后果由和BIO一样。下面我们来看看如何实现NIO
##ByteBuffer
- ByteBuffer使我们NIO通信的一个缓冲区,我们的读写都是借助与它传递的。因为他提供了类似指针指向,我们操作指向就可以获取到字节。
##Channel
- 在NIO中他被看做是一个通道。通过Channel控制读和写。BIO中是通过Stream方式传递的。Channel和Stream相比最大的特点Channel是双向的。简单的理解就是读用Channel,写也用Channel。在BIO中InputStream和OutputStream分别负责读和写。上面提到的
ServerSocketChannel
和SocketChannel
都是Channel的子类。
##Selector
-多路复用器。这里我们可以理解为注册中心。所有的handler再向selector注册的时候会带上标签(SelectorKey)。在某个Channel发生读或写的事件时这个Channel会处于就绪状态。在Selector轮询的时候会筛选出来。然后我们在根据SelectorKey判断监听的是什么事件。从而做出处理。查阅资料得知selector没有连接限制。理论上一个selector可以管理N个Channel。
- 上面是服务器的开发流程。其实就是用一个线程来管理selector。然后不停遍历selector然后捕获事件。服务端和客户端捕获不同的事件。下面大概列举了下事件
- 框架搭建就是流程上的,剩下的就是我们在selectorKey这个事件上进行业务的io处理。在读写期间就是ByteBuffer来实现。NIO的实现和BIO相对比较复杂。
进化
==
关于NIO2.0 和nettry 他们在次基础上进行提升!不得不说现在基本上是netty的天下了。下章节我们针对netty来展开讨论!
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!