日期: 11/20/2021

上一篇 Java语言实战开发 @网络聊天室 (一) 简单实现用户发消息给后台管理员

1. 探索服务端接收消息的机制

在上一次的实践中,使用缓冲输入输出流通过套接字进行网络通信,但是目前只能实现使用客户端发送消息给服务端,服务端无法做出回应,这一次就对此问题进行优化。

由于是初次使用 ServerSocket 类,有一些机制需要自己探索,比如在Server中这段:

while(true){ Socket accept = serverSocket.accept(); BufferedReader r = new BufferedReader(new InputStreamReader(accept.getInputStream())); String message; while((message = r.readLine()) != null) System.out.println("收到: " + message);

最外层使用一个while循环不断执行,接收客户端套接字的程序,同时根据套接字的字节输入流而创建缓冲输入流,进而读取来自客户端的数据,那么服务端最终会执行多少次 while (true) 呢?
是否在客户端发送一次消息过后,服务端就结束一次while循环了呢?只需一个变量就可以解决这个疑问,测试代码如下:

int i = 0; 
while(true){
	// ... 前面部分和之前一样
	System.out.println("-----" + (++i));

接下来先后打开服务端、客户端,并在客户端发送三条测试消息,测试结果如下:

答案是:while (true) 只执行了一次,而且一直没有结束.。

这意味着服务端主线程一直在执行 while(message = r.rindLine() != null); 的语句。

现在把while 改成 if ,同时把socket的获取放在前面,否则会一直阻塞进程,测试效果如下:

Socket accept = serverSocket.accept();
while(true){
	//...
	if(message=r.rindLine())!=null){
		// ...

成功解决了服务端接受一个客户端的线程阻塞问题,接下来测试两个客户端,通过修改端口类的连接IP实现。

很明显,由于 服务端获取socket的代码必须放在外面,否则无法一直执行while(true) 的所有内容(线程阻塞),但是在主线程,服务端必须要随时等待客户端发来的消息,此时也有其他客户端连接的可能,所以这里需要引用多线程的机制,使用Java.lang包下的Thread类实现该过程。

2 多线程机制的应用

实现多线程的方法主要有两种

方式一:实现Runnable接口,重写Run方法

class RunnableImpl implements Runnable{
	@Override
	public void run(){
		// ...
// 使用通过 new Thread(new RunnableImpl()).start()

方式二:继承Thread类,重写Run方法

class myThread extends Thread{
	@Override
	public void run(){
		// ...
// 使用通过 new myThread().start();

它们的区别是:

实现接口方式,还可以继承其他的类,具有更高的扩展性,但只有多线程的run方法。

继承的方式,具有原生Thread的方法,更加全面,但扩展性低,无法再继承第二个类。

当有多个客户端连接服务端时,即多个用户需要管理员进行管理,对于每个用户,管理员需要进行不同的约束,当然也有共性,在程序实现中,给每个用户分配一个线程,这样管理员就不用一一的对用户进行管理,提高了并发性。

现对服务端添加两个线程类,一是负责随时接待连接的客户端,二是针对响应每个客户端的消息。

ServerReadThread.java

该类负责处理一个客户端的消息发送,其中的socket对象是与服务端连接时创建的socket,所以可以根据该对象进行数据通信。

package App;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
public class ServerReadThread extends Thread{
    private Socket socket;
    private DataInputStream in;
    public ServerReadThread(Socket socket){
        this.socket = socket;
        try {
            in = new DataInputStream(socket.getInputStream());
        } catch (IOException e) {
            System.out.println("获取套接字输入流对象失败");
    @Override
    public void run() {
        while (true) {
            String info;
            try {
                if ((info = in.readUTF()) != null)
                    System.out.println(info);
            } catch (IOException e) {
                e.printStackTrace();

ServerReadyThread.java

该类负责处理每个客户端的连接,其中的serverSocket对象是创建服务端时产生的套接字,通过该对象可以调用accept()方法接收到连接的客户端套接字。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerReadyThread extends Thread{
    private ServerSocket serverSocket;
    public ServerReadyThread(ServerSocket serverSocket){
        this.serverSocket = serverSocket;
    @Override
    public void run() {
        while (true){
            try {
                Socket accept = serverSocket.accept();
                new ServerReadThread(accept).start();
                Server.list_socket.add(accept);
            } catch (IOException e) {
                e.printStackTrace();

Server.java

该类是服务端的主要实现类,采用多线程过后,主线程可以不用while(true)循环,

其中的 list_socket 存储当前连接的所有客户端套接字,在客户端连接时会添加进来,这样方便主线程可以对客户端进行消息反馈。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
public class Server {
    private final static int port = 6666;
    public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        int i = 0;
        ServerSocket serverSocket = new ServerSocket(port);
        // 启动负责连接的线程
        new ServerReadyThread(serverSocket).start();

3. 服务端的反馈机制

通过多线程机制,现在服务端的主线程已经可以继续执行其他的操作,比如再添加更多的多线程。

之前所有客户端套接字存储在了 list_sockets对象里,既然是相通的套接字,那么服务端同样可以发送消息给出去,只要客户端也做相应的接收即可。

再次使用多线程机制,这次在客户端类设计多线程负责接收来自服务端的消息,同时在服务端添加发送消息的代码。

Server.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Scanner;
public class Server {
    private final static int port = 6666;
    public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
    public static Scanner in = new Scanner(System.in);
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        int i = 0;
        ServerSocket serverSocket = new ServerSocket(port);
        // 启动负责连接的线程
        new ServerReadyThread(serverSocket).start();
        tips();
        while (true) {
            String operation = in.nextLine();
            switch (operation){
                case "1":
                    sendMsgToClient();
                    break;
                default:
                    System.out.print(">> 不支持当前指令,请重新输入\n>> ");
            tips();
    public static void tips(){
        System.out.println("==========欢迎访问服务端========");
        System.out.println("|      现在可进行的操作如下     |");
        System.out.println("|   1. 向指定客户端发送消息     |");
        System.out.println("================================");
        System.out.printf(">> ");
    public static void sendMsgToClient(){
        showAllClient();
        System.out.print(">> 请选择要发送到的客户端(e可取消操作)\n>> ");
        boolean flag = true;
        while (flag) {
            String choice = in.nextLine();
            switch (choice) {
                case "e":
                    flag = false;
                    break;
                default:
                    int i = Integer.parseInt(choice) -1;
                    if(i < list_socket.size()){
                        System.out.print(">> 选择成功, 接下来请输入要发送的内容\n>> ");
                        try {
                            DataOutputStream out = new DataOutputStream(list_socket.get(i).getOutputStream());
                            out.writeUTF(in.nextLine());
                            System.out.println(">> 消息发送成功!");
                            flag = false;
                        } catch (IOException e) {
                            e.printStackTrace();
                    } else{
                        System.out.println("选择的客户端不存在, 当前客户端总数目为: " + list_socket.size());
    public static void showAllClient(){
        for (int i = 0; i < list_socket.size(); i++) {
            SocketAddress name = list_socket.get(i).getRemoteSocketAddress();
            System.out.printf("%d号客户端:%s\n", (i+1), name);

ClientReadServer.java
该类负责处理客户端接收套接字的消息

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ClientReadThread extends Thread{
    private Socket socket;
    private DataInputStream in;
    public ClientReadThread(Socket socket){
        this.socket = socket;
        try {
            in = new DataInputStream(socket.getInputStream());
        } catch (IOException e) {
            System.out.println("获取套接字输出流失败.");
    @Override
    public void run() {
        while(true){
            String info;
            try {
                if((info = in.readUTF())!=null)
                    System.out.println("服务端发送了:" + info);
            } catch (IOException e) {
                e.printStackTrace();

最后需要在 Client.java中添加 new ClientReadThread(socket).start();确保线程启动。

测试结果:

4. 实现注册登陆功能

通过上面的测试,可以发现在客户端发送消息时,并不知道自己的名称,这显然不符合用户的定义,聊天工具一般都有个人的账号系统,所以在服务端和客户端连接前,最好指定一个登陆状态对象,并且服务端可以通过该对象读取已连接客户端的信息,类似于对上线的用户进行的操作。

PersonInfo.java 用户信息实体类

存储管理员和用户两个身份的信息,主要包括登陆ID、登陆密码和身份

public class PersonInfo {
    private String id;
    private String pw;
    private String identity;
    private boolean isLogin = false;
    public PersonInfo() {};
    public PersonInfo(String id, String pw, String identity) {
        this.id = id;
        this.pw = pw;
        this.identity = identity;
    public String getId() {
        return id;
    public void setId(String id) {
        this.id = id;
    public String getPw() {
        return pw;
    public void setPw(String pw) {
        this.pw = pw;
    public String getIdentity() {
        return identity;
    public void setIdentity(String identity) {
        this.identity = identity;
    public void setLogin(boolean x){
        this.isLogin = x;

Login.java 登陆功能实现类

该类可以通过判断账号ID与密码是否匹配而实现登陆,若账号不存在则可以直接创建,若密码错误则提示重新输入,代码看起来比较简洁,因为大多是调用自定义的数据工具类DataUtils.java的里的静态方法

import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;
public class Login {
    public static Scanner in = new Scanner(System.in);
    public static PersonInfo go() {
        System.out.println("========登陆系统=======");
        PersonInfo user = new PersonInfo(null, null, "admin");
        boolean needLogin = true;
        while (needLogin) {
            System.out.print("id>> ");  user.setId(in.nextLine()) ;
            System.out.print("pw>> ");  user.setPw(in.nextLine());
            if (DataUtils.checkPersonInfo(user) != null) {
                System.out.println(">> 登陆成功!");
                return user;
            } else if (DataUtils.checkId(user.getId()) == false) {
                System.out.println(">> 密码错误");
            } else {
                System.out.println(">> 该用户不存在,是否创建?(y/n)");
                switch (in.nextLine()) {
                    case "y": ;
                        DataUtils.writeRegister(user);
                        System.out.println(">> 已登陆.");
                        needLogin = false;
                        break;
                    default:
                        System.out.println(">> 请重新登陆");
                        break;
        return user;

DataUtils.java 数据工具类

该类存储了所有的登陆用户的信息、以及连接的所有套接字,并提供一些相关的操作,比如读取注册用户,写入用户,验证登陆等。

import java.io.*;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
public class DataUtils {
    // 变量: 存储所有的注册用户 (包括管理员下同)
    private final static ArrayList<PersonInfo> list_origin_info = new ArrayList<>();
    // 变量: 存储所有上线的用户
    private final static ArrayList<PersonInfo> list_online_info = new ArrayList<>();
    // 变量: 存储所有的客户端与服务端的套接字
    public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
    // 功能: 从本地读取所有的注册用户
    public static void readRegister(){
        try {
            BufferedReader in = new BufferedReader(new FileReader("personInfo.txt"));
            String info;
            while ((info = in.readLine()) != null) {
                String[] split = info.split(",");
                if (split != null && split.length == 3)
                    list_origin_info.add(new PersonInfo(split[0], split[1], split[2]));
        } catch (IOException e) { e.printStackTrace();}
    // 功能: 返回所有注册用户
    public static ArrayList<PersonInfo> getOriginInfo() {return list_origin_info; }
    // 功能: 将新用户写入配置文件
    public static void writeRegister(PersonInfo p){
        try {
            BufferedWriter out = new BufferedWriter(new FileWriter("personInfo.txt", true));
            out.write(p.getId() + "," + p.getPw() + "," + p.getIdentity() + "\n");
            out.flush();
            p.setLogin(true);
            list_online_info.add(p);
            System.out.println(p.getId() + " 注册成功!");
        } catch (IOException e) {
            e.printStackTrace();
    // 功能: 返回所有线上用户
    public static ArrayList<PersonInfo> getOnlienInfo() {return list_online_info; }
    // 功能: 检查ID是否存在
    public static boolean checkId(String id){
        for (PersonInfo personInfo : list_origin_info) {
            if (id.equals(personInfo.getId()))
                return false;
        return true;
    // 功能: 显示所有客户端的名称
    public static void showAllClient(){
        for (int i = 0; i < list_socket.size(); i++) {
            SocketAddress name =list_socket.get(i).getRemoteSocketAddress();
            System.out.printf("%d号客户端:%s\n", (i+1), name);
    // 功能: 检查用户是否存在,即验证登陆正确性
    public static PersonInfo checkPersonInfo(PersonInfo user){
        readRegister();
        String id = user.getId();
        String pw = user.getPw();
        String identity = user.getIdentity();
        for (PersonInfo p : DataUtils.getOriginInfo()) {
            if (id.equals(p.getId()) && pw.equals(p.getPw()) &&
                    identity.equals(p.getIdentity())) {
                PersonInfo pi = new PersonInfo(id, pw, identity);
                pi.setLogin(true);
                return pi;
        return null;

Server.java 服务端类, 部分有修改

public class Server {
   //...
    private static PersonInfo admin = null;
    public static void main(String[] args) throws IOException {
        admin = Login.go();
        //...
        while (true) {
            //...

5. 总结 、所有实现代码

5.1 项目结构

项目结构:

各种类的作用描述表

5.2 存在的不足

  • IO流的选择问题,在上一篇中,笔者用到了 BufferedReader 字符缓冲输入流,适合字符数据的传输,另外,DataInputStream似乎更适合数据的传输,但是又有一个缺点,它必须遵循严格的读写类型,比如若在socket的一端是调用readInt() 那么另一端就必须是 writeInt(),否则会抛出异常,同时还有ObjectInputStream这类输入流存在,可以尝试一番。
  • 项目结构层次比较混乱,没有接口,未体现出多态的特性。
  • 未考虑到多线程机制的安全性问题,目前只是按需求创建线程,以便处理事件,还没有考虑到线程是否能够合理地使用系统资源。
  • 5.3 所有实现代码

    Client.java

    import java.io.*;
    import java.net.Socket;
    import java.util.Scanner;
    public class Client {
        private static PersonInfo user = null;
        private static Socket socket;
        public static void main(String[] args) throws IOException {
            System.out.println("客户端启动");
            user = Login.go();
            // 连接服务端
            socket = new Socket("localhost", 6666);
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
            // 启动接收服务端消息的线程
            new ClientReadThread(socket).start();
            Scanner in = new Scanner(System.in);
            out.writeUTF("\n>> " + user.getId() + " 已加入聊天室!");
            while(true){
                System.out.print(user.getId() + " >> ");
                String info = in.nextLine();
                out.writeUTF("\n>> 收到来自 [ " + user.getId() +" ] 的消息: " + info);
    

    ClientReadThread.java

    import java.io.*;
    import java.net.Socket;
    public class ClientReadThread extends Thread{
        private Socket socket;
        private DataInputStream in;
        public ClientReadThread(Socket socket){
            this.socket = socket;
            try {
                in = new DataInputStream(socket.getInputStream());
            } catch (IOException e) {
                System.out.println("获取套接字输出流失败.");
        @Override
        public void run() {
            while(true){
                String info;
                try {
                    if((info = in.readUTF())!=null)
                        System.out.println("\n服务端发送了:" + info);
                } catch (IOException e) {
                    e.printStackTrace();
    

    DataUtils.java

    import java.io.*;
    import java.net.Socket;
    import java.net.SocketAddress;
    import java.util.ArrayList;
     * @author Uni
     * @create 2021/11/20 10:55
    public class DataUtils {
        // 变量: 存储所有的注册用户 (包括管理员下同)
        private final static ArrayList<PersonInfo> list_origin_info = new ArrayList<>();
        // 变量: 存储所有上线的用户
        private final static ArrayList<PersonInfo> list_online_info = new ArrayList<>();
        // 变量: 存储所有的客户端与服务端的套接字
        public final static ArrayList<Socket> list_socket = new ArrayList<>(5);
        // 功能: 从本地读取所有的注册用户
        public static void readRegister(){
            try {
                BufferedReader in = new BufferedReader(new FileReader("personInfo.txt"));
                String info;
                while ((info = in.readLine()) != null) {
                    String[] split = info.split(",");
                    if (split != null && split.length == 3)
                        list_origin_info.add(new PersonInfo(split[0], split[1], split[2]));
                in.close();
            } catch (IOException e) { e.printStackTrace();}
        // 功能: 返回所有注册用户
        public static ArrayList<PersonInfo> getOriginInfo() {return list_origin_info; }
        // 功能: 将新用户写入配置文件
        public static void writeRegister(PersonInfo p){
            try {
                BufferedWriter out = new BufferedWriter(new FileWriter("personInfo.txt", true));
                out.write(p.getId() + "," + p.getPw() + "," + p.getIdentity() + "\n");
                out.flush();
                p.setLogin(true);
                list_online_info.add(p);
                System.out.println(p.getId() + " 注册成功!");
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
        // 功能: 返回所有线上用户
        public static ArrayList<PersonInfo> getOnlienInfo() {return list_online_info; }
        // 功能: 检查ID是否存在
        public static boolean checkId(String id){
            for (PersonInfo personInfo : list_origin_info) {
                if (id.equals(personInfo.getId()))
                    return false;
            return true;
        // 功能: 显示所有客户端的名称
        public static void showAllClient(){
            for (int i = 0; i < list_socket.size(); i++) {
                SocketAddress name =list_socket.get(i).getRemoteSocketAddress();
                System.out.printf("%d号客户端:%s\n", (i+1), name);
        // 功能: 检查用户是否存在,即验证登陆正确性
        public static PersonInfo checkPersonInfo(PersonInfo user){
            readRegister();
            String id = user.getId();
            String pw = user.getPw();
            String identity = user.getIdentity();
            for (PersonInfo p : DataUtils.getOriginInfo()) {
                if (id.equals(p.getId()) && pw.equals(p.getPw()) &&
                        identity.equals(p.getIdentity())) {
                    PersonInfo pi = new PersonInfo(id, pw, identity);
                    pi.setLogin(true);
                    return pi;
            return null;
    

    Login.java

    import java.io.*;
    import java.util.ArrayList;
    import java.util.Scanner;
    public class Login {
        public static Scanner in = new Scanner(System.in);
        public static PersonInfo go() {
            System.out.println("========登陆系统=======");
            PersonInfo user = new PersonInfo(null, null, "admin");
            boolean needLogin = true;
            while (needLogin) {
                System.out.print("id>> ");  user.setId(in.nextLine()) ;
                System.out.print("pw>> ");  user.setPw(in.nextLine());
                if (DataUtils.checkPersonInfo(user) != null) {
                    System.out.println(">> 登陆成功!");
                    return user;
                } else if (DataUtils.checkId(user.getId()) == false) {
                    System.out.println(">> 密码错误");
                } else {
                    System.out.println(">> 该用户不存在,是否创建?(y/n)");
                    switch (in.nextLine()) {
                        case "y": ;
                            DataUtils.writeRegister(user);
                            System.out.println(">> 已登陆.");
                            needLogin = false;
                            break;
                        default:
                            System.out.println(">> 请重新登陆");
                            break;
            return user;
    

    PersonInfo.java

    public class PersonInfo {
        private String id;
        private String pw;
        private String identity;
        private boolean isLogin = false;
        public PersonInfo() {};
        public PersonInfo(String id, String pw, String identity) {
            this.id = id;
            this.pw = pw;
            this.identity = identity;
        public String getId() {
            return id;
        public void setId(String id) {
            this.id = id;
        public String getPw() {
            return pw;
        public void setPw(String pw) {
            this.pw = pw;
        public String getIdentity() {
            return identity;
        public void setIdentity(String identity) {
            this.identity = identity;
        public void setLogin(boolean x){
            this.isLogin = x;
    

    Server.java

    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.net.SocketAddress;
    import java.util.ArrayList;
    import java.util.Scanner;
    public class Server {
        private final static int port = 6666;
        private static PersonInfo admin = null;
        public static Scanner in = new Scanner(System.in);
        public static void main(String[] args) throws IOException {
            System.out.println("服务端启动");
            admin = Login.go();
            int i = 0;
            ServerSocket serverSocket = new ServerSocket(port);
            // 启动负责连接的线程
            new ServerReadyThread(serverSocket).start();
            tips();
            while (true) {
                String operation = in.nextLine();
                switch (operation) {
                    case "1":
                        sendMsgToClient();
                        break;
                    default:
                        System.out.print(">> 不支持当前指令,请重新输入\n>> ");
                tips();
        public static void tips() {
            System.out.println("==========欢迎访问服务端========");
            System.out.println("|      现在可进行的操作如下     |");
            System.out.println("|   1. 向指定客户端发送消息     |");
            System.out.println("================================");
            System.out.printf(">> ");
        public static void sendMsgToClient() {
            DataUtils.showAllClient();
            System.out.print(">> 请选择要发送到的客户端(e可取消操作)\n>> ");
            boolean flag = true;
            while (flag) {
                String choice = in.nextLine();
                switch (choice) {
                    case "e":
                        flag = false;
                        break;
                    default:
                        int i = Integer.parseInt(choice) - 1;
                        if (i < DataUtils.list_socket.size()) {
                            System.out.print(">> 选择成功, 接下来请输入要发送的内容\n>> ");
                            try {
                                DataOutputStream out = new DataOutputStream(DataUtils.list_socket.get(i).getOutputStream());
                                while (true){
                                    String info = in.nextLine();
                                    if(!info.equals("e")){
                                        out.writeUTF(info);
                                        System.out.print(">> 消息发送成功!\n>> ");
                                    else break;
                                flag = false;
                            } catch (IOException e) {
                                e.printStackTrace();
                        } else {
                            System.out.println("选择的客户端不存在, 当前客户端总数目为: " + DataUtils.list_socket.size());
    

    ServerReadThread.java

    import java.io.BufferedReader;
    import java.io.DataInputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.Socket;
    public class ServerReadThread extends Thread{
        private static Socket socket;
        private static DataInputStream in = null;
        public ServerReadThread(Socket socket){
            this.socket = socket;
            try {
                in = new DataInputStream(this.socket.getInputStream());
            } catch (IOException e) {
                System.out.println("获取套接字输入流对象失败");
        @Override
        public void run() {
            while (true) {
                try {
                    String info;
                    if (in != null && (info = in.readUTF()) != null)
                        System.out.println(info);
                } catch (IOException e) {
                    e.printStackTrace();
    

    ServerReadyThread.java

    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    public class ServerReadyThread extends Thread{
        private static ServerSocket serverSocket;
        public ServerReadyThread(ServerSocket serverSocket){
            this.serverSocket = serverSocket;
        @Override
        public void run() {
            while (true){
                try {
                    Socket accept = serverSocket.accept();
                    new ServerReadThread(accept).start();
                    DataUtils.list_socket.add(accept);
                } catch (IOException e) {
                    e.printStackTrace();