【巩固java基础】网络编程,我奶看了都说好


网络编程三要素

  • 协议:TCP/UDP

  • IP地址

  • 端口号

TCP

TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

TCP协议特点: 面向连接,传输数据安全,传输速度低

在这里插入图片描述

在这里插入图片描述

三次握手

• 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。 你愁啥?

• 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我愁你咋地?

• 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。你再愁试试

完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等

socket中TCP的四次握手释放连接

在这里插入图片描述

四次挥手:TCP协议中,在发送数据结束后,释放连接时需要经过四次挥手。

  • 第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。

  • 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据。

  • 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。

  • 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。

UDP

UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如shipin会议、QQ聊天等

  • UDP特点: 面向无连接,传输数据不安全,传输速度快

  • 例如: 村长发现张三家的牛丢了

  • UDP协议: 村长在村里的广播站广播一下张三家的牛丢了,信息丢失,信息发布速度快

IP

  • 查看本机IP地址,在控制台输入:

ipconfig

  • 检查网络是否连通,在控制台输入:

ping IP地址
ping 220.181.57.216
ping www.baidu.com

小结

  • 协议: 计算机在网络中通信需要遵守的规则,常见的有TCP,UDP协议

  • TCP: 面向连接,传输数据安全,传输速度慢

  • UDP: 面向无连接,传输不数据安全,传输速度快

  • IP地址: 用来标示网络中的计算机设备

    • 分类: IPV4 IPV6
    • 本地ip地址: 127.0.0.1 localhost
  • 端口号: 用来标示计算机设备中的应用程序、0–65535

    • 自己写的程序指定的端口号要是1024以上
    • 如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

TCP通信程序

在这里插入图片描述

// 客户端
public class Client {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 客户端连接服务端
        Socket socket = new Socket("127.0.0.1", 6666);
        System.out.println("我是客户端:" + socket.getLocalPort() + " 启动成功");

        while (true) {
    
    
            // 客户端向服务端发数据
            OutputStream out = socket.getOutputStream();
            Scanner scanner = new Scanner(System.in);
            String info = scanner.next();
            out.write(("客户端:"+info).getBytes());
            out.flush();

            // 客户端接受服务端数据
            InputStream in = socket.getInputStream();
            byte[] bytes = new byte[8721];
            int len = in.read(bytes);
            String s = new String(bytes, 0, len);
            System.out.println(s);
        }

        // socket.close();
    }
}
// 服务端
public class Server {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 服务端开启端口
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("我是服务端:" + serverSocket.getLocalPort() + " 启动成功");
        // 等待客户端连接,没连接之前会阻塞
        Socket accept = serverSocket.accept();
        System.out.println("我是服务端,你好呀客户端:" + accept.getPort() + ",欢迎你连接~");

        while (true) {
    
    
            // 服务端接受客户端数据
            InputStream in = accept.getInputStream();
            byte[] bytes = new byte[8721];
            int len = in.read(bytes);
            String s = new String(bytes, 0, len);
            System.out.println(s);

            // 服务端向客户端发数据
            OutputStream out = accept.getOutputStream();
            Scanner scanner = new Scanner(System.in);
            String info = scanner.next();
            out.write(("服务端:"+info).getBytes());
            out.flush();
        }

        // accept.close();
        // serverSocket.close();

    }
}

TCP通信文件操作

在这里插入图片描述

