NIO+BIO+AIO. Intensive lecture on IO mode of Java tutorial. Dark horse follow-up (1)

NIO+BIO.Java tutorial IO mode intensive lecture. Dark horse follow-up

Chapter 1 BIO, NIO, AIO course introduction

1.1 Course Description

In Java software design and development, the communication architecture is inevitable. We need to use network communication-related technologies for data interaction between different systems or different processes, or in high-concurrency communication scenarios. For some For experienced programmers, Java's early network communication architecture had some flaws, the most irritating of which was the synchronous blocking I/O communication (BIO) based on low performance . Requirements, Java began to support non-blocking I/O communication technology (NIO) in 2002. When most readers learn network communication-related technologies, they only come into contact with fragmentary communication technology points, without a complete technical architecture, so that there is always no clear solution for Java communication scenarios. This course will start from the most basic BIO communication to NIO and AIO through a large number of clear and direct cases. Readers can clearly understand the phenomena, concepts and characteristics of blocking, synchronization and asynchrony, as well as their advantages and disadvantages. This course combines a large number of cases to allow readers to quickly understand the use of each communication architecture.

1.2 Learning requirements for this course

  • This course is not suitable for complete 0 basic students.
  • At least you need to master: Java SE basic programming, such as Java multi-threading, Java IO stream programming, Java network basics (such as: IP, port, protocol), common Java design patterns must have a certain understanding.
  • Proficiency in Java OOP programming, with a certain programming thinking.

1.3 Problems solved by communication technology as a whole

  • Communication requirements within the LAN.
  • The underlying message passing mechanism between multiple systems.
  • Under high concurrency, communication scenarios with large data volumes are required.
  • gaming industry. Whether it is a mobile game server or a large-scale online game, the Java language has been more and more widely used.

Chapter 2 Java's I/O Evolution Road

2.1 Basic description of I/O model

I/O model: What kind of channel or communication mode and architecture is used for data transmission and reception, which largely determines the performance of program communication. Java supports 3 network programming/IO models: BIO , Under the actual communication requirements of NIO and AIO
, different I/O models should be selected according to different business scenarios and performance requirements

2.2 I/O model

Java BIO

Synchronous and blocking (traditional blocking type), the server implementation mode is one thread per connection, that is, when the client has a connection request, the server needs to start a thread for processing. If the connection does not do anything, it will cause unnecessary thread overhead [ Simple schematic diagram]
insert image description here

Java NIO

Java NIO: Synchronous non-blocking , the server implementation mode is one thread to process multiple requests (connections), that is, the connection requests sent by the client will be registered on the multiplexer, and the multiplexer polls the connection with I/ O request is processed [simple diagram]
insert image description here

Java AIO

Java AIO (NIO.2): Asynchronous and non-blocking . The server implementation mode is one effective request and one thread. The client's I/O request is completed by the OS first and then notifies the server application to start the thread for processing. It is generally applicable to the number of connections. Many applications with long connection times.

2.3 Application scenario analysis of BIO, NIO and AIO

1. The BIO method is suitable for architectures with a relatively small and fixed number of connections. This method has relatively high requirements on server resources, and concurrency is limited to applications. It was the only choice before JDK1.4, but the program is simple and easy to understand.
2. The NIO method is suitable for architectures with a large number of connections and relatively short connections (light operation), such as chat servers, barrage systems, and communication between servers. Programming is more complicated, and JDK1.4 began to support it.

3. The AIO method is used in architectures with a large number of connections and long connections (heavy operations), such as photo album servers, which fully invoke the OS to participate in concurrent operations. The programming is more complicated, and JDK7 began to support it.

Chapter 3 JAVA BIO in-depth analysis

3.1 Basic Introduction of Java BIO

  • Java BIO is traditional java io programming, and its related classes and interfaces are in java.io
  • BIO(blocking I/O): Synchronous blocking, the server implementation mode is one thread per connection, that is, when the client has a connection request, the server needs to start a thread for processing. If the connection does not do anything, unnecessary threads will be created The overhead can be improved through the thread pool mechanism (implementing multiple clients connecting to the server).

3.2 Java BIO working mechanism

insert image description here
Sorting out the BIO programming process

  1. The server starts a ServerSocket , registers the port, and calls the accpet method to monitor the client's Socket connection.
  2. The client starts the Socket to communicate with the server. By default, the server needs to establish a thread for each client to communicate with it.

3.3 Review of traditional BIO programming examples

The basic model of network programming is the Client/Server model, that is, two processes communicate with each other, in which the server provides location information (binding IP address and port), and the client initiates a connection operation to the port address monitored by the server. The connection request is based on the three-way handshake connection under the TCP protocol. After the connection is successful, the two parties communicate through the network socket (Socket).

In the development of the traditional synchronous blocking model, the server-side ServerSocket is responsible for binding the IP address and starting the listening port; the client-side Socket is responsible for initiating the connection operation. After the connection is successful, the two parties perform synchronous blocking communication through the input and output streams.

Based on the communication in the BIO mode, the client-server is fully synchronized and fully coupled.

The client case is as follows

Create a new empty project: iomodule
insert image description here


insert image description here
Create a new Module: name bio-demo
insert image description here
Create a new client class
Client.java
insert image description here

Server.java

package com.itheima._02bio01;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
    目标: Socket网络编程。

    Java提供了一个包:java.net下的类都是用于网络通信。
    Java提供了基于套接字(端口)Socket的网络通信模式,我们基于这种模式就可以直接实现TCP通信。
    只要用Socket通信,那么就是基于TCP可靠传输通信。

    功能1:客户端发送一个消息,服务端接口一个消息,通信结束!!

    创建客户端对象:
        (1)创建一个Socket的通信管道,请求与服务端的端口连接。
        (2)从Socket管道中得到一个字节输出流。
        (3)把字节流改装成自己需要的流进行数据的发送
    创建服务端对象:
        (1)注册端口
        (2)开始等待接收客户端的连接,得到一个端到端的Socket管道
        (3)从Socket管道中得到一个字节输入流。
        (4)把字节输入流包装成自己需要的流进行数据的读取。

    Socket的使用:
        构造器:public Socket(String host, int port)
        方法:  public OutputStream getOutputStream():获取字节输出流
               public InputStream getInputStream() :获取字节输入流

    ServerSocket的使用:
        构造器:public ServerSocket(int port)

    小结:
        通信是很严格的,对方怎么发你就怎么收,对方发多少你就只能收多少!!

 */
public class ClientDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        System.out.println("==客户端的启动==");
        // (1)创建一个Socket的通信管道,请求与服务端的端口连接。
        Socket socket = new Socket("127.0.0.1",8888);
        // (2)从Socket通信管道中得到一个字节输出流。
        OutputStream os = socket.getOutputStream();
        // (3)把字节流改装成自己需要的流进行数据的发送
        PrintStream ps = new PrintStream(os);
        // (4)开始发送消息
        ps.println("我是客户端,我想约你吃小龙虾!!!");
        ps.flush();
    }
}

I write here as

