Java Category/Java

[Java] TCP 채팅 프로그램

ReBugs 2023. 8. 16.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


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("[클라이언트] 서버 연결 안됨");
		}
	}
}

댓글