본문 바로가기
롯데E커머스 채용연계형 교육

Socket을 활용한 채팅 프로그램 [로비, 방만들기, 채팅방 입장, 방별 채팅, 채팅방 나가기]

by 29살아저씨 2022. 6. 15.
반응형

주요 기능

- 로비에서 방 만들기

- 채팅방 입장

- 채팅방 별 채팅

- 채팅방 나가기

 

기술 스택

Java

새로 알게된 기술

Thread / Socket

 

Thread란?

프로세스(process)란 단순히 실행 중인 프로그램(program)이라고 할 수 있다.

즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다.

이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성된다.

 

스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미한다.

모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행한다.

또한, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 한다.

 

Socket이란?

소켓이란 네트워크상에서 동작하는 프로그램 간 통신의 종착점. 즉, 프로그램이 네트워크에서 데이터를 통신할 수 있도록 해주는 연결부이다,

구현 기술

공통

- 클라이언트와 서버 데이터 통신을 하기 위해 String이 아닌 객체 형식으로 데이터 전송

BufferedReader / BufferedWriter 방식은 채팅방 내에서 메시지 전송할 때에만 쓰고

그 외(방 생성, 방 나가기, 방 리스트 불러오기...)의 코드들은 객체형식으로 Type값을 할당해줘서 메인서버에서 역할을 구분하여 처리를 해줌

 

서버

1. 9000 PORT를 활용하여 MainThread 동작 및 클라이언트 프로그램 접속 확인

2. 채팅방 생성 시 마다 9001번 부터 PORT++ 를 하면서 각 채팅방에 따른 포트 할당

3. 채팅 서버소켓에 접속하는 클라이언트를 확인하기 위한 ServerThread 동작

4. 채팅 서버소켓에 접속하여 채팅을 주고 받기 위한 ChatTread 동작

 

총 3개의 Thread를 활용하여 서버에서 프로그램이 의존되지 않고 독립적으로 각자의 역할에 맞게 돌아가게 하였다.

 

서버의 폴더구조

Main / MainClass.java

package main;

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

import threadex.MainThread;

import java.net.*;

public class MainClass {
	public static int PORT = 9001; // 채팅용 포트 서버
	public static List<ServerSocket> serverList = new ArrayList<ServerSocket>(); // 채팅 서버 리스트
	public static List<String> roomList = new ArrayList<String>(); // 방 이름
	public static List<Integer> port = new ArrayList<Integer>(); // 포트 번호
	public static List<List<Socket>> connectSocket = new ArrayList<List<Socket>>(); // 채팅 서버에 들어온 클라이언트 소켓 리스트

