文章目录

  • 🚀一.TCP协议
  • 🔎1.TCP发送数据
  • 🔎2.TCP接收数据
  • 🔎3.TCP通信案例
  • 🦋3.1 聊天室案例
  • 🦋3.2 文件上传案例
  • 🦋3.3 图片上传案例
  • 🦋3.4 浏览器通信案例
  • 🔎4.TCP程序服务器优化
  • 🦋4.1 优化方案一
  • 🦋4.2 优化方案二
  • 🚀二、即时通讯案例
  • 🔎1.客户端界面
  • 🔎2.服务端界面
  • 🚀感谢:给读者的一封信



🚀一.TCP协议

TCP协议(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、传输层的协议,它在网络通信中广泛使用。TCP协议提供了数据传输的可靠性和流控制功能,保证了数据的有序传送和数据包的完整性。

TCP协议的主要特点包括:

  1. 面向连接:在进行数据传输前,TCP协议要先进行“三次握手”建立连接,传完数据后再进行“四次挥手”释放连接。
  2. 可靠性:通过序列号、确认应答机制、重传等方式保证数据的可靠性,也就是保证数据的正确传输,不会丢失、重复、或者乱序。
  3. 流控制:通过滑动窗口机制,防止发送方发送速度过快,导致接收方无法及时处理数据,从而影响数据传输的质量。
  4. 面向字节流:TCP协议是以字节流的形式传输数据,发送方将数据拆分成小的数据块,接收方再将这些数据块组合成完整的数据。

TCP协议在网络通信中的作用非常重要,它被广泛用于Web、FTP、邮件等应用程序中。

🔎1.TCP发送数据

以下是Java中TCP发送数据的方法大全表格展示和示例:

方法名称

返回类型

方法说明

示例代码

Socket.getOutputStream()

OutputStream

返回一个可以写入数据的OutputStream对象

OutputStream out = socket.getOutputStream();

out.write(dataBytes);

DataOutputStream.write()

void

向输出流写入单个字节

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.write(65);

DataOutputStream.writeBytes()

void

将一个字符串作为字节数组写入输出流

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.writeBytes(“Hello World”);

DataOutputStream.writeByte()

void

向输出流写入一个字节

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.writeByte(65);

DataOutputStream.writeShort()

void

向输出流写入一个短整型值

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.writeShort(10);

DataOutputStream.writeInt()

void

向输出流写入一个整型值

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.writeInt(20);

DataOutputStream.writeLong()

void

向输出流写入一个长整型值

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

dos.writeLong(30L);

ObjectOutputStream.writeObject()

void

向输出流写入一个对象

ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

oos.writeObject(object);

其中,Socket.getOutputStream()返回一个可以写入数据的OutputStream对象,可以通过调用该对象的write()方法来向输出流写入数据;DataOutputStream类继承自OutputStream类,提供了一些更高层次的写入方法,如writeBytes()、writeByte()、writeShort()、writeInt()、writeLong()等;ObjectOutputStream类提供了一种将Java对象序列化为字节流并写入输出流的方法,可以通过writeObject()方法向输出流写入对象。

示例代码:

// 使用Socket.getOutputStream()向输出流写入数据
Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
byte[] dataBytes = "Hello World".getBytes();
out.write(dataBytes);

// 使用DataOutputStream向输出流写入数据
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeBytes("Hello World");
dos.writeByte(65);
dos.writeShort(10);
dos.writeInt(20);
dos.writeLong(30L);

// 使用ObjectOutputStream向输出流写入对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(object);

🔎2.TCP接收数据

方法名

返回类型

描述

Socket.getInputStream()

InputStream

返回套接字的输入流,用于从套接字读取数据。

InputStream.read()

int

从输入流中读取下一个字节。当到达输入流的末尾时返回-1。

InputStream.read(byte[] b)

int

读取一定数量的字节,并将它们存储到缓冲区数组b中。返回实际读取的字节数。

InputStream.read(byte[] b, int off, int len)

int

读取最多len个字节,并将它们存储到缓冲区数组b中,从数组的off位置开始存储。返回实际读取的字节数。

DataInputStream.read()

int

从数据输入流中读取下一个字节。当到达输入流的末尾时返回-1。

DataInputStream.read(byte[] b)

int

读取一定数量的字节,并将它们存储到缓冲区数组b中。返回实际读取的字节数。

DataInputStream.read(byte[] b, int off, int len)

int

读取最多len个字节,并将它们存储到缓冲区数组b中,从数组的off位置开始存储。返回实际读取的字节数。

示例:

使用 Socket.getInputStream() 方法:

Socket socket = new Socket("127.0.0.1", 8888);
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
String result = new String(buffer, 0, len);
System.out.println("接收到的数据:" + result);

使用 InputStream.read() 方法:

Socket socket = new Socket("127.0.0.1", 8888);
InputStream inputStream = socket.getInputStream();
int res;
while ((res = inputStream.read()) != -1) {
    System.out.print((char) res);
}

使用 InputStream.read(byte[] b) 方法:

Socket socket = new Socket("127.0.0.1", 8888);
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
    String result = new String(buffer, 0, len);
    System.out.println("接收到的数据:" + result);
}

