Background
I'd like to send a large (30MB, but could be much larger in the future) amount of data using Java's non-blocking SocketChannel
Why non-blocking? So that the computation of the next bytes to send isn't blocked waiting for the network
When I use the SocketChannel in blocking mode, the transfer completes without a problem
When I set the SocketChannel to non-blocking, it completes significantly faster, but the server doesn't receive all of the data
The server does receive some of the data, though
Question
Why is my large (30MB) file transfer failing when using a non-blocking Java NIO SocketChannel, and how do I fix it?
Files
I gutted the program and wrote the example so it so that it will all run in one go via javac *.java && java Main
It creates a Future for the server, a Future for the client, has the client send 30MB of random bytes to the server, and then blocks on the main thread until both Futures complete (though the server never does)
Note: This is primarily about TheClient.java
If the line between the comments <CommentOutToMakeWork> and </CommentOutToMakeWork> is commented out, the SocketChannel will be blocking and the transfer will complete
Main.java:
import java.io.IOException;
import java.lang.InterruptedException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws ExecutionException, IOException, InterruptedException {
final SocketAddress address = new InetSocketAddress("127.0.0.1", 12345);
final int size = 30 * 1000 * 1000;
ExecutorService executor = Executors.newFixedThreadPool(2);
TheServer theServer = new TheServer(address, size);
TheClient theClient = new TheClient(address, size);
Future<String> serverFuture = executor.submit(theServer);
Thread.sleep(2000);
Future<String> clientFuture = executor.submit(theClient);
System.out.println("MAIN: Received from client: " + clientFuture.get());
System.out.println("MAIN: Received from server: " + serverFuture.get());
executor.shutdown();
}
}
TheClient.java:
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;
import java.util.concurrent.Callable;
class TheClient implements Callable<String> {
private TheClient() {}
public TheClient(SocketAddress address, int size) {
this.size = size;
this.from = new byte[size];
this.serverAddress = address;
new Random().nextBytes(from);
}
private int size;
private byte[] from;
private SocketAddress serverAddress;
public String call() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
System.out.println("CLIENT: Attempting to connect to server...");
socketChannel.connect(serverAddress);
// <CommentOutToMakeWork>
socketChannel.configureBlocking(false);
// </CommentOutToMakeWork>
System.out.println("CLIENT: Connection established. Sending " + size + " bytes.");
// For this example, this is one large write, but even my actual
// program, which uses a loop and puts smaller chunks onto the channel,
// is too fast for the SocketChannel.
socketChannel.write(ByteBuffer.wrap(from));
System.out.println("CLIENT: Write completed.");
return "CLIENT: Success!";
}
}
TheServer.java:
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.util.Random;
import java.util.concurrent.Callable;
class TheServer implements Callable<String> {
private TheServer() {}
public TheServer(SocketAddress address, int size) {
this.size = size;
this.to = new byte[size];
this.serverAddress = address;
}
private int size;
private byte[] to;
private SocketAddress serverAddress;
public String call() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open().bind(serverAddress);
System.out.println("SERVER: Awaiting connection...");
InputStream clientSocketInputStream = serverChannel.accept().socket().getInputStream();
System.out.println("SERVER: Connection established. Attempting to read " + size + " bytes.");
for (int i = 0; i < size; ++i) {
to[i] = (byte) clientSocketInputStream.read();
}
System.out.println("SERVER: Read completed.");
return "SERVER: Success!";
}
}
I believe the answer lies in the WritableByteChannel.write documentation:
Unless otherwise specified, a write operation will return only after writing all of the r requested bytes. Some types of channels, depending upon their state, may write only some of the bytes or possibly none at all. A socket channel in non-blocking mode, for example, cannot write any more bytes than are free in the socket's output buffer.
So it looks like you need to use the return value of write to find out how much has been written, and handle the case when it's not all been written. What isn't clear from the description is how you handle that case - you may find you need to do some scheduling to continue writing when the socket output buffer has drained, for example.
Related
My main objective is to create live streaming of encrypted voice chat from mic.
The encrypted audio is then transmitted over the network from one client to another.
The problem is that the audio is always getting stuttering and choppy while running the program (streaming).
I tried different types of hardware (PC, laptop, Raspberry Pi).
Different OSes as well.
Only sampling un-encrypted audio to eliminated any issue causes by the encryption algorithm.
Changing audio sample rate.
Unfortunately everything failed.
To make it simple, I only included the code needed to transmit the audio over the network without the encryption.
MAIN CLASS - both sender and receiver
package com.emaraic.securevoice;
import com.emaraic.securevoice.utils.AES;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import javax.sound.sampled.*;
public class SecureVoice
{
public static void main(String[] args)
{
Receiver rx = new Receiver();
rx.start();
Transmitter tx = new Transmitter();
tx.start();
}
public static AudioFormat getAudioFormat()
{ //you may change these parameters to fit you mic
float sampleRate = 8000.0f; //8000,11025,16000,22050,44100
int sampleSizeInBits = 16; //8,16
int channels = 1; //1,2
boolean signed = true; //true,false
boolean bigEndian = false; //true,false
return new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);
}
public static final String ANSI_BOLD = "\033[0;1m"; //not working in NetBeans
public static final String ANSI_RESET = "\033[0m";
public static final String ANSI_BLACK = "\033[30m";
public static final String ANSI_RED = "\033[31m";
public static final String ANSI_GREEN = "\033[32;4m";
public static final String ANSI_YELLOW = "\033[33m";
public static final String ANSI_BLUE = "\033[34m";
public static final String ANSI_PURPLE = "\033[35m";
public static final String ANSI_CYAN = "\033[36m";
public static final String ANSI_WHITE = "\033[37m";
}
SENDER
package com.emaraic.securevoice;
import com.emaraic.securevoice.utils.AES;
import java.io.*;
import java.io.File;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.sound.sampled.TargetDataLine;
public class Transmitter extends Thread
{
// these parameters must be copied and used in the Receiver class of the other client
private static final String TX_IP = "10.101.114.179"; //ip to send to
private static final int TX_PORT = 1034;
#Override
public void run()
{
SecureVoice color = new SecureVoice();
Mixer.Info minfo[] = AudioSystem.getMixerInfo();
System.out.println(color.ANSI_BLUE + "Detecting sound card drivers...");
for (Mixer.Info minfo1 : minfo)
{
System.out.println(" " + minfo1);
}
if (AudioSystem.isLineSupported(Port.Info.MICROPHONE))
{
try
{
DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, SecureVoice.getAudioFormat());
final TargetDataLine line = (TargetDataLine) AudioSystem.getLine(dataLineInfo); //recording from mic
line.open(SecureVoice.getAudioFormat());
line.start(); //start recording
System.out.println(color.ANSI_GREEN + "Recording...");
byte tempBuffer[] = new byte[line.getBufferSize()];
System.out.println(color.ANSI_BLUE + "Buffer size = " + tempBuffer.length + " bytes");
//AudioCapture audio = new AudioCapture(line); //capture the audio into .wav file
//audio.start();
while (true) //AES encryption
{
int read = line.read(tempBuffer, 0, tempBuffer.length);
byte[] encrypt = AES.encrypt(tempBuffer, 0, read);
// sendToUDP(encrypt);
sendToUDP(tempBuffer);
}
}
catch (Exception e)
{
System.out.println(e.getMessage());
System.exit(0);
}
}
}
public static void sendToUDP(byte soundpacket[])
{
try
{
// EncryptedAudio encrypt = new EncryptedAudio(soundpacket);
// encrypt.start();
DatagramSocket sock = new DatagramSocket();
sock.send(new DatagramPacket(soundpacket, soundpacket.length, InetAddress.getByName(TX_IP), TX_PORT));
sock.close();
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
RECEIVER
package com.emaraic.securevoice;
import com.emaraic.securevoice.utils.AES;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
public class Receiver extends Thread {
// these parameters must by used in the Transmitter class of the other client
private static final String RX_IP = "localhost";
private static final int RX_PORT = 1034;
#Override
public void run() {
byte b[] = null;
while (true) {
b = rxFromUDP();
speak(b);
}
}
public static byte[] rxFromUDP() {
try {
DatagramSocket sock = new DatagramSocket(RX_PORT);
byte soundpacket[] = new byte[8192];
DatagramPacket datagram = new DatagramPacket(soundpacket, soundpacket.length, InetAddress.getByName(RX_IP), RX_PORT);
sock.receive(datagram);
sock.close();
// return AES.decrypt(datagram.getData(),0,soundpacket.length); // soundpacket ;
return soundpacket; // if you want to hear encrypted form
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
}
public static void speak(byte soundbytes[]) {
try {
DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, SecureVoice.getAudioFormat());
try (SourceDataLine sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo)) {
sourceDataLine.open(SecureVoice.getAudioFormat());
sourceDataLine.start();
sourceDataLine.write(soundbytes, 0, soundbytes.length);
sourceDataLine.drain();
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
EXTRA LINK
http://emaraic.com/blog/secure-voice-chat
IDE Used
- Netbeans 11.1
Java JDK version
- Java 13 (Windows)
- OpenJDK11 (Linux)
Two problems. Network streamed data will have jitter in arrival time. Starting and stopping audio play will cause delay gaps and jitter due to OS and hardware driver overhead time. There is also the smaller problem of audio sample rate clock rate synchronization between the record and play systems. All of those can impact a continuous stream of audio samples at a fixed rate.
To avoid the audio start-up latency problem, don't stop your audio play or record system between network packets, always have audio data ready to play continuously at the current sample rate. To help cover network jitter, buffer some amount of audio data before starting playback, so there is always some audio ready to play even if the next network packet is sightly delayed.
You may have to gather some statistics on the audio startup and network latency and latency variation to determine a suitable amount to buffer. The alternative is an audio dropout concealment algorithm, which is far more complicated to implement.
I am trying to implement simple UDP communication between two sockets. In order to be sent in UDP packet, data needs to be converted to byte array. I created simple class named Packet which is used to store various data types. This class implements Serializable interface so it can be converted to byte array and sent over UDP protocol. Program worked fine until I put HashMap object inside Packet class. Error occurs on server side when recived data is being converted back to Packet object.
Here is client code:
package hr.fer.tel.rassus.udp.server;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
public class UDPClient {
final static int PORT = 10001; // server port
/**
* #param args the command line arguments
*/
public static void main(String args[]) throws IOException {
HashMap<Integer, Integer> vector = new HashMap<Integer, Integer>();
vector.put(1, 2);
Packet p = new Packet(10000,10001,"co2",0,vector);
byte[] serializedMessage = null;
try {
ByteArrayOutputStream bStream = new ByteArrayOutputStream();
ObjectOutput oo = new ObjectOutputStream(bStream);
oo.writeObject(p);
serializedMessage = bStream.toByteArray();
oo.close();
} catch (Exception ex) {
System.out.println("error "+ex.toString());
}
// determine the IP address of a host, given the host's name
InetAddress address = InetAddress.getByName("localhost");
// create a datagram socket and bind it to any available
// port on the local host
DatagramSocket socket = new DatagramSocket(); //SOCKET
// create a datagram packet for sending data
DatagramPacket packet = new DatagramPacket(serializedMessage, serializedMessage.length,
address, PORT);
// send a datagram packet from this socket
socket.send(packet); //SENDTO
socket.close(); //CLOSE
}
}
Server code:
package hr.fer.tel.rassus.udp.server;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UDPServer {
final static int PORT = 10001; // server port
/**
* #param args the command line arguments
*/
public static void main(String args[]) throws IOException {
byte[] rcvBuf = new byte[256]; // received bytes
// create a UDP socket and bind it to the specified port on the local
// host
DatagramSocket socket = new DatagramSocket(PORT); //SOCKET -> BIND
while (true) {
// create a DatagramPacket for receiving packets
DatagramPacket packet = new DatagramPacket(rcvBuf, rcvBuf.length);
// receive packet
socket.receive(packet); //RECVFROM
// construct a new String by decoding the specified subarray of
// bytes
// using the platform's default charset
ObjectInputStream iStream;
Packet p = null;
try {
iStream = new ObjectInputStream(new ByteArrayInputStream(packet.getData()));
p = (Packet) iStream.readObject();
iStream.close();
} catch (ClassNotFoundException ex) {
Logger.getLogger(UDPServer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
Packet class:
package hr.fer.tel.rassus.udp.server;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
/**
*
* #author adrianzgaljic
*/
public class Packet implements Serializable{
private int fromPort;
private int toPort;
private String co2;
private boolean flag;
private long timeStamp;
private HashMap<Integer, Integer> vector;
Set<Integer> keys;
public Packet(int from, int to, String co2, long time, HashMap<Integer, Integer> vector){
this.fromPort = from;
this.toPort = to;
this.co2= co2;
this.timeStamp = time;
this.vector = vector;
flag = false;
}
public Packet(int from){
this.fromPort = from;
this.flag = true;
}
public int getFromPort() {
return fromPort;
}
public int getToPort() {
return toPort;
}
public String getCo2() {
return co2;
}
public long getTimeStamp() {
return timeStamp;
}
public boolean isFlag() {
return flag;
}
public HashMap<Integer, Integer> getVector() {
return vector;
}
}
Stack trace:
Exception in thread "main" java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2325)
at java.io.ObjectInputStream$BlockDataInputStream.readUTFBody(ObjectInputStream.java:3063)
at java.io.ObjectInputStream$BlockDataInputStream.readUTF(ObjectInputStream.java:2864)
at java.io.ObjectInputStream.readUTF(ObjectInputStream.java:1072)
at java.io.ObjectStreamClass.readNonProxy(ObjectStreamClass.java:704)
at java.io.ObjectInputStream.readClassDescriptor(ObjectInputStream.java:830)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1990)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1915)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1798)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at hr.fer.tel.rassus.udp.server.UDPServer.main(UDPServer.java:48)
Java Result: 1
After running your code I realized that my previous answer was not solving the issue, however I will leave it along with the new one, because it describes an issue that may cause similar effect.
New answer:
Your server's datagram packet size is too small to receive all the data you are sending, increase its size by increasing the buffer size:
byte[] rcvBuf = new byte[256]; // received bytes
e.g. new byte[2048]
Old answer:
Swap these two lines in the client:
serializedMessage = bStream.toByteArray();
oo.close();
So you should have this:
oo.close();
serializedMessage = bStream.toByteArray();
The issue with your code is that you are getting the bytes, while ObjectOutputStream didn't actually flush all the data to your ByteArrayOutputStream.
I am not talking about threading or anything to make this more complicated.
Most server programs I saw are like this or while(true){...} (same concept).
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.io.DataOutputStream;
import java.io.IOException;
public class TCPServer {
ServerSocket welcomeSocket;
public TCPServer(int port) throws IOException {
welcomeSocket = new ServerSocket(port);
}
public void go() throws IOException {
// This is not a valid way to wait for a socket connection, You should
// not have a forever loop or while(true)
**for (; ;) {**
Socket connectionSocket = welcomeSocket.accept();
Scanner clientIn = new Scanner(connectionSocket.getInputStream());
DataOutputStream clientOut = new DataOutputStream(connectionSocket.getOutputStream());
String clientLine = clientIn.nextLine();
String modLine = clientLine.toUpperCase();
clientOut.writeBytes(modLine + "\n");
}
}
public static void main(String[] args){
try {
TCPServer server = new TCPServer(6789);
server.go();
}
catch(IOException ioe) {
ioe.printStackTrace();
}
}
}
It is not looping permanently, your code blocks on line welcomeSocket.accept() until someone connects and only after that next lines are executed then it waits for a new connection on welcomeSocket.accept(). In other words it loops as many times as it needs( per each connection ).
If you just want to allow only one client to connect, remove for (; ;) statement. But it will require to restart your server every time.
The while(!finished) option might be a better solution than the "empty" for loop. When the exit event occurs, you just set finished to true.
You can run a scheduler with any number of thread in the pool
and prevent the main thread from termination. Here I've used input stream but you can do it in different ways. Here you can use multiple threads to get connections and customize the frequency of the scheduler.
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
public class MyTest {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
#Test
public void hello() {
final Runnable helloServer = new Runnable() {
public void run() {
// handle soket connection here
System.out.println("hadling connection");
}
};
final ScheduledFuture<?> helloHandle = scheduler.scheduleAtFixedRate(helloServer, 1000, 1000, TimeUnit.MILLISECONDS);
try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
}
i am really puzzled with java nio,
package org.eclipse.java.nio.selector.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class MySelector {
public static void main(String[] args) throws IOException {
// Create selector
Selector selector = null;
selector = Selector.open();
////////////////////////////////////////////////////////////////////////
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(
"localhost", 4321));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
/*
* Let's begin select
*/
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) continue;
System.out.println("Hello, selector!");
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey )it.next();
if (key.isReadable()) {
System.out.println("It's readable!");
}
it.remove();
}
}
}
}
I want the selector to wait next input event from remote server, but it was fallen into a infinitely loop after the server reply any words, why?
i really can not understand, the 'remove' does not work?
I do not want to cancel or close the channel, i want to keep the connection, make the client wait for server's reply...
It is necessary to execute reading in the block of isReadable judge.
If you do not execute reading the data transmitted from the readable channel,
the channel is selected again. So the while loop doesn't stop.
while (it.hasNext()) {
SelectionKey key = (SelectionKey )it.next();
if (key.isReadable()) {
System.out.println("It's readable!");
// Added read operation
ByteBuffer buffer = ByteBuffer.allocate(1024);
Charset charset = Charset.forName("UTF-8");
buffer.clear();
if (socketChannel.read(buffer) < 0) {
// Client connection refused
socketChannel.close();
return;
}
buffer.flip();
System.out.println("Value = " + charset.decode(buffer).toString());
}
it.remove();
}
I think that the "remove" works. "Hello, Read selector!" might be repeatedly output.
Is there any better to way to check if I can receive a given IP Multicast transmission. Following code works fine but there is a problem in this code - it blocks the current thread until it gets the multicast packets.
Thank you.
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class MulticastTest {
static String MCAST_ADDR = "FF7E:230::1234";// "235.1.1.1";
static int DEST_PORT = 1234;
static int BUFFER_LENGTH = 16;
public static void main(String args[]) {
try {
byte[] b = new byte[BUFFER_LENGTH];
DatagramPacket dgram = new DatagramPacket(b, b.length);
MulticastSocket socket = new MulticastSocket(DEST_PORT);
socket.joinGroup(InetAddress.getByName(MCAST_ADDR));
socket.receive(dgram); // blocks until a datagram is received
System.err.println("Received " + dgram.getLength() + " bytes from " + dgram.getAddress());
dgram.setLength(b.length); // must reset length field!
} catch (Exception e) {
}
}
}
You could set a timeout-value with the method socket.setSoTimeout(int).
http://download.oracle.com/javase/1.4.2/docs/api/java/net/Socket.html#setSoTimeout(int)
If you don't receive any data within the timeout, a SocketTimeoutException is raised