java-线程及NIO浅谈

这里简单谈一下线程,但是要把一个线程谈好,要结合NIO,结合锁机制一起学习,记忆才会深刻。所以,以下说明。结合这三个方面的线程和NIO进行谈谈,锁部分另外再谈。

1、进程

1.1概念

进程=程序+执行。当把一个程序从磁盘中加载到内存中,cpu去运算和处理这个进程(运行起来的程序就是进程)。

从三个维度来看进程的模型

维度 说明
从内存维度 每个进程都独占一块地址空间,cpu处理进程实际上就是处理这个进程内存的首地址到尾地址的数据库信息
从执行的逻辑维度 每一个进程都可以被CPU所处理和计算,此外,每一个进程也可以挂起,让其他进程得以处理。在同一个时刻,只能有一个进程被cpu所处理。总结:进程模型,在宏观上是并行处理的,但是微观上看,是“串行”处理的(单核)。如果是多核架构,宏观和微观上都是并行处理的。
时间维度 每个进程执行一段时间之后,肯定都完成了一定的工作量。即进程是随时间向前推进的。

在这里插入图片描述

1.2 为什么会引入进程模型?

最开始的操作系统是单道编程(一个程序处理完,再处理下一个程序)
缺点:1、响应时间慢(客户体验差)2、cpu利用率非常低
例如:假设一个进程,20%需要做cpu运算,80%在做IO(发送IO事件时,cpu时闲置的)=>cpu利用率是20%
单道编程模型:cup利用率=1-0.8=20%
多道编程模型下:
同时执行两个进程的话:1-0.80.8=36%
同时执行三个进程的话:1-0.8
0.8*0.8=48.8%
总结:引入进程模型,目的就是为了满足多道编程,而多道编程的目的就是为了提高cpu的利用率。随着进程数量的增加,cpu的利用率逐步提高。
在这里插入图片描述

1.3 进程产生和消亡的时间

产生 消亡
1、操作系统会产生服务进程 1、进程的所有运算都处理完之后,自行退出。
2、父进程创建子进程用户请求创建一个进程 2、进程在运行过程中产生错误或异常而强行退出。
3、用户请求创建一个进程 3、一个进程被其他进程所杀死而退出。

进程的状态

状态 说明
执行态 一个进程正在被cpu运行
挂起态 对于挂起态,要讨论什么原因导致的挂起:1、一个进程由于发生了某些阻塞操作,比如I/O事件,2、一个进程由于执行了很长时间,主动挂起,让其他进程得以处理。3、用户主动将进程挂起,比如sleep操作总结:针对第一类和第三类挂起的进程,即使把cpu让给这个进程,cpu也处理不了;我们把这样挂起的进程称为:阻塞态进程。第二类进程,称之为:就绪态进程。

所以细分进程的状态:执行态|就绪态|阻塞态(计算机在做进行调度时,是不会调度和查看阻塞态的进程的)

进程的状态转变

在这里插入图片描述

2、线程模型

2.1 概念

一个进程必须有一个线程,也可以有多个线程。
比如:拿word写笔记,这个word进程会监听打字;此外,每隔10分钟(默认值)自动保存一次。
总结:引入线程模型,目的就是为了让进程可以同时做多件事(线程是进程的分身)。此外,引入线程模型后,cpu的最小执行单位就变成了线程。

最后总结:进程相当于资源组织单位,线程是cpu最小执行单位。

调度算法
1、FCFS(First come first server )先来先服务调度算法,处理先来的线程,线程处理完后再处理下一个来的线程。
2、时间片轮转调度算法。这个算法的出现,解决了FCFS算法的问题,有效缩短了响应时间,提高了cpu利用率。但是这个算法可能存在的缺陷是:短任务处于饥饿状态。
在这里插入图片描述

3、短任务优先调度算法:优先去完成短任务。这个算法的局限性:可能会导致长任务时常处于饥饿状态。
4、优先级调度算法:为线程分别分配优先级,优先级高的线程优先被处理。局限性就是优先级低的线程长期处于饥饿状态。
5、混合调度算法:这个算法把之前算法的优点进行结合,然后做线程调度

