채팅 서버 구현
목표 : 채팅 서버 구현 코드를 보면서 스레드풀(ExecutorService), ServerSocket, Socket들이 어떻게 사용되는지 이해한다.
서버 클래스 구조(ServerExample.java)
public class ServerExample extends Application {//javaFX 메인 클래스로 만들기 위해 APPlication 상속
ExecutorService executorService;//스레드풀인 ExecutorService 필드가 선언
ServerSocket serverSocket;//클라이언트의 연결을 수락하는 ServerSocket 필드가 선언
List<Client> connections = new Vector<Client>();//연결된 클라이언트를 저장하는 List<Client> 타입의 connections 필드가 선언되어 있다. 그리고 스레드에 안전한 Vector 초기화
void startServer() { //서버 시작 코드 }//startServer()는 서버 시작 시 호출되는 메소드
void stopServer() { //서버 종료 코드 }//stopServer()는 서버 종료 시 호출되는 메소드
class Client { //데이터 통신 코드 }//Client 클래스는 연결된 클라이언트를 표현하는데, 데이터 통신 코드를 포함
////////////////////////////////
//UI 생성 코드//UI 생성 코드는 레이아웃을 구성하고 ServerExample 실행
}
실행 화면에서 [start] 버튼을 클릭하면 startServer() 메소드가 실행하는데, startServer() 메소드에는 ExecutorService 생성, ServerSocket 생성 및 포트 바인딩, 연결 수락 코드가 있다. 먼저 ExecutorService 생성코드를 보자.
void startServer() {
executorService = Executors.newFixedThreadPool(//ExecutorService 객체를 얻기 위해 Executors.newFixedThreadPool() 메소드를 호출한다.
Runtime.getRuntime().availableProcessors()//CPU 코어의 수만큼 스레드를 만들도록 한다.
);
이번에는 ServerSocket 생성 및 포트 바인딩 코드를 보자.
try {
serverSocket = new ServerSocket();//ServerSocket 객체 생성
serverSocket.bind(new InetSocketAddress("localhost", 5001));//ServerSocket을 로컬 컴퓨터 5001 포트와 바인딩
} catch(Exception e) {
if(!serverSocket.isClosed()) { stopServer(); }//예외가 발생할 경우 ServerSocket이 닫혀있지 않은면 stopServer() 메소드 호출
return;//startServer() 메소드 종료
}
연결 수락 코드
Runnable runnable = new Runnable() {//수락 작업 생성, 연결 수락 작업을 runnable로 정한다.
@Override
public void run() {//run() 메소드를 재정의한다.
Platform.runLater(()->{//작업 스레드는 UI를 변경하지 못하므로 Platform.runLater() 사용되었다.
displayText("[서버 시작]");//"[서버 시작]"을 출력하도록 displayText()를 호출한다.
btnStartStop.setText("stop");//[start] 버튼의 글자를 [stop]으로 변경한다.
});
while(true) {//클라이언트 연결 수락을 무한히 반복하도록 한다.
try {
Socket socket = serverSocket.accept();//연결 수락, 클라이언트의 연결 요청을 기다리고, 연결 수락을 하는 accept() 메소드를 호출한다.
String message = "[연결 수락: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";//연결 수락이 되면 클라이언트 IP주소와 스레드 이름이 포함된 연결 수락 메시지 문자열을 만든다.
Platform.runLater(()->displayText(message));
//Client 객체 저장
Client client = new Client(socket);//Client 객체를 생성한다.
connections.add(client);//Client 객체를 connections 컬렉션에 추가한다.
Platform.runLater(()->displayText("[연결 개수: " + connections.size() + "]"));//connections 컬렉션에 저장된 Client 객체 수를 출력하도록 displayText()를 호출한다.
} catch (Exception e) {
if(!serverSocket.isClosed()) { stopServer(); }//예외가 발생했을 경우, ServerSocket이 닫혀있지 않으면 stopServer()를 호출한다.
break;//break를 사용해서 while문을 멈춘다.
}
}
}
};
//스레드풀에서 처리
executorService.submit(runnable);//스레드풀의 작업 스레드에서 연결 수락 작업을 처리하기 위해 submit()을 호출한다.
}//startServer() 메소드 끝
stopServer() 메소드
void stopServer() {
try {
//모든 Socket 닫기
Iterator<Client> iterator = connections.iterator();//connections 컬렉션으로부터 반복자를 얻어낸다.
while(iterator.hasnNext()) {//while문으로 반복자를 반복한다.
Client client = iterator.next();//반복자로부터 Client를 하나씩 얻는다.
Client.socket.close();//Client가 가지고 있는 Socket을 닫는다.
iterator.remove();//connections 컬렉션에서 Client를 제거한다.
}
//ServerSocket 닫기
if(serverSocket!=null && !serverSokcet.isClosed()) {//serverSocket이 null이 아니고, 닫혀있지 않으면
serverSocket.close();//ServerSocket을 닫는다.
}
//ExecutorService 종료
if(executorService!=null && !executorService.isShutdown()) {//ExecutorService가 null이 아니고, 종료 상태가 아니면
executorService.shutdown();//ExecutorService를 종료한다.
}
Platform.runLater(()->{//작업 스레드는 UI를 변경하지 못하므로 Platform.runLater()가 사용되었다.
displayText("[서버 멈춤]");//"[서버 멈춤]"을 출력하도록 displayText()를 호출한다.
btnStartStop.serText("start");//[stop] 버튼의 글자를 [start]로 변경한다.
});
} catch (Exception e) { }
}
Client 클래스
class Client {//Client를 ServerExample의 내부 클래스로 선언한다.
Socket socket;//Socket 필드를 선언한다.
Client(Socket socket) {//Client 생성자를 선언한다.
this.socket = socket;//매개값으로 받은 Socket을 필드값으로 저장한다.
receive();//receive() 메소드를 호출한다.
}
void receive() { //데이터 받기 코드 }//데이터를 받기 위해 receive() 메소드를 선언한다.
void send(String data) { //데이터 전송 코드 }//데이터를 보내기 위해 send() 메소드를 선언한다.
}
클라이언트의 데이터를 받는 receive() 메소드
void receive() {
Runnable runnable = new Runnable() {//받기 작업 생성
@Override
public void run() {
try (
while(true) {
byte[ ] byteArr = new byte[100];//받은 데이터를 저장할 byte[] 배열인 byteArr를 생성한다.
InputStream inputStream = socket.getInputStream();
//클라이언트가 비정상 종료를 했을 경우 IOException 발생
int readByteCount = inputStream.read(byteArr);//데이터 받기
//클라이언트가 정상적으로 Socket의 close()를 호출했을 경우
if(readByteCount == -1) { throw new IOException(); }
String message = "[요청 처리: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";
Platform.runnable.runLater(()->displaytext(message));
String data = new String(byteArr, 0, readByteCount, "UTF-8");//문자열로 변경
//모든 클라이언트에게 보냄
for(Client client : connections) {
client.send(data);
}
}
} catch(Exception e) {
try {
connections.remove(Client.this);//예외가 발생하면 connections 컬렉션에서 Client 객체를 제거한다.
String message = "[클라이언트 통신 안됨: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";
Platform.runLater(()->displayText(message));
socket.close();
} catch (IOException e2) {}
}
}
};
executorService.submit(runnable);//스레드풀에서 처리
}
클라이언트로 메시지를 보내는 send(String data) 메소드 코드
void send(String data) {
runnable runnable = new Runnable() {//보내기 작업 생성
@Override
public void run() {
try {
//클라이언트로 데이터 보내기
byte[ ] byteArr = data.getBytes("UTF-8");//보낼 문자열로부터 UTF-8로 인코딩한 바이트 배열을 얻는다.
OutputStream outputStream = socket.getOutputStream();
outputStream.write(byteArr);
outputStream.flush();//출력 스트림의 내부 버퍼를 완전히 비우도록 flush()를 호출한다.
} catch(Exception e) {
try {
String message = "[클라이언트 통신 안됨: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";
Platform.runLater(()->displayText(message));
connections.remove(Client.this);//connections 컬렉션에서 예외가 발생한 Client를 제거한다.
socket.close();
} catch (IOException e2) {}
}
}
};
executorService.submit(runnable);//스레드풀에서 처리
}
UI 생성 코드
TextArea txtDisplay;
Button btnStartStop;
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
root.setPrefSize(500, 300);
txtDisplay = new TextArea();
txtDisplay.setEditable(false);
BorderPane.setMargin(txtDisplay, new Insets(0,0,2,0));
root.setCenter(txtDisplay);
btnStartStop = new Button("start");
btnStartStop.setPrefHeight(30);
btnStartStop.setMaxWidth(Double.MAX_VALUE);
btnStartStop.setOnAction(e->{//startdhk stop 버튼을 클릭했을 때 이벤트 처리 코드
if(btnStartStop.getText().equals("start")) {
startServer();
} else if(btnStartStop.getText().equals("stop")){
stopServer();
}
});
root.setBottom(btnStartStop);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toString());
primaryStage.setScene(scene);
primaryStage.setTitle("Server");
primaryStage.setOnCloseRequest(event->stopServer());//원도우 우측 상단 닫기 버튼을 클릭했을 때 이벤트 처리 코드
primaryStage.show();
}
//작업 스레드의 작업 처리 내용을 출력할 때 호출하는 메소드
void displayText(String text) {
txtDisplay.appendText(text + "\n");
}
public static void main(String[ ] args) {
launch(args);
}
외부 CSS 파일(app.css)을 Scene에 적용하고 있다. 그 이유는 TextArea의 배경색 때문인데, TextArea는 여러 겹의 컨테이너로 둘러싸여 있는 복잡한 구조를 가지고 있어 단순히 TextArea의 setStyle()로 배경색을 바꿀 수 없다. 그래서 다음과 같이 외부 CSS 클래스 선택자를 이용해서 컨테이너의 배경색을 변경했다.
외부 CSS 파일(app.css)
/*text-area 배경색*/
.text-area {
-fx-background-color: gold;
}
/*scroll-pane 배경색*/
.text-area .scroll-pane {
-fx-background-color: transparent;
}
/*viewport 배경색*/
.text-area .scroll-pane .viewport{
-fx-background-color: transparent;
}
/*content 배경색*/
.text-area .scroll-pane .content{
-fx-background-color: transparent;
}
text-area만 gold색으로 설정하고, 나머지 컨테이너들은 모두 투명(transparent)으로 설정함으로써 TextArea 전체 배경색이 gold색으로 보이게 했다.
채팅 클라이언트 구현
채팅 클라이언트 구현 코드를 보면서 Socket이 어떻게 사용되는지 보자.
클라이언트 클래스 구조(ClientExample.java)
Public class ClientExample extends Application {//JavaFX 메인 클래스로 만들기 위해 Application을 상속한다.
Socket socket;//클라이언트 통신을 위해 Socket 필드를 선언
void startClient() { //연결 시작 코드 }
void stopClient() { //연결 끊기 코드 }
void receive() { //데이터 받기 코드 }//서버에서 보낸 데이터를 받는다.
void send(String data) { //데이터 전송 코드 }//[send] 버튼을 클릭하면 호출되는데, 서버로 데이터를 보낸다.
//////////////////////////////////////////
//UI 생성 코드//UI생성 코드는 레이아웃을 구성하고 ClientExample을 실행시키다.
}
*실행 화면에서 [start] 버튼을 클릭하면 startClient() 메소드가 호출되고, [start] 버튼은 [stop] 버튼으로 변경된다. [stop] 버튼을 클릭하면 stopClient() 메소드가 호출되고, 다시 [start] 버튼으로 변경된다.
StartClient() 메소드
[start] 버튼을 클릭하여 startClient() 메소드가 실행하는데, startClient() 메소드에는 Socket 생성 및 연결 요청 코드가 있다.
void startClient() {
Thread thread = new Thread() {//스레드 생성, 작업 스레드가 필요한 이유는 connect()와 receive()에서 블로킹이 일어나기 때문이다.
@Override
public coid run() {
try {
socket = new Socket();//소켓 생성(통신용)
socket.connect(new InetSocketAddress("localhost", 5001));//연결 요청
Platform,runLater(()->{//작업 스레드는 UI를 변경하지 못하므로 Platform.runLater()가 사용되었다.
displayText("[연결 완료: " + socket.getRemoteSoketAddress() + "]");
btnConn.setText("stop");
btnSend.setDisable(false);//[send] 버튼 활성화
});
} catch(Exception e) {
Platform.runLatet(()->displayText("[서버 통신 안됨]"));
if(!socket.isClosed()) { stopClient(); }//Socket이 닫혀있지 않으면 stopClient() 메소드를 호출
return;
}
receive();//서버에서 보낸 데이터 받기//예외가 발생하지 않으면 receive()메소드를 호출
}
};
thread.start();//스레드 시작
}
stopClient() 메소드
[stop] 버튼을 클릭하거나 서버 통신이 안 될 경우 stopClient() 메소드가 실행되는데, stopClient() 메소드에는 Socket을 닫는 close() 메소드 호출 코드가 있다.
void stopClient() {
try {
Platform.runLater(()->{//UI를 변경하기 위해 Platform.runLater()가 사용되었다.
displayText("[연결 끊음]");
btnConn.setText("start");
btnSend.setDisable(true);//[send] 버튼을 비활성화한다.
});
if(socket!=null && !socket.isClosed()) {//연결 끊기
socket.close();
}
} catch (IOException e) {}
}
receive() 메소드
receive() 메소드는 서버에서 보낸 데이터를 받는 역할을 한다. 이 메소드는 startClient()에서 생성한 작업 스레드상에서 호출된다.
void receive() {
while(true) {
try {
byte[ ] byteArr = new byte[100];//받은 뎅터 저장용(길이 100인 바이트 배열)
InputStream inputStream = socket.getInputStream();
//서버가 비정상적으로 종료했을 경우 IOException 발생
int readByteCount = inputStream.read(byteArr);//데이터 받기, 서버기 데이터를 보내기 전까지 블로킹되며, 데이터를 받으면 byteArr에 저장하고 받은 바이트 개수는 readByteCount에 저장
//서버가 정상적으로 Socket의 close()를 호출했을 경우
if(readByteCount == -1) { throw new IOException(); }
String data = new String(byteArr, 0, readByteCount, "UTF-8");//문자열로 변환, 정상적으로 데이터를 받았을 경우, UTF-8로 디코딩한 문자열을 얻는다.
Platform.runLater(()->displayText("[받기 완료] " + data));
} catch (Exception e) {
Platform.runLater(()->displayText("[서버 통신 안됨]"));//서버가 비정상적으로 연결을 끊게 되면 8라인의 IOException이 발생하고, 서버 측 Socket이 close()를 호출해서 정상적으로 연결을 끊게 되면 11라인에서 IOException이 발생한다. 예외가 발생하면 해당 문자열 출력
stopClient();
break;
}
}
}
send(String data) 메소드
send(Sting data) 메소드는 사용자가 메시지를 입력하고 [send] 버튼을 클릭하면 메시지를 매개값으로 호출한다. send() 메소드는 서버로 메시지를 보내는 역할을 한다.
void send(String data) {
Thread thread = new Thread() {//데이터를 서버로 보내는 새로운 작업 스레드를 생성한다.
@Override
public void run() {
try {
byte[ ] byteArr = data.getBytes("UTF-8");//보낼 문자열로부터 UTF-8로 인코딩한 바이트 배열을 얻는다.
OutputStream outputStream = socket.getOutputStream();//Socket에서 출력 스트림을 얻는다.
outputStream.write(byteArr);
outputStream.flush();//출력 스트림의 내부 버퍼를 완전히 비우도로고 flush()를 호출한다.
Platform.runLater(()->displayText("[보내기 완료]"));
} catch(Exception e) {
Platform.runLater(()->displayText("[서버 통신 안됨]"));//8fkdls 예외발생하면 해당 문자열 출력
stopClient();
}
}
};
thread.start();//스레드 생성
}
UI 생성 코드
다음은 JavaFX를 이용한 UI 생성 코드를 보여준다. 프로그램적 레이아웃을 이용해서 컨트롤을 배치했다.
TextArea txtDisplay;
TextField txtInput;
Button btnConn, btnSend;
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new Borderpane();
root.serPrefSize(500, 300);
txtDisplay = new TextArea();
txtDisplay.setEditable(false);
BorderPane.setMargin(txtDisplay, new Insets(0,0,2,0));
root.setCenter(txtDisplay);
BorderPane bottom = new BorderPane();
txtInput = new TextField();
txtInput.setPrefSize(60, 30);
BorderPane.setMargin(txtInput, new Insets(0,1,1,1));
btnConn = new Button("start");
btnConn.setPrefSize(60,30);
btnConn.setOnAction(e->{//start와 stop 버튼을 클릭했을 때 이벤트 처리 코드
if(btnConn.getText().eauals("start")) {
startClient();
} else if(btnConn.getText().equals("stop")){
stopClient();
}
});
btnSend = new Button("send");
btnSend.setPrefSize(60, 30);
btnSend.setDisable(true);
btnSend.setOnAction(e->send(txtInput.getText()));//send 버튼을 클릭했을 때 이벤트 처리 코드
bottom.setCenter(txtInput);
bottom.setLeft(btnConn);
bottom.setRight(btnSend);
root.setBottom(bottom);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toString());
primaryStage.setScene(scene);
PrimaryStage.setTitle("Client");
primaryStage.setOnCloseRequest(event->stopClient());//윈도우 우측 상단 닫기 버튼을 클릭했을 때 이벤트 처리 코드
primaryStage.show();
}
//TextArea에 문자열을 추가하는 메소드
void displayText(String text) {
txtDisplay.appendText(text + "\n");
}
public static void main(String[ ] args) {
launch(args);
}
다음 채팅 서버와 클라이언트를 실행하는 방법을 보여준다. 서버인 ServerExample을 한 개 실행하고 클라이언트인 ClientExample을 두 개 실행한다.
[ServerExample.java]
package chatting;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServerExample extends Application {
ExecutorService executorService;
ServerSocket serverSocket;
List<Client> connections = new Vector<Client>(); // 스레드에 안전함
void startServer(){
// 서버 시작 코드 (Executor Service 생성, ServerSocket 생성 및 포트 바인딩, 연결 수락)
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
try{
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", 5001));
}catch(Exception e){
if(!serverSocket.isClosed()) { stopServer();}
return;
}
Runnable runnable = new Runnable() { // 수락 작업을 생성
@Override
public void run() {
Platform.runLater(()-> {
// UI 설정
displayText("[server start]");
btnStartStop.setText("stop");
});
while(true){
try{
Socket socket = serverSocket.accept(); // 연결 수락
String message = "[accept connections : " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";
Platform.runLater(()->displayText(message));
Client client = new Client(socket);
connections.add(client);
Platform.runLater(()-> displayText("[number of connections : " + connections.size() + " ]"));
}catch(Exception e){
if(!serverSocket.isClosed()) { stopServer();}
break;
}
}
}
};
executorService.submit(runnable); // 스레드 풀에서 처리
}
void stopServer(){
// 서버 종료 코드
try{
Iterator<Client> iterator = connections.iterator();
while(iterator.hasNext()){ // 모든 socket 닫기
Client client = iterator.next();
client.socket.close();
iterator.remove();
}
if(serverSocket != null && !serverSocket.isClosed()){ // server socket 닫기
serverSocket.close();
}
if(executorService != null && !executorService.isShutdown()){ // ExecutorService 종료
executorService.shutdown();
}
Platform.runLater(() -> {
displayText("[server stop]");
btnStartStop.setText("start");
});
}catch(Exception e){ }
}
class Client {
// 데이터 통신 코드
Socket socket;
Client(Socket socket){
this.socket = socket;
receive();
}
void receive(){
// 데이터 받기 코드
Runnable runnable = new Runnable() { // 받기 작업 생성
@Override
public void run() {
try{
while(true){
byte[] byteArr = new byte[100];
InputStream inputStream = socket.getInputStream();
int readByteCount = inputStream.read(byteArr); // 데이터 받기
// 클라이언트가 정상적으로 Socket의 close()를 호출했을 경우
if(readByteCount == -1) { throw new IOException();}
String message = "[request processing: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + "]";
Platform.runLater(()->displayText(message));
String data = new String(byteArr, 0, readByteCount, "UTF-8");
for(Client client : connections){
client.send(data); // 모든 클라이언트에게 보냄
}
}
}catch(Exception e){
try{
connections.remove(Client.this);
String message = "[client communication failure: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + " ]";
Platform.runLater(()->displayText(message));
socket.close();
}catch(IOException e2) {}
}
}
};
executorService.submit(runnable); // 스레드풀에서 처리
}
void send(String data){
// 데이터 전송 코드
Runnable runnable = new Runnable() { // 보내기 작업 생성
@Override
public void run() {
try{
byte[] byteArr = data.getBytes("UTF-8");
OutputStream outputStream = socket.getOutputStream();
outputStream.write(byteArr);
outputStream.flush();
}catch(Exception e){
try {
String message = "[client communication failure: " + socket.getRemoteSocketAddress() + ": " + Thread.currentThread().getName() + " ]";
Platform.runLater(() -> displayText(message));
connections.remove(Client.this);
socket.close();
}catch(IOException e2) {}
}
}
};
executorService.submit(runnable); // 스레드풀에서 처리
}
}
////////
// UI 생성 코드
TextArea txtDisplay;
Button btnStartStop;
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
root.setPrefSize(500, 300);
txtDisplay = new TextArea();
txtDisplay.setEditable(false);
BorderPane.setMargin(txtDisplay, new Insets(0,0,2,0));
root.setCenter(txtDisplay);
btnStartStop = new Button("start");
btnStartStop.setPrefHeight(30);
btnStartStop.setMaxWidth(Double.MAX_VALUE);
btnStartStop.setOnAction(e->{
if(btnStartStop.getText().equals("start")){
startServer();
}else if(btnStartStop.getText().equals("stop")){
stopServer();
}
});
root.setBottom(btnStartStop);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toString());
primaryStage.setScene(scene);
primaryStage.setTitle("Server");
primaryStage.setOnCloseRequest(event->stopServer());
primaryStage.show();
}
void displayText(String text){
txtDisplay.appendText(text + "\n");
}
public static void main(String[] args){
launch(args);
}
}
[ClientExample.java]
package chatting;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
public class ClientExample extends Application {
Socket socket;
void startClient(){
// 연결 시작 코드
Thread thread = new Thread(){
@Override
public void run(){
try {
socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 5001));
Platform.runLater(()->{
displayText("[connection complete: " + socket.getRemoteSocketAddress() + "]");
btnConn.setText("stop");
btnSend.setDisable(false);
});
}catch(Exception e){
Platform.runLater(()->displayText("[server communication failure]"));
if(!socket.isClosed()) { stopClient(); }
return;
}
receive(); // 서버에서 보낸 데이터 받기
}
};
thread.start();
}
void stopClient(){
// 연결 끊기 코드
try{
Platform.runLater(()->{
displayText("[disconnect from]");
btnConn.setText("start");
btnSend.setDisable(true);
});
if(socket != null && !socket.isClosed()){
socket.close();
}
}catch(IOException e){ }
}
void receive(){
// 데이터 받기 코드
while(true){
try {
byte[] byteArr = new byte[100];
InputStream inputStream = socket.getInputStream();
int readByteCount = inputStream.read(byteArr);
if(readByteCount == -1) { throw new IOException();}
String data = new String(byteArr, 0, readByteCount, "UTF-8");
Platform.runLater(()->displayText("[Received] " + data));
} catch (IOException e) {
Platform.runLater(()->displayText("[server communication failure]"));
stopClient();
break;
}
}
}
void send(String data){
// 데이터 전송 코드
Thread thread = new Thread(){
@Override
public void run() {
try {
byte[] byteArr = data.getBytes("UTF-8");
OutputStream outputStream = socket.getOutputStream();
outputStream.write(byteArr);
outputStream.flush();
Platform.runLater(()->displayText("[completion of sending]"));
} catch (IOException e) {
Platform.runLater(()->displayText("[server communication failure]"));
stopClient();
}
}
};
thread.start();
}
///////
// UI 생성 코드
TextArea txtDisplay;
TextField txtInput;
Button btnConn, btnSend;
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
root.setPrefSize(500, 300);
txtDisplay = new TextArea();
txtDisplay.setEditable(false);
BorderPane.setMargin(txtDisplay, new Insets(0,0,2,0));
root.setCenter(txtDisplay);
BorderPane bottom = new BorderPane();
txtInput = new TextField();
txtInput.setPrefSize(60, 30);
BorderPane.setMargin(txtInput, new Insets(0,1,1,1));
btnConn = new Button("start");
btnConn.setPrefSize(60, 30);
btnConn.setOnAction(e->{
if(btnConn.getText().equals("start")){
startClient();
}else if(btnConn.getText().equals("stop")){
stopClient();
}
});
btnSend = new Button("send");
btnSend.setPrefSize(60, 30);
btnSend.setDisable(true);
btnSend.setOnAction(e->send(txtInput.getText()));
bottom.setCenter(txtInput);
bottom.setLeft(btnConn);
bottom.setRight(btnSend);
root.setBottom(bottom);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toString());
primaryStage.setScene(scene);
primaryStage.setTitle("Client");
primaryStage.setOnCloseRequest(event->stopClient());
primaryStage.show();
}
void displayText(String text){
txtDisplay.appendText(text + "\n");
}
}
[app.css]
/*text-area 배경색*/
.text-area {
-fx-background-color: gold;
}
/*scroll-pane 배경색*/
.text-area .scroll-pane {
-fx-background-color: transparent;
}
/*viewport 배경색*/
.text-area .scroll-pane .viewport{
-fx-background-color: transparent;
}
/*content 배경색*/
.text-area .scroll-pane .content{
-fx-background-color: transparent;
}
[실행결과]
'👨🏫Study > JAVA' 카테고리의 다른 글
[JAVA] 18 - 4. UDP 네트워킹 (0) | 2022.03.31 |
---|---|
[JAVA] 예외(EXCEPTION) 처리 (0) | 2022.03.31 |
[JAVA] NullPointerException 처리방법 (0) | 2022.03.30 |
[JAVA] 18 - 2. 네트워킹 (0) | 2022.03.29 |
[JAVA] 18 - 1. IO 기반 입출력 및 네트워킹 (0) | 2022.03.25 |
댓글