03【BIO编程】

三、BIO编程

BIO是同步阻塞IO,Java中的BIO编程分为传统的文件IO编程网络IO编程。文件IO编程相关类包在java.io包中,网络IO编程相关类在java.net包下;文件IO关联的是某个文件,从文件中读取数据;网络IO则关联的是某个网络连接,从网络上读取数据;但两者都是属于BIO模型(同步阻塞);

3.2 文件IO编程

我们之前学过的IO流中四个顶层父类:InputStream、OutputStream、Reader、Writer都是属于BIO,即同步阻塞式IO

  • 同步: 当调用read或write方法时,FileInputStream/FileOutputStream的处理是同步的,在处理完read/write方法之前并不能处理其他业务
  • 阻塞: 当没有数据可读时/可写,当前线程发生阻塞,不能处理其他事情

Tips:FileXxxStream关联的是某个文件,如果文件中没有数据可读(读取到文件末尾)则会返回-1,因此看不到阻塞效果;


  • 示例代码:
package com.bio.file;

import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_传统文件IO {
    public static void main(String[] args) throws Exception {
        
        FileInputStream fis = new FileInputStream("");

        FileOutputStream fos = new FileOutputStream("");

        /*
            同步: 当调用read或write方法时,FileInputStream/FileOutputStream的处理是同步的,在处理完read/write方法之前并不能处理其他业务
            阻塞: 当没有数据可读时/可写,当前线程发生阻塞,不能处理其他事情
         */
    }
}

我们将文件流换成标准输入流测试

  • 测试代码:
package com.bio.file;

import java.io.InputStream;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_标准输入流 {
    public static void main(String[] args) throws Exception {

        // 获取到标准输入流(指向的是控制台)
        InputStream in = System.in;

        // 创建一个扫描器,并且从从控制台扫描数据
        Scanner scanner = new Scanner(in);

        // 如果控制台没有数据输入,则阻塞
        String str = scanner.nextLine();

        System.out.println("str: " + str);
    }
}

当控制台没有数据输入时,将会一直阻塞下去,用户线程(main线程)什么事情都不能干;并且nextLine()方法的处理是同步的,这就意味着,在执行nextLine()方法的同时不能执行其他代码;

3.3 网络IO编程

Java中的网络IO编程分为UDP编程和TCP编程;

TCP和UDP的特点:

  • UDP:
    • 1)面向无连接,资源消耗小,传输速度高
    • 3)不保证数据的完整性,可靠性低,安全性低
  • TCP:
    • 1)面向连接,每次连接都需要三次握手,每次断开连接都需要四次挥手,资源消耗大,传输速度低
    • 2)保证数据的完整性,可靠性高,安全性高

3.3.1 UDP编程

在UDP编程中,分为数据的发送端和接收端,Java在UDP编程中,提供有两个类,分别为DatagramPacketDatagramSocket,其中DatagramPacket用于封装一个UDP报文,DatagramSocket则是用于发送UDP报文;

1)UDP相关API
  • DatagramPacket构造方法:
    • public DatagramPacket(byte[] buf, int length, InetAddress address, int port):创建一个数据包对象
      • buf:要发送的内容
      • length:要发送的内容长度,单位字节
      • address:接收端的ip地址
      • port:接收端口号
    • public DatagramPacket(byte buf[], int length):创建一个数据包对象
  • 其他方法:
    • public synchronized int getLength():获取此UDP数据包载荷的数据长度(单位字节)
    • public synchronized int getPort():获取此UDP数据包的目的端口号
    • public synchronized byte[] getData() :获取此UDP数据包的载荷部分(数据)

  • DatagramSocket构造方法:
    • public DatagramSocket(int port):通过端口构建一个发送端/接收端
  • 其他方法:
    • public void send(DatagramPacket p):发送一个UDP数据包
    • public synchronized void receive(DatagramPacket p):接收一个UDP数据包
    • public void close():释放该Socket占用的资源
2)UDP案例测试
  • 发送端测试代码:
package com.bio.net.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro: 发送端
 */
public class Demo01_Sender {

    public static void main(String[] args) throws Exception {

        // 套接字(用于发送UDP报文)
        DatagramSocket socket = new DatagramSocket();

        String str = "hello";

        // 准备一个UDP数据包
        DatagramPacket packet = new DatagramPacket(
                str.getBytes(),
                str.getBytes().length,
                InetAddress.getLocalHost(),
                9999
        );

        // 发送数据包
        socket.send(packet);
        socket.close();
    }
}
  • 接收端测试代码:
package com.bio.net.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * @author lscl
 * @version 1.0
 * @intro: 接收端
 */
public class Demo02_Receive {

    public static void main(String[] args) throws Exception {

        // 创建一个套接字,接收UDP报文
        DatagramSocket socket = new DatagramSocket(9999);

        byte[] bytes = new byte[1024];

        // 创建一个UDP报文,用于存储UDP报文的数据
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length);

        System.out.println("正在接收数据...");

        /*
            同步: socket.receive方法的处理是同步的,最多只能同时处理一个UDP请求,无法同时处理多个

            阻塞: socket调用receive方法时,如果还未接收到UDP报文,那么将阻塞
         */
        socket.receive(packet);                     // 接收UDP报文

        System.out.println("成功接收数据: ");

        // 获取接收数据的大小
        int len = packet.getLength();
        System.out.println("已经接收到:" + len + "个字节");

        // 转换为字符串打印
        System.out.println(new String(bytes, 0, len));
        socket.close();
    }
}
3)UDP实现网络图片传递
  • 发送端:
package com.bio.net.udp02;

import java.io.FileInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_Sender {
    public static void main(String[] args) throws Exception {

        // 关联一个本地文件
        FileInputStream fis = new FileInputStream("100.jpg");

        // 创建一个套接字
        DatagramSocket socket = new DatagramSocket();

        // 准备一个字节数组存储读取到的文件数据
        byte[] data = new byte[8192];

        // 创建一个UDP数据包
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9999);

        while (true) {

            // 从文件中读取数据
            int len = fis.read(data);

            // 将读取到的数据设置到UDP报文中
            packet.setData(data);

            // 设置文件输入流读取到的实际有效字节个数
            packet.setLength(len == -1 ? 0 : len);

            // 发送报文
            socket.send(packet);

            if (len == -1) {
                break;
            }
        }

        socket.close();
    }
}
  • 接收端:
package com.bio.net.udp02;

import java.io.FileOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_Receive {
    public static void main(String[] args) throws Exception {

        // 创建一个套接字(用于接收UDP报文)
        DatagramSocket socket = new DatagramSocket(9999);

        byte[] data = new byte[8192];

        // 创建一个UDP数据包
        DatagramPacket packet = new DatagramPacket(data, data.length);

        // 创建一个文件输出流
        FileOutputStream fos = new FileOutputStream("000.jpg");

        // 死循环监听报文
        while (true) {
            /*
                同步: socket.receive方法的处理是同步的,最多只能同时处理一个UDP请求,无法同时处理多个

                阻塞: socket调用receive方法时,如果还未接收到UDP报文,那么将阻塞
             */
            socket.receive(packet);

            if (packet.getLength() == 0) {
                // 代表读取到了末尾
                break;
            }

            // 写出到指定文件
            fos.write(packet.getData(), 0, packet.getLength());
        }

        fos.close();
        socket.close();
    }
}

我们知道UDP是面向无连接的通讯协议,并且传递数据是不安全的(UDP由于报文的设计在网络数据传递时,如果出现丢包现象那么将无法补发数据),因此在UDP传递完了文件后,文件大小会比原文件要小,这是由于UDP在传输过程中丢包所导致的:

在这里插入图片描述

这是拷贝之后的文件(出现一些瑕疵):

在这里插入图片描述

3.3.2 TCP编程

在TCP通信中,分为数据的发送端(客户端)和接收端(服务器),当建立连接成功后(三次握手),才可以进行数据的发送;

在Java中,提供了两个类用于实现TCP通信程序:

  • 1)客户端:java.net.Socket 类表示;用于与服务器端建立连接,向服务器端发送数据报文等;

  • 2)服务端:java.net.ServerSocket 类表示;用于与客户端的交互;

1)TCP相关API
  • Socket构造方法:

    public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为本机地址。

  • 成员方法:

    • public InputStream getInputStream() : 返回此套接字的输入流。关闭生成的InputStream也将关闭相关的Socket。
    • public OutputStream getOutputStream() : 返回此套接字的输出流。关闭生成的OutputStream也将关闭相关的Socket。
    • public void close() :关闭此套接字。关闭此socket也将关闭相关的InputStream和OutputStream 。
    • public void shutdownOutput() : 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。

  • ServerSocket构造方法:
    • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
  • 成员方法:
    • public Socket accept() :监听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。
