BIO介绍
同步阻塞模型,一个客户端需要一个线程来处理,服务端处理客户端信息是阻塞的,如果客户端通讯慢,那服务端的线程就一直不会释放,造成资源的浪费,适用于通讯不多的场景,不过代码简单易于维护,一个线程对应一个通讯,执行效率高,不像NIO需要轮询的来处理通讯.
客户端
public class SocketClient {
public static void main(String[] args) throws IOException {
//创建链接,创建链接之后,服务端的serverSocket.accept ();阻塞结束
Socket socket = new Socket ("127.0.0.1",9000);
System.out.println ("开始发送数据");
OutputStream outputStream = socket.getOutputStream ();
//向客户端发送消息,服务端read阻塞结束
outputStream.write ("hellosocketClient".getBytes ());
outputStream.flush ();
System.out.println ("开始读取数据");
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream ();
inputStream.read (bytes);
System.out.println ("服务端返回消息"+new String (bytes));
socket.close ();
}
}
服务端
public class SocketServer {
public static void main(String[] args) throws IOException {
//server端socket
ServerSocket serverSocket = new ServerSocket (9000);
//无限循环,服务端一直处理请求
while (true) {
System.out.println ("等待链接");
//阻塞,等待客户端链接,new Socket ("127.0.0.1",9000);实例化之后代表建立链接,此阻塞结束
Socket socket = serverSocket.accept ();
System.out.println ("客户端链接成功");
//使用多线程处理请求,需要考虑线程安全问题,如果使用单线程,性能低
new Thread (() -> {
try {
handler (socket);
} catch (IOException e) {
e.printStackTrace ();
}
}).start ();
}
}
private static void handler(Socket socket) throws IOException {
//inputStream 获取输入信息
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream ();
//read方法阻塞方法,等待客户端发送消息
int read = inputStream.read (bytes);
System.out.println ("服务端接受消息"+new String (bytes,0 ,read));
//outputStream 服务端返回信息
OutputStream outputStream = socket.getOutputStream ();
outputStream.write ("hellosocketServer".getBytes ());
outputStream.flush ();
}
}
可以发现BIO(Blocking)服务端等待链接阻塞,等待信息传入也是阻塞,服务端只有一个线程来处理,首先阻塞accept方法,等待被链接,链接之后创建一个线程处理通讯,如果同时一万个请求,那就要创建一万个线程,当然系统本身和tomcat容器对线程有限制,不会同时创建一万个线程,但是如果应用可能处理通讯不多,而且是通讯时间比较长可以考虑使用BIO
NIO介绍
同步非阻塞模型,服务端一个线程可以处理多个客户端的请求,适用于连接时间短,连接数据量大的场景,聊天室,弹幕系统.
NIO三个核心组件 Channle(通道) Buffer(缓冲) Selector(多路复用器)
Channel通道,类似管道,负责通讯
Buffer 缓存区,每个通道对应一个buffer,
Selector channel注册进selector, selector根据channel的发生的事件,交给线程去处理
代码
服务端
/**
* selector的多路复用器,解释
*
* 服务器a
* 客户端b
* 客户端c
*
* 首先将a链接事件注册进selector中,起名叫key-a
* 当b链接到a时,被监听到,从selector的key集合返回满足条件的key-a
* 处理key-a,并将b客户端注册进selector中并监听读事件 起名叫key-b
* 此时selector中有key-a key-b
*
* 当很短的事件内同时发生了客户端c链接a,b发消息给a,c也发送消息给a
* (假设很短的时间满足系统底层收集事件的时间,系统不是监听到一个事件就马上解除阻塞,会等很短的时间,如果这段时间有几个事件被监听到,就一起返回)
* 那么selector就会返回key-a 和key-b
* 遍历得到的key
* 首先处理key-a的链接事件,将key-c的读事件注册进selector中
* 再处理key-b的读事件
* 处理结束后,由于while(true),马上就执行 selector.select ();等待监听,发现c发消息过来了
* 处理key-c发过来的消息
* 经过上面的流程发现,c链接并发送消息,不会马上的执行,会轮询key,如果有事件处理事件,而c的发消息事件是第二轮遍历的时候处理
*
*/
public class SocketServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocketChannel ssc = ServerSocketChannel.open ();
//设置为非阻塞模式
ssc.configureBlocking (false);
//绑定9000端口
ssc.bind (new InetSocketAddress (9000));
//创建多路复用器
Selector selector = Selector.open ();
//将服务端的channel注册进selector中,可以看到返回的是SelectionKey,就是将这个key注册进selector中,该代码是注册服务端accept事件,关心的事客户端链接到服务端事件
//一般在客户端链接之后,获取channel,将该channel也注册进selector中,并赋于读写事件
SelectionKey register = ssc.register (selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println ("等待事件被注册进selector");
//轮询channel中selectionKey,第一次只有一个就是刚才注册进的OP_ACCEPT,也就是目前是等待被客户端注册进selection,阻塞状态
//当多路复用器获取到事件的发生,比如注册了OP_ACCEPT事件,当有客户端链接过来的时候就会取消阻塞,向下执行
selector.select ();
System.out.println ("有客户端注册进selector");
//当检测事件的发生,将发生事件的selectedKeys获取到,比如selector中有服务端链接事件的key和客户端的读写事件的key的集合,当监听到客户端链接或者客户端发过来消息的时候,返回对应key的集合
//客户端链接过来返回服务端accept事件的key,客户端发消息过来返回客户端read事件的key,如果在很短时间同时发生就两个key同时返回
//这里有个疑问就是 selector.select ();方法监听到事件发生的时候就会解除阻塞,那岂不是这里只能收集到一个key,这就涉及到系统底层原理,因为事件监听是系统完成的,当监听到一个事件发生的时候
//并不会马上的解除阻塞,会等一会,时间非常的短,不会马上的解除阻塞,这就解释了这里用key的集合,
//这里要区分 获取到的key集合并不是注册进selector的key集合, 获取到的集合是注册进selector的key集合,根据发生的事件筛选出来的key集合
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys ().iterator ();
System.out.println ("共有多少个链接要处理" + selector.selectedKeys ().size ());
while (selectionKeyIterator.hasNext ()) {
SelectionKey selectionKey = selectionKeyIterator.next ();
//防止事件的重复解析
selectionKeyIterator.remove ();
//处理key
handle (selectionKey);
}
}
}
private static void handle(SelectionKey selectionKey) throws IOException, InterruptedException {
//客户端链接
if (selectionKey.isAcceptable ()) {
System.out.println (("有客户端链接进来"));
//当客户端链接时,这里返回的服务端channel
ServerSocketChannel ssc = ( ServerSocketChannel ) selectionKey.channel ();
//这个方法也是阻塞的,但是既然能进到这里,证明就是客户端链接过来,所以下面方法虽说时阻塞,但是会马上执行,注意这里返回的是SocketChannel,就是客户端的channel
//而且客户端与之对应也会生成一个channel
SocketChannel channel = ssc.accept ();
//设置非阻塞
channel.configureBlocking (false);
System.out.println ("链接执行结束");
//将刚才获取到的客户端channel组策进selector中,当下一次客户端发消息的时候,selector就可以监听到了
channel.register (selectionKey.selector (), SelectionKey.OP_READ);
//这里的判断,就是再客户端的读事件注册进selector后,当客户端发送消息的时候就可以监听到
} else if (selectionKey.isReadable ()) {
System.out.println ("服务端读取客户端发送的消息");
//获取客户端的channele
SocketChannel socketChannel = ( SocketChannel ) selectionKey.channel ();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate (1024);
int let = socketChannel.read (buffer);
//读取buffer
if (let != -1) {
System.out.println ("读取客户端的信息" + new String (buffer.array (), 0, let));
}
ByteBuffer byteBuffer = ByteBuffer.wrap ("helloClient".getBytes ());
//channel是双向的,对于同一个客户端来说
// 这个channel和else之前注册的channel是同一个对象,
// 因为是双向的可以读也可以写,当客户端的selector注册了读事件,就可以监听到服务端返回的消息
socketChannel.write (byteBuffer);
//刚注册的是read事件,这里将channel赋予读写的事件监听
selectionKey.interestOps (SelectionKey.OP_READ | SelectionKey.OP_WRITE);
socketChannel.close ();
}
}
}
客户端
public class SocketClient {
public Selector selector;
public static void main(String[] args) throws IOException {
SocketClient socketClient = new SocketClient ();
socketClient.initClient ("127.0.0.1", 9000);
socketClient.connect ();
}
private void initClient(String ip, int port) throws IOException {
//创建客户端channel,注意区分服务端的ServerSocketChannel
SocketChannel socketChannel = SocketChannel.open ();
//非阻塞模式
socketChannel.configureBlocking (false);
//获取selector
Selector open = Selector.open ();
this.selector = open;
//链接服务端
socketChannel.connect (new InetSocketAddress (ip, port));
//将客户端注册进selector中,这个channel和服务端监听到客户端链接服务端产生的channel是一对
socketChannel.register (selector, SelectionKey.OP_CONNECT);
}
public void connect() throws IOException {
while (true) {
//阻塞,等带事件被监听,从而解除阻塞
selector.select ();
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys ().iterator ();
while (selectionKeyIterator.hasNext ()) {
SelectionKey selectionKey = selectionKeyIterator.next ();
selectionKeyIterator.remove ();
//如果通道是连接事件
if (selectionKey.isConnectable ()) {
SocketChannel channel = ( SocketChannel ) selectionKey.channel ();
//如果链接正在进行中
if (channel.isConnectionPending ()) {
//完成链接
channel.finishConnect ();
}
channel.configureBlocking (false);
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.wrap ("HelloNIOServer".getBytes ());
channel.write (byteBuffer);
//让通道注册进selector中,赋值read事件,用来读取服务端发过来的消息
channel.register (selector, SelectionKey.OP_READ);
//读取服务端返回的消息
} else if (selectionKey.isReadable ()) {
SocketChannel channel = ( SocketChannel ) selectionKey.channel ();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate (1024);
int len = channel.read (byteBuffer);
if (len != -1) {
System.out.println ("客户端接收到消息" + new String (byteBuffer.array (), 0, len));
}
}
}
}
}
}
网友评论