public class Client {
    
    
    public static void main(String[] args) throws Exception{
    
    
       // 客户端:
       // 1.创建Socket对象,指定要连接的服务器的ip地址和端口号
        Socket socket = new Socket("127.0.0.1",6666);

       // 2.创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("d:\\img\\a.jpg");

       // 3.通过Socket获取输出流
        OutputStream os = socket.getOutputStream();

        // 4.定义一个字节数组,用来存储读取到的字节数据
        byte[] bys = new byte[8192];

       // 4.定义一个int变量,用来存储读取到的字节个数
        int len;

       // 5.循环读取数据
        while ((len = fis.read(bys)) != -1) {
    
    
            // 6.在循环中,写出数据
            os.write(bys, 0, len);
        }

        // os.write("");
        // - 在文件上传时,客户端从文件中读不到数据,就会停止发送。
        // 但是服务器端不知道客户端停止了写数据,所以会一直等待接收客户端写过来的数据。
        // 解决办法:在客户端调用s.shutdownOutput();通知服务器端发送结束了。
        socket.shutdownOutput();

        System.out.println("=======开始接收服务器回写的数据=======");
        //7.通过Socket获得输入流
        InputStream is = socket.getInputStream();

        //8.读取服务器回写的字符串数据
        int read = is.read(bys);// 卡死
        System.out.println(new String(bys,0,read));

        // 9.关闭流,释放资源
        fis.close();
        socket.close();

    }

public class Server {
    
    
    public static void main(String[] args) throws Exception{
    
    
        // 服务器:
        // 1.创建ServerSocket对象,指定端口号(6666)
        ServerSocket ss = new ServerSocket(6666);

        // 循环接收请求
        while (true){
    
    
            // 2.调用accept()方法接收客户端请求,得到Socket对象
            final Socket socket = ss.accept();
            System.out.println("新客户端: " + socket.getPort());
            // 只要建立连接,就开辟线程上传文件
            new Thread(new Runnable() {
    
    

                public void run() {
    
    
                    try{
    
    
                        // 3.通过返回的Socket对象获得输入流
                        InputStream is = socket.getInputStream();

                        // 4.创建字节输出流对象,关联目的地文件路径
                        FileOutputStream fos = new FileOutputStream("d:\\img\\"+System.currentTimeMillis()+".jpg");

                        // 5.定义一个字节数组,用来存储读取到的字节数据
                        byte[] bys = new byte[8192];

                        // 5.定义一个int变量,用来存储读取到的字节个数
                        int len;

                        Thread.sleep(10000);
                        // 6.循环读取数据
                        while ((len = is.read(bys)) != -1) {
    
    // 卡死
                            // 7.在循环中,写出数据
                            fos.write(bys, 0, len);
                        }

                        System.out.println("============开始回写数据给客户端==========");
                        // 8.通过返回的Socket对象获得输出流
                        OutputStream os = socket.getOutputStream();

                        // 9.写出字符串数据给客户端
                        os.write("文件上传成功!".getBytes());

                        // 10.关闭流,释放资源
                        fos.close();
                        socket.close();
                        //ss.close();
                    }catch (Exception e){
    
    

                    }
                }
            }).start();

        }
    }
}

TCP模拟网站服务器

  • 从浏览器中访问本地项目中的资源(类似Tomcat)
// 服务端
public class Server {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 服务器:
        // 1.创建ServerSocket对象,指定端口号(6666)
        ServerSocket ss = new ServerSocket(6666);

        while (true) {
    
    
            // 2.调用accept()方法,获得请求,得到Socket对象
            final Socket socket = ss.accept();
            // 连接建立,开辟线程
            new Thread(new Runnable() {
    
    

                public void run() {
    
    
                    try {
    
    
                        // 3.通过返回的Socket对象,获得输入流
                        InputStream is = socket.getInputStream();

                        // 4.通过输入流,去连接通道中获取数据,进行筛选,筛选出浏览器需要访问的页面路径
                        // 4.1 把字节输入流转换为字符输入流
                        InputStreamReader isr = new InputStreamReader(is);
                        //  4.2 读取第一行数据,因为我们要读取url请求 所以一行一行的读,采用BufferedReader封装
                        BufferedReader br = new BufferedReader(isr);
                        String line = br.readLine();
                    	// 当我们请求一个url的时候,服务器收到的请求为:请求方式(GET)+我们请求的路径+HTTP版本
                        System.out.println(line);// GET /socketnio/web/index.html HTTP/1.1
                        // 4.3 截取浏览器需要访问的页面路径,也就是我们要访问socketnio下的web下的index页面
                        // 按空格分离成数组,然后取索引为1的元素,从这个元素索引为1的位置开始截取
                        String path = line.split(" ")[1].substring(1);
                        System.out.println(path);// socketnio/web/index.html
                        // 拿到我们的路径后就相当于文件传输了
                        // 5.创建字节输入流对象,关联要访问的页面路径
                        FileInputStream fis = new FileInputStream(path);
                        // 6.通过Socket对象获得输出流
                        OutputStream os = socket.getOutputStream();
                        // 7.定义一个byte数组,用来存储读取到的字节数据
                        byte[] bys = new byte[8192];
                        // 7.定义一个int变量,用来存储读取到的字节个数
                        int len;
                        // 响应页面的时候需要同时把以下响应过去给浏览器
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type:text/html\r\n".getBytes());
                        os.write("\r\n".getBytes());
                        // 8.循环读取数据
                        while ((len = fis.read(bys)) != -1) {
    
    
                            // 9.在循环中写出数据
                            os.write(bys, 0, len);
                        }
                        // 10.关闭流,释放资源
                        fis.close();
                        socket.close();
                        //ss.close();
                    } catch (Exception e) {
    
    
                    }
                }
            }).start();
        }
    }
}

