在这里应用了许多前所未有的概念,它是一个简单的聊天服务器,能够处理多个客户端,这些客户端通过JavaFX运行,并在应用程序线程中实例化和处理了它们的各个线程(比预期的要复杂)。
< br聊天服务器:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.HashSet;
import java.util.Scanner;

public class ChatServer {
    private static final int PORT = 9001;
    private static HashSet<String> names = new HashSet<>();
    private static HashSet<String> userNames = new HashSet<>();
    private static HashSet<PrintWriter> writers = new HashSet<>();
    private static int usersConnected = 0;

    public static void main(String[] args) {
        System.out.println(new Date() + "\nChat Server online.\n");

        try (ServerSocket chatServer = new ServerSocket(PORT)) {
            while (true) {
                Socket socket = chatServer.accept();
                new ClientHandler(socket).start();
            }
        } catch (IOException ioe) {}
    }

    private static String names() {
        StringBuilder nameList = new StringBuilder();

        for (String name : userNames) {
            nameList.append(", ").append(name);
        }

        return "In lobby: " + nameList.substring(2);
    }

    private static class ClientHandler extends Thread {
        private String name;
        private String serverSideName;
        private Socket socket;
        private BufferedReader in;
        private PrintWriter out;

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

        @Override
        public void run() {
            try {
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                out = new PrintWriter(socket.getOutputStream(), true);

                out.println("SUBMIT_NAME");
                name = in.readLine();
                serverSideName = name.toLowerCase();

                synchronized (names) {
                    while (names.contains(serverSideName) || name == null || name.trim().isEmpty()) {
                        out.println("RESUBMIT_NAME");
                        name = in.readLine();
                        serverSideName = name.toLowerCase();
                    }
                }

                out.println("NAME_ACCEPTED");
                System.out.println(name + " connected. IP: " + socket.getInetAddress().getHostAddress());

                messageAll("CONNECT" + name);
                userNames.add(name);
                names.add(serverSideName);
                writers.add(out);
                out.println("INFO" + ++usersConnected + names());


                while (true) {
                    String input = in.readLine();

                    if (input == null || input.isEmpty()) {
                        continue;
                    }

                    messageAll("MESSAGE " + name + ": " + input);
                }
            } catch (IOException e) {
                if (name != null) {
                    System.out.println(name + " disconnected.");
                    userNames.remove(name);
                    names.remove(serverSideName);
                    writers.remove(out);
                    messageAll("DISCONNECT" + name);
                    usersConnected--;
                }   
            } finally {     
                try {
                    socket.close();
                } catch (IOException e) {}
            }
        }
    }

    private static void messageAll(String... messages) {
        if (!writers.isEmpty()){
            for (String message : messages) {
                for (PrintWriter writer : writers) {
                    writer.println(message);
                }
            }
        }
    }
}


