Socket(BIO)、UDP、NIO、AIO
IO流
- I代表Input, O代表Output, I/O 描述了计算机系统与外部设备之间通信的过程。
- 计算机中输入设备对应Input, 输出设备对应Output;输入设备向计算机输入数据,输出设备接收计算机输出的数据。
- java中,IO主要是文件IO,网络IO;
- Java中, I/O的顶级接口是InputStream/OutputStream, Writer/Reader; 前者针对字节流,后者针对字符流;
字节流
若是文件IO, 则主要涉及类有:
- FileInputStream : 用于从文件读取数据(字节信息)到内存中;
- FileOutputStream : 用于将内存中的数据输出到文件中;
- BufferInputStream :基于缓存的输入流;
- BufferOutputStream : 基于缓存的输出流;
业务开发常常需要把用户提交的文件保存到磁盘中,这就是文件拷贝操作;
字节流与文件复制
方式一:严重的性能问题,业务开发应该避免写出这种代码
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("E:\\Java_spring设计原理\\SpringMVC父子容器.md");
FileOutputStream fileOutputStream = new FileOutputStream("SpringMVC2");
copy(fileInputStream, fileOutputStream);
fileInputStream.close();
fileOutputStream.close();
}
private static void copy(FileInputStream fileInputStream, FileOutputStream fileOutputStream) throws IOException {
long count = 0;
while (true) {
int read = fileInputStream.read();
if (read < 0) {
break;
}
if (read == 0) {
continue;
}
fileOutputStream.write(read);
}
}
原因: 通过fileInputStream.read方法,一次磁盘文件只读取了一个字节, 在计算机系统中,磁盘IO是非常耗费性能的,如果业务开发中, 如果遇到大文件复制,业务中会出现阻塞卡顿;
- 有个业务功能:将用户提交的文件保存到临时目录,再从临时目录拷贝到存储目录,血泪啊,上家公司有个小弟把文件从临时目录拷贝到存储目录就是这么写,由于功能测试没有测大文件,导致上线后,客户每次上传大文件都要等个半小时,最后反馈才发现这个问题;
方式二:效率较好, 再读取数据的时候,创建了Byte数组,一次读取一批数据到内存;
public class IOTest {
public static void main(String[] args) throws Exception {
//代表用户request提交过来的数据
FileInputStream fileInputStream = new FileInputStream("E:\\Java_spring设计原理\\SpringMVC父子容器.md");
//代表磁盘的某个位置;
FileOutputStream fileOutputStream = new FileOutputStream("E:\\Java_spring设计原理\\SpringMVC2");
//拷贝操作
copy(fileInputStream, fileOutputStream);
fileInputStream.close();
fileOutputStream.close();
}
public static long copy(InputStream in, OutputStream out) throws Exception
{
long count = 0;
byte[] buf = new byte[8192];
while (true)
{
int n = in.read(buf);
if (n < 0)
break;
if (n == 0)
continue;
out.write(buf, 0, n);
count += n;
}
return count;
}
}
方式三:使用基于缓存的IO类,BufferInputStream, BufferOutputStream;
public static void main(String[] args) throws Exception {
long currentTimeMillis3 = System.currentTimeMillis();
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("E:\\mysql5.7\\mysql-installer-community-5.7.27.0.msi"));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("mysql-installer1.msi"));
copy(bufferedInputStream, bufferedOutputStream);
System.out.println("IO copy end3 : " + (System.currentTimeMillis() - currentTimeMillis3));
bufferedInputStream.close();
bufferedOutputStream.close();
}
private static void copy(InputStream fileInputStream, OutputStream fileOutputStream) throws IOException {
long count = 0;
while (true) {
int read = fileInputStream.read();
if (read < 0) {
break;
}
if (read == 0) {
continue;
}
fileOutputStream.write(read);
count++;
}
}
同样是一字节字节的读,为什么BufferInputStream, BufferOutputStream比FileOutputStream, FileInputStream要高呢?
答案:
- 点开BufferInputStream类源码,创建BufferInputStream会创建一个默认的字节数组,其实就是开辟了一块字节缓存区,长度为8192;
- 读取数据时、先从字节缓冲区中读取,当缓冲区读取完后,就会一次性从磁盘读取一批数据到缓冲区中, 减少了磁盘IO次数;
原理和第二种文件复制一致;
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
字符流与文本读写
Reader
表示用于从文件中读取字符信息到内存中;
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。而字符流IO出现意义,业务中经常会见到字符流?
- java业务中文本的读写经常可见,字符流存在则更方便了业务的开发;
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 我们不知道编码类型就很容易出现乱码问题。
示例:text.txt内容为:“你好,我是test”
public static void main(String[] args) throws Exception {
readAndPrintText();
}
private static void readAndPrintText() {
try (InputStream fis = new FileInputStream("text.txt")) {
int content;
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
--------------------------------------测试结果-----------------------------------
Number of remaining bytes:19
The content read from file:ä½ å¥½ï¼Œæˆ‘æ˜¯test
因此使用FileReader将字节流转换为字符流,按字符读取;
private static void readAndPrintText2() {
try (FileReader fis = new FileReader("text.txt")) {
int content;
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//------------------------------测试结果-------------------------------------
//The content read from file:你好,我是test
Writer
将内存中的字符信息写入磁盘或者网络中;业务中比较少见,通常文件的复制都是基于InputStream/OutputStream完成;
有些时候,需要把一些JSON字符串数据,XML字符串数据保存到本地文件中,则可以使用上Writer;
private static void readAndPrintText2() {
try (FileReader fis = new FileReader("text.txt");
FileWriter fos = new FileWriter("text2.txt")) {
int content;
// long skip = fis.skip(2);
// System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
fos.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
适用于小文本,大文件文本需要使用字符缓冲流;
字符缓冲流
BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息
public static void main(String[] args) throws Exception {
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("text2.txt"));
BufferedReader bufferedReader = new BufferedReader(new FileReader("text.txt"));
readAndWrite(bufferedReader, bufferedWriter);
}
private static void readAndWrite(Reader reader, Writer writer) throws IOException {
int content;
System.out.print("The content read from file:");
while ((content = reader.read()) != -1) {
System.out.print((char) content);
writer.write(content);
}
}
IO类设计模式:装饰器模式
- 在不改变原有对象的情况下拓展其功能;
- 通过组合方式而非继承方式,再原功能的基础上扩展其他功能;
BufferInputStream, BufferOutputStream创建时,都需要创建一个InputStream,OutputStream对象作为参数传进去(控制反转-构造器注入);
从缓冲区中读写字节数据是扩展功能,而从磁盘中读取数据是原有功能;
- FileInputStream, BufferInputStream都实现了InputStream接口,实现了read方法;
- BufferInputStream创建时,传入了FileInputStream对象,依赖FileInputStream;
- 调用read方法
- 当缓冲区中有数据时、先调用BufferInputStream的read方法, 从缓冲区中读取数据
- 否则,调用FileInputStream的read方法,从磁盘文件中读取数据到缓冲区中,再返回数据;
业务开发中的装饰器模式
我在政务钉钉的一期项目中,客户端会调用服务端获取策略接口,等到二期项目时,需要对获取策略接口进行扩展,
- 增加Http调用从第三方获取用户信息
- 并且需要判断策略内容是否一致,如果一直则策略内容返回为空,即策略内容没有发生变化;
一期代码:
@RestController
public class ClientStrategyController {
@Autowired
ClientStrategyService clientStreategyService;
@PostMapping("getStrategy")
public JSONObject getStrategy(StrategyRequest strategyRequest) {
JSONObject strategy = clientStreategyService.getStrategy(strategyRequest);
return strategy;
}
}
@Data
class StrategyRequest {
private String userId;
private String deptId;
private String strategyMd5;
}
//--------------------业务层------------------
public interface ClientStrategyService {
/**
* 获取策略接口
* @return
* @param strategyRequest
*/
JSONObject getStrategy(StrategyRequest strategyRequest);
}
@Service
public class ClientStreategyServiceImpl implements ClientStrategyService{
@Override
public JSONObject getStrategy(StrategyRequest strategyRequest) {
System.out.println("一期获取策略内容业务代码....");
String strategy = "策略内容";
JSONObject jsonObject = JSONUtil.createObj();
jsonObject.set("strategy", strategy);
return jsonObject;
}
}
到了二期,需要在原有接口上扩展功能,增加获取用户详情, 增加内容是否相同,相同则内容字段置空,
当时想着不能修改原有的service实现类代码,因为这些都是稳定代码,在这基础上扩展代码,即遵循开闭原则;
定义一个新的service实现,依赖原有的实现类;
在通过@Primary注解标记主类,因为容器中存在两个相同接口ClientStrategyService 的Bean;
最终写法如下:
@Service
@Primary
public class ClientStrategyExtServiceImpl implements ClientStrategyService {
@Resource
ClientStrategyService clientStreategyService;
@Override
public JSONObject getStrategy(StrategyRequest strategyRequest) {
//1. 请求时,再Filter中会从请求头中读取用户ID, 用户员工编号、设备ID, 用户名称, 然后放入ThreadLocal
//2. 从ThreadLocal中获取用户ID,员工编码去http调用政务钉开放平台查询用户信息
String userInfo = "xxxx";
//3. 查询策略;
JSONObject result = clientStreategyService.getStrategy(strategyRequest);
result.set("user", userInfo);
//4.判断策略内容是否一致,若是一致,则置为空;
Object strategyContent = result.get("strategy");
String md5Hex = DigestUtil.md5Hex(strategyContent.toString());
if (strategyRequest.getStrategyMd5().equals(md5Hex)) {
result.set("strategy", "");
}
return result;
}
}
这样就做到了不改变原有代码,不使用继承的基础上,扩展原有代码,这就是装饰器模式;
IO模型
- 用什么样的通道进行数据的发送与接收、Java支持3中网络编程IO模型:BIO、NIO、AIO;
- 为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space )
Socket网络通信与BIO(Socket)模型
同步阻塞模型、一个客户端对应一个处理线程;
Server端代码
package com.tuling.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待连接。。");
//阻塞方法
Socket clientSocket = serverSocket.accept();
System.out.println("有客户端连接了。。");
handler(clientSocket);
/*new Thread(new Runnable() {
@Override
public void run() {
try {
handler(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();*/
}
}
private static void handler(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("准备read。。");
//接收客户端的数据,阻塞方法,没有数据可读时就阻塞
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕。。");
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
clientSocket.getOutputStream().write("HelloClient".getBytes());
clientSocket.getOutputStream().flush();
}
}
-
为什么下面使用new Thread来处理用户请求呢?
如果不创建新线程处理,整个服务端都在等handler方法执行结束,在方法没有执行结束前如果其他客户端发送请求过来,服务端是无法接受请求的。创建新线程的话,由于线程异步的特点,将每个请求交由线程去处理,服务端立即返回,继续接受新的客户端链接请求。 -
BIO的阻塞问题?
- 不管服务端还是客户端,都会关联一个本地输入输出缓冲区。write对应的本地输出缓冲区,read对应的是本地输入缓冲区。
- IO代码里的read操作是阻塞操作,如果用户不发送数据给服务端,那么服务端线程就会阻塞在read方法处。
- IO代码里的write操作也是阻塞操作,如果本地输出缓冲区满了,那么就无法继续写数据到缓冲区,只能等操作系统将这些写入的数据发送出去,一旦缓冲区有空间了,write阻塞就被唤醒了。
- IO代码里的accept操作也是阻塞操作,如果服务端没有接受到客户端的连接请求,那么服务端线程就会阻塞在accept处,直到有连接请求到来时,线程被唤醒;
- 既然使用了线程处理,那还存在什么问题呢?
操作系统的线程资源不是无限,大概创建3000个线程后,操作系统就运行不了了。因此,在并发量大的情况下,一次可能来10万个请求,那么操作系统就要创建10万个请求,明显不可能(可能服务器集群,但治标不治本)。因此,这种BIO方式适用于连接数目比较少且固定的架构。
客户端代码
//客户端代码
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9000);
//向服务端发送数据
socket.getOutputStream().write("HelloServer".getBytes());
socket.getOutputStream().flush();
System.out.println("向服务端发送数据结束");
byte[] bytes = new byte[1024];
//接收服务端回传的数据
socket.getInputStream().read(bytes);
System.out.println("接收到服务端的数据:" + new String(bytes));
socket.close();
}
}
总结
- 基于Socket的BIO模型是一些并发量不高的传统项目中存在,如果要使用BIO,则可以对BIO代码进行封装;
- 业务开发中,除非是很老的代码,否则Socket网络编程已经很少见了。大部分都是用了Netty作为网络编程
NIO模型(多路复用模型)
- 同步非阻塞,服务器实现模式为一个线程可以处理多个请求。NIO支持面向缓冲区的、基于通道的IO操作。
- 设计到三个核心:Selector、Channel、Buffer
Buffer缓冲区
- 用于特定基本数据类型的容器,有java.nio包定义的,所有的缓冲区都是Buffer抽象类的子类。
- Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
非直接缓冲区
- 写操作:用户程序需要先把数据写到JVM的缓存里,在从JVM缓存(用户态)复制到本地物理缓存(内核态),操作系统再把复制的数据写入磁盘
- 读操作:操作系统先将磁盘的数据读取到本地物理缓存(内核态),再将数据复制到JVM缓存中(用户态),再被用户程序读取。
- 传统的IO操作中,中间的复制操作其实是内核态和用户态的切换。比较耗费时间。
直接缓冲区
- 用户程序需要写IO操作时,直接把数据写到物理内存映射文件中,之后的操作就不归JVM管了,等待操作系统将物理内存映射文件中的数据写到物理磁盘上,省去中间复制的操作。
- 省去了数据复制操作的时间、也避免了内核态与用户态的切换;
- 直接缓冲区与非直接缓冲区
- 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。(用户态)
- 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率(内核态)。底层调用的Unsafe.allocateMemory(size)来申请本地物理内存空间;
channel通道
- 通道:由java.nio.channels包定义
- Channel表示IO源与目标打开的连接。
- Channel类似于传统的流,但其自身不能直接访问数据,Channel只能与Buffer进行交互。
选择器Selector
- 选择器是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,如果已经准备就绪,就可以创建线程(或者获取线程)来处理IO请求,因此,可用Selector可以使一个单独的线程管理多个Channel。
- 客户端通信的channel连接到服务器channel之后,应该把客户端的channel注册到Selector中,这样的话,selector就可以监控多个客户端的SelectableChannel的IO情况,如果客户端发送数据为读事件,即为read事件,selector就可以感知到,然后返回相关的selectKey,通过这些SelectionKey获取channel进行操作。
- Selector是非阻塞IO的核心。
NIO 的非阻塞式网络通信
- 传统的IO流都是阻塞式的,当一个线程调用read()或者write()方法,如果本地缓冲区空,read()方法就会阻塞,如果本地缓冲区满,write()方法会阻塞。如果服务器是单线程,则整个应用程序都会被阻塞,因此,可以使用多线程技术,为每一个IO请求创建一个线程,在每个线程里进行IO操作,但是,如果需要处理大量的IO请求时,线程数量有限,服务器性能就会降低。另外,如果某些线程也一直阻塞住了,那么这线程资源就会被占用,无法复用。
- Java NIO 是非阻塞模式的,当线程从某通道进行读写数据,若没有数据交互时,该线程可以进行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作。所以单独的线程可以管理多个输入或者输出通道。因此,NIO可以让服务器使用一个或者有限的几个线程来同时处理连接到服务器的所有客户端。
选择键SelectionKey
- 每个注册到selector中的channel都会有对应的唯一一个标识、这里用SelectionKey标识,可以通过SelectionKey获得客户端channel;
- 当调用register(Selector sel,int ops)将通道注册到选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。
可以监听的时间类型(可使用SelectionKey 的四个常量表示)
读:SelectionKey.OP_READ
写:SelectionKey…OP_WRITE
连接:SelectionKey.OP_CONNECT
接收:SelectionKey.OP_ACCEPT
NIO非阻塞代码示例
public class NioServer {
// 保存客户端连接
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) throws IOException, InterruptedException {
// 创建NIO ServerSocketChannel,与BIO的serverSocket类似
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
System.out.println("服务启动成功");
while (true) {
// 非阻塞模式accept方法不会阻塞,否则会阻塞
// NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
SocketChannel socketChannel = serverSocket.accept();
if (socketChannel != null) { // 如果有客户端进行连接
System.out.println("连接成功");
// 设置SocketChannel为非阻塞
socketChannel.configureBlocking(false);
// 保存客户端连接在List中
channelList.add(socketChannel);
}
// 遍历连接进行数据读取
Iterator<SocketChannel> iterator = channelList.iterator();
while (iterator.hasNext()) {
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 非阻塞模式read方法不会阻塞,否则会阻塞
int len = sc.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
}
}
- 存在的问题
效果上确实非阻塞了,read,accept阻塞了。但是每次都要去遍历整个客户端链接请求,如果有上万个请求,每次却只有几百个通道存在数据交互,就会导致很多时间浪费在遍历上;
NIO引入多路复用器代码示例
Netty是基于NIO封装优化,业务中,使用原生NIO开发不多, 使用Netty进行网络编程更多,因为效率更快,Netty设计上性能也更好;
public class NioSelectorServer {
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);
System.out.println("服务启动成功");
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);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = socketChannel.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
}
通过代码与介绍可以大概理解NIO模型:
AIO(NIO 2.0)
- 异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
- 业务中基本不会使用,Netty都是基于NIO进行封装优化的,AIO日常业务中基本没有使用到;
AIO代码示例
public class AIOServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("2--"+Thread.currentThread().getName());
// 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
serverChannel.accept(attachment, this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
System.out.println("3--"+Thread.currentThread().getName());
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
System.out.println("1--"+Thread.currentThread().getName());
Thread.sleep(Integer.MAX_VALUE);
}
}
public class AIOClient {
public static void main(String... args) throws Exception {
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(512);
Integer len = socketChannel.read(buffer).get();
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
UDP通信
Java NIO中的DatagramChannel是一个能收发UDP包的通道,UDP通信是基于报文通信的;
public class UDPNoBlockIODemo {
@Test
public void send() throws IOException {
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.configureBlocking(false);
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
FileChannel fileChannel=FileChannel.open(Paths.get("E:/af-service.xml"), StandardOpenOption.READ);
while (fileChannel.read(byteBuffer)!=-1){
byteBuffer.flip();
datagramChannel.send(byteBuffer,new InetSocketAddress("127.0.0.1",8081));
byteBuffer.clear();
}
datagramChannel.close();
fileChannel.close();
}
@Test
public void revice() throws IOException {
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(8081));
Selector selector=Selector.open();
datagramChannel.register(selector, SelectionKey.OP_READ);
while ( true ){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
if(selectionKey.isReadable()){
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
SelectableChannel channel = selectionKey.channel();
FileChannel fileChannel=FileChannel.open(Paths.get("af-service8.xml"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
datagramChannel.receive(byteBuffer);
byteBuffer.flip();
fileChannel.write(byteBuffer);
byteBuffer.clear();
//UDP面向无连接数据包的, DatagramChannel.read(),write()都是需要建立连接才能使用的。所以以下代码走不通
// while(datagramChannel.read(byteBuffer)!=-1){
// byteBuffer.flip();
// fileChannel.write(byteBuffer);
// byteBuffer.clear();
// }
}
iterator.remove();
}
}
}
}