TCP/IP学习笔记三:NIO的网络编程-单线程实例
标签(空格分隔):NIO 网络编程
NIO网络编程-单线程
对于BIO的网络编程存在一些问题,NIO的出现就是对BIO的完善,在学习NIO之前要了解NIO的整体设计,也就是要知道为什么NIO能够做到不阻塞?如何做到的不阻塞?
NIO的做法就是中间使用一个通道选择器,将使用到的管道都注册到管道选择器中,使用的过程中在管道选择器中取需要的管道,让管道选择器来判断当前管道是否就绪,如果就绪就返回给用户使用,如果没有就返回空(代表没有就绪的管道,也就是I/O阻塞)。在任何使用管道的地方都向管道选择器要,在表达意图的时候将管道注册给管道选择器。下面使用代码展现具体实例。
服务器端代码:
package com.socket.nio;
import java.io.ByteArrayOutputStream;
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;
/**
* NIO 服务器端
*
* @author MOTUI
*
*/
public class NIOServerSocket {
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 = 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<SelectionKey> selectedKeys = selector.selectedKeys();
// 使用iterator遍历所有的keys
Iterator<SelectionKey> 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()) {
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()));
// 注册写操作
readChannel.register(selector, SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
// 写操作
SocketChannel writeChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
String message = "你好,我好,大家好!!";
buffer.put(message.getBytes());
buffer.flip();
writeChannel.write(buffer);
writeChannel.close();
}
}
}
}
}
}
客户端代码:
NIO客户端的代码和BIO代码基本相同,因为客户端不牵涉到并发问题,所以在NIO的客户端没有使用管道选择器。
package com.socket.nio;
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 < 5; 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="我是客户端";
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端口
客户端执行:
客户端收到:你好,我好,大家好!!
....(省略4个客户端收到:你好,我好,大家好!!)
服务器端结果:
处理请求:/192.168.0.117:57087
服务器端:正在监听8989端口
处理请求:/192.168.0.117:57086
服务器端:正在监听8989端口
处理请求:/192.168.0.117:57088
服务器端:正在监听8989端口
服务器端接收到的数据:我是客户端
服务器端接收到的数据:我是客户端
处理请求:/192.168.0.117:57089
服务器端:正在监听8989端口
处理请求:/192.168.0.117:57090
服务器端接收到的数据:我是客户端
服务器端接收到的数据:我是客户端
服务器端:正在监听8989端口
服务器端接收到的数据:我是客户端
服务器端:正在监听8989端口
服务器端:正在监听8989端口
总结:
在分析我们的程序的时候发现我们在实际运行过程中会在读操作接收一些参数,然后再写操作之前进行处理数据然后相应数据,我们该如何传递数据?定义成员变量?如果出现并发情况该如何处理数据,数据会不会出现混乱的情况?java已经帮我们实现了,即在管道(channel)中将数据传递过去,保证每个管道对应自己的数据,无论何种情况都不会出现数据污染的情况。
//att Object类型,可以传递参数
channel.register(sel, ops, att)
//接收参数,返回值Object类型
key.attachment();
我们的单线程程序就可以正确运行了,但是我们并没有实现真正的多线程并发处理。多线程编程在下一篇展现。