总结:
短任务优先调度算法:更适合应用在大池子小队列
大池子小队列解释:

  • 没有核心线程,临时线程无限大(Integer的最大值),存活时间是60S,同步队列(只能容纳一个线程)
    优点:
  • 适用于高访问量、高并发的场景(短请求)。用户请求不需要
  • 等待排队,能够快速响应用户请求
  • 隐患:可能会出现线程的频繁创建和销毁。此外,如果用户的请求都是长请求,
  • 可能会发生内存溢出的场景
    核心代码如下:
ThreadPoolExecutor(0, Integer.MAX_VALUE,
  60L, TimeUnit.SECONDS,
  new SynchronousQueue<Runnable>())

时间片轮转调度算法,从小池子大队列去谈。
小池子大队列解释:

  • 核心线程数=最大线程数,没有临时线程
  • 阻塞队列是无限的
  • 优点:能够降低服务器的压力。
  • 缺点:用户请求的响应时间较长。
    核心代码如下:
ThreadPoolExecutor(nThreads, nThreads,
  0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue<Runnable>());

3、 NIO和BIO

3.1 NIO和BIO对比

NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O
之前学习的I/O操作是BIO,即阻塞I/O

BIO NIO
面向流的:InputStream(),OuputStream字节输入流,字节输出流Reader,Writer 面向缓冲区的(Buffer)NIO是通过缓冲区去操作数据的,可以用缓冲区灵活操作数据。
字符输入流,字符输出流.特点:流是有方向性的、连续不断的,总结局限性:不能灵活的操作流里的数据 此外,NIO是面向通道的(Channel),在通道上,即可以读数据,也可以写数据。总结:通道相当于提供了运算环境,缓冲区是运输数据的载体。在这里插入图片描述
阻塞的IO比如Socket,它的底层用的BIO机制,accept()、connect()、write()、read()调用时会产生阻塞。阻塞模型的局限性:不可能应对高并发、高访问量的场景。 NIO是非阻塞的IO,可以利用NIO处理高并发和高访问量的场景。

在这里插入图片描述

3.2 BIO API使用

具体流程:
A:测试accept()方法的阻塞

/**accept()方法:创建一个连接,这个方法会产生阻塞,直到连接建立之后,阻塞才会放开。
 * Listens for a connection to be made to this socket and accepts
 * it. The method blocks until a connection is made.
 * @throws Exception
 */
@Test
public void testAccept() throws Exception{
	ServerSocket ss = new ServerSocket();
	//绑定端口
	ss.bind(new InetSocketAddress(6666));
	//获取连接
	Socket sk = ss.accept();
	System.out.println("有客户端接入");
}

JUnit测试,“有客户端接入”没有输出,说明accept()方法产生阻塞了。
B:然后添加connect()方法测试的代码:

/**connect()方法:作用根据指定的ip地址和端口号建立连接
 * 该方法会产生阻塞,直到连接建立之后阻塞才会被放开。
 * Connects this socket to the server with a specified timeout value.
 * A timeout of zero is interpreted as an infinite timeout. The connection
 * will then block until established or an error occurs.
 * @throws Exception
 */
