系列文章:【Netty】知识脉络
前言?
- Java的IO ,就是 输入/输出 (Input/Output),分为IO设备和IO接口两个部分。
- 常听输入输出流、输入输出字节、输入输出字符...Java与外部交互都可转化为流、字节字符进而封装为对象、进而方便程序员编程。
- Java与网络交互就是网络IO、Java与磁盘交互就是磁盘IO。
- Java网络IO是什么?用系统调用read从socket中读取数据。
?一、Java网络编程基础
1、Socket
- 网络上两个程序通过一个双向通讯连接实现数据的交换。
- 这个双向通讯链路两端端点称为Socket,通常用来实现连接用。
- 一个socket必须由IP+端口号port组成。
- socket是个支持TCP/IP等协议的编程界面。
2、Java中的Socket
- Java中socket主要是基于TCP/IP。
- Java的java.net包中提供了socket(客户端)和serverSocket(服务端)。
- Java中socket使用方法:
- 创建socket
- 打开连接到socket的输入/输出流
- 按照协议对socket的读取/写入
- 关闭socket? ?
3、Java中的IO? ?
Socket建设完毕,网络数据的传输通路没问题,那么数据,该怎么读取呢?
- 关于读取JDK 1.0就有读取的包提供——java.io
- Java 的 I/O输入输出系统解决的问题是:
- 各种I/O源端和与之通信的接收端(文件/控制台/网络链接...)
- 多种不同方式进行通信:顺序/随机/缓冲/二进制/按字符、按字、按行...
- Java的“流”屏蔽了实现I/O设备中处理数据的细节。
?二、Java网络IO的历史演进
与其说是Java的IO历史,不如说是操作系统的网络IO历史。
- read是操作系统的方法,java只是调用这个接口。
以Linux为例:
第一阶段:调用read读取socket数据,有数据则读取,没数据则等待。
第二阶段:调用read读取socket数据,有数据则读取,没数据则返回-1,
? ? ? ? ? ? ? ? ? errno设置为EAGAIN。
第三阶段:监听socket,有数据则通知。
第一阶段? Java网络编程情况
第一阶段:调用read读取socket数据,有数据则读取,没数据则等待。
在此前提下如何设计Java程序呢:
- 线程若阻塞在read中,那么为了程序继续向下执行,就只能开启新的线程。
1、为代码编程示例
?A) ServerSocket服务端通道建设伪代码示例
public class ServerSocketDemo {
public static void main(String[] args) throws IOException {
// 创建一个线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
// 创建ServerSocket
ServerSocket serverSocket = new ServerSocket(3000);
// 死循环(监听serverSocket),等待客户端连接
while (true) {
Socket clientSocket = serverSocket.accept();
// 创建一个线程用于处理通信数据
threadPool.execute(new Runnable() {
@Override
public void run() {
//handler方法为处理数据的方法:见下文 B)IO处理数据为代码示例
handler(clientSocket);
}
});
}
}
}
B) IO处理数据为代码示例
private static void handler(Socket clientSocket) throws IOException {
//接收数据,没有数据可读时就阻塞
int read = clientSocket.getInputStream().read(new byte[1024]);
if (read != -1) {
//处理数据的业务方法
}
}
2、弊端
read() 操作卡住的(阻塞),如果单线程很可能卡死住,如果多线程呢(如上),可以解决卡住问题,但是带来哪些影响?
- 每个线程处理一个网络请求,1000个并发请求就开1000个线程。
- 每个线程占用一定内存做为线程栈,每个1M,1000个就是1G。
- 都没数据时候,这1000个线程闲着。
- 如果用线程池,就限制了并发的数量。
3、总结
这种调用read读取socket数据,有数据则读取,没数据则等待。称之为BIO,即阻塞IO。
?第二阶段? Java网络编程情况
第二阶段:调用read读取socket数据,有数据则读取,没数据则返回-1,
? ? ? ? ? ? ? ? ? errno设置为EAGAIN。
Java为此做了哪些改变呢?NIO模型登场!
- NIO新增缓冲区(Buffer)。
- 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。
- NIO数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。增加了处理的灵活性。
-
buffer 底层就是个数组。
- NIO新增双向通道(Channel)。
- 一个单独的线程现在可以管理多个输入和输出通道(channel)。
- NIO新增多路复用器(Selector)。
- 将Channel注册在Selector上
- Selector可以监听Channel的四种状态(Connect、Accept、Read、Write)
- 监听到某一Channel的某个状态时,才对Channel进行相应的操作
对应的操作系统是什么呢?
I/O多路复用底层主要用的Linux 内核·函数(select,poll,epoll)来实现
windows不支持epoll实现,windows底层是基于winsock2的select函数实现的(不开源)
?
?
1、NIO如何使用? 客户端代码示例
public class NioServer {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待需要处理的事件发生
selector.select();
// 获取selector中注册的全部事件的 SelectionKey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历SelectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
socketChannel.register(selector, SelectionKey.OP_READ);
//客户端连接成功
// 如果是OP_READ事件,则进行读取和打印
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = socketChannel.read(byteBuffer);
// 如果有数据
if (len > 0) {
//接收数据
String data = new String(byteBuffer.array());
//执行处理数据的业务逻辑
// 如果客户端断开连接,关闭Socket
} else if (len == -1) {
//客户端断开连接,关闭socket
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
}
?2、总结
NIO整个调用流程就是
- Java调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符。
- Java再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上。
事件的异步通知
- 实现了使用一条线程
- 不需要太多的无效的遍历
- 将事件处理交给了操作系统内核(操作系统中断程序实现)
- 大大提高了效率
?
|