标签(空格分隔): 网络编程 NIO 多线程
NIO的多线程编程
对于单线程的程序来说,我们无法达到并行处理,我们要向达到并行处理,必定会使用多线程,但是我们哪些代码使用子线程呢?我们可以对单线程程序进行分析,在程序中最耗时的操作就是I/O操作(读和写)。找到入口就进行改造程序。
服务器端修改代码:
package com.socket.nio3;
import java.io.IOException;
import java.net.InetSocketAddress;
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;
/**
* NIO 服务器端
*
* @author MOTUI
*
*/
public class NIOServerSocket {
private static Selector selector = null;
public static void main(String[] args) throws IOException {
// 1.创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.绑定端口
serverSocketChannel.bind(new InetSocketAddress(8989));
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.创建通道选择器
selector = Selector.open();
/*
* 5.注册事件类型
*
* sel:通道选择器
* ops:事件类型 ==>SelectionKey:包装类,包含事件类型和通道本身。四个常量类型表示四种事件类型
* SelectionKey.OP_ACCEPT 获取报文 SelectionKey.OP_CONNECT 连接
* SelectionKey.OP_READ 读 SelectionKey.OP_WRITE 写
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("服务器端:正在监听8989端口");
// 6.获取可用I/O通道,获得有多少可用的通道
int num = selector.select();
if (num > 0) { // 判断是否存在可用的通道
// 获得所有的keys
Set selectedKeys = selector.selectedKeys();
// 使用iterator遍历所有的keys
Iterator iterator = selectedKeys.iterator();
// 迭代遍历当前I/O通道
while (iterator.hasNext()) {
// 获得当前key
SelectionKey key = iterator.next();
// 调用iterator的remove()方法,并不是移除当前I/O通道,标识当前I/O通道已经处理。
iterator.remove();
// 判断事件类型,做对应的处理
if (key.isAcceptable()) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssChannel.accept();
System.out.println("处理请求:"+ socketChannel.getRemoteAddress());
// 获取客户端的数据
// 设置非阻塞状态
socketChannel.configureBlocking(false);
// 注册到selector(通道选择器)
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
//调用读操作工具类
RequestProcessor.ProcessorRequest(key,selector);
} else if (key.isWritable()) {
//调用写操作工具类
ResponeProcessor.ProcessorRespone(key);
}
}
}
}
}
}
RequestProcessor:
package com.socket.nio3;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 读操作的工具类
* @author MOTUI
*
*/
public class RequestProcessor {
//构造线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void ProcessorRequest(final SelectionKey key,final Selector selector){
//获得线程并执行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始读");
SocketChannel readChannel = (SocketChannel) key.channel();
// I/O读数据操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = readChannel.read(buffer);
if (len == -1) break;
buffer.flip();
while (buffer.hasRemaining()) {
baos.write(buffer.get());
}
}
System.out.println("服务器端接收到的数据:"+ new String(baos.toByteArray()));
System.out.println("开始注册------");
//注册写操作
readChannel.register(selector, SelectionKey.OP_WRITE);
System.out.println("注册完成-------");
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
ResponeProcessor:
package com.socket.nio3;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 写操作工具类
* @author MOTUI
*
*/
public class ResponeProcessor {
//构造线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void ProcessorRespone(final SelectionKey key) {
//获得线程并执行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始写");
// 写操作
SocketChannel writeChannel = (SocketChannel) key.channel();
//拿到客户端传递的数据
//ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();
//System.out.println("客户端发送来的数据:"+new String(attachment.toByteArray()));
ByteBuffer buffer = ByteBuffer.allocate(1024);
String message = "你好,我好,大家好!!";
buffer.put(message.getBytes());
buffer.flip();
writeChannel.write(buffer);
writeChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
两个工具类分别处理读操作和写操作,分别加入了线程
客户端(未做修改):
package com.socket.nio3;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* NIO 客户端
*/
public class NIOClientSocket {
public static void main(String[] args) throws IOException {
//1.创建SocketChannel
SocketChannel socketChannel=SocketChannel.open();
//2.连接服务器
socketChannel.connect(new InetSocketAddress("192.168.0.117",8989));
//写数据
String msg="我是客户端~";
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.put(msg.getBytes());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
//读数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = socketChannel.read(buffer);
if (len == -1)
break;
buffer.flip();
while (buffer.hasRemaining()) {
bos.write(buffer.get());
}
}
System.out.println("客户端收到:"+new String(bos.toByteArray()));
socketChannel.close();
}
执行结束:
服务器端:正在监听8989端口
处理请求:/192.168.0.117:60194
服务器端:正在监听8989端口
服务器端:正在监听8989端口
服务器端:正在监听8989端口
开始读
开始读
...
开始写
开始写
开始写
开始写
会出现一个严重的问题:在主线程中,读操作会执行多次。因为在子线程没有执行结束,主线程认为读操作没有结束(在通道选择器中读操作的管道一直存在),会一直开启新的线程去执行读操作,这样就会产生严重的错误。产生这种问题的原因是我们将读操作交给子线程去处理,主线程依然对此通道保留监控,所以会一直执行读操作,我们取消主线程对此通道的监控。将服务器端代码稍作修改(添加:key.cancel();):
else if (key.isReadable()) {
//取消读事件的监控
key.cancel();
//调用读操作工具类
RequestProcessor.ProcessorRequest(key,selector);
}
执行结果为:
服务器端:正在监听8989端口
处理请求:/192.168.0.117:60378
服务器端:正在监听8989端口
服务器端:正在监听8989端口
开始读
服务器端接收到的数据:我是客户端~
开始注册------
取消了主线程对通道的监控,但是出现了注册写操作无法执行(无法注册成功)。出现这种问题主要是由于selector.select()和注册操作需要同步。这种方式不能达到目的。换一种解决方式,我们在主线程控制通道的注册会怎样呢?
服务器端:
package com.socket.nio3;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* NIO 服务器端
*
* @author MOTUI
*
*/
public class NIOServerSocket {
//存储SelectionKey的队列
private static List writeQueen = new ArrayList();
private static Selector selector = null;
//添加SelectionKey到队列
public static void addWriteQueen(SelectionKey key){
synchronized (writeQueen) {
writeQueen.add(key);
//唤醒主线程
selector.wakeup();
}
}
public static void main(String[] args) throws IOException {
// 1.创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2.绑定端口
serverSocketChannel.bind(new InetSocketAddress(8989));
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.创建通道选择器
selector = Selector.open();
/*
* 5.注册事件类型
*
* sel:通道选择器
* ops:事件类型 ==>SelectionKey:包装类,包含事件类型和通道本身。四个常量类型表示四种事件类型
* SelectionKey.OP_ACCEPT 获取报文 SelectionKey.OP_CONNECT 连接
* SelectionKey.OP_READ 读 SelectionKey.OP_WRITE 写
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("服务器端:正在监听8989端口");
// 6.获取可用I/O通道,获得有多少可用的通道
int num = selector.select();
if (num > 0) { // 判断是否存在可用的通道
// 获得所有的keys
Set selectedKeys = selector.selectedKeys();
// 使用iterator遍历所有的keys
Iterator iterator = selectedKeys.iterator();
// 迭代遍历当前I/O通道
while (iterator.hasNext()) {
// 获得当前key
SelectionKey key = iterator.next();
// 调用iterator的remove()方法,并不是移除当前I/O通道,标识当前I/O通道已经处理。
iterator.remove();
// 判断事件类型,做对应的处理
if (key.isAcceptable()) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = ssChannel.accept();
System.out.println("处理请求:"+ socketChannel.getRemoteAddress());
// 获取客户端的数据
// 设置非阻塞状态
socketChannel.configureBlocking(false);
// 注册到selector(通道选择器)
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
//取消读事件的监控
key.cancel();
//调用读操作工具类
RequestProcessor.ProcessorRequest(key);
} else if (key.isWritable()) {
//取消读事件的监控
key.cancel();
//调用写操作工具类
ResponeProcessor.ProcessorRespone(key);
}
}
}else{
synchronized (writeQueen) {
while(writeQueen.size() > 0){
SelectionKey key = writeQueen.remove(0);
//注册写事件
SocketChannel channel = (SocketChannel) key.channel();
Object attachment = key.attachment();
channel.register(selector, SelectionKey.OP_WRITE,attachment);
}
}
}
}
}
}
RequestProcessor类:
package com.socket.nio3;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 读操作的工具类
* @author MOTUI
*
*/
public class RequestProcessor {
//构造线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void ProcessorRequest(final SelectionKey key){
//获得线程并执行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
SocketChannel readChannel = (SocketChannel) key.channel();
// I/O读数据操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = readChannel.read(buffer);
if (len == -1) break;
buffer.flip();
while (buffer.hasRemaining()) {
baos.write(buffer.get());
}
}
System.out.println("服务器端接收到的数据:"+ new String(baos.toByteArray()));
//将数据添加到key中
key.attach(baos);
//将注册写操作添加到队列中
NIOServerSocket.addWriteQueen(key);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
ResponeProcessor类:
package com.socket.nio3;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 写操作工具类
* @author MOTUI
*
*/
public class ResponeProcessor {
//构造线程池
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void ProcessorRespone(final SelectionKey key) {
//拿到线程并执行
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 写操作
SocketChannel writeChannel = (SocketChannel) key.channel();
//拿到客户端传递的数据
ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();
System.out.println("客户端发送来的数据:"+new String(attachment.toByteArray()));
ByteBuffer buffer = ByteBuffer.allocate(1024);
String message = "你好,我好,大家好!!";
buffer.put(message.getBytes());
buffer.flip();
writeChannel.write(buffer);
writeChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
客户端代码:
package com.socket.nio3;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* NIO 客户端
*/
public class NIOClientSocket {
public static void main(String[] args) throws IOException {
//使用线程模拟用户 并发访问
for (int i = 0; i < 2; i++) {
new Thread(){
public void run() {
try {
//1.创建SocketChannel
SocketChannel socketChannel=SocketChannel.open();
//2.连接服务器
socketChannel.connect(new InetSocketAddress("192.168.0.117",8989));
//写数据
String msg="我是客户端"+Thread.currentThread().getId();
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.put(msg.getBytes());
buffer.flip();
socketChannel.write(buffer);
socketChannel.shutdownOutput();
//读数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
while (true) {
buffer.clear();
len = socketChannel.read(buffer);
if (len == -1)
break;
buffer.flip();
while (buffer.hasRemaining()) {
bos.write(buffer.get());
}
}
System.out.println("客户端收到:"+new String(bos.toByteArray()));
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
}
}
服务器执行结果:
服务器端:正在监听8989端口
处理请求:/192.168.0.117:60792
服务器端:正在监听8989端口
处理请求:/192.168.0.117:60791
服务器端:正在监听8989端口
服务器端:正在监听8989端口
服务器端接收到的数据:我是客户端10
服务器端接收到的数据:我是客户端9
服务器端:正在监听8989端口
服务器端:正在监听8989端口
客户端发送来的数据:我是客户端10
客户端发送来的数据:我是客户端9
客户端执行结果:
客户端收到:你好,我好,大家好!!
客户端收到:你好,我好,大家好!!
总结:
对于NIO的多线程网络编程:NIO也是使用的阻塞I/O,使用了通道选择器进行选择就绪的通道进行处理I/O,达到非阻塞的目的。
编程思路:
1.创建ServerSocketChannel
2.绑定端口
3.设置为非阻塞
4.创建通道选择器
5.注册事件类型
6.获取可用I/O通道,获得有多少可用的通道
7.将耗时的读写操作开启子线程去处理
8.将注册写操作交给主线程处理
重点是7、8两步,如何将注册操作交给主线程处理。
执行流程:
1:启动TestServiceSocketChannel(服务器),加载服务器端中的代码;进入到第一个while(true){
停留到int num = selector.select();查询是否有可用通道
}
2:启动TestClientSocketChannel(客户端),加载客户端中的代码;
3:这时客户端有可用的通道,开始向下执行进入下个while(true)循环,然后将这个iterator.remove();标记移除,以为这个请已经被处理,(防止高并发时进行重复处理),
4:判断当前时间类型并进行处理
a:key.isAcceptable(),key.channeal拿到一个通道,并用通道接收报文请求,然后进行通道设置为非阻塞的,然后再将读操作注入到通道里。执行完毕后,此时跳出当前while(iterator.hasNext()因为此时测试的环境是一个线程,如果是多个线程则进行循环处理)
b:因为在步骤1中已经将读取所以进行判断后会进入else if(key.isReadable()){这里边,进入后会将key.cancel();(会将这个key做上标记)调用RequestProcess.request(key);这个类中的方法进行读操作。此时,服务器端的主线程会阻塞。因为此时已经没有要处理的通道或者说是key的时间
c:进入读操作后,执行代码并将从客户端接受到数据,防止到key的附件中key.attach(bos);然后调用服务器端的TestServiceSocketChannel.addWriteQueen(key);方法,将可以存入WriteQueen集合中,存入后会将已经 阻塞的主线程 进行唤醒(selector.wakeup();)
d:唤醒主线程后,以为此时已经有了所对象,会进行注册写入事件和从客户端读取到的数据 放入通道,进行写入操作.进行判断if(key.isWritable())取消key的写标记,调用写方法ResponseProcess.response(key);将写入数据传到客户端,释放通道,完成响应。