JAVA 소켓 기반 채팅 프로그램 기능 업데이트
명령어 인식코드 개선
강퇴 기능
참가자 리스트
그 외 로그인 로그아웃 시에 채팅창 입력 제어
반복 코드 메서드 분리
업데이트 부분만 주석
서버
package ex03;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.Date;
import java.text.SimpleDateFormat;
public class MultiChatServer {
private static final int LOGIN = 100;
private static final int LOGOUT = 200;
private static final int EXIT = 300;
private static final int NOMAL = 400;;
private static final int WISPER = 500;;
private static final int VAN = 600;
private static final int CPLIST= 700;
private static final int ERR_DUP = 800;
private ServerSocket serverSocket = null;
private Socket socket = null;
ArrayList <ChatThread> chatlist = new ArrayList <ChatThread>(); //스레드 리스트
HashMap<String, ChatThread> hash= new HashMap<String, ChatThread>();
//ID를 KEY로 해서 스레드를 VALUE로 갖고 있는 HASHMAP
Date now = new Date(System.currentTimeMillis());
SimpleDateFormat simple= new SimpleDateFormat("(a hh:mm)");
//채팅내용 옆에 시간을 같이 출력하기 위해서 현재시간에 포맷을 지정
public void start() {
try {
serverSocket = new ServerSocket(8888);
System.out.println("server start");
while(true) {
socket = serverSocket.accept();
ChatThread chat = new ChatThread(socket);
chatlist.add(chat);
chat.start();
}
} catch(IOException e) {
System.out.println("통신소켓 생성불가");
if(!serverSocket.isClosed()) {
stopServer();
}
}catch(Exception e) {
System.out.println("서버소켓 생성불가");
if(!serverSocket.isClosed()) {
stopServer();
}
}
}
public void stopServer() {
try {
Iterator<ChatThread> iterator = chatlist.iterator();
//chatlist에 있는 스레드 전체를 가져오기 위해 iterator 객체 생성
while (iterator.hasNext()) { //다음 객체가 있는 동안
ChatThread chat = iterator.next(); // 다음 객체를 스레드에 대입
chat.soket.close(); //해당 스레드 통신소켓제거
iterator.remove(); //스레드 제거
}
if(serverSocket!=null && !serverSocket.isClosed()) {
serverSocket.close(); //서버소켓 닫기
}
System.out.println("서버종료");
}catch (Exception e) {}
}
public static void main(String[] args) {
MultiChatServer server = new MultiChatServer();
server.start();
}
void broadCast(String msg) { //채팅방 인원 전체출력
for(ChatThread ct : chatlist) {
ct.outMsg.println(msg+ simple.format(now));
//매개변수로 받은 채팅내용을 시간과 함께 출력
}
}
void wisper(ChatThread from, ChatThread to,String msg) { //송신그레드,수신스레드,대화내용 매개변수)
from.outMsg.println(msg+ simple.format(now)); //송신스레드 채팅창에 출력
to.outMsg.println(msg+ simple.format(now)); // 수신스레드 채팅창에 출력
}
void updatinglist() {
Set<String> list = hash.keySet();// hashmap에서 아이디(key)만 set으로 가져옴
for(ChatThread ct : chatlist) {
ct.outMsg.println(CPLIST+"/"+list); //CPLIST명령어로 전체에게 출력
}
}
void disconnect(ChatThread thread, String id) {
try {
thread.soket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
hash.remove(id); //hashmap에서 제거
chatlist.remove(thread); //chatlist에서 제거
}
class ChatThread extends Thread {
public ChatThread(Socket socket) {
//통신소켓을 닫기 위해서 스레드 생성할 때 생성자 매개변수로 소켓을 받아서 멤버변수에 대입
this.soket = socket;
}
Socket soket;
String msg;
String[] rmsg;
private BufferedReader inMsg = null;
private PrintWriter outMsg = null;
public void run() {
boolean status = true;
System.out.println("##ChatThread start...");
try {
inMsg = new BufferedReader(new InputStreamReader(socket.getInputStream())); //예외발생 가능성
outMsg = new PrintWriter(socket.getOutputStream(), true);
while(status) { //수신부
msg = inMsg.readLine();
rmsg = msg.split("/");
int commend = Integer.parseInt(rmsg[0]);
//가장앞에 있는 명령어를 스위치문으로 처리하기 위해 int로 형변환
switch (commend) {
case LOGIN: {
System.out.println(commend);
if(hash.containsKey(rmsg[1])) { //id로 hashmap에서 중복검사
this.outMsg.println(ERR_DUP+"/"+"[SERVER]" +"/" +"로그인불가>ID 중복");
//로그인 한 상대방 채팅창에 로그인 불가 안내메시지 출력
socket.close(); //소켓 제거
chatlist.remove(this); //스레드리스트에서 제거
status = false; // 상태변경으로 while문 탈출
break;
}
else{
hash.put(rmsg[1], this); //중복이 아니면 해당 아이디를 key/ 스레드를 value로 추가
broadCast(NOMAL+"/"+"[SERVER]" +"/"+rmsg[1]+"님이 로그인했습니다.");
//채팅창에 로그인 메세지 출력
updatinglist(); //변경된 참가자 리스트를 송신
break;
}
}
case LOGOUT: {
disconnect(this, rmsg[1]); //해당 스레드와 연결을 해제하는 메서드
broadCast(NOMAL+"/"+"[SERVER]" +"/"+ rmsg[1] + "님이 종료했습니다.");
//나감을 알림
updatinglist(); // 변경된 채팅 참가자 리스트 송신
status = false; //while문 반복탈출
break;
}
case EXIT: {
disconnect(this, rmsg[1]);
broadCast(NOMAL+"/"+"[SERVER]" +"/" + rmsg[1] + "님과 연결이 끊어졌습니다.");
updatinglist();
status = false;
break;
}
case NOMAL: {
broadCast(msg);
break;
}
case WISPER: {
ChatThread from = hash.get(rmsg[1]); // rmsg[1] 송신 id를 key값으로 value스레드 찾기
ChatThread to = hash.get(rmsg[2]); // rmsg[2] 수신 id를 key값으로 value스레드 찾기
wisper(from,to, msg); //찾은 송신 스레드 , 수신 스레드, 내용을 매개변수로 wisper메소드 호출
break;
}
case VAN : {
if(chatlist.indexOf(this)!=0) {//0번 스레드에 방장권한을 줌 (방장이 아니라면)
this.outMsg.println(NOMAL+"/"+"[SERVER]" +"/"+ "강퇴권한이 없습니다."+ simple.format(now));
//해당 스레드에만 권한이 없음을 송신
break;
}
else {
broadCast(NOMAL+"/"+"[SERVER]" +"/" + rmsg[2] + "님이 강제퇴장하셨습니다.");
// 해당 스레드가 방장이라면 강퇴당한 사실을 전체에게 출력
ChatThread thread = hash.get(rmsg[2]);
//hashmap에서 강퇴할 id로 해당 스레드를 검색해서 thread에 대입
thread.outMsg.println(VAN+"/");
disconnect(thread, rmsg[2]);//강퇴당한 스레드 연결해제
updatinglist();
break;
}
}
}//switch
}//while
this.interrupt();
System.out.println("##"+this.getName()+"stop!!");
}catch(IOException e) {
try {
this.soket.close();
} catch (IOException e1) {}
chatlist.remove(this);
// e.printStackTrace();
System.out.println("[ChatThread]run() IOException 발생!!");
}
}
}
}
클라이언트
package ex03;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
public class MultiChatClient implements ActionListener, Runnable {
private static final int LOGIN = 100;
private static final int LOGOUT = 200;
private static final int EXIT = 300;
private static final int NOMAL = 400;;
private static final int WISPER = 500;;
private static final int VAN = 600;
private static final int CPLIST= 700;
private static final int ERR_DUP = 800;
private String ip;
private String id;
private String contents;
private Socket socket;
private BufferedReader inMsg = null;
private PrintWriter outMsg = null;
private JPanel loginPanel;
private JButton loginButton;
private JLabel label1;
private JTextField idInput;
private JPanel logoutPanel;
private JLabel label2;
private JButton logoutButton;
private JPanel msgPanel;
private JTextField msgInput;
private JButton exitButton;
private JFrame jframe;
private JTextArea msgOut;
private JPanel chatpListPanel;
private JLabel label3;
private JTextArea listOut;
private Container tab;
private CardLayout clayout;
private Thread thread;
boolean status;
public MultiChatClient(String ip) {
this.ip = ip;
loginPanel = new JPanel();
loginPanel.setLayout(new BorderLayout());
idInput = new JTextField(15);
loginButton = new JButton("로그인");
loginButton.addActionListener(this);
label1 = new JLabel("대화명");
loginPanel.add(label1, BorderLayout.WEST);
loginPanel.add(idInput, BorderLayout.CENTER);
loginPanel.add(loginButton, BorderLayout.EAST);
logoutPanel = new JPanel();
logoutPanel.setLayout(new BorderLayout());
label2 = new JLabel();
logoutButton = new JButton("로그아웃");
logoutButton.addActionListener(this);
logoutPanel.add(label2, BorderLayout.CENTER);
logoutPanel.add(logoutButton, BorderLayout.EAST);
msgPanel = new JPanel();
msgPanel.setLayout(new BorderLayout());
msgInput = new JTextField(30);
msgInput.addActionListener(this);
msgInput.setEditable(false); //로그인 하기 전에는 채팅입력 불가
exitButton = new JButton("종료");
exitButton.addActionListener(this);
msgPanel.add(msgInput, BorderLayout.CENTER);
msgPanel.add(exitButton, BorderLayout.EAST);
tab = new JPanel();
clayout = new CardLayout();
tab.setLayout(clayout);
tab.add(loginPanel, "login");
tab.add(logoutPanel, "logout");
jframe = new JFrame("::멀티챗::");
msgOut = new JTextArea("", 10, 30);
msgOut.setEditable(false);
JScrollPane jsp = new JScrollPane(msgOut,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
chatpListPanel = new JPanel(); //채팅 참자가 리스트가 붙을 패널
chatpListPanel.setLayout(new BorderLayout());
label3 = new JLabel("채팅 참가자"); // 라벨
listOut =new JTextArea("",10,10); //채팅참가자를 나타낼 영역
listOut.setEditable(false); //편집불가
JScrollPane jsp2 = new JScrollPane(listOut,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
chatpListPanel.add(label3,BorderLayout.NORTH); //패널에 라벨과 스크롤을 갖다 붙임
chatpListPanel.add(jsp2,BorderLayout.CENTER);
jframe.add(tab, BorderLayout.NORTH);
jframe.add(jsp, BorderLayout.WEST);
jframe.add(chatpListPanel,BorderLayout.EAST);
jframe.add(msgPanel, BorderLayout.SOUTH);
clayout.show(tab, "login");
jframe.pack();
jframe.setResizable(false);
jframe.setVisible(true);
}
public void connectServer() {
try {
socket = new Socket(ip, 8888); //예외발생 가능성
System.out.println("[Client]Server 연결 성공!!");
inMsg = new BufferedReader(new InputStreamReader(socket.getInputStream())); //예외발생 가능성
outMsg = new PrintWriter(socket.getOutputStream(), true);
outMsg.println(LOGIN+"/"+id); //LOGIN 명령어로 해당 ID 출력
thread = new Thread(this);
thread.start();
} catch(IOException e) { //해당포트에 서버가 실행하고 있지 않은 경우
// e.printStackTrace();
System.out.println("서버연결불가");
if(!socket.isClosed()) {
stopClient();
}
return;
}
}
public void stopClient() {
System.out.println("연결끊음");
msgOut.setText(""); //채팅창 비우기
listOut.setText(" "); //참가자 창 비우기
msgInput.setEditable(false); //채팅입력불가
clayout.show(tab, "login");
status = false;
if(socket!=null && !socket.isClosed()) {
try {
socket.close(); //예외 발생 가능성
} catch (IOException e) {}
}
}
public void actionPerformed(ActionEvent arg0) {
Object obj = arg0.getSource();
if(obj == exitButton) {
outMsg.println(EXIT+"/"+id );
stopClient();
System.exit(0);
}
else if(obj == loginButton) {
id = idInput.getText().trim();
label2.setText("대화명 : " + id);
clayout.show(tab, "logout");
msgInput.setEditable(true); //채팅입력 창 활성화(채팅입력 가능)
connectServer();
}
else if(obj == logoutButton) {
outMsg.println(LOGOUT+"/"+id );
stopClient();
}
else if(obj == msgInput) {
Thread thread = new Thread() {
//출력 쓰레드 새로생성(도배방지 기능 구현 관계상 출력 쓰레드만 sleep 시키기 위해)
//입력 스레드는 계속 일을 해야 채팅제한시간에도 내용이 채팅창에 추가되므로
@Override
public void run() {
contents = msgInput.getText();
//입력창의 내용 contents에 대입
if(contents.indexOf("to")==0) {
// 처음 시작이 to (예전 코드는 중간에 to가 들어갈경우 구분 불가)
int begin = contents.indexOf(" ") + 1;
// to 1111 안녕하세요 일 경우 처음 빈칸 다음자리부터
int end = contents.indexOf(" ", begin);
//끝자리 포함x(+1 안함) // 다음 빈칸까지(마지막 자리는 포함 안됨)
String toid = contents.substring(begin, end);
//contents에서 해당 부분을 찾아 id에 대입
String wisper = contents.substring(end+1);
//두번째 빈칸 다음자리부터 끝까지를 뽑아서 wisper에 저장(내용)
outMsg.println(WISPER+"/"+id + "/"+ toid+ "/" + wisper);
// 각 내용을 /로 구분해서 출력
}
else if(contents.indexOf("van")==0) { //처음 시작이 van
int begin = contents.indexOf(" ") + 1;
// to 1111 안녕하세요 일 경우 처음 빈칸 다음자리부터
String vanid = contents.substring(begin);
//contents에서 해당 부분을 찾아 vanid에 대입
outMsg.println(VAN+"/"+id + "/"+ vanid); // 각 내용을 /로 구분해서 출력
}
else {
outMsg.println(NOMAL+"/"+id + "/" + contents);
int len = contents.length();
if(len>30){
try {
msgOut.append("30자를 초과하여 도배방지를 위해 1분간 입력을 제한합니다.\n");
//해당 클라이언트에서 채팅창에 메시지 출력
msgInput.setText(""); //입력창 비우기
msgInput.setEditable(false); // 채팅입력칸 수정불가
Thread.sleep(60000); //60초간 재우기
msgInput.setEditable(true);//다시 살림
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} //if
}//else
msgInput.setText("");
} //run
};// thread
thread.start();
} //else if(obj == msgInput)
}//action
public void run() {
String msg;
String[] rmsg;
status = true;
while(status) { //수신부
try {
msg = inMsg.readLine();
rmsg = msg.split("/");
int commend = Integer.parseInt(rmsg[0]);
//0번 인덱스에 있을 명령어를 INT형으로 형변환
switch (commend) {
case WISPER: { //귓속말이 온 경우
msgOut.append(rmsg[1] + ">>"+rmsg[2] + "\n" + rmsg[3] +"\n");
//귓속말을 보낸 사람과 받은사람을 채팅창에 표시
break;
}
case CPLIST: { //채팅참가자 리스트가 온 경우
String []userlist = rmsg[1].split(",");
// 1번 인덱스에 있는 참가자 ID SET을 ,를 구분자로 하여 userlist배열에 담기
int size = userlist.length;
listOut.setText(" "); //참가자 리스트창 비우기
for(int i = 0;i<size;i++) { // 요소 하나씩 읽어들여서 참가자 리스트에 추가
listOut.append(userlist[i]);
listOut.append("\n");
}
break;
}
case VAN:{
clayout.show(tab, "login"); //로그인버튼 바꾸기
stopClient();
}
case ERR_DUP:{ //id 중복으로 접속이 팅겼을 경우에 처리
stopClient();
msgOut.append(rmsg[1] + ">"+rmsg[2] + "\n");
break;
}
default:
msgOut.append(rmsg[1] + ">"+rmsg[2] + "\n");
break;
}//switch
msgOut.setCaretPosition(msgOut.getDocument().getLength());
} catch(Exception e) {
// e.printStackTrace();
status = false;
}
}//while
System.out.println("[MultiChatClient]" + thread.getName() + "종료됨");
}
public static void main(String[] args) {
MultiChatClient mcc = new MultiChatClient("127.0.0.1");
}
}
실행화면



스레드 리스트의 0번 스레드만 (가장 먼저 들어온 사람) 강퇴 권한을 갖게 했다.
2222번이 강퇴를 시도하면 권한이 없다고 안내메시지 출력
1111이 강퇴시도 하면 가능
3333이 강제퇴장 당해서 채팅창이 초기화되고 참가자 리스트도 비워짐.

1111번이 로그아웃 한 경우 2222가 강퇴 권한을 갖게 돼서 강퇴 가능
참가자 리스트는 변경이 생길 때마다 서버에서 출력을 해서 현황 동기화
채팅 시간을 내용 옆에 같이 출력하게 했다.
로그인하기 전 혹은 로그아웃 후에는 채팅 입력 창 입력 불가.
로그인하면 입력창 활성화
여러 가지 채팅 명령어를 구현하는 과정에서 충돌하는 것을 막기 위해
명령어를 알아보기 편하게 상수로 정의하고 송수신 시에 명령어에 따라 처리가 가능하도록 구현.
hashmap에서 value 값인 스레드로 key값을 가져오려고 했는데 그렇게는 안됨.
key로 판별을 하기 때문에 검색도 그렇고.
hash 자료구조에 대한 이해가 부족해서 생긴 일... 이걸 고민한 시간이 아깝다.
hashmap에서 key set으로 아이디 set을 가져와서 참가자창에 입력하려고 했다. 엄청 쉽게 생각을 했으나.
set으로 출력을 하기 때문에 예쁘게 출력이 안되고 [1111,2222,3333,4444] 이렇게 출력만 가능.
그래서 값을 가져와서 클라이언트에서 분할 출력을 하게 함. 지금 생각하면 정말 간단한 문제인데
굳이 서버에서 보낼 때부터 예쁘게 보내려고 하다가 시간을 뺏김
추가로 구현해야 하는 기능
파일 전송 기능
욕설 필터
두 가지 기능 모두 text가 아니라 파일 입출력을 활용해야 가능한 기능
욕설 필터의 경우 간단하게 text파일을 만들어서 그걸 계속 읽어서 채팅 내용이랑 대조하는 스레드를 따로 만들어야 할 것으로 예상 DB 연동을 구현하면 좀 더 고급진 프로그램이 될 것 같으나 일단 하나씩....
채팅 인원 제한 기능(무제한 스레드 생성은 서버의 다운을 야기하므로)
사실 스레드 풀로 방지할 수 있긴 한데......
hashmap이나 스레드 리스트를 공유하는데 지금은 클라이언트 수도 적고 사실상 조작하는 사람이
나 하나뿐이라서 문제가 안 생긴 것 같지만
hashmap과 list에 추가 삭제하는 작업을 메서드로 따로 분리해서 메소드 전체를 동기화 블록으로 지정해야
최소한 추가 삭제할 때만큼은 접근이 안되게... 근데 정확히 어떻게 처리해야 하는지 개념이 안 잡힘.
멀티스레드..... 상태 제어, 동기화.... 공부 시작할 때부터 와 이거 문제없이 작동하게 하려면 장난 아니겠는데? 했는데
진짜 장난 없음. 사실 그렇게 멀티스레드 상태 제어를 많이 해야 하는 프로그램도 아닌데 어떻게 어디서부터 손을 대야 할지 모르겠다. 책을 추천받고 싶으나 찾아봐도 원서는 추천해주더구먼.... 그나마 번역본이 있긴 한데 그 책은 번역본을 읽지 말라고 하더라... 번역을 좀 이상하게 해서 개념을 알고 있는 사람이 봐도 좀 이해가 어렵다고....
아무튼 동기화 구현
각 기능별로 클래스 분리가 필요해 보인다
사실 시도를 안 해본 건 아닌데
싱글 스레드에서 클래스 분리는 아무것도 아니었는데... 그냥 매개변수 왔다 갔다 하는 것만 조심하면
멀티스레드 개념이 덜 잡힌 상태에서 분리하려고 하니 여지없이 데드락, 예외 등 아주 빨간색 뿜 뿜이다.
이걸로 최소한 자바에서는 멀티스레드 개념을 파악하고 있다고 피력하고 싶은데.. 가능할지 모르겠다.
'JAVA' 카테고리의 다른 글
재고관리 프로그램 (DB연동) (1) | 2021.03.30 |
---|---|
채팅 프로그램 ( 욕설필터 기능 구현) (1) | 2021.03.15 |
소켓 기반 채팅 프로그램 귓속말 기능 구현 (0) | 2021.03.10 |
JAVA 소켓 채팅 프로그램 (server) (0) | 2021.03.04 |
JAVA 소켓 채팅 프로그램 (Client) (0) | 2021.03.04 |