NIO

同异步、阻与非阻塞

学习Java的NIO流之前,先了解几个关键词

  • 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系

    • 同步: 调用方法之后,必须要得到一个返回值 例如: 买火车票,一定要买到票,才能继续下一步
    • 异步: 调用方法之后,没有返回值,但是会有回调函数,回调函数指的是满足条件之后会自动执行的方法 例如: 买火车票, 不一定要买到票,我可以交代售票员,当有票的话,你就帮我出张票
  • 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理

    • 阻塞:如果没有达到方法的目的,就会一直停在那里(等待) , 例如: ServerSocket的accept方法
    • 非阻塞: 不管方法有没有达到目的,都直接往下执行(不等待)

NIO是同步的,因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程

三个组成

NIO的三个主要组成部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器)。NIO是在访问个数特别大的时候才使用的 , 比如流行的软件或者流行的游戏中会有高并发和大量连接.

Buffer类

缓冲区

三个创建方式

// 方式一: 在堆中创建缓冲区:间接缓冲区----------------> 推荐
ByteBuffer b1 = ByteBuffer.allocate(10);

// 方式二: 在系统内存创建缓冲区:直接缓冲区
ByteBuffer b2 = ByteBuffer.allocateDirect(10);

// 方式三: 在堆中创建缓冲区
byte[] bys = new byte[10];
ByteBuffer b3 = ByteBuffer.wrap(bys);
// 阅读源码可以看到allocate是调用的HeapByteBuffer,所以是堆内存创建
public static ByteBuffer allocate(int capacity) {
    
    
    if (capacity < 0)
        throw createCapacityException(capacity);
    return new HeapByteBuffer(capacity, capacity, null);
}
// wrap方法其实也是调用的HeapByteBuffer
public static ByteBuffer wrap(byte[] array,
                              int offset, int length)
{
    
    
    try {
    
    
        return new HeapByteBuffer(array, offset, length, null);
    } catch (IllegalArgumentException x) {
    
    
        throw new IndexOutOfBoundsException();
    }
}
  • 并且ByteBuffer类内部封装了一个byte[]数组

在这里插入图片描述

  • 间接缓冲区的创建和销毁效率要高于直接缓冲区
  • 间接缓冲区的工作效率要低于直接缓冲区
主要方法

put、capacity、limit、position、mark、reset、clear、flip

  • 主要理解clear、flip
package com.liu;

import java.nio.ByteBuffer;
import java.util.Arrays;

