我的套接字连接是否丢失了一些通过它发送的数据?

时间:2019-04-03 21:07:56

标签: java sockets gson locking race-condition

我正在编写一个Connection类,该类通过CommandCommandResult双向发送和接收数据。但是,当多个请求通过Connection快速发送时,有些请求无法正确通过。

这听起来像是种比赛条件,但我觉得我已经为此做好了准备:

  • 锁定和解锁写入套接字,
  • 有一张已发送的Command的表,其中尚未收到CommandResult
  • 并锁定和解锁对该表的更改。

Command是在单个线程上接收和处理的,因此这不应该成为问题。

我已经看过足够多次的代码,以至于我认为问题必须存在于其他地方,但是我的团队非常有信心Connection是罪魁祸首。

这个样本有点长,但是我觉得我可以做一个完整的例子,它是如此之小。我确实确保已对其进行了充分的记录。要了解的重要事项是:

  • AwaitWrapper只是未来。获取资源将一直阻塞,直到它被实际填充为止。
  • Message仅包装请求和响应,
  • Serializer基本上是gson包装器,
  • 使用通用的UUID跟踪
  • CommandCommandResult
  • ICommandHandler接受Command并输出CommandResultCommandCommandResult的内容对此无关紧要。

Connection.java:

public class Connection {

    private Socket socket;
    private ICommandHandler handler;
    private Serializer ser;
    private Lock resultsLock;
    private Lock socketWriteLock;
    private Map<UUID,AwaitWrapper<CommandResult>> reservations;

    public Connection(Socket socket) {
        ser = new Serializer();
        reservations = new TreeMap<UUID,AwaitWrapper<CommandResult>>();
        handler = null;
        this.socket = socket;

        // Set up locks
        resultsLock = new ReentrantLock();
        socketWriteLock = new ReentrantLock();
    }

    public Connection(String host, int port) throws UnknownHostException, IOException {
        socket = new Socket(host, port);
        ser = new Serializer();
        reservations = new TreeMap<UUID,AwaitWrapper<CommandResult>>();
        handler = null;

        // Set up locks
        resultsLock = new ReentrantLock(true);
        socketWriteLock = new ReentrantLock(true);
    }


    /* Sends a command on the socket, and waits for the response
     *
     * @param com The command to be sent
     * @return The Result of the command operation.
     */
    public CommandResult sendCommand(Command com) {
        try {
            AwaitWrapper<CommandResult> delayedResult = reserveResult(com);
            write(new Message(com));

            CommandResult res = delayedResult.waitOnResource();
            removeReservation(com);
            return res;

        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /* Sets handler for incoming Commands. Also starts listening to the socket
     *
     * @param handler The handler for incoming Commands
     */
    public void setCommandHandler(ICommandHandler handler) {
        if (handler == null) return;
        this.handler = handler;
        startListening();
    }

    /* Starts a thread that listens to the socket
     *
     * Note: don't call this until handler has been set!
     */
    private void startListening() {
        Thread listener = new Thread() {
            @Override
            public void run() {
                while (receiveMessage());
                handler.close();
            }
        };
        listener.start();
    }

    /* Recives all messages (responses _and_ results) on a socket
     *
     * Note: don't call this until handler has been set!
     *
     * @return true if successful, false if error
     */
    private boolean receiveMessage() {
        InputStream in = null;
        try {
            in = socket.getInputStream();

            Message message = (Message)ser.deserialize(in, Message.class);
            if (message == null) return false;

            if (message.containsCommand()) {
                // Handle receiving a command
                Command com = message.getCommand();
                CommandResult res = handler.handle(com);
                write(new Message(res));

            } else if (message.containsResult()) {
                // Handle receiving a result
                CommandResult res = message.getResult();
                fulfilReservation(res);
            } else {
                // Neither command or result...?
                return false;
            }

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }


    //--------------------------
    // Thread safe IO operations

    private void write(Message mes) throws IOException {
        OutputStream out = socket.getOutputStream();
        socketWriteLock.lock();
        ser.serialize(out, mes);
        socketWriteLock.unlock();
    }


    //----------------------------------
    //Thread safe reservation operations

    private AwaitWrapper<CommandResult> reserveResult(Command com) {
        AwaitWrapper<CommandResult> delayedResult = new AwaitWrapper<CommandResult>();

        resultsLock.lock();
        reservations.put(com.getUUID(), delayedResult);
        resultsLock.unlock();

        return delayedResult;
    }

    private void fulfilReservation(CommandResult res) {
        resultsLock.lock();
        reservations.get(res.getUUID()).setResource(res);
        resultsLock.unlock();
    }

    private void removeReservation(Command com) {
        resultsLock.lock();
        reservations.remove(com.getUUID());
        resultsLock.unlock();
    }


    //-------------------------------------------------------------------
    // A Message wraps both commands and results for easy deserialization

    private class Message {
        ...
    }
}

在监视Connection的接收方时,对于发送的某些Command永远不会触发处理程序。它应该由每个传入的Command触发并处理。

我正在考虑放弃保留表并锁定对套接字的写入,直到收到响应为止,但是我希望这样做不会有明显的性能损失。

我错过了一些阻止比赛条件的关键步骤吗?


编辑:为好奇的人添加SerializerICommandHandler类。

Serializer.java:

public class Serializer {

    private Gson gson;

    public Serializer() {
        gson = new Gson();
    }

    public Object deserialize(InputStream is, Class type) throws IOException {
        JsonReader reader = new JsonReader(new InputStreamReader(is, StandardCharsets.UTF_8));
        reader.setLenient(true);
        if (reader.hasNext()) {
            Object res = gson.fromJson(reader, type);
            return res;
        }
        return null;
    }

    public void serialize(OutputStream os, Object obj) throws IOException {
        JsonWriter writer = new JsonWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
        gson.toJson(obj, obj.getClass(), writer);
        writer.flush();
    }
}

ICommandHandler:

public interface ICommandHandler {
    public CommandResult handle(Command com);
    public void close();
}

1 个答案:

答案 0 :(得分:-1)

在您的情况下,锁不执行任何操作,那里没有竞争条件,如果同一处理程序用于多个套接字,则锁需要位于处理程序内部,您使用的是单线程客户端,因此锁可以注意:使用锁时,请使用try-finally。 如果您只是从Sockets开始,您可能不知道这一点,但是SocketChannel比极其古老的Socket类要高效得多。 如果没有看到SerializerICommandHandler,我将为您提供更多帮助。 Serializer中最有可能是问题。