首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Java网络框架

Java网络框架
EN

Code Review用户
提问于 2019-08-19 14:01:24
回答 1查看 219关注 0票数 3

因此,我刚刚编写了一个框架,它应该使使用本机java.net包创建基于网络的应用程序(客户机-服务器)变得更容易。我想知道我的代码的结构是否可以理解,以及是否有方法改进我的代码(在可读性和性能方面)。我提供了一个UML图,以便更容易地获得我编写的代码的快速概述:

下面是这些类的来源:

AbstractClient.java

代码语言:javascript
复制
package com.joshuafeld.net;

import java.io.IOException;
import java.net.Socket;

/**
 * A client can connect to a running server using native Java sockets. It can perform requests
 * and perform actions based on potential requests it can get from the server.
 */
public abstract class AbstractClient extends AbstractSocketWrapper {

    /**
     * Creates a new client with a socket that is connected to the specified port number on the
     * named host.
     *
     * <p>If the specified host is {@code null} it is the equivalent of specifying the address as
     * {@link java.net.InetAddress#getByName(String) InetAddress.getByName}{@code (null)}. In
     * other words, it is equivalent to specifying an address of the loopback interface.
     *
     * @param  address     The host name, or {@code null} for the loopback address.
     * @param  port        The port number.
     *
     * @throws IOException If an I/O error occurs when creating the underlying socket.
     */
    public AbstractClient(final String address, final int port) throws IOException {
        super(new Socket(address, port));
        new Thread(this).start();
    }

    /**
     * This is just used to guarantee consistency in the client-server system. It calls the
     * method {@link #handleMessage(String) handleMessage} which then can be extended by a subclass.
     * This is created in the hope of making the system more understandable for a new user.
     *
     * @param message The message to handle.
     */
    @Override
    /* default */ void handle(final String message) {
        handleMessage(message);
    }

    /**
     * Handles an incoming message from the server.
     *
     * @param message The incoming message.
     */
    public abstract void handleMessage(String message);
}

AbstractServer.java

代码语言:javascript
复制
package com.joshuafeld.net;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;

/**
 * A server that is based on the native Java sever socket implementation. A server waits for
 * requests to come in over the network. It performs some operation based on that request, and then
 * possibly returns a result to the requester.
 */
public abstract class AbstractServer {

    /**
     * Logs messages to the console.
     */
    private static final Logger logger = LoggerFactory.getLogger(AbstractServer.class);

    /**
     * Waits for requests to come in over the network.
     */
    private ServerSocket serverSocket;

    /**
     * The currently open socket connections.
     */
    private final List<ConnectionSocket> sockets;

    /**
     * Creates a server with a server socket, bound to the specified port. A port number of
     * {@code 0} means that the port number is automatically allocated, typically from an ephemeral
     * port range. This port number can then be retrieved by calling {@link #getPort() getPort}.
     *
     * @param  port                     The port number, or {@code 0} to use a port number that is
     *                                  automatically allocated.
     *
     * @throws IllegalArgumentException If the port parameter is outside the specified range of
     *                                  valid port values which is between 0 and 65535, inclusive.
     */
    public AbstractServer(final int port) {
        if (port < 0 || port > 0xFFFF) {
            throw new IllegalArgumentException("Port value out of range: " + port);
        }
        sockets = new LinkedList<>();
        try {
            serverSocket = new ServerSocket(port);
        } catch (final IOException exception) {
            logger.error("An I/O error occurred while opening the server socket", exception);
        }
        new Thread(new ConnectionAcceptor()).start();
    }

    /**
     * Closes the server socket as well as all connected sockets.
     */
    public void close() {
        try {
            serverSocket.close();
        } catch (final IOException exception) {
            logger.error("An I/O error occurred while closing the server socket", exception);
        }
        sockets.forEach(this::close);
    }

    /**
     * Closes the specified socket and removes it from the list of currently connected sockets.
     *
     * @param socket The socket to close.
     */
    public void close(final ConnectionSocket socket) {
        handleDisconnect(socket);
        sockets.remove(socket);
        socket.close();
    }

    /**
     * Broadcasts a message to all connected sockets/clients.
     *
     * @param message The message to broadcast.
     */
    public void broadcast(final String message) {
        sockets.forEach(socket -> socket.send(message));
    }