public class Demo01 {
    
    
    public static void main(String[] args) {
    
    
        ByteBuffer buffer = ByteBuffer.allocate(10);
        // buffer的capacity= 10
        System.out.println("buffer的capacity= " + buffer.capacity());

        buffer.put((byte) 1);
        buffer.put((byte) 2);
        // buffer= [1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer= " + Arrays.toString(buffer.array()));

        // 见名知意,限制buffer只能装n(3)个,并且n<=capacity
        buffer.limit(3);
        buffer.put((byte) 3);
        // buffer.put((byte)4); 出异常:java.nio.BufferOverflowException
        // buffer在limit(3)= [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer在limit(3)= " + Arrays.toString(buffer.array()));

        // 重新定位索引位置(相当于有个指针在操作数组索引,而position就是改变指针位置),下次操作就会从索引n(1)开始,并且n<=limit中的n
        buffer.position(1);
        buffer.put((byte) 4);
        buffer.put((byte) 5);
        // buffer在position(1)= [1, 4, 5, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer在position(1)= " + Arrays.toString(buffer.array()));

        // mark 和 reset是一起的
        // mark标记position的位置(也就是数组索引指针的位置)
        // reset重新将指针定位到mark标记的位置,也就是1
        buffer.position(1);
        buffer.mark();
        buffer.put((byte) 6);
        buffer.put((byte) 7);
        // buffer在position(1)后mark= [1, 6, 7, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer在position(1)后mark= " + Arrays.toString(buffer.array()));
        buffer.reset();
        buffer.put((byte) 8);
        // buffer在position(1)后mark后reset= [1, 8, 7, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer在position(1)后mark后reset= " + Arrays.toString(buffer.array()));

        // flip将指向position索引的指针位置给到limit,position置为0
        // flip多用于读写文件,读多少就写多少
        buffer.limit(10);// 先把limit限制去除
        buffer.position(3);
        buffer.flip();// 把position=0,limit=3
        buffer.put((byte) 9);
        buffer.put((byte) 10);
        buffer.put((byte) 11);
        // buffer.put((byte) 12); 异常java.nio.BufferOverflowException
        // buffer在=flip [9, 10, 11, 0, 0, 0, 0, 0, 0, 0]
        System.out.println("buffer在flip= " + Arrays.toString(buffer.array()));

        // 重置position=0和limit=capacity
        buffer.clear();
        for (int i = 0; i < 10; i++) {
    
    
            buffer.put((byte) i);
        }
        // buffer在clear= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        System.out.println("buffer在clear= " + Arrays.toString(buffer.array()));
    }
}

Channel接口

通道

  • 用于读、写文件的通道。可以看成是IO中的输入和输出流,不同的是:Channel是双向的,可读可写

分类

  • FileChannel:从文件读取数据的 输入流和输出流
  • DatagramChannel:读写UDP网络协议数据 Datagram
  • SocketChannel:读写TCP网络协议数据 Socket(客户端)
  • ServerSocketChannel:可以监听TCP连接 ServerSocket(服务端)
FileChannel类

可以通过FileInputStream和FileOutputStream的getChannel()方法获取一个它的子类对象

文件的复制
public static void main(String[] args) throws Exception {
    
    
    // 需求: 复制a.jpg文件
    // 思路: 使用FileChannel读取数据到字节缓冲数组中,再从字节缓冲数组中写入到目的地文件中
    // 1.创建字节输入流对象,关联数据源文件路径
    FileInputStream fis = new FileInputStream("d:\\img\\a.jpg");

    // 2.创建字节输出流对象,关联目的地文件路径
    FileOutputStream fos = new FileOutputStream("d:\\img\\b.jpg");

    // 3.通过输入流对象获取FileChannel对象
    FileChannel c1 = fis.getChannel();

    // 3.通过输出流对象获取FileChannel对象
    FileChannel c2 = fos.getChannel();

    // 4.创建字节缓冲数组,用来存储读取到的字节数据
    ByteBuffer b = ByteBuffer.allocate(8192);

    // 5.循环读取字节数据
    while (c1.read(b) != -1) {
    
    
        // flip一下: position变成:0,limit变成:position(8192,但是最后一次可能不是8192)  为了保证写出的时候,写出的是读取的数据
        // 采用flip后就不用定义一个len来截取了,可以参考上面TCP文件操作
        b.flip();

        // 6.在循环中,写出字节数据
        c2.write(b);

        // clear一下: position变成:0,limit变成:capacity(8192)  供下一次循环使用
        b.clear();

    }
    // 7.关闭流,释放资源
    c2.close();
    c1.close();
    fos.close();
    fis.close();
}
FileChannel结合MappedByteBuffer实现高效读写

