IO

IO

  • 定义:[流是一组有 顺序的,单向的,动态 的字符或者字节的集合]{.red}

    《Java 编程思想》定义:流代表了任何有能力产出数据的 数据源对象 或者是有能力接收数据的 接收端对象(流为什么会是一种对象呢?)

  • 核心:[屏蔽输入输出设备实际处理数据的细节]{.red}

    理解:就是不需要手动创建缓冲区等结构去处理数据,只需要创建流对象就可以帮助我们完成数据的传输

  • 分类:

    • 按照流向分类:输入流和输出流

      • 核心:[输入输出流的名称都是相对而言的]{.red}

      • 输入流:从数据输入源流向数据接收源(外部设备数据流向内存)

      • 输出流:从数据接收源流向数据接收源(内存数据流向外部设备)

    • 按照功能分类:节点流(基本类)和处理流(装饰类)

      • 核心:设计模式:装饰器模式Java I/O 就是围绕装饰器模式进行设计的)

      • 节点类:进行最基本的数据传输功能的类:CharArrayReader、ByteArrayReader...

      • 处理类:扩展数据传输功能的类:BufferedReader...

        a6e24a137dc7e2736fed6d47cbe26246.png
    • 按照数据单位分类:字节流和字符流

      • 字节流:[可以读取任何类型的文件]{.red},主要用于读取二进制文件(图片、视频文件等):InputStream、OutputStream..

      • 字符流:[仅能够读取文本文件]{.red}:Reader、Writer...

      • 细节:

        • [设计之初时字节流是没有自带缓冲区的,后续添加的字符流就默认携带缓冲区]{.red}

        • JVM 内部运行采用 UTF-16BE 编码,class 文件采用 UTF-8 编码,Java 源代码可以采用任何形式编码

          注:JVM 内部和 class 文件采用的编码并不相同,也就意味着中间会发生转换

核心类

文件类

:::info

  • 文件工具类在 JDK 1.7 之后又进行了更新 -> 提供了更加方便的方法
  • 实际开发中已经很少直接操作文件了,通常借助数据库管理文件

:::

  • 定义:提供操作文件相关的方法,[不包含读取和写入文件内容的操作]{.red}

  • 特点:文件类既可以表示文件也可以表示目录

  • 继承 & 实现:

    • 实现 Serializable 接口:文件类可以进行序列化操作
    • 实现 Comparable 接口:文件类可以相互比较
  • 构造方法:[只有有参构造器没有无参构造器]{.red}

    /*最常使用的构造方法:传入文件的路径名称*/
    public File(String pathname) {
    // 如果传入的路径是空才会报错,如果传入的路径是错误的是不会抛出异常的
    if (pathname == null) {
    throw new NullPointerException();
    }
    }
    /*第一个参数传入父目录,第二个参数传入子目录*/
    public File(String parent, String child) {...}
    /*传入文件的 URI 地址*/
    public File(URI uri) {...}
  • 方法

    • 创建文件、目录、删除文件、目录

      public static void create() throws IOException
      {
      File file = new File("file.txt");
      // 创建文件
      file.createNewFile();
      // 单级目录
      File directory = new File("first");
      // 创建单级目录
      directory.mkdir();
      // 多级目录
      File directories = new File("first/second");
      // 创建多级目录
      directories.mkdirs();
      // 删除文件或者目录
      file.delete();
      directory.delete();
      }
    • 判断路径表示的是文件还是目录

      public static void judge()
      {
      File file = new File("file.txt");
      // 判断路径表示文件或者目录是否存在
      file.exists();
      // 判断是否是文件
      file.isFile();
      // 判断是否为目录
      file.isDirectory();
      }
    • 获取表示文件或者目录的路径

      public static void getPath()
      {
      File directory = new File("D:\\JavaWeb\\Java 网络编程\\src\\main\\java\\myio");
      // 获取路径表示的目录或者文件名称
      directory.getName();
      // 获取该目录或者文件的父路径
      directory.getParent();
      // 获取绝对路径
      directory.getAbsolutePath();
      }
    • 遍历目录

      // 非递归遍历
      public static void listFile(File directory)
      {
      // 如果文件不存在就创建目录
      if (!directory.exists())
      directory.mkdir();
      // 如果不是目录就直接抛出异常
      if (!directory.isDirectory())
      throw new IllegalArgumentException("这不是目录!");
      String[] files = directory.list();
      for (String file : files)
      {
      System.out.println("子路径: " + file);
      }
      }
      // 递归遍历
      public static void listFileByRecursive(File directory)
      {
      // 如果文件不存在就创建目录
      if (!directory.exists())
      directory.mkdir();
      // 如果传入的是文件就直接返回
      if (directory.isFile())
      return;
      File[] files = directory.listFiles();
      for (File file : files)
      {
      if (directory.isDirectory())
      listFileByRecursive(file);
      System.out.println("父路径: " + file.getParent() + "\t" + "子路径: " + file.getName());
      System.out.println("绝对路径: " + file.getAbsolutePath());
      }
      }