@Test
public void connect() throws Exception{
	Socket sk = new Socket();
	//建立连接
	sk.connect(new InetSocketAddress("127.0.0.1", 6666));
	System.out.println("连接建立");

先运行服务器端方法(testAccept()),再运行客户端方法,发现accept()方法阻塞释放了。另外“连接建立”正确输出。如果不先启动服务器端方法,而直接运行客户端方法,发现先是阻塞了一下,然后JUnit测试抛出异常。
总结:connect()方法会产生阻塞,直到连接成功,阻塞才释放。
accept()方法产生的阻塞,直到服务器获得连接后,阻塞才释放。
C:测试read()方法的阻塞性
C1: 再次修改testAccept()方法

//获取连接
	Socket sk = ss.accept();
	System.out.println("有客户端接入");
	InputStream in = sk.getInputStream();
	in.read();
	System.out.println("读取到了内容");

先运行服务器的方法testAccept方法,在运行connect(),发现有客户端连入输出了,但是读取到了内容并没有显示出来。说明read()方法也会产生阻塞。片刻后connect()快速执行完毕,socket连接断开,接着testAccept()结束了。
C2:为了不让连接中断,需要修改testConnect()

System.out.println("连接建立");
while(true);

总结:read()方法会产生阻塞,直到读取到内容后,阻塞才被释放。
D:测试write()方法的阻塞性
D1:修改testAccept()方法

OutputStream out = sk.getOutputStream();
	for(int i =1;i<200000;i++){
		//65513*10 
		out.write("helloworld".getBytes());
		System.out.println(i);
	}
	System.out.println("写出了数据");

先运行服务器端方法,再运行客户端方法;发现i输出值为65513,阻塞了。

for(int i =1;i<200000;i++){
		//65513*10  ~ 131026*5
		out.write("hello".getBytes());
		System.out.println(i);
	}

微调代码,输出到131026阻塞了。
总结:write()方法也会产生阻塞,write()一直往出写数据,但是没有任何一方读取数据,直到写出到一定量(我的是655130B,不同电脑可能不同)的时候,产生阻塞。向网卡设备缓冲区中写数据。

3.3 NIO 相关API

Channel查看API:
ServerSocketChannel, SocketChannel基于NIO的(基于tcp实现的,安全的基于握手机制)
DatagramChannel基于UDP协议,不安全

NIO-Channel API(缓冲通道)

accept和connect使用

/**NIO提供两种模式:阻塞模式和非阻塞模式;默认是阻塞模式。
	 * 运行程序,“有客户端连入了”并没有输出,说明NIO默认是阻塞模式。
	 * @throws Exception
	 */
	@Test
	public void testAccept() throws Exception{
		//创建ServerSocketChannel对象
		ServerSocketChannel ssc = ServerSocketChannel.open();
		//绑定监听的端口
		ssc.socket().bind(new InetSocketAddress(5555));
		//获取socketChannel连接
		SocketChannel socketChannel = ssc.accept();
		System.out.println("有客户端连入了");
}

运行发现,并没有输出“有客户端连接接入”,通道提供阻塞和非阻塞两种模式,默认为阻塞模式。
可以在bind port之前添加ssc.configureBlocking(false);
设置通道的非阻塞模式。再次运行“有客户端连接接入”便输出了。

	/**NIO提供两种模式:阻塞模式和非阻塞模式;默认是阻塞模式。
	 * 运行程序,“有客户端连入了”并没有输出,说明NIO默认是阻塞模式。
	 * @throws Exception
	 */
	@Test
	public void testAccept() throws Exception{
		//创建ServerSocketChannel对象
		ServerSocketChannel ssc = ServerSocketChannel.open();
		//设置为非阻塞模式
		ssc.configureBlocking(false);
		//绑定监听的端口
		ssc.socket().bind(new InetSocketAddress(5555));
		//获取socketChannel连接
		SocketChannel socketChannel = ssc.accept();
		System.out.println("有客户端连入了");
}
/**connect()建立连接的方法,默认也会产生阻塞。
 * @throws Exception
 */
@Test
public void connect() throws Exception{
	//创建SocketChannel对象
	SocketChannel sc = SocketChannel.open();
	//建立连接
	sc.connect(new InetSocketAddress("127.0.0.1", 5555));
	System.out.println("连接成功");
}

**加sc.configureBlocking(false);**之前,运行该方法显示阻塞一下然后抛出异常,并没有输出“连接成功”,通道的connect()方法也是阻塞的;**使用方法sc.configureBlocking(false);**可以将客户端连接通道设置为非阻塞模式。

accept和connect总结

/**NIO提供两种模式:阻塞模式和非阻塞模式;默认是阻塞模式。
 * 运行程序,“有客户端连入了”并没有输出,说明NIO默认是阻塞模式。
 * @throws Exception
 */
@Test
public void testAccept() throws Exception{
	//创建ServerSocketChannel对象
	ServerSocketChannel ssc = ServerSocketChannel.open();
	//设置为非阻塞模式
	ssc.configureBlocking(false);
	//绑定监听的端口
	ssc.socket().bind(new InetSocketAddress(5555));
	//获取socketChannel连接
	SocketChannel socketChannel = ssc.accept();
	System.out.println("有客户端连入了");
}
/**connect()建立连接的方法,默认也会产生阻塞。
 * 设置为非阻塞模式的话,需要在调用connect方法之前,使用
 * configureBlocking(false)将模式设置为非阻塞模式。
 * @throws Exception
 */
@Test
public void connect() throws Exception{
	//创建SocketChannel对象
SocketChannel sc = SocketChannel.open();
//设置阻塞模式为非阻塞
sc.configureBlocking(false);
//建立连接
sc.connect(new InetSocketAddress("127.0.0.1", 5555));
System.out.println("连接成功");
}

注意的是,需要在服务器和客户端分别设置阻塞模式。

3.4 read()、write()方法测试(过度)

sc.read(ByteBuffer dst)
sc.write(ByteBuffer src)
由于这两个方法都需要ByteBuffer对象作为参数,所以我们需要先讲ByteBuffer缓冲区。

3.4.1 NIO-ByteBuffer缓冲区API

/**ByteBuffer字节缓存区
 * 1)三个重要的属性:
 * capacity:缓存的容量
 * limit:缓存区的限制(position<limit)
 * position:缓存区的当前位置
 * 2)重要的基本方法
 * allocate(int cap):新建一个指定容量的缓存区。
 * flip():反转缓存区,将limit设置为position的值,在将position=0.
 * hasRemaining():判断position和limit限制之间是否还有元素。
 * wrap("**".getBytes()):根据字符串的内容创建“对应”的缓存区。
 * 
 * put(byte bt):向缓冲区中添加元素。执行一次position++
 * get():从缓存区中获取元素。执行一次position++
 * 
 * limit():获取limit的值
 * limit(int newlt):设置limit()的值
 * position():获取position的值
 * position(int newPs):设置position的值
 * @author jinxf
 *
 */
public class ByteBufferDemo {
	/**ByteBuffer字节缓存区
	 * 该类有三个重要的属性:
	 * capacity:新缓冲区的容量,以字节为单位 ,指定缓存区的长度
	 * limit:缓冲区的限制(限定获取元素的最大下标)
	 * position:缓冲区的位置
	 */
	@Test
    public void bbInfo(){
    	ByteBuffer buf =ByteBuffer.allocate(10);
    	System.out.println();
    }
	/**向缓冲区中添加内容。
	 * 可以添加字节内容;
	 * 也可以其他的基本数据类型,一般不建议使用。
	 * 每添加一个字节后,position++。
	 * 其他两个属性capacity和limit不会发生改变。
	 */
	@Test
	public void testPut(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		Byte b1 = 1;
		Byte b2 = 3;
		buf.put(b1);
		buf.put(b2);
		//buf.putInt(10);
		System.out.println();
	}
	/**get():从当前position值对应的位置获取元素,每
	 * 获取一个元素值后position++;
	 * buf.position()获取postion值
	 * buf.position(int newPs):设置postion的值
	 * limit()获取限定值
	 * limit(int newlt):设置限定的值。
	 * BufferUnderflowException:通过get()从缓冲区中取元素内容
	 * 时position>=limit
	 */
	@Test
	public void testGet(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		Byte b1 = 1;
		Byte b2 = 3;
		buf.put(b1);
		buf.put(b2);
		//获取当前的位置的值buf.position();
		//设置当前位置的值
		buf.position(0);
		//获取limit的值:buf.limit();
		buf.limit(2);
		System.out.println(buf.get());
		System.out.println(buf.get());
		System.out.println(buf.get());
	}
	/*flip():反转缓冲区的方法
	 * 作用是:
	 * 将limit的值设置为position
	 * 将position值设置为0
	 */
	@Test
	public void testFilp() {
		ByteBuffer buf = ByteBuffer.allocate(10);
		Byte b1 = 1;
		Byte b2 = 3;
		buf.put(b1);
		buf.put(b2);
		Byte b3 = 4;
		buf.put(b3);
		buf.flip();
		/*System.out.println(buf.get());
		System.out.println(buf.get());
		System.out.println(buf.get());
		//当执行以下代码时抛出异常
		System.out.println(buf.get());*/
		for(int i=0;i<buf.limit();i++){
			System.out.println(buf.get());
		}
	}
	/**hasRemaining()检查position和limit之间是否还有元素。
	 * 本质是判断position是否小于limit
	 */
	@Test
	public void hasRemaining(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		Byte b1 = 1;
		Byte b2 = 3;
		buf.put(b1);
		buf.put(b2);
		Byte b3 = 4;
		buf.put(b3);
		buf.flip();
		while(buf.hasRemaining()){
			System.out.println(buf.get());
		}
	}
	@Test
	public void testWrap() throws Exception{
		/*ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());*/
		ByteBuffer buf = ByteBuffer.wrap("中国".getBytes("UTF-8"));
		while(buf.hasRemaining()){
			System.out.print(buf.get());
		}
	}
}

1、read()方法
修改NIODemo类的testAccept方法:

System.out.println("有客户端连入了");
//读取缓冲区中的内容
ByteBuffer buf = ByteBuffer.allocate(10);
//将读取的内容保存到缓冲区中
socketChannel.read(buf);
System.out.println("读取到内容"+new String(buf.array()));

testConnect()方法不做任何修改,先运行testAccept()方法,发现在sc.read(buf)行抛出了空指针异常。buf对象不可能为null,所以socketChannel为null.
非阻塞编程最大的问题:不知道是否真正的有客户端接入,所以容易产生空指针;所以需要人为设置阻塞。
将**SocketChannel sc = ssc.accept();**改为:

	//获取socketChannel连接
	SocketChannel socketChannel = null;
	while(socketChannel==null){
		socketChannel = ssc.accept();
	}

再次运行testAccept()方法,空指针的问题解决了;然后再运行testConnect()方法,发现连接能够正常建立,但是“读取到内容**”并没有输出。
修改connect()方法:

while(true);

再次运行testAccept(),再运行connect(),”有客户端连入了”输出了,但是” 读取到内容**”并没有输出。说明即使ssc服务通道设置了非阻塞,也没有改变得到的通道socketChannel默认为阻塞模式,所以socketChannel.read(buf)阻塞了。要不想让read()方法阻塞,需要在调用read()之前加

socketChannel.configureBlocking(false);

这样即使没有读到数据,“读取到内容”也能打印出来。

write()方法
修改contect()方法,追加以下代码:

ByteBuffer buf = ByteBuffer.wrap("helloworld".getBytes());
sc.write(buf);
while(true);

测试bug,先不运行服务器端方法,直接运行客户端方法connect(),输出“连接成功”,但是sc.write(buf)行抛出NotYetConnectedException异常。sc为何会抛出该异常呢?非阻塞模式很坑的地方在于不知道连接是否真正的建立。修改connect():

while(!sc.isConnected()){
	sc.finishConnect();
}
System.out.println("连接成功");

再次运行testConnect(),空指针解决了,但是有出现了新的异常:

java.net.ConnectException: Connection refused: no further information
	at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)