使用 DataInputStream.read() 方法:

Socket socket = new Socket("127.0.0.1", 8888);
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
int res;
while ((res = dataInputStream.read()) != -1) {
    System.out.print((char) res);
}

使用 DataInputStream.read(byte[] b) 方法:

Socket socket = new Socket("127.0.0.1", 8888);
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
byte[] buffer = new byte[1024];
int len;
while ((len = dataInputStream.read(buffer)) != -1) {
    String result = new String(buffer, 0, len);
    System.out.println("接收到的数据:" + result);
}

🔎3.TCP通信案例

🦋3.1 聊天室案例

以下是应用程序的基本工作原理:

  1. 服务器启动并开始监听客户端连接。
  2. 客户端启动并连接到服务器。
  3. 一旦客户端连接成功,它会发送它的用户名给服务器。
  4. 服务器将接收到的用户名存储在一个列表中,并向所有连接的客户端广播该用户已连接。
  5. 客户端可以向其他客户端广播消息,并且服务器将向所有连接的客户端广播该消息。

以下是服务器端代码:

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

public class ChatServer {
    private ArrayList<PrintWriter> clientOutputStreams;
    private ArrayList<String> connectedUsers;

    public static void main(String[] args) {
        new ChatServer().go();
    }

    public void go() {
        clientOutputStreams = new ArrayList<PrintWriter>();
        connectedUsers = new ArrayList<String>();
        try {
            ServerSocket serverSock = new ServerSocket(5000);

            while (true) {
                Socket clientSocket = serverSock.accept();
                PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
                clientOutputStreams.add(writer);

                Thread t = new Thread(new ClientHandler(clientSocket));
                t.start();
                System.out.println("got a connection");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void broadcast(String message) {
        for (PrintWriter writer : clientOutputStreams) {
            writer.println(message);
            writer.flush();
        }
    }

    public void addUser(String username) {
        connectedUsers.add(username);
    }

    public void removeUser(String username) {
        connectedUsers.remove(username);
    }

    public ArrayList<String> getConnectedUsers() {
        return connectedUsers;
    }

    class ClientHandler implements Runnable {
        private BufferedReader reader;
        private Socket socket;
        private String username;

        public ClientHandler(Socket clientSocket) {
            try {
                socket = clientSocket;
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                username = reader.readLine();
                addUser(username);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        public void run() {
            String message;
            try {
                while ((message = reader.readLine()) != null) {
                    broadcast(username + ": " + message);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                removeUser(username);
                broadcast(username + " has left the chat.");
                try {
                    socket.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

以下是客户端代码:

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

public class ChatClient {
    private BufferedReader reader;
    private PrintWriter writer;
    private Socket sock;
    private String username;

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

    public void go() {
        try {
            Scanner sc = new Scanner(System.in);

            System.out.print("Enter your username: ");
            username = sc.nextLine();

            sock = new Socket("127.0.0.1", 5000);
            reader = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            writer = new PrintWriter(sock.getOutputStream());
            writer.println(username);
            writer.flush();

            Thread readerThread = new Thread(new IncomingReader());
            readerThread.start();

            String message;
            while ((message = sc.nextLine()) != null) {
                writer.println(message);
                writer.flush();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    class IncomingReader implements Runnable {
        public void run() {
            String message;
            try {
                while ((message = reader.readLine()) != null) {
                    System.out.println(message);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

在这个例子中,我们使用ServerSocket和Socket类来实现TCP通信。服务器端创建一个ServerSocket对象来监听客户端连接请求,客户端使用Socket类来连接服务器。

在服务器端,我们使用了一个ArrayList存储所有连接的客户端的输出流(PrintWriter类型)和一个ArrayList存储所有连接的客户端的用户名。当客户端连接服务器时,服务器将客户端的输出流存储在列表中,同时从输入流中读取客户端的用户名,并将其存储在另一个列表中。每当客户端发送消息时,服务器将向所有连接的客户端广播该消息。当客户端断开连接时,服务器将从列表中删除客户端的用户名,并通知所有连接的客户端该用户已离开。

在客户端,我们使用Scanner类从用户输入中读取用户名。我们使用Socket类连接到服务器,并通过输出流发送用户名。我们还使用一个单独的线程来读取服务器广播的消息,以便在客户端的控制台上显示它们。每当用户在客户端输入框中输入消息时,我们将其发送到服务器。

🦋3.2 文件上传案例

  1. 客户端代码:
import java.io.*;
import java.net.Socket;

public class TCPClient {

    private static final String SERVER_IP = "127.0.0.1";
    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) throws IOException {
        String file = "file.txt";
        File inputFile = new File(file);
        if (!inputFile.exists()) {
            System.out.println("文件不存在!");
            return;
        }
        System.out.println("开始上传文件:" + file);

        Socket socket = null;
        FileInputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            socket = new Socket(SERVER_IP, SERVER_PORT);
            inputStream = new FileInputStream(inputFile);
            outputStream = socket.getOutputStream();

            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }

            System.out.println("文件上传完成!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
            if (socket != null) {
                socket.close();
            }
        }
    }
}
  1. 服务端代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {

    private static final int SERVER_PORT = 8888;

    public static void main(String[] args) throws IOException {
        System.out.println("服务端已启动,等待客户端连接...");

        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            serverSocket = new ServerSocket(SERVER_PORT);
            socket = serverSocket.accept();
            inputStream = socket.getInputStream();

            String fileName = "output.txt";
            File outputFile = new File(fileName);
            outputStream = new FileOutputStream(outputFile);

            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }

            System.out.println("文件保存在:" + outputFile.getAbsolutePath());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
            if (socket != null) {
                socket.close();
            }
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

客户端将本地的file.txt文件上传到服务端,服务端将文件保存为output.txt。客户端使用FileInputStream读取本地文件,写入到Socket的OutputStream中。服务端使用Socket的InputStream读取客户端传来的数据,写入到本地文件的FileOutputStream中。在这个过程中,客户端和服务端都需要使用try-catch-finally语句块来关闭IO流和Socket连接。

🦋3.3 图片上传案例

  • 案例需求
    客户端:数据来自于本地文件,接收服务器反馈
    服务器:接收到的数据写入本地文件,给出反馈
  • 案例分析
  • 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
  • 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
  • 客户端接受服务端的回馈信息
  • 相关方法

方法名

说明

void shutdownInput()

将此套接字的输入流放置在“流的末尾”

void shutdownOutput()

禁止用此套接字的输出流

  • 代码实现
// 客户端
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",10000);

        //是本地的流,用来读取本地文件的.
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("socketmodule\\ClientDir\\1.jpg"));

        //写到服务器 --- 网络中的流
        OutputStream os = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(os);

        int b;
        while((b = bis.read())!=-1){
            bos.write(b);//通过网络写到服务器中
        }
        bos.flush();
        //给服务器一个结束标记,告诉服务器文件已经传输完毕
        socket.shutdownOutput();

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line;
        while((line = br.readLine()) !=null){
            System.out.println(line);
        }
        bis.close();
        socket.close();
    }
}
// 服务器
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        Socket accept = ss.accept();

        //网络中的流,从客户端读取数据的
        BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
        //本地的IO流,把数据写到本地中,实现永久化存储
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("socketmodule\\ServerDir\\copy.jpg"));

        int b;
        while((b = bis.read()) !=-1){
            bos.write(b);
        }

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
        bw.write("上传成功");
        bw.newLine();
        bw.flush();

        bos.close();
        accept.close();
        ss.close();
    }
}

🦋3.4 浏览器通信案例

public class BSServer {

    // 定义一个静态的线程池对象。
    public static ExecutorService pool =
            new ThreadPoolExecutor(2,3,
                    5,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动。。。");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(8080);

        while (true) {
            // 2、开始等待接收客户端的socket连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:"
                    + socket.getRemoteSocketAddress());

            // 3、把当前客户端的socket做成任务对象交给线程池来处理它的消息。
            pool.execute(new ServerReaderRunnable(socket));
        }
    }
}
public class ServerReaderRunnable implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        try {
            // 响应一个网页数据回去即可
            PrintStream ps = new PrintStream(socket.getOutputStream());
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Type:text/html;charset=UTF-8");
            ps.println(); // 换行!
            ps.println("<html><body><h1>Hello, World!</h1></body></html>");
            ps.flush();

            socket.close();
        } catch (Exception e) {
            System.out.println("有人非正常下线了:" + socket.getRemoteSocketAddress());
        }
    }
}

在浏览器中输入 http://localhost:8080/ 后,即可在浏览器中看到响应的 “Hello, World!” 页面。

🔎4.TCP程序服务器优化

🦋4.1 优化方案一

开启多线程处理可以带来以下优势:

  1. 提高并发处理能力:多线程可以同时处理多个客户端请求,从而提高服务器的并发处理能力。
  2. 提高系统响应速度:多线程可以缩短客户端请求的等待时间,提高系统响应速度。
  3. 减少线程等待时间:在单线程模式下,当一个线程正在等待客户端的请求返回时,其他线程需要等待该线程完成后才能执行。而多线程模式下,每个线程独立处理一个客户端请求,无需等待其他线程的完成。
  4. 提高资源利用率:开启多线程可以充分利用服务器的CPU和内存资源,提高资源利用率。

开启多线程处理可以提高服务器的并发处理和响应能力,缩短客户端请求的等待时间,减少线程等待时间,并提高资源利用率,是TCP程序服务器优化的重要方式之一。

1、客户端

public class ClientDemo {
    public static void main(String[] args) throws Exception {
        // 1、创建一个Socket对象,请求与服务器的连接(TCP通信)
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2、从socket通信管道中得到一个字节输出流,负责写数据出去
        OutputStream os = socket.getOutputStream();

        // 3、把低级字节输出流交给打印流
        PrintStream ps = new PrintStream(os);

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)) {
                 ps.close();
                 socket.close();
                 break;
            }

            // 4、发送消息出去
            ps.println(msg);
            // 5、刷新
            ps.flush();
        }
    }
}

2、服务端

public class ServerDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动。。。");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(9999);

        while (true) {
            // 2、开始等待接收客户端的socket连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

            // 3、创建一个独立的线程负责处理这个客户端socket的通信消息
            new ServerReaderThread(socket).start();
        }
    }
}
public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动。。。");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(9999);

        while (true) {
            // 2、开始等待接收客户端的socket连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

            // 3、创建一个独立的线程负责处理这个客户端socket的通信消息
            new ServerReaderThread(socket).start();
        }
    }
}

🦋4.2 优化方案二

在Java中,TCP程序服务器可以通过线程池的方式进行优化。开启线程池处理可以带来以下优势:

  1. 提高系统响应速度:线程池可以复用已经创建好的线程,减少线程的创建和销毁次数,从而提高系统响应速度。
  2. 控制并发线程数量:线程池可以控制并发线程的数量,避免线程过多导致系统崩溃。
  3. 减少线程上下文切换开销:在单线程模式下,当多个线程竞争同一个CPU时,线程间会发生频繁的上下文切换,导致系统开销过大。而线程池可以将多个任务按照一定的规则分配到不同的线程中,避免上下文切换开销。
  4. 提高资源利用率:线程池可以充分利用服务器的CPU和内存资源,提高资源利用率。

开启线程池处理可以提高系统响应速度,控制并发线程数量,减少线程上下文切换开销,并提高资源利用率,是TCP程序服务器优化的重要方式之一。

1、客户端

public class ClientDemo {
    public static void main(String[] args) throws Exception {
        // 1、创建一个Socket对象,请求与服务器的连接(TCP通信)
        Socket socket = new Socket("127.0.0.1", 7777);

        // 2、从socket通信管道中得到一个字节输出流,负责写数据出去
        OutputStream os = socket.getOutputStream();

        // 3、把低级字节输出流交给打印流
        PrintStream ps = new PrintStream(os);

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)) {
                 ps.close();
                 socket.close();
                 break;
            }

            // 4、发送消息出去
            ps.println(msg);
            // 5、刷新
            ps.flush();
        }
    }
}