网络类

InetSocketAddress 类

  • 定义:用于表示套接字的类

    [注:Socket 类构造方法内部就是创建了 InetSocketAddress 对象,所以也表示套接字]{.blue}

  • 构造方法

    // 服务器常调用的构造方法
    public InetSocketAddress(int port) {
    this(InetAddress.anyLocalAddress(), port);
    }
    // 客户端常调用的构造方法
    public InetSocketAddress(String hostname, int port) {
    checkHost(hostname);
    InetAddress addr = null;
    String host = null;
    try {
    addr = InetAddress.getByName(hostname);
    } catch(UnknownHostException e) {
    host = hostname;
    }
    holder = new InetSocketAddressHolder(host, addr, checkPort(port));
    }

URL 类

  • 定义:表示统一资源定位符,也就是表示某个网页的资源

  • 构造方法

    // 仅指定 URL 地址
    // 不指定端口号时,访问该网址的时候使用默认端口号,但是调用获取端口号的方法时就会返回 -1
    public URL(String spec) throws MalformedURLException {
    this(null, spec);
    }
    // 访问的 URL 采用的协议、IP 地址、端口号、需要获取的资源的地址
    public URL(String protocol, String host, int port, String file) throws MalformedURLException
    {
    this(protocol, host, port, file, null);
    }
  • 方法

    // 获取该 URL 携带的查询参数:? 之后表示的就是携带的参数
    public String getQuery() {
    return query;
    }
    // 获取该 URL 的端口号:如果没有指定就会返回 -1
    public int getPort() {
    return port;
    }
    // 获取 URL 采用的协议
    public String getProtocol() {
    return protocol;
    }
    // 获取该 URL 的流对象:可以借助流对象传输 URL 表示的资源信息
    public final InputStream openStream() throws java.io.IOException {
    return openConnection().getInputStream();
    }
    // 获取该 URL 表示的内容:实际获取的是网页的源代码(前端)
    public final Object getContent() throws java.io.IOException {
    return openConnection().getContent();
    }

Socket 类

  • 核心:[基于 TCP 协议设计的网络通信类]{.red}

  • 前提:每个采用 TCP 协议并且想要参与网络通信的进程都需要创建 自己的 [Socket(套接字)]{.blue}

  • 客户端(Socket 类):

    • 构造方法:[需要传入访问的服务器的端口和服务器的主机地址]{.red}

      /*Socket 类常用的构造方法*/
      public Socket(String host, int port) throws UnknownHostException, IOException
      {
      this(host != null ? new InetSocketAddress(host, port) :
      new InetSocketAddress(InetAddress.getByName(null), port),
      (SocketAddress) null, true);
      }

      private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException {
      // 客户端连接到服务器的端口号
      connect(address)
      }
  • 服务器(ServerSocket 类):

    • 构造方法:[仅需要传入服务器当前的端口号;默认主机地址为 127.0.0.1]{.red}

      /*ServerSocket 类常用的构造方法*/
      public ServerSocket(int port) throws IOException {
      this(port, 50, null);
      }
      /*调用的另一个构造方法*/
      public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
      ...
      /*将服务器创建的套接字绑定到相应的网卡设备上:如果没有绑定是没有办法通信的*/
      bind(new InetSocketAddress(bindAddr, port), backlog);

      }
    • 核心方法:accept() ==阻塞式监听端口==:只要没有客户端连接就一直阻塞在当前位置监听;客户端发送连接请求,该方法立刻返回相应的客户端套接字

流类

:::info

所有传统的 I/O 工具类的核心方法都是输入输出和各式各样的重载,没有太大的难度

参考博客:【Java基础-3】吃透Java IO:字节流、字符流、缓冲流

:::

