SuldenLion의 통신 프로그램 SuldenTalk을 소개해 보겠다.
이전 포스팅의 통신 프로그램을 업그레이드 한 버전이기도 하며 자잘한 기능을 더 추가해보기도 하였다.
이전 프로그램과 다른점인 SuldenTalk의 특징은 돌아가는 프로그램 주체 각각이 Server가 될 수도 있고 Client가 될 수도 있다는 점이다. 즉 사용자입장에서 프로그램을 Server인지 Client인지 구분할 필요 없이 같은 소스코드를 가지고 서로 사용할 수 있다. 각 프로그램이 ServerRole을 하면서 Client로서 동작할 수 있게 하였다. 각 프로그램의 port번호만 맞춰주면 된다.
프로그램을 코드와 동작하는 것까지 보여주겠다.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.net.*;
import java.time.*;
import java.time.format.*;
class Message
{
private String who;
private String message;
Message(String who, String message) {
this.who = who;
this.message = message;
}
String getWho() {
return who;
}
String getMessage() {
return message;
}
}
class ButtonPanel extends JPanel
{
MainPanel mainWnd;
JTextField ip;
JButton connectButton;
ButtonPanel(MainPanel mainWnd) {
this.mainWnd = mainWnd;
ip = new JTextField(15);
connectButton = new JButton("Connect");
ip.setText("localhost");
add(ip);
add(connectButton);
connectButton.addActionListener(mainWnd);
}
String getIpAddress() {
return ip.getText();
}
void setActionCommand(String s) {
connectButton.setActionCommand(s);
connectButton.setLabel(s);
}
void setIPAddress(String address) {
ip.setText(address);
}
}
class InputPanel extends JPanel
{
MainPanel mainWnd;
private JTextField textInput;
InputPanel(MainPanel mainWnd) {
setLayout(new GridLayout(1,1));
this.mainWnd = mainWnd;
textInput = new JTextField();
textInput.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent ev) {
int keyCode = ev.getKeyCode();
if (keyCode == KeyEvent.VK_ENTER)
{
String msg = textInput.getText().trim();
Message newMessage = new Message("I", msg);
mainWnd.writeText(newMessage);
mainWnd.sendLine(msg);
textInput.setText("");
}
}
});
textInput.setBorder(BorderFactory.createLoweredBevelBorder());
add(textInput);
}
void setEditable(boolean flag) {
textInput.setEditable(flag);
}
}
class ChatPanel extends JPanel
{
ArrayList<Message> text = new ArrayList<Message>();
MainPanel mainWnd;
ArrayList<String> currentTime = new ArrayList<String>();;
ChatPanel(MainPanel mainWnd) {
this.mainWnd = mainWnd;
}
void writeText(Message msg) {
text.add(msg);
repaint();
}
void writeTime(String time) {
currentTime.add(time);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int currentY = 20;
int deltaY = 25;
int timeIndex = 0;
for (int i = 0; i < text.size(); i++)
{
Message m = text.get(i);
String who = m.getWho();
String s;
g.setFont(new Font(null, Font.PLAIN, 15));
if (who.equals("I"))
{
s = "나 : " + m.getMessage();
String tmp = "";
for (int j = 0; j < s.length(); j++)
{
tmp += s.charAt(j);
if (j != 0 && j % 10 == 0)
{
g.drawString(tmp,10,currentY);
tmp = "";
currentY = currentY + deltaY;
} else if (j % 10 != 0) {
g.drawString(tmp,10,currentY);
}
}
currentY += deltaY;
g.setFont(new Font(null, Font.PLAIN, 10));
g.drawString(currentTime.get(timeIndex),50,currentY);
} else if (who.equals("U"))
{
s = "상대방 : " + m.getMessage();
String tmp = "";
for (int j = 0; j < s.length(); j++)
{
tmp += s.charAt(j);
if (j != 0 && j % 10 == 0)
{
g.drawString(tmp,250,currentY);
tmp = "";
currentY = currentY + deltaY;
} else if (j % 10 != 0) {
g.drawString(tmp,250,currentY);
}
}
currentY += deltaY;
g.setFont(new Font(null, Font.PLAIN, 10));
g.drawString(currentTime.get(timeIndex),290,currentY);
} else if (who.equals("Info"))
{
s = m.getMessage();
String tmp = "";
for (int j = 0; j < s.length(); j++)
{
tmp += s.charAt(j);
g.drawString(tmp,10,currentY);
}
}
timeIndex++;
currentY = currentY + deltaY + 10;
Dimension sz = getSize();
if (currentY >= sz.height)
{
setPreferredSize(new Dimension(sz.width, currentY+deltaY));
updateUI();
mainWnd.scrollDown(currentY);
}
}
}
}
class ServerRole extends Thread
{
MainPanel mainWnd;
ServerRole(MainPanel mainWnd) {
this.mainWnd = mainWnd;
}
public void run() {
try
{
int portNumber = 8000;
ServerSocket listenerSocket = new ServerSocket(portNumber);
mainWnd.informText("Server started...");
mainWnd.informText(mainWnd.getMyIP() + " on port: " + portNumber);
while(true) {
Socket client = listenerSocket.accept();
String ip = client.getInetAddress().getHostAddress();
mainWnd.informText(ip + " is connected...");
mainWnd.setEditable(true);
mainWnd.setIPAddress(ip);
mainWnd.setActionCommand("Disconnect");
BufferedReader fromChatClient = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter toChatClient = new PrintWriter(client.getOutputStream());
Thread fromClientThread = new Thread() {
public void run() {
try
{
String msg;
while (true)
{
msg = fromChatClient.readLine();
Message m = new Message("U", msg);
if (msg == null || msg.equals("####"))
{
break;
}
mainWnd.writeText(m);
}
}
catch (IOException ex)
{
System.out.println(ex);
}
try
{
client.close();
fromChatClient.close();
toChatClient.close();
}
catch (IOException ex)
{
}
mainWnd.setEditable(false);
mainWnd.setActionCommand("Connect");
}
};
mainWnd.setClientInfo(fromChatClient, toChatClient, fromClientThread);
fromClientThread.start();
}
}
catch (IOException ex)
{
System.out.println(ex);
}
}
}
class MainPanel extends JPanel implements ActionListener
{
boolean amIServer = true;
String myIP;
String ipAddress = "localhost";
ButtonPanel buttonPanel;
ChatPanel chatPanel;
InputPanel inputPanel;
JScrollPane chatPane;
LocalTime now;
DateTimeFormatter formatter;
String formattedNow;
Socket chatClient;
BufferedReader fromChatServer;
PrintWriter toChatServer;
Thread fromServerThread;
private BufferedReader fromChatClient;
private PrintWriter toChatClient;
private Thread fromClientThread;
void setClientInfo(BufferedReader fromChatClient, PrintWriter toChatClient, Thread fromClientThread) {
this.fromChatClient = fromChatClient;
this.toChatClient = toChatClient;
this.fromClientThread = fromClientThread;
}
void sendLine(String msg) {
if (amIServer)
{
toChatClient.println(msg);
toChatClient.flush();
} else {
toChatServer.println(msg);
toChatServer.flush();
}
}
void setIPAddress(String address) {
ipAddress = address;
buttonPanel.setIPAddress(address);
}
MainPanel() {
setLayout(new BorderLayout());
buttonPanel = new ButtonPanel(this);
chatPanel = new ChatPanel(this);
chatPane = new JScrollPane(chatPanel);
chatPanel.setBorder(BorderFactory.createLoweredBevelBorder());
inputPanel = new InputPanel(this);
inputPanel.setEditable(false);
add(buttonPanel, BorderLayout.NORTH);
add(chatPane, BorderLayout.CENTER);
add(inputPanel, BorderLayout.SOUTH);
try
{
InetAddress address = InetAddress.getLocalHost();
myIP = address.getHostAddress();
}
catch (Exception ex)
{
}
ServerRole server = new ServerRole(this);
server.start();
}
void scrollDown(int x) {
chatPane.getVerticalScrollBar().setValue(x+100);
chatPane.updateUI();
}
void writeText(Message msg) {
setCurrentTime();
chatPanel.writeText(msg);
chatPanel.writeTime(formattedNow);
}
void writeText(String s) {
setCurrentTime();
Message m = new Message("I", s);
chatPanel.writeText(m);
chatPanel.writeTime(formattedNow);
}
void informText(String s) {
setCurrentTime();
Message m = new Message("Info", s);
chatPanel.writeText(m);
chatPanel.writeTime(formattedNow);
}
void setCurrentTime() {
now = LocalTime.now();
formatter = DateTimeFormatter.ofPattern("HH시 mm분 ss초");
formattedNow = now.format(formatter);
}
String getMyIP() {
return myIP;
}
void setActionCommand(String s) {
buttonPanel.setActionCommand(s);
}
void setEditable(boolean flag) {
inputPanel.setEditable(flag);
}
public void actionPerformed(ActionEvent ev) {
String cmd = ev.getActionCommand();
String ipAddress = buttonPanel.getIpAddress();
if (cmd.equals("Connect"))
{
amIServer = false;
setEditable(true);
setActionCommand("Disconnect");
try
{
chatClient = new Socket(ipAddress, 7000);
informText("Connected...");
fromChatServer = new BufferedReader(new InputStreamReader(chatClient.getInputStream()));
toChatServer = new PrintWriter(chatClient.getOutputStream());
fromServerThread = new Thread() {
public void run() {
try
{
String msg;
while (true)
{
msg = fromChatServer.readLine();
Message newMessage = new Message("U", msg);
if (msg == null || msg.equals("####"))
{
break;
}
writeText(newMessage);
}
}
catch (IOException ex)
{
System.out.println(ex);
}
amIServer = true;
setEditable(false);
setActionCommand("Connect");
}
};
fromServerThread.start();
}
catch (IOException ex)
{
System.out.println(ex);
fromServerThread.stop();
}
} else if (cmd.equals("Disconnect"))
{
sendLine("####");
if (amIServer)
{
try
{
fromChatClient.close();
toChatClient.close();
fromClientThread.stop();
}
catch (IOException ex)
{
}
} else {
try
{
fromChatServer.close();
toChatServer.close();
chatClient.close();
fromServerThread.stop();
}
catch (IOException ex)
{
}
}
amIServer = true;
setEditable(false);
setActionCommand("Connect");
}
}
}
class SuldenTalkFrame extends JFrame
{
SuldenTalkFrame() {
super("SuldenTalk");
setSize(400,600);
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container contentPane = getContentPane();
contentPane.add(new MainPanel());
}
}
class SuldenTalk
{
public static void main(String[] args)
{
SuldenTalkFrame frame = new SuldenTalkFrame();
frame.setVisible(true);
}
}
SuldenTalk의 디자인은 ButtonPanel과 InputPanel, ChatPanel로 구성되어있고 MainPanel에 포함된다.
MainPanel에서 해줄 주요 작업들을 보자면 reader/writer를 통해 들어온 client를 세팅해주는 setClientInfo(), 현재 돌아가는 프로그램이 Server일때와 Client일때를 구분해서 메시지를 날려주는 sendLine(), 받아온 IP주소를 세팅해주는 setIPAddress(), chatPanel을 scrollPane으로 감싸서 스크롤 내리는 작업을 해줄 scrollDown(), 그리고 메시지를 띄워줄 writeText()는 두가지 경우인데 parameter가 Message로 바로 넘어온 경우에는 actionPerformed에서 상대측이 보낸 메시지를 고대로 띄워주는 경우가 될 것이고, parameter가 String인 경우는 InputPanel에서 입력받은 문자열을 메시지 형태로 바꾸어주고 ChatPanel에 띄워주는 것이다. 그리고 채팅을 치고 난 뒤에 현재 시각을 띄워주기 위해 setCurrentTime()을 해주고 데이터를 ChatPanel에 쏴준다. informText()는 처음 프로그램 돌릴때의 IP정보와 port번호를 띄워주는 일이 되겠다. IP 정보를 String형태로 리턴해주는 getMyIP(), ButtonPanel의 버튼이 Connect일때와 Disconnect일때 각각 해당 상태를 표시해줄 setActionCommand(), Connect시에 InputPanel의 textField를 열어주고 Disconnect시에 닫아줄 setEditable(), ButtonPanel에 있는 connectButton의 actionEvent를 처리해줄 actionPerformed()는 connect를 눌렀을때 Socket연결 및 Thread 돌리는 작업을 해주고 disconnect를 눌렀을때 Socket과 Stream연결을 해제해준다.
ServerRole은 이제 고유의 port번호를 설정해주고 client socket 연결을 listen해주는 역할을 한다. 그리고 client로부터 온 메시지를 실시간으로 받아줄 fromClientThread를 생성해서 run 시켜준다.
ChatPanel에서는 메시지랑 그 메시지밑에 함께 출력될 현재시간을 각각의 ArrayList에 담아 관리해주고, 메시지가 누가 썼느냐(who가 I이냐 U이냐 Info이냐)에 맞춰 알맞은 형식으로 각 위치에 메시지를 draw 해준다. 그리고 현재 메시지의 y값이 화면을 벗어날 경우 scrollDown을 해준다.
InputPanel은 메시지를 입력할 textField와 keyEvent Enter를 통해 String을 Message로 인스턴스화 해서 MainPanel에 넘겨준다.
ButtonPanel은 ip주소를 입력받을 textField와 connect를 위한 버튼을 둔다. IP 주소의 getter/setter 메서드를 만들어주고 setActionCommand()에서는 넘어온 s값으로 JButton의 상위 클래스 AbstractButton()의 setActionCommand(s)와 setLabel(s)를 해준다. (setActionCommand → sets the action command for this button / setLabel → Sets the label text)
Message 클래스는 who와 message 세트로 된 객체이다. ChatPanel에서 getWho()를 통해 어느쪽에서 보냈는지 구분해주고 getMessage()로 채팅내용을 추가해준다.
프로그램의 설명은 이정도로 끝이다.
채팅 프로그램 동작하는 화면을 보여주고 마무리 하겠다.
대충 채팅 프로그램으로써의 역할을 잘 해낸다.
(local환경에서 프로그램 테스트, 두 개의 파일을 띄워놓고 port번호를 각각 7000, 8000으로 설정. 오류가 딱히 없을거라고 생각했는데 disconnect시 한쪽만 되는 오류, scrollBar가 아래쪽에 고정되는 오류 수정 등 글 작성하는 중에도 소스코드 수정이 꽤 있었다.)
댓글