File Descriptors
File descriptors are identifiers (handles) for files, sockets, pipes or other resources.
To work with file descriptors, Java provides us with java.io.FileDescriptor. junixsocket elevates the use of java.io.FileDescriptor
by
- allowing to send and receive them between processes (via UNIX domain sockets), and
- providing a sophisticated casting mechanism to access the referenced resource via Java classes such as
Socket
,InputStream
, etc. - passing file descriptors to new processes via
ProcessBuilder.Redirect
.
Getting a FileDescriptor reference
Anything that implements the junixsocket interface FileDescriptorAccess
(all AFSockets, for example), can return the corresponding FileDescriptor
:
AFSocket socket = ...;
FileDescriptor fd = socket.getFileDescriptor();
You can also get the file descriptor from some classes in the Java API, for example:
FileInputStream in = ...;
FileDescriptor fd = in.getFD();
RandomAccessFile raf = ...;
FileDescriptor fd = in.getFD();
// Standard file descriptors
FileDescriptor stdin = FileDescriptor.in;
FileDescriptor stdout = FileDescriptor.out;
FileDescriptor stderr = FileDescriptor.err;
Getting a FileDescriptor from a native fd integer value
You may use a somewhat “unsafe” operation to convert a system-native file descriptor, described as an integer value, to a FileDescriptor
(or other types, see below), via FileDescriptorCast.unsafeUsing(fdVal).as(FileDescriptor.class)
.
This functionality may not be available in all environments (e.g., on Windows, or when manually disabled by setting the system property -Dorg.newsclub.net.unix.library.disable.CAPABILITY_UNSAFE=true
).
Sending and receiving File Descriptors
A very useful feature of Unix Domain Sockets is the ability to send and receive file descriptors between processes.
This, for example, enables non-privileged processes to access otherwise restricted files: A privileged process (e.g., running as root, or as a UID with special access rights) opens the restricted file, and then exposes the file handle to another process via AF_UNIX sockets.
File descriptors are sent as so-called “ancillary messages” along with regulary payload. They cannot be sent alone, so make sure that you send at least some data, even if it's just one byte.
Sending file descriptors
AFUNIXSocket socket = ...
FileInputStream fin = new FileInputStream(file);
socket.setOutboundFileDescriptors(fin.getFD());
// you can also send more than one FD at the same time, just make sure they're all part of the same call to setOutboundFileDescriptors.
// Ancillary messages are sent _along_ regular in-band messages, so we have to send something here.
os.write("Some message".getBytes("UTF-8"));
Receiving file descriptors
AFUNIXSocket socket = ...
// set ancillary receive buffer to a reasonable size (disabled by default!)
socket.setAncillaryReceiveBufferSize(1024);
InputStream in = socket.getInputStream();
// do you regular socket IO here
in.read(...)
// If there were any file descriptors sent as ancillary messages, check them right after a call to read
FileDescriptor[] descriptors = socket.getReceivedFileDescriptors();
if (descriptors != null) {
for (FileDescriptor fd : descriptors) {
FileInputStream fin = new FileInputStream(fd);
// do something with the stream
}
}
Also see FileDescriptorCast
below for how to better handle file descriptors that describe sockets and other resources.
Even easier with RMI
If you're using junixsocket-rmi
for inter-process communication, you can simply wrap streams with
RemoteFileInput
/RemoteFileOutput
(or the generic RemoteFileDescriptor
) and not worry about the
technicalities:
FileInputStream fin = ...;
RemoteFileInput rfi = new RemoteFileInput(socketFactory, fin);
// rfi can now be used for inter-process communication via RMI:
someRMIService.someMethod(rfi);
// on the receiving side:
public void someMethod(RemoteFileInput rfi) throws IOException {
FileInputStream fin = rfi.asFileInputStream();
// ...
fin.close(); // closes the stream for this process only (different file handle).
}
Due diligence
It is recommended to authenticate the connection, since you certainly don't want unauthorized callers to obtain access to potentially secret information.
You may either use your own SSL/TLS authentication atop the Socket, or simply use peer credentials.
Not supported by all platforms
Not all platforms support file descriptors over AF_UNIX. Make sure they're available using
AFSocket.supports(AFSocketCapability.CAPABILITY_FILE_DESCRIPTORS)
.
“Casting” FileDescriptor objects to Socket etc.
When you receive a FileDescriptor from another process, you want to use it as if it was created locally. While the Java standard API does not provide us with much more than a FileDescriptor-taking constructor for java.io.FileInputStream
/FileOutputStream
, junixsocket gives you more control via FileDescriptorCast
:
First, you instantiate a FileDescriptorCast
instance using the FileDescriptor of your choice, then you specify as what class you want to access it:
FileDescriptor fd = ...;
// NOTE: check `fd.valid()` or an `IOException` may be thrown.
Class<T> desiredClass = ...;
T instance = FileDescriptorCast.using(fd).as(desiredClass);
For example, if you want to simply read from this file descriptor, use:
InputStream in = FileDescriptorCast.using(fd).as(InputStream.class);
If the file descriptor is a Socket, you can use:
Socket sock = FileDescriptorCast.using(fd).as(Socket.class);
// or:
AFSocket sock = FileDescriptorCast.using(fd).as(AFSocket.class);
// or:
AFUNIXSocket sock = FileDescriptorCast.using(fd).as(AFUNIXSocket.class);
// etc.
If you want to access the native file descriptor value as an integer (only where supported), you can use:
int fdVal = FileDescriptorCast.using(fd).as(Integer.class); // won't work for all types on Windows
Note that if the specified FileDescriptor
is incompatible with the target class, a ClassCastException
is thrown. Also be aware that this technically isn't a cast, since a different object reference is returned.
In order to check if a file descriptor can be “cast” to a particular class, you can use FileDescriptorCast#isAvailable
and FileDescriptorCast#availableTypes
:
FileDescriptorCast fdc = FileDescriptorCast.using(fd);
if (fdc.isAvailable(Socket.class)) {
Socket socket = fdc.as(Socket.class);
} else {
throw new IllegalStateException("Cannot cast to Socket, only to: " + fdc.availableTypes());
}
Sending a FileDescriptor via ProcessBuilder.Redirect
You can pass a FileDescriptor as standard input (“stdin”) to a process launched with ProcessBuilder
:
FileDescriptor fd = ...;
ProcessBuilder pb = ...;
pb.redirectInput(FileDescriptorCast.using(fd).as(ProcessBuilder.Redirect.class));
Process p = pb.start();
Not supported by all platforms
Note that this functionality is only available if the environment supports it (Java 9 or newer, and not on Windows). Moreover, the feature currently uses Java SDK internals that may change/disappear.
Be sure to check availability with
if (AFSocket.supports(AFSocketCapability.CAPABILITY_FD_AS_REDIRECT)) {...}
Important considerations
-
On some platforms (e.g., Solaris, Illumos) you may need to re-apply a read timeout (e.g., using
Socket#setSoTimeout(int)
after obtaining the socket. -
You may lose Java port information for
AFSocketAddress
implementations that do not encode this information directly (such asAFUNIXSocketAddress
andAFTIPCSocketAddress
) -
The “blocking” state of a socket may be forcibly changed to “blocking” when performing the cast, especially when casting to
Socket
,DatagramSocket
orServerSocket
and any of their subclasses where “blocking” is the expected state. -
When calling
FileDescriptorCast#using(FileDescriptor)
for aFileDescriptor
obtained from another socket or other resource in the same JVM (i.e., not from another process), especially for sockets provided by junixsocket itself, there is a chance that the garbage collector may clean up the original socket at an opportune moment, thereby closing the resource underlying the shared file descriptor prematurely.This is considered an edge-case, and deliberately not handled automatically for performance and portability reasons: We would have to do additional reference counting on all FileDescriptor instances, either through patching
FileCleanable
or a shared data structure.The issue can be prevented by keeping a reference to the original object, such as keeping it in an enclosing try-with-resources block or as a member variable, for example. Alternatively, using a “duplicate” file descriptor (via
FileDescriptorCast#duplicating(FileDescriptor)
circumvents this problem, at the cost of using additional system resources. -
As a consequence of the previous point: For
FileDescriptorCast#using(FileDescriptor)
: when casting file descriptors that belong to a junixsocket-controlled socket, the target socket is configured in a way such that garbage collection will not automatically close the target's underlying file descriptor (but still potentially any file descriptors received from other processes via ancillary messages). -
The same restrictions as for
#using(FileDescriptor)
apply to `#unsafeUsing(int) as well.