	public static void main(String[] args) {
		Socket clientSocket = null;
		boolean isFirst = true;
		try {
			ServerSocket serverSocket = new ServerSocket(9000);

			List<Socket> list = new ArrayList<Socket>();

			while (true) {
				// 서버는 항상 열려있어야함
				System.out.println("접속 대기중...");
				clientSocket = serverSocket.accept();
				list.add(clientSocket);

				System.out.println("Client IP: " + clientSocket.getInetAddress() + "Port:" + clientSocket.getPort());

				// 아래 부분은 접속 된 소켓을 통해 소통을 해야함 - 스레드로 이동해야함
				new MainThread(clientSocket, list, isFirst).start();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

 

threadex

/ MainThread.java

package threadex;

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

import main.MainClass;
import vo.ChatVO;
import vo.CreateVO;
import vo.RoomListVO;
import vo.TypeVO;

import java.io.*;

public class MainThread extends Thread {
	Socket socket;
	List<Socket> list;
	boolean isFirst;

	public MainThread(Socket socket, List<Socket> list, boolean isFirst) {
		super();
		this.socket = socket;
		this.list = list;
		this.isFirst = isFirst;
	}

	@Override
	public void run() {

		super.run();

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;

		try {
			while (true) {
				// 수신 (recv)
				ois = new ObjectInputStream(socket.getInputStream());
				if (isFirst) { // 만약 처음 들어왔다면 채팅방 리스트 보내주기 - 일단 이게 안됨.
					oos = new ObjectOutputStream(socket.getOutputStream());
					RoomListVO roomListVO = new RoomListVO();
					roomListVO.setType("RoomList"); // Type : RoomList
					roomListVO.roomList = MainClass.roomList; // 방 정보
					roomListVO.port = MainClass.port; // 포트 정보
					oos.writeObject(roomListVO);
					oos.flush();
					isFirst = false;
				}
				TypeVO typeVO = ((TypeVO) ois.readObject());
				System.out.println("타입 들어옴" + typeVO);
				String Type = typeVO.getType();
				if (Type.equals("Create")) {
					System.out.println("여기여기");
					Socket clientSocket = null;
					CreateVO createVO = (CreateVO) typeVO;
					// 이름에 맞게 포트 생성해주기
					MainClass.roomList.add(createVO.getrName()); // 방 이름, 포트번호 저장
					MainClass.port.add(MainClass.PORT); // 방 이름, 포트번호 저장
					ServerSocket nowServerSocket = new ServerSocket(MainClass.PORT++); // 채팅 서버소켓 생성
					MainClass.serverList.add(nowServerSocket); // 포트 연결
					List<Socket> clientList = new ArrayList<Socket>(); // 채팅 클라이언트 소켓 만들어주기
					MainClass.connectSocket.add(clientList); // 클라이언트 소켓 리스트에 넣어주기
					System.out.println(createVO.getrName() + "과 PORT " + (MainClass.PORT - 1) + "생성완료!");
					RoomListVO roomListVO = new RoomListVO();
					roomListVO.setType("RoomList"); // Type : RoomList
					roomListVO.roomList = MainClass.roomList; // 방 정보
					roomListVO.port = MainClass.port; // 포트 정보

					// 모든 사람에게 생성된 방 보내주기
					for (Socket s : list) {
						oos = new ObjectOutputStream(s.getOutputStream());
						oos.writeObject(roomListVO);
						oos.flush();
					}
					int nowPort = MainClass.PORT - 1;
					new ServerThread(nowPort, clientSocket, nowServerSocket, clientList).start();
				}
				Thread.sleep(300);
			}
		} catch (Exception e) {
			System.out.println("연결이 끊긴 IP: " + socket.getInetAddress());
			list.remove(socket); // index뿐만 아니라 Object를 넣어도 제거가 됨

			// 접속되어 있는 남아있는 클라이언트
			for (Socket s : list) {
				System.out.println("접속되어 있는 IP: " + s.getInetAddress() + " Port: " + s.getPort());
			}

			try {
				socket.close(); // 종료 후 소켓 닫아주기
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}
}

/ ServerThread.java

package threadex;

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

import main.MainClass;
import vo.ChatVO;
import vo.CreateVO;
import vo.RoomListVO;
import vo.TypeVO;

import java.io.*;

public class ServerThread extends Thread {
	Socket socket;
	ServerSocket nowServerSocket;
	List<Socket> list;
	int nowPort;

	public ServerThread(int nowPort, Socket socket, ServerSocket nowServerSocket, List<Socket> list) {
		super();
		this.nowPort = nowPort;
		this.socket = socket;
		this.nowServerSocket = nowServerSocket;
		this.list = list;
	}

	@Override
	public void run() {

		super.run();

		try {
			while (true) { // 채팅방 스레드 생성
				// 서버는 항상 열려있어야함
				System.out.println(nowPort + "포트 접속 대기중...");
				socket = nowServerSocket.accept();
				list.add(socket);

				System.out.println(
						"[" + nowPort + "] Client IP: " + socket.getInetAddress() + "Port:" + socket.getPort());
				
				// 아래 부분은 접속 된 소켓을 통해 소통을 해야함 - 스레드로 이동해야함
				new ChatThread(socket, list).start();
			}
		} catch (Exception e) {
			System.out.println("연결이 끊긴 IP: " + socket.getInetAddress());
			list.remove(socket); // index뿐만 아니라 Object를 넣어도 제거가 됨

			// 접속되어 있는 남아있는 클라이언트
			for (Socket s : list) {
				System.out.println("접속되어 있는 IP: " + s.getInetAddress() + " Port: " + s.getPort());
			}

			try {
				socket.close(); // 종료 후 소켓 닫아주기
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}
}

/ ChatThread.java

package threadex;

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

public class ChatThread extends Thread {
	Socket socket;
	List<Socket> list;

	public ChatThread(Socket socket, List<Socket> list) {
		super();
		this.socket = socket;
		this.list = list;
	}

	@Override
	public void run() {
		super.run();

		try {
			Loop1: while (true) {
				// 수신 (recv)
				BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				String str = reader.readLine();
				if (str == null) { // 만약 소켓이 나갔다면 리스트에서 소켓 제거해주고 종료
					for (int i = 0; i < list.size(); i++) {
						if (socket.equals(list.get(i))) {
							list.remove(i);
							break Loop1;
						}
					}
				}
				System.out.println("[받은 메시지]: " + str);
				// 송신 (send)
				for (int i = 0; i < list.size(); i++) {
					System.out.println(list.get(i) + " " + socket);
					PrintWriter writer = new PrintWriter(list.get(i).getOutputStream());
					writer.println(str);
					writer.flush();
				}
				Thread.sleep(300);
			}
		} catch (Exception e) {
			System.out.println("연결이 끊긴 IP: " + socket.getInetAddress());
			list.remove(socket); // index뿐만 아니라 Object를 넣어도 제거가 됨

			// 접속되어 있는 남아있는 클라이언트
			for (Socket s : list) {
				System.out.println("접속되어 있는 IP: " + s.getInetAddress() + " Port: " + s.getPort());
			}

			try {
				socket.close(); // 종료 후 소켓 닫아주기
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}
}

vo

/ ChatVO.java

package vo;

import java.io.Serializable;

public class ChatVO extends TypeVO implements Serializable {

	private static final long serialVersionUID = -507330558705119015L;
	private String name;
	private String msg;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}
}

/ CreateVO.java

package vo;

import java.io.Serializable;

public class CreateVO extends TypeVO implements Serializable {

	private static final long serialVersionUID = -5957375119826327685L;
	private String rName;

	public String getrName() {
		return rName;
	}

	public void setrName(String rName) {
		this.rName = rName;
	}
}

/ RoomListVO.java

package vo;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class RoomListVO extends TypeVO implements Serializable {

	private static final long serialVersionUID = 6835675812016930981L;
	public List<Integer> port = new ArrayList<Integer>();
	public List<String> roomList = new ArrayList<String>();
}

/ TypeVO.java

package vo;

import java.io.Serializable;

public class TypeVO implements Serializable {

	private static final long serialVersionUID = 1370373687986753626L;
	private String type;

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}
}

 

클라이언트

1. MainClass에서 ReadThreadClass 스레드 생성하여 방이 생성될 때 마다 감지하여 리스트를 최신화 시켜주었다.

2. 채팅방에 접속할 때 ReadThread 스레드를 생성하여 해당 스레드에 Socket과 ChatFrame을 넘겨주어 채팅방 화면에서 포트에 맞게 Socket을 활용하여 데이터를 Read할 수 있도록 하였다.

3. 채팅방에 접속하여 데이터를 입력하면 WriteClass 스레드에 Socket을 보내주어 채팅방 포트에 맞는 Socket에 채팅을 보내주도록 하였다.

 

클라이언트의 폴더구조

클라이언트 코드

- vo는 공통이므로 생략

main / MainClass.java

package main;

import java.net.Socket;

import net.ReadClassThread;
import net.ReadThread;
import view.ChatListFrame;

public class MainClass {
	public static ReadThread rt;
	public static String IP = "192.168.0.103";

	public static void main(String[] args) {
		try {
			Socket socket = new Socket(IP, 9000); // 서버 접속
			System.out.println("connection Success!!");

			ChatListFrame cf = new ChatListFrame(socket);

			new ReadClassThread(socket).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

dto / Room.java

package dto;

import java.net.Socket;

public class Room {
	private Socket socket;
	private String rName;
	private int rNum;

	public Socket getSocket() {
		return socket;
	}

	public void setSocket(Socket socket) {
		this.socket = socket;
	}

	public String getrName() {
		return rName;
	}

	public void setrName(String rName) {
		this.rName = rName;
	}

	public int getrNum() {
		return rNum;
	}

	public void setrNum(int rNum) {
		this.rNum = rNum;
	}
}

net

/ ReadClassThread.java

package net;

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

import view.ChatListFrame;
import vo.RoomListVO;
import vo.TypeVO;

public class ReadClassThread extends Thread {
	Socket socket;
//	ClientFrame cf;

	public ReadClassThread(Socket socket/* , ClientFrame cf */) {
		this.socket = socket;
//		this.cf = cf;
	}

	@Override
	public void run() {
		super.run();
		ObjectInputStream ois = null;
		try {
			while (true) {
				ois = new ObjectInputStream(socket.getInputStream());
				TypeVO typeVO = ((TypeVO) ois.readObject());
				String Type = typeVO.getType();
				if (Type.equals("RoomList")) {
					RoomListVO roomListVO = (RoomListVO) typeVO; // 방 정보 받아오기
					ChatListFrame.cliRoomList = roomListVO; // 복사
					ChatListFrame.list.clear();
					for (int i = 0; i < roomListVO.roomList.size(); i++) { // 실시간으로 방 받아와지게
						ChatListFrame.list.add(roomListVO.roomList.get(i));
					}
				} 
				Thread.sleep(300);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

/ ReadThread.java

package net;

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

import view.ChatFrame;

public class ReadThread extends Thread {
	Socket socket;
	ChatFrame cf;

	public ReadThread(Socket socket, ChatFrame cf) {
		this.socket = socket;
		this.cf = cf;
	}

	@Override
	public void run() {
		super.run();
		try {
			while (true) {
				BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				String str = br.readLine();
				System.out.println("str is" + str);
				if (str == null) { // 접속 끊김
					System.out.println("접속 끊김");
				}
				cf.TextList.append(str + "\n");

				Thread.sleep(300);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

/ WriteClass.java

package net;

import java.net.InetAddress;
import java.net.Socket;
import java.io.*;

import view.ChatFrame;
import view.LoginFrame;

public class WriteClass extends Thread {
	Socket socket;
	ChatFrame cf;

	public WriteClass(Socket socket, ChatFrame cf) {
		this.socket = socket;
		System.out.println("socketwrite" + socket);
		this.cf = cf;
	}

	public void sendMessage() {
		try {
			PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
			String msg = "";
			String id = LoginFrame.id.getText();
			// 그 외 전송
			msg = "[" + id + "]" + cf.input.getText();

			// server로 전송
			pw.println(msg);
			pw.flush();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

view

/ ChatFrame.java

package net;

import java.net.InetAddress;
import java.net.Socket;
import java.io.*;

import view.ChatFrame;
import view.LoginFrame;

public class WriteClass extends Thread {
	Socket socket;
	ChatFrame cf;

	public WriteClass(Socket socket, ChatFrame cf) {
		this.socket = socket;
		System.out.println("socketwrite" + socket);
		this.cf = cf;
	}

	public void sendMessage() {
		try {
			PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
			String msg = "";
			String id = LoginFrame.id.getText();
			// 그 외 전송
			msg = "[" + id + "]" + cf.input.getText();

			// server로 전송
			pw.println(msg);
			pw.flush();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

/ ChatListFrame.java

package view;

import java.util.*;
import java.util.List;

import main.MainClass;
import net.ReadThread;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

import vo.RoomListVO;

public class ChatListFrame extends Frame implements WindowListener, ActionListener {
	CreateRoomFrame crf;
	Socket socket;
	Label mainLabel = new Label("STALK");
	Button createRoombtn = new Button("create");
	Button enterbtn = new Button("enter");
	public static java.awt.List list = new java.awt.List();

	public List<RoomListVO> chatList = new ArrayList<>();
	public static RoomListVO cliRoomList = new RoomListVO();

	public ChatListFrame(Socket socket) {
		setLayout(null);
		this.socket = socket;
		crf = new CreateRoomFrame(this, socket);

		new LoginFrame(this);

		mainLabel.setBounds(50, 50, 150, 30);
		add(mainLabel);
		createRoombtn.setBounds(350, 50, 50, 30);
		createRoombtn.addActionListener(this);
		add(createRoombtn);
		enterbtn.setBounds(200, 520, 50, 50);
		enterbtn.addActionListener(this);
		add(enterbtn);
		list.setBounds(50, 100, 350, 400);
		add(list); // 채팅방 리스트

		setBounds(0, 0, 450, 600);

		setVisible(false);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		Object obj = e.getSource();
		if (obj == createRoombtn) {
			// CraetaRoomFrame 을 시각화
			crf.setVisible(true);
			// 현재창 닫기
			this.dispose();
		} else if (obj == enterbtn) {
			int index = -1;
			ChatFrame cf = null;
			for (int i = 0; i < cliRoomList.roomList.size(); i++) {
				if (list.getSelectedItem().equals(cliRoomList.roomList.get(i))) {
					index = i;
					break;
				}
			}
			int selectPort = cliRoomList.port.get(index);
			String roomName = cliRoomList.roomList.get(index);
			try {

				// 채팅 포트 소켓 생성
				System.out.println("select port is " + selectPort);
				Socket chatSocket = new Socket(MainClass.IP, selectPort);
				cf = new ChatFrame(this, chatSocket, roomName);
				MainClass.rt = new ReadThread(chatSocket, cf);
				MainClass.rt.start();

			} catch (Exception e1) {
				e1.printStackTrace();
			}

			// ChatFrame 을 시각화
			cf.setVisible(true);
			// 현재창 닫기
			this.dispose();
		}

	}

	@Override
	public void windowOpened(WindowEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void windowClosing(WindowEvent e) {
		System.exit(0);
	}

	@Override
	public void windowClosed(WindowEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void windowIconified(WindowEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void windowActivated(WindowEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void windowDeactivated(WindowEvent e) {
		// TODO Auto-generated method stub

	}

}

/ CreateRoomFrame.java

package view;

import java.awt.Button;
import java.awt.Frame;
import java.awt.Label;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.ArrayList;

import vo.CreateVO;

public class CreateRoomFrame extends Frame implements ActionListener {
	public static TextField rName = new TextField(8); // 방 이름 입력
	Label mainLabel = new Label("STALK");
	Button createBtn = new Button("생성");
	ChatListFrame clf;
	Socket socket;
	private ObjectOutputStream oos;

	public CreateRoomFrame(ChatListFrame clf, Socket socket) {
		this.socket = socket;
		this.clf = clf;
		mainLabel.setBounds(50, 50, 150, 30);
		add(mainLabel);
		setLayout(null);
		setTitle("방생성");

		Label label = new Label("방 이름 입력: ");
		label.setBounds(50, 100, 100, 30);
		add(label);

		rName.setBounds(150, 100, 250, 30);
		add(rName);

		createBtn.setBounds(200, 180, 100, 30);
		createBtn.addActionListener(this);
		add(createBtn);

		setBounds(0, 0, 500, 300);
		setVisible(false);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		Object obj = e.getSource();

		if (obj == createBtn) {
			// 데이터 송신
			try {
				oos = new ObjectOutputStream(socket.getOutputStream());
				CreateVO createList = new CreateVO();
				createList.setType("Create");	// Type : Create
				createList.setrName(CreateRoomFrame.rName.getText());	// 방 이름
				// server에 객체 전송
				oos.writeObject(createList);
				oos.flush();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			// CraetaRoomFrame 을 시각화
			clf.setVisible(true);
			// 현재창 닫기
			this.dispose();
		}
	}

}

/ LoginFrame.java

package view;

import java.awt.Button;
import java.awt.Frame;
import java.awt.Label;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class LoginFrame extends Frame implements ActionListener {

	public static TextField id = new TextField(8); // id 입력
	Button loginBtn = new Button("입장");
	ChatListFrame clf;

	public LoginFrame(ChatListFrame clf) {
		System.out.println(clf+"clf");
		this.clf = clf;

		setLayout(null);
		setTitle("채팅방 입장");

		Label label = new Label("이름 입력: ");
		label.setBounds(50, 60, 100, 30);
		add(label);

		id.setBounds(150, 60, 250, 30);
		add(id);

		loginBtn.setBounds(200, 110, 100, 30);
		loginBtn.addActionListener(this);
		add(loginBtn);

		setBounds(0, 0, 500, 200);
		setVisible(true);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// 로그인 버튼 클릭 시
		
		// ClientFrame 을 시각화
		clf.setVisible(true);
		// 현재창 닫기
		this.dispose();

	}

}

 

반응형

댓글