MappedByteBuffer类 是一个ByteBuffer子类,它可以将文件直接映射至内存,把硬盘中的读写变成内存中的读写, 所以可以提高大文件的读写效率

  • FileChannel的map()方法获取一个MappedByteBuffer
  • MappedByteBuffer map(MapMode mode, long position, long size)。说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。
    • FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写
    • FileChannel.MapMode.READ_WRITE:得到的镜像可读可写
    • FileChannel.MapMode.PRIVATE:得到一个私有的镜像
  • 使用RandomAccessFile获得Channel
    • 使用输入流获得的Channel只能指定读模式,也就是"r"
    • 使用输出流获得的Channel只能指定写模式,也就是"rw"
    • 只有RandomAccessFile获取的Channel才能开启任意的这三种模式(mode)
public static void main(String[] args) throws Exception {
    
    
    long a = System.currentTimeMillis();
    // 1.创建RandomAccessFile对象: r表示读,rw表读写
    RandomAccessFile r1 = new RandomAccessFile("d:\\img\\a.rar", "r");
    RandomAccessFile r2 = new RandomAccessFile("d:\\img\\b.rar", "rw");

    // 2.获得Channel对象
    FileChannel c1 = r1.getChannel();
    FileChannel c2 = r2.getChannel();

    // 3.获取源文件的字节大小
    long size = c1.size();
    System.out.println(size);

    // 循环复制
    // 文件总大小: size
    // 假设每次复制的字节大小: everySize = 500MB
    long everySize = 500 * 1024 * 1024;

    // 总共需要复制多少次: count =  size % everySize == 0 ? size / everySize  :  size / everySize +1
    long count = size % everySize == 0 ? size / everySize : size / everySize + 1;

    // 循环复制
    for (long i = 0; i < count; i++) {
    
    
        // 每次复制的起始字节位置:  start = i * everySize
        long start = i * everySize;
        // 每次真实复制多少个字节: trueSize = size - start > everySize ?  everySize :  size - start;
        // 主要是为了最后一次复制不等于每次复制的大小everySize
        long trueSize = size - start > everySize ?  everySize :  size - start;

        // 3.使用Channel调用map方法获得MappedByteBuffer
        MappedByteBuffer m1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
        MappedByteBuffer m2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);

        // 4.把m1数组中的字节数据拷贝到m2中
        for (long j = 0; j < trueSize; j++) {
    
    
            //System.out.println("j = " + j);
            // 获取m1中的字节
            byte b = m1.get();
            // 把获取到的字节存储到m2中
            m2.put(b);
        }
    }
    // 5.释放资源
    c2.close();
    c1.close();
    r2.close();
    r1.close();
    long b = System.currentTimeMillis();
    System.out.println("耗时: " +  (b -a)/1000 + "秒");
}
SocketChannel与ServerSocketChannel
  • SocketChannel类用于连接的客户端,它相当于:Socket

  • ServerSocketChannel类用于连接的服务器端,它相当于:ServerSocket

/**
 * 客户端
 */
// 获得通道
SocketChannel sc = SocketChannel.open();
// 连接服务器
sc.connect(new InetSocketAddress("127.0.0.1", 6666));
// 创建ByteBuffer字节缓冲数组
ByteBuffer b = ByteBuffer.allocate(1024);
// 把数据存储到字节缓冲数组中
b.put("服务器,你好,今晚约吗?".getBytes());
// position设置为0,limit设置为之前的position
b.flip();
// 写出数据到服务器
sc.write(b);
// 释放资源
sc.close();
/**
 * 服务端
 */