先启动服务器端(testAccept()),后启动客户端(connect())即可。
手写NIO非阻塞模式难度较大,代码不是重点,重要在于引出设计思想。

public class NIODemo {
/**NIO提供两种模式:阻塞模式和非阻塞模式;默认是阻塞模式。
 * 运行程序,“有客户端连入了”并没有输出,说明NIO默认是阻塞模式。
 * @throws Exception
 */
@Test
public void testAccept() throws Exception{
	//创建ServerSocketChannel对象
	ServerSocketChannel ssc = ServerSocketChannel.open();
	//设置为非阻塞模式
	ssc.configureBlocking(false);
	//绑定监听的端口
	ssc.socket().bind(new InetSocketAddress(5555));
	//获取socketChannel连接
	SocketChannel socketChannel = null;
	while(socketChannel==null){
		socketChannel = ssc.accept();
	}
	System.out.println("有客户端连入了");
	//设置当前socketChannel设置为非阻塞
	socketChannel.configureBlocking(false);
	//读取缓冲区中的内容
	ByteBuffer buf = ByteBuffer.allocate(10);
	//将读取的内容保存到缓冲区中
	socketChannel.read(buf);
	System.out.println("读取到内容"+new String(buf.array()));
}
/**connect()建立连接的方法,默认也会产生阻塞。
 * 设置为非阻塞模式的话,需要在调用connect方法之前,使用
 * configureBlocking(false)将模式设置为非阻塞模式。
 * @throws Exception
 */
@Test
public void connect() throws Exception{
	//创建SocketChannel对象
SocketChannel sc = SocketChannel.open();
//设置阻塞模式为非阻塞
sc.configureBlocking(false);
//建立连接
sc.connect(new InetSocketAddress("127.0.0.1", 5555));
while(!sc.isConnected()){
	sc.finishConnect();
}
System.out.println("连接成功");
ByteBuffer buf = ByteBuffer.wrap("helloworld".getBytes());
sc.write(buf);
System.out.println("写出数据");
while(true);
}
}

