XModem via TCP for Java
807603Aug 10 2007 — edited Dec 18 2007I am sure that many of you experienced developers have read requests in the past concerning implmentation of Ward Christenen's XModem protocol over a TCP socket. If not, well... you are about to...
This is a major hack... but it is starting to come together... thanks to Fred Potter for his source code to start this project...
Objective:
Basically, I want to create a console application which accepts an incoming connection and starts the receive mode for a XModem file transfer. I am using CGTerm (for Commodore retrocomputing) but can test with HyperTerminal as well...
The user who connects to the server selects SEND and the FILE to send for a XModem file transfer... and the transfer begins...
The incoming blocks of 128 bytes are written to a file
After the transfer is over the server disconnects the client terminal.
Here is what I have so far:
import java.net.*;
import java.lang.*;
import java.io.*;
// X-Modem Server implementation via TCP/IP socket
public class XServer {
public static FileWriter fw;
public static void main(String[] args) throws IOException {
// define the file
try {
fw = new FileWriter("filename.txt");
} catch (Exception e) {
System.out.println(e);
System.exit(0);
}
int port = Integer.parseInt(args[0]);
ServerSocket server = new ServerSocket(port);
System.out.println("X-Server v1.0 - waiting for connection");
Socket client = server.accept();
// Handle a connection and exit.
try {
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
new PrintStream(outputStream).println("Go to send file mode!"); // sent to client
System.out.println("Ready to receive file via X-Modem...");
/**
* BEGIN TRANSFER HERE!
*/
// set the debug flag
XModem.debug = true;
/**
* Here we are instantiating a new InputStream that represents the remote
* file that we are receiving. In this single line we are attempting to
* start the flow.
*
* Behind The Scenes: We're sending a NAK across the serial line repeatedly
* until we finaly start seeing the data flow. If we don't see the data
* flow, then we throw an exception.
*/
System.out.println("Sending NAK to start receive mode...");
InputStream incomingFile;
try {
incomingFile = new XModemRXStream(inputStream, outputStream);
} catch (IOException e) {
System.out.println("ERROR! Unable to start file transfer!");
e.printStackTrace();
return;
}
System.out.println("Starting file transfer...");
/**
* Here we are reading from the incoming file, byte by byte, and printing out.
*
* Behind The Scenes: Internally, the read() method is handling the task of
* asking for the next data block from the remote computer, processing it (i.e.
* parsing, running checksums), and then putting it in an internal buffer. Not
* all calls to read() will request a new data block as each block contains at
* least 128 bytes of data. Sometimes you will only hit the buffer.
*/
try {
for (;;) {
int c = incomingFile.read();
if (c==-1)
break; // End of File
// print character / byte
System.out.print(c+",");
// write to file
try {
//System.out.print(".");
fw.write(c);
} catch (Exception e) {
System.out.println(e);
System.exit(0);
}
}
} catch (IOException e) {
System.out.println("error while reading the incoming file.");
e.printStackTrace();
return;
}
// done
System.out.println("File sent.");
new PrintStream(outputStream).println("");
new PrintStream(outputStream).println("transfer successful!");
} finally {
//client.close();
// save the file
try {
fw.close();
System.out.println("file saved.");
} catch (Exception e) {
System.out.println(e);
System.exit(0);
}
}
}
}
/**
* XModem keeps track of settings that the Receive and Transmit Stream classes will
* reference.
* <p>Copyright: Copyright (c) 2004</p>
* @author Fred Potter
* @version 0.1
*/
class XModem {
public static boolean debug = false;
}
/**
* XModemRXStream is an easy to use class for receiving files via the XModem protocol.
* @author Fred Potter
* @version 0.1
*/
class XModemRXStream
extends InputStream {
// CONSTANTS
private static final int SOH = 0x01;
private static final int EOT = 0x04;
private static final int ACK = 0x06;
private static final int NAK = 0x15;
private static final int CAN = 0x18;
private static final int CR = 0x0d;
private static final int LF = 0x0a;
private static final int EOF = 0x1a;
// block size - DON'T CHANGE - I toyed with the idea of adding 1K support but the code is NOT there yet.
private static final int bs = 128;
// PRIVATE STUFF
private int ebn; // expected incoming block #
private byte[] data; // our data buffer
private int dataPos; // our position with the data buffer
private InputStream in;
private OutputStream out;
/**
* Creates a new InputStream allowing you to read the incoming file. All of the XModem
* protocol functions are handled transparently.
*
* As soon as this class is instantiated, it will attempt to iniatate the transfer
* with the remote computer - if unsuccessful, an IOException will be thrown. If it
* is successful, reading may commense.
*
* NOTE: It is important not to wait too long in between calls to read() - the remote
* computer will resend a data block if too much time has passed or even just give up
* on the transfer altogether.
*
* @param in InputStream from Serial Line
* @param out OutputStream from Serial Line
*/
public XModemRXStream(InputStream in, OutputStream out) throws
IOException {
this.in = in;
this.out = out;
//
// Initiate the receive sequence - basically, we send a NAK until the data
// starts flowing.
//
init:for (int t = 0; t < 10; t++) {
if (XModem.debug) {
System.out.println("Waiting for response [ try #" + t + " ]");
}
long mark = System.currentTimeMillis();
out.write(NAK);
// Frequently check to see if the data is flowing, give up after a couple seconds.
for (; ; ) {
if (in.available() > 0) {
break init;
}
try {
Thread.sleep(10);
}
catch (Exception e) {}
if (System.currentTimeMillis() - mark > 2000) {
break;
}
}
}
// We have either successfully negotiated the transfer, OR, it was
// a failure and timed out. Check in.available() to see if we have incoming
// bytes and that will be our sign.
if (in.available() == 0) {
throw new IOException();
}
//
// Initialize some stuff
//
ebn = 1; // the first block we see should be #1
data = new byte[bs];
dataPos = bs;
}
/**
* Reads the next block of data from the remote computer. Most of the real XModem protocol
* is encapsulated within this method.
* @throws IOException
*/
private synchronized void getNextBlock() throws IOException {
if (XModem.debug) {
//System.out.println("Getting block #" + ebn);
}
//
// Read block into buffer. There is a 1 sec timeout for each character,
// otherwise we NAK and start over.
//
byte[] buffer;
recv:for (; ; ) {
buffer = new byte[bs + 4];
for (int t = 0; t < 10; t++) {
System.out.println("\nReceiving block [ #" + ebn + " ]");
// Read in block
buffer = new byte[buffer.length];
for (int i = 0; i < buffer.length; i++) {
int b = readTimed(1);
// if EOT - don't worry about the rest of the block.
if ( (i == 0) && (b == EOT)) {
buffer[0] = (byte) (b & 0xff);
break;
}
// if CAN - the other side has cancelled the transfer
if (b == CAN) {
throw new IOException("cancelled");
}
if (b < 0) {
if (XModem.debug) {
System.out.println("Time out... NAK'ing");
}
out.write(NAK);
continue recv;
}
else {
buffer[i] = (byte) (b & 0xFF);
}
}
break;
}
int type = buffer[0] & 0xff; // either SOH or EOT
if (type == EOT) {
if (XModem.debug) {
System.out.println("EOT!");
}
out.write(ACK);
break;
}
int bn = buffer[1] & 0xff; // block number
int bnc = buffer[2] & 0xff; // one's complement to block #
if (
(bn != ebn) && (bn != (ebn - 1)) ||
(bn + bnc != 255)) {
if (XModem.debug) {
System.out.println("NAK'ing type = " + type + " bn = " + bn +
" ebn = " +
ebn + " bnc = " + bnc);
}
out.write(NAK);
continue recv;
}
byte chksum = buffer[ (buffer.length - 1)];
byte echksum = 0;
for (int i = 3; i < (buffer.length - 1); i++) {
echksum = (byte) ( ( (echksum & 0xff) + (buffer[i] & 0xff)) & 0xff);
}
if (chksum != echksum) {
out.write(NAK);
continue recv;
}
out.write(ACK);
if (ebn == 255) {
ebn = 0;
}
else {
ebn++;
}
break;
}
// We got our block, now save it in our data buffer.
data = new byte[bs];
for (int i = 3; i < (buffer.length - 1); i++) {
data[(i - 3)] = buffer;
}
dataPos = 0;
}
public synchronized int read() throws IOException {
// If at the end of our buffer, refill it.
if (dataPos == bs) {
try {
getNextBlock();
}
catch (IOException e) {
throw new IOException();
}
}
// If we're still at end of buffer, say so.
if ( dataPos == bs) {
return -1;
}
int d = data[dataPos];
if (d == EOF)
return -1;
dataPos++;
return d;
}
/**
* A wrapper around the native read() call that provides the ability
* to timeout if no data is available within the specified timeout value.
* @param timeout timeout value in seconds
* @throws IOException
* @return int an integer representing the byte value read.
*/
private int readTimed(int timeout) throws IOException {
long start = System.currentTimeMillis();
for (; ; ) {
if (in.available() > 0) {
return (in.read());
}
try {
Thread.sleep(10);
}
catch (InterruptedException ex) {
}
//if (System.currentTimeMillis() - start > timeout * 1000) {
if (System.currentTimeMillis() - start > timeout * 5000) {
return -1;
}
}
}
}
Here was the output...
Original file:
(Commodore CBM SEQ file exported to PC using DirMaster)
��
� �
� ��� �� �� ��� ��
� �� �� ���� �� ��� ��
� ��� ����������������������������������������������
�� ����� ������� ����� �� ����� ������ ����� ���
� �� ������ ������ ��� ��� �� ��� ���� �� ������
� � ���
����
� � ��OWERED BY �OLOR 64 ��� V8
�UNNING �ETWORK64 V1.26A
�
�UPPORTING 38400 �AUD �ATES
�����/����/�������
�
�ESTING �CHO-�ET V1 BETA
�
�EATURING �ESSAGES, �ILES,
�ET�AIL, AND �NLINE �AMES!
�YS�P: � � � � � � � � �
�
�RESS ANY KEY TO LOGIN\C�
The result when the file was uploaded and received by my XServer:
??
? ?
? ??? ?? ?? ??? ??
? ?? ?? ???? ?? ??? ??
? ??? ??????????????????????????????????????????????
?? ????? ??????? ????? ?? ????? ?????? ????? ???
? ?? ?????? ?????? ??? ??? ?? ??? ???? ?? ??????
? ? ???
????
? ? ??OWERED BY ?OLOR 64 ??? V8
?UNNING ?ETWORK64 V1.26A
?
?UPPORTING 38400 ?AUD ?ATES
?????/????/???????
?
?ESTING ?CHO-?ET V1 BETA
?
?EATURING ?ESSAGES, ?ILES,
?ET?AIL, AND ?NLINE ?AMES!
?YS?P: ? ? ? ? ? ? ? ? ?
?
?RESS ANY KEY TO LOGIN\C?
The result is different!
Can someone help me along here... I have been trying to figure out how to do this for approx. a year or so... it has been a very slow process.
I could use a guru to help me out so I can write the upload and download routines for my Commodore BBS PETSCII Emulation Server.
Visit http://www.retrogradebbs.com for details.
Thanks.
Please help out a dedicated developer who is in over his head...
-Dave