가톨릭대학교 컴퓨터네트워크 소켓 프로그래밍 프로젝트에서 발표한 자료 및 코드입니다.
프로젝트: 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(); } }
발표 자료