网络编程和NIO
一、 网络编程入门
1.软件结构
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机在网络中的通信的程序。
2.网络编程三要素
协议
网络通信协议: 通信协议是计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
java.net 包中提供了两种常见的网络协议的支持:
- TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
- TCP协议特点: 面向连接,传输数据安全,传输速度低
- 例如: 村长发现张三家的牛丢了
- TCP协议: 村长一定要找到张三,面对面的告诉他他家的牛丢了 打电话: 电话一定要接通,并且是张三接的
- 连接三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。 你愁啥?
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我愁你咋地?
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。你再愁试试
? 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
- UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中**,例如视频会议、QQ聊天等。**
- UDP特点: 面向无连接,传输数据不安全,传输速度快
- 例如: 村长发现张三家的牛丢了
- UDP协议: 村长在村里的广播站广播一下张三家的牛丢了,信息丢失,信息发布速度快
IP地址
- IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
** IP地址分类 **
-
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。 -
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
常用命令
ipconfig
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com
特殊的IP地址
- 本机IP地址:
127.0.0.1 、localhost 。
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
- **端口号:用两个字节表示的整数,它的取值范围是065535**。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
利用协议 +IP地址 +端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
3.InetAddress类
InetAddress类的概述
InetAddress类的方法
static InetAddress getLocalHost() | 获得本地主机IP地址对象 |
---|
static InetAddress getByName(String host) | 根据IP地址字符串或主机名获得对应的IP地址对象 | String getHostName() | 获得主机名 | String getHostAddress() | 获得IP地址字符串 |
二、 TCP通信程序
1.TCP协议概述
TCP概述
- TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收器端建立逻辑连接,然后再传输数据。它提供了两台计算机之间可靠无差错的数据传输。TCP通信过程如下图所示:
TCP协议相关的类
- Socket : 一个该类的对象就代表一个客户端程序。
- Socket(String host, int port) 根据ip地址字符串和端口号创建客户端Socket对象
**注意事项:**只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。 如果连接成功,则表示三次握手通过。 OutputStream getOutputStream(); 获得字节输出流对象InputStream getInputStream(); 获得字节输入流对象void close(); 关闭Socket, 会自动关闭相关的流,关闭通过Socket获得流,也会关闭Socket - ServerSocket : 一个该类的对象就代表一个服务器端程序。
ServerSocket(int port); 根据指定的端口号开启服务器。Socket accept(); 等待客户端连接并获得与客户端关联的Socket对象 如果没有客户端连接,该方法会一直阻塞void close(); 关闭ServerSocket,一般不关闭
TCP通信案例
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1",6666);
while (true) {
OutputStream os = socket.getOutputStream();
Scanner sc = new Scanner(System.in);
System.out.println("请输入向服务器发送的数据:");
String str = sc.nextLine();
os.write(str.getBytes());
InputStream is = socket.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
System.out.println(new String(bys,0,len));
}
}
}
public class Server {
public static void main(String[] args) throws Exception{
ServerSocket ss = new ServerSocket(6666);
Socket socket = ss.accept();
while (true) {
InputStream is = socket.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
System.out.println(new String(bys,0,len));
OutputStream os = socket.getOutputStream();
Scanner sc = new Scanner(System.in);
System.out.println("请输入向客户端发送的数据:");
String str = sc.nextLine();
os.write(str.getBytes());
}
}
}
文件上传案例
使用TCP协议, 通过客户端向服务器上传一个文件
流程:
-
【客户端】输入流,从硬盘读取文件数据到程序中。 -
【客户端】输出流,写出文件数据到服务端。 -
【服务端】输入流,读取文件数据到服务端程序。 -
【服务端】输出流,写出文件数据到服务器硬盘中。 -
【服务端】获取输出流,回写数据。 -
【客户端】获取输入流,解析回写数据。
拷贝文件
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8888);
while (true) {
Socket socket = ss.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream("day12\\aaa\\"+System.currentTimeMillis()+".jpg");
byte[] bys = new byte[8192];
int len;
while ((len = is.read(bys)) != -1) {
fos.write(bys, 0, len);
}
System.out.println("======服务器开始回写数据给客户端=======");
OutputStream os = socket.getOutputStream();
os.write("恭喜您,上传成功!".getBytes());
fos.close();
socket.close();
} catch (IOException e) {
}
}
}).start();
}
}
}
public class Client {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("aaa\\hb.jpg");
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
OutputStream os = socket.getOutputStream();
byte[] bys = new byte[8192];
int len;
while ((len = fis.read(bys)) != -1) {
os.write(bys, 0, len);
}
socket.shutdownOutput();
System.out.println("============客户端开始接受服务器返回的数据==============");
InputStream is = socket.getInputStream();
int read = is.read(bys);
System.out.println("服务器回写的数据是:" + new String(bys, 0, read));
socket.close();
fis.close();
}
}
三、 NIO
NIO概述
- 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
- 同步: 调用方法之后,必须要得到一个返回值 例如: 买火车票,一定要买到票,才能继续下一步
- 异步: 调用方法之后,没有返回值,但是会有回调函数,回调函数指的是满足条件之后会自动执行的方法 例如: 买火车票, 不一定要买到票,我可以交代售票员,当有票的话,你就帮我出张票
- 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理
- 阻塞:如果没有达到方法的目的,就会一直停在那里(等待) , 例如: ServerSocket的accept()方法
- 非阻塞: 不管方法有没有达到目的,都直接往下执行(不等待)
在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,每一个操作在一步中产生或者消费一个数据,按块处理要比按字节处理数据快的多。
在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程
首先,我们要先了解一下NIO的三个主要组成部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器)
四、(缓冲区)
1.Buffer的概述和分类
概述:Buffer是一个抽象类,它是对某种基本类型的数组进行了封装。
作用: 在NIO中,就是通过 Buffer 来读写数据的。所有的数据都是用Buffer来处理的,它是NIO读写数据的中转池, 通常使用字节数组。
Buffer主要有如下几种:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
2.创建ByteBuffer
-
ByteBuffer类内部封装了一个byte[]数组,并可以通过一些方法对这个数组进行操作。 -
创建ByteBuffer对象
-
方式一:在堆中创建缓冲区:allocate(int capacity) public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
}
-
方式二: 在系统内存创建缓冲区:allocatDirect(int capacity) public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
}
-
在堆中创建缓冲区称为:间接缓冲区 -
在系统内存创建缓冲区称为:直接缓冲区 -
间接缓冲区的创建和销毁效率要高于直接缓冲区 -
间接缓冲区的工作效率要低于直接缓冲区 -
方式三:通过数组创建缓冲区:wrap(byte[] arr) public static void main(String[] args) {
byte[] byteArray = new byte[10];
ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
}
3.添加数据-put
-
public ByteBuffer put(byte b):向当前可用位置添加数据。 -
public ByteBuffer put(byte[] byteArray):向当前可用位置添加一个byte[]数组 -
public ByteBuffer put(byte[] byteArray,int offset,int len):添加一个byte[]数组的一部分
4.容量-capacity
-
Buffer的容量(capacity)是指:Buffer所能够包含的元素的最大数量。定义了Buffer后,容量是不可变的。 -
示例代码: public static void main(String[] args) {
ByteBuffer b1 = ByteBuffer.allocate(10);
System.out.println("容量:" + b1.capacity());
byte[] byteArray = {97, 98, 99, 100};
ByteBuffer b2 = ByteBuffer.wrap(byteArray);
System.out.println("容量:" + b2.capacity());
}
5.限制-limit
-
限制limit是指:第一个不应该读取或写入元素的index索引。缓冲区的限制(limit)不能为负,并且不能大于容量。 -
有两个相关方法:
- public int limit():获取此缓冲区的限制。
- public Buffer limit(int newLimit):设置此缓冲区的限制。
-
示例代码: public class Test_添加数据 {
public static void main(String[] args) {
ByteBuffer b1 = ByteBuffer.allocate(10);
b1.put((byte)10);
int limit1 = b1.limit();
System.out.println("limit1:"+limit1);
b1.limit(3);
b1.put((byte)20);
b1.put((byte)30);
}
}
图示:
6.位置-position
-
位置position是指:当前可写入的索引。位置不能小于0,并且不能大于"限制"。 -
有两个相关方法:
- public int position():获取当前可写入位置索引。
- public Buffer position(int p):更改当前可写入位置索引。
-
示例代码: public class Test_添加数据 {
public static void main(String[] args) {
ByteBuffer b1 = ByteBuffer.allocate(10);
b1.put((byte)11);
int position = b1.position();
System.out.println("position:"+position);
b1.position(5);
b1.put((byte)22);
b1.put((byte)33);
System.out.println("position:"+b1.position());
System.out.println(Arrays.toString(b1.array()));
}
}
小结
7.标记-mark
-
标记mark是指:当调用缓冲区的reset()方法时,会将缓冲区的position位置重置为该索引。 -
相关方法:
- public Buffer mark():设置此缓冲区的标记为当前的position位置。
- public Buffer reset() : 将此缓冲区的位置重置为以前标记的位置。
-
示例代码: public static void main(String[] args) {
ByteBuffer b1 = ByteBuffer.allocate(10);
b1.put((byte)11);
int position = b1.position();
System.out.println("position:"+position);
b1.mark();
b1.put((byte)22);
b1.put((byte)33);
System.out.println("position:"+b1.position());
System.out.println(Arrays.toString(b1.array()));
b1.reset();
System.out.println("position:"+b1.position());
b1.put((byte)44);
System.out.println(Arrays.toString(b1.array()));
}
8.其它方法
- public int remaining():获取position与limit之间的元素数。
- public boolean isReadOnly():获取当前缓冲区是否只读。
- public boolean isDirect():获取当前缓冲区是否为直接缓冲区。
- public Buffer rewind():重绕此缓冲区。
- 将position位置设置为:0
- 限制limit不变。
- 丢弃标记。
- public Buffer clear():还原缓冲区的状态。
- 将position设置为:0
- 将限制limit设置为容量capacity;
- 丢弃标记mark。
- public Buffer flip():缩小limit的范围。
- 将limit设置为当前position位置;
- 将当前position位置设置为0;
- 丢弃标记。
package com.itheima.demo11_ByteBuffer常用方法;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class Test6_clear和flip {
public static void main(String[] args) {
ByteBuffer b = ByteBuffer.allocate(10);
System.out.println("容量:" + b.capacity() + ",限制:" + b.limit() + ",位置:" + b.position());
b.put((byte) 10);
b.put((byte) 20);
b.put((byte) 30);
System.out.println("容量:" + b.capacity() + ",限制:" + b.limit() + ",位置:" + b.position());
System.out.println(Arrays.toString(b.array()));
b.flip();
System.out.println("容量:"+b.capacity()+",限制:"+b.limit()+",位置:"+b.position());
System.out.println(Arrays.toString(b.array()));
}
}
五、 Channel(通道)
1.Channel概述
Channel 的概述
Channel(通道):Channel是一个接口,可以通过它读取和写入数据, 可以把它看做是IO中的流,不同的是:Channel是双向的, Channel对象既可以调用读取的方法, 也可以调用写出的方法 。
输入流: 读
输出流: 写
Channel: 读,写
Channel 的分类
在Java NIO中的Channel主要有如下几种类型:
- FileChannel:从文件读取数据的 输入流和输出流
- DatagramChannel:读写UDP网络协议数据 Datagram
- SocketChannel:读写TCP网络协议数据 Socket
- ServerSocketChannel:可以监听TCP连接 ServerSocket
2.FileChannel类的基本使用
获取FileChannel类的对象
-
java.nio.channels.FileChannel (抽象类):用于读、写文件的通道。 -
FileChannel是抽象类,我们可以通过FileInputStream和FileOutputStream的getChannel()方法方便的获取一个它的子类对象。 FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
使用FileChannel类完成文件的复制
- 我们将通过CopyFile这个示例让大家体会NIO的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("day19\\aaa\\a.txt");
FileOutputStream fos = new FileOutputStream("day19\\aaa\\aCopy1.txt");
FileChannel c1 = fis.getChannel();
FileChannel c2 = fos.getChannel();
ByteBuffer b = ByteBuffer.allocate(1000);
while ((c1.read(b)) != -1){
b.flip();
c2.write(b);
b.clear();
}
c2.close();
c1.close();
fos.close();
fis.close();
}
3.FileChannel结合MappedByteBuffer实现高效读写
MappedByteBuffer类的概述
-
上例直接使用FileChannel结合ByteBuffer实现的管道读写,但并不能提高文件的读写效率。 -
ByteBuffer有个抽象子类:MappedByteBuffer,它可以将文件直接映射至内存,把硬盘中的读写变成内存中的读写, 所以可以提高大文件的读写效率。 -
可以调用FileChannel的map()方法获取一个MappedByteBuffer,map()方法的原型: ? MappedByteBuffer map(MapMode mode, long position, long size); ? 说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。
复制2GB以下的文件
- 复制d:\b.rar文件,此文件大概600多兆,复制完毕用时不到2秒。此例不能复制大于2G的文件,因为map的第三个参数被限制在Integer.MAX_VALUE(字节) = 2G。
public static void main(String[] args) throws Exception{
RandomAccessFile r1 = new RandomAccessFile("day19\\aaa\\a.txt","r");
RandomAccessFile r2 = new RandomAccessFile("day19\\aaa\\aCopy2.txt","rw");
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel();
long size = c1.size();
MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
for (long i = 0; i < size; i++) {
byte b = b1.get();
b2.put(b);
}
c2.close();
c1.close();
r2.close();
r1.close();
}
-
代码说明: -
map()方法的第一个参数mode:映射的三种模式,在这三种模式下得到的将是三种不同的MappedByteBuffer:三种模式都是Channel的内部类MapMode中定义的静态常量,这里以FileChannel举例: 1). FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用get之类的读取Buffer中的内容); 2). FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点; 3). FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个(position, size)区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的ByteBuffer了!! -
为什么使用RandomAccessFile? 1). 使用InputStream获得的Channel可以映射,使用map时只能指定为READ_ONLY模式,不能指定为READ_WRITE和PRIVATE,否则会抛出运行时异常! 2). 使用OutputStream得到的Channel不可以映射!并且OutputStream的Channel也只能write不能read! 3). 只有RandomAccessFile获取的Channel才能开启任意的这三种模式!
复制2GB以上的文件
- 下例使用循环,将文件分块,可以高效的复制大于2G的文件
public static void main(String[] args) throws Exception{
RandomAccessFile r1 = new RandomAccessFile("H:\\课堂资料.zip","r");
RandomAccessFile r2 = new RandomAccessFile("H:\\课堂资料2.zip","rw");
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel();
long size = c1.size();
int everySize = 1024*1024*500;
long count = size % everySize == 0 ? size/everySize : size/everySize+1;
for (long i = 0; i < count; i++) {
long start = everySize*i;
long trueSize = size - start > everySize ? everySize : size - start;
MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);
for (long j = 0; j < trueSize; j++) {
byte b = b1.get();
b2.put(b);
}
}
c2.close();
c1.close();
r2.close();
r1.close();
}
4.ServerSocketChannel和SocketChannel创建连接
SocketChannel创建连接
-
客户端:SocketChannel类用于连接的客户端,它相当于:Socket。 1). 先调用SocketChannel的open()方法打开通道: SocketChannel socket = SocketChannel.open()
2). 调用SocketChannel的实例方法connect(SocketAddress add)连接服务器: socket.connect(new InetSocketAddress("localhost", 8888));
示例:客户端连接服务器: public class Client {
public static void main(String[] args) throws Exception {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("localhost", 8888));
System.out.println("后续代码......");
}
}
ServerSocketChanne创建连接
-
服务器端:ServerSocketChannel类用于连接的服务器端,它相当于:ServerSocket。 -
调用ServerSocketChannel的静态方法open()就可以获得ServerSocketChannel对象, 但并没有指定端口号, 必须通过其套接字的bind方法将其绑定到特定地址,才能接受连接。 ServerSocketChannel serverChannel = ServerSocketChannel.open()
-
调用ServerSocketChannel的实例方法bind(SocketAddress add):绑定本机监听端口,准备接受连接。 ? 注:java.net.SocketAddress(抽象类):代表一个Socket地址。 ? 我们可以使用它的子类:java.net.InetSocketAddress(类) ? 构造方法:InetSocketAddress(int port):指定本机监听端口。 serverChannel.bind(new InetSocketAddress(8888));
-
调用ServerSocketChannel的实例方法accept():等待连接。 SocketChannel accept = serverChannel.accept();
System.out.println("后续代码...");
示例:服务器端等待连接(默认-阻塞模式) public class Server {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("【服务器】等待客户端连接...");
SocketChannel accept = serverChannel.accept();
System.out.println("后续代码......");
}
}
运行后结果: 【服务器】等待客户端连接...
-
我们可以通过ServerSocketChannel的configureBlocking(boolean b)方法设置accept()是否阻塞 public class Server {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8888));
System.out.println("【服务器】等待客户端连接...");
ssc.configureBlocking(false);
while(true) {
SocketChannel sc = ssc.accept();
if(sc != null){
System.out.println("连接上了。。");
break;
}else{
System.out.println("打会儿游戏~");
Thread.sleep(2000);
}
}
}
}
运行后结果: 【服务器】等待客户端连接...
打会儿游戏~
有客户端来了就输出: 连接上了。。
5.NIO网络编程收发信息
书写服务器代码
public class Server {
public static void main(String[] args) throws IOException{
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8888));
SocketChannel sc = ssc.accept();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = sc.read(buffer);
System.out.println(new String(buffer.array(),0,len));
sc.close();
}
}
书写客户端代码
public class Client {
public static void main(String[] args) {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1",8888));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("你好啊~".getBytes());
buffer.flip();
sc.write(buffer);
sc.close();
}
}
六、 Selector(选择器)
1.多路复用的概念
选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。
"多路"是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。
小结
- 多路复用的意思就是一个Selector可以监听多个服务器端口。
2.选择器Selector的获取和注册
Selector选择器的概述和作用
概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上, 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了, 减少系统负担, 提高效率。因为线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 .
Selector选择器的获取
Selector selector = Selector.open();
注册Channel到Selector
通过调用 channel.register(Selector sel, int ops)方法来实现注册:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:
-
连接就绪–常量:SelectionKey.OP_CONNECT -
接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项) -
读就绪–常量:SelectionKey.OP_READ -
写就绪–常量:SelectionKey.OP_WRITE 注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
-
案例演示; 监听一个通道 public class Test1 {
public static void main(String[] args) throws Exception{
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ssc1.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
}
}
-
示例:服务器创建3个通道,同时监听3个端口,并将3个通道注册到一个选择器中
public class Test2 {
public static void main(String[] args) throws Exception{
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector,SelectionKey.OP_ACCEPT);
ssc3.register(selector,SelectionKey.OP_ACCEPT);
}
}
接下来,就可以通过选择器selector操作三个通道了。
3.Selector的常用方法
Selector的select()方法:
-
作用: 服务器等待客户端连接的方法 -
阻塞问题:
- 在连接到第一个客户端之前,会一直阻塞
- 当连接到客户端后,如果客户端没有被处理,该方法会计入不阻塞状态
- 当连接到客户端后,如果客户端有被处理,该方法又会进入阻塞状态
public class Server1 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println(1);
selector.select();
System.out.println(2);
Set<SelectionKey> keySet = selector.selectedKeys();
for (SelectionKey key : keySet) {
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
System.out.println("...开始处理,接受数据,代码省略...");
}
}
}
}
Selector的selectedKeys()方法
-
获取已连接的所有通道集合 public class Server2 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
Set<SelectionKey> keySet = selector.selectedKeys();
System.out.println("被连接的服务器对象有多少个:"+keySet.size());
while (true) {
System.out.println(1);
selector.select();
System.out.println(2);
System.out.println("被连接的服务器对象个数:"+keySet.size());
}
}
}
Selector的keys()方法
-
获取已注册的所有通道集合 public class Server3 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
Set<SelectionKey> keySet = selector.selectedKeys();
System.out.println("被连接的服务器对象有多少个:"+keySet.size());
Set<SelectionKey> keys = selector.keys();
System.out.println("被注册的服务器对象有多少个:"+keys.size());
while (true) {
System.out.println(1);
selector.select();
System.out.println(2);
System.out.println("被连接的服务器对象个数:"+keySet.size());
System.out.println("被注册的服务器对象个数:"+keys.size());
}
}
}
Selector多路复用
需求
- 使用Selector进行多路复用,监听3个服务器端口
分析
- 创建3个服务器通道,设置成非阻塞
- 获取Selector选择器
- 把Selector注册到三个服务器通道上
- 循环去等待客户端连接
- 遍历所有被连接的服务器通道集合
- 处理客户端请求
实现
-
案例: public class Server1 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println(1);
selector.select();
Set<SelectionKey> keySet = selector.selectedKeys();
for (SelectionKey key : keySet) {
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
ByteBuffer b = ByteBuffer.allocate(1024);
int len = sc.read(b);
System.out.println(new String(b.array(), 0, len));
sc.close();
}
}
}
}
-
问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,导致在遍历集合时,遍历到已经没用的对象,出现了异常 -
解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历 -
代码如下: public class Server2 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc1 = ServerSocketChannel.open();
ssc1.bind(new InetSocketAddress(7777));
ServerSocketChannel ssc2 = ServerSocketChannel.open();
ssc2.bind(new InetSocketAddress(8888));
ServerSocketChannel ssc3 = ServerSocketChannel.open();
ssc3.bind(new InetSocketAddress(9999));
ssc1.configureBlocking(false);
ssc2.configureBlocking(false);
ssc3.configureBlocking(false);
Selector selector = Selector.open();
ssc1.register(selector, SelectionKey.OP_ACCEPT);
ssc2.register(selector, SelectionKey.OP_ACCEPT);
ssc3.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println(1);
selector.select();
Set<SelectionKey> keySet = selector.selectedKeys();
Iterator<SelectionKey> it = keySet.iterator();
while (it.hasNext()){
SelectionKey key = it.next();
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
ByteBuffer b = ByteBuffer.allocate(1024);
int len = sc.read(b);
System.out.println(new String(b.array(), 0, len));
sc.close();
it.remove();
}
}
}
}
七、 NIO2-AIO(异步、非阻塞)
知识点–AIO概述
同步,异步,阻塞,非阻塞概念回顾
- 同步:调用方法之后,必须要得到一个返回值。
- 异步:调用方法之后,没有返回值,但是会有回调函数。回调函数指的是满足条件之后会自动执行的方法
- 阻塞:如果没有达到方法的目的,就一直停在这里【等待】。
- 非阻塞:不管有没有达到目的,都直接【往下执行】。
AIO相关类和方法介绍
AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。
但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2---->AIO,主要在Java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。
在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
AIO 同步连接同步读(没有意义,不要求写)
需求
分析
- 获取AsynchronousServerSocketChannel对象,绑定端口
- 同步接收客户端请求
- 读取数据
实现
public static void main(String[] args) throws Exception {
AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
assc.bind(new InetSocketAddress(8888));
System.out.println("准备连接客户端");
Future<AsynchronousSocketChannel> future = assc.accept();
AsynchronousSocketChannel sc = future.get();
System.out.println("连接上了客户端");
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("准备读取数据");
Future<Integer> future2 = sc.read(buffer);
Integer len = future2.get();
System.out.println("读取到了数据");
System.out.println(new String(buffer.array(),0,len));
}
AIO 异步非阻塞连接和异步读
需求
分析
- 获取AsynchronousServerSocketChannel对象,绑定端口
- 异步接收客户端请求
- 在CompletionHandler的completed方法中异步读数据
实现
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
assc.bind(new InetSocketAddress(8000));
System.out.println(1);
assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel s, Object attachment) {
System.out.println(5);
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(3);
s.read(buffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer len, Object attachment) {
System.out.println(6);
System.out.println(new String(buffer.array(),0,len));
}
@Override
public void failed(Throwable exc, Object attachment) {
}
});
System.out.println(4);
}
@Override
public void failed(Throwable exc, Object attachment) {
}
});
System.out.println(2);
while(true){
}
}
|