:::warning

  • 装饰类通常会添加许多基础类没有的方法,所以可以额外增加基本类的功能
  • [多个装饰类叠加的时候可能导致重写了同一个方法,所以这样就只能使用最外面那个装饰类的方法]{.blue}

:::

字节流框架图

:::info

通常不再使用字节流,因为效率相对字符流较低

:::

f18048073f2e60c18823536388883e6e.png

字节输入流

  • 节点类(基本类)
    • ByteArrayInputStream:数据输入源是 [字节数组]{.red}
    • FileInputStream:数据输入源是 [文件]{.red}
    • PipeInputStream:数据输入源是 [流]{.red}(从其他线程共用的管道流中获取数据)
  • 处理类(装饰类)
    • BufferedInputStream:为节点类的增加缓冲区功能([字节流默认没有缓冲机制所以需要手动添加缓冲区]{.red})
    • DataInputStream:为节点类可以读出各式各样的数据的功能,可以直接读出整型、字符、布尔、浮点数等类型,不需要通过字节进行转换
    • PushBackInputStream
    • LineNumberInputStream:已经被废弃的类
  • 序列化:ObjectInputStream:数据输入源是 [对象]{.red}

字节输出流

  • 节点类(基本类)
    • ByteArrayOutputStream
    • FileOutputStream
    • PipeOutputStream
  • 处理类(装饰类)
    • DataOutputStream
    • BufferedOutputStream
    • PrintStream:[格式化打印流;可以将数据按照一定格式放入流中]{.red}
  • 序列化:ObjectOutputStream

字节流方法

  • 读方法

    • 节点类:① 仅列出常用的方法 ② 所有节点类的方法基本相同

      // 每次仅读取一个字节
      public synchronized int read();
      // 每次读取一个数组大小的字节数量:相当于数组是缓冲区
      public synchronized int read(byte b[], int off, int len);
      // 判断还有多少字节数量没有读取
      public synchronized int available();
    • 处理类:仅列出新增的方法

      • DataInputStream

        // 读取布尔类型的数据
        public final boolean readBoolean() throws IOException;
        // 读取短整型的数据
        public final short readShort() throws IOException;
        // 读取整型数据
        public final int readInt() throws IOException;
        // 读取长整型的数据
        public final long readLong() throws IOException;
        // 读取字符
        public final char readChar() throws IOException;
        // 读取单精度浮点数的数据
        public final float readFloat() throws IOException;
        // 读取双精度浮点数的数据
        public final double readDouble() throws IOException
      • BufferedInputStream:只是为节点类增加了缓冲区,没有新增任何方法

  • 写方法

    • 节点类:① 仅列出常用的方法 ② 所有节点类的方法基本相同

      // 写入单个字节:虽然写入的是整型,但是在实现中会将其强制转换为字节类型
      public synchronized void write(int b);
      // 写入字节数组
      public synchronized void write(byte b[], int off, int len);
      // 输出流中存在的字节数量
      public synchronized int size();
    • 处理类:仅列出新增的方法

      • DataOutputStream

        // 写入布尔类型的数据
        public final boolean writeBoolean(boolean v) throws IOException;
        // 写入短整型的数据
        public final short writeShort(int v) throws IOException;
        // 写入整型数据
        public final void writeInt(int v) throws IOException;
        // 写入长整型数据
        public final void writeLong(long v) throws IOException'
        // 写入单个字符
        public final void writeChar(int v) throws IOException;
        // 写入字符串
        public final void writeChars(String s) throws IOException;
        // 写入单精度浮点数的数据
        public final float writeFloat(float v) throws IOException;
        // 写入双精度浮点数的数据
        public final double writeDouble(double v) throws IOException
      • PrintStream:如果不使用格式化类,那么输出内容时需要自己手动添加换行符等结束信息,否则接收信息时可能接收不到

        // 仅列出常用的:还有各种重载
        public void print(String s);
        public void println(String x);
        public PrintStream printf(String format, Object ... args);
      • BufferedOutputStream:只是为节点类增加了缓冲区,没有新增任何方法

字符流框架图

0f9da60b5bbd7e20578e361b30f1cc2d.png

