网络编程和NIO

软件结构:
1.C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
特点: 客户端和服务器是分开的,需要下载客户端,对网络要求相对低, 开发和维护成本高,相对稳定
2.B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
特点:没有客户端,只有服务器,不需要下载客户端,直接通过浏览器访问, 对网络要求相对高, 开发和维护成本低,服务器压力很大,相对不稳定

网络编程三要素:协议,IP地址,端口号
网络通信协议:通信协议是计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。
1.TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
TCP协议特点: 面向连接,传输数据安全,传输速度低
2.UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
UDP特点: 面向无连接,传输数据不安全,传输速度快
IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。分类:IPV4和IPV6
端口号就可以唯一标识设备中的进程(应用程序)了。
端口号:用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收器端建立逻辑连接,然后再传输数据。它提供了两台计算机之间可靠无差错的数据传输。TCP通信过程如下图所示:
在这里插入图片描述
TCP协议相关的类
1.java.net.Socket : 一个该类的对象就代表一个客户端程序。
Socket(String host, int port) 根据ip地址字符串和端口号创建客户端Socket对象 * 注意事项:只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。 如果连接成功,则表示三次握手通过。
OutputStream getOutputStream(); 获得字节输出流对象
InputStream getInputStream();获得字节输入流对象
void close();关闭Socket, 会自动关闭相关的流
2.java.net.ServerSocket : 一个该类的对象就代表一个服务器端程序。
ServerSocket(int port); 根据指定的端口号开启服务器。
Socket accept(); 等待客户端连接并获得与客户端关联的Socket对象 如果没有客户端连接,该方法会一直阻塞
void close();关闭ServerSocket,一般不关闭

同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
同步: 调用方法之后,必须要得到一个返回值 例如: 买火车票,一定要买到票,才能继续下一步
异步: 调用方法之后,不需要有返回值,但是会有回调函数,回调函数指的是满足条件之后会自动执行的方法 例如: 买火车票, 不一定要买到票,我可以交代售票员,当有票的话,你就帮我出张票
阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理
阻塞:如果没有达到方法的目的,就会一直停在那里(等待) , 例如: ServerSocket的accept()方法
非阻塞: 不管方法有没有达到目的,都直接往下执行(不等待)
IO: 同步阻塞
NIO: 同步非阻塞
AIO: 异步非阻塞

NIO的三个主要组成部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器)

概述:Buffer是一个对象,它是对某种基本类型的数组进行了封装。
作用: 在NIO中,就是通过 Buffer 来读写数据的。所有的数据都是用Buffer来处理的,它是NIO读写数据的中转池, 通常使用字节数组。
Buffer主要有如下几种:
ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer

ByteBuffer类内部封装了一个byte[]数组,并可以通过一些方法对这个数组进行操作。
创建ByteBuffer对象
方式一:在堆中创建缓冲区:allocate(int capacity)
方式二: 在系统内存创建缓冲区:allocatDirect(int capacity)
方式三:通过数组创建缓冲区:wrap(byte[] arr)

在堆中创建缓冲区称为:间接缓冲区
在系统内存创建缓冲区称为:直接缓冲区
间接缓冲区的创建和销毁效率要高于直接缓冲区
间接缓冲区的工作效率要低于直接缓冲区

Buffer的容量(capacity)是指:Buffer所能够包含的元素的最大数量。定义了Buffer后,容量是不可变的
限制limit是指:第一个不应该读取或写入元素的index索引。缓冲区的限制(limit)不能为负,并且不能大于容量。
位置position是指:当前可写入的索引。位置不能小于0,并且不能大于"限制"。
标记mark是指:当调用缓冲区的reset()方法时,会将缓冲区的position位置重置为该索引。
常用方法:方法都是操作position到limit之间的位置
public ByteBuffer put(byte b):向当前可用位置添加数据。
public ByteBuffer put(byte[] byteArray):向当前可用位置添加一个byte[]数组
public ByteBuffer put(byte[] byteArray,int offset,int len):添加一个byte[]数组的一部分
public int limit():获取此缓冲区的限制。
public Buffer limit(int newLimit):设置此缓冲区的限制。
public int position():获取当前可写入位置索引。
public Buffer position(int p):更改当前可写入位置索引。
public Buffer mark():设置此缓冲区的标记为当前的position位置。
public Buffer reset() : 将此缓冲区的位置重置为以前标记的位置。
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,丢弃标记。