// 获得ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定端口号
ssc.bind(new InetSocketAddress(6666));
// 设置非阻塞
ssc.configureBlocking(false);
// 接收请求
SocketChannel sc = null;
while (true) {
    
    
    // 接收客户端连接
    sc = ssc.accept(); // 这里并不会阻塞
    if (sc == null){
    
    
        System.out.println("还未连接"); // 在客户端没启动连接之前一直循环
    }else{
    
    
        System.out.println("连接成功了!");
        break;
    }
}
// 创建ByteBuffer字节缓冲数组
ByteBuffer b = ByteBuffer.allocate(1024);
// 接收客户端写过来的数据
int len = sc.read(b);
System.out.println(new String(b.array(),0,len));
// 释放资源
ssc.close();

Selector

选择器

  • 多路是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接
  • 多路复用的意思就是一个Selector可以监听多个服务器端口
  • 服务器端的非多路复用效果

在这里插入图片描述

如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。

  • 服务器端的多路复用效果

在这里插入图片描述

使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段业务环境下有非常重要的优势

  • 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率

  • Selector selector = Selector.open()

  • channel对象.register(Selector sel, int ops)方法来实现注册

    • 参数1: 选择器

    • 参数2: 通过Selector监听Channel时对什么事件感兴趣

1、 连接就绪–常量:SelectionKey.OP_CONNECT
2、 接收就绪–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)
3、 就绪–常量:SelectionKey.OP_READ
4、就绪–常量:SelectionKey.OP_WRITE

  • 注意
    1.对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
    2.对于ServerSocketChannel必须设置为非阻塞

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

public static void main(String[] args) throws Exception{
    
    
    // 获取3个端口的ServerSocketChannel对象
    ServerSocketChannel ssc1 = ServerSocketChannel.open();
    ServerSocketChannel ssc2 = ServerSocketChannel.open();
    ServerSocketChannel ssc3 = ServerSocketChannel.open();

    // 绑定端口号
    ssc1.bind(new InetSocketAddress(6666));
    ssc2.bind(new InetSocketAddress(7777));
    ssc3.bind(new InetSocketAddress(8888));

    // 设置Channel为非阻塞
    ssc1.configureBlocking(false);
    ssc2.configureBlocking(false);
    ssc3.configureBlocking(false);

    // 获取选择器
    Selector selector = Selector.open();

    // 把Channel注册到选择器上
    ssc1.register(selector, SelectionKey.OP_ACCEPT);
    ssc2.register(selector, SelectionKey.OP_ACCEPT);
    ssc3.register(selector, SelectionKey.OP_ACCEPT);
    // 、、、、、、、做个标记
}
常用方法
  • select、selectedKeys、keys

select服务器等待客户端连接的方法

  • 阻塞问题:
    • 在连接到第一个客户端之前,会一直阻塞
    • 当连接到客户端后,如果客户端没有被处理,该方法会计入不阻塞状态
    • 当连接到客户端后,如果客户端有被处理,该方法又会进入阻塞状态
// 客户端
public static void main(String[] args) throws Exception{
    
    
    // 获得SocketChannel对象
    SocketChannel sc = SocketChannel.open();
    // 连接服务器
    sc.connect(new InetSocketAddress("127.0.0.1", 6666));
    // 创建ByteBuffer字节缓冲数组
    ByteBuffer b = ByteBuffer.allocate(1024);
    // 把数据存储到字节缓冲数组中
    b.put("服务器,你好,今晚约吗6666?".getBytes());
    // position设置为0,limit设置为之前的position
    b.flip();
    Thread.sleep(4000);
    // 写出数据到服务器
    sc.write(b);
    // 释放资源
    sc.close();
}
// 从上面标记位置代码重复
while (true) {
    
    
    // 服务器等待客户端连接
    selector.select();
    
    // Selector的selectedKeys()方法:获取已连接的所有通道集合
    // 获取所有已连接的Channel通道
    Set<SelectionKey> keys = selector.selectedKeys();
    System.out.println("所有已连接的通道个数:"+keys.size());
    
    // Selector的keys()方法:获取已注册的所有通道集合
    // 获取已注册的连接通道
    Set<SelectionKey> set = selector.keys();
    System.out.println("已注册的连接通道:"+set.size());

    // 循环遍历已连接的Channel通道
    for (SelectionKey key : keys) {
    
    
        // SelectionKey封装ServerSocketChannel
        // 获取客户端要连接的ServerSocketChannel
        ServerSocketChannel channel = (ServerSocketChannel)key.channel();
        // 处理客户端的请求
        SocketChannel sc = channel.accept();
        // sc.read....

    }
}
  • 其实selector就相当于一个中间商,再来看这个图

