이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다.
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에 추가
- removeSocketClient() : 연결이 끊긴 클라이언트의 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("[클라이언트] 서버 연결 안됨");
}
}
}
'Java Category > Java' 카테고리의 다른 글
[Java] 데이터베이스에 저장(쓰기, 수정, 삭제) 및 읽기 (0) | 2023.08.18 |
---|---|
[Java] JDBC 개요 및 DB 연결하기 (0) | 2023.08.17 |
[Java] JSON 데이터 형식 (0) | 2023.08.15 |
[Java] 서버의 동시 요청 처리(스레드풀 이용) (0) | 2023.08.14 |
[Java] UDP 네트워킹 (0) | 2023.08.13 |