聊天客户端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class ChatClient extends Application {
    final int PORT = 9001;
    final String SERVER_ADDRESS = "localhost";
    BufferedReader in;
    PrintWriter out;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        TextArea messageArea = new TextArea();
        messageArea.setEditable(false);

        TextField textField = new TextField();
        textField.setEditable(false);
        textField.setOnAction(e -> {
            out.println(textField.getText());
            textField.clear();
        });

        VBox layout = new VBox(5);
        layout.getChildren().addAll(messageArea, textField);

        stage.setScene(new Scene(layout));
        stage.setTitle("Chatter App By Legato");

        Task task = new Task<Void>() {
            @Override
            public Void call() {
                try {
                    Socket socket = new Socket(SERVER_ADDRESS, PORT);
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    out = new PrintWriter(socket.getOutputStream(), true);

                    while (true) {
                        String line = in.readLine();

                        if (line.startsWith("SUBMIT_NAME")) {
                            FutureTask<String> futureTask = new FutureTask<>(new NamePrompt("Choose a screen name:"));
                            Platform.runLater(futureTask);
                            try {
                                out.println(futureTask.get());
                            } catch(InterruptedException | ExecutionException ex) {}
                        } else if (line.startsWith("RESUBMIT_NAME")) {
                            FutureTask<String> futureTask = new FutureTask<>(new NamePrompt("Duplicate name. Try another:"));
                            Platform.runLater(futureTask);
                            try {
                                out.println(futureTask.get());
                            } catch(InterruptedException | ExecutionException ex) {}
                        } else if (line.startsWith("NAME_ACCEPTED")) {
                            textField.setEditable(true);
                            Platform.runLater(() -> textField.requestFocus());
                        } else if (line.startsWith("INFO")) {
                            messageArea.appendText("Users connected: " + line.charAt(4) + '\n' + line.substring(5) + "\n\n");
                        } else if (line.startsWith("CONNECT")) {
                            messageArea.appendText(line.substring(7) + " has connected.\n\n");
                        } else if (line.startsWith("MESSAGE")) {
                            messageArea.appendText(line.substring(8) + '\n');
                        } else if (line.startsWith("DISCONNECT")) {
                            messageArea.appendText(line.substring(10) + " has disconnected.\n\n");
                        }
                    } 
                } catch(IOException ioe) {
                    messageArea.appendText("Server is offline.\nPlease exit.");
                }

                return null;
            }
        };

        Thread severIO = new Thread(task);
        severIO.setDaemon(true);
        severIO.start();

        stage.show();
    }

    class NamePrompt implements Callable<String> {
        String message;

        NamePrompt(String message) {
            this.message = message;
        }

        @Override
        public String call() {
            TextInputDialog dialog = new TextInputDialog();
            dialog.setTitle("Welcome to Chatter");
            dialog.setHeaderText("Screen name selection");
            dialog.setContentText(message);
            dialog.setGraphic(null);
            return dialog.showAndWait().get();
        }
    }
}


我想专注于:


对线程处理的一般批评。
我处理客户端和客户端输入的方式。
我还想知道将flush用于流。似乎没有什么不同,但是会提高性能,尤其是在通过网络访问时?到目前为止,我只在本地测试。
服务器和服务器协议。
总体性能和效率。
我的设计选择的总体可扩展性。

关于最佳实践,高级库或欢迎使用库或程序的一般可读性。我还没有处理一些异常,这在我创建它时是有意的。

对于任何有兴趣的人来说,这是Github存储库。

#1 楼

服务器

private static String names() {
    StringBuilder nameList = new StringBuilder();

    for (String name : userNames) {
        nameList.append(", ").append(name);
    }

    return "In lobby: " + nameList.substring(2);
}


通过使用Collectors.joining(CharSequence, CharSequence, CharSequence)(感谢@ toto2!)通过String.join(CharSequence, Iterable)进行流式传输和收集,可以简化上述操作:

private static String names() {
    // return userNames.stream().collect(Collectors.joining(", ", "In lobby: ", ""));
    return "In lobby: " + String.join(", ", userNames);
}


也许您也可以考虑使用extends Thread代替ClientHandler ...当然,您也必须用implements Runnable代替new ClientHandler(socket).start()

private static void messageAll(String... messages) {
    if (!writers.isEmpty()){
        for (String message : messages) {
            for (PrintWriter writer : writers) {
                writer.println(message);
            }
        }
    }
}


我认为您不会首先检查是否有任何编写程序来获得最大的优化……这是一件很不错的事情,但是我宁愿做一个早期的new Thread(new ClientHandler(socket)).start()而不是嵌套代码块。基于流的相同方法可能是:

private static void messageAll(String... messages) {
    writers.forEach(w -> Stream.of(messages).forEach(w::println));
}


这里的另一个好处是return的检查也被内部化了,因此这样做是多余的

客户端

这里没有太多评论,除了也许您可以考虑将isEmpty()方法中基于String的比较替换为call()驱动的方法,例如作为(请考虑如何也可以正确访问enumout之类的变量,我没有考虑在内):

