가톨릭대학교 컴퓨터네트워크 소켓 프로그래밍 프로젝트에서 발표한 자료 및 코드입니다.
프로젝트: CUK Chat Community
소개: 가톨릭대 학생들이 자유롭게 채팅할 수 있는 커뮤니티 (채팅 프로그램)
기능: 채팅 서버 접속, 메시지 전송, 채팅 인원 확인, 학교 주변 음식점 검색, 채팅 내용 삭제, 채팅 서버 나가기

프로그램 설계

1:N 채팅 설계

프로토콜


코드
1. Packet.java
import java.io.Serializable;
//패킷 클래스
public class Packet implements Serializable{
private int cmd; // 명령어
private String msg; // 메시지
public Packet(int cmd, String msg) {
this.cmd=cmd;
this.msg=msg;
}
public int get_cmd() {
return cmd;
}
public String get_msg() {
return msg;
}
}
2. server.java
import java.net.*;
import java.nio.charset.Charset;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
//카카오 클래스
class kakao{
private String place_name;
private String address_name;
public kakao(String place_name, String address_name) {
this.place_name=place_name;
this.address_name=address_name;
}
public String get_place_name() {
return place_name;
}
public String get_address_name() {
return address_name;
}
}
//음식점 찾기 클래스
class findRestaurant{
private static String keyword; // 키워드
private static int size; // 검색 결과 수
private static String GEOCODE_URL; // 검색 주소
private static String GEOCODE_USER_INFO="KakaoAK 4eb81c3defb0830a786e6fd84f3e3f7c"; // 카카오 API에서 발급받은 Rest API Key
public List<kakao> search(String keyword){
URL obj; // kakao api url
List<kakao> list=new ArrayList<kakao>(); // 리스트: ((음식점 이름, 음식점 주소),...)
try{
// Kakao API - 키워드로 지역 검색
keyword = URLEncoder.encode("역곡"+keyword, "UTF-8");
size=15;
GEOCODE_URL=String.format("http://dapi.kakao.com/v2/local/search/keyword.json?query=%s&size=%d",keyword,size);
System.out.println("검색 URL: "+GEOCODE_URL);
obj = new URL(GEOCODE_URL);
HttpURLConnection con = (HttpURLConnection)obj.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Authorization",GEOCODE_USER_INFO);
con.setRequestProperty("content-type", "application/json");
con.setDoOutput(true);
con.setUseCaches(false);
con.setDefaultUseCaches(false);
Charset charset = Charset.forName("UTF-8");
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream(), charset));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
String jsonString=response.toString();
// Json 문자열 파싱 -> List<kakao>에 저장
JSONParser jsonParser = new JSONParser();
JSONObject jsonObject = (JSONObject) jsonParser.parse(jsonString);
JSONArray jsonArray = (JSONArray) jsonObject.get("documents");
try {
for(int i=0; i<jsonArray.size(); i++){
JSONObject objectInArray = (JSONObject) jsonArray.get(i);
String place_name=(String) objectInArray.get("place_name");
String address_name=(String) objectInArray.get("address_name");
kakao k=new kakao(place_name,address_name);
list.add(k);
}
}
catch(Exception e){
e.printStackTrace();
}
}
catch(Exception e){
// TODO Auto-generated catch block
e.printStackTrace();
}
return list;
}
}
//수신 스레드
//클라이언트의 메시지를 받는 역할
class ServerReceiver extends Thread {
server s;
Socket socket;
ServerSender server_sender;
ObjectInputStream in;
ObjectOutputStream out;
// (접속한 클라이언트와 수신하는) 스레드 객체의 멤버 초기화
ServerReceiver(server s, Socket socket, ServerSender server_sender) {
this.s=s;
this.socket = socket;
this.server_sender=server_sender;
try {
in = new ObjectInputStream(socket.getInputStream()); // 데이터 수신을 편하게 하도록 도와주는 객체 (접속한 클라이언트 -> 서버)
out = new ObjectOutputStream(socket.getOutputStream()); // 데이터 송신을 편하게 하도록 도와주는 객체 (서버 -> 접속한 클라이언트)
}
catch(IOException e) {}
}
@Override
public void run() {
String name = "";
String code = "";
String client_key = "";
try {
// 처음 클라이언트가 접속한 상황 -> 대기하다가 수신된 클라이언트 이름을 받아서 clients에 저장, 현재 클라이언트 수 출력
Packet packet = (Packet)in.readObject();
// 클라이언트 이름 및 학번 확인
name = packet.get_msg();
name = name.substring(0,name.length()-9);
code = packet.get_msg();
code = code.substring(code.length()-9,code.length()-5);
// 클라이언트 이름(학번 일부), 데이터 송신 관련 객체를 저장
client_key=name+"("+code+")";
s.clients.put(client_key, out);
// 클라이언트 입장
System.out.println("#"+name+"님이 입장하셨습니다.");
server_sender.sendMemberToClient(client_key); // 접속한 클라이언트에게 온라인 멤버 리스트 보내주기
server_sender.sendToAllFirst(client_key,"#"+name+"님이 입장하셨습니다.");
System.out.println("현재 서버접속자 수는 " + s.clients.size() + "입니다.");
// 이후에는 다음을 반복 (수신 대기하다 메시지 받으면 저장 -> 모든 클라이언트에게 메시지 송신)
while(in!=null) {
// 패킷 얻으면 분석
packet = (Packet)in.readObject();
int cmd=packet.get_cmd();
String msg=packet.get_msg();
System.out.println(msg);
// 멤버 리스트 요청
if(cmd==0x40) {
server_sender.sendMemberToClient(client_key);
}
// 음식점 검색
else if(cmd==0x30) {
msg=msg.substring(1,msg.length());
server_sender.sendRestaurantToClient(client_key,msg);
}
// 메시지 전송
else {
server_sender.sendToAll(msg);
}
}
}
catch(Exception e) {
System.out.println("예외 메시지:"+e.getMessage());
}
// try문이 종료되면 다음을 실행
// ex) 클라이언트가 나가서 in객체가 제대로 동작을 안 함
// 0x10
finally {
server_sender.sendToAll("#"+name+"님이 퇴장하셨습니다.");
s.clients.remove(client_key);
System.out.println("["+socket.getInetAddress()+":"+socket.getPort()+"]"+"에서 접속을 종료하였습니다.");
System.out.println("현재 서버접속자 수는 "+ s.clients.size()+"입니다.");
}
}
}
//송신 클래스 (스레드 x)
//클라이언트에게 메시지를 보내는 역할
class ServerSender{
server s;
public ServerSender(server s) {
this.s=s;
}
// 특정 클라이언트에게 송신(멤버 리스트) : 0x40
void sendMemberToClient(String client_key) {
String members="";
Iterator it = s.clients.keySet().iterator();
while(it.hasNext()) {
members=members.concat(it.next()+"\n");
}
try {
ObjectOutputStream out = (ObjectOutputStream)s.clients.get(client_key);
out.writeObject(new Packet(0x40,members));
}
catch(Exception e){
System.out.println("예외 메시지:"+e.getMessage());
}
}
// 특정 클라이언트에게 송신(음식점 검색) : 0x30
void sendRestaurantToClient(String client_key, String msg) {
String rsts="[음식점 검색] "+msg+'\n'; // 특수기호로 멤버를 보내는 메시지 구분
List<kakao> list=s.rst.search(msg);
for(int i=0; i<list.size(); i++) {
if(i<10)
rsts=rsts.concat((i+1)+". "+list.get(i).get_place_name()+"("+list.get(i).get_address_name()+")"+"\n");
}
System.out.println(rsts);
try {
ObjectOutputStream out = (ObjectOutputStream)s.clients.get(client_key);
out.writeObject(new Packet(0x30,rsts));
}
catch(Exception e){
System.out.println("예외 메시지:"+e.getMessage());
}
}
// (처음 통신) 모든 클라이언트에게 메시지 송신 : 0x20
void sendToAllFirst(String client_key, String msg) {
Iterator it = s.clients.keySet().iterator();
// clients에 저장된 key값이 있으면 반복
while(it.hasNext()) {
try {
String crt_client_key=(String) it.next();
System.out.println(crt_client_key);
System.out.println(client_key);
// 처음 통신을 시도한 클라이언트
if(crt_client_key.equals(client_key)) {
ObjectOutputStream out = (ObjectOutputStream)s.clients.get(client_key);
out.writeObject(new Packet(0x20,msg));
}
else {
ObjectOutputStream out = (ObjectOutputStream)s.clients.get(crt_client_key);
out.writeObject(new Packet(0x21,msg));
}
}
catch(Exception e){
System.out.println("예외 메시지:"+e.getMessage());
}
}
}
// 모든 클라이언트에게 메시지 송신 : 0x21
void sendToAll(String msg) {
Iterator it = s.clients.keySet().iterator();
// clients에 저장된 key값이 있으면 반복
while(it.hasNext()) {
try {
// 현재 클라이언트와 송신하기 위해서 out 객체 가져오기
ObjectOutputStream out = (ObjectOutputStream)s.clients.get(it.next());
// 현재 클라이언트에게 송신
out.writeObject(new Packet(0x21,msg));
}
catch(Exception e){
System.out.println("예외 메시지:"+e.getMessage());
}
}
}
}
public class server {
// 접속한 클라이언트 목록
ConcurrentHashMap clients;
// 음식점 검색 객체
findRestaurant rst;
// key:클라이언트 이름+아이피, value:데이터 송신 관련 객체 (서버 -> 클라이언트)
public server(findRestaurant rst) {
clients = new ConcurrentHashMap();
this.rst=rst;
}
// 서버 오픈
public void start() {
ServerSocket serverSocket = null;
Socket socket = null;
try {
// 서버 컴퓨터의 7777 포트에서 채팅 프로그램 동작
serverSocket = new ServerSocket(7777);
System.out.println("서버가 시작되었습니다.");
// 서버의 송신 객체 생성
ServerSender server_sender=new ServerSender(this);
// 반복 (접속할 클라이언트 대기 -> 접속한 클라이언트에 대한 수신 스레드 생성)
while(true) {
// 대기하다가 연결된 클라이언트가 있으면 해당 클라이언트에 대한 소켓을 생성함
socket = serverSocket.accept();
System.out.println("["+socket.getInetAddress() + ":"+socket.getPort()+"]"+"에서 접속하였습니다.");
// 해당 클라이언트의 메시지를 수신하는 스레드 생성
ServerReceiver thread = new ServerReceiver(this,socket,server_sender);
// 스레드 동작
thread.start();
}
}
catch(Exception e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
public static void main(String args[]) {
// 음식점 검색 객체 생성
findRestaurant rst=new findRestaurant();
// 서버 객체 생성
server s = new server(rst);
// 서버 오픈
s.start();
}
}
3. client.java
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;
import javax.swing.text.DefaultCaret;
import java.net.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.Color;
import java.io.*;
import java.util.Scanner;
//수신 클래스
//서버의 메시지를 받는 역할 <스레드>
class ClientReceiver extends Thread{
Socket socket;
ObjectInputStream in;
ClientGUI gui;
// (서버와 수신하는) 스레드 객체의 멤버 초기화
ClientReceiver(ClientGUI gui, Socket socket) {
this.gui=gui;
this.socket = socket;
try {
in = new ObjectInputStream(socket.getInputStream()); // 데이터 수신을 편하게 하도록 도와주는 객체 (서버 -> 클라이언트)
}
catch(Exception e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
@Override
public void run() {
// 반복 (수신 대기하다가 메시지 받으면 저장 -> TextArea에 출력)
while(in!=null) {
try {
Packet packet = (Packet)in.readObject();
String msg=packet.get_msg();
// 멤버 리스트
if(packet.get_cmd()==0x40) {
gui.txtarea2.append(msg);
gui.stt_field.setText("0x40");
}
// 처음 온 메시지
else if(packet.get_cmd()==0x20) {
gui.txtarea.append(msg+'\n');
gui.stt_field.setText("0x20");
}
// 메시지
else if(packet.get_cmd()==0x21) {
gui.txtarea.append(msg+'\n');
gui.stt_field.setText("0x21");
}
// 음식점
else if(packet.get_cmd()==0x30){
gui.txtarea.append(msg+'\n');
gui.stt_field.setText("0x30");
}
}
catch(Exception e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
}
}
//송신 클래스
//서버에게 메시지를 보내는 역할
/*
* 0x10: 종료
* 0x20: 처음 통신
* 0x21: 통신
* 0x30: 검색
* 0x40: 인원 확인
* */
class ClientSender{
Socket socket;
ObjectOutputStream out;
String name;
String code;
ClientGUI gui;
// 송신 준비
ClientSender(ClientGUI gui, Socket socket, String name, String code) {
this.gui=gui;
this.socket = socket;
try {
this.out = new ObjectOutputStream(socket.getOutputStream()); // 데이터 송신을 편하게 하도록 도와주는 객체 (클라이언트 -> 서버)
this.name = name;
this.code=code;
}
catch(Exception e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
// 처음에는 서버에 이름과 학번 전송
public void first() {
try {
Packet packet=new Packet(0x20,name.concat(code));
out.writeObject(packet);
}
catch(IOException e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
// 음식점 검색, 메시지 전송
public void msg_send(String msg) {
try {
// 음식점 검색
if(msg.charAt(0)=='!')
out.writeObject(new Packet(0x30,msg));
// 메시지 전송
else
out.writeObject(new Packet(0x21, "["+name+"] "+msg));
}
catch(IOException e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
// 멤버 리스트 요청
public void request_member() {
try {
out.writeObject(new Packet(0x40,""));
}
catch(IOException e) {
System.out.println("예외 메시지:"+e.getMessage());
}
}
}
// 버튼 이벤트 처리
// (서버 연결, 메시지 전송, 인원 수 확인)
class Action implements ActionListener{
ClientGUI gui;
ClientSender sender;
Thread receiver;
// 생성자
public Action(ClientGUI gui) {
this.gui=gui;
// 키 이벤트 생성 <Connect, Send>
// <Connect>
gui.name_field.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if(e.getKeyCode()==e.VK_ENTER) {
connect_func(); // 서버 연결
gui.msg_field.requestFocusInWindow(); // 커서 위치 변경
}
}
});
// <Send>
gui.msg_field.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if(e.getKeyCode()==e.VK_ENTER) {
send_func(); // 메시지 전송
}
}
});
}
// 학생 검사
/*
* 1. 학번에는 숫자만 포함돼있음
* 2. 학번의 자릿수는 9자리
* 3. 학번의 앞 네 자리는 1855~2022 사이
*/
boolean check_code(String code) {
try {
int code_int=Integer.parseInt(code);
int four=Integer.parseInt(code.substring(0,4));
if(code.length()==9 && four>=1855 && four<=2022)
return true;
else
return false;
}
catch(NumberFormatException e) {
return false;
}
}
// 서버 연결
public void connect_func() {
// TextArea 초기화
gui.txtarea.setText(null);
gui.txtarea2.setText(null);
String ip="(서버 외부 IP 주소)";
String port="7777";
// 컴포넌트 값 가져오기
String code=gui.code_field.getText();
String name= gui.name_field.getText();
// 올바른 학번인지 검사
boolean student=check_code(code);
// 학생이 확인되고 닉네임이 존재하면 서버에 연결
if(student && !name.equals("")) {
try {
// port 정수화
int port_int=Integer.parseInt(port);
// 소켓 생성
Socket socket = new Socket(ip, port_int);
// 서버와 송신하는 객체 생성
sender = new ClientSender(gui,socket, name, code);
sender.first();
// 서버와 수신하는 객체 생성
receiver = new Thread(new ClientReceiver(gui,socket));
receiver.start();
// 접속 버튼 비활성화
gui.cnxt_btn.setEnabled(false);
}
catch(ConnectException err) {
// 서버에 접속 실패
gui.txtarea.append("서버와 연결을 실패했습니다.");
err.printStackTrace();
}
catch(Exception err) {
// 서버에 접속 실패
gui.txtarea.append("서버와 연결을 실패했습니다.");
err.printStackTrace();
}
}
// 학생이 아님
else {
gui.txtarea.append("올바르지 않은 학번이거나, 닉네임과 학번이 입력되지 않았습니다.");
}
}
// 전송
public void send_func() {
// 컴포넌트 값 가져오기
String msg=gui.msg_field.getText();
// msg가 채워져 있으면 실행
if(!msg.equals("")) {
// 메시지 전송
try {
sender.msg_send(msg);
}
catch(Exception err){}
// 메시지필드 초기화
gui.msg_field.setText(null);
}
}
// 버튼이 눌린 경우 처리
@Override
public void actionPerformed(ActionEvent e) {
JButton act=(JButton) e.getSource(); //활동하는 버튼
String actname=act.getText(); // 활동하는 버튼의 텍스트
switch(actname) {
case "Connect":
connect_func();
break;
case "Send":
send_func();
break;
case "Clear Chat":
// TextArea 초기화
gui.txtarea.setText(null);
break;
case "Refresh":
gui.txtarea2.setText(null);
sender.request_member();
break;
}
}
}
class ClientGUI extends JFrame{
// 컴포넌트 선언
JLabel code_label;
JTextField code_field;
JLabel name_label;
JTextField name_field;
JLabel chat_label;
JButton clear_btn;
JLabel stt_label;
JTextField stt_field;
JTextArea txtarea;
JScrollPane txtareaplus;
JLabel msg_label;
JTextField msg_field;
JButton cnxt_btn;
JLabel user_label;
JButton refresh_btn;
JTextArea txtarea2;
JScrollPane txtareaplus2;
JButton send_btn;
public ClientGUI() {
// GUI 기본 세팅
setSize(670,420);
setLocation(700,300);
setTitle("CUK Chat Community");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
/*
* 채팅 앱 디자인
*/
// contentPane 정의
JPanel contentPane = new JPanel();
setContentPane(contentPane);
contentPane.setLayout(null);
// 패널 정의
JPanel panel_0 = new JPanel();
panel_0.setBounds(0, 0, 470, 400);
panel_0.setLayout(null);
add(panel_0);
JPanel panel_1 = new JPanel();
panel_1.setBounds(470, 0, 200, 400);
panel_1.setLayout(null);
add(panel_1);
// 패널 0 디자인
// 패널 0 - 1행
code_label=new JLabel("Code:");
code_label.setBounds(10,10,80,25);
panel_0.add(code_label);
code_field=new JTextField();
code_field.setBounds(60,10,160,25);
panel_0.add(code_field);
name_label=new JLabel("Name:");
name_label.setBounds(240,10,80,25);
panel_0.add(name_label);
name_field=new JTextField();
name_field.setBounds(300,10,160,25);
name_field.setText(null);
panel_0.add(name_field);
// 패널 0 - 2행
chat_label=new JLabel("Chat:");
chat_label.setBounds(10,45,80,25);
panel_0.add(chat_label);
clear_btn=new JButton("Clear Chat");
clear_btn.setBounds(60,45,160,25);
panel_0.add(clear_btn);
stt_label=new JLabel("Status:");
stt_label.setBounds(240,45,80,25);
panel_0.add(stt_label);
stt_field=new JTextField();
stt_field.setBounds(300,45,160,25);
stt_field.setForeground(new Color(0,0,255));
stt_field.setText("None");
panel_0.add(stt_field);
// 패널 0 - 3행
txtarea = new JTextArea();
DefaultCaret caret = (DefaultCaret)txtarea.getCaret(); // auto-scroll
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
txtareaplus = new JScrollPane(txtarea);
txtarea.setEditable(false);
txtareaplus.setBounds(10,80,450,260);
panel_0.add(txtareaplus);
// 패널 0 - 4행
msg_label=new JLabel("Msg:");
msg_label.setBounds(10,350,80,25);
panel_0.add(msg_label);
msg_field=new JTextField();
msg_field.setBounds(60,350,400,25);
panel_0.add(msg_field);
// 패널 1 디자인
// 패널 1 - 1행
cnxt_btn=new JButton("Connect");
cnxt_btn.setBounds(0,10,175,25);
panel_1.add(cnxt_btn);
// 패널 1 - 2행
user_label=new JLabel("Users:");
user_label.setBounds(0,45,80,25);
panel_1.add(user_label);
refresh_btn=new JButton("Refresh");
refresh_btn.setBounds(50,45,125,25);
panel_1.add(refresh_btn);
// 패널 1 - 3행
txtarea2 = new JTextArea();
DefaultCaret caret2 = (DefaultCaret)txtarea2.getCaret(); // auto-scroll
caret2.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
txtareaplus2 = new JScrollPane(txtarea2);
txtarea2.setEditable(false);
txtareaplus2.setBounds(0,80,175,260);
panel_1.add(txtareaplus2);
// 패널 1 - 4행
send_btn=new JButton("Send");
send_btn.setBounds(0,350,175,25);
panel_1.add(send_btn);
// 버튼 이벤트 처리
Action action=new Action(this);
clear_btn.addActionListener(action);
cnxt_btn.addActionListener(action);
refresh_btn.addActionListener(action);
send_btn.addActionListener(action);
// 시각화
setVisible(true);
}
}
public class client{
public static void main(String[] args) {
ClientGUI gui=new ClientGUI();
}
}
발표 자료