前言
中秋节不出去玩,除了加班一天,花两天时间死磕IO模型。
1 IO定义
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间内核空间、内核空间设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
1.1 磁盘I/O
磁盘I/O的访问方式又包括以下几种:缓存I/O、直接I/O以及内存映射。
1.1.1 缓存I/O
如图,Process是我们的操作系统进程,它们是无法直接操作I/O设备的,必须借助内核进行资源调度,以完成I/O操作,并且在该处理过程中,操作系统会为每个IO设备分配独立的缓冲区。对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间;而对于输出操作,操作系统将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了同步命令。
优点:简而言之两点---分离保证安全(用户空间与内核空间分离);读磁盘的次数更少
缺点:从上图可以看出,从磁盘到内核地址空间需要层层拷贝,这些操作非常耗时且特别吃CPU
1.1.2 直接I/O
直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。
优点:减少一次拷贝(内核缓冲区->用户程序缓存),配合异步IO会得到较好的性能(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)
缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓慢,因为每次加载都要去读磁盘。
1.1.3 内存映射
内存映射是指将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域一一对应,当要访问内存中一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
通俗点讲:内存映射文件是由一个文件到一块内存的映射,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问,因此内存文件映射非常适合于用来管理大文件。“映射”就是建立一种对应关系,在这里主要是指硬盘上文件的位置与进程逻辑地址空间中一块相同区域之间一一对应,这种关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在的,在内存映射过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上放入了内存,具体到代码,就是建立并初始化了相关的数据结构。这里有两个疑惑:
内存映射的方式在访问时究竟拷贝了几次?1次(从磁盘到缓冲区)
为什么比直接IO快:同样是1次拷贝,为了比直接IO快?内存映射文件是由一个文件到一块内存的映射,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问,因此内存文件映射非常适合于用来管理大文件,因此可以大大减少读写磁盘的次数。
1.2 网络I/O
网络IO也分为三种:普通网络IO、sendfile和zerocopy
1.2.1 普通网络IO
普通网络IO过程大致如下:
1、操作系统将数据从磁盘复制到操作系统内核缓存中
2、应用将数据从内核缓存复制到应用的缓存中
3、应用将数据写回内核的Socket缓存中
4、操作系统将数据从Socket缓存区复制到网卡缓存,然后将其通过网络发出
上述过程经历了三次拷贝:磁盘到内核缓存,内核缓存到用户缓存,用户缓存再到内核缓存;两次模式切换:内核态到用户态,再从用户态切换至内核态。
1.2.2 sendfile
sendfile大致过程如下:
通过sendfile传送文件只需要一次系统调用,当调用 sendfile时:
1、首先通过DMA copy将数据从磁盘读取到kernel buffer中
2、然后通过CPU copy将数据从kernel buffer copy到sokcet buffer中
3、最终通过DMA copy将socket buffer中数据copy到网卡buffer中发送
sendfile与read/write方式相比,少了 一次模式切换一次CPU copy。但是从上述过程中也可以发现从kernel buffer中将数据copy到socket buffer是没必要的。
1.2.3 zerocopy
zerocopy大致过程如下:
1、DMA copy将磁盘数据copy到kernel buffer中
2、向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
3、DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
经过上述过程,数据只经过了2次copy就从磁盘传送出去了,这里的零拷贝指的是内核态的零拷贝。而且使用DMA拷贝,CPU能够更加空闲,有时间去做其他的事。其实zerocopy就是一种改进的sendfile方式,相对于传统的网络IO,数据从内核出去,绕了一圈,又回到内核,没有任何变化,看起来真是浪费时间。而sendfile(两种)的目的便在于内核希望请求的处理尽量在内核完成,减少内核态的切换以及用户态数据复制的开销。
2 Java 网络IO编程
常见的IO模型可以从两个角度去划分,同步或异步、阻塞或非阻塞,接着我们介绍常见的3种Java网络IO编程模式,BIO(伪异步IO其实也是BIO),NIO,AIO。
2.1.1 传统 BIO
服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。 传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答模型。
示例
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author jhz
* @date 18-9-22 下午9:15
*/
public class BIOservice {
private static void handler(Socket socket){
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true){
//读取字节流
int reader = inputStream.read(bytes);
if(reader != -1){
System.out.println(new String(bytes,0,reader));
}
else {
break;
}
}
}
catch (Exception ex){
ex.printStackTrace();
}
finally {
try {
System.out.println("socket关闭");
socket.close();
}
catch (Exception ex){
ex.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("BIO服务器启动----------");
while (true){
//获取socket
final Socket socket = serverSocket.accept();
System.out.println("有一个新连接--------------");
handler(socket);
}
}
}
测试
首先启动BIOserver:
使用telnet连接服务器:
服务器提示有新连接:
发送消息并查看服务器控制台:
可以看到已经成功接收到消息,再启动一个连接测试:
可以看到控制台并无任何连接响应,也没有接收到“123”,接着把第一个连接关掉,再查看控制台:
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了。
2.1.2 伪异步IO
也被成为M:N客户服务模型。即通过线程池模型的形式用M个线程来服务N个客户端的连接;其中M的大小可以根据服务器的配置来设置最大值,而可服务客户端个数N则可以远远的大于M.这样来提高服务器的服务效率,提高线程利用率。同BIO模型类似,只不过,Acceptor接受客户端请求后,不再独立启动线程来处理,而是将客户请求交给线程池来处理,从而减少线程的创建数量,提高线程利用率,增加服务器的处理能力;
示例
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author jhz
* @date 18-9-22 下午10:00
*/
public class BIOserviceWithPool {
public static void main(String[] args) throws IOException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("伪异步IO服务器启动----------");
while (true){
final Socket socket = serverSocket.accept();
System.out.println("有一个新连接--------------");
newCachedThreadPool.execute(() -> BIOservice.handler(socket));
}
}
}
这里用的不是固定线程数量的线程池,如果使用这种方式,最好使用固定数量的线程池,以保证资源得到有效控制,不会因大量连接而耗光。
测试
按上面相同的测试步骤进行测试:
可以看到似乎达到了异步的效果,但是,如果我们将线程池数量固定为2,再新建一个连接,就会发生阻塞的现象了,原因就是,这种方式的本质还是BIO,实质上还是一对一模型,如果不限制,如果有大量连接涌入,那将耗光所有的系统资源。
2.2 NIO(new io)
New IO是对BIO的改进,本质上就是多路复用IO,基于Reactor模型。我们知道,一个socket连接只有在特点时候才会发生数据传输IO操作,大部分时间这个“数据通道”是空闲的,但还是占用着线程。NIO作出的改进就是“一个请求一个线程”,在连接到服务端的众多socket中,只有需要进行IO操作的才能获取服务端的处理线程进行IO。这样就不会因为线程不够用而限制了socket的接入。客户端的socket连接到服务端时,就会在事件分离器注册一个 IO请求事件 和 IO 事件处理器。在该连接发生IO请求时,IO事件处理器就会启动一个线程来处理这个IO请求,不断尝试获取系统的IO的使用权限,一旦成功(即:可以进行IO),则通知这个socket进行IO数据传输。
在2.2中介绍了伪异步IO,线程池本身可以缓解线程创建-销毁的代价,这样优化确实会好很多,不过还是存在一些问题的,就是线程的粒度太大。每一个线程把一次交互的事情全部做了,包括读取和返回,甚至连接,表面上似乎连接不在线程里,但是如果线程不够,有了新的连接,也无法得到处理,所以,目前的方案线程里可以看成要做三件事,连接,读取和写入。这里需要了解一下Reactor模式:https://blog.csdn.net/pistolove/article/details/53152708。 在Reactor中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。
在NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。
Channe(通道),顾名思义,就是通向什么的道路,为某个提供了渠道。在传统IO中,我们要读取一个文件中的内容,通常是像下面这样读取的:
public class Test {
public static void main(String[] args) throws IOException {
File file = new File("data.txt");
InputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[1024];
inputStream.read(bytes);
inputStream.close();
}
}
这里的InputStream实际上就是为读取文件提供一个通道的。因此可以将NIO 中的Channel同传统IO中的Stream来类比,但是要注意,传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而Channel是双向的,既可用来进行读操作,又可用来进行写操作。
Buffer(缓冲区),是NIO中非常重要的一个东西,在NIO中所有数据的读和写都离不开Buffer。比如上面的一段代码中,读取的数据时放在byte数组当中,而在NIO中,读取的数据只能放在Buffer中。同样地,写入数据也是先写入到Buffer中。
Selector(选择器)。可以说它是NIO中最关键的一个部分,Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。
2.2.1 传统NIO
示例
NIOServer
/**
* @author jhz
* @date 18-9-24 下午1:25
*/
public class NIOServer {
private static int DEFAULT_PORT = 12345;
private static ServerHandler serverHandler;
public static void start(){
start(DEFAULT_PORT);
}
public static synchronized void start(int port){
if(serverHandler!=null)
serverHandler.stop();
serverHandler = new ServerHandler(port);
new Thread(serverHandler,"Server").start();
}
public static void main(String[] args){
System.out.println("NIO服务器启动---------");
start();
}
}
ServerHandler
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;
import java.util.Set;
/**
* @author jhz
* @date 18-9-24 下午1:26
*/
public class ServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 构造方法
* @param port 指定要监听的端口号
*/
public ServerHandler(int port) {
try{
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);//开启非阻塞模式
//绑定端口 backlog设为1024
serverChannel.socket().bind(new InetSocketAddress(port),1024);
//监听客户端连接请求
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//处理新接入的请求消息
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//通过ServerSocketChannel的accept创建SocketChannel实例
//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
System.out.println("新连接建立完成");
//设置为非阻塞的
sc.configureBlocking(false);
//注册为读
sc.register(selector, SelectionKey.OP_READ);
}
//读消息
if(key.isReadable()){
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String expression = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + expression);
//处理数据
String result = null;
try{
result = "我收到了你的消息:"+ expression;
}catch(Exception e){
result = "通信错误:" + e.getMessage();
}
//发送应答消息
doWrite(sc,result);
}
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//异步发送应答消息
private void doWrite(SocketChannel channel, String response) throws IOException {
//将消息编码为字节数组
byte[] bytes = response.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
}
}
NIOClient
import java.util.Scanner;
/**
* @author jhz
* @date 18-9-24 下午1:36
*/
public class NIOClient {
private static String DEFAULT_HOST = "127.0.0.1";
private static int DEFAULT_PORT = 12345;
private static ClientHandler clientHandle;
public static void start(){
start(DEFAULT_HOST,DEFAULT_PORT);
}
public static synchronized void start(String ip,int port){
if(clientHandle!=null)
clientHandle.stop();
clientHandle = new ClientHandler(ip,port);
new Thread(clientHandle,"Server").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
if(msg.equals("q")) return false;
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
System.out.println("NIO客户端启动-------------");
start();
while(sendMsg(new Scanner(System.in).nextLine()));
}
}
ClientHandler
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.Set;
/**
* @author jhz
* @date 18-9-24 下午1:37
*/
public class ClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean started;
public ClientHandler(String ip,int port) {
this.host = ip;
this.port = port;
try{
//创建选择器
selector = Selector.open();
//打开监听通道
socketChannel = SocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
socketChannel.configureBlocking(false);//开启非阻塞模式
started = true;
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
try{
doConnect();
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Exception e){
e.printStackTrace();
System.exit(1);
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
if(key.isConnectable()){
if(sc.finishConnect());
else System.exit(1);
}
//读消息
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:" + result);
}
//没有读取到字节 忽略
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//异步发送消息
private void doWrite(SocketChannel channel,String request) throws IOException{
byte[] bytes = request.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
private void doConnect() throws IOException{
if(socketChannel.connect(new InetSocketAddress(host,port)));
else socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
public void sendMsg(String msg) throws Exception{
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel, msg);
}
}
可以看到,客户端与服务端都是对缓冲区进行操作,祥见注释。
测试
分别启动服务端和客户端:
可以看到服务端已经成功的接收到客户端的消息并反馈,接着再启动一个客户端:
可以看到,前面的连接不会阻塞到后面的连接,对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。
2.2.2 Netty
Netty是一个基于Java NIO的client-server网络服务框架,人们可以利用netty快速地开发网络应用。同时netty相对于其他网络框架更加简单并且扩展性更强,这主要得益于其提供的简单易用的api将业务逻辑和网络处理代码解耦开来。能够使你更加专注于业务的实现而不需要太多关心网络底层实现。
我们先总结下2.2.1中NIO的方式:
1.绑定服务器到指定端口
2.打开 selector 处理 channel
3.注册 ServerSocket 到 ServerSocket ,并指定这是专门意接受 连接。
4.等待新的事件来处理。这将阻塞,直到一个事件是传入。
5.从收到的所有事件中 获取 SelectionKey 实例。
6.检查该事件是一个新的连接准备好接受。
7.接受客户端,并用 selector 进行注册。
8.检查 socket 是否准备好写数据。
9.将数据写入到所连接的客户端。如果网络饱和,连接是可写的,那么这个循环将写入数据,直到该缓冲区是空的。
10.关闭连接。
接着我们使用同样的例子来看下Netty:
示例
NettyServer
package netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.Charset;
/**
* @author jhz
* @date 18-9-24 下午9:05
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
ch.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
ch.pipeline().addLast(new NettyServerHandler()); // 客户端触发操作
ch.pipeline().addLast(new ByteArrayEncoder());
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
}
finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new NettyServer(12345).start(); // 启动
}
}
NettyServerHandler
package netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.UnsupportedEncodingException;
/**
* @author jhz
* @date 18-9-24 下午9:25
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/*
* channelAction
*
* channel 通道 action 活跃的
*
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().localAddress().toString() + " 通道已激活!");
}
/*
* channelInactive
*
* channel 通道 Inactive 不活跃的
*
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().localAddress().toString() + " 通道不活跃!");
// 关闭流
}
/**
*
* @author Taowd
* TODO 此处用来处理收到的数据中含有中文的时 出现乱码的问题
* 2017年8月31日 下午7:57:28
* @param buf
* @return
*/
private String getMessage(ByteBuf buf) {
byte[] con = new byte[buf.readableBytes()];
buf.readBytes(con);
try {
return new String(con, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 功能:读取服务器发送过来的信息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
String rev = getMessage(buf);
System.out.println("客户端收到服务器数据:" + rev);
}
/**
* 功能:读取完毕客户端发送过来的数据之后的操作
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("服务端接收数据完毕..");
// 写一个空的buf,并刷新写出区域。完成后关闭sock channel连接。
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
// ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
/**
* 功能:服务端发生异常的操作
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常信息:\r\n" + cause.getMessage());
}
}
NettyClient
package netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
/**
* @author jhz
* @date 18-9-24 下午9:30
*/
public class NettyClient {
private final String host;
private final int port;
public NettyClient() {
this(0);
}
public NettyClient(int port) {
this("localhost", port);
}
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group) // 注册线程池
.channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
.remoteAddress(new InetSocketAddress(this.host, this.port))
// 绑定连接端口和host信息
.handler(new ChannelInitializer<SocketChannel>() { // 绑定连接初始化器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("正在连接中...");
ch.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
ch.pipeline().addLast(new NettyClientHandler());
ch.pipeline().addLast(new ByteArrayEncoder());
ch.pipeline().addLast(new ChunkedWriteHandler());
}
});
ChannelFuture cf = b.connect().sync(); // 异步连接服务器
System.out.println("服务端连接成功..."); // 连接完成
cf.channel().closeFuture().sync(); // 异步等待关闭连接channel
System.out.println("连接已关闭.."); // 关闭完成
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
}
}
public static void main(String[] args) throws Exception {
new NettyClient("127.0.0.1", 12345).start(); //
}
}
NettyClientHandler
package netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.Scanner;
/**
* @author jhz
* @date 18-9-24 下午9:32
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 向服务端发送数据
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端与服务端通道-开启:" + ctx.channel().localAddress() + "channelActive");
while(true){
ctx.writeAndFlush(Unpooled.copiedBuffer(new Scanner(System.in).nextLine(), CharsetUtil.UTF_8));
}
}
/**
* channelInactive
*
* channel 通道 Inactive 不活跃的
*
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端与服务端通道-关闭:" + ctx.channel().localAddress() + "channelInactive");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
System.out.println("异常退出:" + cause.getMessage());
}
}
可以看到使用Netty进行编写只有以下几步(自己写感触才深):
1、创建 NIO 线程组 EventLoopGroup 和 ServerBootstrap。
2、设置 ServerBootstrap 的属性:线程组、SO_BACKLOG 选项,设置 NioServerSocketChannel 为 Channel,设置业务处理 Handler。
3、绑定端口,启动服务器程序。
4、在业务处理 NettyServerHandler 中,读取客户端发送的数据,并给出响应。
对事件的处理只需要重写即可,NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序,而使用Netty,我们只需要关注业务即可,而且Netty在性能上还具有以下优点:传输采用异步通信模式(nio也是),从根本上是事件驱动的;通信协议默认支持Protobuf(高性能的序列化框架);主从多线程Reactor模型。
小结
明天要上班了,AIO后面再学习。