public class Client {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;
        OutputStream outputStream = null;
        BufferedWriter bufferedWriter = null;
        try {
    
    
            // 1.创建Socket对象,请求服务端的连接
            socket = new Socket(InetAddress.getLocalHost(), 8888);
            // 2.从Socket对象中,获取一个字节输出流
            outputStream = socket.getOutputStream();
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
            bufferedWriter.write("今天天气不错");
            bufferedWriter.flush();

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 关闭流
            try {
    
    
                bufferedWriter.close();
                outputStream.close();
                socket.close();

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

The server case is as follows

Create a new server
insert image description here
Server: Server.java

public class Server {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        InputStreamReader inputStreamReader = null;
        try {
    
    
            // 1. 定义一个ServerSocket对象进行服务端端口的注册
            ServerSocket serverSocket = new ServerSocket(8888);
            // 2.监听客户端的Socket连接请求
            socket = serverSocket.accept();
            // 3.从Socket管道中得到一个字节输入流
            inputStream = socket.getInputStream();
            // 4.按照行序来读取数据,把字节输入流包装成一个缓冲字符输入流
            // 通过转换流,将缓冲字节输入流转换成缓冲字符输入流
            inputStreamReader = new InputStreamReader(inputStream);
            bufferedReader = new BufferedReader(inputStreamReader);
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                System.out.println("服务端接收到:" + msg);
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                bufferedReader.close();
                inputStreamReader.close();
                inputStream.close();
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Start the server first, then start the client, and the server receives the message.
insert image description here

summary

  • In the above communication, the server will always wait for the message from the client. If the client does not send the message, the server will always enter the blocking state .
  • At the same time, the server obtains messages according to the line, which means that the client must also send messages according to the line, otherwise the server will enter the blocking state of waiting for the message!

3.4 Multi-send and multi-receive messages in BIO mode

In the case of 1.3, the client can only send messages, and the server can receive messages , but cannot repeatedly receive and send messages. We only need to add the logic of repeatedly sending messages according to the line in the client case. That's it! The case code is as follows:

We create a new package demotwo, copy the above code, and modify
insert image description here

Client.java

The client code is as follows

package com.itheima._03bio02;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

/**
    目标: Socket网络编程。

    功能1:客户端可以反复发消息,服务端可以反复收消息

    小结:
        通信是很严格的,对方怎么发你就怎么收,对方发多少你就只能收多少!!

 */
ppublic static void main(String[] args) {
    
    
        Socket socket = null;
        OutputStream outputStream = null;
        PrintStream printStream = null;
        Scanner scanner = null;
        try {
    
    
            // 1.创建Socket对象,请求服务端的连接
            socket = new Socket(InetAddress.getLocalHost(), 8888);
            // 2.从Socket对象中,获取一个字节输出流
            outputStream = socket.getOutputStream();
            printStream = new PrintStream(outputStream);
            while (true) {
    
    
                System.out.println("请输入:");
                scanner = new Scanner(System.in);
                String str = scanner.nextLine();
                printStream.println(str);
                printStream.flush();
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 关闭流
            try {
    
    
                printStream.close();
                scanner.close();
                outputStream.close();
                socket.close();

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

The server code is as follows

Server.java

package com.itheima._03bio02;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服务端
 */
public class ServerDemo {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        InputStreamReader inputStreamReader = null;
        try {
    
    
            // 1. 定义一个ServerSocket对象进行服务端端口的注册
            ServerSocket serverSocket = new ServerSocket(8888);
            // 2.监听客户端的Socket连接请求
            socket = serverSocket.accept();
            // 3.从Socket管道中得到一个字节输入流
            inputStream = socket.getInputStream();
            // 4.按照行序来读取数据,把字节输入流包装成一个缓冲字符输入流
            // 通过转换流,将缓冲字节输入流转换成缓冲字符输入流
            inputStreamReader = new InputStreamReader(inputStream);
            bufferedReader = new BufferedReader(inputStreamReader);
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                System.out.println("服务端接收到:" + msg);
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                bufferedReader.close();
                inputStreamReader.close();
                inputStream.close();
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Run the server first, then the client.
Messages sent on the client side
insert image description here
Messages received on the server side
insert image description here

summary

  • In this case, it is true that the client can send and receive more
  • But the server can only handle one client request, because the server is single-threaded. Messages can only be communicated with one client at a time.

3.5 Receive multiple clients in BIO mode

In the original mode, we try to implement a server and multiple clients. Start the server first
insert image description here
and then start the client. Here you need to configure multiple clients:
Click Edit Configurations ,
insert image description here
click Modefiy options ,
insert image description here
check Allow multiple instances
insert image description here
and start here One client
insert image description here
Here we use the first client to send a message,
insert image description here
and then we use the second client to send a message.
insert image description here
We find that the server only receives the message sent by the first client, but not the second client. The
insert image description here
reason is: There is only one thread on the server side, and only one client's message will be processed.

overview

In the above case, a server can only receive communication requests from one client, so how should the server handle message communication requests from many clients? At this point, we need to introduce threads on the server, that is It is said that every time the client initiates a request, the server creates a new thread to process the client's request, thus realizing a client-one-thread model. The diagram mode is as follows: Let's introduce the thread to see: Create a new package
insert image description here
, Then copy the previous class
insert image description here

The client case code is as follows

Client

/**
    目标: Socket网络编程。

    功能1:客户端可以反复发,一个服务端可以接收无数个客户端的消息!!

    小结:
         服务器如果想要接收多个客户端,那么必须引入线程,一个客户端一个线程处理!!

 */
public class Client {
    
    
     public static void main(String[] args) {
    
    
        Socket socket = null;
        OutputStream outputStream = null;
        PrintStream printStream = null;
        Scanner scanner = null;
        try {
    
    
            // 1.创建Socket对象,请求服务端的连接
            socket = new Socket(InetAddress.getLocalHost(), 8888);
            // 2.从Socket对象中,获取一个字节输出流
            outputStream = socket.getOutputStream();
            printStream = new PrintStream(outputStream);
            // 3.使用循环不断发送消息给服务端
            while (true) {
    
    
                System.out.println("请输入:");
                scanner = new Scanner(System.in);
                String str = scanner.nextLine();
                printStream.println(str);
                printStream.flush();
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 关闭流
            try {
    
    
                printStream.close();
                scanner.close();
                outputStream.close();
                socket.close();

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

The server case code is as follows

Server

/**
 * @ClassName: Server服务端 实现多收取 引入线程
 * 思路:服务端每接收到一个客户端Socket的请求后,都交给一个独立的线程处理客户端的数据交互
 * @Description:
 * @Author: wty
 * @Date: 2023/4/4
 */
public class Server {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;

        try {
    
    
            // 1. 定义一个ServerSocket对象进行服务端端口的注册
            ServerSocket serverSocket = new ServerSocket(8888);
            // 2.定义一个死循环,监听客户端的Socket连接请求
            while (true) {
    
    
                socket = serverSocket.accept();
                // 3.创建一个独立的线程来处理与这个客户端的Socket的通信需求
                new ServerThreadReader(socket).start();
            }

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

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

Create a new thread class
insert image description here
ServerThreadReader.java

public class ServerThreadReader extends Thread {
    
    
    private Socket socket;

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

    @Override
    public void run() {
    
    
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        try {
    
    
            // 3.从Socket管道中得到一个字节输入流
            inputStream = socket.getInputStream();
            // 4.按照行序来读取数据,把字节输入流包装成一个缓冲字符输入流
            // 通过转换流,将缓冲字节输入流转换成缓冲字符输入流
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                System.out.println("服务端接收到:" + msg);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                bufferedReader.close();
                inputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Run the server first, then run the client
insert image description here
Client 1 sends a message
insert image description here
Client 2 sends a message
insert image description here
Server receives a message
insert image description here

summary

  • 1. When each Socket is received, a thread will be created. Thread competition and context switching affect performance;
  • 2. Each thread will occupy stack space and CPU resources;
  • 3. Not every socket performs IO operations, meaningless thread processing;
  • 4. When the concurrent access of the client increases. The server will have a thread overhead of 1:1. The greater the number of visits, the system will overflow the thread stack, and the thread creation will fail, which will eventually cause the process to crash or freeze, so that it cannot provide external services.

3.6 Pseudo-asynchronous I/O programming

overview

​ In the above case: when the concurrent access of the client increases. The server will have a thread overhead of 1:1. The greater the number of visits, the system will overflow the thread stack, and the thread creation will fail, which will eventually cause the process to crash or freeze, so that it cannot provide external services.

​ Next, we adopt a pseudo-asynchronous I/O communication framework, which is implemented by thread pool and task queue . When the client accesses, the client's Socket is encapsulated into a Task (this task implements the java.lang.Runnable thread task interface ) to the backend thread pool for processing. JDK's thread pool maintains a message queue and N active threads to process Socket tasks in the message queue. Since the thread pool can set the size of the message queue and the maximum number of threads, its resource occupation is controllable, no matter How many clients access concurrently will not cause resource exhaustion and downtime.

The diagram is as follows:
insert image description here
Create a demofour package and copy the class of demotwo
insert image description here

Client source code analysis

public class Client {
    
    

    /**
     * 通过PrintStream 伪异步IO编程
     *
     * @param args
     */
    public static void main(String[] args) {
    
    
        Socket socket = null;
        OutputStream outputStream = null;
        PrintStream printStream = null;
        Scanner scanner = null;
        try {
    
    
            // 1.创建Socket对象,请求服务端的连接
            socket = new Socket(InetAddress.getLocalHost(), 8888);
            // 2.从Socket对象中,获取一个字节输出流
            outputStream = socket.getOutputStream();
            printStream = new PrintStream(outputStream);
            while (true) {
    
    
                System.out.println("请输入:");
                scanner = new Scanner(System.in);
                String str = scanner.nextLine();
                printStream.println(str);
                printStream.flush();
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 关闭流
            try {
    
    
                printStream.close();
                scanner.close();
                outputStream.close();
                socket.close();

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

Thread pool processing class

insert image description here
HandlerSocketServerPool

// 线程池处理类
public class HandlerSocketThreadPool {
    
    
   
   // 线程池 
   private ExecutorService executor;
   
   public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
    
    
      
      this.executor = new ThreadPoolExecutor(
            3, // 8
            maxPoolSize,  
            120L, 
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(queueSize) );
   }
   
   public void execute(Runnable task){
    
    
      this.executor.execute(task);
   }
}

Server source code analysis

Server.java

public class Server {
    
    
   public static void main(String[] args) {
    
    
      try {
    
    
         System.out.println("----------服务端启动成功------------");
         ServerSocket ss = new ServerSocket(9999);

         // 一个服务端只需要对应一个线程池
         HandlerSocketThreadPool handlerSocketThreadPool =
               new HandlerSocketThreadPool(3, 1000);

         // 客户端可能有很多个
         while(true){
    
    
            Socket socket = ss.accept() ; // 阻塞式的!
            System.out.println("有人上线了!!");
            // 每次收到一个客户端的socket请求,都需要为这个客户端分配一个
            // 独立的线程 专门负责对这个客户端的通信!!
            handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
         }

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

}

Create ServerRunnableTarget.java for processing tasks inside the server-side thread.
insert image description here

public class ServerRunnableTarget implements Runnable {
    
    
    private Socket socket = null;

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

    @Override
    public void run() {
    
    
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        try {
    
    
            System.out.println("=====服务端启动=====");
            // 3.从Socket管道中得到一个字节输入流
            inputStream = socket.getInputStream();
            // 4.按照行序来读取数据,把字节输入流包装成一个缓冲字符输入流
            // 通过转换流,将缓冲字节输入流转换成缓冲字符输入流
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                System.out.println("服务端接收到:" + msg);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                bufferedReader.close();
                inputStream.close();
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Start the client first, then start the server
Client 1
insert image description here
Client 2
insert image description here
Client 3
insert image description here
Client 4
insert image description here

Then we looked at the server side
insert image description here
and found that the server side only received 3 requests. The reason is that we set the number of core threads to 3, and the remaining client 4 went to the blocking queue to wait in line.
Then we turn off client 1
insert image description here
and look at the server again, and find that the request from client 4 has been received
insert image description here

summary

  • Pseudo-asynchronous io is implemented using a thread pool, so it avoids the problem of exhausting thread resources caused by creating an independent thread for each request, but because the bottom layer is still using the synchronous blocking model, it cannot fundamentally solve the problem.
  • If a single message is processed slowly, or all threads in the server thread pool are blocked, then subsequent socket I/O messages will be queued in the queue. New Socket requests will be rejected, and a large number of connection timeouts will occur on the client side.

3.7 File upload based on BIO format

New package file
insert image description here

Target

Supports uploading of any type of file.

client development

package com.itheima.file;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

/**
    目标:实现客户端上传任意类型的文件数据给服务端保存起来。

 */
public class Client {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;
        DataOutputStream dataOutputStream = null;
        FileInputStream fileInputStream = null;
        try {
    
    
            // 1. 请求与服务端的连接Socket
            socket = new Socket(InetAddress.getLocalHost(), 8888);

            // 2.把字节输出流包装成一个数据输出流
            dataOutputStream = new DataOutputStream(socket.getOutputStream());
            // 3.先发送上传文件的后缀给客户端
            dataOutputStream.writeUTF(".PNG");
            // 4.把文件数据发送给服务端进行接收
            fileInputStream = new FileInputStream("D:\\2.PNG");
            byte[] bytes = new byte[1024];
            int length = 0;
            while ((length = fileInputStream.read(bytes)) > 0) {
    
    
                dataOutputStream.write(bytes, 0, length);
                
            }
            dataOutputStream.flush();

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                dataOutputStream.close();
                fileInputStream.close();
                socket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

server development

package com.itheima.file;

import java.net.ServerSocket;
import java.net.Socket;

/**
    目标:服务端开发,可以实现接收客户端的任意类型文件,并保存到服务端磁盘。
 */
public class Server {
    
    
    public static void main(String[] args) {
    
    
        ServerSocket serverSocket = null;
        Socket socket = null;
        try {
    
    
            serverSocket = new ServerSocket(8888);
            while (true) {
    
    
                socket = serverSocket.accept();
                // 交给一个独立的线程来处理与这个客户端的文件通信需求
                ServerReaderThread serverReaderThread = new ServerReaderThread(socket);
                serverReaderThread.start();

            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Create a new class to open the thread ServerReaderThread.java
insert image description here

/**
 * @ClassName: ServerReaderThread
 * @Description:
 * @Author: wty
 * @Date: 2023/4/5
 */

public class ServerReaderThread extends Thread {
    
    
    private Socket socket;

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

    @Override
    public void run() {
    
    
        DataInputStream dataInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
    
    
            // 1.得到一个数据输入流读取客户端发送的数据
            dataInputStream = new DataInputStream(socket.getInputStream());
            // 2.读取客户端发送过来的文件类型
            String suffix = dataInputStream.readUTF();
            System.out.println("服务端接收到了文件,文件类型" + suffix);
            String fileName = UUID.randomUUID().toString();
            // 3.定义一个字节输出管道负责把客户端发来的文件写进去
            fileOutputStream = new FileOutputStream("D:\\server\\" + fileName + suffix);
            // 4.从数据输入流中读取文件数据,写出到字节输出流中去
            byte[] bytes = new byte[1024];
            int length = 0;
            while ((length = dataInputStream.read(bytes)) > 0) {
    
    
                fileOutputStream.write(bytes, 0, length);
            }

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                dataInputStream.close();
                fileOutputStream.close();
                System.out.println("文件保存成功");
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Start the server first, and then start the client. We
see that the IDEA background tells us that the file is saved successfully.
insert image description here
Let's go to the new file
insert image description here
and try to open it. It turns out to be damaged. As
insert image description here
a solution, we only need to add it to the Client.

socket.shutdownOutput();

insert image description here

Let’s try to send it again, this time it’s successful . Let’s try another txt file,
insert image description here
modify Client.
insert image description here

insert image description here

summary

How the client sends, how the server receives

3.9 Port forwarding idea in Java BIO mode

Requirements: It is necessary to realize that a client's message can be sent to all clients to receive. (Group chat implementation)
insert image description here
Create a new Module
Name: bio_chat_demo
insert image description here
to create a package.
insert image description here
The project structure is as follows:
insert image description here

client development

Client

/**
 * @ClassName: Client
 * 目标:实现客户端的开发
 * <p>
 * 基本思路:
 * 1、客户端发送消息给服务端
 * 2、客户端可能还需要接收服务端发送过来的消息
 * @Description:
 * @Author: wty
 * @Date: 2023/4/5
 */

public class Client {
    
    
    public static void main(String[] args) {
    
    
        Socket socket = null;
        PrintStream printStream = null;
        try {
    
    
            // 1、创建于服务端的Socket链接
            socket = new Socket(InetAddress.getLocalHost(), 8888);
            // 2、分配一个线程为客户端socket服务接收服务端发来的消息
            new ClientReaderThread(socket).start();

            printStream = new PrintStream(socket.getOutputStream());
            System.out.println("请输入内容:");
            while (true) {
    
    
                Scanner scanner = new Scanner(System.in);
                String str = scanner.nextLine();
                printStream.println(str);
                printStream.flush();
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            /*try {
                printStream.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }*/
        }
    }
}

Client thread task ServerReaderThread.java

public class ServerReaderThread extends Thread {
    
    
    private Socket socket = null;

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

    @Override
    public void run() {
    
    
        BufferedReader bufferedReader = null;
        try {
    
    
            // 1.从socket中获取当前客户端的输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 2.按照行,读取消息
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                // 服务端接收到消息后,要推送给所有的在线socket
                sendMsgToAllClient(msg);
            }
        } catch (IOException e) {
    
    
            System.out.println("当前有人下线");
            // 下线后,从在线集合中移除当前下线用户
            Server.allSocketOnline.remove(this.socket);
        } finally {
    
    
           /* try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }*/
        }
    }

    /**
     * 把当前服务端接收到的消息,推送给所有在线的客户端socket
     *
     * @param msg
     */
    private void sendMsgToAllClient(String msg) {
    
    
        PrintStream printStream = null;
        try {
    
    
            List<Socket> socketOnlines = Server.allSocketOnline;
            for (Socket socketOnline : socketOnlines) {
    
    
                // 如果当前客户端是自己,就没必要发送了
                if (socketOnline.equals(this.socket)) {
    
    
                    continue;
                }
                // 相当于在把消息写入管道,推送给所有在线客户端
                printStream = new PrintStream(socketOnline.getOutputStream());
                printStream.println(Thread.currentThread().getName() + "群发消息:" + msg);
                printStream.flush();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //printStream.close();
        }
    }
}

Server implementation

Server

/**
 * @ClassName: Server BIO模式下的端口转发思想-服务端实现
 * 服务端实现的需求:
 * 1.注册端口
 * 2. 接收客户端的socket连接,交给一个独立的线程来处理
 * 3.把当前连接的客户端socket存入到一个所谓的在线socket集合中保存
 * 4.接收客户端的消息后,推送给当前所有在线的socket接收
 * @Description:
 * @Author: wty
 * @Date: 2023/4/5
 */

public class Server {
    
    
    public static List<Socket> allSocketOnline = new ArrayList<>();

    public static void main(String[] args) {
    
    
        ServerSocket serverSocket = null;
        Socket socket = null;
        try {
    
    
            // 1.注册端口
            serverSocket = new ServerSocket(8888);
            // 2.定义一个循环
            while (true) {
    
    
                socket = serverSocket.accept();
                // 3.把登录的所有客户端存入到一个在线集合中去
                System.out.println("服务端正在监听……");
                allSocketOnline.add(socket);
                // 4.为当前登录成功的socket分配一个独立的线程来处理与之通信
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            /*try {
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }*/
        }
    }
}

Create class ServerReaderThread.java to handle thread logic

/**
 * @ClassName: ServerReaderThread
 * @Description:
 * @Author: wty
 * @Date: 2023/4/5
 */
public class ServerReaderThread extends Thread {
    
    
    private Socket socket = null;

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

    @Override
    public void run() {
    
    
        BufferedReader bufferedReader = null;
        try {
    
    
            // 1.从socket中获取当前客户端的输入流
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 2.按照行,读取消息
            String msg = "";
            while ((msg = bufferedReader.readLine()) != null) {
    
    
                // 服务端接收到消息后,要推送给所有的在线socket
                sendMsgToAllClient(msg);
            }
        } catch (IOException e) {
    
    
            System.out.println("当前有人下线");
            // 下线后,从在线集合中移除当前下线用户
            Server.allSocketOnline.remove(this.socket);
        } finally {
    
    
           /* try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }*/
        }
    }

    /**
     * 把当前服务端接收到的消息,推送给所有在线的客户端socket
     *
     * @param msg
     */
    private void sendMsgToAllClient(String msg) {
    
    
        PrintStream printStream = null;
        try {
    
    
            List<Socket> socketOnlines = Server.allSocketOnline;
            for (Socket socketOnline : socketOnlines) {
    
    
                // 如果当前客户端是自己,就没必要发送了
                if (socketOnline.equals(this.socket)) {
    
    
                    continue;
                }
                // 相当于在把消息写入管道,推送给所有在线客户端
                printStream = new PrintStream(socketOnline.getOutputStream());
                printStream.println(Thread.currentThread().getName() + "群发消息:" + msg);
                printStream.flush();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //printStream.close();
        }
    }
}

Note: Group chat is implemented here to ensure that everyone is chatting all the time, so don't close the stream
Client 1
insert image description here
Client 2
insert image description here
Client 3
insert image description here
Server
insert image description here

summary

3.10 Instant messaging based on BIO mode

Based on instant messaging in BIO mode, we need to solve client-to-client communication, that is, we need to implement port message forwarding logic between client and client.

Project function demo

Project case description

This project case is an instant messaging software project, which is suitable for a large case of strengthening the foundation and is comprehensive. To learn the case of this project, at least the following Java SE technology points are required:

    1. Java object-oriented design, syntax design.
    1. multi-threading technology.
    1. IO stream technology.
    1. Network communication related technologies.
    1. collection frame.
    1. Project development thinking.
    1. Commonly used APIs in Java.

A brief description of the function list:

1. Client login function

  • You can start the client to log in, and you only need to enter the user name and server ip address to log in.

2. The number of people online is updated in real time.

  • After the client user logs in, the contact information columns of all clients need to be updated synchronously.

3. Update the number of offline users

  • After detecting that a client is offline, the contact information columns of all clients need to be updated synchronously.

4. Group chat

  • Messages from any client can be pushed to all current clients to receive.

5. Private chat

  • You can select an employee, click the private chat button, and then the message sent can be received by the client alone.

6.@message

  • An employee can be selected and messages sent can @ that user, but everyone else can

7. Message users and message time points

  • The server can record the user's message time point in real time, and then perform multi-channel forwarding or selection of the message.

Project Kickoff and Demonstration

Demonstration of project code structure.
insert image description here

Project start steps:

  • 1. First, you need to start the server. Click the ServerChat class and right-click to start it. It will show that the server has started successfully!

  • 2. Secondly, click on the client class ClientChat, and enter the IP of the server and the nickname of the current client in the pop-up box
    insert image description here

  • 3. After logging in, the chat interface is as follows, and you can perform related operations.

    • If you click send directly, the group chat message will be sent by default
  • If you select a user in the online list on the right, @message will be sent by default

    • If you select a user in the online list on the right, and then select the private chat button on the lower right, the private chat message will be sent by default.

insert image description here
insert image description here

Technology selection analysis

The case of this project involves the case of strengthening the Java foundation, and the specific technical points involved are as follows:

    1. Java object-oriented design, syntax design.
    1. multi-threading technology.
    1. IO stream technology.
    1. Network communication related technologies.
    1. collection frame.
    1. Project development thinking.
    1. Commonly used APIs in Java.

server design

The server receives multiple client logic

Target

The server needs to receive access from multiple clients.

Implementation steps
  • 1. The server needs to receive multiple clients. The current strategy we adopt is that one client corresponds to one server thread.
  • 2. In addition to registering the port, the server also needs to allocate an independent thread for each client to communicate with it.
Code
  • The main body code of the server mainly performs port registration, receives the client, and allocates a thread to process the client request
public class ServerChat {
    
    
    
    /** 定义一个集合存放所有在线的socket  */
	public static Map<Socket, String> onLineSockets = new HashMap<>();

   public static void main(String[] args) {
    
    
      try {
    
    
         /** 1.注册端口   */
         ServerSocket serverSocket = new ServerSocket(Constants.PORT);

         /** 2.循环一直等待所有可能的客户端连接 */
         while(true){
    
    
            Socket socket = serverSocket.accept();
            /**3. 把客户端的socket管道单独配置一个线程来处理 */
            new ServerReader(socket).start();
         }
      } catch (Exception e) {
    
    
         e.printStackTrace();
      }
   }
}
  • The independent thread class assigned by the server is responsible for processing the pipeline request of the client Socket.
class ServerReader extends Thread {
    
    
   private Socket socket;
   public ServerReader(Socket socket) {
    
    
      this.socket = socket;
   }
   @Override
   public void run() {
    
    
      try {
    
    
       
      } catch (Exception e) {
    
    
            e.printStackTrace();
      }
   }
}

The constant package is responsible for port configuration

public class Constants {
    
    
   /** 常量 */
   public static final int PORT = 7778 ;

}
summary

​ This section realizes that the server can receive multiple client requests.

The server receives login messages and monitors offline

Target

In the previous section, we realized that the server can receive multiple clients, and then the server can receive multiple client connections. Next, we need to receive the login message from the client.

Implementation steps
  • The login message of the client's thread needs to be processed on the server side.
  • It should be noted that there may be many kinds of messages that the server needs to receive from the client.
    • They are login messages, group chat messages, private chat messages and @messages.
    • Here it is necessary to agree that if the client needs to send the message type before sending the message, we use the signal value flag (1, 2, 3) for the type.
      • 1 represents receiving a login message
      • 2 represents group sending | @ message
      • 3 represents private chat messages
  • There is an exception verification mechanism in the thread of the server. Once the client is found to be offline, it will be processed in the exception mechanism, and then the current client user will be removed, and the latest user list will be sent back to all clients to update the number of online users.
Code
public class ServerReader extends Thread {
    
    
	private Socket socket;
	public ServerReader(Socket socket) {
    
    
		this.socket = socket;
	}

	@Override
	public void run() {
    
    
		DataInputStream dis = null;
		try {
    
    
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循环一直等待客户端的消息 */
			while(true){
    
    
				/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
    
    
					/** 先将当前登录的客户端socket存到在线人数的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
    
    
			System.out.println("--有人下线了--");
			// 从在线人数中将当前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
    
    
				// 从新更新在线人数并发给所有客户端 
				writeMsg(1,dis);
			} catch (Exception e1) {
    
    
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
    
    
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定义一个变量存放最终的消息形式 
		String msg = null ;
		if(flag == 1){
    
    
			/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判断是否存在在线人数 
			if(onlineNames != null && onlineNames.size() > 0){
    
    
				for(String name : onlineNames){
    
    
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最后的一个分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息发送给所有的客户端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
    
    
			
			}
		}
	}
	
	private void sendMsgToAll(int flag, String msg) throws Exception {
    
    
		// 拿到所有的在线socket管道 给这些管道写出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
    
    
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息类型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
summary
  • Here, the login message of the client is received, and then all the currently online user names and the currently logged in user names are extracted and sent to all online users to update their online number list.

The server receives group chat messages

Target

In the previous section, the login message of the client is received, and then all the currently online user names and the currently logged in user names are extracted and sent to all online users to update their online number list. Next, we need to receive the group chat message sent by the client and push it to all clients who are currently online

Implementation steps
  • The next step is to receive group chat messages from the client.
  • It should be noted that there may be many kinds of messages that the server needs to receive from the client.
    • They are login messages, group chat messages, private chat messages and @messages.
    • Here it is necessary to agree that if the client needs to send the message type before sending the message, we use the signal value flag (1, 2, 3) for the type.
      • 1 represents receiving a login message
      • 2 represents group sending | @ message
      • 3 represents private chat messages
Code
public class ServerReader extends Thread {
    
    
	private Socket socket;
	public ServerReader(Socket socket) {
    
    
		this.socket = socket;
	}

	@Override
	public void run() {
    
    
		DataInputStream dis = null;
		try {
    
    
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循环一直等待客户端的消息 */
			while(true){
    
    
				/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
    
    
					/** 先将当前登录的客户端socket存到在线人数的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
    
    
			System.out.println("--有人下线了--");
			// 从在线人数中将当前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
    
    
				// 从新更新在线人数并发给所有客户端 
				writeMsg(1,dis);
			} catch (Exception e1) {
    
    
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
    
    
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定义一个变量存放最终的消息形式 
		String msg = null ;
		if(flag == 1){
    
    
			/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判断是否存在在线人数 
			if(onlineNames != null && onlineNames.size() > 0){
    
    
				for(String name : onlineNames){
    
    
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最后的一个分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息发送给所有的客户端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
    
    
			// 读到消息  群发的 或者 @消息
			String newMsg = dis.readUTF() ; // 消息
			// 得到发件人 
			String sendName = ServerChat.onLineSockets.get(socket);
	
			// 内容
			StringBuilder msgFinal = new StringBuilder();
			// 时间  
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
			if(flag == 2){
    
    
				msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				sendMsgToAll(flag,msgFinal.toString());
			}else if(flag == 3){
    
    
	
			}
		}
	}
	
	private void sendMsgToAll(int flag, String msg) throws Exception {
    
    
		// 拿到所有的在线socket管道 给这些管道写出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
    
    
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息类型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
summary
  • Here, according to the type of the message, it is judged as a group chat message, and then the group chat message is pushed to all currently online clients.

The server receives private chat messages

Target

In the previous section, we received the group chat message sent by the client and pushed it to all the currently online clients. Next, we need to solve the push logic of the private chat message

Implementation steps
  • Solve the push logic of private chat messages, private chat messages need to know to push to a specific client
  • We can receive the private chat user name sent by the client, locate the user's Socket pipe according to the user name, and then push a message to the Socket pipe separately.
  • It should be noted that there may be many kinds of messages that the server needs to receive from the client.
    • They are login messages, group chat messages, private chat messages and @messages.
    • Here it is necessary to agree that if the client needs to send the message type before sending the message, we use the signal value flag (1, 2, 3) for the type.
      • 1 represents receiving a login message
      • 2 represents group sending | @ message
      • 3 represents private chat messages
Code
public class ServerReader extends Thread {
    
    
	private Socket socket;
	public ServerReader(Socket socket) {
    
    
		this.socket = socket;
	}

	@Override
	public void run() {
    
    
		DataInputStream dis = null;
		try {
    
    
			dis = new DataInputStream(socket.getInputStream());
			/** 1.循环一直等待客户端的消息 */
			while(true){
    
    
				/** 2.读取当前的消息类型 :登录,群发,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
    
    
					/** 先将当前登录的客户端socket存到在线人数的socket集合中   */
					String name = dis.readUTF() ;
					System.out.println(name+"---->"+socket.getRemoteSocketAddress());
					ServerChat.onLineSockets.put(socket, name);
				}
				writeMsg(flag,dis);
			}
		} catch (Exception e) {
    
    
			System.out.println("--有人下线了--");
			// 从在线人数中将当前socket移出去  
			ServerChat.onLineSockets.remove(socket);
			try {
    
    
				// 从新更新在线人数并发给所有客户端 
				writeMsg(1,dis);
			} catch (Exception e1) {
    
    
				e1.printStackTrace();
			}
		}

	}

	private void writeMsg(int flag, DataInputStream dis) throws Exception {
    
    
        // DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 
		// 定义一个变量存放最终的消息形式 
		String msg = null ;
		if(flag == 1){
    
    
			/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
			/** onlineNames = [波仔,zhangsan,波妞]*/
			StringBuilder rs = new StringBuilder();
			Collection<String> onlineNames = ServerChat.onLineSockets.values();
			// 判断是否存在在线人数 
			if(onlineNames != null && onlineNames.size() > 0){
    
    
				for(String name : onlineNames){
    
    
					rs.append(name+ Constants.SPILIT);
				}
				// 波仔003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣波妞003197♣♣㏘♣④④♣
				// 去掉最后的一个分隔符 
				msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

				/** 将消息发送给所有的客户端 */
				sendMsgToAll(flag,msg);
			}
		}else if(flag == 2 || flag == 3){
    
    
			// 读到消息  群发的 或者 @消息
			String newMsg = dis.readUTF() ; // 消息
			// 得到发件人 
			String sendName = ServerChat.onLineSockets.get(socket);
	
			// 内容
			StringBuilder msgFinal = new StringBuilder();
			// 时间  
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
			if(flag == 2){
    
    
				msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				sendMsgToAll(flag,msgFinal.toString());
			}else if(flag == 3){
    
    
			msgFinal.append(sendName).append("  ").append(sdf.format(System.currentTimeMillis())).append("对您私发\r\n");
				msgFinal.append("    ").append(newMsg).append("\r\n");
				// 私发 
				// 得到给谁私发 
				String destName = dis.readUTF();
				sendMsgToOne(destName,msgFinal.toString());
			}
		}
	}
	/**
	 * @param destName 对谁私发 
	 * @param msg 发的消息内容 
	 * @throws Exception
	 */
	private void sendMsgToOne(String destName, String msg) throws Exception {
    
    
		// 拿到所有的在线socket管道 给这些管道写出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
    
    
			// 得到当前需要私发的socket 
			// 只对这个名字对应的socket私发消息
			if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){
    
    
				DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
				dos.writeInt(2); // 消息类型
				dos.writeUTF(msg);
				dos.flush();
			}
		}

	}
	

	private void sendMsgToAll(int flag, String msg) throws Exception {
    
    
		// 拿到所有的在线socket管道 给这些管道写出消息
		Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
		for(Socket sk :  allOnLineSockets){
    
    
			DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
			dos.writeInt(flag); // 消息类型
			dos.writeUTF(msg);
			dos.flush();
		}
	}
}
summary
  • In this section, we have solved the push logic of private chat messages. Private chat messages need to be pushed to a specific client socket channel
  • We can receive the private chat user name sent by the client, locate the user's Socket pipe according to the user name, and then push a message to the Socket pipe separately.

client design

Start the client interface, log in, refresh online

Target

Start the client interface , log in, and refresh the list of online users

Implementation steps
  • The client interface is mainly GUI design, and the main page is divided into a login interface, a chat window, and a list of online users.
  • GUI interface readers can copy and use by themselves.
  • Login After entering the server ip and user name, it is necessary to request a login with the server, and then immediately assign a read thread to the current client to process the client's read data message. Because the client may receive various instant message information forwarded from the server at any time.
  • After the client login is completed, the server will immediately send the latest user list to the client after receiving the login username.
Code

Client body code:

public class ClientChat implements ActionListener {
    
    
   /** 1.设计界面  */
   private JFrame win = new JFrame();
   /** 2.消息内容框架 */
   public JTextArea smsContent =new JTextArea(23 , 50);
   /** 3.发送消息的框  */
   private JTextArea smsSend = new JTextArea(4,40);
   /** 4.在线人数的区域  */
   /** 存放人的数据 */
   /** 展示在线人数的窗口 */
   public JList<String> onLineUsers = new JList<>();

   // 是否私聊按钮
   private JCheckBox isPrivateBn = new JCheckBox("私聊");
   // 消息按钮
   private JButton sendBn  = new JButton("发送");

   // 登录界面
   private JFrame loginView;

   private JTextField ipEt , nameEt , idEt;

   private Socket socket ;

   public static void main(String[] args) {
    
    
      new ClientChat().initView();

   }

   private void initView() {
    
    
      /** 初始化聊天窗口的界面 */
      win.setSize(650, 600);

      /** 展示登录界面  */
      displayLoginView();

      /** 展示聊天界面 */
      //displayChatView();

   }

   private void displayChatView() {
    
    

      JPanel bottomPanel = new JPanel(new BorderLayout());
      //-----------------------------------------------
      // 将消息框和按钮 添加到窗口的底端
      win.add(bottomPanel, BorderLayout.SOUTH);
      bottomPanel.add(smsSend);
      JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
      btns.add(sendBn);
      btns.add(isPrivateBn);
      bottomPanel.add(btns, BorderLayout.EAST);
      //-----------------------------------------------
      // 给发送消息按钮绑定点击事件监听器
      // 将展示消息区centerPanel添加到窗口的中间
      smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
      // 让展示消息区可以滚动。
      win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
      smsContent.setEditable(false);
      //-----------------------------------------------
      // 用户列表和是否私聊放到窗口的最右边
      Box rightBox = new Box(BoxLayout.Y_AXIS);
      onLineUsers.setFixedCellWidth(120);
      onLineUsers.setVisibleRowCount(13);
      rightBox.add(new JScrollPane(onLineUsers));
      win.add(rightBox, BorderLayout.EAST);
      //-----------------------------------------------
      // 关闭窗口退出当前程序
      win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      win.pack();  // swing 加上这句 就可以拥有关闭窗口的功能
      /** 设置窗口居中,显示出来  */
      setWindowCenter(win,650,600,true);
      // 发送按钮绑定点击事件
      sendBn.addActionListener(this);
   }

   private void displayLoginView(){
    
    

      /** 先让用户进行登录
       *  服务端ip
       *  用户名
       *  id
       *  */
      /** 显示一个qq的登录框     */
      loginView = new JFrame("登录");
      loginView.setLayout(new GridLayout(3, 1));
      loginView.setSize(400, 230);

      JPanel ip = new JPanel();
      JLabel label = new JLabel("   IP:");
      ip.add(label);
      ipEt = new JTextField(20);
      ip.add(ipEt);
      loginView.add(ip);

      JPanel name = new JPanel();
      JLabel label1 = new JLabel("姓名:");
      name.add(label1);
      nameEt = new JTextField(20);
      name.add(nameEt);
      loginView.add(name);

      JPanel btnView = new JPanel();
      JButton login = new JButton("登陆");
      btnView.add(login);
      JButton cancle = new JButton("取消");
      btnView.add(cancle);
      loginView.add(btnView);
      // 关闭窗口退出当前程序
      loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setWindowCenter(loginView,400,260,true);

      /** 给登录和取消绑定点击事件 */
      login.addActionListener(this);
      cancle.addActionListener(this);

   }

   private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
    
    
      /** 得到所在系统所在屏幕的宽高 */
      Dimension ds = frame.getToolkit().getScreenSize();

      /** 拿到电脑的宽 */
      int width1 = ds.width;
      /** 高 */
      int height1 = ds.height ;

      System.out.println(width1 +"*" + height1);
      /** 设置窗口的左上角坐标 */
      frame.setLocation(width1/2 - width/2, height1/2 -height/2);
      frame.setVisible(flag);
   }

   @Override
   public void actionPerformed(ActionEvent e) {
    
    
      /** 得到点击的事件源 */
      JButton btn = (JButton) e.getSource();
      switch(btn.getText()){
    
    
         case "登陆":
            String ip = ipEt.getText().toString();
            String name = nameEt.getText().toString();
            // 校验参数是否为空
            // 错误提示
            String msg = "" ;
            // 12.1.2.0
            // \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\
            if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
    
    
               msg = "请输入合法的服务端ip地址";
            }else if(name==null || !name.matches("\\S{1,}")){
    
    
               msg = "姓名必须1个字符以上";
            }

            if(!msg.equals("")){
    
    
               /** msg有内容说明参数有为空 */
               // 参数一:弹出放到哪个窗口里面
               JOptionPane.showMessageDialog(loginView, msg);
            }else{
    
    
               try {
    
    
                  // 参数都合法了
                  // 当前登录的用户,去服务端登陆
                  /** 先把当前用户的名称展示到界面 */
                  win.setTitle(name);
                  // 去服务端登陆连接一个socket管道
                  socket = new Socket(ip, Constants.PORT);

                  //为客户端的socket分配一个线程 专门负责收消息
                  new ClientReader(this,socket).start();

                  // 带上用户信息过去
                  DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                  dos.writeInt(1); // 登录消息
                  dos.writeUTF(name.trim());
                  dos.flush();

                  // 关系当前窗口 弹出聊天界面
                  loginView.dispose(); // 登录窗口销毁
                  displayChatView(); // 展示了聊天窗口了


               } catch (Exception e1) {
    
    
                  e1.printStackTrace();
               }
            }
            break;
         case "取消":
            /** 退出系统 */
            System.exit(0);
            break;
         case "发送":
            
            break;

      }

   }
}

Client socket processing thread:

public class ClientReader extends Thread {
    
    

   private Socket socket;
    // 接收客户端界面,方便收到消息后,更新界面数据。
   private ClientChat clientChat ;

   public ClientReader(ClientChat clientChat, Socket socket) {
    
    
      this.clientChat = clientChat;
      this.socket = socket;
   }

   @Override
   public void run() {
    
    
      try {
    
    
         DataInputStream dis = new DataInputStream(socket.getInputStream());
         /** 循环一直等待客户端的消息 */
         while(true){
    
    
            /** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
            int flag = dis.readInt();
            if(flag == 1){
    
    
               // 在线人数消息回来了
               String nameDatas = dis.readUTF();
               // 展示到在线人数的界面
               String[] names = nameDatas.split(Constants.SPILIT);

               clientChat.onLineUsers.setListData(names);
            }else if(flag == 2){
    
    
              
            }
         }
      } catch (Exception e) {
    
    
         e.printStackTrace();
      }
   }
}
summary
  • Here it is explained that if the client interface is activated and the login function is activated, the server will respond with an online list and update the number of online users to the client after receiving a new login message!

Client sends message logic

Target

The client sends group chat messages, @ messages, and private chat messages.

Implementation steps
  • After the client is started, you need to push group chat messages, @ messages, and private chat messages through the send button on the chat interface.
    insert image description here

  • If you click send directly, the group chat message will be sent by default

  • If you select a user in the online list on the right, @message will be sent by default

  • If you select a user in the online list on the right, and then select the private chat button on the lower right, the private chat message will be sent by default.

Code

Client body code:

public class ClientChat implements ActionListener {
    
    
	/** 1.设计界面  */
	private JFrame win = new JFrame();
	/** 2.消息内容框架 */
	public JTextArea smsContent =new JTextArea(23 , 50);
	/** 3.发送消息的框  */
	private JTextArea smsSend = new JTextArea(4,40);
	/** 4.在线人数的区域  */
	/** 存放人的数据 */
	/** 展示在线人数的窗口 */
	public JList<String> onLineUsers = new JList<>();

	// 是否私聊按钮
	private JCheckBox isPrivateBn = new JCheckBox("私聊");
	// 消息按钮
	private JButton sendBn  = new JButton("发送");

	// 登录界面
	private JFrame loginView;

	private JTextField ipEt , nameEt , idEt;

	private Socket socket ;

	public static void main(String[] args) {
    
    
		new ClientChat().initView();

	}

	private void initView() {
    
    
		/** 初始化聊天窗口的界面 */
		win.setSize(650, 600);

		/** 展示登录界面  */
		displayLoginView();

		/** 展示聊天界面 */
		//displayChatView();

	}

	private void displayChatView() {
    
    

		JPanel bottomPanel = new JPanel(new BorderLayout());
		//-----------------------------------------------
		// 将消息框和按钮 添加到窗口的底端
		win.add(bottomPanel, BorderLayout.SOUTH);
		bottomPanel.add(smsSend);
		JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
		btns.add(sendBn);
		btns.add(isPrivateBn);
		bottomPanel.add(btns, BorderLayout.EAST);
		//-----------------------------------------------
		// 给发送消息按钮绑定点击事件监听器
		// 将展示消息区centerPanel添加到窗口的中间
		smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
		// 让展示消息区可以滚动。
		win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
		smsContent.setEditable(false);
		//-----------------------------------------------
		// 用户列表和是否私聊放到窗口的最右边
		Box rightBox = new Box(BoxLayout.Y_AXIS);
		onLineUsers.setFixedCellWidth(120);
		onLineUsers.setVisibleRowCount(13);
		rightBox.add(new JScrollPane(onLineUsers));
		win.add(rightBox, BorderLayout.EAST);
		//-----------------------------------------------
		// 关闭窗口退出当前程序
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.pack();  // swing 加上这句 就可以拥有关闭窗口的功能
		/** 设置窗口居中,显示出来  */
		setWindowCenter(win,650,600,true);
		// 发送按钮绑定点击事件
		sendBn.addActionListener(this);
	}

	private void displayLoginView(){
    
    

		/** 先让用户进行登录
		 *  服务端ip
		 *  用户名
		 *  id
		 *  */
		/** 显示一个qq的登录框     */
		loginView = new JFrame("登录");
		loginView.setLayout(new GridLayout(3, 1));
		loginView.setSize(400, 230);

		JPanel ip = new JPanel();
		JLabel label = new JLabel("   IP:");
		ip.add(label);
		ipEt = new JTextField(20);
		ip.add(ipEt);
		loginView.add(ip);

		JPanel name = new JPanel();
		JLabel label1 = new JLabel("姓名:");
		name.add(label1);
		nameEt = new JTextField(20);
		name.add(nameEt);
		loginView.add(name);

		JPanel btnView = new JPanel();
		JButton login = new JButton("登陆");
		btnView.add(login);
		JButton cancle = new JButton("取消");
		btnView.add(cancle);
		loginView.add(btnView);
		// 关闭窗口退出当前程序
		loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setWindowCenter(loginView,400,260,true);

		/** 给登录和取消绑定点击事件 */
		login.addActionListener(this);
		cancle.addActionListener(this);

	}

	private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
    
    
		/** 得到所在系统所在屏幕的宽高 */
		Dimension ds = frame.getToolkit().getScreenSize();

		/** 拿到电脑的宽 */
		int width1 = ds.width;
		/** 高 */
		int height1 = ds.height ;

		System.out.println(width1 +"*" + height1);
		/** 设置窗口的左上角坐标 */
		frame.setLocation(width1/2 - width/2, height1/2 -height/2);
		frame.setVisible(flag);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
    
    
		/** 得到点击的事件源 */
		JButton btn = (JButton) e.getSource();
		switch(btn.getText()){
    
    
			case "登陆":
				String ip = ipEt.getText().toString();
				String name = nameEt.getText().toString();
				// 校验参数是否为空
				// 错误提示
				String msg = "" ;
				// 12.1.2.0
				// \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\
				if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
    
    
					msg = "请输入合法的服务端ip地址";
				}else if(name==null || !name.matches("\\S{1,}")){
    
    
					msg = "姓名必须1个字符以上";
				}

				if(!msg.equals("")){
    
    
					/** msg有内容说明参数有为空 */
					// 参数一:弹出放到哪个窗口里面
					JOptionPane.showMessageDialog(loginView, msg);
				}else{
    
    
					try {
    
    
						// 参数都合法了
						// 当前登录的用户,去服务端登陆
						/** 先把当前用户的名称展示到界面 */
						win.setTitle(name);
						// 去服务端登陆连接一个socket管道
						socket = new Socket(ip, Constants.PORT);

						//为客户端的socket分配一个线程 专门负责收消息
						new ClientReader(this,socket).start();

						// 带上用户信息过去
						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeInt(1); // 登录消息
						dos.writeUTF(name.trim());
						dos.flush();

						// 关系当前窗口 弹出聊天界面
						loginView.dispose(); // 登录窗口销毁
						displayChatView(); // 展示了聊天窗口了


					} catch (Exception e1) {
    
    
						e1.printStackTrace();
					}
				}
				break;
			case "取消":
				/** 退出系统 */
				System.exit(0);
				break;
			case "发送":
				// 得到发送消息的内容
				String msgSend = smsSend.getText().toString();
				if(!msgSend.trim().equals("")){
    
    
					/** 发消息给服务端 */
					try {
    
    
						// 判断是否对谁发消息
						String selectName = onLineUsers.getSelectedValue();
						int flag = 2 ;// 群发 @消息
						if(selectName!=null&&!selectName.equals("")){
    
    
							msgSend =("@"+selectName+","+msgSend);
							/** 判断是否选中了私法 */
							if(isPrivateBn.isSelected()){
    
    
								/** 私法 */
								flag = 3 ;//私发消息
							}

						}

						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeInt(flag); // 群发消息  发送给所有人
						dos.writeUTF(msgSend);
						if(flag == 3){
    
    
							// 告诉服务端我对谁私发
							dos.writeUTF(selectName.trim());
						}
						dos.flush();

					} catch (Exception e1) {
    
    
						e1.printStackTrace();
					}

				}
				smsSend.setText(null);
				break;

		}

	}
}

Client socket processing thread:

class ClientReader extends Thread {
    
    

	private Socket socket;
	private ClientChat clientChat ;

	public ClientReader(ClientChat clientChat, Socket socket) {
    
    
		this.clientChat = clientChat;
		this.socket = socket;
	}

	@Override
	public void run() {
    
    
		try {
    
    
			DataInputStream dis = new DataInputStream(socket.getInputStream());
			/** 循环一直等待客户端的消息 */
			while(true){
    
    
				/** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
				int flag = dis.readInt();
				if(flag == 1){
    
    
					// 在线人数消息回来了
					String nameDatas = dis.readUTF();
					// 展示到在线人数的界面
					String[] names = nameDatas.split(Constants.SPILIT);

					clientChat.onLineUsers.setListData(names);
				}else if(flag == 2){
    
    
					//群发,私聊 , @消息 都是直接显示的。
					String msg = dis.readUTF() ;
					clientChat.smsContent.append(msg);
					// 让消息界面滾動到底端
					clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length());
				}
			}
		} catch (Exception e) {
    
    
			e.printStackTrace();
		}
	}
}
summary
  • Here, the client sends group chat messages, @ messages, and private chat messages.
  • If you click send directly, the group chat message will be sent by default
  • If you select a user in the online list on the right, @message will be sent by default
  • If you select a user in the online list on the right, and then select the private chat button on the lower right, the private chat message will be sent by default.

Guess you like

Origin blog.csdn.net/sinat_38316216/article/details/129959256