Channel(通道):Channel是一个对象,可以通过它读取和写入数据, 可以把它看做是IO中的流,不同的是:Channel是双向的, Channel对象既可以调用读取的方法, 也可以调用写出的方法 。
Channel 的分类:在Java NIO中的Channel主要有如下几种类型:
FileChannel:从文件读取数据的 输入流和输出流
DatagramChannel:读写UDP网络协议数据 DatagramPackge
SocketChannel:读写TCP网络协议数据 Socket
ServerSocketChannel:可以监听TCP连接 ServerSocket

获取FileChannel类的对象:
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
//获得传输通道channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();

FileChannel结合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{
        //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
        //"r"表示:只读--输入流,只读就可以。
        RandomAccessFile r1 = new RandomAccessFile("day19\\aaa\\a.txt","r");
        //"rw"表示:读、写--输出流,需要读、写。
        RandomAccessFile r2 = new RandomAccessFile("day19\\aaa\\aCopy2.txt","rw");
        // 获得FileChannel管道对象
        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才能开启任意的这三种模式!

下例使用循环,将文件分块,可以高效的复制大于2G的文件

public static void main(String[] args) throws Exception{
        //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
        //"r"表示:只读--输入流,只读就可以。
        RandomAccessFile r1 = new RandomAccessFile("H:\\课堂资料.zip","r");
        //"rw"表示:读、写--输出流,需要读、写。
        RandomAccessFile r2 = new RandomAccessFile("H:\\课堂资料2.zip","rw");
        // 获得FileChannel管道对象
        FileChannel c1 = r1.getChannel();
        FileChannel c2 = r2.getChannel();
        // 获取文件的大小
        long size = c1.size();
        // 每次期望复制500M
        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();   
    }

ServerSocketChannel和SocketChannel创建连接

客户端:SocketChannel类用于连接的客户端,它相当于:Socket。
1). 先调用SocketChannel的open()方法打开通道:
2). 调用SocketChannel的实例方法connect(SocketAddress add)连接服务器:

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);// 写成false叫非阻塞, 写成true叫阻塞
        
         while(true) {
              //获取客户端连接
              SocketChannel sc = ssc.accept();

              if(sc != null){
                  //不等于null说明连接上了客户端
                  System.out.println("连接上了。。");
                  //读取数据操作
                  break;
              }else{
                  //没连接上客户端
                  System.out.println("打会儿游戏~");
                  Thread.sleep(2000);
              }
          }
    }
}

书写服务器代码

public class Server {
    public static void main(String[] args)  throws IOException{
		//创建对象
        //ServerSocket ss = new ServerSocket(8888);

        //创建
        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) {
        //创建对象
        //Socket s = new Socket("127.0.0.1",8888);

        //创建对象
        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(选择器)–多路复用

选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。
"多路"是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。
如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。
在这里插入图片描述
使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段业务环境下有非常重要的优势

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服务器通道对象
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        // 绑定端口号
        ssc1.bind(new InetSocketAddress(7777));// 设置非阻塞
        ssc1.configureBlocking(false);// 获取Selector选择器对象
        Selector selector = Selector.open();// 把服务器通道的accept()交给选择器来处理
        // 注册Channel到Selector选择器上
        ssc1.register(selector, SelectionKey.OP_ACCEPT);
    }
}

示例:服务器创建3个通道,同时监听3个端口,并将3个通道注册到一个选择器中

public class Test2 {
    public static void main(String[] args) throws Exception{
        /*
            把多个Channel注册到一个选择器上
         */
        // 获取ServerSocketChannel服务器通道对象
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        // 绑定端口号
        ssc1.bind(new InetSocketAddress(7777));
        // 获取ServerSocketChannel服务器通道对象
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        // 绑定端口号
        ssc2.bind(new InetSocketAddress(8888));
        // 获取ServerSocketChannel服务器通道对象
        ServerSocketChannel ssc3 = ServerSocketChannel.open();
        // 绑定端口号
        ssc3.bind(new InetSocketAddress(9999));
        // 设置非阻塞
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);
        // 获取Selector选择器对象
        Selector selector = Selector.open();
        // 把服务器通道的accept()交给选择器来处理
        // 注册Channel到Selector选择器上
        ssc1.register(selector, SelectionKey.OP_ACCEPT);
        ssc2.register(selector,SelectionKey.OP_ACCEPT);
        ssc3.register(selector,SelectionKey.OP_ACCEPT);
    }
}

接下来,就可以通过选择器selector操作三个通道了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值