본문 바로가기

IT/Java

[Java 소켓 프로그래밍 기초/실습] 간단한 채팅 프로그램 구현해보기

이 포스팅은 개인적인 socket programming 공부를 위해 여러 글들과 코드들을 참조하였습니다. 포스트 맨 아래, 그리고 코드 윗부분에 주석으로 참조를 첨부하였으니 확인 부탁드립니다.

 

이번에는 Java Socket Programming을 이용하여 간단한 채팅 프로그램을 구현해보고자 합니다.

 

들어가기에 앞서서...

항상 상속에 대하여 헷갈리는 부분이 있었기에 정리하고 가겠습니다.

Java에서 상속(Inheritance)은 extends와 implements 두 가지가 존재합니다.

1)  extends

- 부모 메소드를 그대로 사용할 수 있으며 오버라이딩(메소드 재정의)을 할 필요가 없습니다.

- class가 class를 상속할 때, interface가 interface를 상속할 때 extends를 사용합니다.

- extends를 사용하면 다중 상속이 불가능합니다.

2) implements

- Java는 타 언어와 다르게 다중 상속을 지원하지 않습니다. implements는 다중 상속을 지원하지 않는 Java에서 매우 용이한 상속 방법입니다.
ex) public class kim implements Daddy, Mommy

- 부모 메소드를 반드시 오버라이딩(메소드 재정의) 해야합니다.

- class가 interface일 경우에는 반드시 implements를 사용해야 합니다.

- implements한 클래스는 해당 클래스의 모든 메소드를 전부 사용해야 합니다.

 

다음은 Thread에 대하여 정말 간단하게 알아보겠습니다.

Thread란?

- Thread는 어떠한 프로그램 내에서 실행되는 흐름의 단위입니다.

- 메모리를 할당받아 실행 중인 프로그램을 프로세스라 하며 Thread는 프로세스 내에서 명령어 블록으로 시작점과 종료점을 가집니다.

- 실행 중에 멈출 수 있으며, 동시 수행(multi-thread)이 가능합니다.

 

위에서 설명한 Thread라는 것을 사용하는 이유는 채팅 서버에서 여러 명의 유저들이 실시간으로 채팅을 입력하기 때문입니다. 즉, 여러 프로세스가 동시간대에 같이 실행되고 있기 떄문에 이를 Thread로 실행합니다.

 

Java Socket Programming을 이용한 채팅 프로그램 구현

- Client, Server, Send, User, Receiver, 총 5개의 클래스가 존재합니다.

- Server가 열리고 Client가 접속하면 Receiver를 통해 User 등록과 메세지 출력이 이루어지며, Send를 통하여 Client는 메세지를 전달합니다.

1. Server.java

package chatting;

import java.net.*;

public class Server {
	/*
	 * Java 간단한 채팅 참조 : https://lktprogrammer.tistory.com/64?category=672211
	 */
	public static void main(String[] args) {
		Socket socket = null; // Client와 통신하기 위한 Socket 생성
		User user = new User(); // 채팅방에 접속해 있는 Client 관리 객체
		ServerSocket server_socket = null; // Client 접속을 받기 위한 ServerSocket
		
		int count = 0; // 쓰레드 할당을 위한 정수
		Thread thread[] = new Thread[10]; // 10개까지 쓰레드 할당, 즉 채팅방에 10명이 접속 가능
		
		try {
			server_socket = new ServerSocket(4444); // 서버 포트로 소켓을 연다
			
			while(true) {
				socket = server_socket.accept(); // 통신이 종료되기 전까지 연결 

				/*
				 * receiver Class에서 implements Runnable을 사용했기 때문에
				 * Thread 객체 안에 쓰레드를 사용하려는 객체를 넣어줘서 객체화를 할 수 있습니다.
				 */
				thread[count] = new Thread(new Receiver(user, socket)); //Receiver 클래스를 Thread에서 돌림
				thread[count].start(); // 쓰레드 시작
				count++;
			}
		}catch(Exception e) {
			
		};
	}
}

