By 조나단 크누드센, 역 한빛 리포터 2기 신동섭
Oreilly.com에 그가 쓴 기사, "The Big Small Platform"에서 조나단 크누드센은 독자들에게 Java 2.Micro Edition(J2ME)와 Palm OS구현에 대해 소개했다. 이번 기사에서 그는 썬 마이크로시스템즈의 Palm OS J2ME에서 작동하는 network-chat client 소스 코드를 공개했다.
썬은 J2ME에 대해 말해왔지만, 과연 이것이 현실적인가, 아님 단지 생각뿐인가? 나는 이 문제의 해답을 찾기 위해 J2ME의
Connected Limited Device Configuration (CLDC)의 Palm OS에 구현된 chat-client 응용프로그램에 대해 기사를 썼다.
필요 조건
Chat client 예제를 구현하기 위해 몇 가지 도구가 필요하다:
- 먼저, CLDC Palm OS가 필요하다. 소프트웨어를 다운로드하기 위해서는 등록과정이 필요하며 등록은 무료이다.
- 다음으로, Palm OS 장치가 필요하다. Palm OS CLDC는 Palm III, Palm V, Palm VII, 또는 Handspring Visor에서 동작해야 한다. 데스크 탑 PC에서 동작하며 Palm OS 장치를 에뮬레이트하는 소프트웨어인 Palm OS emulator (POSE)를 사용할 수도 있다.
- 마지막으로, 데스크 탑 PC에 install된 Java를 가지고 있어야 한다. 이것은 SDK 1.2나 SDK 1.3을 제공하는 Java 2. Standard Edition (J2SE)의 특징일 수도 있다. 난 JDK 1.1에 대해 연구해 볼 생각이다. 하지만 사용하지는 않을 것이다. Chat Server를 개발하고 동작시키기위해 여러분의 데스크 탑을 사용할 것이다.
POSE에 대한 더 많은 것들
개발작업을 위해, 난
Palm OS Emulator (POSE)를 사용하길 권장한다.
POSE는 단지 Palm 하드웨어를 에뮬레이트하며, 여러분은 Palm OS가 올라가 있는 ROM 이미지를 공급할 필요가 있다. 실제 Palm OS device를 가지고 있다면, device에서 에뮬레이터로 ROM을 다운로드할 수 있다. 그렇지 않은 경우, 위의 사이트에서 일정 양식을 작성하면 Palm OS ROM 이미지를 얻을 수 있다.
개발 주기는 real Palm OS device를 사용하는 것보다 에뮬레이터를 사용하는 것이 더 짧다. Device에 응용프로그램을 물리적으로 다운로드할 필요가 없으며, 네트워킹 애플리케이션을 테스트할 필요가 있을 때 Palm OS device로부터 네트워크 연결을 하는데 대해 걱정할 필요가 없다.
POSE는 유용한 스위치를 가지고 있는데 이것은 Palm OS 네트워크 콜을 여러분의 데스크탑 컴퓨터의 네트워크 연결로 전송하는 역할을 한다. 이것을 셋업하기 위해, 오른쪽 버튼을 POSE에 클릭하고, setting을 선택하고, 다음으론 Properties, 그리고 "Redirect NetLib calls to host TCP/IP"를 선택하면 된다. 언제라도 POSE application으로 네트워크 연결을 하려고 할 때, 여러분 컴퓨터의 TCP/IP스택을 사용할 것이다.
애플리케이션 생성
내 기사 중
"The Big Small Platform"은 Palm OS CLDC 구현을 위한 애플리케이션을 생성하는 방법을 다루고 있다. 이번 달의 예제 애플리케이션은 좀 더 복잡한 것을 만들어주는 클래스로 구성되어 있다. 윈도우에서, 난 애플리케이션을 자동생성하기 위해 아래의 배치파일을 사용했다.
@setlocal
@set cldc=₩Apps₩j2me_cldc
@set lib=%cldc%₩bin₩api₩classes
@set palm_lib=%cldc%₩tools₩palm₩src
javac -bootclasspath %lib% -classpath .;%lib% Client.java ₩
Listener.java BufferedReader.java Login.java
%cldc%₩bin₩preverify -classpath .;%lib% -d . Client Client$1 ₩
Listener Listener$1 BufferedReader Login
java -classpath .;%palm_lib% palm.database.MakePalmApp ₩
-bootclasspath %lib% -o %cldc%₩bin₩Client.prc -networking ₩
Client Client$1 Listener Listener$1 BufferedReader Login
@endlocal
|
배치파일의 javac 라인에서 애플리케이션을 위한 모든 소스파일을 컴파일한다. 다음 라인에서 컴파일 후 생성된 내부 클래스를 가지고 있는 클래스를 preverify한다. 자바 라인에서 모든 preverify된 클래스에서 Palm OS 애플리케이션 파일을 생성한다.
채팅 서버, J2SE Application
Chat client는 서버에 연결되지않으면 아무 역할을 할 수 없다. 서버는 두개부분으로 구성된다: Server.java와 Listener.java, 추상클래스. 난 이 추상클래스에 대해 설명하진 않을 것이다.-이는 표준이며 쉽게 컴파일 할 수 있으며 서버를 작동시킬 수 있고, 명령어 라인에서 서버명과 사용할 port number를 지정할 수 있다(서버와 J2SE chat client에 대해 논의하고 있는 나의
Bite-Size Java 컬럼 중의 하나인
"Shooting Fish in a Barrel"을 읽어봐라).
J2ME World에서 Chat Client
Palm CLDC chat client는 4개의 클래스로 구성된다:
- Client클래스(Listing 3)는 대부분의 작업을 한다. 하나의 인터페이스를 제공하고, Palm에서 chat server로 라인들을 보낸다. 그리고 스크린에 chat server로부터 받은 text를 디스플레이한다.
- login class(listing 4)는 client 초기화 정보를 요청한다: 사용자는 chat server의 스크린 이름, 주소, port number를 넣기 위해 Login을 사용할 수 있다.
- Listener abstract class(Listing 2)은 서버로부터 온 text를 기다리기 위해 분리되어 있는 스레드를 셋업 하는데 사용된다.
- BufferedReader class(Listing 5)은 서버로부터 text lines을 읽기 위한 작은 glue로 제공된다.
코드의 대부분은 com.sun.java 패키지의 요소인 사용자 인터페이스와 관계를 가진다. 이것은 CLDC 바깥에 있기 때문에 나는 그것에 많은 시간을 투자할 필요가 없다고 생각한다.
중요한 본질은 Login클래스의 login()메쏘드에 있다. StreamConnection은 Connector 클래스로부터 얻은 이 메쏘드 안에 있다.
String name = mNameField.getText();
String host = mHostField.getText();
int port = Integer.parseInt(mPortField.getText());
String url = "socket://" + host + ":" + port;
StreamConnection s = (StreamConnection)
Connector.open(url);
Client c = new Client(name, s);
c.register(NO_EVENT_OPTIONS);
|
호스트와 port number는 user interface control에서 알 수 있으며 "socket://"을 사용하여 url 문자열을 생성하기위해 컴파일된다. 다음으로 그 url 문자열은 StreamConnection 객체를 돌려주는 Connector"s open() method로 넘어간다. 이 객체는 Client"s constructor로 이동하고 control은 Client object로 넘어간다.
Client constructor는 helper method,wireNetwork()를 호출하는데 이것은 두 가지 목적을 위해 StreamConnection을 사용한다. Client constructor는 텍스트를 writing하기위해 PrintStream를 server에 생성한다. 그 다음 Listener의 익명 내부 subclass를 생성한다. 이 object는 서버로부터 오는 텍스트 줄을 읽고 Palm의 screen에 이를 나타내어 준다. 완전한 wireNetwork() method는 아래와 같다:
protected void wireNetwork(StreamConnection s)
throws IOException {
mOut = new PrintStream(s.openOutputStream());
new Listener(s.openInputStream()) {
public void processLine(String line) {
for (int i = 0; i < mDisplayLines.length - 1; i++)
mDisplayLines[i] = mDisplayLines[i + 1];
mDisplayLines[mDisplayLines.length - 1] = line;
paint();
}
};
}
|
스크린은 단순히 문자열, mDisplayLines를 표시해준다. 새로운 한 줄을 서버로부터 받을 때, 그 줄은 위로 이동하고 새로운 줄이 바닥에 보인다.
사용자가 Palm OS device에 텍스트를 입력하면, 다음으로 새로운 라인을 쓰고 wireNetwork()으로 생성된 PrintStream을 사용하여, 텍스트는 서버로 보내어진다. 이러한 것은 Client의 sendLine() 메쏘드에 나타난다:
protected void sendLine() {
// Send the stuff to the server.
String line = mName + ": " + mInputField.getText();
mOut.println(line);
mInputField.setText("");
}
|
이 텍스트는 사용자 스크린 명과 함께 첨부되어 서버로 보내어진다. 입력 텍스트 필드는 다음 텍스트를 위한 준비과정에서 삭제된다.
드라이브하러 가다.
이 애플리케이션을 설정하는 것은 몇몇 다른 클래스를 포함하고 있기 때문에 다소 복잡하다. 위에 설명한 배치파일 같은 것을 사용하라 그러면 아무 문제가 없을 것이다.
일단 모든 것을 컴파일하고 에뮬레이터에 결과적인 Client.prc를 인스톨하라. Chat client를 테스트하기위해, 여러분은 데스크 탑에서 실행되고 있는 서버를 시작토록 해야 할 필요가 있을 것이다. 다음으로 여러분의 Palm OS에서 Client를 실행시키고 서버 주소와 port number에 point를 두고 Login버튼을 눌러라. 모든 것이 잘 수행되었다면 여러분의 Palm OS 스크린에 서버에서 보내어온 welcome 메시지를 보게 된다. 다른 사람들이 동시에 여러분의 서버에 연결할 수 있어야 한다(위에서 언급한 Shooting Fish in a Barrel이라는 제목이 붙여진 same Bite-Sized Java column에 나와있는 J2SE client가 있다).
여러분은 타이핑을 하여 서버에 텍스트를 보낼 수 있거나 (만약 에뮬레이터를 사용할 수 있다면) stylus로 writing할 수도 있다. 서버에 새로운 라인을 보내기위해서는 리턴 키를 누르거나 stylus로 새로운 줄 심볼을 기록해야 한다. 그 텍스트는 서버로 보내어져서 연결된 모든 client들에게 보내어지며 이들의 스크린에 이 데이터를 표시할 수 있게 된다.
결론
Palm OS CLDC 구현은 J2ME를 체험해 볼 수 있는 훌륭한 플랫폼으로 현재 Palm OS에서 네트워크 응용프로그램을 개발하는데 사용될 수 있다. Production-level 애플리케이션을 위해서는 J2ME Profile중 하나가 Palm OS에 port될 때까지 기다려야 한다. 나는 Mobile Information Device Profile (MIDP)가 Palm OS에 port하고 있다는 소문을 들어왔으며, 또한 현재 진행중인 PDA profile이 있다.
J2ME는 흥분할 만한 기술이다. 일단 한번 구현되면 어디에서나 동작한다는 그 약속은 실현되고 있다. -작은 device세계에서 빠르게 이러한 것들이 실현되길 희망한다.
소스 코드와 클래스 파일 다운로드
조나단 크누드센은 J2ME Mobile Information Device Profile을 다루는 출간 예정 도서『Mobile Java』의 저자이다. 현재 그는
LearningPatterns.com Courseware의 개발 책임자이다. 앞서 그는 오라일리의 저자이기도 하며 편집장을 지냈다. 그가 저술한 책으로는
『The Unofficial Guide to LEGO MINDSTORMS Robots』,
『Java 2D Graphics』,
『Learning Java』등이 있다. 오라일리 재직시절엔
Bite-Size Java란 제목으로 월간 컬럼을 쓰기도 있다. 조나단은 뉴저지의 그의 집에서 그의 아내, 한 마리의 고양이, 4명의 아이들과 함께 살면서 작업을 하고 있다.
Listing 1 - Server.java
import java.io.*;
import java.net.*;
import java.util.*;
public class Server {
public static void main(String[] args) throws IOException {
String name = args[0];
int port = Integer.parseInt(args[1]);
new Server(name, port);
}
private List mClients;
private String mName;
public Server(String name, int port) throws IOException {
mClients = new Vector();
mName = name;
ServerSocket serverSocket = new ServerSocket(port);
while (true) addClient(serverSocket.accept());
}
public void addClient(Socket clientSocket) throws IOException {
final PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
mClients.add(out);
out.println("[Welcome to " + mName + ".]");
new Listener(clientSocket.getInputStream()) {
public void processLine(String line) {
if (line.length() == 0) mClients.remove(out);
else sendToClients(line);
}
};
}
public void sendToClients(String line) {
Iterator iterator = mClients.iterator();
while(iterator.hasNext()) {
PrintWriter out = (PrintWriter)iterator.next();
out.println(line);
}
}
}
|
Listing 2 - Listener.java
import java.io.*;
public abstract class Listener {
public abstract void processLine(String line);
private BufferedReader mIn;
public Listener(InputStream in) {
mIn = new BufferedReader(new InputStreamReader(in));
Thread t = new Thread() {
public void run() {
try {
String line;
while ((line = mIn.readLine()) != null) processLine(line);
}
catch (IOException ioe) {}
}
};
t.start();
}
}
|
Listing 3 - Client.java
import java.io.*;
import javax.microedition.io.*;
import com.sun.kjava.*;
public class Client
extends Spotlet {
private static Graphics sGraphics;
public static void main(String[] args) throws IOException {
// Clear off the KVM splash screen.
sGraphics = Graphics.getGraphics();
sGraphics.clearScreen();
// Put up the login screen.
Login l = new Login();
l.register(NO_EVENT_OPTIONS);
}
private String mName;
private String[] mDisplayLines;
private TextField mInputField;
private PrintStream mOut;
public Client(final String name, StreamConnection s)
throws IOException {
mName = name;
mDisplayLines = new String[12];
for (int i = 0; i < mDisplayLines.length; i++)
mDisplayLines[i] = "";
createUI();
wireNetwork(s);
}
public void shutDown() {
mOut.println("");
mOut.close();
}
protected void createUI() {
// Create the Input field and give it focus.
mInputField = new TextField("Send", 0, 0, 144, 12);
mInputField.setText("");
mInputField.setFocus();
}
protected void wireNetwork(StreamConnection s)
throws IOException {
mOut = new PrintStream(s.openOutputStream());
new Listener(s.openInputStream()) {
public void processLine(String line) {
for (int i = 0; i < mDisplayLines.length - 1; i++)
mDisplayLines[i] = mDisplayLines[i + 1];
mDisplayLines[mDisplayLines.length - 1] = line;
paint();
}
};
}
public void paint() {
sGraphics.clearScreen();
// Paint UI controls.
mInputField.paint();
// Draw the chat lines.
for (int i = 0; i < mDisplayLines.length; i++)
sGraphics.drawString(mDisplayLines[i], 0, 16 + 12 * i);
}
public void keyDown(int key) {
if (mInputField.hasFocus()) {
if (key == "₩n") sendLine();
else mInputField.handleKeyDown(key);
}
}
protected void sendLine() {
// Send the stuff to the server.
String line = mName + ": " + mInputField.getText();
mOut.println(line);
mInputField.setText("");
}
}
|
Listing 4 - Login.java
import java.io.*;
import javax.microedition.io.*;
import com.sun.kjava.*;
public class Login
extends Spotlet {
private static Graphics sGraphics = Graphics.getGraphics();
private TextField mNameField, mHostField, mPortField;
private Button mLoginButton, mCancelButton;
public Login() {
mNameField = new TextField("Name", 30, 30, 100, 12);
mNameField.setText("Palmer");
mNameField.setFocus();
mHostField = new TextField("Host", 30, 60, 100, 12);
mHostField.setText("172.16.0.3");
mPortField = new TextField("Port", 30, 90, 100, 12);
mPortField.setText("7099");
mLoginButton = new Button("Login", 30, 120);
mCancelButton = new Button("Cancel", 90, 120);
paint();
}
public void paint() {
// Paint UI controls.
mNameField.paint();
mHostField.paint();
mPortField.paint();
mLoginButton.paint();
mCancelButton.paint();
}
public void keyDown(int key) {
if (mNameField.hasFocus()) mNameField.handleKeyDown(key);
else if (mHostField.hasFocus()) mHostField.handleKeyDown(key);
else if (mPortField.hasFocus()) mPortField.handleKeyDown(key);
}
public void penDown(int x, int y) {
if (mLoginButton.pressed(x, y)) login();
else if (mCancelButton.pressed(x, y)) System.exit(0);
else if (mNameField.pressed(x, y)) setFocus(mNameField);
else if (mHostField.pressed(x, y)) setFocus(mHostField);
else if (mPortField.pressed(x, y)) setFocus(mPortField);
}
private void setFocus(TextField t) {
killFocus();
t.setFocus();
}
private void killFocus() {
if (mNameField.hasFocus()) mNameField.loseFocus();
if (mHostField.hasFocus()) mHostField.loseFocus();
if (mPortField.hasFocus()) mPortField.loseFocus();
}
public void login() {
killFocus();
unregister();
try {
String name = mNameField.getText();
String host = mHostField.getText();
int port = Integer.parseInt(mPortField.getText());
String url = "socket://" + host + ":" + port;
StreamConnection s = (StreamConnection)
Connector.open(url);
Client c = new Client(name, s);
c.register(NO_EVENT_OPTIONS);
}
catch (IOException ioe) {
System.out.println(ioe);
System.exit(0);
}
}
}
|
Listing 5 - BufferedReader.java
import java.io.*;
public class BufferedReader {
private Reader mReader;
private char[] mBuffer;
public BufferedReader(Reader r) {
mReader = r;
mBuffer = new char[512];
}
public String readLine() throws IOException {
boolean trucking = true;
int index = 0;
while (trucking) {
int c = mReader.read();
if (c == "₩n" || c == -1) trucking = false;
else if (c != "₩r") mBuffer[index++] = (char)c;
}
if (index == 0) return null;
String line = new String(mBuffer, 0, index);
return line;
}
}
|