import java.util.Scanner;
public class Main {
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
int number;
String string;
System.out.print("숫자 입력 : ");
number = sc.nextInt();
System.out.println("문자열 입력 : ");
string = sc.nextLine();
System.out.println("숫자 입력 결과 : " + number);
System.out.println("문자열 입력 결과 : " + string);
}
}
/*
숫자 입력 : 6546201
문자열 입력 :
숫자 입력 결과 : 6546201
문자열 입력 결과 :
*/
의도대로라면 숫자와 문자를 모두 입력을 받아야 하지만, 숫자만 입력받고 프로그램이 종료되었다.
이유는 next()는 개행문자(\n)을 무시하고 입력을 받고, 반대로 nextLine()은 개행문자를 포함해서 입력을 받기 때문이다.
따라서 입력 버퍼에는 6546201\n이 들어오게 되고 next()는 개행문자를 무시하기 때문에 6546201만 가져오게 되고, 따라서 버퍼에는 \n만 남게된다.
결과적으로 버퍼에 남은 \n을 nextLine()이 가져오게되고 프로그램이 종료된 것이다.
next() 버퍼에 입력된 문자나 문자열에서 공백과 개행문자 전까지를 가져온다.
nextLine() 개행문자를 만날 때까지의 문자열 전체를 입력받는다. 버퍼에 입력된 문자열(공백포함)을 개행문자까지 다 가져온다.
따라서 이러한 문제를 해결하기 위해서는 버퍼에 잔류하는 내용물을 비워줄 필요가 있다.
즉, 버퍼를 비워줘야 한다.
next()와 nextLine() 사이에 매개 문자열이 없는 nextLine()을 두면 버퍼를 비울 수 있다.
import java.util.Scanner;
public class Main {
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
int number;
String string;
System.out.print("숫자 입력 : ");
number = sc.nextInt();
sc.nextLine();
System.out.println("문자열 입력 : ");
string = sc.nextLine();
System.out.println("숫자 입력 결과 : " + number);
System.out.println("문자열 입력 결과 : " + string);
}
}
/*
숫자 입력 : 1234
문자열 입력 :
asd asdasd
숫자 입력 결과 : 1234
문자열 입력 결과 : asd asdasd
*/
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다.
트랜잭션
트랜잭션(transaction)은 기능 처리의 최소 단위를 말한다.
하나의 기능은 여러가지 소작업들로 구성된다.
최소 단위라는 것은 이 소작업들을 분리할 수 없으며, 전체를 하나로 본다는 개념이다.
트랜잭션은 소작업들이 모두 성공하거나 실패해야 한다.
예를 들어 계좌 이체는 출금과 입금으로 구성된 트랜잭션이다.
출금과 입금 작업 중 하나만 성공할 수 없으며, 모두 성공하거나 모두 실패해야 한다.
계좌 이체는 DB 입장에서 보면 두 개의 계좌 금액을 수정하는 작업이다.
출금 계좌에서 금액을 감소시키고, 입금 계좌에서 금액을 증가시킨다.
따라서 아래와 같이 두 개의 UPDATE 문이 필요하다.
두 UPDATE 문은 모두 성공하거나 모두 실패해야 하며, 하나만 성공할 수 없다.
DB는 트랜잭션을 처리하기 위해 커밋(commit)과 롤백(rollback)을 제공한다.
커밋은 내부 작업을 모두 성공 처리하고, 롤백은 실행 전으로 돌아간다는 의미에서 모두 실패 처리한다.
JDBC에서는 INSERT, UPDATE, DELETE 문을 실행할 때마다 자동 커밋이 일어난다.
이 기능은 계좌 이체와 같이 두 가지 UPDATE 문을 실행할 때 문제가 된다.
출금 작업이 성공되면 바로 커밋이 되기 때문에, 입금 작업의 성공 여부와 상관없이 출금 작업만 별도 처리된다.
따라서 JDBC에서 트랜잭션을 코드로 제어하려면 자동 커밋 기능을 꺼야 한다.
자동 커밋 설정 여부는 Connection의 setAutoCommit() 메소드로 할 수 있다.
아래의 코드는 자동 커밋 기능을 끈다.
conn.setAutoCommit(false);
자동 커밋 기능이 꺼지면, 아래와 같은 코드로 커밋과 롤백을 제어할 수 있다.
conn.commit();
conn.rollback();
트랜잭션을 처리한 이후에는 원래대로 자동 커밋 기능을 켜둬야 한다.
Connection을 다른 기능 처리를 위해 계속 사용해야 한다면 setAutoCommit(true) 코드로 자동 커밋 기능을 켜둬야 한다.
참고 수동 커밋 모드나 자동 커밋 모드는 한번 설정하면 해당 세션에서는 계속 유지된다. 중간에 변경하는 것은 가능 하다.
특히, 커넥션 풀을 사용할 때 주의해야할 부분이다.
커넥션 풀(Connection Pool) 다수의 클라이언트 요청을 처리하는 서버 프로그램은 대부분 커넥션 풀을 사용한다. 커넥션 풀은 일정량의 Connection을 미리 생성시켜놓고, 서버에서 클라이언트 요청을 처리할 때 Connection을 제공해주고 다시 반환받는 역할을 수행한다.
커넥션 풀을 사용하면 생성된 Connection을 재사용할 수 있기 때문에 DB 연결 시간을 줄일 수 있고, 전체 Connection 수를 관리할 수도 있다.
사용 예제
아래의 예제 코드는 하여름의 계좌에서 한겨울의 계좌로 10,000원을 송금하는 예제이다.
물론, DB에 등록되지 않은 계좌로 송금을 하려고 한다던가, 계좌 금액이 부족하면 커밋이 되지 않고 롤백이 된다.
아래의 코드를 실행하고 나면 DB는 아래처럼 된다.
Oracle
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransactionExample {
public static void main(String[] args) {
Connection conn = null;
try {
//JDBC Driver 등록
Class.forName("oracle.jdbc.OracleDriver");
//연결하기
conn = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521/orcl",
"java",
"oracle"
);
//트랜잭션 시작 ----------------------------------------------------
//자동 커밋 기능 끄기
conn.setAutoCommit(false);
//출금 작업
String sql1 = "UPDATE accounts SET balance=balance-? WHERE ano=?";
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
pstmt1.setInt(1, 10000);
pstmt1.setString(2, "111-111-1111");
int rows1 = pstmt1.executeUpdate();
if(rows1 == 0) throw new Exception("출금되지 않았음");
pstmt1.close();
//입금 작업
String sql2 = "UPDATE accounts SET balance=balance+? WHERE ano=?";
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
pstmt2.setInt(1, 10000);
pstmt2.setString(2, "222-222-2222");
int rows2 = pstmt2.executeUpdate();
if(rows2 == 0) throw new Exception("입금되지 않았음");
pstmt2.close();
//수동 커밋 -> 모두 성공 처리
conn.commit();
System.out.println("계좌 이체 성공");
//트랜잭션 종료 ----------------------------------------------------
} catch (Exception e) {
try {
//수동 롤백 -> 모두 실패 처리
conn.rollback();
} catch (SQLException e1) {}
System.out.println("계좌 이체 실패");
e.printStackTrace();
} finally {
if(conn != null) {
try {
//원래대로 자동 커밋 기능 켜기
conn.setAutoCommit(true);
//연결 끊기
conn.close();
} catch (SQLException e) {}
}
}
}
}
/*
계좌 이체 성공
*/
MySQL
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransactionExample {
public static void main(String[] args) {
Connection conn = null;
try {
//JDBC Driver 등록
Class.forName("com.mysql.cj.jdbc.Driver");
//연결하기
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/thisisjava",
"java",
"mysql"
);
//트랜잭션 시작 ----------------------------------------------------
//자동 커밋 기능 끄기
conn.setAutoCommit(false);
//출금 작업
String sql1 = "UPDATE accounts SET balance=balance-? WHERE ano=?";
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
pstmt1.setInt(1, 10000);
pstmt1.setString(2, "111-111-1111");
int rows1 = pstmt1.executeUpdate();
if(rows1 == 0) throw new Exception("출금되지 않았음");
pstmt1.close();
//입금 작업
String sql2 = "UPDATE accounts SET balance=balance+? WHERE ano=?";
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
pstmt2.setInt(1, 10000);
pstmt2.setString(2, "333-222-2222");
int rows2 = pstmt2.executeUpdate();
if(rows2 == 0) throw new Exception("입금되지 않았음");
pstmt2.close();
//커밋 -> 모두 성공 처리
conn.commit();
System.out.println("계좌 이체 성공");
//트랜잭션 종료 ----------------------------------------------------
} catch (Exception e) {
try {
//롤백 -> 모두 실패 처리
conn.rollback();
//원래대로 자동 커밋 기능 켜기
conn.setAutoCommit(true);
} catch (SQLException e1) {}
System.out.println("계좌 이체 실패");
e.printStackTrace();
} finally {
if(conn != null) {
try {
//원래대로 자동 커밋 기능 켜기
conn.setAutoCommit(true);
//연결 끊기
conn.close();
} catch (SQLException e) {}
}
}
}
}
프로시저와 함수의 리턴 값을 받기 위해 registerOutParameter()에 들어가는 두 번째 매개값(리턴 타입)의 종류는 아래의 링크에서 자세히 확인할 수 있다.(공식 API Document) https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Types.html
?에 대한 설정이 끝나면 프로시저 또는 함수를 호출하기 위해 execute() 메소드를 아래와 같이 호출한다.
cstmt.execute();
호출 후에는 Getter 메소드로 리턴값을 얻을 수 있다. 리턴 타입이 정수라고 가정하면, 프로시저의 세 번째 ?의 리턴값과 함수의 리턴값은 아래와 같이 얻을 수 있다.
더 이상 CallableStatement를 사용하지 않는다면 close() 메소드로 사용했던 메모리를 해제해야 한다.
cstmt.close();
프로시저 호출
create or replace PROCEDURE user_create ( a_userid IN users.userid%TYPE, a_username IN users.username%TYPE, a_userpassword IN users.userpassword%TYPE, a_userage IN users.userage%TYPE, a_useremail IN users.useremail%TYPE, a_rows OUT PLS_INTEGER ) IS BEGIN INSERT INTO users (userid, username, userpassword, userage, useremail) VALUES (a_userid, a_username, a_userpassword, a_userage, a_useremail); a_rows := SQL%ROWCOUNT; COMMIT; END;
DB에 위와 같이 프로시저가 정의되어 있다.
위 프로시저는 SQL문에 의해 실행된 명령(users 테이블 행 추가)의 수를 반환하는 프로시저이다.
즉, 프로그램에서 매개값으로 넘긴 값들을 INSERT문으로 행을 추가하고, 처리된 행의 수를 리턴하는 것이다.
IN 매개변수는 호출 시 필요한 매개값으로 사용되며, OUT 매개변수는 리턴값으로 사용된다.
위와 같이 작성된 프로시저를 호출하기 위해 아래와 같이 매개변수화된 호출문을 작성하고 CallableStatement를 얻는다.
프로시저와 함수의 리턴 값을 받기 위해 registerOutParameter()에 들어가는 두 번째 매개값(리턴 타입)의 종류는 아래의 링크에서 자세히 확인할 수 있다.(공식 API Document) https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Types.html
이제 user_create 프로시저를 실행하고, 아래와 같이 리턴값을 얻는다.
user_create 프로시저의 리턴값은 사용자 정보가 성공적으로 저장되었을 때 항상 1이 된다.
cstmt.execute();
int rows = cstmt.getInt(6); //6번째 ? 값 얻기
사용 예제(Oracle)
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;
public class ProcedureCallExample {
public static void main(String[] args) {
Connection conn = null;
try {
//JDBC Driver 등록
Class.forName("oracle.jdbc.OracleDriver");
//연결하기
conn = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521/orcl",
"java",
"oracle"
);
//매개변수화된 호출문 작성과 CallableStatement 얻기
String sql = "{call user_create(?, ?, ?, ?, ?, ?)}";
CallableStatement cstmt = conn.prepareCall(sql);
//? 값 지정 및 리턴 타입 지정
cstmt.setString(1, "summer");
cstmt.setString(2, "한여름");
cstmt.setString(3, "12345");
cstmt.setInt(4, 26);
cstmt.setString(5, "summer@mycompany.com");
cstmt.registerOutParameter(6, Types.INTEGER);
//함수 실행 및 리턴값 얻기
cstmt.execute();
int rows = cstmt.getInt(6);
System.out.println("저장된 행 수 " + rows);
//CallableStatement 닫기
cstmt.close();
}catch (Exception e) {
e.printStackTrace();
} finally {
if(conn != null) {
try {
//연결 끊기
conn.close();
} catch (SQLException e) {}
}
}
}
}
/*
저장된 행 수 1
*/
함수 호출
create or replace FUNCTION user_login ( a_userid users.userid%TYPE, a_userpassword users.userpassword%TYPE ) RETURN PLS_INTEGER IS v_userpassword users.userpassword%TYPE; v_result PLS_INTEGER; BEGIN SELECT userpassword INTO v_userpassword FROM users WHERE userid = a_userid;
IF v_userpassword = a_userpassword THEN RETURN 0; ELSE RETURN 1; END IF; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN 2; END;
DB에 함수는 위와 같이 선언되어 있다.
user_login()은 2개의 매개변수와 PLS_INTEGER 리턴 타입으로 구성되어 있다. 2개의 매개변수는 호출 시 값을 제공하고, 호출 후에는 정수 값을 리턴한다.
위 함수는 받은 매개값(ID와 PW) 을 DB에서 검색을 하고, ID와 PW가 일치하면 0을 리턴하고, 일치하지 않으면 1을 리턴하고, 해당 ID가 없으면 2를 리턴한다.
user_login() 함수를 호출하기 위해 아래와 같이 매개변수화된 호출문을 작성하고 CallableStatement를 얻는다.
프로시저와 함수의 리턴 값을 받기 위해 registerOutParameter()에 들어가는 두 번째 매개값(리턴 타입)의 종류는 아래의 링크에서 자세히 확인할 수 있다.(공식 API Document) https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Types.html
위 코드의 매개값에는 두 번째 매개값과 세 번째 매개값에 자동으로 노란색으로 칠한 매개값이 들어가 있다. 두 번째 매개값 : TYPE_FORWORD_ONLY 세 번째 매개값 : CONCUR_READ_ONLY 즉, 커서가 뒤로 이동할 수 없고 오직 앞으로만 이동 가능하며, 읽기 전용이라는 뜻이다.
두 번째 매개값과 세 번째 매개값에 초록색으로 칠한 매개값을 넣어주면 ResultSet 메소드를 통해 커서를 자유자재로 이동할 수 있고, 값 수정도 가능하다.
SELECT 문에 따라 ResultSet에는 많은 데이터 행이 저장될 수 있기 때문에 ResultSet을 더 이상 사용하지 않는다면 close() 메소드를 호출해서 사용한 메모리를 해제하는 것이 좋다.
rs.close();
데이터 행 읽기
커서가 있는 데이터 행에서 각 컬럼의 값은 Getter 메소드로 읽을 수 있다.
컬럼의 데이터 타입에 따라 getXxx() 메소드가 사용되며, 매개값으로 컬럼의 이름 또는 컬럼 순번을 줄 수 있다.
ResultSet에서 컬럼 순번은 1부터 시작하기 때문에 userid = 1, username = 2, userage = 3이 된다.
만약 SELECT 문에 연산식이나 함수 호출이 포함되어 있다면 컬럼 이름 대신에 컬럼 순번으로 읽어야 한다. 예를 들어 아래와 같은 SELECT 문에서 userage -1 연산식이 사용되면 컬럼 순번으로만 읽을 수 있다. userage -1은 컬럼명이 아니기 때문이다.
(userage -1) as userage와 같이 별명이 있다면 별명이 컬럼 이름이 된다.
데이터 추출
users 테이블
어떤 조건에 만족하는 데이터만 가져오려면 WHERE 에 조건을 명시한다.
예를 들어, userid가 winter인 사용자 정보를 가져오는 SELECT 문은 아래와 같다.
SELECT userid, username, userpassword, userage, useremail FROM users WHERE userid = 'winter';
조건절의 값을 ?로 대체한 매개변수화된 SQL 문을 String 타입 변수 sql에 대입한다.
Blob 객체에 저장된 바이너리 데이터를 얻기 위해서는 아래와 같이 입력 스트림 또는 바이트 배열을 얻어내야 한다.
입력스트림을 사용할 때 : 파일을 저장할 때 또는 전송할 때
바이트 배열을 사용할 때 : UI프로그램 등에서 화면상에서 그림을 그려야할 때
아래의 코드는 Blob 객체에서 InputStream을 얻고, 읽은 바이트를 파일로 저장하는 방법을 보여준다.
InputStream is = blob.getBinaryStream();
OutputStream os = new FileOutputStream("C:/Temp/" + board.getBfilename());
is.transferTo(os);
os.flush();
os.close();
is.close();
transferTo()메소드 기존에는 입력스트림을 출력스트림으로 전달하려면 아래와 같은 코드를 작성해야 했다.
byte[] data = new byte[1024];
while(true)
{
int num = is.read(data);
if (num == -1) break;
os.write(data, 0, num);
}
이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다.
TCP 채팅 프로그램 작동 원리
클래스
용도
ChatServer
-채팅 서버 실행 클래스 -ServerSocket을 생성하고 50001 포트에 바인딩 -ChatClient 연결 수락 후 SocketClient 생성
SocketClient
-ChatClient와 1:1로 통신
ChatClient
-채팅 클라이언트 실행 클래스 -ChatServer에 연결 요청 -SocketClient와 1:1로 통신
ChatServer 클래스에는 ServerSocket 객체가 50001번 포트로 들어오는 클라이언트 연결 요청을 수락한다.
SocketClient 객체는 연결 요청한 클라이언트 수만큼 생성된다.(스레드풀로 관리)
SocketClient의 Socket은 ChatClient 안에 있는 Socket과 1:1로 통신한다.
서버
ChatServer 클래스
필드
serverSocket : 클라이언트 요청 수락
threadPool : 스레드풀의 스레드 수를 100개로 제한(총 100개의 클라이언트가 동시 채팅 가능)
chatRoom : 통신용 SocketClient를 관리하는 동기화된 Map
메소드
start() : 채팅 서버가 시작할 때 가장 먼저 호출됨 50001번 포트에 바인딩하는 ServerSocket 객체를 생성 작업 스레드가 처리할 Runnable 구현 객체를 람다식으로 제공 클라이언트가 연결 요청을 하면 수락하고 해당 클라이언트와 통신용 SocketClient를 생성
addSocketClient() : 연결된 클라이언트의 SocketClient를 chatRoom에 추가
sendToAll() : JSON 메시지를 생성해 채팅방에 있는 모든 클라이언트에게 보내는 역할 chatRoom.values()로 Collection<SocketClient>(맵의 키가 아닌 값)를 얻은 후 모든 SocketClient로 send() 메소드로 JSON을 보냄 JSON은 아래와 같이 구성된다.
stop() : 서버를 종료시키는 역할 작업 스레드와 스레드풀을 종료시키지 않으면 프로세스가 종료되지 않으므로 메인 스레드, 작업 스레드, 스레드풀 모두 종료시킨다.
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.json.JSONObject;
public class ChatServer {
//필드
ServerSocket serverSocket;
ExecutorService threadPool = Executors.newFixedThreadPool(100); //스레드풀의 스레드 개수를 100개로 제한
Map<String, SocketClient> chatRoom = new Hashtable<>(); //해시 테이블은 동기화된 메소드를 제공해서 안전
/*
String : chatName@IP
SocketClient : SocketClient 객체
*/
//메소드: 서버 시작
public void start() throws IOException {
serverSocket = new ServerSocket(50001); //50001번 포트로 바인딩
System.out.println( "[서버] 시작됨");
Thread thread = new Thread(() -> { //서버에 연결 요청 수락을 대기하는 스레드
try {
while(true) {
Socket socket = serverSocket.accept(); //연결 요청이 들어오면 수락
SocketClient sc = new SocketClient(ChatServer.this, socket); // SocketClient 생성자에 이 클래스 객체와 socket 객체를 매개값으로 넘김
}
} catch(IOException e) {
}
});
thread.start(); //스레드 시작
}
//메소드: 클라이언트 연결시 SocketClient 생성 및 추가
public void addSocketClient(SocketClient socketClient) {
String key = socketClient.chatName + "@" + socketClient.clientIp;
chatRoom.put(key, socketClient); //chatRoom에 <chatName@IP, socketClient> 추가
System.out.println("입장: " + key);
System.out.println("현재 채팅자 수: " + chatRoom.size() + "\n"); //현재 서버에 몇명이 들어왔는지 표시
}
//메소드: 클라이언트 연결 종료시 SocketClient 제거
public void removeSocketClient(SocketClient socketClient) {
String key = socketClient.chatName + "@" + socketClient.clientIp;
chatRoom.remove(key); //chatRoom에서 해당 키 제거
System.out.println("나감: " + key);
System.out.println("현재 채팅자 수: " + chatRoom.size() + "\n");
}
//메소드: 모든 클라이언트에게 메시지 보냄
public void sendToAll(SocketClient sender, String message) {
JSONObject root = new JSONObject();
root.put("clientIp", sender.clientIp); //JSON에 IP 추가
root.put("chatName", sender.chatName); //JSON에 chatName 추가
root.put("message", message); //JSON에 보낼 메시지 추가
String json = root.toString(); //String 타입으로 변환
Collection<SocketClient> socketClients = chatRoom.values(); //chatRoom의 키가 아닌 값들만 뽑아옴
for(SocketClient sc : socketClients) { //연결된 클라이언트들에게
if(sc == sender) continue; //발송자를 제외하고
sc.send(json); //메시지 전송
}
}
//메소드: 서버 종료
public void stop() {
try {
serverSocket.close();
threadPool.shutdownNow();
chatRoom.values().stream().forEach(sc -> sc.close()); //내부 반복자를 이용하여 연결된 클라이언트 모두 종료
System.out.println( "[서버] 종료됨 ");
} catch (IOException e1) {}
}
//메소드: 메인
public static void main(String[] args) {
try {
ChatServer chatServer = new ChatServer();
chatServer.start();
System.out.println("----------------------------------------------------");
System.out.println("서버를 종료하려면 q를 입력하고 Enter.");
System.out.println("----------------------------------------------------");
Scanner scanner = new Scanner(System.in);
while(true) {
String key = scanner.nextLine();
if(key.equals("q")) break;
}
scanner.close();
chatServer.stop();
} catch(IOException e) {
System.out.println("[서버] " + e.getMessage());
}
}
}
SocketClient 클래스
필드
chatServer : ChatServer 객체의 메소드를 호출하기 위함
socket : 연결을 끊을 때 필요
dis, dos : 문자열을 읽고 보내기 위한 보조스트림
clientIp : 클라이언트의 IP
chatName : 클라이언트의 대화명
메소드
receive() : 클라이언트가 보낸 JSON 메시지를 읽는 역할 dis.readUTF로 JSON을 읽는다. JSON의 command가 incoming이라면 대화명을 읽고 chatRoom에 추가한다. command가 message라면 메시지를 읽고 모든 클라이언트에게 메시지를 보낸다.
send() : 연결된 클라이언트에게 JSON 메시지를 보내는 역할
close() : 클라이언트와 연결을 끊는 역할
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.json.JSONObject;
public class SocketClient {
//필드
ChatServer chatServer;
Socket socket;
DataInputStream dis; //송신
DataOutputStream dos; //수신
String clientIp;
String chatName;
//생성자
public SocketClient(ChatServer chatServer, Socket socket) {
try {
this.chatServer = chatServer;
this.socket = socket;
this.dis = new DataInputStream(socket.getInputStream());
this.dos = new DataOutputStream(socket.getOutputStream());
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress(); //클라이언트의 IP와 포트 정보를 저장
this.clientIp = isa.getHostString(); //IP정보를 저장
receive();
} catch(IOException e) {
}
}
//메소드: JSON 받기
public void receive() {
chatServer.threadPool.execute(() -> { //ChatServer의 스레드풀에 작업 전달
try {
while(true) {
String receiveJson = dis.readUTF(); //JSON을 받아옴
JSONObject jsonObject = new JSONObject(receiveJson);
String command = jsonObject.getString("command"); //명령 내용에 따라 액션이 달라짐
switch(command) {
case "incoming": //첫 입장인 경우 incoming 을 받음
this.chatName = jsonObject.getString("data"); //첫 입장일 경우 채팅네임이 data임
chatServer.sendToAll(SocketClient.this, "들어오셨습니다.");
chatServer.addSocketClient(this);
break;
case "message":
String message = jsonObject.getString("data"); //첫 입장이 아니면 메시지가 data임
chatServer.sendToAll(SocketClient.this, message);
break;
}
}
} catch(IOException e) { //클라이언트와 연결이 끊겼을 경우
chatServer.sendToAll(SocketClient.this, "나가셨습니다.");
chatServer.removeSocketClient(SocketClient.this);
}
});
}
//메소드: JSON 보내기
public void send(String json) {
try {
dos.writeUTF(json);
dos.flush();
} catch(IOException e) {
}
}
//메소드: 연결 종료
public void close() {
try {
socket.close();
} catch(Exception e) {}
}
}
클라이언트
ChatClient 클래스
필드
socket : 연결 요청과 연결을 끊을 때 필요
dis, dos : 문자열을 읽고 보내기 위한 보조 스트림
chatName : 클라이언트의 대화명
메소드
connect() : 채팅 서버(서버IP, 포트번호) : 에 연결을 요청하고 Socket을 필드에 저장한다.
receive() : 서버가 보낸 JSON 메시지를 읽는 역할 readUTF()로 JSON을 읽고 파싱한다.
send() : 서버로 JSON 메시지를 보내는 역할
unconnect() : 서버와의 연결을 끊는 역할
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
import org.json.JSONObject;
public class ChatClient {
//필드
Socket socket;
DataInputStream dis; //수신
DataOutputStream dos; //송신
String chatName;
//메소드: 서버 연결
public void connect() throws IOException {
socket = new Socket("localhost", 50001); //연결할 서버의 내용 제공, localhost에 서버 IP를 넣어야함, 50001은 서버의 포트번호
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
System.out.println("[클라이언트] 서버에 연결됨");
}
//메소드: JSON 받기
public void receive() {
Thread thread = new Thread(() -> { //메시지를 받는 스레드 생성
try {
while(true) {
String json = dis.readUTF(); //JSON 수신
JSONObject root = new JSONObject(json);
String clientIp = root.getString("clientIp");
String chatName = root.getString("chatName");
String message = root.getString("message");
System.out.println("<" + chatName + "@" + clientIp + "> " + message);
}
} catch(Exception e1) { //서어봐 연결이 끊겼을 시
System.out.println("[클라이언트] 서버 연결 끊김");
System.exit(0);
}
});
thread.start();
}
//메소드: JSON 보내기
public void send(String json) throws IOException {
dos.writeUTF(json);
dos.flush();
}
//메소드: 서버 연결 종료
public void unconnect() throws IOException {
socket.close();
}
//메소드: 메인
public static void main(String[] args) {
try {
ChatClient chatClient = new ChatClient();
chatClient.connect(); //서버에 연결
Scanner scanner = new Scanner(System.in);
System.out.println("대화명 입력: ");
chatClient.chatName = scanner.nextLine();
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", "incoming"); //command 에 incoming (첫 입장) 내용 전달
jsonObject.put("data", chatClient.chatName); //설정한 채팅네임 전달
String json = jsonObject.toString();
chatClient.send(json); //전달
chatClient.receive(); //메시지 수신을 기다림
System.out.println("--------------------------------------------------");
System.out.println("보낼 메시지를 입력하고 Enter");
System.out.println("채팅를 종료하려면 q를 입력하고 Enter");
System.out.println("--------------------------------------------------");
while(true) {
String message = scanner.nextLine();
if(message.toLowerCase().equals("q")) {
break;
} else {
jsonObject = new JSONObject();
jsonObject.put("command", "message"); //command 종류 : message
jsonObject.put("data", message); //data에 보낼 메시지 전달
json = jsonObject.toString();
chatClient.send(json); //전달
}
}
scanner.close();
chatClient.unconnect();
} catch(IOException e) {
System.out.println("[클라이언트] 서버 연결 안됨");
}
}
}