原生JDK网络编程
网络编程里通用常识
既然是通信,那么是肯定是有两个对端的。在通信编程里提供服务的叫服务端,连接服务端使用服务的叫客户端。在开发过程中,如果类的名字有Server或者ServerSocket的,表示这个类是给服务端用的,如果类的名字只有Socket的,那么表示这是负责具体的网络读写的。那么对于服务端来说ServerSocket就只是个场所,具体和客户端沟通的还是一个一个的socket,所以在通信编程里,ServerSocket并不负责具体的网络读写,ServerSocket就只是负责接收客户端连接后,新启一个socket来和客户端进行沟通。这一点对所有模式的通信编程都是适用的。
在通信编程里,我们关注的其实也就是三个事情:连接(客户端连接服务器,服务器等待和接收连接)、读网络数据、写网络数据,所有模式的通信编程都是围绕着这三件事情进行的。
BIO编程
代码
1、BioServer
package cn.enjoyedu.ch01.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
/**
* 类说明:bio的服务端主程序
*/
public class BioServer {
//服务器端必须
private static ServerSocket server;
//线程池,处理每个客户端的请求
private static ExecutorService executorService
= Executors.newFixedThreadPool(5);
private static void start() throws IOException{
try{
//通过构造函数创建ServerSocket
//如果端口合法且空闲,服务端就监听成功
server = new ServerSocket(DEFAULT_PORT);
System.out.println("服务器已启动,端口号:" + DEFAULT_PORT);
while(true){
Socket socket= server.accept();
System.out.println("有新的客户端连接----" );
//当有新的客户端接入时,打包成一个任务,投入线程池
executorService.execute(new BioServerHandler(socket));
}
}finally{
if(server!=null){
server.close();
}
}
}
public static void main(String[] args) throws IOException {
start();
}
}
2、BioClient
package cn.enjoyedu.ch01.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.Scanner;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_SERVER_IP;
/**
* 类说明:客户端
*/
public class BioClient {
public static void main(String[] args) throws InterruptedException,
IOException {
//通过构造函数创建Socket,并且连接指定地址和端口的服务端
Socket socket = new Socket(DEFAULT_SERVER_IP,DEFAULT_PORT);
System.out.println("请输入请求消息:");
//启动读取服务端输出数据的线程
new ReadMsg(socket).start();
PrintWriter pw = null;
//允许客户端在控制台输入数据,然后送往服务器
while(true){
pw = new PrintWriter(socket.getOutputStream());
pw.println(new Scanner(System.in).next());
pw.flush();
}
}
//读取服务端输出数据的线程
private static class ReadMsg extends Thread {
Socket socket;
public ReadMsg(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//负责socket读写的输入流
try (BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()))){
String line = null;
//通过输入流读取服务端传输的数据
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就将结果进行业务处理
while((line=br.readLine())!=null){
System.out.printf("%s
",line);
}
} catch (SocketException e) {
System.out.printf("%s
", "服务器断开了你的连接");
} catch (Exception e) {
e.printStackTrace();
} finally {
clear();
}
}
//必要的资源清理工作
private void clear() {
if (socket != null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、BioServerHandler
package cn.enjoyedu.ch01.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import static cn.enjoyedu.ch01.Ch01Const.response;
public class BioServerHandler implements Runnable{
private Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try(//负责socket读写的输出、输入流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(),
true)){
String message;
String result;
//通过输入流读取客户端传输的数据
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就将结果进行业务处理
while((message = in.readLine())!=null){
System.out.println("Server accept message:"+message);
result = response(message);
//将业务结果通过输出流返回给客户端
out.println(result);
}
}catch(Exception 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() ;
}
}
测试:
客户端输入我是阿里-马云
AIO编程-----了解即可
jdk中的AIO
1、概念
异步IO采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。
2、解释
java从jdk1.7开始支持AIO核心类有AsynchronousSocketChannel 、AsynchronousServerSocketChannel。
java AIO为TCP通信提供的异步Channel AsynchronousServerSocketChannel创建成功后,类似于ServerSocket,也是调用accept()方法来接受来自客户端的连接,由于异步IO实际的IO操作是交给操作系统来做的,用户进程只负责通知操作系统进行IO和接受操作系统IO完成的通知。所以异步的
ServerChannel调用accept()方法后,当前线程不会阻塞,程序也不知道accept()方法什么时候能够接收到客户端请求并且操作系统完成网络IO,为解决这个问题,AIO中accept方法是这样的:
<A> void accept(A attachment ,CompletionHandler<AsynchronousSocketChannel,? super A> handler)
开始接受来自客户端请求,连接成功或失败都会触发CompletionHandler对象的相应方法。
其中AsynchronousSocketChannel就代表该CompletionHandler处理器在处理连接成功时的result,就是一个AsynchronousSocketChannel的实例。? super A代表这个io操作上附加的数据的类型。
而CompletionHandler接口中定义了两个方法,
completed(V result , A attachment)-----当IO完成时触发该方法,该方法的第一个参数代表IO操作返回的对象,第二个参数代表发起IO操作时传入的附加参数。
faild(Throwable exc, A attachment)-----当IO失败时触发该方法,第一个参数代表IO操作失败引发的异常或错误。
AsynchronousSocketChannel的的用法与Socket类似,有三个方法:
connect():用于连接到指定端口,指定IP地址的服务器
read()、write():完成读写。
代码
1、服务端
package cn.enjoyedu.ch01.aio.server;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
/**
* 类说明:服务器主程序
*/
public class AioServer {
private static AioServerHandler serverHandle;
//统计客户端个数
public volatile static long clientCount = 0;
public static void start(){
if(serverHandle!=null)
return;
serverHandle = new AioServerHandler(DEFAULT_PORT);
new Thread(serverHandle,"Server").start();
}
public static void main(String[] args){
AioServer.start();
}
}
package cn.enjoyedu.ch01.aio.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;
/**
* 类说明:响应网络操作的处理器
*/
public class AioServerHandler implements Runnable {
public CountDownLatch latch;
/*进行异步通信的通道*/
public AsynchronousServerSocketChannel channel;
public AioServerHandler(int port) {
try {
//创建服务端通道
channel = AsynchronousServerSocketChannel.open();
//绑定端口
channel.bind(new InetSocketAddress(port));
System.out.println("Server is start,port:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
//用于接收客户端的连接,异步操作,
// 需要实现了CompletionHandler接口的处理器处理和客户端的连接操作
channel.accept(this,new AioAcceptHandler());
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package cn.enjoyedu.ch01.aio.server;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
/**
* 类说明:处理用户连接的处理器
*/
public class AioAcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AioServerHandler> {
@Override
public void completed(AsynchronousSocketChannel channel, AioServerHandler serverHandler) {
AioServer.clientCount++;
System.out.println("连接的客户端数:" + AioServer.clientCount);
//重新注册监听,让别的客户端也可以连接
serverHandler.channel.accept(serverHandler,this);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//1)ByteBuffer dst:接收缓冲区,用于从异步Channel中读取数据包;
//2) A attachment:异步Channel携带的附件,通知回调的时候作为入参使用;
//3) CompletionHandler<Integer,? super A>:系统回调的业务handler,进行读操作
channel.read(readBuffer,readBuffer, new AioReadHandler(channel));
}
@Override
public void failed(Throwable exc, AioServerHandler serverHandler) {
exc.printStackTrace();
serverHandler.latch.countDown();
}
}
package cn.enjoyedu.ch01.aio.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import static cn.enjoyedu.ch01.Ch01Const.response;
/**
* 类说明:读数据的处理器
*/
public class AioReadHandler
implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public AioReadHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
//读取到消息后的处理
@Override
public void completed(Integer result, ByteBuffer attachment) {
//如果条件成立,说明客户端主动终止了TCP套接字,这时服务端终止就可以了
if(result == -1) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
//flip操作
attachment.flip();
byte[] message = new byte[attachment.remaining()];
attachment.get(message);
try {
System.out.println(result);
String msg = new String(message,"UTF-8");
System.out.println("server accept message:"+msg);
String responseStr = response(msg);
//向客户端发送消息
doWrite(responseStr);
} catch (Exception e) {
e.printStackTrace();
}
}
//发送消息
private void doWrite(String result) {
byte[] bytes = result.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//异步写数据
channel.write(writeBuffer, writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
if(attachment.hasRemaining()){
channel.write(attachment,attachment,this);
}else{
//读取客户端传回的数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//异步读数据
channel.read(readBuffer,readBuffer,
new AioReadHandler(channel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2、客户端
package cn.enjoyedu.ch01.aio.client;
import java.util.Scanner;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_SERVER_IP;
/**
* 类说明:aio的客户端主程序
*/
public class AioClient {
//IO通信处理器
private static AioClientHandler clientHandle;
public static void start(){
if(clientHandle!=null)
return;
clientHandle = new AioClientHandler(DEFAULT_SERVER_IP,DEFAULT_PORT);
//负责网络通讯的线程
new Thread(clientHandle,"Client").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
if(msg.equals("q")) return false;
clientHandle.sendMessag(msg);
return true;
}
public static void main(String[] args) throws Exception{
AioClient.start();
System.out.println("请输入请求消息:");
Scanner scanner = new Scanner(System.in);
while(AioClient.sendMsg(scanner.nextLine()));
}
}
package cn.enjoyedu.ch01.aio.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
/**
* 类说明:IO通信处理器,负责连接服务器,对外暴露对服务端发送数据的API
*/
public class AioClientHandler
implements CompletionHandler<Void,AioClientHandler>,Runnable {
private AsynchronousSocketChannel clientChannel;
private String host;
private int port;
private CountDownLatch latch;//防止线程退出
public AioClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
//创建一个实际异步的客户端通道
clientChannel = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//创建CountDownLatch,因为是异步调用,下面的connect不会阻塞,
// 那么整个run方法会迅速结束,那么负责网络通讯的线程也会迅速结束
latch = new CountDownLatch(1);
//发起异步连接操作,回调参数就是这个实例本身,
// 如果连接成功会回调这个实例的completed方法
clientChannel.connect(new InetSocketAddress(host,port),
null,this);
try {
latch.await();
clientChannel.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//连接成功,这个方法会被系统调用
@Override
public void completed(Void result, AioClientHandler attachment) {
System.out.println("已经连接到服务端。");
}
//连接失败,这个方法会被系统调用
@Override
public void failed(Throwable exc, AioClientHandler attachment) {
System.err.println("连接失败。");
exc.printStackTrace();
latch.countDown();
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//对外暴露对服务端发送数据的API
public void sendMessag(String msg){
/*为了把msg变成可以在网络传输的格式*/
byte[] bytes = msg.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
/*进行异步写,同样的这个方法会迅速返回,
需要提供一个接口让系统在一次网络写操作完成后通知我们的应用程序。
所以我们传入一个实现了CompletionHandler的AioClientWriteHandler
第1个writeBuffer,表示我们要发送给服务器的数据;
第2个writeBuffer,考虑到网络写有可能无法一次性将数据写完,需要进行多次网络写,
所以将writeBuffer作为附件传递给AioClientWriteHandler。
*/
clientChannel.write(writeBuffer,writeBuffer,
new AioClientWriteHandler(clientChannel,latch));
}
}
package cn.enjoyedu.ch01.aio.client;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
/**
* 类说明:网络写的处理器,CompletionHandler<Integer, ByteBuffer>中
* Integer:本次网络写操作完成实际写入的字节数,
* ByteBuffer:写操作的附件,存储了写操作需要写入的数据
*/
public class AioClientWriteHandler
implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel clientChannel;
private CountDownLatch latch;
public AioClientWriteHandler(AsynchronousSocketChannel clientChannel,
CountDownLatch latch) {
this.clientChannel = clientChannel;
this.latch = latch;
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
//有可能无法一次性将数据写完,需要检查缓冲区中是否还有数据需要继续进行网络写
if(buffer.hasRemaining()){
clientChannel.write(buffer,buffer,this);
}else{
//写操作已经完成,为读取服务端传回的数据建立缓冲区
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
/*这个方法会迅速返回,需要提供一个接口让
系统在读操作完成后通知我们的应用程序。*/
clientChannel.read(readBuffer,readBuffer,
new AioClientReadHandler(clientChannel,latch));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("数据发送失败...");
try {
clientChannel.close();
latch.countDown();
} catch (IOException e) {
}
}
}
package cn.enjoyedu.ch01.aio.client;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
/**
* 类说明:网络读的处理器
* CompletionHandler<Integer, ByteBuffer>中
* Integer:本次网络读操作实际读取的字节数,
* ByteBuffer:读操作的附件,存储了读操作读到的数据 *
*/
public class AioClientReadHandler
implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel clientChannel;
private CountDownLatch latch;
public AioClientReadHandler(AsynchronousSocketChannel clientChannel,
CountDownLatch latch) {
this.clientChannel = clientChannel;
this.latch = latch;
}
@Override
public void completed(Integer result,ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String msg;
try {
msg = new String(bytes,"UTF-8");
System.out.println("accept message:"+msg);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc,ByteBuffer attachment) {
System.err.println("数据读取失败...");
try {
clientChannel.close();
latch.countDown();
} catch (IOException e) {
}
}
}
NIO编程
1、服务端
package cn.enjoyedu.ch01.nio;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
/**
* 类说明:nio通信服务端
*/
public class NioServer {
private static NioServerHandle nioServerHandle;
public static void start(){
if(nioServerHandle !=null) {
nioServerHandle.stop();
}
nioServerHandle = new NioServerHandle(DEFAULT_PORT);
new Thread(nioServerHandle,"Server").start();
}
public static void main(String[] args){
start();
}
}
package cn.enjoyedu.ch01.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;
import java.util.Set;
import static cn.enjoyedu.ch01.Ch01Const.response;
/**
* 类说明:nio通信服务端处理器
*/
public class NioServerHandle implements Runnable{
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 构造方法
* @param port 指定要监听的端口号
*/
public NioServerHandle(int port) {
try {
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.register(selector,SelectionKey.OP_ACCEPT);
started = true;
System.out.println("服务器已启动,端口号:"+port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
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()){
//获得关心当前事件的channel
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
//通过ServerSocketChannel的accept创建SocketChannel实例
//完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel sc = ssc.accept();
System.out.println("======socket channel 建立连接" );
//设置为非阻塞的
sc.configureBlocking(false);
//连接已经完成了,可以开始关心读事件了
sc.register(selector,SelectionKey.OP_READ);
}
//读消息
if(key.isReadable()){
System.out.println("======socket channel 数据准备完成," +
"可以去读==读取=======");
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 message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + message);
//处理数据
String result = response(message) ;
//发送应答消息
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);
}
}
2、客户端
package cn.enjoyedu.ch01.nio;
import java.util.Scanner;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_PORT;
import static cn.enjoyedu.ch01.Ch01Const.DEFAULT_SERVER_IP;
/**
* 类说明:nio通信客户端
*/
public class NioClient {
private static NioClientHandle nioClientHandle;
public static void start(){
if(nioClientHandle !=null)
nioClientHandle.stop();
nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT);
new Thread(nioClientHandle,"Client").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
nioClientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
start();
Scanner scanner = new Scanner(System.in);
while(NioClient.sendMsg(scanner.next()));
}
}
package cn.enjoyedu.ch01.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.Set;
/**
* 类说明:nio通信客户端处理器
*/
public class NioClientHandle implements Runnable{
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean started;
public NioClientHandle(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();
}
}
public void stop(){
started = false;
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
//循环遍历selector
while(started){
try {
//阻塞,只有当至少一个注册的事件发生的时候才会继续
selector.select();
//获取当前有哪些事件可以使用
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 (IOException e) {
e.printStackTrace();
if(key!=null){
key.cancel();
if(key.channel()!=null){
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector!=null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//具体的事件处理方法
private void handleInput(SelectionKey key) throws IOException{
if(key.isValid()){
//获得关心当前事件的channel
SocketChannel sc = (SocketChannel)key.channel();
if(key.isConnectable()){//连接事件
if(sc.finishConnect()){
socketChannel.register(selector,SelectionKey.OP_READ);
}
else{System.exit(1);}
}
//有数据可读事件
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("accept message:"+result);
}else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
//发送消息
private void doWrite(SocketChannel channel,String request)
throws IOException {
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
}
private void doConnect() throws IOException {
/*如果此通道处于非阻塞模式,
则调用此方法将启动非阻塞连接操作。
如果立即建立连接,就像本地连接可能发生的那样,则此方法返回true。
否则,此方法返回false,
稍后必须通过调用finishConnect方法完成连接操作。*/
if(socketChannel.connect(new InetSocketAddress(host,port))){}
else{
//连接还未完成,所以注册连接就绪事件,向selector表示关注这个事件
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
//写数据对外暴露的API
public void sendMsg(String msg) throws Exception{
//socketChannel.register(selector,SelectionKey.OP_READ);
doWrite(socketChannel,msg);
}
}
Buffer
概念
Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读缓冲的数据。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
重要属性
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
Buffer的分配
要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有allocate方法(可以在堆上分配,也可以在直接内存上分配)。
分配48字节capacity的ByteBuffer的例子:
ByteBuffer buf = ByteBuffer.allocate(48);
分配一个可存储1024个字符的CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);
//堆上分配
ByteBuffer buffer = ByteBuffer.allocate(1024000);
System.out.println("buffer = " + buffer);
System.out.println("after alocate:" + Runtime.getRuntime().freeMemory());
// 这部分用的直接内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(102400);
System.out.println("directBuffer = " + directBuffer);
System.out.println("after direct alocate:" + Runtime.getRuntime().freeMemory());
wrap方法:把一个byte数组或byte数组的一部分包装成ByteBuffer:
ByteBuffer wrap(byte [] array);
ByteBuffer wrap(byte [] array, int offset, int length);
Buffer的读写
向Buffer中写数据
写数据到Buffer有两种方式:
-
读取Channel写到Buffer。
-
通过Buffer的put()方法写到Buffer里。
从Channel写到Buffer的例子
int bytesRead = inChannel.read(buf); //read into buffer.
通过put方法写Buffer的例子:
buf.put(127);
put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。 更多Buffer实现的细节参考JavaDoc。
flip()方法
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等 —— 现在能读取多少个byte、char等。
从Buffer中读取数据
从Buffer中读取数据有两种方式:
-
从Buffer读取数据写入到Channel。
-
使用get()方法从Buffer中读取数据。
从Buffer读取数据到Channel的例子:
int bytesWritten = inChannel.write(buf);
使用get()方法从Buffer中读取数据的例子
byte aByte = buf.get();
get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。
使用Buffer读写数据常见步骤:
-
写入数据到Buffer(可能是自己的应用程序,也可能是系统)
-
调用flip()方法
-
从Buffer中读取数据
-
调用clear()方法或者compact()方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
其他常用操作
rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
clear()与compact()方法
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。
如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。
如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
buffer.mark();//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals()与compareTo()方法
可以使用equals()和compareTo()方法两个Buffer。
equals()
当满足下列条件时,表示两个Buffer相等:
-
有相同的类型(byte、char、int等)。
-
Buffer中剩余的byte、char等的个数相等。
-
Buffer中所有剩余的byte、char等都相同。
如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
compareTo()方法
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
-
第一个不相等的元素小于另一个Buffer中对应的元素 。
-
所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
Buffer常用方法总结
NIO
JDK中的NIO
JAVA NIO就是采用多路复用IO模型,IO多路复用模型使用了Reactor设计模式实现了这一机制。
Selector
Selector的英文含义是“选择器”,也可以称为为“轮询代理器”、“事件订阅器”、“channel容器管理机”都行。
事件订阅和Channel管理:
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
Channels
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据。
-
所有被Selector(选择器)注册的通道,只能是继承了SelectableChannel类的子类。
-
ServerSocketChannel:应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。
-
ScoketChannel:TCP Socket套接字的监听通道,一个Socket套接字对应了一个客户端IP:端口 到 服务器IP:端口的通信连接。
-
DatagramChannel:UDP 数据报文的监听通道。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
相关示例
package arithmetic.com.ty.binary;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception {
String str = "hello,马云";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream(new File("e:\file01.txt"));
//通过 fileOutputStream 获取 对应的 FileChannel
//这个 fileChannel 真实 类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
// 创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将 str 放入 byteBuffer
byteBuffer.put(str.getBytes());
// 对 byteBuffer 进行 flip
byteBuffer.flip();
// 将 byteBuffer 数据写入到 fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel02 { public static void main(String[] args) throws Exception { // 创建文件的输入流 File file = new File("e:\file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //通过 fileInputStream 获取对应的FileChannel -> 实际类型 FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); //将通道的数据读入到 Buffer fileChannel.read(byteBuffer); //将 byteBuffer 的 字节数据 转成 String System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } }
import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel03 { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("e:\file01.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("e:\file02.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { //清空 buffer byteBuffer.clear(); int read = fileChannel01.read(byteBuffer); System.out.println("read =" + read); //表示读完 if(read == -1) { break; } //将 buffer中的数据写入到 fileChannel02 -- 2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } //关闭相关的流 fileInputStream.close(); fileOutputStream.close(); } }
应用实例 4-拷贝文件transferFrom方法
实例要求:使用 FileChannel(通道)和方法transferFrom,完成文件的拷贝
import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NIOFileChannel04 { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("e:\file01.txt"); FileChannel sourceCh = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("e:\file02.txt"); FileChannel destCh = fileOutputStream.getChannel(); //使用 transferForm 完成拷贝 destCh.transferFrom(sourceCh,0,sourceCh.size()); //关闭相关通道和流 sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } }
操作类型SelectionKey
JAVA NIO共定义了四种操作类型:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。
-
服务器启动ServerSocketChannel,关注OP_ACCEPT事件,
-
客户端启动SocketChannel,连接服务器,关注OP_CONNECT事件
-
服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件
-
客户端这边的客户端SocketChannel发现连接建立后,可以关注OP_READ、OP_WRITE事件,一般是需要客户端需要发送数据了才关注OP_READ事件
-
连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件。
我们可以看看每个操作类型的就绪条件。
Reactor模式
“反应”器名字中”反应“的由来:
“反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有时间来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(不要调用我,让我来调用你)
单线程Reactor模式流程
① 服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector(选择器)来实现IO的多路复用。注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
② 客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了。
③ 当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannel的read()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
④ 每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。
注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所有的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
单线程Reactor,工作者线程池
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
使用线程池的优势:
① 通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程产生的巨大开销。
② 另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
③ 通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态。同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。
改进的版本中,所以的I/O操作依旧由一个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量的应用场景却不合适,主要原因如下:
① 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
② 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
多Reactor线程模式
Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。
mainReactor可以只有一个,但subReactor一般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。
流程:
① 注册一个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环。
② 客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池。
③ subReactor线程池分配一个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的循环逻辑。
④ 当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这里subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。
注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程 或 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑。
多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的
客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。
Netty服务端使用了“多Reactor线程模式”
和观察者模式的区别
观察者模式:
也可以称为为 发布-订阅 模式,主要适用于多个对象依赖某一个对象的状态并,当某对象状态发生改变时,要通知其他依赖对象做出更新。是一种一对多的关系。当然,如果依赖的对象只有一个时,也是一种特殊的一对一关系。通常,观察者模式适用于消息事件处理,监听者监听到事件时通知事件处理者对事件进行处理(这一点上面有点像是回调,容易与反应器模式和前摄器模式的回调搞混淆)。
Reactor模式:
reactor模式,即反应器模式,是一种高效的异步IO模式,特征是回调,当IO完成时,回调对应的函数进行处理。这种模式并非是真正的异步,而是运用了异步的思想,当IO事件触发时,通知应用程序作出IO处理。模式本身并不调用系统的异步IO函数。
reactor模式与观察者模式有点像。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。当一个主体发生改变时,所有依属体都得到通知。