2)TCP案例测试
  • 服务器:
package com.bio.net.tcp;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_Server {


    public static void main(String[] args) throws Exception {

        // 创建一个服务器
        ServerSocket serverSocket = new ServerSocket(6666);

        System.out.println("等待客户端的连接.....");
        /*
            同步: accept方法只能同时处理一个客户端的连接,不能同时处理多个客户端的连接,在没有处理完客户端连接时当前线程什么都干不了
            阻塞: 如果没有客户端来连接,accept方法会一直阻塞当前线程
         */
        Socket socket = serverSocket.accept();      // 接收到一个客户端
        System.out.println("客户端连接成功!");

        // InputStream和OutputStream都是BIO
        // 获取与客户端的输入流(用于读取客户端发送过来的数据)
        InputStream in = socket.getInputStream();

        // 获取与客户端的输出流(用于往客户端写出数据)
        OutputStream out = socket.getOutputStream();

        byte[] data = new byte[1024];

        // 读取客户端发送过来的数据
        int len = in.read(data);

        System.out.println("客户端的数据: " + new String(data, 0, len));

        // 往客户端写出数据
        out.write("请问您需要点什么呢?".getBytes());

        // 读取客户端发送过来的数据
        len = in.read(data);

        System.out.println("客户端的数据: " + new String(data, 0, len));

        // 往客户端写出数据
        out.write("好的".getBytes());

        // 关闭流
        socket.close();
        serverSocket.close();
    }
}
  • 客户端:
package com.bio.net.tcp;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.channels.SocketChannel;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_Client {
    public static void main(String[] args) throws Exception{

        // 创建一个客户端
        Socket socket = new Socket("127.0.0.1", 6666);

        // 获取与服务器的输入流(用于读取服务器的数据)
        InputStream in = socket.getInputStream();

        // 获取与服务器的输出流(用于往服务器写出数据)
        OutputStream out = socket.getOutputStream();

        // 往客户端写出数据
        out.write("你好,请问在吗?".getBytes());

        byte[] data = new byte[1024];

        // 读取服务器发送过来的数据
        int len = in.read(data);
        System.out.println("服务器的数据: " + new String(data, 0, len));

        // 往客户端写出数据
        out.write("来一份南昌拌粉~".getBytes());

        // 读取服务器发送过来的数据
        len = in.read(data);
        System.out.println("服务器的数据: " + new String(data, 0, len));

        socket.close();
    }
}
3)TCP实现网络图片传递
  • 服务器:
package com.bio.net.tcp02;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_Server {
    public static void main(String[] args) throws Exception{

        // 创建一个服务器
        ServerSocket serverSocket = new ServerSocket(6666);

        // 接收一个客户端
        Socket socket = serverSocket.accept();

        // 获取与服务器的输入流
        InputStream in = socket.getInputStream();

        // 将读取到的数据写出到磁盘中
        FileOutputStream fos = new FileOutputStream("000.jpg");

        byte[] data=new byte[8192];

        int len;
        while ((len=in.read(data))!=-1){
            fos.write(data,0,len);
        }

        socket.close();
        fos.close();
        serverSocket.close();
    }
}
  • 客户端:
package com.bio.net.tcp02;

import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_Client {
    public static void main(String[] args) throws Exception {

        // 创建一个套接字
        Socket socket = new Socket("127.0.0.1", 6666);

        // 获取与服务器的输出流
        OutputStream out = socket.getOutputStream();

        FileInputStream fis = new FileInputStream("100.jpg");

        byte[] data = new byte[8192];

        int len;
        while ((len = fis.read(data)) != -1) {
            out.write(data,0,len);
        }

        fis.close();
        socket.close();
    }
}

TCP是面向连接的通讯协议,并且传递数据是安全的(TCP的报文中有序列号,确认号等标志位,如果在网络数据传递过程中出现丢包可以侦测到丢失的数据,然后数据重发),因此在使用TCP传递数据时,不会出现丢包等情况;但TCP的传输速度将不如UDP协议;

3.2.3 伪异步IO实现网络IO编程

我们之前使用的网络编程都是属于同步阻塞式的;其中同步指的是同时最多只能处理一个请求,阻塞指的是当调用某方法时,方法还未获取到数据时当前线程将会处于阻塞状态;我们可以利用多线程来实现一个伪异步网络编程,即可以"同时"处理多个请求;