enum Action implements Consumer<String> {
    SUBMIT_NAME {
        @Override
        public void accept(String input) {
            FutureTask<String> futureTask = new FutureTask<>(
                                                new NamePrompt("Choose a screen name:"));
            Platform.runLater(futureTask);
            try {
                out.println(futureTask.get());
            } catch(InterruptedException | ExecutionException ex) {}
        }
    }, 
    // ...
    INFO {
        @Override
        public void accept(String input) {
            int delimiter = input.indexOf(',');
            messageArea.appendText(String.format("Users connected: %s%n%s%n%n", 
                    input.substring(0, delimiter), input.substring(delimiter + 1)));
        }
    }, 
    // ...
    DISCONNECT {
        @Override
        public void accept(String input) {
            messageArea.appendText(input + " has disconnected.\n\n");
        }
    };

    static void handle(String input) {
        EnumSet.allOf(Action.class).stream()
                .filter(v -> input.startsWith(v.toString()))
                .forEach(v -> v.accept(input.substring(v.toString().length())));
    }
}


处理完全在messageArea内完成方法,该方法在每个handle(String)toString()表示形式上均等地循环,同时注意通过执行enum将实际输入预格式化为“消费”。对于input.substring(v.toString().length())处理,只需要注意一件事:看起来您正在将客户数量硬编码为一位数,因此我自由地说明了如何通过策略性地使用以下方法来解决此问题:放置定界符,例如"INFO"

评论


\ $ \ begingroup \ $
实现Runnable的好处是,当最终使用ThreadPool和其他“复杂”的线程处理方式时,您无需更改任何内容,因为它们“消耗”了Runnable。
\ $ \ endgroup \ $
–马克·安德烈(Marc-Andre)
2015年9月15日14:31在

\ $ \ begingroup \ $
@ Marc-Andre-虽然我同意人们应该实现可运行的,而不是扩展线程,但具体原因是无效的-线程本身实现了Runnable,因此可以提交给服务;-)真正的问题是线程运行东西,而代码执行的是执行的东西,而不是运行的东西,所以如果您扩展Thread,则OOP是错误的,因为您的对象不是Thread,而是在线程上运行的东西。
\ $ \ endgroup \ $
–rolfl
2015年9月19日上午11:56

#2 楼

您可以拥有更多的课程
您的start(Stage stage)对于我想看到的内容做的太多了。您正在创建一个匿名类,其中包含读取套接字和向客户端呈现的逻辑。我将创建一个单独的类来提取该逻辑。这需要传递套接字和文本框,但是我敢肯定,可以通过构造函数或有一个封装逻辑部分的类来做到这一点。
枚举是您的朋友
目前,您可以直接在代码中使用命令的String。这很好,因为您只有两个类,但是有出错的机会。我将创建一个看起来像这样的Enum
public enum Command {

    SUBMIT_NAME("SUBMIT_NAME"),
    RESUBMIT_NAME("RESUBMIT_NAME"),
    NAME_ACCEPTED("NAME_ACCEPTED"),
    INFO("INFO"),
    CONNECT("CONNECT"),
    MESSAGE("MESSAGE"),
    DISCONNECT("DISCONNECT");

    private Command(final String pValue){
        value = pValue;
    }

    private String value;

    public String getValue() {
        return value;
    }
}