在这里插入图片描述

NIO多路复用

public class Server {
    
    
    public static void main(String[] args) throws Exception{
    
    
        // 获取3个端口的ServerSocketChannel对象
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        ServerSocketChannel ssc3 = ServerSocketChannel.open();

        // 绑定端口号
        ssc1.bind(new InetSocketAddress(6666));
        ssc2.bind(new InetSocketAddress(7777));
        ssc3.bind(new InetSocketAddress(8888));

        // 设置Channel为非阻塞
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);

        // 获取选择器
        Selector selector = Selector.open();

        // 把Channel注册到选择器上
        ssc1.register(selector, SelectionKey.OP_ACCEPT);
        ssc2.register(selector, SelectionKey.OP_ACCEPT);
        ssc3.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
    
    
            // 服务器等待客户端连接
            selector.select();
            // 处理客户端的请求
            // 获取所有已连接的Channel通道
            Set<SelectionKey> keys = selector.selectedKeys();
            /**
             * 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,导致在遍历集合时,遍历到已经			  * 象,出现了异常
             * 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历     
        	 */
            // 循环遍历所有已连接的Channel通道
            /*for (SelectionKey key : keys) {
                // SelectionKey封装了ServerSocketChannel
                // 把key转换为ServerSocketChannel对象
                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();

            }*/

            // 获取迭代器
            Iterator<SelectionKey> it = keys.iterator();
            while (it.hasNext()) {
    
    
                SelectionKey key = it.next();
                // 把key转换为ServerSocketChannel对象
                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();
            }
        }
    }
}
多路复用整理
  • 服务端