字符输入流

  • 节点类:

    • CharArrayReader:数据输入源是 [字符数组]{.red}
    • StringReader:数据输入源是 [字符串]{.red}
    • PipeReader:数据输入源是 [流]{.red}(从其他线程共用的管道流中获取数据)
  • 处理类:

    • BufferedReader:为节点类增加缓冲区功能(JDK 1.1 之后添加的 [字符流默认携带缓冲区]{.red})

      注:BufferedReader 不是 FilterReader 的子类

  • 字节转换类:

    • InputStreamReader:[将字节流转换成字符流,网络通信中经常使用该类]{.red}

      /*将客户端的字节流转换成字符流*/
      BufferedReader reader = new BufferedReader(
      new InputStreamReader(
      socket.getInputStream()))
    • FileReader:[便捷类]{.red}

      • 负责将文件字节流转换成文件字符流
      • [直接继承 InputStreamReader 类;内部直接调用父类的构造方法]{.red}
      /*下面两者是完全等价的*/
      InputStreamReader reader = new InputStreamReader(new FileInputStream(filename));
      FileReader reader = new FileReader(filename);

字符输出流

  • 节点类
    • CharArrayWriter
    • StringWriter
    • PipeWriter
  • 处理类
    • BufferedWriter
    • PrintWriter
  • 字节转换类:
    • OutputStreamWriter
    • FilterWriter

