1. UDP
特点:不建立连接;将数据源和目的封装成数据包中;
涉及类 DatagramSocket
与DatagramPacket
public class DatagramSocket extends Object
描述:此类表示用来发送和接收数据报包的套接字。在 DatagramSocket 上总是启用 UDP 广播发送。
- 构造方法
- public DatagramSocket():构造数据报套接字并将其绑定到本地主机上任何可用的端口。
- public DatagramSocket(
int port
):创建数据报套接字并将其绑定到本地主机上的指定端口。 - public DatagramSocket(
int port
,InetAddress laddr
):创建数据报套接字,将其绑定到指定的本地地址。 - public DatagramSocket(
SocketAddress bindaddr
):创建数据报套接字,将其绑定到指定的本地套接字地址。
- 常用方法
- public void
send
(DatagramPacket p):从此套接字 发送 数据报包。 - public void
receive
(DatagramPacket p):从此套接字 接收 数据报包。
- public void
public final class DatagramPacket extends Object
描述:此类表示数据报包。数据报包用来实现无连接包投递服务。
- 构造方法
- public DatagramPacket(
byte[] buf
,int length
):构造 DatagramPacket,用来 接收 长度为 length 的数据包。 - public DatagramPacket(
byte[] buf
,int length
,InetAddress address
,int port
):构造数据报包,用来将长度为 length 的包 发送 到指定主机上的指定端口号。 - public DatagramPacket(
byte[] buf
,int offset
,int length
,InetAddress address
,int port
):构造数据报包,用来将长度为 length 偏移量为 offset 的包 发送 到指定主机上的指定端口号。 - public DatagramPacket(
byte[] buf
,int length
,SocketAddress address
):构造数据报包,用来将长度为 length 的包 发送 到指定主机上的指定端口号。 - public DatagramPacket(
byte[] buf
,int offset
,int length
,SocketAddress address
):构造数据报包,用来将长度为 length 偏移量为 offset 的包 发送 到指定主机上的指定端口号。
- public DatagramPacket(
- 常用方法
- public InetAddress
getAddress()
:获取数据报将要发往该机器或者是从该机器接收的 InetAddress 地址(通常为 IP 地址)。 - public SocketAddress
getSocketAddress()
:获取要将此包发送到的或发出此数据报的远程主机的 SocketAddress(通常为 IP 地址 + 端口号)。 - public byte[]
getData()
:返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。 - public byte[]
getLength()
:返回数据缓冲区大小。
- public InetAddress
client发送端思路
- 建立udp的socket服务。
- 将要发送的数据封装成数据包。
- 通过udp的socket服务,将数据包发送出。
- 关闭资源。
client发送端代码
public class UdpClientDemo {
public static void main(String[] args) throws IOException {
// 1、创建发送端DatagramSocket对象
DatagramSocket ds = new DatagramSocket();
// 创建数据
byte[] bys = "hello,udp,我来了".getBytes();
// 长度
int length = bys.length;
// IP地址对象
InetAddress address = InetAddress.getByName("192.168.0.106");
// 端口
int port = 10086;
// 2、创建数据,并把数据打包
// DatagramPacket(byte[] buf, int length, InetAddress address, int port)
DatagramPacket dp = new DatagramPacket(bys, length, address, port);
// 3、调用Socket对象的发送方法发送数据包
// public void send(DatagramPacket p)
ds.send(dp);
// 释放资源
ds.close();
}
}
server接收端思路
- 建立udp的socket服务。
- 通过receive方法接收数据。
- 将收到的数据存储到数据包对象中。
- 通过数据包对象的功能来完成对接收到数据进行解析。
- 可以对资源进行关闭。
server接收端代码
public class UdpServerDemo {
public static void main(String[] args) throws IOException {
// 1、创建接收端DatagramSocket对象
DatagramSocket ds = new DatagramSocket(10086);
// 2、创建一个数据包(接收容器)
// DatagramPacket(byte[] buf, int length)
byte[] bys = new byte[1024];
int length = bys.length;
DatagramPacket dp = new DatagramPacket(bys, length);
// 3、调用Socket对象的接收方法接收数据
// public void receive(DatagramPacket p)
ds.receive(dp); // 阻塞式
// 4、解析数据包,并显示在控制台
// 获取对方的ip
// public InetAddress getAddress()
String ip = dp.getAddress().getHostAddress();
// public byte[] getData():获取数据缓冲区
// public int getLength():获取数据的实际长度
byte[] bys2 = dp.getData();
int len = dp.getLength();
String s = new String(bys2, 0, len);
System.out.println(ip + "传递的数据是:" + s);
// 释放资源
ds.close();
}
}
运行服务端后,ds.receive(dp)
该方法会一直阻塞等待数据。当客户端运行后,服务端打印 “192.168.0.106传递的数据是:hello,udp,我来了” 结束。
UDP案例
案例一
客户端:从键盘录入数据进行发送,如果输入的是886那么客户端就结束输入数据。
服务端:一直输出客户端发来的数据。
客户端-UdpClientDemo:
public class UdpClientDemo {
public static void main(String[] args) throws IOException {
// 创建发送端的Socket对象
DatagramSocket ds = new DatagramSocket();
// 封装键盘录入数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while ((line = br.readLine()) != null) {
if ("886".equals(line)) {
break;
}
// 创建数据并打包
byte[] bys = line.getBytes();
// DatagramPacket dp = new DatagramPacket(bys, bys.length,
// InetAddress.getByName("192.168.0.106"), 12345);
DatagramPacket dp = new DatagramPacket(bys, bys.length,
InetAddress.getByName("192.168.0.255"), 12345);
// 发送数据
ds.send(dp);
}
// 释放资源
ds.close();
}
}
服务端-UdpServerDemo:
public class UdpServerDemo {
public static void main(String[] args) throws IOException {
// 创建接收端的Socket对象
DatagramSocket ds = new DatagramSocket(12345);
while (true) {
// 创建一个包裹
byte[] bys = new byte[1024];
DatagramPacket dp = new DatagramPacket(bys, bys.length);
// 接收数据
ds.receive(dp);
// 解析数据
String ip = dp.getAddress().getHostAddress();
String s = new String(dp.getData(), 0, dp.getLength());
System.out.println("from " + ip + " data is : " + s);
}
// 释放资源
// 接收端应该一直开着等待接收数据,是不需要关闭
// ds.close();
}
}
上述 DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("192.168.0.255"), 12345);
中,192.168.0.255为广播地址,该网段所有主机均可接收。
案例二
发送和接收程序分别用线程进行封装,完成一个UDP的聊天程序。
客户端-UdpClientDemo:
public class UdpClientDemo implements Runnable{
public DatagramSocket ds;
public UdpClientDemo(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
// 封装键盘录入数据
BufferedReader br = new BufferedReader(new InputStreamReader(
System.in));
String line = null;
while ((line = br.readLine()) != null) {
if ("886".equals(line)) {
break;
}
// 创建数据并打包
byte[] bys = line.getBytes();
DatagramPacket dp = new DatagramPacket(bys, bys.length,
InetAddress.getByName("192.168.0.255"), 12306);
// 发送数据
ds.send(dp);
}
// 释放资源
ds.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端-UdpServerDemo:
public class UdpServerDemo implements Runnable {
public DatagramSocket ds;
public UdpServerDemo(DatagramSocket ds) {
this.ds = ds;
}
@Override
public void run() {
try {
while (true) {
// 创建一个包裹
byte[] bys = new byte[1024];
DatagramPacket dp = new DatagramPacket(bys, bys.length);
// 接收数据
ds.receive(dp);
// 解析数据
String ip = dp.getAddress().getHostAddress();
String s = new String(dp.getData(), 0, dp.getLength());
System.out.println("from " + ip + " data is : " + s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
聊天室:
public class ChatRoom {
public static void main(String[] args) throws IOException {
DatagramSocket dsSend = new DatagramSocket();
DatagramSocket dsReceive = new DatagramSocket(12306);
UdpClientDemo client = new UdpClientDemo(dsSend);
UdpServerDemo server = new UdpServerDemo(dsReceive);
Thread t1 = new Thread(client);
Thread t2 = new Thread(server);
t1.start();
t2.start();
}
}
2. TCP
特点:建立连接;形成传输数据的通道,在连接中进行大数据量传输;
涉及类Socket
和ServerSocket
public class Socket extends Object
描述:此类实现客户端套接字。
- 构造函数
- public Socket(
String host
,int port
):创建一个流套接字并将其连接到指定主机上的指定端口号。 - public Socket(
InetAddress address
,int port
):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 - public Socket(
String host
,int port
,InetAddress localAddr
,int localPort
):创建一个套接字并将其连接到指定远程主机上的指定远程端口。- host - 远程主机名,或者为 null,表示回送地址。
- port - 远程端口
- localAddr - 要将套接字绑定到的本地地址
- localPort - 要将套接字绑定到的本地端口
- public Socket(
InetAddress address
,int port
,InetAddress localAddr
,int localPort
):创建一个套接字并将其连接到指定远程地址上的指定远程端口。
- public Socket(
- 常用方法
- public void
connect
(SocketAddress endpoint):将此套接字连接到服务器。 - public SocketChannel
getChannel()
:返回与此数据报套接字关联的唯一 SocketChannel 对象(如果有)。 - public InputStream
getInputStream()
:返回此套接字的输入流。 - public OutputStream
getOutputStream()
:返回此套接字的输出流。 - public void
close()
:关闭此套接字。
- public void
public class ServerSocket extends Object
描述:此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
- 构造函数
- public ServerSocket(
int port
):创建绑定到特定端口的服务器套接字。 - public ServerSocket(
int port
,int backlog
,InetAddress bindAddr)
:使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。
- public ServerSocket(
- 常用方法
- public Socket
accept()
:侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。 - public void
close()
:关闭此套接字。 - public ServerSocketChannel
getChannel()
:返回与此套接字关联的唯一 ServerSocketChannel 对象(如果有)。
- public Socket
client发送端思路
- 建立客户端的socket服务,并明确要连接的服务器。
- 如果连接建立成功,就表明已经建立了数据传输的通道。就可以在该通道通过IO进行数据的读取和写入。该通道称为Socket流,Socket流中既有读取流,也有写入流。
- 通过Socket对象的方法,获取读取流、写入流。
- 通过流对象可以对数据进行传输。
- 如果传输数据完毕,关闭资源。
client发送端代码
public class TcpClientDemo {
public static void main(String[] args) throws IOException {
// 1、创建客户端Socket对象
Socket s = new Socket("192.168.0.106", 22222);
// 键盘录入数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 2、包装通道内的输出流
PrintWriter printWriter = new PrintWriter(s.getOutputStream(), true);
String line = null;
while ((line = br.readLine()) != null) {
// 键盘录入数据要自定义结束标记
if ("886".equals(line)) {
break;
}
// 3、往通道内写入数据
printWriter.println(line);
// 4、包装通道内的输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 5、读取通道内数据
String readLine = bufferedReader.readLine();
System.out.println("接收数据:" + readLine);
}
// 释放资源
// bw.close();
// br.close();
s.close();
}
}
server接收端思路
- 建立服务器端的socket服务,绑定一个端口。
- 通过
accept()
方法获取客户端对象,再通过获取到的客户端对象的流和客户端进行通信。 - 通过客户端的获取流对象的方法,读取数据或者写入数据。
- 如果服务完成,需要关闭客户端,然后关闭服务器。但是,一般会关闭客户端,不会关闭服务器,因为服务端是一直提供服务的。
server接收端代码
public class TcpServerDemo {
public static void main(String[] args) throws IOException {
// 1、创建服务器Socket对象
ServerSocket ss = new ServerSocket(22222);
// 2、监听客户端连接
Socket s = ss.accept();
// 3、包装通道内容的输入流
BufferedReader br = new BufferedReader(new InputStreamReader(
s.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(s.getInetAddress().getHostAddress() + "---" + line);
// 4、包装通道内的输出流
PrintWriter printWriter = new PrintWriter(s.getOutputStream(), true);
// 5、往通道内写入数据
printWriter.println("服务器响应数据" + line);
}
// br.close();
s.close();
// ss.close();
}
}
先启动服务端,再启动客户端。
111
接收数据:服务器响应数据111
22
接收数据:服务器响应数据22
886
TCP案例
上传文件
UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
// 1、创建客户端Socket对象
Socket s = new Socket("192.168.0.106", 11111);
// 2、封装文本文件
BufferedReader br = new BufferedReader(new FileReader(
"E:\\demo\\1.txt"));
// 3、封装通道内流
PrintWriter printWriter = new PrintWriter(s.getOutputStream(),true);
String line = null;
while( (line = br.readLine()) != null){
printWriter.println(line);
}
s.close();
br.close();
}
}
UploadServer:
public class UploadServer {
public static void main(String[] args) throws IOException {
// 1、创建服务器端的Socket对象
ServerSocket ss = new ServerSocket(11111);
// 2、监听客户端连接
Socket s = ss.accept();
// 3、封装通道内的流
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 4、封装文本文件
PrintWriter printWriter = new PrintWriter("E:\\demo\\download.java");
String line = null;
while((line = br.readLine()) != null){
printWriter.println(line);
}
s.close();
printWriter.close();
}
}
通过Socket通讯,将文件远程下载到本地。
上传图片
图片属于二进制文件,需要使用字节流。
UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
// 创建客户端Socket对象
Socket s = new Socket("192.168.0.106", 11111);
// 封装文本文件
BufferedInputStream br = new BufferedInputStream(new FileInputStream(
"E:\\demo\\林青霞.jpg"));
// 封装通道内流
BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
byte[] bytes = new byte[1024];
int len = 0;
while( (len = br.read(bytes)) != -1){
bos.write(bytes);
bos.flush();
}
s.close();
br.close();
}
}
UploadServer:
public class UploadServer {
public static void main(String[] args) throws IOException {
// 创建服务器端的Socket对象
ServerSocket ss = new ServerSocket(11111);
// 监听客户端连接
Socket s = ss.accept();
// 封装通道内的流
BufferedInputStream br = new BufferedInputStream(s.getInputStream());
// 封装文本文件
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\demo\\mn.jpg"));
byte[] bytes = new byte[1024];
int len = 0;
while((len = br.read(bytes)) != -1){
bos.write(bytes);
}
s.close();
bos.close();
}
}
下载图片并完成通知
服务器接收图片完成后,通知客户端上传成功。
UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
// 1、创建客户端Socket对象
Socket s = new Socket("192.168.0.106", 11111);
// 2、封装文本文件
BufferedInputStream br = new BufferedInputStream(new FileInputStream(
"E:\\demo\\林青霞.jpg"));
// 3、封装通道内流
BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
byte[] bytes = new byte[1024];
int len = 0;
while( (len = br.read(bytes)) != -1){
bos.write(bytes);
bos.flush();
}
// 4、接收反馈
BufferedReader brClient = new BufferedReader(new InputStreamReader(
s.getInputStream()));
String client = brClient.readLine(); // 阻塞
System.out.println(client);
s.close();
br.close();
}
}
UploadServer:
public class UploadServer {
public static void main(String[] args) throws IOException {
// 1、创建服务器端的Socket对象
ServerSocket ss = new ServerSocket(11111);
// 2、监听客户端连接
Socket s = ss.accept();
// 3、封装通道内的流
BufferedInputStream br = new BufferedInputStream(s.getInputStream());
// 4、封装文本文件
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:\\demo\\mn.jpg"));
byte[] bytes = new byte[1024];
int len = 0;
while((len = br.read(bytes)) != -1){ // 阻塞
bos.write(bytes);
bos.flush();
}
// 5、给出反馈
BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(
s.getOutputStream()));
bwServer.write("图片上传成功");
bwServer.newLine();
bwServer.flush();
s.close();
bos.close();
}
}
服务器增加反馈,并往通道写入数据;客户端从通道读取数据,并打印。
运行发现,E:\demo\mn.jpg图片复制成功,但是服务端、客户端一直阻塞,且没有任何数据打印。
为什么呢?
我们加入打印语句
UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
...
while( (len = br.read(bytes)) != -1){
bos.write(bytes);
bos.flush();
}
System.out.println("client 111");
// 接收反馈
BufferedReader brClient = new BufferedReader(new InputStreamReader(
s.getInputStream()));
String client = brClient.readLine(); // 阻塞
System.out.println(client);
System.out.println("client 222");
s.close();
br.close();
}
}
UploadServer:
public class UploadServer {
public static void main(String[] args) throws IOException {
...
while((len = br.read(bytes)) != -1){ // 阻塞
bos.write(bytes);
bos.flush();
System.out.println("server 111");
}
System.out.println("server 222");
...
}
}
运行结果:
UploadClient:
client 111
UploadServer:
server 111
server 111
server 111
....
server 111
运行结果发现,客户端发送数据完成后并没有从通道内获取到反馈的数据,一直阻塞等待。服务器从通道内while循环读取数据并没有退出循环,所以没有打印“server 222”。
读取文本文件是可以以null作为结束信息的,但是,通道内是不能这样结束信息的。所以,服务器不知道客户端结束,就会一直read阻塞等待客户端发送数据,不会执行后续的写入操作。而客户端一直read阻塞等待服务器反馈,不会执行关闭操作,形成了死锁等待。
Socket对象提供了一种解决方案:
- public void
shutdownOutput()
:关闭输出流。 - public void
shutdownInput()
:关闭输入流。
改进后:
UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
...
while( (len = br.read(bytes)) != -1){
bos.write(bytes);
bos.flush();
System.out.println("len="+len);
}
//Socket提供了一个终止,它会通知服务器你别等了,我没有数据过来了。
s.shutdownOutput();
// 接收反馈
BufferedReader brClient = new BufferedReader(new InputStreamReader(
s.getInputStream()));
String client = brClient.readLine();
System.out.println(client);
s.close();
br.close();
}
}
UploadServer:
不需要改变
图片复制成功,并且客户端收到反馈 “图片上传成功”。
多线程下载服务器图片
思路:
服务端循环接收请求,如果收到请求,启动一个线程处理。
服务器-UploadServer:
public class UploadServer {
public static void main(String[] args) throws IOException {
// 创建服务器Socket对象
ServerSocket ss = new ServerSocket(11111);
while (true) {
Socket s = ss.accept();
new Thread(new UserThread(s)).start();
}
}
}
处理线程:
public class UserThread implements Runnable {
private Socket s;
public UserThread(Socket s) {
this.s = s;
}
@Override
public void run() {
try {
// 封装通道内的流
BufferedReader br = new BufferedReader(new InputStreamReader(
s.getInputStream()));
// 封装文本文件
// BufferedWriter bw = new BufferedWriter(new
// FileWriter("Copy.java"));
// 为了防止名称冲突
String newName = System.currentTimeMillis() + ".java";
BufferedWriter bw = new BufferedWriter(new FileWriter(newName));
String line = null;
while ((line = br.readLine()) != null) { // 阻塞
bw.write(line);
bw.newLine();
bw.flush();
}
// 给出反馈
BufferedWriter bwServer = new BufferedWriter(
new OutputStreamWriter(s.getOutputStream()));
bwServer.write("文件上传成功");
bwServer.newLine();
bwServer.flush();
// 释放资源
bw.close();
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端连接服务器,读取文件上传,完成后通知服务器。
客户端-UploadClient:
public class UploadClient {
public static void main(String[] args) throws IOException {
// 创建客户端Socket对象
Socket s = new Socket("192.168.0.106", 11111);
// 封装文本文件
// BufferedReader br = new BufferedReader(new FileReader(
// "InetAddressDemo.java"));
BufferedReader br = new BufferedReader(new FileReader(
"ReceiveDemo.java"));
// 封装通道内流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
s.getOutputStream()));
String line = null;
while ((line = br.readLine()) != null) { // 阻塞
bw.write(line);
bw.newLine();
bw.flush();
}
// Socket提供了一个终止,它会通知服务器你别等了,我没有数据过来了
s.shutdownOutput();
// 接收反馈
BufferedReader brClient = new BufferedReader(new InputStreamReader(
s.getInputStream()));
String client = brClient.readLine(); // 阻塞
System.out.println(client);
// 释放资源
br.close();
s.close();
}
}
3. BIO/NIO/AIO
- BIO 编程:
Blocking IO: 同步阻塞的编程方式。
BIO编程方式通常是在 JDK1.4 版本之前常用的编程方式。编程实现过程为:首先在服务端启动一个 ServerSocket 来监听网络请求,客户端启动 Socket 发起网络请求,默认情况下 ServerSocket 会建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。
同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池
机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO 编程:
NIO 编程:Unblocking IO(New IO): 同步非阻塞的编程方式。
NIO 本身是基于事件驱动
思想来完成的,其主要想解决的是 BIO 的大并发问题,NIO基于Reactor
,当 socket 有流可读或可写入 socket 时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理
,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有 BIO 一样的问题。
- AIO编程:
Asynchronous IO: 异步非阻塞的编程方式。
AIO 基于Proactor
,与 NIO 不同,当进行读写操作时,只须直接调用 API 的read()或write()方法即可
。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel
、AsynchronousServerSocketChannel
、AsynchronousFileChannel
、AsynchronousDatagramChannel
。