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

Java-对全双工套接字输出流阻塞的写调用,java,sockets,network-programming,nio,socketchannel,Java,Sockets,Network Programming,Nio,Socketchannel,我正在编写一个客户机-服务器应用程序,我想从两个不同的线程(一个线程用于读取,一个线程用于写入)在一个套接字上读写。我的系统几乎可以正常工作了,但有一个令人困惑的错误,我似乎无法理解。读和写彼此独立地工作,但是当我开始在一个线程中从套接字的输出流读取时,所有对输入流的写入调用都会无限期地在不同的线程块中进行 我已经编写了一个小测试程序,用于快速重现问题,并尽可能多地消除外部变量。我使用java.nio服务器SocketChannel和SocketChannel来建立连接,我使用java.io的S

我正在编写一个客户机-服务器应用程序,我想从两个不同的线程(一个线程用于读取,一个线程用于写入)在一个套接字上读写。我的系统几乎可以正常工作了,但有一个令人困惑的错误,我似乎无法理解。读和写彼此独立地工作,但是当我开始在一个线程中从
套接字
输出流
读取时,所有对
输入流
的写入调用都会无限期地在不同的线程块中进行

我已经编写了一个小测试程序,用于快速重现问题,并尽可能多地消除外部变量。我使用
java.nio
服务器SocketChannel
SocketChannel
来建立连接,我使用
java.io
Socket
(一个
SocketChannel
的底层套接字)来方便使用
ObjectInputStream
ObjectOutputStream
。测试程序设计为运行两次;第一次运行时,用户输入
s
启动服务器,第二次运行时,用户输入
c
运行客户端

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

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

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
s可能不支持从不同线程同时读写,但我对堆栈溢出的搜索表明它们确实支持这种同时操作(全双工)。这是我对上述代码的操作感到困惑的主要原因

客户

预期产出:

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
这将确认客户端已成功发送和接收第一个对象。由于服务器中的这种奇怪的阻塞行为,客户端似乎正在等待服务器从未发送的第二个对象


提前非常感谢任何人提供的任何建议。如果全双工很容易以另一种方式实现,我愿意重写我的代码,但如果有一种使用上述结构的解决方案,我更愿意坚持这样做,因为不必重构大量代码。

这段代码有很多错误,我将不得不逐行处理:

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();
上面没有“初始化连接”的内容。客户端初始化连接。这段代码接受它

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

        while ( !socketChannel.finishConnect() )
            Thread.sleep( 100 );
你不应该叫这个
finishConnect()
适用于在非阻塞模式下调用了
connect()
的客户端。您是一个服务器,没有调用
connect()
,也没有处于非阻塞模式。如果您是处于非阻塞模式的客户机,则不应在带有睡眠的循环中调用它:您应使用
Selector.select()
OP\u CONNECT

        Socket socket = socketChannel.socket();
        ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );
当您使用阻塞模式和输出流时,根本不可能理解为什么要使用
ServerSocketChannel
SocketChannel
,事实上,这至少是问题的一部分。一个鲜为人知的事实是,从NIO通道派生的流在通道上使用同步进行读写,因此它们根本不是全双工的,即使底层TCP连接是全双工的。删除所有这些内容并使用
ServerSocket
Socket
重写

        // 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

如果有帮助的话,我正在Eclipse中运行Java9!感谢所有的反馈。这一切都很有道理。我最初开始使用nio是因为我要手动写入和读取字节。在做出选择之后,为了便于使用对象流,我又回到了普通套接字。关于从同步的SocketChannel创建的套接字中产生的流的部分正是我想要的。再次感谢!我可以证实。我做了你答案中建议的所有更改,我的代码运行完美无瑕。有朝一日我可能会回到nio,了解他们的选择器和其他特性,但对于这个项目,这非常有效。
        // 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();
            }