public class BioClient {
private static final int PORT = Utils.PORT;
private static final String IP = Utils.IP;
static PrintWriter printWriter = null;
public static void main(String[] args) throws IOException {
Socket socket = new Socket(IP, PORT);
System.out.println("Input your info:");
//新启动一个写线程用于发送消息到服务端
//新启动一个读线程用于接收服务端的响应消息
new WriteThread(socket).start();
new ReadThread(socket).start();
}
static class WriteThread extends Thread {
static Socket socket;
WriteThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
String line;
while (true) {
line = new Scanner(System.in).next();
printWriter.println(line);
if ("over".equalsIgnoreCase(line)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("write thread end ...");
}
}
}
static class ReadThread extends Thread {
static Socket socket;
ReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while (true) {
while ((line = bufferedReader.readLine()) != null) {
if ("over".equalsIgnoreCase(line)) {
return;
}
System.out.printf("收到数据: %s", line);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("read thread end ...");
clear(socket);
}
}
}
private static void clear(Socket socket) {
if (socket != null) {
try {
System.out.println("client close...");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class BioServer {
private static final int PORT = Utils.PORT;
private static ServerSocket serverSocket;
/**
* 执行任务的线程池
*/
private static ExecutorService executorService = new ThreadPoolExecutor(
5,
10,
10L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(20),
new NamedThreadFactory()
);
private static void start() throws Exception {
try {
//1.绑定端口
serverSocket = new ServerSocket(PORT);
System.out.println("Bio server start , the port listening is : " + PORT);
while (true) {
//1.服务端等待socket上客户端的连接,注意这个方法会一直阻塞直到有连接位置
Socket accept = serverSocket.accept();
System.out.println("New connection established ...,ready to execute the task... ");
//2.将建立的连接交给线程处理,因此一个连接需要一个线程处理
executorService.execute(new BioServerHandler(accept));
}
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
public static void main(String[] args) throws Exception {
start();
}
}
/**
* 处理逻辑很简单,如果客户端输入的是“over”,那么就断开此次连接,反之则给与一个回复表示自己收到了。
* @Date 2019/4/22 12:43
*/
public class BioServerHandler implements Runnable {
private Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
System.out.println("服务端处理端口:" + socket.getPort());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
String line;
String result;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("Server received :" + line);
if ("over".equalsIgnoreCase(line)) {
printWriter.println("over");
break;
}
printWriter.println("收到了!len:" + line.length());
}
} catch (Exception e) {
//如果客户端关闭,则提示连接关闭
if (e instanceof SocketException && "Connection reset".equalsIgnoreCase(e.getMessage())) {
System.out.println("Connection closed ... ");
} else {
e.printStackTrace();
}
} finally {
clear();
}
}
private void clear() {
if (socket != null) {
try {
System.out.println("server close...");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
下面是服务端打印:
Bio server start , the port listening is : 12345
New connection established ...,ready to execute the task...
服务端处理端口:53930
New connection established ...,ready to execute the task...
服务端处理端口:53937
New connection established ...,ready to execute the task...
服务端处理端口:53944
Server received :client1
Server received :client2
Server received :client3
三个客户端分表输入client1、client2、client3:
Input your info:
client1
Input your info:
client2
Input your info:
client3
1.Selector多路选择器可以管理多个channel,在不同平台对应的抽象不一样,在Linux中channel对应文件描述符,在Windows中对应句柄。如此完成一个线程对多个连接的管理,
因此真正处理数据的线程就不需要阻塞了。等到真正某个channel有数据需要读写的时候,通知到对应的事件处理器来处理这个连接,这个处理的过程是阻塞的,在处理的过程中
也是对缓冲区的处理,而不需要对流进程处理。
NIO中Channel和BIO中流的区别:
普通IO流中,中要么是输入流要么是输出流,不可能同时为输入和输出流,Cahnnel是双向的,NIO中通过flip既可以读也可以写
普通IO流中直接对流进行读写,NIO中是通过buffer操作,永远不能直接操作Channel,
public class NioServerTest {
private static final int[] PORTS = new int[]{12345, 12346, 12347};
public static void main(String[] args) throws Exception {
//1.创建一个Selector
Selector selector = Selector.open();
for (int port : PORTS) {
//2.创建 serverSocketChannel,注册到 selector 选择器 , 设置非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//3.端口绑定,通过 ServerSocketChannel 关联的 ServerSocket 绑定
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
}
while (true) {
//4.select 是阻塞方法,有事件就返回
int num = selector.select();
//5.获取事件,可能多个通道有事件,因此返回的是一个集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//可接受事件
if (selectionKey.isAcceptable()) {
//拿到channel对象
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//得到 SocketChannel ,代表 TCP 连接对象
SocketChannel socketChannel = channel.accept();
System.out.println("服务端处理端口:" + socketChannel.socket().getPort());
//配置非阻塞,由此可以看到客户端 socketChannel 也可以是非阻塞的,
// configureBlocking 方法实际上定义在父类,因此客户端服务端都是非阻塞的
socketChannel.configureBlocking(false);
//也把连接对象注册到selector,连接对象关心的应该是读写事件
socketChannel.register(selector, SelectionKey.OP_READ);
//移除非常关键,因此这个连接事件已经处理了,不移除的话会多次处理
iterator.remove();
System.out.println("获取到客户端的连接: " + socketChannel);
} else if (selectionKey.isReadable()) {
//拿到channel对象
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
int readBytes = socketChannel.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("服务端收到消息 : " + body);
//将消息写回客户端
byte[] resp = body.getBytes();
ByteBuffer write = ByteBuffer.allocate(body.getBytes().length);
write.put(resp).flip();
socketChannel.write(write);
}
iterator.remove();
}
}
}
}
}
//服务端打印
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:54844]
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12346 remote=/127.0.0.1:54856]
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12347 remote=/127.0.0.1:54866]
服务端收到消息 : client1
服务端收到消息 : client2
服务端收到消息 : client3
对比项 | BIO | NIO |
---|---|---|
读写 | 基于流( Stream ) | 基于缓冲区( Buffer ) |
模式 | 阻塞 IO | 非阻塞 IO |
核心组件 | 无 | 选择器或者多路复用器(Selector) |
优点 | 大数据量传输或者并发不高时,性能更好,响应更及时 | 较少数量的线程就可以管理较多的连接,支持更高并发 |
缺点 | 一个线程处理一个请求,难以支持并发数高的场景 | 响应有微弱延时 |
应用场景 | 并发不高场景下的基于线程的服务模式 | 基于事件的服务模式 |
BIO在读取数据时会一直阻塞,如果此时没有数据或者数据尚未准备好,线程会一直阻塞直到数据准备好,显然这个阻塞等待的过程线程不能做其他的事情是一种很大的资源浪费。这种模式下,对于一个客户端的连接,服务端就需要一个线程来处理,即使客户端这边一直不发数据,服务端也要阻塞在那里等着。
NIO模式下如果没有数据,那么线程会立即返回,返回后可以对下一个连接进行读取操作,这样不阻塞就可以让一个线程处理多个连接。而在网络连接中,读取数据往往对时间的占比不多,比如即时通信,很多连接只会在其一小部分时间里面发送少量数据,此种情况用NIO就很有优势,如果用BIO就需要很多线程且每个线程有大量的时间是被阻塞的。
Netty是基于Java NIO的一个异步事件驱动的网络通信框架,其底层是依赖于NIO相关的API的。和NIO一样,Netty的核心特点是异步和事件驱动;
异步:Netty 中Api基本是异步的,调用Api之后会立刻返回,但是并不确定结果是否成功,需要通过回调来判断结果。异步方式比同步方式复杂很多,但是性能更好;
事件驱动:Netty本身实现了很多协议,将事件映射到一个个回调方法上,对应事件发生后,对应的方法就会被调用。
参照 Netty官网 的部分描述
设计:各种传输类型(阻塞/非阻塞)下统一的API、可定制化的线程模型、灵活的事件模型
性能:更高的吞吐,更低的延迟、更少的资源消耗、减少不必要的内存拷贝(零拷贝)
安全:SSL支持
Core :核心部分,底层的网络通用抽象和部分实现。
Extensible Event Model :可拓展的事件模型。Netty 是基于事件模型的网络应用框架。
Universal Communication API :通用的通信 API 层。Netty 定义了一套抽象的通用通信层的 API,比如NIO和BIO的切换,所使用的API非常接近 。
Zero-Copy-Capable Rich Byte Buffer :支持零拷贝特性的 Byte Buffer 实现。
Transport Services :传输( 通信 )服务,具体的网络传输的定义与实现。
Socket & Datagram :TCP 和 UDP 的传输实现。
HTTP Tunnel :HTTP 通道的传输实现。
In-VM Piple :JVM 内部的传输实现。
Protocol Support :协议支持。Netty 对常用协议的编解码实现。比如:HTTP、DNS、Redis、telnet、sctp 等,在Netty源码的 example模块有很多示例。
支持应用层协议,比如用Netty实现Http协议就很方便,使用JDK的NIO,相关的编解码等工繁杂,相关协议细节复杂性可想而知
TCP半包粘包问题,应用层的信息在TCP层可能会拆成多个包或者多个请求合成一个包,JDK NIO它只是构建了一个通信的通道但是不会处理这些问题,netty可以帮我们解决
定制功能,比如流量整型,黑白名单等,在Netty中可以较为容易的实现
完善的断连、Idel、异常处理等,Netty已经支持的很好了
Netty解决了Java Nio中的 epoll bug,(一个导致Cpu 100%的bug,在Linux 2.4下异常唤醒,却没有事件发生,导致Cpu100% ,称为:epoll bug 也叫空轮训bug,而且在JDK中并未根本解决,Netty中源码会判断Cpu空转的次数,超过一定的次数就会rebuild selector 解决该bug)
TCP协议中的IP_TOS参数,它控制了IP包的优先级和Qos选型,使用的时候会抛出异常,提示选项找不到,在JDK12才会解决该问题,是Netty的使用者发现由Netty维护者report该问题,Netty中直接不支持该选型,避免错误。
Api更加强大,友好;JDK中的Api友好性差,比如JDK中的buffer的单指针,内部不能扩容,而Netty读写切换更方便,可以扩容,另外ThreadLocal中,Netty中有对应的优化后的ThreadLocal,性能更好
Netty屏蔽了细节,比如NIO的切换,使用Java的NIO修改非常多,Netty可能只需要修改2行代码
屏蔽细节,Jdk的NIO我们需要关系事件,移除事件等,Netty更好用,很多时候我们只需要关注自己的Handler处理器就可以了。
在NIO中有三剑客,在Netty中也有对应的体现,不过Netty 并没有直接使用 JDK NIO中原生的 Channel 和 Selector,
Channel: Netty 重新定义了 io.netty.channel.Channel 接口,其实现类也全部在 io.netty.channel 包下,对于 Channel 的理解我觉得对比 NIO 中的Channel 没什么问题,Netty 中重新定义只是说为了更好的使用和封装以及实现;
Buffer: Netty也没有直接使用NIO的Buffer,而是定义了 io.netty.buffer.ByteBuf,后面的文章我们对比这两个直接的区别,ByteBuf的实现类也都在io.netty.buffer包下;
Selector:对于Selector,Netty底层封装了它,但是代码上层体现的是事件循环组 EventLoopGroup ,内部有 EventLoop 代表事件循环,他会监听事件的发送,看起来和Selector功能一样,他的内部就封装了 Selector,其本质是一个线程池,能够不断的监听通道事件。
本文大体上介绍和对比了BIO,NIO,引入了Netty,后续先分析NIO的三大组件,再开始Netty的学习和分析。