    /**
     * Handles an incoming message from the specified socket.
     *
     * @param socket  The socket at which the message arrived.
     * @param message The message.
     */
    public abstract void handleMessage(ConnectionSocket socket, String message);

    /**
     * Handles a newly connected socket.
     *
     * @param socket The newly connected socket.
     */
    public abstract void handleConnect(ConnectionSocket socket);

    /**
     * Handles a disconnected/closed socket.
     *
     * @param socket The closed socket.
     */
    public abstract void handleDisconnect(ConnectionSocket socket);

    /**
     * Returns the IP address to which the server socket is bound.
     *
     * <p>If the server socket was bound prior to being {@link #close() closed}, then this method
     * will continue to return the IP address after the server socket is closed.
     *
     * @return The IP address to which the server socket is bound.
     */
    public String getAddress() {
        return serverSocket.getInetAddress().getHostAddress();
    }

    /**
     * Returns the port number on which the server socket is listening.
     *
     * <p>If the server socket was bound prior to being {@link #close() closed}, then this method
     * will continue to return the port number after the server socket is closed.
     *
     * @return The port number on which the server socket is listening or -1 if the socket is not
     *         bound yet.
     */
    public int getPort() {
        return serverSocket.getLocalPort();
    }

    /**
     * Listens for a connection to be made to the server socket and accepts it.
     */
    private class ConnectionAcceptor implements Runnable {

        /**
         * Creates a new connection acceptor.
         */
        private ConnectionAcceptor() {
            // empty on purpose
        }

        /**
         * Listens for a connection to be made to the server socket and accepts it. The method
         * blocks until a connection is made or the server is {@link #close() closed}.
         */
        @Override
        public void run() {
            while (!serverSocket.isClosed()) {
                try {
                    final ConnectionSocket socket = new ConnectionSocket(serverSocket.accept());
                    handleConnect(socket);
                    new Thread(socket).start();
                    sockets.add(socket);
                } catch (final IOException exception) {
                    logger.error("An I/O error occurred while waiting for a connection", exception);
                }
            }
        }
    }

    /**
     * The socket that represents the connection to a specific client.
     */
    public class ConnectionSocket extends AbstractSocketWrapper {

        /**
         * Creates a new wrapped socket. The socket has to be connected already.
         *
         * @param socket The socket to wrap.
         */
        public ConnectionSocket(final Socket socket) {
            super(socket);
        }

        /**
         * Passes the handling of the message on to the method
         * {@link #handleMessage(ConnectionSocket, String) handleMessage}.
         *
         * @param message The message to handle.
         */
        @Override
        void handle(final String message) {
            handleMessage(this, message);
        }
    }
}

AbstractSocketWrapper.java

代码语言:javascript
复制
package com.joshuafeld.net;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.net.Socket;

/**
 * A simple wrapper for the native Java {@link Socket Socket} that automatically handles messages
 * as soon as they are received by the underlying socket.
 */
public abstract class AbstractSocketWrapper implements Runnable {

    /**
     * Logs messages to the console.
     */
    private static final Logger logger = LoggerFactory.getLogger(AbstractSocketWrapper.class);

    /**
     * The underlying socket/communication endpoint.
     */
    private final Socket socket;

    /**
     * Reads incoming messages from the input stream.
     */
    private BufferedReader input;
    /**
     * Writes messages to the output stream.
     */
    private PrintWriter output;

    /**
     * Creates a new wrapped socket. The socket has to be connected already.
     *
     * @param socket The socket to wrap.
     */
    public AbstractSocketWrapper(final Socket socket) {
        this.socket = socket;
        try {
            input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            output = new PrintWriter(socket.getOutputStream(), true);
        } catch (final IOException exception) {
            logger.error("An I/O error occurred while creating the input/output stream or the "
                    + "socket is not connected", exception);
        }
    }

    /**
     * Listens for new messages on the input stream and passes them on to the
     * {@link #handle(String) handle} method for further handling.
     */
    @Override
    public void run() {
        while (!socket.isClosed()) {
            try {
                input.lines().forEach(this::handle);
            } catch (final UncheckedIOException exception) {
                // This will happen if the input stream is closed. Because this is natural behavior
                // and not really an error, we will just ignore this and not log it into the
                // console.
                break;
            }
        }
    }