Server는 말 그대로 Client들이 User로 등록이 되었다면 자유롭게 채팅을 주고 받는 공간이 됩니다.

서버가 열리지 않았다면 Client들은 채팅을 할 수 없으며, 위 코드에서는 최대 10명의 Client를 유저로 간주합니다.

 

2. Client.java

package chatting;

import java.io.*;
import java.net.*;

public class Client {
	/*
	 * Java 간단한 채팅 참조 : https://lktprogrammer.tistory.com/64?category=672211
	 * BufferedReader 참조 : https://jhnyang.tistory.com/92
	 */
	public static void main(String[] args) {
		Socket socket = null;
		DataInputStream in = null; // 이 변수는 사용자가 입력하는 채팅 부분에 해당됩니다.
		BufferedReader in2 = null; // 이 변수는 사용자의 닉네임에 해당됩니다
		
		DataOutputStream out = null; // 이 변수는 사용자가 입력한 데이터를 출력할 때 사용합니다.
		
		try {
			InetAddress ia = null; // Local Host IP Address 가져오기 위한 변수
			ia = InetAddress.getLocalHost(); // 현재 PC의 IP Address 가져오기
			socket = new Socket(ia, 4444); // client의 IP와 port 번호 입력
			
			/*
			 * DataInputStream은 입력 스트림을 받는 매개변수이며,
			 * socket.getInputStream()함수를 통해 소켓에서 전달되는 데이터 스트림을 읽어옵니다.
			 * BufferedReader는 Scanner와 비슷한 개념입니다.
			 * Scanner보다 빠르다는 장점이 있지만 String형으로 버퍼에 저장하기 때문에
			 * 따로 데이터를 가공해서 사용해야하는 경우가 많습니다.
			 */
			in = new DataInputStream(socket.getInputStream());
			in2 = new BufferedReader(new InputStreamReader(System.in));
			out = new DataOutputStream(socket.getOutputStream());
			
			System.out.print("닉네임을 입력해주세요 : ");
			String data = in2.readLine(); // 채팅에 사용할 닉네임을 받아옵니다.
			
			out.writeUTF(data); // 닉네임을 UTF-8로 변경 후 출력스트림에 넣습니다.
			Thread th = new Thread(new Send(out)); // 새로운 쓰레드에 out을 집어넣도록 합니다.
			th.start(); // 쓰레드 시작
		}catch(IOException e) {
			
		}
		
		try {
			/*
			 * 끊어지기 전까지는 계속 채팅을 입력받기 때문에
			 * 무한루프(while)안에서 동작합니다.
			 * 만일 끊고 싶다면 break에 대한 조건문을 걸면 됩니다.
			 */
			while(true) {
				String str2 = in.readUTF();
				System.out.println(str2);
			}
		}catch(IOException e) {
			
		}
	}
}

Client는 말그대로 사용자 입니다.

처음 닉네임을 입력하였다면, Server와 Socket을 통해 연결되는 순간 User의 권한을 얻고 채팅을 할 수 있게 됩니다.

 

3. Send.java

package chatting;

import java.io.*;

/*
 * extends Thread, implements Runnable은 쓰레드 구현을 위해 사용합니다.
 * extends Thread는 Thread를 상속받고 객채화한 뒤에 객체명.start()를 통해 사용하며,
 * implements Runnable은 Thread 객체 안에 쓰레드를 사용하려는 객체를 넣어줘서 객체화한 뒤 사용합니다.
 * extends Thread와 implements Runnable은 비슷합니다.
 * 하지만 implements Runnable을 사용하면 다중 상속이 가능합니다.
 */
public class Send implements Runnable{
	 /*
	  * Java 간단한 채팅 참조 : https://lktprogrammer.tistory.com/64?category=672211
	  * DataOutputStream 정보 참조 : https://apphappy.tistory.com/69
	  * Send Class는 Client에서 쓰레드를 실행할 때 사용됩니다.
	  */
	DataOutputStream out;
	