2、服务端

public class ServerDemo {

    // 定义一个静态的线程池对象。
    public static ExecutorService pool =
            new ThreadPoolExecutor(2,3,
                    5,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动。。。");
        // 1、注册端口
        ServerSocket serverSocket = new ServerSocket(7777);

        while (true) {
            // 2、开始等待接收客户端的socket连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

            // 3、把当前客户端的socket做成任务对象交给线程池来处理它的消息。
            pool.execute(new ServerReaderRunnable(socket));
        }
    }
}
public class ServerReaderRunnable implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流读取数据
            InputStream is = socket.getInputStream();

            // 4、读取消息
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            // 5、读取一行消息。
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + line);
            }
            System.out.println("有人正常下线了:" + socket.getRemoteSocketAddress());
        } catch (Exception e) {
            System.out.println("有人非正常下线了:" + socket.getRemoteSocketAddress());
        }
    }
}

🚀二、即时通讯案例

🔎1.客户端界面

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;

/**
 * 客户端界面
 */
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;

		}

	}
}

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();
		}

	}
}
public class Constants {
	/** 常量 */
	public static final int PORT = 7778 ;
	
	/** 协议分隔符 */
	public static final String SPILIT = "003197♣♣㏘♣④④♣";
}

🔎2.服务端界面

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class ServerChat {

	/** 定义一个集合存放所有在线的socket  */
	public static Map<Socket, String> onLineSockets = new HashMap<>();

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

			/** 循环一直等待所有可能的客户端连接 */
			while(true){
				Socket socket = serverSocket.accept();
				/** 把客户端的socket管道单独配置一个线程来处理 */
				new ServerReader(socket).start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

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());
			/** 循环一直等待客户端的消息 */
			while(true){
				/** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
				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();
		}
	}
}
public class User {
	private Integer id ;
	private String name ;
	
	public User(Integer id, String name) {
		this.id = id;
		this.name = name;
	}
	
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public String toString() {
		return "User [id=" + id + ", name=" + name + "]";
	}
	
	
}