问题
I need a java 7 TCP/IP client that will block until it receives a user specified character sequence (in my case a message terminator/separator - this would automatically "chunk" the data into individual messages for further processing). I expected that this would be very standard code freely available on the web - but so far no luck.
Complicating things, "chunking" the received data using standard line separators (e.g. readLine() in Oracle's KnockKnock Client), is not possible since those characters are valid data inside the messages. The message format is an international standard and can't be changed.
After trying a few things (see below) I'm wondering if I'm taking the right approach. Is there a freeware example somewhere that I could draw on for inspiration? Or perhaps a class meeting my needs already exists somewhere in the depths of "rt.jar" or elsewhere. (BTW I used eclipse to take a look at rt.jar's contents - the huge number of packages/classes (according to http://www.findjar.com/jar/com.sun/jars/rt.jar.html?all=true JVM 6 contains 13200+ classes) makes a manual search impractical).
I've used Oracles example "KnockKnock" client as a starting point. My first thought was that all that would be necessary is to modify one line:
while ( (fromServer = in.readLine()) != null )
to something like:
while ( (fromServer = in.readLine( separator = UserSpecifiedRegExValue )) != null )
Unfortunately this extremely useful overloading/generalization of readLine() does not exist in Java.
Oracle's example works because readLine() blocks until it receives the line separator value on the TCP/IP link. My thinking was that a generalized verson of readLine() would also block until it received the user specified character string (i.e. the message terminator) thus giving me exactly what I want. Since that approach isn't available my next thought was to replace readLine() with a getNextMessage() function that would block until the user specified character string was received by TCP/IP. Based on other posts I came up with this function:
static String getNextMessage( java.io.BufferedReader MessageSource,
String EndOfMessage_RegEx )
{
try ( java.util.Scanner s = new java.util.Scanner( MessageSource ) )
{
return s.useDelimiter( EndOfMessage_RegEx ).hasNext() ? s.next() : "";
}
}
and tested it by emulating readLine(), passing in the O/S specific line separator, as done in this variant:
final static String LineSeparator = System.getProperty( "line.separator" ); // LineSeparator = ODOA (<CR><LF>) on Win7
final static String MessageSeparator = Pattern.quote( LineSeparator ); // MessageSeparator = 5C510D0A5C45 (the RegEx string "\Q<CR><LF>\E")
final static Pattern EndOfMessageRegEx = Pattern.compile( MessageSeparator );
static String getNextMessage( java.io.BufferedReader MessageSource )
// This function needs to block until a complete message (terminated by
// "EndOfMessageRegEx") is received by TCPIP from the other machine.
{
try ( java.util.Scanner s = new java.util.Scanner( MessageSource ).useDelimiter( EndOfMessageRegEx ) )
{
if ( s.hasNext() )
{
return s.next();
}
else
{
return "";
}
}
}
Unfortunately both versions always return the null string, immediately terminating my client - which makes sense if hasNext() does not block. (The hasNext() documentation says it "may" - i.e. not guaranteed to - block.) How do I get the blocking effect?
Another problem I see with both versions is that they pointlessly recreate a scanner every time the function is invoked.
Or am I forced into using the much more primitive approach of creating a buffer, using .read() and searching for the specified character string instead?
SOLUTION: Moved to accepted answer
回答1:
Consider this InputStream derived from PushbackInputStream:
public static class TerminatorInputString extends PushbackInputStream {
private String terminator;
public TerminatorInputString(InputStream inputStream, String string) {
super(inputStream, 256);
terminator = string;
}
public String nextMessage() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data = new byte[8];
int len = read(data, 0, data.length);
while(len > 0) {
baos.write(data, 0, len);
byte[] all = baos.toByteArray();
int idx = baos.toString().indexOf(terminator);
if(idx >= 0) {
String message = baos.toString().substring(0, idx);
byte[] unread = new byte[all.length-idx-terminator.length()];
System.arraycopy(all, idx+terminator.length(), unread, 0, unread.length);
super.unread(unread);
return message;
}
len = read(data, 0, data.length);
}
baos.flush();
return new String(baos.toByteArray());
}
}
It reads until terminator is found, then skips terminator and continues after that. End of stream will close final message.
Test frame:
public static void main(String[] args) {
try {
//System.in is a bad stream (if used in eclipse at least)
// - it will only flush and make data available
// on new line
TerminatorInputString tis = new TerminatorInputString(System.in, "SCHWARZENEGGER");
String message = tis.nextMessage();
while(message != null) {
System.out.println("MSG:>" + message + "<");
message = tis.nextMessage();
}
} catch (Exception e) {
e.printStackTrace();
}
}
With this input
oneSCHWARZENEGGERtwoSCHWARZENEGGERthreeSCHWARZENEGGER
produces this output:
MSG:>one<
MSG:>two<
MSG:>three<
回答2:
As per @kayman's suggestions, the solution has been moved here and improved to use InputStreamReader's character encoding option. In my case the encoding is predetermined, you may need to look at using getEncoding() instead.
This code, combined with using Scanner's useDelimiter() and the \Q\E form of regex expression (see below), worked for me when I used the results of System.getProperty( "line.separator" ) as the user specified line separator:
import java.io.*;
import java.net.*;
import java.util.Scanner;
import java.util.regex.Pattern;
///------------------------------------------------------------------------------
public class ZZ
{
final static String LineSeparator = System.getProperty( "line.separator" ); // ODOA (<CR><LF>) on Win7
final static String MessageSeparator = Pattern.quote( LineSeparator ); // 5C510D0A5C45 = RegEx string "\Q<CR><LF>\E" on Win7
final static Pattern EndOfMessageRegEx = Pattern.compile( MessageSeparator );
final static String CharacterEncoding = "US-ASCII"; // or UTF-8, UTF-16, ISO-8859-1, etc,
//----------------------------------------------------------------------------------
public static void main( String[] args )
throws IOException
{
String hostName = "localhost"; // = 127.0.0.1
int portNumber = 14576;
try (
Socket TcpipLink = new Socket( hostName, portNumber );
BufferedReader FromServer = new BufferedReader( new InputStreamReader( TcpipLink.getInputStream(), CharacterEncoding ) );
Scanner ReceivedData = new Scanner( FromServer ).useDelimiter( EndOfMessageRegEx );
) {
String ReceivedMessage;
while ( (ReceivedMessage = ReceivedData.next()) != null ) {
//Process the Inbound message
}
System.out.println( "Client fell out off message handler loop" ); // should never get here
}
catch ( UnknownHostException e ) {
System.err.println( "Don't know about host " + hostName );
System.exit( 1 );
}
catch ( IOException e ) {
System.err.println( "Could not connect to " + hostName + "on port" + portNumber );
System.exit( 1 );
}
System.out.println( "Client exited" );
} // end function main()
} // end class "ZZ"
来源:https://stackoverflow.com/questions/34183493/need-tcpip-client-that-blocks-until-a-specific-character-sequence-is-received