Java - 以全双工方式写入对套接字输出流阻塞的调用

时间:2018-02-04 02:02:22

标签: java sockets network-programming nio socketchannel

我正在编写一个客户端服务器应用程序,我希望在两个不同的线程(一个用于读取的线程,一个用于写入)的一个套接字上进行读写。我的系统几乎正常工作,但有一个令人困惑的错误,我似乎无法绕过我的脑袋。阅读和写作完美地相互独立,但当我开始在一个帖子中从Socket OutputStream开始阅读时,所有调用都以不同的方式写入InputStream线程无限期阻塞。

我已经编写了一个小型测试程序,用于快速复制问题并消除尽可能多的外部变量。我使用java.nio' ServerSocketChannelSocketChannel来设置连接,我使用java.io' s Socket(底层套接字) SocketChannel的{​​{1}}与ObjectInputStreamObjectOutputStream的易用性。测试程序设计为运行两次;对于第一次运行,用户输入s以启动服务器,在第二次运行时,用户输入c以运行客户端。

我的问题是:为什么在objectOutput.writeObject( message );方法中第二次调用server()时执行以下程序? (该方法中的第四行到最后一行)

我已经包含了预期的输出和实际输出以及我认为它们在程序代码之下的含义。

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class Main {

    private static final String IP_ADDRESS = "localhost";
    private static final int WELL_KNOWN_PORT = 4000;

    public static void main( String... args ) throws Exception {
        Scanner scanner = new Scanner( System.in );
        System.out.print( "choose (s)erver or (c)lient: " );
        char choice = scanner.nextLine().charAt( 0 );
        switch ( choice ) {
        case 's':
            server();
            break;
        case 'c':
            client();
            break;
        default:
            break;
        }
        scanner.close();
    }

    private static void server() throws Exception {

        // initialize connection

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind( new InetSocketAddress( WELL_KNOWN_PORT ) );
        System.out.println( "waiting for client to connect" );
        SocketChannel socketChannel = serverSocketChannel.accept();
        System.out.println( "client connected" );
        socketChannel.configureBlocking( true );
        while ( !socketChannel.finishConnect() )
            Thread.sleep( 100 );
        Socket socket = socketChannel.socket();
        ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );

        // write first object to stream

        Message message = new Message( 1 );
        System.out.println( "writing first object to object output stream: " + message );
        objectOutput.writeObject( message );
        System.out.println( "first object written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );

        // start reading in a separate thread

        new Thread( () -> {
            ObjectInput objectInput = null;
            try {
                objectInput = new ObjectInputStream( socket.getInputStream() );
            } catch ( IOException e ) {
                e.printStackTrace();
            }
            Message messageIn = null;
            try {
                System.out.println( "reading on object input stream" );
                messageIn = (Message) objectInput.readObject();
                System.out.println( "read object on object input stream: " + messageIn );
            } catch ( ClassNotFoundException | IOException e ) {
                e.printStackTrace();
            }
            System.out.println( messageIn );
        } ).start();
        Thread.sleep( 100 ); // allow time for object listening to start

        // write second object to stream

        message = new Message( 2 );
        System.out.println( "writing second object to object output stream: " + message );
        objectOutput.writeObject( message ); // this call seems to block??
        System.out.println( "second object written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );
    }

    private static void client() throws Exception {

        // initialize connection

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking( true );
        socketChannel.connect( new InetSocketAddress( IP_ADDRESS, WELL_KNOWN_PORT ) );
        while ( !socketChannel.finishConnect() )
            Thread.sleep( 100 );
        Socket socket = socketChannel.socket();
        ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );
        ObjectInput objectInput = new ObjectInputStream( socket.getInputStream() );

        // read first object

        System.out.println( "reading first object on object input stream" );
        Message message = (Message) objectInput.readObject();
        System.out.println( "read first object on object input stream: " + message );

        // read second object

        System.out.println( "reading second object on object input stream" );
        message = (Message) objectInput.readObject();
        System.out.println( "read second object on object input stream: " + message );

        // write confirmation message

        message = new Message( 42 );
        System.out.println( "writing confirmation message to object output stream: " + message );
        objectOutput.writeObject( message );
        System.out.println( "confirmation message written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );
    }

    private static class Message implements Serializable {

        private static final long serialVersionUID = 5649798518404142034L;
        private int data;

        public Message( int data ) {
            this.data = data;
        }

        @Override
        public String toString() {
            return "" + data;
        }
    }
}

服务器

预期产出:

choose (s)erver or (c)lient: s
waiting for client to connect
client connected
writing first object to object output stream: 1
first object written to object output stream
object output stream flushed
reading on object input stream
writing second object to object output stream: 2
second object written to object output stream
object output stream flushed
read object on object input stream: 42

实际输出:

choose (s)erver or (c)lient: s
waiting for client to connect
client connected
writing first object to object output stream: 1
first object written to object output stream
object output stream flushed
reading on object input stream
writing second object to object output stream: 2

应用程序成功发送第一个对象,但在第二个对象上无限期地阻塞。我能看到的唯一区别是第二次写调用是在一个单独的线程上进行读操作时发生的。我的第一直觉是,Socket可能不支持从不同线程同时读取和写入,但我对Stack Overflow的搜索表明它们确实支持这种同时操作(全双工)。这是我与上述代码操作混淆的主要原因。

客户端

预期产出:

choose (s)erver or (c)lient: c
reading on object input stream
read first object on object input stream: 1
reading second object on object input stream
read second object on object input stream: 2
writing confirmation message to object output stream: 42
confirmation message written to object output stream
object output stream flushed

实际输出:

choose (s)erver or (c)lient: c
reading first object on object input stream
read first object on object input stream: 1
reading second object on object input stream

这确认了客户端成功发送和接收了第一个对象。由于服务器中存在这种奇怪的阻塞行为,客户端似乎正在等待服务器永远不会发送的第二个对象。

非常感谢任何人可以提出的任何建议。如果可以通过其他方式轻松实现全双工,我可以重写我的代码,但如果有使用上述结构的解决方案,我更倾向于坚持使用,以简化不必重构大部分代码。

1 个答案:

答案 0 :(得分:4)

这段代码有很多问题我必须逐行采用:

private static void server() throws Exception {

        // initialize connection

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind( new InetSocketAddress( WELL_KNOWN_PORT ) );
        System.out.println( "waiting for client to connect" );

        SocketChannel socketChannel = serverSocketChannel.accept();

上面没有“初始化连接”。客户端初始化连接。此代码接受它。

        System.out.println( "client connected" );
        socketChannel.configureBlocking( true );

这是默认值。您不需要断言默认值。

        while ( !socketChannel.finishConnect() )
            Thread.sleep( 100 );

你不应该叫这个。 finishConnect()适用于以非阻止模式调用connect()客户端。您是服务器,尚未调用connect(),并且您未处于非阻止模式。如果您 是非阻塞模式的客户端,则不应在具有睡眠的循环中调用它:您应该将Selector.select()OP_WRITE一起使用。

        Socket socket = socketChannel.socket();
        ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );

当您使用阻止模式和输出流时,根本无法理解您使用ServerSocketChannelSocketChannel的原因,事实上这至少是问题的一部分。一个鲜为人知的事实是,从NIO通道派生的流在通道上使用同步进行读取和写入,因此它们根本不是全双工的,即使底层的TCP连接是。删除所有内容并使用ServerSocketSocket重写。

        // write first object to stream

        Message message = new Message( 1 );
        System.out.println( "writing first object to object output stream: " + message );
        objectOutput.writeObject( message );
        System.out.println( "first object written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );

        // start reading in a separate thread

        new Thread( () -> {
            ObjectInput objectInput = null;
            try {
                objectInput = new ObjectInputStream( socket.getInputStream() );
            } catch ( IOException e ) {
                e.printStackTrace();
            }

不要写这样的代码。像上面这样依赖于上述try块成功的代码必须在try块内。否则,例如以下代码可能会获得NullPointerExceptions

            Message messageIn = null;
            try {
                System.out.println( "reading on object input stream" );
                messageIn = (Message) objectInput.readObject();
                System.out.println( "read object on object input stream: " + messageIn );
            } catch ( ClassNotFoundException | IOException e ) {
                e.printStackTrace();
            }

同上。

            System.out.println( messageIn );
        } ).start();
        Thread.sleep( 100 ); // allow time for object listening to start

        // write second object to stream

        message = new Message( 2 );
        System.out.println( "writing second object to object output stream: " + message );
        objectOutput.writeObject( message ); // this call seems to block??
        System.out.println( "second object written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );
    }

请参阅上文,了解为什么在单独的线程中执行此操作不适用于从NIO通道派生的流。

    private static void client() throws Exception {

        // initialize connection

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking( true );
        socketChannel.connect( new InetSocketAddress( IP_ADDRESS, WELL_KNOWN_PORT ) );
        while ( !socketChannel.finishConnect() )
            Thread.sleep( 100 );

上面的最后两行是没有意义的,因为连接已经完成,因为你处于阻止模式。

        Socket socket = socketChannel.socket();
        ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );
        ObjectInput objectInput = new ObjectInputStream( socket.getInputStream() );

        // read first object

        System.out.println( "reading first object on object input stream" );
        Message message = (Message) objectInput.readObject();
        System.out.println( "read first object on object input stream: " + message );

        // read second object

        System.out.println( "reading second object on object input stream" );
        message = (Message) objectInput.readObject();
        System.out.println( "read second object on object input stream: " + message );

        // write confirmation message

        message = new Message( 42 );
        System.out.println( "writing confirmation message to object output stream: " + message );
        objectOutput.writeObject( message );
        System.out.println( "confirmation message written to object output stream" );
        objectOutput.flush();
        System.out.println( "object output stream flushed" );
    }

你可以按原样使用其余部分,但NIO频道在这里也毫无意义。您也可以使用Socket