现在您只有一个来源,并且您不能输错命令(当然,您仍然可以,但是至少会出现错字像@rolfl一样,
Runnable
不继承Thread的充分理由是您没有修改/扩展Thread类的功能。您正在实现需要在Thread上运行的代码,这是Runnable接口的职责。 (他的评论都是对的,所以我建议您同时阅读它们。)
我对线程仍然不很了解,但是在某些时候,您可能会想使用ThreadPool或其他可以实现此功能的线程类管理线程(这是线程中的难点),您只需传递那些Runnable即可。
不要泄漏实现
您直接使用HashSet<>而不是使用Set<>。这与List和几乎所有接口相同。除非出于特定原因确实需要该特定实现,否则请使用该接口。然后,您的代码将不会被特定的实现锁定。

评论


\ $ \ begingroup \ $
仅供参考-Runnable或Thread,skeet说了什么
\ $ \ endgroup \ $
–rolfl
2015年9月19日在12:04

#3 楼

服务器


我认为您过度使用了static关键字。不要使每个函数都是静态的,请创建类的对象并改为使用该对象。
您有一些catch部分为空或仅打印很少的信息。我建议您对这些异常可能发生的不同原因进行更多调查,并添加一些有关您要忽略的情况的注释。
在继承上使用组合,不要扩展Thread。而是按照其他人的建议实现Runnable
这是start中的一种重要的ClientHandler方法。您应该将其切成单独的部分,例如:requestNameconnectedmessageLoop
如果将ConcurrentHashSet用作names,则无需使用synchronized (names)
总体看来,您正在同步太少,可能导致并发错误。在userNamesnameswriters中添加/删除时,您没有任何同步。
我不明白为什么会有usersConnected变量(这也不是任何同步操作的一部分)。为什么不使用一组变量的大小而不是此int变量?
userNamesnameswriters绑定在一起。我将其重构为Set<Client>,其中客户端具有userNamenamewriter。此集合最好是ConcurrentHashSet。请至少将您的类字段设置为private
您可以使用Java FXML文件来指定布局,而不用编程方式创建布局。
客户端和服务器都依赖于默认编码,例如InputStreamReader。考虑改用UTF-8。我相信,如果您使用findbugs工具,也会提醒您有关此问题。

FutureTask

此部分重复的代码:

FutureTask<String> futureTask = new FutureTask<>(new NamePrompt("Choose a screen name:"));
Platform.runLater(futureTask);
try {
    out.println(futureTask.get());
} catch(InterruptedException | ExecutionException ex) {}


您可以轻松地将其重构为createNamePromptTask("Choose a screen name:")

另外,您的FutureTask会在showAndWait上调用dialog,从而使其成为同步对话框。如果创建,显示对话框并且代码继续,我希望使用它。然后,当对话框关闭时,您可以使用执行out.println(/* get value of text field in dialog */);的事件侦听器或回调。您可以通过使用Dialog中的onXYZ属性之一来完成此操作。

服务器已离线

messageArea.appendText("Server is offline.\nPlease exit.");


您确定发生这种情况的唯一原因是服务器处于脱机状态吗?如果断开网络电缆怎么办?

此外,考虑到调用messageArea.appendText的次数,您可能希望提取message(String)或类似方法。

消息类型

似乎客户端一旦连接,就只能向服务器发送一种消息类型,即聊天消息。我建议您通过将类似“ MESSAGE”的字符串附加到从客户端发送到服务器的当前消息的开头来打开发送其他消息类型的可能性。

所有else-ifs本质上用作Map<String, Consumer<String>>,即:您检查消息开头的字符串,然后对其进行处理。

您可以重构此代码以使用真实的Map<String, Consumer<String>>,这可能导致代码更整洁并被处理消息要容易一些。只有一个小缺点(实际上我不认为这是个缺点):您的类别字符串(“ MESSAGE”,“ RESUBMIT_NAME”等)必须具有相同的长度,否则您可以使用空格字符作为消息类型和消息数据之间的分隔符。

请考虑以下事项:始终没有消息类型。您当前的代码行如下:

Map<String, Consumer<String>> handlers = new HashMap<>();
handlers.put("NAME", this::submitName);
handlers.put("INFO", str -> messageArea.appendText("Users connected: " + str.charAt(0) + '\n' + str.substring(1) + "\n\n"));
handlers.put("CONN", str -> messageArea.appendText(str + " has connected.\n\n"));
...


猜猜str是什么?是一个神奇的数字!

相反,您可以让一段代码来处理消息类型和消息数据的拆分:

messageArea.appendText(line.substring(8) + '\n');


>您的具体问题


线程处理:最好在循环中执行类似8之类的操作来检查线程是否已被中断。否则,看起来不错。
客户端和客户端输入:我不是if (Thread.interrupted()) break;的粉丝,它会导致同步对话框。在服务器showAndWaituserNamesnames中与您紧密相关的三个变量对我来说也是一种气味。
刷新流:您无需显式调用它,因为使用参数writers构造了PrintWriter
服务器协议:INFO消息有一个缺陷,即如果连接了10个或更多用户,它将无法正确显示。此外,就我个人而言,我更喜欢“类别”(INFO,SUBMIT_NAME等)具有相同的长度,如上所述。
性能和效率应该很好。
设计选择的可扩展性?最重要的是:避免true