3.5 Selector设计思想

3.5.1问题的引入

在这里插入图片描述
使用BIO编写代码模拟一下
(编写一个服务器端和客户端程序,运行一次服务器程序,运行四次客户端程序模拟四个用户线程)

public class Server {
	public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(7777));
		while(true){
			Socket sk = ss.accept();
			new Thread(new ClientRunner(sk)).start();
		}
	}
}
class ClientRunner implements Runnable{
	Socket sk = null;
	public ClientRunner(Socket sk){
		this.sk = sk;
	}
	public void run() {
		System.out.println("负责为客户端提供服务,当前线程的id:"+Thread.currentThread().getId());
	}
}
public class Client {
	public static void main(String[] args) throws Exception {
		Socket sk = new Socket();
		sk.connect(new InetSocketAddress("127.0.0.1", 7777));
		//System.out.println("连接建立");
		while(true);
	}
}

执行结果:

服务器启动
负责为客户端提供服务,当前线程的id:9
负责为客户端提供服务,当前线程的id:10
负责为客户端提供服务,当前线程的id:11
负责为客户端提供服务,当前线程的id:12

在这里插入图片描述
分析该模式的缺点:
缺点1:每有一个用户请求,就会创建一个新的线程为之提供服务。当用户请求量特别巨大,线程数量就会随之增大,继而内存的占用增大,所以不适用于高并发、高访问的场景。
缺点2:线程特别多,不仅占用内存开销,也会占用大量的cpu开销,因为cpu要做线程调度。
缺点3:如果一个用户仅仅是连入操作,并且长时间不做其他操作,会产生大量闲置线程。会使cpu做无意义的空转,降低整体性能。
缺点4:这个模型会导致真正需要被处理的线程(用户请求)不能被及时处理。