	/*
	 * Send 클래스에서의 in2는 채팅 내용을 받아오는 역할을 합니다.
	 */
	BufferedReader in2 = new BufferedReader(new InputStreamReader(System.in));
	
	
	public Send(DataOutputStream out) {
		this.out = out;
	}
	
	public void run() {
		while(true) {
			try {
				String msg = in2.readLine(); // 메세지를 받아오면
				out.writeUTF(msg); // msg 출력
			}catch(Exception e) {
				
			}
		}
	}
}

Send 클래스는 유저가 보낸 닉네임, 메세지 등을 서버에 전달하는 역할을 합니다.

Thread를 통해 동작하기 때문에 Runnable이라는 조건자를 달았습니다.

 

4. Receiver.java

package chatting;

import java.io.DataInputStream;
import java.net.Socket;

/*
 * extends Thread, implements Runnable은 쓰레드 구현을 위해 사용합니다.
 * extends Thread는 Thread를 상속받고 객채화한 뒤에 객체명.start()를 통해 사용하며,
 * implements Runnable은 Thread 객체 안에 쓰레드를 사용하려는 객체를 넣어줘서 객체화한 뒤 사용합니다.
 * extends Thread와 implements Runnable은 비슷합니다.
 * 하지만 implements Runnable을 사용하면 다중 상속이 가능합니다.
 */
public class Receiver implements Runnable {
	/*
	 * Java 간단한 채팅 참조 : https://lktprogrammer.tistory.com/64?category=672211
	 * DataInputStream 정보 참조 : https://apphappy.tistory.com/69
	 * implements runnable 정보 참조 : 
	 */
	Socket socket;
	DataInputStream in;
	String name;
	User user = new User();
	
	/*
	 * 전역변수들을 전부 초기화합니다.
	 * 또한 user 클래스에 AddClient를 호출하여 사용자를 등록합니다.
	 */
	public Receiver(User user, Socket socket) throws Exception // throws는 예약어를 사용한 exception 던지기
	{
		this.user = user;
		this.socket = socket;
		in = new DataInputStream(socket.getInputStream());
		this.name = in.readUTF(); // UTF-8 로 인코딩 후 읽어옴
		user.AddClient(name, socket); // 사용자 등록
	}
	
	public void run() {
		try {
			while(true) {
				String msg = in.readUTF(); // in에 들어온 메세지를 UTF-8 로 인코딩 후 읽어옴
				user.sendMsg(msg, name); // name 이름을 가진 user가 msg라는 메세지를 보냄
			}
		}catch(Exception e) {
			user.RemoveClient(this.name); // 에러 발생시 name 유저를 client에서 삭제
		}
	}
}

Server에서 Client와 소켓을 통해 통신이 연결되면 Receiver 클래스에서 User를 등록하고 메세지를 주고 받을 수 있도록 기초를 마련합니다.

Receiver는 Send가 보낸 메세지를 받아 출력해줍니다.

 

5. User.java

package chatting;

import java.io.*;
import java.net.*;
import java.util.*;

public class User {
	/*
	 * Java 간단한 채팅 참조 : https://lktprogrammer.tistory.com/64?category=672211
	 * Hasmap 참조 : https://codechacha.com/ko/java-map-hashmap/
	 * Iterator 참조 : https://vaert.tistory.com/108
	 */
	HashMap<String, DataOutputStream> clientmap = new HashMap<String, DataOutputStream>();
	// clientmap은 String형의 key와 DataOutputStream 형의 Value(값)을 받음

	public synchronized void AddClient(String name, Socket socket) {
		try {
			sendMsg(name + " 님이 입장하셨습니다.", "Server"); // server에 입장 메세지 전달
			clientmap.put(name, new DataOutputStream(socket.getOutputStream()));
			// HashMap의 put(key, value)함수는 key와 value를 받는다 
			
			System.out.println("채팅 참여 인원 : " + clientmap.size());
		} catch (Exception e) {

		}
	}
	
	public synchronized void RemoveClient(String name) {
		try {
			clientmap.remove(name); // name value를 가지는 client 제거
			sendMsg(name + " 님이 퇴장하였습니다.", "Server");
			System.out.println("채팅 참여 인원 : " + clientmap.size());
		}catch(Exception e) {
			
		}
	}

