M S*_*ker 4 java sockets network-programming nio socketchannel
我正在编写一个客户端服务器应用程序,我想从两个不同的线程(一个用于读取的线程,一个用于写入的线程)在一个套接字上读写。我的系统几乎可以正常工作,但是有一个令人困惑的错误,我似乎无法解决。阅读和写作相互独立的工作完美,但是当我开始从阅读Socket的OutputStream一个线程,写所有来电来InputStream在不同的线程块下去。
我编写了一个小型测试程序,用于快速重现该问题并消除尽可能多的外部变量。我使用java.nio的ServerSocketChannel和SocketChannel来建立连接,而我使用java.io的Socket(一个的基础套接字SocketChannel)则易于与ObjectInputStreamand 结合使用ObjectOutputStream。测试程序设计为运行两次;对于第一次运行,用户输入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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
服务器
预期产量:
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
Run Code Online (Sandbox Code Playgroud)
实际输出:
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
Run Code Online (Sandbox Code Playgroud)
该应用程序成功发送了第一个对象,但在第二个对象上无限期地阻塞。我可以看到的唯一区别是,第二个写调用是在单独线程上进行读操作时发生的。我的第一个直觉是,可能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
Run Code Online (Sandbox Code Playgroud)
实际输出:
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
Run Code Online (Sandbox Code Playgroud)
这确认客户端成功发送和接收了第一个对象。由于服务器中这种奇怪的阻止行为,客户端似乎正在等待服务器从未发送过的第二个对象。
提前非常感谢任何人可能提供的任何建议。如果可以通过另一种方式轻松实现全双工,我愿意重写我的代码,但是如果有使用上述结构的解决方案,我将更愿意坚持这样做,以简化不必重构大部分代码的工作。
此代码有太多错误,我将不得不逐行介绍:
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();
Run Code Online (Sandbox Code Playgroud)
上面没有什么可以“初始化连接”的。客户端初始化连接。此代码接受它。
System.out.println( "client connected" );
socketChannel.configureBlocking( true );
Run Code Online (Sandbox Code Playgroud)
这是默认值。您无需声明默认值。
while ( !socketChannel.finishConnect() )
Thread.sleep( 100 );
Run Code Online (Sandbox Code Playgroud)
你不应该这样称呼。finishConnect()适用于在非阻止模式下调用的客户端。您是服务器,尚未调用,也未处于非阻止模式。如果你是在非阻塞模式下的客户端你不应该把它与睡一个循环:你应该使用带。connect()connect()Selector.select()OP_WRITE
Socket socket = socketChannel.socket();
ObjectOutput objectOutput = new ObjectOutputStream( socket.getOutputStream() );
Run Code Online (Sandbox Code Playgroud)
当你正在使用阻塞模式和输出流,这是不可能明白你为什么要使用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();
}
Run Code Online (Sandbox Code Playgroud)
不要写这样的代码。像下面这样的代码,取决于上面的前一个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();
}
Run Code Online (Sandbox Code Playgroud)
同上。
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" );
}
Run Code Online (Sandbox Code Playgroud)
请参阅上面的内容,以了解为什么在从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 );
Run Code Online (Sandbox Code Playgroud)
上面的最后两行毫无意义,因为连接已完成,因为您处于阻止模式。
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" );
}
Run Code Online (Sandbox Code Playgroud)
您可以按原样使用其余的内容,但是NIO通道在这里也毫无意义。您不妨使用Socket。