3.5.2 解决方法

针对缺点3和缺点4,可以将闲置的线程设置为阻塞态,cpu是不会调度阻塞态的线程,避免了cpu的空转。所以引入事件监听机制实现。
Selector多路复用选择器,起到事件监听的作用。
监听哪个用户执行操作,就唤醒对应的线程执行。那么都有哪些事件呢?
事件:1.accept事件、2.connect事件、3.read事件、4.write事件
在这里插入图片描述

针对缺点1和缺点2,可以利用非阻塞模型来实现,利用少量线程甚至一个线程来处理多用户请求。但是注意,这个模型是有使用场景的,适用于大量短请求场景。(比如用户访问电商网站),不适合长请求场景(比如下载大文件,这种场景,NIO不见得比BIO好)

在这里插入图片描述

扩展知识
惊群现象,隐患:cpu的负载会在短时间之内聚升,最严重的情况时出现短暂卡顿甚至死机。第二个问题就是性能不高。

3.6 Selector服务通道API

accept事件
编写服务器端程序:

public class Server {
	public static void main(String[] args) throws Exception {
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);
		ssc.socket().bind(new InetSocketAddress("127.0.0.1", 9999));
		//创建多路复用选择器
		Selector selector = Selector.open();
		//在scc上注册accpet事件,并指定由selector负责处理这些事件
		ssc.register(selector, SelectionKey.OP_ACCEPT);
		while(true){
			//select()会产生阻塞,直到监听到事件触发阻塞的释放
			selector.select();
			//获取事件的集合对象
			Set<SelectionKey> keys = selector.selectedKeys();
			//获取迭代器
			Iterator<SelectionKey> iter = keys.iterator();
			//遍历迭代器
			while(iter.hasNext()){
				//sk就是一个封装了事件信息、通道信息
				SelectionKey sk = iter.next();
				//当前事件是否为accept事件
				if(sk.isAcceptable()){
					//注意:要使用sk中封装的ServerSocketChannel
					ServerSocketChannel ss = (ServerSocketChannel)sk.channel();
					//设置非阻塞
					ss.configureBlocking(false);
					SocketChannel sc = ss.accept();
					sc.configureBlocking(false);
					System.out.println("有客户端连入,负责处理该请求的线程id:"+Thread.currentThread().getId());
					//将该通道上的accpet事件
					//OP_ACCEPT:1 << 4    0001 0000
					//OP_READ:1<<0        0000 0001
					//OP_WRITE = 1 << 2   0000 0100
					//OP_CONNECT = 1 << 3 0000 1000
					sc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
				}
				if(sk.isReadable()){
					
				}
				if(sk.isWritable()){
					
				}
				//勿忘我:将该事件对象移除
				iter.remove();
			}
		}
	}
}