	public synchronized void sendMsg(String msg, String name) throws Exception {
		Iterator iterator = clientmap.keySet().iterator(); // clientmap의 key 값들을 읽어옵니다.
		while (iterator.hasNext()) { // key 값의 next가 존재한다면 계속 루프
			String clientname = (String) iterator.next();  // 채팅을 입력한 유저 이름을 받아와서
			clientmap.get(clientname).writeUTF(name + " : " + msg); // 양식대로 채팅을 출력합니다.
		}
	}
}

Client에게 User 권한을 얻을 수 있도록 해주는 클래스입니다.

Server에서 Receiver가 선언되면, Receiver를 통해 User가 등록이 되고, User 클래스를 통해 양식에 맞춰 실제 저희 눈에 보이게 되는 메세지 전달이 가능하게 됩니다.

 

실행 결과 화면

<1. 서버를 오픈합니다>
<2. Client 클래스를 실행하고 닉네임을 입력합니다>
<3. Server 실행창으로 넘어 오면 누군가 1명이 접속한 것을 확인할 수 있습니다>
<4. '프리로하'라는 이름의 클라이언트가 채팅을 입력하면 Client 실행 창에 정해진 양식대로 채팅이 출력됩니다.>
<5. '로하스' 라는 이름의 새로운 Client를 실행합니다.>
<6. '로하스' Client로 채팅을 입력합니다.>
<7. '프리로하' Client 창에 '로하스'가 뜨는 것을 확인할 수 있습니다.>

 

채팅 프로그램 동작 설명

블로그들을 돌아다니다 보면 좋은 코드들은 정말 많은데 개인적으로는 코드에 대한 설명이 없어서 이해가 어렵고 아쉬움을 느꼈습니다.

이를 위해 여러 글들을 참고하여 설명을 기재합니다.

<채팅 프로그램 Flow-Chart>

1) Server가 열리게 되면 Client와 Socket 통신이 될떄까지 무한 루프합니다.

2) Client를 실행하고 닉네임을 입력하면 out에 저장합니다.

3) out에 저장된 닉네임을 send를 통해 전송합니다. 그렇게 하면 Server에서는 Receiver를 통하여 Send로 받은 닉네임을 이용하여 User 등록을 진행합니다.

4) 그 후 해당 Client가 입력하는 값들을 메세지로 간주하여 send를 통해 계속 보내게 됩니다. User 안에 있는 HashMap을 통해 해당 유저가 보낸 메세지는 해당 유저 이름으로 화면에 출력됩니다. 즉, Client와 Server간에 연결이 되었지만, 실제로 메세지를 주고 받는 것은 Receiver와 Send 클래스이며, User 클래스를 통하여 이를 식별합니다.

 

* Receiver와 Send 사이에 통신에 있어서

1) Client가 메세지를 보내면 Send 클래스에서 in2.readLine()을 통해 받아오고 이를 out.writeUTF(msg)로 보내게 됩니다.

2) Receiver에서는 run() 메소드(p.s. Thread에서 run() 메소드는 항상 동작합니다) 에서 해당 메세지를 String msg 라는 문자형에서 in.readUTF()로 받습니다. 받은 메세지는 User 클래스에 정의된 sendMsg(msg, 유저이름) 함수를 통해 정해진 양식대로 출력이 됩니다.

3) User 클래스에 정의된 sendMsg는 Iterator를 이용하여 HashMap에 존재하는 모든 Client들을 받아 옵니다. 그 후, 자신이 받은 메세지를 정해진 출력 양식에 맞춰 모든 Client들에게 writeUTF() 메소드를 이용하여 전달합니다.

4) Client 클래스에 정의된 두번째 try-catch 문에는 while(true)를 이용하여 무한루프가 돌고 있습니다. User 클래스에서 sendMsg 함수를 통해 보낸 메세지 양식을 String str2 가 in.readUTF()로 받아 System.out.println(str2)를 하여 실행창에 출력하면 채팅이 되는 것을 확인할 수 있습니다.

 

