|
Welcome to the Core Java Technologies Tech Tips, September 9, 2003. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
Working with SocketChannels
Understanding AffineTransform
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.
This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net.
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
WORKING WITH SOCKETCHANNELS
The next time you have a networking task that has you reaching for the Web Services API or some other high level API, consider whether you can accomplish the same task more simply and with less overhead using Sockets. In this Tech Tip you will use the SocketChannel and ServerSocketChannel classes in the java.nio package to create a simple networked application.
When you use a browser to view a particular web page, the details are hidden from you. Open a browser and type in:
http://developer.java.sun.com/developer/JDCTechTips/
As a result, this entry connects you to port 80 of the web site. It also sends a particular HTTP request that contains the name of the page you want to view.
Now let's take these actions explicitly with a SocketChannel. The following program, TechTipReader, uses a SocketChannel to retrieve the page. Notice the getTips() method in the program. The method first instantiates a SocketChannel. It then uses the instance to connect to port 80 of developer.java.sun.com. Then the method sends a standard HTTP "GET" request to retrieve the JDCTechTips page.
The response from the server is printed to standard out.
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import java.net.InetSocketAddress;
import java.io.IOException;
public class TechTipReader {
private Charset charset =
Charset.forName("UTF-8");
private SocketChannel channel;
public void getTips() {
try {
connect();
sendRequest();
readResponse();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {}
}
}
}
private void connect() throws IOException {
InetSocketAddress socketAddress =
new InetSocketAddress(
"developer.java.sun.com", 80);
channel = SocketChannel.open(socketAddress);
}
private void sendRequest() throws IOException {
channel.write(charset.encode("GET "
+ "/developer/JDCTechTips/"
+ "\r\n\r\n"));
}
private void readResponse() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while ((channel.read(buffer)) != -1) {
buffer.flip();
System.out.println(charset.decode(buffer));
buffer.clear();
}
}
public static void main(String[] args) {
new TechTipReader().getTips();
}
}
Looking further into the TechTipReader program, notice that the connect() method creates an InetSocketAddress by passing in two parameters: a String that represents the domain, and the standard HTTP port 80. The call to the static method SocketChannel.open() takes the InetSocketAddress as a parameter. That call is equivalent to the following two steps:
channel = SocketChannel.open();
channel.connect(socketAddress);
The sendRequest() method sends the three-line request. The first line is "GET /developer/JDCTechTips/" and the second and third lines are empty. The call to channel.write() takes a ByteBuffer as its argument so you have to transform the String. Much of the time in working with SocketChannels involves converting to and from ByteBuffers. There are many ways to do the conversion, this example uses the encode() method of the java.nio.charset.Charset
class.
The readResponse() method is responsible for handling the response that is returned to the SocketChannel. The method reads from the SocketChannel into a ByteBuffer. As long as it hasn't reached the end of the file being transferred, the method flips the buffer from reading to being ready to write. The contents of the ByteBuffer are transformed by the charset.decode() method to a String that is sent to standard out.
When you run the TechTipReader program, you should see the HTML for the page displayed.
Now let's look at a second example. In this example, two numbers are added. The example involves a client and a server. When the client has two numbers to add, it sends the numbers to a server. The server then performs the addition and returns the sum. This example is based on the SocketChannel API. Other possible solutions could use Web services, RMI, or servlets.
In the example, the contents of the ByteBuffer are two ints. This allows you to restrict the ByteBuffer to eight bytes. It also allows you to create an IntBuffer that serves as a view into the ByteBuffer, as follows:
private ByteBuffer buffer = ByteBuffer.allocate(8);
private IntBuffer intBuffer = buffer.asIntBuffer();
The following program, SumClient, provides the client part of the example. In order to run SumClient, you first have start up the server part of the example, SumServer, which you can find later in this tip.
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class SumClient {
private SocketChannel channel;
private ByteBuffer buffer = ByteBuffer.allocate(8);
private IntBuffer intBuffer = buffer.asIntBuffer();
public void getSum(int i, int j) {
try {
channel = connect();
sendSumRequest(i, j);
receiveResponse();
} catch (IOException e) {
// add exception handling code here
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
// add exception handling code here
e.printStackTrace();
}
}
}
}
private SocketChannel connect()
throws IOException {
InetSocketAddress socketAddress =
new InetSocketAddress("localhost", 9099);
return SocketChannel.open(socketAddress);
}
private void sendSumRequest(int i, int j)
throws IOException {
buffer.clear();
intBuffer.put(0, i);
intBuffer.put(1, j);
channel.write(buffer);
System.out.println("Sent request for sum of "
+ i + " and " + j + "...");
}
private void receiveResponse()
throws IOException {
buffer.clear();
channel.read(buffer);
System.out.println(
"Received response that sum is "
+ intBuffer.get(0) + ".");
}
public static void main(String[] args) {
new SumClient().getSum(14, 23);
}
}
The getSum() method in SumClient connects a SocketChannel to a predetermined address. The connect() method is essentially the same as the one in the TechTipReader example. After calling connect(), the getSum() method passes two ints (in the example, 14 and 23) to the sendSumRequest() method, which, in turn, loads the ByteBuffer and writes the contents to the SocketChannel. The receiveResponse() method then takes the contents of the returned method and writes them to standard out.
When you run SumClient (after starting SumServer), it should display the following:
Sent request for sum of 14 and 23...
Received response that sum is 37.
Looking further at SumClient, notice that sendSumRequest() uses two different views of the same ByteBuffer. First, buffer.clear() discards the mark in the buffer, sets the position back to zero, and sets the limit back to its capacity. In effect it clears the buffer. Next, the ByteBuffer is viewed as a buffer that holds two ints using the intBuffer handle. The int passed in as the first parameter to sendSumRequest() is put in the first slot using intBuffer.put(0,i), and the second parameter is similarly put in the second slot. The write() method of SocketChannel takes a ByteBuffer, so the first view of the buffer is required.
The receiveResponse() method parallels sendSumRequest(). The buffer is again cleared. Then the contents of the SocketChannel are read into the ByteBuffer view of the buffer. Next, the IntBuffer is used to return the int in the first position.
Before moving on to the SumServer, the server part of the solution, you might want to put an additional safeguard into the places where you are calling the read() and write() methods on the SocketChannel object. These methods return a long with the number of bytes written or read. In this case you are expecting eight bytes. Find the following line inside of sendSumRequest():
channel.write(buffer);
Replace it with this check on the size of the buffer being written.
if (channel.write(buffer)!= 8){
throw new IOException("Expected 8 bytes.");
}
Put a similar guard around the channel.read() call in the receiveResponse() method.
Here's SumServer. To make the example work, you need to set up a server that listens for incoming requests in an agreed upon location. In the openChannel() method below the port 9099 is used. A call is made to the static method open() in ServerSocketChannel, the next line binds the socket to the specified port. As soon as channel.isOpen() returns true, a confirmation message is sent to standard out.
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.io.IOException;
import java.net.InetSocketAddress;
public class SumServer {
ByteBuffer buffer = ByteBuffer.allocate(8);
IntBuffer intBuffer = buffer.asIntBuffer();
ServerSocketChannel channel = null;
SocketChannel sc = null;
public void start() {
try {
openChannel();
waitForConnection();
} catch (IOException e) {
e.printStackTrace();
}
}
private void openChannel() throws IOException {
channel = ServerSocketChannel.open();
channel.socket().bind(
new InetSocketAddress(9099));
while (!channel.isOpen()) {
}
System.out.println("Channel is open...");
}
private void waitForConnection()
throws IOException {
while (true) {
sc = channel.accept();
if (sc != null) {
System.out.println(
"A connection is added...");
processRequest();
sc.close();
}
}
}
private void processRequest() throws IOException {
buffer.clear();
sc.read(buffer);
int i = intBuffer.get(0);
int j = intBuffer.get(1);
buffer.flip();
System.out.println("Received request to add "
+ i + " and " + j);
buffer.clear();
intBuffer.put(0, i + j);
sc.write(buffer);
System.out.println("Returned sum of " +
intBuffer.get(0) + ".");
}
public static void main(String[] args) {
new SumServer().start();
}
}
When you start SumServer, it should display the following:
Channel is open...
Sent request for sum of 14 and 23...
Received response that sum is 37.
Then, when you run SumClient, SumServer should display the following:
A connection is added...
Received request to add 14 and 23
Returned sum of 37.
In the waitForConnection() method, the application cycles until a request to connect to the ServerSocketChannel arrives. When a request does arrive, channel.accept() returns a SocketChannel, so the variable sc is no longer null. The incoming request is processed and then the SocketChannel is closed. The server then awaits another connection request. Note that in this simple example requests are processed fully one at a time. A different structure would be required for a more sophisticated server that handles many concurrent requests but that is beyond the scope of this tip.
The processRequest() method is much like the corresponding methods in the SumClient. The buffer is cleared, the channel is read into the buffer. Then the IntBuffer view is used to retrieve the two ints from the buffer. The buffer is then cleared and the sum is loaded back in using the IntBuffer view. The buffer is then written back to the SocketChannel.
For more information about SocketChannels, see the technical
article "New I/0 Functionality for Java 2 Standard Edition 1.4".
UNDERSTANDING AFFINETRANSFORM
Included in the Java 2D classes is the class java.awt.geom.AffineTransform which allows you to perform affine transformations. An affine transformation transforms the coordinates of a two-dimensional image in such a way that parallel lines remain parallel. For example, if you start with a rectangle, an affine transformation might produce a parallelogram in a different location, but lines remain lines and parallelism is preserved. Affine transformations can be translations, rotations, scaling (which includes flips), and shears.
The documentation for the AffineTransform class explains that:
Such a coordinate transformation can be represented by a 3 row
by 3 column matrix with implied last row of [0 0 1]. This
matrix transforms source coordinates (x,y) into destination
coordinates (x',y') by considering them to be a column vector
and multiplying the coordinate vector by the matrix according
to the following process:
[ x'] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ]
[ y'] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ]
[ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ]
This Tech Tip explains why you need a 3 by 3 matrix to express the affine transformations of two-dimensional space. It relates the matrices for the fundamental transformations to the method calls in the AffineTransform class.
In this tip, you'll run the following program, AffineChecker, to compare AffineTransform transformations to matrix multiplications. Notice the three instance variables of type Point2D.Double in the program. These correspond to a starting point that has coordinate (2,3), and the points that result from each of the transformations, respectively.
import java.awt.geom.Point2D;
import java.awt.geom.AffineTransform;
public class AffineChecker {
private static Point2D startingPoint
= new Point2D.Double(2,3);
private static Point2D affineResult
= new Point2D.Double();
private static Point2D matrixResult
= new Point2D.Double();
public static void matrixMultiply(
double m00, double m01,
double m10, double m11){
double x = m00 * startingPoint.getX() +
m01 * startingPoint.getY();
double y = m10 * startingPoint.getX() +
m11 * startingPoint.getY();
matrixResult.setLocation(x,y);
}
public static void perform(AffineTransform at){
at.transform(startingPoint,affineResult);
}
public static void report(){
System.out.println("Affine Result = ("
+ affineResult.getX() + " , "
+ affineResult.getY() + ")");
System.out.println("Matrix Result = ("
+ matrixResult.getX() + " , "
+ matrixResult.getY() + ")");
if (affineResult.distance(matrixResult)<.0001){
System.out.println(" No real difference");
}
}
}
The matrixMultiply() method in AffineChecker provides the standard matrix multiplication:
[ m00 m01 ] [ x ] [ m00x + m01y]
[ m10 m11 ] [ y ] = [ m10x + m11y]
The report() method in the program prints the results for the transformation and the matrix multiplication, and indicates whether the results are close enough to be essentially the same.
As a first test, let's do nothing to the starting point and then make comparisons. To do this, run the following program:
import java.awt.geom.AffineTransform;
public class NothingChecker {
public static void main(String[] args){
System.out.println(
"The result of doing nothing:");
AffineChecker.perform(
AffineTransform.getRotateInstance(0));
AffineChecker.matrixMultiply(1,0,
0,1);
AffineChecker.report();
}
}
NothingChecker creates a new rotation transformation and passes in the degrees to be rotated (in this case, 0) using the static method call:
AffineTransform.getRotateInstance(0)
NothingChecker then calls the transform() method (through AffineChecker's perform() method) on the returned AffineTransform. Note that the transform() method is passed the startingPoint and affineResult to transform the point (2,3) and store the resulting point in affineResult. Next, NothingChecker compares the Affine Result with the result of multiplying by the 2 by 2 identity matrix:
[ 1 0 ]
[ 0 1 ]
When you run NothingChecker, you should see the following:
The result of doing nothing:
Affine Result = (2.0 , 3.0)
Matrix Result = (2.0 , 3.0)
No real difference
In other words, you should find that the result of both transforms is the point (2,3).
You can scale in the x and/or y directions using the getScaleInstance() static method of AffineTransform. Try to scale the x direction by a factor of four and the y direction by a factor of five. Here is the matrix multiplication that corresponds to this scaling.
[ 4 0 ] [ x ] [ 4x ]
[ 0 5 ] [ y ] = [ 5y ].
The following program shows the call to getScaleInstance(). You can run the program to compare the transformation to the matrix multiplication:
import java.awt.geom.AffineTransform;
public class Scale4Xand5YChecker {
public static void main(String[] args) {
System.out.println(
"The results of scaling four " +
"times in the x direction " +
"and five times in the y:");
AffineChecker.perform(
AffineTransform.getScaleInstance(4,5));
AffineChecker.matrixMultiply(4,0,
0,5);
AffineChecker.report();
}
}
The results should look like this:
The results of scaling four times in the x direction and five
times in the y:
Affine Result = (8.0 , 15.0)
Matrix Result = (8.0 , 15.0)
No real difference
Again, the results are the same.
You can reflect about the x and/or y axes by entering a negative number into one of the scaling coefficients. For example,
[ -1 0 ] [ x ] [ -x ]
[ 0 1 ] [ y ] = [ y ]
is a reflection in the x direction (reflecting about the y axis). Here is a program that performs the reflection and compares it to the matrix multiplication:
import java.awt.geom.AffineTransform;
public class FlipChecker {
public static void main(String[] args) {
System.out.println(
"The results of flipping over" +
"the y-axis:");
AffineChecker.perform(
AffineTransform.getScaleInstance(-1,1));
AffineChecker.matrixMultiply(-1, 0,
0, 1);
AffineChecker.report();
}
}
and the results are:
The results of flipping over the y-axis:
Affine Result = (-2.0 , 3.0)
Matrix Result = (-2.0 , 3.0)
No real difference
A rotation expects its argument to be given in radians and not degrees. As a reminder, you can easily convert from degrees to radians by multiplying by pi and dividing by 180 degrees. For example, thirty degrees corresponds to pi/6. To rotate through pi/6 radians you multiply by the rotation matrix:
[ cos pi/6 -sin pi/6 ]
[ sin pi/6 cos pi/6 ]
You might recall that one characteristic of rotation matrices is that their determinant is 1. In code, you check the results like this:
import java.awt.geom.AffineTransform;
public class RotateThirtyChecker {
public static void main(String[] args) {
System.out.println(
"The results of a thirty degree"
+ " counter clockwise rotation:");
AffineChecker.perform(
AffineTransform.getRotateInstance(
Math.PI/6));
AffineChecker.matrixMultiply(
Math.cos(Math.PI/6), -Math.sin(Math.PI/6),
Math.sin(Math.PI/6), Math.cos(Math.PI/6));
AffineChecker.report();
}
}
Run RotateThirtyChecker and get:
The results of a thirty degree counter clockwise
rotation:
Affine Result =
(0.23205080756887764 , 3.598076211353316)
Matrix Result =
(0.23205080756887764 , 3.598076211353316)
No real difference
A shearing transform changes the shape of the transformed object. The coefficients shx and shy indicate how much the x direction is transformed as a factor of the y coordinate, and how much the y direction is transformed as a factor of the x coordinate. This is the transformation that turns a rectangle into a parallelogram. The matrix representation is
[ 1 shx ] [ x ] [ x + y (shx) ]
[ shy 1 ] [ y ] = [ y + x (shy) ].
The code to do the transform and compare it to the matrix multiplication is this:
import java.awt.geom.AffineTransform;
public class ShearChecker {
public static void main(String[] args) {
System.out.println(
"The results of shearing:");
AffineChecker.perform(
AffineTransform.getShearInstance(5,6));
AffineChecker.matrixMultiply(1, 5,
6, 1);
AffineChecker.report();
}
}
Run ShearChecker and get:
The results of shearing:
Affine Result = (17.0 , 15.0)
Matrix Result = (17.0 , 15.0)
No real difference
What remains is, in some sense, the easiest transform. It's a translation that just moves the shape over by some fixed amount. To move tx in the x direction and ty in the y direction you perform the following addition.
[ tx ] [ x ] [ tx + x ]
[ ty ] + [ y ] = [ ty + y ].
The problem is that until now, every transformation has been expressable in terms of multiplication with a 2 by 2 matrix. One way to combine these ideas is that all transformations can be written using the following.
[ m00 m01 ] [ x ] [ tx ] [ m00x + m01y + tx ]
[ m10 m11 ] [ y ] + [ ty ] = [ m10x + m11y + ty ]
You can write the left side more simply if you add a third row like this.
[ m00 m01 tx ] [ x ] [ m00x + m01y + tx ]
[ m10 m11 ty ] [ y ] = [ m10x + m11y + ty ]
[ 0 0 1 ] [ 1 ] [ 1 ]
Now all of these building block transformations can be written in terms of the six coefficients m00, m01, m10, m11, tx, and ty. Note that the order matters. This corresponds to applying the top left 2 by 2 matrix and then performing the translation. Add these two methods to AffineChecker.
private static void translate(double tx, double ty){
matrixResult.setLocation(
matrixResult.getX() + tx,
matrixResult.getY() + ty);
}
public static void matrixTransform(
double m00, double m01,
double m10, double m11,
double tx, double ty){
matrixMultiply(m00,m01,m10,m11);
translate(tx,ty);
}
You can now call any of the basic transformations using the matrixTransform() method. For example, you can replace the call in ShearChecker to matrixMultiply(1,5,6,1) with a call to the new method matrixTransform(1,5,6,1,0,0). A translation can also be called using the matrixTransform() method as is shown in the following program:
import java.awt.geom.AffineTransform;
public class TranslationChecker {
public static void main(String[] args) {
System.out.println("The results of translating " +
"by (-2,5):");
AffineChecker.perform(
AffineTransform.getTranslateInstance(-2,5));
AffineChecker.matrixTransform(1,0,0,1,-2,5);
AffineChecker.report();
}
}
Run TranslationChecker (remember to first update AffineChecker with the two additional methods) and get:
The results of translating by (-2,5):
Affine Result = (0.0 , 8.0)
Matrix Result = (0.0 , 8.0)
No real difference
These basic operations can be combined to get sophisticated results many of which are captured in convenience methods in the AffineTransform class.
For more information about AffineTransform, see "Transforming Shapes, Text, and Images" in the Java Tutorial.
IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=sdn
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
Copyright 2003 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|