文章目录
本文档整理并出自尚硅谷韩顺平Netty教程
https://www.bilibili.com/video/BV1DJ411m7NR
关键词:Buffer、Channel、SelectionKey、Selector、事件
Buffer:
- 缓存数组,就是一个内存块,底层用数组实现
- 与
Channel
进行数据的读写。 - 数据的读取写入是通过
Buffer
, 这个和BIO
一样, 而BIO
中要么是输入流,或者是输出流, 不能双向,但是NIO
的Buffer
是可以读也可以写, 需要flip
方法切换。
Channel:
- 通信通道,每个客户端连接都会建立一个Channel通道
- 我的理解是:客户端直接与
Channel
进行通信,当客户端发送消息时,消息就流通到Channel
里面,本地程序需要将Channel
里面的数据存放在Buffer
里面,才可以查看;当本地需要发送消息时,先把消息存在Buffer
里面,再将Buffer
里面的数据放入Channel
,数据就流通到了客户端 - 总而言之:
Buffer
就是本地程序与Channel
数据交换的一个中间媒介。
SelectionKey、Selector:
-
NIO之所以是非阻塞的,关键在于它一个线程可以同时处理多个客户端的通信。而
Selector
就是它一个线程如何处理多个客户端通信的关键,一个Selector
就对应一个线程 -
首先在创建与客户端连接的
Channel
时,应该调用Channel.register()
方法,将Channel
注册到一个Selector
上面。调用该方法后,会返回一个SelectionKey
对象,该对象与Channel
是一一对应的。而Selector
则通过管理SelectionKey
的集合间接的去管理各个Channel
。示例图如下: -
Selector
具体是如何管理这么多个通信的呢?这就引出了事件。
事件、以及NIO的工作流程介绍
-
**事件:**当将
Channel
绑定到Selector
上面时,必须同时为该Channel
声明一个监听该Channel
的事件(由Channel
和该Channel
的事件一起组成了SelectionKey
),并将SelectionKey
加入到Selector
的Set
集合中去 -
当有客户端建立连接或者进行通信,会在对应的各个Channel中产生不同的事件。
-
Selector
会一直监听所有的事件,当他监听到某个SelectionKey
中有事件产生时,会将所有产生事件的SelectionKey
统一加入到一个集合中去 -
而我们则需要获取到这个集合,首先对集合中的各个
SelectionKey
进行判断,判断它产生的是什么事件,再根据不同的事件进行不同的处理。 -
在操作这个
SelectionKey
集合的时候,其实我们就是在一个线程里面对几个不同客户端的连接进行操作。具体的关系图如下:
1、缓冲区(Buffer)
1.1、基本介绍
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel
提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
。
1.2、Buffer类介绍
- 基类是
Buffer
抽象类 - 基类派生出基于基本数据类型的7个
xxxBuffer
抽象类,没有boolean
相关的buffer
类。 - 除了
ByteBuffer
外,每个基本数据的抽象类xxxBuffer
类下面都派生出转向ByteBuffer
的类ByteBufferAsXxxBufferL
和ByteBufferAsXxxBufferB
实现类;以及DirectXxxBufferU
和DirectXxxBufferS
和HeapXxxBuffer
(具体实例对象类)这五个类。 - 就只有抽象类
CharBuffer
派生出了第六个类StringCharBuffer
。 ByteBuffer
只派生出了HeapByteBuffer
和MappedByteBufferR
两个类
1.2.1、Buffer类主要属性
//标记 ,一般不会主动修改,在flip()被调用后,mark就作废了。
private int mark = -1;
//位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
private int position = 0;
//表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
private int limit;
//容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
private int capacity;
mark <= position <= limit <= capacity
1.2.2、Buffer类使用示例
//创建一个Buffer,大小为5,即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
//向buffer中存放数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
//如何从buffer中读取数据
//将buffer转换,读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
-
Buffer
刚创建时,capacity = 5
,固定不变。limit
指针指向5
,position
指向0
,mark
指向-1
-
之后调用
intBuffer.put
方法,向buffer
中添加数据,会不断移动position
指针,最后position
变量会和limit
指向相同。 -
调用
buffer.flip()
实际上是重置了position
和limit
两个变量,将limit
放在position
的位置,position
放在0
的位置。这里只是最后的position
和limit
位置相同,所以flip
后limit
位置没变。 -
调用
intBuffer.get()
实际上是不断移动position
指针,直到它移动到limit
的位置
2、通道(Channel)
2.1、基本介绍
- NIO的通道类似于流,但有些区别
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓存读数据,也可以写数据到缓存
- BIO 中的
stream
是单向的,例如:FileInputStream
对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。 Channel
在 NIO 中是一个接口:public interface Channel extends Closeable{}
- 常用的
Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
(类似ServerSocket
)、SocketChannel
(类似Socket
) FileChannel
用于文件数据的读写,DatagramChannel
用于UDP数据的读写,ServerSocketChannel
和SocketChannel
用于TCP数据读写
2.2、FileChannel类
2.2.1、常见方法
public int read(ByteBuffer dst)
,从通道读取数据并放到缓冲区中public int write(ByteBuffer src)
,把缓冲区的数据写到通道public long transferFrom(ReadableByteChannel src, long position, long count)
,从目标通道复制数据到当前通道public long transferTo(long position, long count, WritableByteChannel target)
,把数据从当前通道复制到目标通道,该方法拷贝数据使用了零拷贝,通常用来在网络IO
传输中,将FileChannel
里面的文件数据直接拷贝到与客户端或者服务端连接的Channel
里面从而达到文件传输
2.2.2、应用实例
实例1:将数据写入到本地文件
String str="Hello,Netty!!!";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream("e:\\file01.txt");
//通过fileOutputStream获取对应的FileChannel
//这个channel的真实类型是FileChannelImpl
FileChannel channel = fileOutputStream.getChannel();
//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将字符串读入缓冲区
buffer.put(str.getBytes());
//切换缓冲区的读写
buffer.flip();
//将缓冲区的数据写入通道
channel.write(buffer);
fileOutputStream.close();
实例2:从本地文件读取数据
//创建文件
File file = new File("e:\\file01.txt");
//创建输入流
FileInputStream fileInputStream = new FileInputStream(file);
//根据输入流获取channel
FileChannel channel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
//将通道的数据写入缓冲区
channel.read(buffer);
//将buffer中的字节转化为字符串
System.out.println(new String(buffer.array()));
//关闭流
fileInputStream.close();
实例3:使用一个Buffer完成文件的读取
File file = new File("e:\\file01.txt");
//输入流
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel channel01 = fileInputStream.getChannel();
//创建输出流
FileOutputStream fileOutputStream = new FileOutputStream("e:\\file02.txt");
FileChannel channel02 = fileOutputStream.getChannel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
//循环读写
while (true){
//将buffer清空,重要操作,不能忘
buffer.clear();
int read = channel01.read(buffer);
System.out.println("read:"+read);
if(read==-1){
break;
}
//将缓冲区的读写转换
buffer.flip();
channel02.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
实例4:拷贝文件 transferFrom 方法
//创建相关的流
FileInputStream fileInputStream = new FileInputStream("e:\\file01.txt");
FileOutputStream fileOutputStream = new FileOutputStream("e:\\file03.txt");
//获取channel
FileChannel source = fileInputStream.getChannel();
FileChannel dest = fileOutputStream.getChannel();
//使用transferFrom完成拷贝
dest.transferFrom(source,0,source.size());
//关闭相关流
source.close();;
dest.close();
fileInputStream.close();
fileOutputStream.close();
2.3、ServerSocketChannel 和 SocketChannel 类
2.3.1、常见方法
ServerSocketChannel:主要用于在服务器监听新的客户端Socket
连接
public static ServerSocketChannel open()
,得到一个ServerSocketChannel
通道public final ServerSocketChannel bind(SocketAddress local)
,设置服务器监听端口public final SelectableChannel configureBlocking(boolean block)
,用于设置阻塞或非阻塞模式,取值false
表示采用非阻塞模式public abstract SocketChannel accept()
,接受一个连接,返回代表这个连接的通道对象public final SelectionKey register(Selector sel, int ops)
,将Channel
注册到选择器并设置监听事件,也可以在绑定的同时注册多个事件,例如:channel.register(selector,Selectionkey.OP_READ | Selectionkey.OP_CONNECT)
SocketChannel:网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区
public static SocketChannel open()
,得到一个SocketChannel通道public final SelectableChannel configureBlocking(boolean block)
,用于设置阻塞或非阻塞模式,取值false
表示采用非阻塞模式public abstract boolean connect(SocketAddress remote)
,连接服务器public boolean finishConnect()
,如果上面的方法连接失败就用该方法连接public int write(ByteBuffer src)
,往通道里写数据public int read(ByteBuffer dst)
,从通道里读数据public final SelectionKey register(Selector sel, int ops, Object att)
,注册Channel
到选择器并设置监听事件,最后一个参数可以设置共享数据public final void close()
,关闭通道
2.3.2、应用实例
通过Buffer
数组来完成读写操作,即Scattering
和Gathering
/**
* Scattering:将数据写入buffer时,可以采用buffer数组依次写入
* Gathering:从buffer读取数据时,可以从buffer数组中依次读入
*/
public class ScatteringAndGathering {
public static void main(String[] args) throws IOException {
//使用ServerSocketChannel和SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0]=ByteBuffer.allocate(5);
byteBuffers[1]=ByteBuffer.allocate(3);
//等待客户端的连接(telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
//假设从客户端接收八个字节
int messageLength=8;
//循环读取
while (true){
int byteRead=0;
while (byteRead<messageLength){
long l = socketChannel.read(byteBuffers);
byteRead+=l;
System.out.println("byteRead:"+byteRead);
//使用流打印,看看当前buffer的position和limit
Arrays.asList(byteBuffers).stream().map(buffer->"position="+buffer.position()+",limit="+buffer.limit())
.forEach(System.out::println);
}
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(buffer->buffer.flip());
//将数据读取显示到客户端
long byteWrite=0;
while (byteWrite<messageLength){
long l = socketChannel.write(byteBuffers);
byteWrite+=l;
}
//将所有buffer进行清理
Arrays.asList(byteBuffers).forEach(buffer->{
buffer.clear();
}); System.out.println("byteRead="+byteRead+"byteWrite="+byteWrite+",messageLength="+messageLength);
}
}
}
3、Selector(选择器)
3.1、基本介绍
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
- Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销
Netty
的IO
线程NioEventLoop
聚合了Selector
(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。- 当线程从某客户端
Socket
通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。 - 线程通常将非阻塞
IO
的空闲时间用于在其他通道上执行IO
操作,所以单独的线程可以管理多个输入和输出通道。 - 由于读写操作都是非阻塞的,这就可以充分提升
IO
线程的运行效率,避免由于频繁I/O
阻塞导致的线程挂起。 - 一个
I/O
线程可以并发处理N
个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O
一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.2、SelectionKey介绍
Selector
通过管理SelectionKey
的集合从而去监听各个Channel
。当Channel
注册到Selector
上面时,会携带该Channel
关注的事件**(SelectionKey包含Channel以及与之对应的事件)**,并会返回一个SelectionKey
的对象,Selector
将该对象加入到它统一管理的集合中去,从而对Channel
进行管理。SelectionKey
表示的是Selector
和网络通道的注册关系,故FileChannel
是没有办法通过SelectionKey
注册到Selector
上去的。
四大事件
//读操作
public static final int OP_READ = 1 << 0;
//写操作
public static final int OP_WRITE = 1 << 2;
//一般在ServerSocketChannel上绑定该事件,结合 channel.finishConnect()在连接建立异常时进行异常处理
public static final int OP_CONNECT = 1 << 3;
//与ServerSocketChannel进行绑定,用于创建新的SocketChannel,并把其注册到Selector上去
public static final int OP_ACCEPT = 1 << 4;
3.3、Selector常见方法
public static Selector open()
,得到一个选择器对象,实例化出WindowsSelectorImpl
对象public int select(long timeout)
,监控所有注册的通道,当其中有IO
操作可以进行时,将对应的SelectionKey
加入到内部集合中并返回,返回的结果为Channel
响应的事件总和,当结果为0
时,表示本Selector
监听的所有Channel
中没有Channel
产生事件。如果不传入timeout
值,就会阻塞线程,传入值则为阻塞多少毫秒,通过它设置超时时间。之所以需要传入时间,是为了让它等待几秒钟再看有没有Channel
会产生事件,从而获取一段时间内产生事件的Channel
的总集合再一起处理。selector.selectNow()
,不会阻塞,立马返回冒泡的事件数public Set<SelectionKey> selectedKeys()
,从内部集合中得到所有的SelectionKey
4、Demo实例
编码步骤:
- 当客户端连接时,会通过
ServerSocketChannel
得到SocketChannel
Selector
进行监听select
方法, 返回有事件发生的通道的个数.- 将
SocketChannel
注册到Selector
上,register(Selector sel, int ops)
, 一个selector
上可以注册多个SocketChannel
- 注册后返回一个
SelectionKey
, 会和该Selector
关联(集合) - 进一步得到各个
SelectionKey
(有事件发生) - 在通过
SelectionKey
反向获取SocketChannel
, 方法channel()
- 判断该
Channel
的事件类型,对不同事件进行不同的业务处理
4.1、NIO入门案例:实现服务器和客户端的简单通讯
服务端
public class NioServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//得到一个Selector对象
Selector selector = Selector.open();
//将ServerSocketChannel设置成非阻塞
serverSocketChannel.configureBlocking(false);
//将ServerSocketChannel注册到Selector,事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
//这里我们等待一秒,如果没有事件发生,返回
if(selector.select(1000)==0){
System.out.println("客户端无连接,等待一秒");
continue;
}
//如果返回>0,就获取相应的SelectionKey集合
//1.如果返回>0,表示已经获取到关注的事件
//2.selector.selectedKeys()返回关注事件集合
//通过selectionKeys反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//使用迭代器
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()){
//获取SelectionKey
SelectionKey key = selectionKeyIterator.next();
//根据key对应通道的事件做出相应的处理
if(key.isAcceptable()){ //如果是OP_ACCEPT,有新用户连接
//给该客户端创建一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//输出连接的SocketChannel
System.out.println("socketChannel:"+socketChannel.hashCode());
//将SocketChannel设置成非阻塞的
socketChannel.configureBlocking(false);
//将SocketChannel注册到selector
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable()){ //发生OP_READ
//通过key获取当前通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//获取该channel关联的Buffer
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
socketChannel.read(byteBuffer);
System.out.println("from 客户端:"+ new String(byteBuffer.array()));
}
//手动从集合中移除当前的SelectionKey,防止重复操作
selectionKeyIterator.remove();
}
}
}
}
客户端
public class NioClient {
public static void main(String[] args) throws IOException {
//创建一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置成非阻塞
socketChannel.configureBlocking(false);
//提供服务器的IP 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
}
}
//如果连接成功
String str="hello,My name is wangmengcheng!!";
//包含一个字节数组,直接返回数组大小的Buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//将缓冲区的数据写入通道
socketChannel.write(buffer);
System.in.read();
}
}
4.2、群聊系统Demo
需要实现客户端和服务器端之间的数据通讯,服务端能够将数据转发给其他所有客户端
public class GroupChatServer {
//监听通道
private ServerSocketChannel lsitenChannel;
//选择器
private Selector selector;
//端口
private static final int PORT=6667;
//初始化工作
public GroupChatServer(){
try {
lsitenChannel=ServerSocketChannel.open();
selector=Selector.open();
//绑定端口
lsitenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
lsitenChannel.configureBlocking(false);
//将listenChannel注册到selector
lsitenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
//监听
public void listen(){
try {
while (true){
int count = selector.select();
//有事件处理
if(count>0){
//直接得到SelectionKey集合的迭代器
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//监听到Accept事件
if(key.isAcceptable()){
SocketChannel sc = lsitenChannel.accept();
//设置非阻塞并注册到selector
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
//提示客户端上线
System.out.println(sc.getRemoteAddress()+"已上线...");
}
if(key.isReadable()){
//处理读,专门写方法
readData(key);
}
//清除当前的key,避免重复处理
iterator.remove();
}
}else {
System.out.println("等待连接...");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
//读取客户端的消息
private void readData(SelectionKey key) {
//定义一个SocketChannel
SocketChannel channel = null;
try {
//取到关联的Channel
channel= (SocketChannel) key.channel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将缓冲区的数据读取到通道
int read = channel.read(buffer);
//根据read判断是否读取到数据
if(read>0){
//把缓冲区的数据转化成字符串
String msg = new String(buffer.array());
//输出此消息
System.out.println("from 客户端:"+msg);
//向其他客户端转发此消息
sendInfoToOtherClients(msg,channel);
}
}catch (IOException e){
try {
System.out.println(channel.getRemoteAddress()+"离线了...");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
//将信息发送至其他客户端
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
System.out.println("服务器转发消息中...");
for(SelectionKey key:selector.keys()){
Channel targetChannel = key.channel();
if(targetChannel instanceof SocketChannel && targetChannel!=self){
SocketChannel dest = (SocketChannel) targetChannel;
//创建缓冲区
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将缓冲区的数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
public class GroupChatClient01 {
//定义相关的属性
private static final String HOST = "127.0.0.1"; //服务器的IP地址
private static final int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器,初始化操作
public GroupChatClient01() throws IOException {
selector=Selector.open();
//连接服务器
socketChannel=SocketChannel.open(new InetSocketAddress(HOST,PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到username
username=socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username+"is ok...");
}
//向服务器发送信息
public void sendInfo(String info){
info = username + "说:"+ info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器回复的消息
public void readInfo(){
try {
int readChannels = selector.select();
if(readChannels>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isReadable()){
//得到相关通道
SocketChannel channel = (SocketChannel) key.channel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
channel.read(buffer);
//把缓冲区的数据转换的字符串
java.lang.String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove();
}
}else {
//System.out.println("没有可用的通道...");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//启动客户端
GroupChatClient01 groupChatClient01 = new GroupChatClient01();
//启动一个线程用于读取服务器的消息
new Thread(()->{
while (true){
groupChatClient01.readInfo();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//主线程用于发送数据给服务端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
java.lang.String s = scanner.nextLine();
groupChatClient01.sendInfo(s);
}
}
}