    /**
     * Sends a message to the connected socket. The message is terminated by a newline.
     *
     * @param message The message to send.
     */
    public void send(final String message) {
        if (!socket.isClosed()) {
            output.println(message);
        }
    }

    /**
     * Closes the socket as well as the input reader and output writer.
     *
     * <p>Once a socket has been closed, it is not available for further networking use (i.e. can't
     * be reconnected or rebound). A new socket needs to be created.
     */
    public void close() {
        try {
            socket.close();
            input.close();
        } catch (final IOException exception) {
            logger.error("An I/O error occurred while closing the socket/input", exception);
        }
        output.close();
    }

    /**
     * Handles a received message.
     *
     * @param message The message to handle.
     */
    /* default */ abstract void handle(final String message);
}

使用以下代码的简单日间服务器/客户端示例:

DaytimeServer.java

代码语言:javascript
复制
package com.joshuafeld.net.daytime;

import com.joshuafeld.net.AbstractServer;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DaytimeServer extends AbstractServer {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(
            "yyyy/MM/dd HH:mm:ss");

    public DaytimeServer() {
        super(13);
    }

    @Override
    public void handleConnect(final ConnectionSocket socket) {
        socket.send(DATE_FORMAT.format(new Date()));
        socket.close();
    }

    @Override
    public void handleMessage(final ConnectionSocket socket, final String message) {
        // nothing to do here
    }

    @Override
    public void handleDisconnect(final ConnectionSocket socket) {
        // nothing to do here
    }

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

DaytimeClient.java

代码语言:javascript
复制
package com.joshuafeld.net.daytime;

import com.joshuafeld.net.AbstractClient;

import java.io.IOException;

public class DaytimeClient extends AbstractClient {

    public DaytimeClient(String address) throws IOException {
        super(address, 13);
    }

    @Override
    public void handleMessage(final String message) {
        System.out.println(message);
        close();
    }

    public static void main(String[] args) throws IOException {
        new DaytimeClient("localhost");
    }
}
EN

回答 1

Code Review用户

回答已采纳

发布于 2019-08-19 14:39:47

我翻阅了你的个人资料,看到你是一个17岁的开发人员。我不知道那个年纪的OO是什么。9~10成熟。我这样做是因为我觉得你是一位经验丰富的专业人士。

AbstractSocketWrapper

  • 构造函数静默地捕获IOException。这将使套接字处于损坏状态。在堆栈上抛出任何异常,让更高级别的类决定如何处理损坏的套接字连接。
  • 方法run可以在套接字被优雅地关闭时( !socket.isClosed()或其他方式)中止:UncheckedIOException。但是容器类没有收到有关这方面的通知。也许你应该举办一个活动。如果没有,容器类应该定期检查是否存在挥之不去的套接字连接并关闭它们。
  • 方法send打印友好消息(带有新行)。这意味着API只能用于人类可读的消息,这限制了它的可用性。这种方法中的警卫也没有那么有用。断开连接的套接字应该被正确地处理,保护程序不能保证调用成功。
  • 方法close看起来很健壮,但是当socket.close();失败时,input.close();不会被调用,可能会导致不必要的行为(内存泄漏?)。

ConnectionAcceptor

  • 方法run的实现不当。在handleConnect(socket);之前执行sockets.add(socket);。正如您在示例服务器中所看到的,服务器可以关闭handleConnect中的套接字。这意味着,即使套接字已经关闭,它也会被添加。

AbstractServer

  • 方法close不具有鲁棒性。如果任何套接字未能关闭sockets.forEach(this::close);,其他套接字将不会关闭,从而导致持续的套接字连接。
  • 方法broadcast存在相同的问题,即向单个客户端的单播失败会阻止将其他客户端发送到。

General

  • 您的API不是线程安全。在操作服务器中的连接列表时,以及在拍摄要发送到的连接的快照时,请考虑使用锁定机制。
  • 您的API应该对异常更加健壮,并且应该检查是否存在挥之不去的连接。
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/226425

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档