编写客户端代码:

public class Client {
	public static void main(String[] args) throws Exception {
		SocketChannel sc = SocketChannel.open();
		sc.configureBlocking(false);
		sc.connect(new InetSocketAddress("127.0.0.1", 9999));
		//System.out.println("连接建立");
		while(true);
	}
}

服务器端启动一次,客户端启动三次,服务器端的控制台输出:

服务器端启动
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1
有客户端连入,负责处理该请求的线程id:1

处理多个请求使用同一个线程。

3.6.2 read事件

修改Server类

if(sk.isReadable()){
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf = ByteBuffer.allocate(10);
	sc.read(buf);
	System.out.println("服务器端读到了数据:"+new String(buf.array()));
	//去掉读事件
	//sc.register(selector,SelectionKey.OP_WRITE);
	//0000 0101  sk.interestOps():原来的事件
	//1111 1110  ~SelectionKey.OP_READ
	//0000 0100  
	sc.register(selector, sk.interestOps()&~SelectionKey.OP_READ);
}

修改Client类

while(!sc.isConnected()){
	sc.finishConnect();
}
ByteBuffer buf = ByteBuffer.wrap("helloworld".getBytes());
sc.write(buf);

3.6.3 write事件

修改Servet

if(sk.isWritable()){
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf =ByteBuffer.wrap("收到".getBytes("UTF-8"));
	sc.write(buf);
	System.out.println("服务器写出了数据");
	//去掉写事件
	sc.register(selector, sk.interestOps()&~SelectionKey.OP_WRITE);
}

修改Client类

ByteBuffer buf2 = ByteBuffer.allocate(6);
		sc.read(buf2);
		System.out.println("客户端接收的内容:"+new String(buf2.array(),"UTF-8"));

在这里插入图片描述

public class Client2 {
	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.configureBlocking(false);
		sc.connect(new InetSocketAddress("127.0.0.1", 9999));
		//对于客户端,最开始要注册连接监听
		Selector selector = Selector.open();
		sc.register(selector, SelectionKey.OP_CONNECT);
		while(true){
			selector.select();
			Set<SelectionKey> set = selector.selectedKeys();
			Iterator<SelectionKey> iter = set.iterator();
			while(iter.hasNext()){
				SelectionKey sk = iter.next();
				if(sk.isConnectable()){
				}
				if(sk.isWritable()){
				}
				if(sk.isReadable()){
				}
				iter.remove();
			}
		}
	}
}

猜你喜欢

转载自blog.csdn.net/laughing1997/article/details/83177292
今日推荐