public class NIOServer {
    
    
	private int port = 8888;
    // 用于字符集编解码
    private Charset charset = Charset.forName("UTF-8");
    // 用于接收数据的缓冲区
    private ByteBuffer rBuffer = ByteBuffer.allocate(1024);
    // 用于发送数据的缓冲区
    private ByteBuffer sBuffer = ByteBuffer.allocate(1024);
    // 用于存放客户端SocketChannel集合
    private Map<String, SocketChannel> clientMap = new HashMap();
    // 用于监听通道事件
    private static Selector selector;
    public NIOServer(int port) {
    
    
        this.port = port;
        try {
    
    
            init();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
    // 初始化服务器
    private void init() throws IOException {
    
    
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress(port));
        selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动,端口为:" + port);
    }
    /**
     * 服务器端轮询监听,select 方法会一直阻塞直到有相关事件发生或超时
     */
    public void listen() {
    
    
        while (true) {
    
    
            try {
    
    
                selector.select();   // 返回值为本次触发的事件数
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //selectionKeys.forEach(selectionKey -> handle(selectionKey));
                for(SelectionKey sk : selectionKeys){
    
    
    				handle(sk);
    			}
                selectionKeys.clear(); // 清除处理过的事件
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
    /**
     * 处理事件
     */
    private void handle(SelectionKey selectionKey) {
    
    
        try {
    
    
            // 有客户端要连接
            if (selectionKey.isAcceptable()) {
    
    
                ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                SocketChannel client = server.accept();
                client.configureBlocking(false);
                client.register(selector, SelectionKey.OP_READ);
                clientMap.put(getClientName(client), client);
            }
            // 客户端发送了消息
            else if (selectionKey.isReadable()) {
    
    
                SocketChannel client = (SocketChannel) selectionKey.channel();
                rBuffer.clear();
                int bytes = client.read(rBuffer);
                if (bytes > 0) {
    
    
                    rBuffer.flip();
                    String receiveText = String.valueOf(charset.decode(rBuffer));
                    System.out.println(client.toString() + ":" + receiveText);
                    dispatch(client, receiveText);
                }
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
    /**
     * 转发消息给各个客户端
     */
    private void dispatch(SocketChannel client, String info) throws IOException {
    
    
        if (!clientMap.isEmpty()) {
    
    
            for (Map.Entry<String, SocketChannel> entry : clientMap.entrySet()) {
    
    
                SocketChannel temp = entry.getValue();
                if (!client.equals(temp)) {
    
    
                    sBuffer.clear();
                    sBuffer.put(charset.encode(getClientName(client) + ":" + info));
                    sBuffer.flip();
                    temp.write(sBuffer);
                }
            }
        }
    }
    /**
     * 生成客户端名字
     */
    private String getClientName(SocketChannel client){
    
    
        Socket socket = client.socket();
        return "[" + socket.getInetAddress().toString().substring(1) + ":" + Integer.toHexString(client.hashCode()) + "]";
    }
    public static void main(String[] args) {
    
    
        NIOServer server = new NIOServer(7777);
        server.listen();
    }
}
  • 客户端
public class NIOClient {
    
    
	// 服务端地址
	private InetSocketAddress SERVER;
	// 用于接收数据的缓冲区
	private ByteBuffer rBuffer = ByteBuffer.allocate(1024);
	// 用于发送数据的缓冲区
	private ByteBuffer sBuffer = ByteBuffer.allocate(1024);
	// 用于监听通道事件
	private static Selector selector;
	// 用于编/解码 buffer
	private Charset charset = Charset.forName("UTF-8");
	public NIOClient(int port) {
    
    
		SERVER = new InetSocketAddress("localhost", port);
		try {
    
    
			init();
		} catch (IOException e) {
    
    
			e.printStackTrace();
		}
	}
	// 初始化客户端
	private void init() throws IOException {
    
    
		SocketChannel socketChannel = SocketChannel.open();
		socketChannel.configureBlocking(false);
		selector = Selector.open();
		socketChannel.register(selector, SelectionKey.OP_CONNECT);
		socketChannel.connect(SERVER);
		while (true) {
    
    
			selector.select();
			Set<SelectionKey> selectionKeys = selector.selectedKeys();
			//selectionKeys.forEach(selectionKey -> handle(selectionKey));
			
			for(SelectionKey sk : selectionKeys){
    
    
				handle(sk);
			}
			
			selectionKeys.clear(); // 清除处理过的事件
		}
	}
	/**
	 * 处理事件
	 */
	private void handle(SelectionKey selectionKey) {
    
    
		try {
    
    
			// 连接就绪事件
			if (selectionKey.isConnectable()) {
    
    
				SocketChannel client = (SocketChannel) selectionKey.channel();
				if (client.isConnectionPending()) {
    
    
					client.finishConnect();
					System.out.println("连接成功!");
					// 启动线程监听客户端输入
					new Thread() {
    
    
						@Override
						public void run() {
    
    
							while (true) {
    
    
								try {
    
    
									sBuffer.clear();
									Scanner scanner = new Scanner(System.in);
									String sendText = scanner.nextLine();
									System.out.println(sendText);
									sBuffer.put(charset.encode(sendText));
									sBuffer.flip();
									client.write(sBuffer);
								} catch (IOException e) {
    
    
									e.printStackTrace();
								}
							}
						}
					}.start();
				}
				// 注册可读事件
				client.register(selector, SelectionKey.OP_READ);
			}
			// 可读事件,有从服务器端发送过来的信息,读取输出到屏幕上
			else if (selectionKey.isReadable()) {
    
    
				SocketChannel client = (SocketChannel) selectionKey.channel();
				rBuffer.clear();
				int count = client.read(rBuffer);
				if (count > 0) {
    
    
					String receiveText = new String(rBuffer.array(), 0, count);
					System.out.println(receiveText);
				}
			}
		} catch (IOException e) {
    
    
			e.printStackTrace();
		}
	}
	public static void main(String[] args) {
    
    
		new NIOClient(7777);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_51998352/article/details/121527322