참조

채팅 프로그램 코드 참조 : lktprogrammer.tistory.com/64?category=672211

 

[JAVA] TCP 소켓 프로그래밍 02 - 간단한 채팅 프로그램 구현

TCP 소켓 프로그래밍 02 - 채팅 앞선 소켓 프로그래밍 포스팅에서는 Server와 Client 사이에 1:1 통신 구현에 대해서 공부 하였습니다. 하지만 이번 포스팅에서 다룰 내용은 서버가 여러명의 Client의 접

lktprogrammer.tistory.com

BufferedReader 참조 : jhnyang.tistory.com/92

 

[Java 자바 입출력] BufferedReader/BufferedWriter

[자바 입출력 함수] BufferedReader / BufferWriter BufferedReader/BufferedWriter은 이름처럼 버퍼를 이용해서 읽고 쓰는 함수입니다. 이 함수는 버퍼를 이용하기 때문에 이 함수를 이용하면 입출력의 효율이..

jhnyang.tistory.com

DataOutputStream 참조 : apphappy.tistory.com/69

 

[자바/안드로이드] DataInputStream DataOutputStream알아보기

DataInputStream / DataOutputStream DataOutputStream DataOutputStream은 자바의 기본 데이터 타입별로 출력하는 별도의 메소드들이 있으며, 기본 데이터를 매개 변수로 호출한다. 예를 들어, double 데이터를..

apphappy.tistory.com

HashMap 참조 : codechacha.com/ko/java-map-hashmap/

 

Java - HashMap 사용 방법 및 예제

HashMap은 Map의 일종으로 key와 value의 쌍으로 이루어진 데이터를 보관합니다. HashMap은 데이터의 저장순서를 보장하지 않으며 null을 허용합니다. 또한 put, putAll, get, remove, keySet, values 등의 API들을 제

codechacha.com

Iterator 참조 : vaert.tistory.com/108

 

[Java] Iterator 함수 제대로 알고 사용하기

Iterator 란? Iterator는 자바의 컬렉션 프레임웍에서 컬렉션에 저장되어 있는 요소들을 읽어오는 방법을 표준화 하였는데 그 중 하나가 Iterator이다. Iterator는 인터페이스인데 그 구성을 살펴보자. pub

vaert.tistory.com

DataInputStream 참조 : apphappy.tistory.com/69

 

[자바/안드로이드] DataInputStream DataOutputStream알아보기

DataInputStream / DataOutputStream DataOutputStream DataOutputStream은 자바의 기본 데이터 타입별로 출력하는 별도의 메소드들이 있으며, 기본 데이터를 매개 변수로 호출한다. 예를 들어, double 데이터를..

apphappy.tistory.com

synchronized 참조 : tourspace.tistory.com/54

 

Java의 동기화 Synchronized 개념 정리#1

Java의 동기화 -Synchronized 키워드의 사용 Java를 프로그래밍 하다면 multi-thread로 인하여 동기화를 제어해야하는 경우가 생깁니다. 그래서 흔히 Synchronized 키워드를 사용하는데요 그냥 multi-thread로 동

tourspace.tistory.com

Thread 참조 : raccoonjy.tistory.com/15

 

자바(Java)의 기초 박살내기 - 스레드(Thread)

이번 시간에는 스레드에 대해서 공부하겠습니다. 스레드란 어떠한 프로그램내에서 특히 프로세스 내에서 실행되는 흐름의 단위입니다.  1. Thread - 메모리를 할당받아 실행 중인 프로그램을 프

raccoonjy.tistory.com

implements, runnable 참조 : tourspace.tistory.com/54

 

Java의 동기화 Synchronized 개념 정리#1

Java의 동기화 -Synchronized 키워드의 사용 Java를 프로그래밍 하다면 multi-thread로 인하여 동기화를 제어해야하는 경우가 생깁니다. 그래서 흔히 Synchronized 키워드를 사용하는데요 그냥 multi-thread로 동

tourspace.tistory.com

참조 많이도 했네요... ㅋㅋ