字符流方法

  • 前提:

    • 方法基本和字节流没有什么区别,只不过读出和写入的类型有一定的变化
    • [字符流的所有方法默认自带缓冲区,即使使用缓冲区装饰类效果也一般]{.red}
  • 读方法:BufferedReader

    	// 最常使用的读取方法
    public String readLine() throws IOException;

    * 写方法

    > **实例:结合网络类 + 流对象编写的例子**

    ```java
    // 客户端
    public static void main(String[] args) throws IOException
    {
    // 客户端访问服务器端口: 客户端的端口是操作系统随机分配的,不需要程序员决定,只需要指定访问哪个服务器端口
    Socket client = null;
    // 客户端向服务器发送数据
    PrintWriter writer = null;
    // 客户端接收服务器返回的数据
    BufferedReader reader = null;
    // 客户端输入需要发送的数据
    BufferedReader consoleReader = new BufferedReader(
    new InputStreamReader(
    System.in));
    client = new Socket(DEFAULT_SERVER_HOSTNAME, DEFAULT_SERVER_PORT);
    System.out.println("客户端启动成功...");
    // 向服务器端发送数据并且接收返回的数据
    String string;
    writer = new PrintWriter(
    new BufferedWriter(
    new OutputStreamWriter(
    client.getOutputStream())));

    // 客户端输入 exit 就退出
    while (!EXIT.equals(string = consoleReader.readLine()))
    {
    System.out.println(string);
    writer.println(string);
    writer.flush();
    reader = new BufferedReader(
    new InputStreamReader(
    client.getInputStream()));
    System.out.println(string);
    }

    reader.close();
    writer.close();
    client.close();
    }
    // 服务器
    public static void main(String[] args) throws IOException
    {
    // 创建服务器处理请求的端口 Socket
    ServerSocket server = null;
    // 服务器读取客户端传递的数据
    BufferedReader reader = null;
    // 服务器返回给客户端的数据
    PrintWriter writer = null;

    server = new ServerSocket(DEFAULT_SERVER_PORT);
    System.out.printf("服务器[%d]:%s\n", DEFAULT_SERVER_PORT, "启动完成...");
    while (true)
    {
    // 服务器等待客户端发出请求,采用阻塞式调用:如果没有客户端发出请求就一直等待
    Socket client = server.accept();
    System.out.printf("客户端[%d]:%s\n", client.getPort(), "建立连接");
    /*
    1. 读取客户端发出的请求并处理并向客户端返回消息
    2. 传输过程中采用的是字节传输,所以 Socket 也只能获得字节流而不是字符流
    */
    String string;
    reader = new BufferedReader(
    new InputStreamReader(
    client.getInputStream()));
    writer = new PrintWriter(
    new BufferedWriter(
    new OutputStreamWriter(
    client.getOutputStream())));
    while ((string = reader.readLine()) != null)
    {
    // 格式化输出客户端发送的消息
    System.out.printf("客户端[%d]:%s\n", client.getPort(), string);
    // 向客户端返回消息
    writer.println("服务器处理完毕");
    // 确保缓冲区中所有的内容都被推出: writer 关闭后会自动推出缓冲区中剩余的内容
    writer.flush();
    }
    System.out.printf("客户端[%d]:%s", client.getPort(), "退出连接");

    reader.close();
    writer.close();
    client.close();
    }
    }

实例:多人聊天室

服务器端

public enum MyServer
{
// 现在仅提供了多人聊天服务器的套接字
CHATSERVER(8888, "127.0.0.1");

// 每个套接字都具有端口号
private int port;
// 每个套接字都具有 IP 地址
private String hostname;

// 枚举类型的构造方法始终都是私有的
MyServer(int port, String hostname)
{
this.port = port;
this.hostname = hostname;
}

// 提供相应的方法以便获取枚举实例中的值

public int getPort()
{
return port;
}

public String getHostname()
{
return hostname;
}
}
package chatroom.v2.chatserver;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;

/**
* 1. v1 版本创建线程的方式采用的是 Lambda 表达式: 变量的使用和流的关闭都受到很大的限制;主要原因还是在于其本质是内部类</br>
* 2. v1 版本此前将控制台输入线程作为主线程处理,导致主线程被堵塞后,子线程全部被堵塞</br>
* 主要原因在于线程的不可控:Java 中的线程调度几乎是随机的,设置优先级仍然无法改善,并且主线程的优先级始终都是很高的</br>
* 3. v2 版本采用实现接口的形式创建线程,并且改进此前的错误设计</br>
*/
public class ChatServer
{
private static final String EXIT = "退出";
// 服务器绑定端口
private ServerSocket server;
/*
1. 记录聊天室目前在线人数,方便之后转发消息
2. 采用哈希表的原因
2.1 每个哈希表可以保存当前向客户端输出的流,方便之后关闭;如果采用链表保存,则每次转发消息都需要创建输出流,非常不好用
2.2 没有采用原始的哈希表,因为服务器会创建多个线程,原始的哈希表是线程不安全的,容易导致并发问题
*/
private ConcurrentHashMap<Integer, PrintWriter> clients;

// 构造方法用于初始化哈希表
public ChatServer()
{
clients = new ConcurrentHashMap<>();
}


/**
* 客户端连接到服务器
* @param client 客户端套接字
* @throws IOException 抛出异常
*/
public void addClient(Socket client) throws IOException
{
// 确保传入的客户端套接字不为空
if (client != null)
{
// 将客户端的端口号作为 Key, 将客户端的输出流作为 Value
clients.put(client.getPort(), new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
client.getOutputStream()))));
}
}

/**
* 客户端断开连接
* @param client 断开连接的套接字
* @throws IOException 抛出异常
*/
public void removeClient(Socket client) throws IOException
{
// 确保套接字不是空并且存在于哈希表中
if (client != null && clients.containsKey(client.getPort()))
{
// 移除哈希表中的客户端之前应该先关闭对客户端的输出流
clients.get(client.getPort()).close();
// 移除
clients.remove(client.getPort());
}
}

/**
* 服务器进程结束时需要释放的资源
* @throws IOException
*/
public void close() throws IOException
{
// 可以选择直接关闭套接字
if (server != null && !server.isClosed())
{
server.close();
}
}

/**
* 线程转发客户端发送的信息
* @param client 发送消息的客户端
* @param message 发送的消息
* @throws IOException 抛出的异常
*/
public void forward(Socket client, String message) throws IOException
{
// 如果发送的是退出消息,那么就只发送回给原客户端,提示它可以退出了
if(EXIT.equals(message))
{
if (clients.containsKey(client.getPort()))
{
PrintWriter writer = clients.get(client.getPort());
writer.println(message);
writer.flush();
}
return;
}

if (clients.containsKey(client.getPort()))
clients.forEach((value, key)->{
if (value != client.getPort())
key.println(message);
key.flush();
});
}

// 调用其他所有方法,并且负责为客户端创建服务线程
public void start()
{
try
{
// 服务器绑定端口号
server = new ServerSocket(MyServer.CHATSERVER.getPort());
System.out.println("服务器:启动成功");
// 服务器永久运行
while (true)
{
// 服务器采用阻塞式调用获取客户端: 只要没有客户端连接, 服务器进行将始终阻塞在这里
Socket socket = server.accept();
// 为客户端创建相应的服务线程:
new Thread(new ChatServerThread(this, socket)).start();
System.out.printf("客户端[%d]:%s", socket.getPort(), "连接成功\n");

}

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

}
}

public static void main(String[] args)
{
// 启动服务器
new ChatServer().start();
}
}
package chatroom.v2.chatserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
* 服务器线程用于处理每个客户端的请求
*/
public class ChatServerThread implements Runnable
{
private static final String EXIT = "退出";
// 线程需要完成的任务:建立和客户端的连接、断开和客户端的连接、转发消息、读取消息:需要服务器才能够调用方法
private ChatServer server;
// 每个客户端都具有相应的服务线程:所以需要记录当前线程服务的哪个客户端
private Socket socket;
// 线程读取客户端的输入流
private BufferedReader reader;

// 最好不要在构造方法中抛出异常
public ChatServerThread(ChatServer server, Socket socket)
{
this.server = server;
this.socket = socket;
}

@Override
public void run()
{

try
{
System.out.println("服务器:创建线程成功");
// 将客户端放入在线列表中
server.addClient(socket);
reader = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// 线程读取客户端的输入流并且转发消息
String message;
// 格式化信息
String formatMessage;
// 只要客户端的输入流没有关闭, 那么线程就会一直在这个循环的判断条件中被阻塞; 输入流已关闭就会立刻跳出循环
// readLine 读入到换行符结束,所以客户端发送的消息必须有换行符,否则认为客户端没有发送结束
while ((message = reader.readLine()) != null)
{
formatMessage = "客户端[" + socket.getPort() +"]:" + message;
// 服务器查看客户端发送的信息
System.out.println(formatMessage);
// 服务器将客户端的信息转发给其余客户端
server.forward(socket, EXIT.equals(message) ? message :formatMessage);
}
// 客户端退出提示信息
System.out.printf("客户端[%d]:%s", socket.getPort(), "断开连接");
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
// 关闭输入流
reader.close();
// 关闭输出流
server.removeClient(socket);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}

客户端

package chatroom.v2.chatclient;

import chatroom.v2.chatserver.MyServer;

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

/**
* 1. idea 默认客户端不可以并行运行,可以在设置中修改
* 2. 控制台的输入线程不能够作为主线程,否则会导致子线程也被堵塞
*/
public class ChatClient
{
// 客户端退出使用的常量
private static final String EXIT = "退出";
// 客户端同样需要端口
private Socket socket;
// 主线程:客户端向服务器输出流
private PrintWriter writer;
// 主线程:客户端读取服务器输入流
private BufferedReader reader;

/**
* 子线程发送消息
* @param message 发送的消息没办法格式化,因为不知道进程的端口号
*/
public void send(String message)
{
if (socket != null)
writer.println(message);
writer.flush();
}

/**
* 接收服务器转发的消息
* @return 转发的消息
*/
public String receive() throws IOException
{
String message;
if (socket != null && (message = reader.readLine()) != null)
return message;

return null;
}

// 调用其他所有方法:主线程只负责接收消息,子线程负责发送消息
public void start()
{
try
{
// 客户端绑定端口号
socket = new Socket(MyServer.CHATSERVER.getHostname(), MyServer.CHATSERVER.getPort());
writer = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())));
reader = new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
System.out.println("客户端:启动成功");
// 启动子线程:等待控制台输入
new Thread(new ChatClientThread(this)).start();
// 客户端不断循环直到想要退出为止
String message;
while (!EXIT.equals(message = receive()))
{
System.out.println(message);
}
System.out.println("退出....");
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
reader.close();
writer.close();
socket.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}

public static void main(String[] args)
{
new ChatClient().start();
}

}

package chatroom.v2.chatclient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ChatClientThread implements Runnable
{
private static final String EXIT = "退出";
// 用于读取控制台的流
private static final BufferedReader READER = new BufferedReader(
new InputStreamReader(
System.in));

private ChatClient client;

public ChatClientThread(ChatClient client)
{
this.client = client;
}

@Override
public void run()
{
String message;
System.out.println("客户端:线程启动成功");
try
{
while ((message = READER.readLine()) != null)
{
/*
客户端的主线程负责接收消息,子线程负责发送消息同时管理控制台的输入
我们没有办法在主线程中获取子线程中的输入内容,也就没有办法直接通过控制台的内容去关闭客户端
只能够先通知服务器,我们要关闭了后,由服务器返回消息给我们,主线程才可以关闭
*/
client.send(message);
if (EXIT.equals(message))
break;
System.out.println(message);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
READER.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}

Author: Fuyusakaiori
Link: http://example.com/2021/08/19/java/io/IO/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.