Commit 6efb70e5 authored by 刘家荣's avatar 刘家荣 💬
Browse files

feat(Client & Server)

parent cf9c78c3
Loading
Loading
Loading
Loading
+214 −37
Original line number Diff line number Diff line
package network;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import model.Account;
import model.History;
import model.dataType.Step;
import model.dataType.*;
import model.game.Game;
import model.game.dataType.Msg;
import network.dataType.OnlineStatus;
import view.AppFrame;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * 饿汉模式单例,操作OnlineInfo
@@ -19,75 +27,244 @@ import java.net.Socket;
public class Client {
    private static Client instance = null;
    
    public Client getInstance() {
    public static Client getInstance() {
        return instance;
    }
    
    private Socket socket;
    private InputStream input;
    private OutputStream output;
    
    private Thread listener;
    
    public final String selfAddr = "localhost";
    public final String selfAddr;
    public final int selfPort = 19933;
    public final String serverAddr = "localhost";
    public final int serverPort = 19944;
    public final String serverAddr;
    public final int serverPort;
    
    /**
     * 连接(断线重连),是阻塞方法,成功会把对象赋给instance
     *
     * @param host 地址
     * @param port 端口
     * @throws ConnectException 连接失败异常
     */
    public static void connect(String host, int port) throws ConnectException{
    public static void connect(String host, int port, boolean isPlayer,
                               Runnable onSuccess, Runnable onFail, Runnable onClose) {
        if (instance != null) disconnect();
        instance = new Client(host, port);
    }
    private Client(String host, int port) throws ConnectException {
        new Thread(() -> {
            try {
            this.socket = new Socket(host, port);
            this.listener = new Thread(()->{
                //TODO
            });
                instance = new Client(host, port, isPlayer, onSuccess, onClose);
            } catch (IOException e) {
            throw new ConnectException("连接失败");
                onFail.run();
                disconnect();
            }
        }).start();
    }
    
    private Client(String host, int port, boolean isPlayer,
                   Runnable onSuccess, Runnable onClose) throws IOException {
        this.selfAddr = InetAddress.getLocalHost().getHostAddress();
        this.serverAddr = host;
        this.serverPort = port;
        this.socket = new Socket();
        socket.bind(new InetSocketAddress(this.selfPort));
        socket.connect(new InetSocketAddress(host, port));
        this.input = socket.getInputStream();
        this.output = socket.getOutputStream();
        sendAccount();
        OnlineInfo.status = isPlayer ? OnlineStatus.CLIENT_PLAYING : OnlineStatus.CLIENT_WATCHING;
        onSuccess.run();
        new Thread(() -> {
            Scanner scanner = new Scanner(input);
            while (scanner.hasNextLine()) {
                recvCallback(scanner.nextLine());
            }
            onClose.run();
            //JOptionPane.showMessageDialog(null, "房间已关闭", "提示", JOptionPane.WARNING_MESSAGE);
        }).start();
    }
    
    public static void disconnect() {
        instance.instanceDisconnect();
        instance = null;
    }
    private void instanceDisconnect(){
    
    private void instanceDisconnect() {
        try {
            output.close();
            socket.close();
        } catch (IOException ignored) {}
    }
    
    private void send(String msg) throws IOException{
        OutputStream os = socket.getOutputStream();
        PrintWriter pw = new PrintWriter(os);
    /**
     * 客户端JSON数据格式:
     * <pre>
     * {
     *     "type": "Account",
     *     "msg": Account Object
     * },
     * {
     *     "type": "Msg",
     *     "msg": Msg Object
     * },
     * {
     *     "type": "FlipStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "MoveStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "EatStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "Full"
     * }
     * </pre>
     */
    private void send(String msg) {
        new Thread(() -> {
            PrintWriter pw = new PrintWriter(output);
            pw.write(msg);
            pw.println();
            pw.flush();
        socket.shutdownOutput();
        }).start();
    }
    
    public void sendStep(Step step) throws IOException{
        this.send(JSON.toJSONString(step));
    private void sendAccount() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "Account");
        jsonObject.put("account", Account.SELF);
        PrintWriter pw = new PrintWriter(this.output);
        pw.write(JSON.toJSONString(jsonObject));
        pw.println();
        pw.flush();
    }
    
    public void sendMsg() throws IOException{
    public void sendStep(Step step) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", switch (step) {
            case FlipStep x -> "FlipStep";
            case MoveStep x -> "MoveStep";
            case EatStep x -> "EatStep";
            default -> throw new IllegalStateException("Unexpected value: " + step);
        });
        jsonObject.put("step", step);
        this.send(JSON.toJSONString(jsonObject));
    }
    
    /**
     * 发送消息,但是不调用前端绘制
     * <br/>只有服务器把消息发回来才会调用recvMsgCallback,它调用前端绘制
     * @param msg
     */
    public void sendMsg(Msg msg) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "Msg");
        jsonObject.put("msg", msg);
        this.send(JSON.toJSONString(jsonObject));
    }
    
    private void recvStepCallback(Step step){
    public void requestFull() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "Full");
        this.send(JSON.toJSONString(jsonObject));
    }
    
    /**
     * 服务器JSON数据格式:
     * <pre>
     * {
     *     "type": "Msg",
     *     "msg": Msg Object,
     * },
     * {
     *     "type": "FlipStep",
     *     "step": FlipStep Object,
     * },
     * {
     *     "type": "MoveStep",
     *     "step": MoveStep Object,
     * },
     * {
     *     "type": "EatStep",
     *     "step": EatStep Object,
     * },
     * {
     *     "type": "OnlineInfo",
     *     "onlineStatus" : onlineStatus    //仅代表游戏是否开始
     *     "serverAccount": Account,
     *     "playerAccount": Account,
     *     "watcherAccounts": Account List,
     * },
     * {
     *     "type": "Full",
     *     "game": Game Object,
     *     "history": History Object,
     *     "onlineStatus" : onlineStatus    //仅代表游戏是否开始
     *     "serverAccount": Account,
     *     "playerAccount": Account,
     *     "watcherAccounts": Account List,
     * }
     * </pre>
     */
    private void recvCallback(String line) {
        try {
            JSONObject json = JSONObject.parseObject(line);
            if (json.containsKey("type") && json.get("type") instanceof String type) {
                switch (type) {
                    case "Msg" -> recvMsgCallback(json.getObject("msg", Msg.class));
                    case "FlipStep" -> recvStepCallback(json.getObject("flipStep", FlipStep.class));
                    case "MoveStep" -> recvStepCallback(json.getObject("moveStep", MoveStep.class));
                    case "EatStep" -> recvStepCallback(json.getObject("eatStep", EatStep.class));
                    case "OnlineInfo" -> recvOnlineInfoCallback(
                        json.getObject("onlineStatus", OnlineStatus.class),
                        json.getObject("serverAccount", Account.class),
                        json.getObject("playerAccount", Account.class),
                        json.getList("watcherAccounts", Account.class)
                    );
                    case "Full" -> recvFullCallback(
                        json.getObject("game", Game.class),
                        json.getObject("history", History.class),
                        json.getObject("onlineStatus", OnlineStatus.class),
                        json.getObject("serverAccount", Account.class),
                        json.getObject("playerAccount", Account.class),
                        json.getList("watcherAccounts", Account.class)
                    );
                }
            }
        } catch (JSONException ignored) {
        }
    }
    
    
    private void recvStepCallback(Step step) {
        step = Game.instance.resolveStep(step);
        if (step != null) {
            History.instance.steps.add(step);
        } else {
            requestFull();
        }
    }
    
    private void recvMsgCallback(Msg msg) {
        Msg.msgList.add(msg);
        //TODO: 调前端添加Msg
    }
    
    private void recvOnlineInfoCallback(
                                  OnlineStatus onlineStatus, Account serverAccount, Account playerAccount, List<Account> watcherAccounts) {
        OnlineInfo.serverAccount = serverAccount;
        OnlineInfo.playerAccount = playerAccount;
        OnlineInfo.watcherAccounts = watcherAccounts;
        AppFrame.instance.repaint();
    }
    
    private void recvFullCallback(Game game, History history){
    private void recvFullCallback(Game game, History history,
                                  OnlineStatus onlineStatus, Account serverAccount, Account playerAccount, List<Account> watcherAccounts) {
        Game.instance = game;
        History.instance = history;
        recvOnlineInfoCallback(onlineStatus, serverAccount, playerAccount, watcherAccounts);
        AppFrame.instance.repaint();
    }
    
+290 −39
Original line number Diff line number Diff line
package network;

import com.alibaba.fastjson2.JSON;
import model.dataType.Step;
import model.game.dataType.Msg;
import com.alibaba.fastjson2.*;
import io.vavr.Tuple3;
import model.Account;
import model.History;
import model.dataType.*;
import model.game.Game;
import network.dataType.OnlineStatus;
import view.AppFrame;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * 饿汉模式单例,操作OnlineInfo
@@ -20,35 +31,101 @@ public class Server {
        return instance;
    }
    
    private Socket socket;
    private Tuple3<Socket, InputStream, OutputStream> player;
    private final Map<Account, Tuple3<Socket, InputStream, OutputStream>> watchers = new HashMap<>();
    
    private Thread listener;
    private Thread playerConnectListener;
    
    public static final String selfAddr = "localhost";
    public static final int selfPort = 19933;
    public String clientAddr = "localhost";
    public int clientPort = 19944;
    public final String selfAddr;
    public static final int playerPort = 10652;
    public static final int watcherPort = 10105;
    
    /**
     * 连接(断线重连),是阻塞方法,成功会把对象赋给instance
     *
     * @param host 地址
     * @param port 端口
     * @throws ConnectException 连接失败异常
     * @throws UnknownHostException 未知主机异常,不知道干啥的(?
     */
    public static void start(String host, int port) throws ConnectException{
    public static void start(String host, int port) throws UnknownHostException {
        if (instance != null) terminate();
        instance = new Server(host, port);
    }
    private Server(String host, int port) throws ConnectException {
    
    private Server(String host, int port) throws UnknownHostException {
        this.selfAddr = InetAddress.getLocalHost().getHostAddress();
        OnlineInfo.serverAccount = Account.SELF;
        OnlineInfo.status = OnlineStatus.SERVER_WAITING;
        
        // Waiting for player's connection
        this.playerConnectListener = new Thread(() -> {
            ServerSocket serverSocket = newServerSocket(playerPort);
            while (true) {
                try {
            this.socket = new Socket(host, port);
                    // 阻塞直到接受客户端的连接请求
                    Socket playerSocket = serverSocket.accept();
                    InputStream input = playerSocket.getInputStream();
                    OutputStream output = playerSocket.getOutputStream();
                    // 等待获取对方账号
                    Account account = listenForAccount(input);
                    if (account != null) {
                        // 有效账号,连接成功,可以开始游戏
                        this.player = new Tuple3<>(playerSocket, input, output);
                        OnlineInfo.playerAccount = account;
                        OnlineInfo.status = OnlineStatus.SERVER_PLAYING;
                        this.sendFull(account);
                        this.broadcastOnlineInfo();
                        
            this.listener = new Thread(()->{
                //TODO
                        // 监听玩家客户端消息
                        new Thread(() -> {
                            Scanner scanner = new Scanner(player._2);
                            while (scanner.hasNextLine()) {
                                recvCallback(scanner.nextLine(), account);
                            }
                            OnlineInfo.playerAccount = null;
                            OnlineInfo.status = OnlineStatus.SERVER_WAITING;
                            this.playerConnectListener.start();
                        }).start();
                        break;  // 跳出循环,结束线程
                    } else {
                        throw new IOException();
                    }
                } catch (IOException ignored) {}
            }
        });
        }catch (IOException e){
            throw new ConnectException("连接失败");
        this.playerConnectListener.start();
        
        // Listening for watcher's connection
        new Thread(() -> {
            ServerSocket serverSocket = newServerSocket(watcherPort);
            while (true) {
                try {
                    // 阻塞直到接受客户端的连接请求
                    Socket watcherSocket = serverSocket.accept();
                    Tuple3<Socket, InputStream, OutputStream> watcherTuple =
                        new Tuple3<>(watcherSocket, watcherSocket.getInputStream(), watcherSocket.getOutputStream());
                    // 等待获取对方账号
                    Account account = listenForAccount(watcherTuple._2);
                    if (account != null) {
                        // 有效账号,连接成功
                        new Thread(() -> {
                            synchronized (watchers) {watchers.put(account, watcherTuple);}
                            synchronized (OnlineInfo.watcherAccounts) {OnlineInfo.watcherAccounts.add(account);}
                            this.sendFull(account);
                            this.broadcastOnlineInfo();
                            // 监听客户端消息
                            Scanner scanner = new Scanner(watcherTuple._2);
                            while (scanner.hasNextLine()) {
                                recvCallback(scanner.nextLine(), account);
                            }
                            // 输入流结束,下线
                            synchronized (watchers) {watchers.remove(account);}
                            synchronized (OnlineInfo.watcherAccounts) {OnlineInfo.watcherAccounts.remove(account);}
                        }).start();
                    } else throw new RuntimeException();
                } catch (IOException | RuntimeException ignored) {}
            }
        }).start();
    }
    
    /**
@@ -58,36 +135,210 @@ public class Server {
        instance.instanceTerminate();
        instance = null;
    }
    
    private void instanceTerminate() {
        
    }
    
    private void send(String msg) throws IOException{
        OutputStream os = socket.getOutputStream();
        PrintWriter pw = new PrintWriter(os);
    private ServerSocket newServerSocket(int port) {
        ServerSocket serverSocket = null;
        while (serverSocket == null) {
            try {
                serverSocket = new ServerSocket(watcherPort);
            } catch (IOException e) {
                serverSocket = null;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {}
            }
        }
        return serverSocket;
    }
    
    /**
     * 服务器JSON数据格式:
     * <pre>
     * {
     *     "type": "Msg",
     *     "msg": Msg Object,
     * },
     * {
     *     "type": "FlipStep",
     *     "step": FlipStep Object,
     * },
     * {
     *     "type": "MoveStep",
     *     "step": MoveStep Object,
     * },
     * {
     *     "type": "EatStep",
     *     "step": EatStep Object,
     * },
     * {
     *     "type": "OnlineInfo",
     *     "onlineStatus" : onlineStatus    //仅代表游戏是否开始
     *     "serverAccount": Account,
     *     "playerAccount": Account,
     *     "watcherAccounts": Account List,
     * },
     * {
     *     "type": "Full",
     *     "game": Game Object,
     *     "history": History Object,
     *     "onlineStatus" : onlineStatus    //仅代表游戏是否开始
     *     "serverAccount": Account,
     *     "playerAccount": Account,
     *     "watcherAccounts": Account List,
     * }
     * </pre>
     */
    private void send(String msg, Account target) {
        new Thread(() -> {
            OutputStream output;
            if (target.equals(OnlineInfo.playerAccount))
                output = player._3;
            else if (watchers.containsKey(target)) {
                output = watchers.get(target)._3;
            } else return;
            PrintWriter pw = new PrintWriter(output);
            pw.write(msg);
            pw.println();
            pw.flush();
        socket.shutdownOutput();
        }).start();
    }
    private void broadcast(String msg) {
        send(msg, OnlineInfo.playerAccount);
        synchronized (watchers) {
            watchers.forEach((account, tuple3) -> send(msg, account));
        }
    }
    
    public void sendStep(Step step) throws IOException{
        this.send(JSON.toJSONString(step));
    /**
     * History只伴随着一次Step广播而增加
     * @param step
     */
    public void broadcastStep(Step step) {
        History.instance.steps.add(step);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", switch (step) {
            case FlipStep x -> "FlipStep";
            case MoveStep x -> "MoveStep";
            case EatStep x -> "EatStep";
            default -> throw new IllegalStateException("Unexpected value: " + step);
        });
        jsonObject.put("step", step);
        this.broadcast(JSON.toJSONString(jsonObject));
    }
    
    public void sendFull(){
    public void broadcastMsg(Msg msg) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "Msg");
        jsonObject.put("msg", msg);
        this.broadcast(JSON.toJSONString(jsonObject));
    }
    
    /**
     * 自己发送一条消息,前端的按钮应该要调这个
     * <br/>(套一层皮,不然好像有点诡异
     */
    public void addSelfMsg(Msg msg){
        recvMsgCallback(msg);
    }
    
    public void sendMsg() throws IOException{
    public void broadcastOnlineInfo(){
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "OnlineInfo");
        jsonObject.put("onlineStatus", OnlineInfo.status);
        jsonObject.put("serverAccounts", OnlineInfo.serverAccount);
        jsonObject.put("playerAccounts", OnlineInfo.playerAccount);
        jsonObject.put("watcherAccounts", OnlineInfo.watcherAccounts);
        this.broadcast(JSON.toJSONString(jsonObject));
    }
    
    public void sendFull(Account target) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", "Full");
        jsonObject.put("game", Game.instance);
        jsonObject.put("history", History.instance);
        jsonObject.put("onlineStatus", OnlineInfo.status);
        jsonObject.put("serverAccounts", OnlineInfo.serverAccount);
        jsonObject.put("playerAccounts", OnlineInfo.playerAccount);
        jsonObject.put("watcherAccounts", OnlineInfo.watcherAccounts);
        this.send(JSON.toJSONString(jsonObject), target);
    }
    
    private void recvStepCallback(Step step){
    
    /**
     * 客户端JSON数据格式:
     * <pre>
     * {
     *     "type": "Account",
     *     "msg": Account Object
     * },
     * {
     *     "type": "Msg",
     *     "msg": Msg Object
     * },
     * {
     *     "type": "FlipStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "MoveStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "EatStep",
     *     "step": FlipStep Object
     * },
     * {
     *     "type": "Full"
     * }
     * </pre>
     */
    private void recvCallback(String line, Account account) {
        try {
            JSONObject json = JSONObject.parseObject(line);
            if (json.containsKey("type") && json.get("type") instanceof String type) {
                switch (type) {
                    case "Msg" -> recvMsgCallback(json.getObject("msg", Msg.class));
                    case "FlipStep" -> recvStepCallback(json.getObject("step", FlipStep.class));
                    case "MoveStep" -> recvStepCallback(json.getObject("step", MoveStep.class));
                    case "EatStep" -> recvStepCallback(json.getObject("step", EatStep.class));
                    case "Full" -> sendFull(account);
                }
            }
        } catch (JSONException ignored) {}
    }
    
    private void recvMsgCallback(Msg msg) {
        Msg.msgList.add(msg);
        //TODO: 调前端绘制信息
        broadcastMsg(msg);
    }
    
    private void recvStepCallback(Step step) {
        step = Game.instance.resolveStep(step);
        if(step != null){
            AppFrame.instance.repaint();
            broadcastStep(step);
        }
    }
    
    /**
     * 监听客户端发送的账号数据,监听时阻塞
     */
    private Account listenForAccount(InputStream inputStream) {
        Scanner scanner = new Scanner(inputStream);
        if (scanner.hasNextLine()) {
            JSONObject json = JSONObject.parseObject(scanner.nextLine());
            if (json.containsKey("type") && json.get("type") instanceof String type && type.equals("Account")) {
                try {
                    return json.getObject("account", Account.class);
                } catch (JSONException ignored) {}
            }
        }
        return null;
    }
    
}