多线程实现TCP网络聊天案例:

  • 客户端:
package com.bio.net.tcp03;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_Client {
    public static void main(String[] args) throws Exception {
        // 获取到一个新的客户端
        Socket socket = new Socket("127.0.0.1", 8888);

        // 读取客户端数据
        InputStream is = socket.getInputStream();

        // 缓冲字符输入流(读取客户端数据更加方便)
        BufferedReader br = new BufferedReader(new InputStreamReader(is));

        // 读线程
        new Thread() {
            @Override
            public void run() {
                try {
                    String info;
                    while (true) {

                        // 死循环读取客户端的信息
                        info = br.readLine();

                        if (info.equals("end")) {

                            socket.close();

                            // 退出应用程序
                            System.exit(0);
                            System.out.println("bye bye....");
                            break;
                        } else {
                            System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
                        }
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();


        // 给客户端写回数据
        OutputStream os = socket.getOutputStream();

        // 缓冲字符输出流(往客户端写数据更加方便)
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

        // 写线程
        new Thread() {
            @Override
            public void run() {
                try {

                    Scanner scanner = new Scanner(System.in);
                    String info;

                    while (true) {
                        // 获取键盘输入的信息
                        info = scanner.nextLine();

                        // 往客户端写数据
                        bw.write(info);
                        bw.newLine();
                        bw.flush();
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }
}
  • 服务器:
package com.bio.net.tcp03;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro: 多线程实现服务器
 */
public class Demo02_Server {
    public static void main(String[] args) throws Exception {
        // 创建一台服务器
        ServerSocket serverSocket = new ServerSocket(8888);

        while (true) {
            // 获取到一个新的客户端
            Socket socket = serverSocket.accept();

            // 开启任务线程执行任务(有读写任务)
            ServerTaskThread task = new ServerTaskThread(socket);
            task.start();
        }
    }
}
  • 服务器任务线程:
package com.bio.net.tcp03;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class ServerTaskThread extends Thread{
    private Socket socket;

    public ServerTaskThread(Socket socket){
        this.socket=socket;
    }

    @Override
    public void run() {

        try {
            System.out.println(socket.getInetAddress().getHostAddress() + "已与您连接成功");

            // 开启读线程
            ReaderThread readerThread = new ReaderThread(socket.getInputStream());
            readerThread.start();

            // 开启写线程
            WriterThread writerThread = new WriterThread(socket.getOutputStream());
            writerThread.start();
        } catch (IOException exception) {
            exception.printStackTrace();
        }

    }

    class ReaderThread extends Thread{
        private InputStream in;

        public ReaderThread(InputStream in){
            this.in=in;
        }

        // 读线程
        @Override
        public void run() {
            while (true){
                // 缓冲字符输入流(读取客户端数据更加方便)
                BufferedReader br = new BufferedReader(new InputStreamReader(in));

                try {
                    String info;
                    while (true) {

                        // 死循环读取客户端的信息
                        info = br.readLine();

                        if (info.equals("end")) {

                            // 关闭与客户端的连接
                            socket.close();
                            System.out.println(socket.getInetAddress().getHostAddress() + "已与您断开连接: ");
                            break;
                        } else {
                            System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
                        }
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 写线程
    class WriterThread extends Thread{
        private OutputStream out;

        public WriterThread(OutputStream out){
            this.out=out;
        }

        @Override
        public void run() {
            while (true){
                // 缓冲字符输出流(往客户端写数据更加方便)
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));

                try {

                    Scanner scanner = new Scanner(System.in);
                    String info;

                    while (true) {
                        // 获取键盘输入的信息
                        info = scanner.nextLine();

                        // 往客户端写数据
                        bw.write(info);
                        bw.newLine();
                        bw.flush();
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

虽然我们服务器处理改为了多线程,但是服务器端的控制台只有一个,当多个线程争抢控制台输入的数据时,最终只会有一个线程抢到了控制台输入的值,也只有一个客户端能收到这个值;

另外还有一点是服务器每接收到一个客户端请求时都会开启一个一个线程来处理这个请求(这个线程里面又开启两个线程分别处理读/写),这种请求与线程一对一的处理显然是比较低效的;再者当客户端断开连接都好不容易创建出来的线程又被销毁了,造成性能浪费;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

緑水長流*z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值