我们一般情况下,不会涉及到网络编程,大部分成熟的框架,已经对于底层的网络通信进行了支持,但是越是透明化的东西越会让人感兴趣。网络编程在编程中占有举足轻重的位置,任何大型的系统不管是运行在局域网还是广域网都会涉及到网络的信息传输,那么我们简单来了解一下JAVA JDK 关于网络传输的一些相关处理和支持。
首先简单定个性
BIO(Blocking IO): 同步阻塞
NIO: (Non-Blocking IO、New IO)同步非阻塞
AIO: (Asynchronous IO)异步非阻塞
关于同步(非同步)、阻塞(非阻塞)的概念区分和理解也是非常关键的。其实理解起来是很困难的,举个网上流传已久的例子来解释一下:
老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
举例子终究是便于理解,实际上我们还需要知道一些定义或者原理类的一些东西。
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
BIO 示例代码
server端
package bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class WangzyBioServer {
public static void main(String[] args) throws IOException {
// 指定服务的端口号为8080
int port = 8080;
ServerSocket server = null;
try {
long start = System.currentTimeMillis();
server = new ServerSocket(port);
long end = System.currentTimeMillis();
System.out.println("Server: The nio server is start in port : " + port
+ " in " + (start - end) + " ms");
Socket socket = null;
// 不断轮询看是否有数据传输
while (true) {
// 此方法为阻塞方法
socket = server.accept();
// 如果有则起一个新线程进行Handle
new Thread(new WangzyBioServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
System.out.println("The Nio server close");
server.close();
server = null;
}
}
}
}
Handler
package bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class WangzyBioServerHandler implements Runnable {
private Socket socket;
public WangzyBioServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// 客户端发送信息获取
BufferedReader in = null;
// 返回客户端信息设值
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(
this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
StringBuffer ret = new StringBuffer();;
String body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
ret.append("#" + body + "#");
System.out.println("Server: 接受到的客户端请求为 "+body);
// 给客户端返回结果
out.println(ret);
}
} catch (IOException e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
Client 端
package bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class WangzyBioClient {
public static void main(String[] args) {
int port = 8080;
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
//绑定IP和端口号
socket = new Socket("127.0.0.1", port);
} catch (IOException e) {
System.out.println("客户端初始化失败");
}
try {
writer = new PrintWriter(socket.getOutputStream(), true);
reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
} catch (IOException e) {
System.out.println("输入输出流获取失败");
}
writer.println("request请求");
System.out.println("Client: 请求已发出");
try {
String readLine = reader.readLine();
System.out.println("Client: 返回结果如下" + readLine);
} catch (IOException e) {
System.out.println("读取失败");
} finally {
if (writer != null) {
writer.close();
writer = null;
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
reader = null;
}
}
}
}
Client Console :
Client: 请求已发出
Client: 返回结果如下#request请求#
Server Console:
Server: The nio server is start in port : 8080 in -15 ms
Server: 接受到的客户端请求为 request请求
一个简单的同步阻塞式的BIO 我们就写完成了,实际上BIO很简单也很容易理解,客户端负责request,服务端负责response,你来一个客户端请求我就开一个线程去回应你,你来两个客户端我就起两个线程去回应你,那么我们其实可以发现,这种代码在实际的过程中使用尤其是大量客户端请求是不太合适的,因为启动线程不是免费的,是要耗费内存的,当客户端太多了,势必要对服务器造成很大的压力,那么我们想到,为什么不用线程池来处理呢?实际上线程池能够帮我们缓解一些服务器的压力,但是这种模式是同步阻塞的,需要不断的轮询来处理请求和等待请求处理完成,也就是在客户端给服务器发送请求到客户端接收到服务器响应之间,我们只能等待,什么也做不了,也不能去做,那么有没有稍微好一点的实现呢?
下面简单讲下NIO
NIO 示例代码
Server端
package nio;
import java.io.IOException;
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;
/**
* NIO服务端
*/
public class NIOServer {
// 通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
*
* @param port
* 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// selector.select(10000);
// selector.wakeup();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
handler(key);
}
}
}
/**
* 处理请求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
// 客户端请求连接事件
if (key.isAcceptable()) {
handlerAccept(key);
// 获得了可读的事件
} else if (key.isReadable()) {
handelerRead(key);
}
}
/**
* 处理连接请求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息哦
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
}
/**
* 处理读的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
// 回写数据
ByteBuffer outBuffer = ByteBuffer.wrap(("服务器收到消息: " + msg)
.getBytes());
channel.write(outBuffer);// 将消息回送给客户端
} else {
System.out.println("客户端关闭");
key.cancel();
}
}
/**
* 启动服务端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8080);
server.listen();
}
}
Client端
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class NIOClient {
public static Selector selector;
public static SocketChannel clntChan;
/**
* 初始化一些基本信息,并进行注册
*/
private static void init() {
try {
selector = Selector.open();
clntChan = SocketChannel.open();
// 设值阻塞模式
clntChan.configureBlocking(false);
clntChan.connect(new InetSocketAddress("localhost", 8080));
// 向selector注册channel
clntChan.register(selector, SelectionKey.OP_READ);
while (!clntChan.finishConnect()) {
}
System.out.println("已成功连接服务器!");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
// 初始化一些参数向selector注册channel
init();
SocketChannel socketChannel = NIOClient.clntChan;
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
new WangzyThread(selector, socketChannel).start();
while (true) {
// 输入要传的数据
Scanner scanner = new Scanner(System.in);
String word = scanner.nextLine();
// 转为Byte数组
byteBuffer.put(word.getBytes());
// buffer复位
byteBuffer.flip();
// 向服务端发出请求
socketChannel.write(byteBuffer);
// 清除数据
byteBuffer.clear();
}
}
/**
* 用来读取服务器返回值的线程
*/
static class WangzyThread extends Thread {
private Selector selector;
public WangzyThread(Selector selector, SocketChannel clntChan) {
this.selector = selector;
}
@Override
public void run() {
try {
// 轮询等待服务器给返回值
while (true) {
// 阻塞方法
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
while (keyIterator.hasNext()) {
SelectionKey selectionKey = keyIterator.next();
if (selectionKey.isValid()) {
// 可读的那么就来读一波
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey
.channel();
// 读取服务器的返回值
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
System.out.println(new String(bytes));
byteBuffer.clear();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
NIO中有两个很重要的概念是selector 和channel
JAVA是面向对象做编程,那么一定要将虚拟的数据传输管道物化为Object,Channel可以理解为客户端和服务器之间的管道,但是一个服务器不能只连接一个客户端,所以我们需要一个东西帮我们去管理众多客户端来的Channel管道,这个时候我们把这个东西物化为Object就是Selector,这样我们可以用一个Selector来帮助管理众多Channel,只用一个线程就能够管控起来众多客户端请求。不用和BIO一样来一个请求就new 一个线程,或者使用线程池来处理多个请求。
简单看一下图助于理解
为什么使用Selector?
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。
先写到这,今天先不写了,简单写下下次要写什么
1、Linux的IO模型(selector多路复用 poll 和epoll)
2、常用的编解码技术和框架
3、拆包和粘包
4、Netty In Action
2018-09-21 22:53