본문 바로가기
뒷북 정리 (국비 교육)/java

[Java] step18. Socket

by 규글 2022. 4. 28.

step18. Socket

 우선 이번 챕터를 시작하기에 앞서 MVN repository 홈페이지에서 json을 다운로드 한다. 당시를 기준으로 최신 버전이었던 20210307 에 해당하는 것을 다운로드한다. [각주:1]

 

 프로젝트에 우클릭을 해서 Build Path > Configure Build Path 를 따라가면 다음의 창을 볼 수 있을 것이다.

 

 Libraries tab에서 Add External JARs 를 클릭하면 파일을 선택할 수 있는데, 다운로드한 json 파일을 선택하고 apply하면, 프로젝트에 json 파일이 추가된 것을 확인할 수 있다.

 

 두 개의 컴퓨터를 바탕으로 내 computer에는 ClientMain을 실행하고, 또 다른 컴퓨터에서는 ServerMain을 실행해서 서로 어떻게 동작하는 것인지 테스트를 하려고 하는 것이 지금은 불가능하다. 그래서 일단은 같은 컴퓨터 내에서 테스트 할 것이고, 그것을 server라고 생각할 것이다.

 

 shell을 열어서 'ipconfig' 라고 작성하면 해당 컴퓨터의 ip를 확인할 수 있다. 내용 중 IPv4 주소에 해당하는 것이 해당 위치에서의 가상 ip가 된다. 127.0.0.1 의 경우는 해당 컴퓨터에서만 접속하는 무언가이며, 자신의 컴퓨터를 의미하는 주소이다.

 

- 01

import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

public class ServerMain {
	public static void main(String[] args) {
		// 필요한 객체를 저장할 지역변수 미리 만들기 
		ServerSocket serverSocket=null;
		try {
			// 5000번 통신 port 을 열고 클라이언트의 접속을 기다린다. 
			serverSocket=new ServerSocket(5000);
			/*
			 *  accept() 메소드는 클라이언트가 실제 접속을 할때 까지 리턴하지 않고
			 *  블록킹 되는 메소드 이다.
			 *  클라이언트가 접속을 해오면 Socket 객체의 참조값을 반환하면서 리턴된다.
			 */
			while(true) {
				System.out.println("클라이언트의 Socket 연결 요청을 대기합니다.");
				Socket socket=serverSocket.accept();
				System.out.println("클라이언트가 접속을 했습니다.");
				String clientIp=socket.getInetAddress().getHostAddress();
				System.out.println("접속한 클라이언트의 아이피:"+clientIp);
				socket.close();
			}
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(serverSocket!=null)serverSocket.close();
				
			}catch(Exception e) {}
		}
	}
}

 위는 ServerMain, 아래는 ClientMain이다. 우선 ServerMain에서 ServerSocket 객체를 만든다. 객체를 만들면서 전달해주는 숫자는 연결할 port의 번호이다. 이렇게 객체를 만들지 않으면 ClientMain에서 접속할 수 없다. 이후 객체의 accept( ) method를 사용하면 client가 socket을 접속할 때까지 method가 동작하지 않고 기다린다. 이런 method를 blocking method라고 했다. 이 Socket 객체는 접속할 때마다 새로운 참조값을 가지게 된다. 같은 ip로 접속한다고 하더라도 접속할 때마다 달라지고, 기존의 것은 버려진다. 그럼 client는 어떨까?

 ClientMain 에서는 Socket 객체를 만들면서 server의 ip와 열려있는 port의 번호를 전달한다. 그리고 객체 생성과 동시에 해당 ip server의 접속 port로 접속 요청을 하게된다. 이 단계가 진행되면 ServerMain에서 대기하고 있던 accept( ) method가 동작하면서 client의 접속을 허가한다. 쭉 넘어가서 그림 하나를 보겠다.

 

import java.net.Socket;

public class ClientMain {
	public static void main(String[] args) {
		Socket socket=null;
		try {
			// 객체 생성과 동시에 서버의 5000번 port로 접속 요청을 하게 됨.
			socket=new Socket("127.0.0.1", 5000);
			System.out.println("Socket 연결 성공!");
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(socket!=null)socket.close();
			}catch(Exception e) {}
		}
		System.out.println("main 메소드가 종료 됩니다.");
	}
}

 Server와 Client가 서로 통신할 때는 서로의 socket을 이용한다. Server에서의 경우 두 가지 종류의 Socket 객체가 있는데, 이 중에 Socket과 client의 Socket이 연결되는 것이다. 만약 Server를 끈다면 해당 server의 ip, port에 대한 정보가 없게 되면서 client 측에서 Socket 객체가 생성되지 않는다.

 

 여러 갈래로 보면 이와 같다. Client 측에서 server 측에 접속을 요청하면, server의 ServerSocket 객체는 Socket의 정보를 반환하고, 이렇게 반환된 server의 socket과 client의 socket이 서로 연결되는 것이다. Server는 연결한 client의 수만큼 Socket 객체를 만든다. 다만 서로의 Socket 객체끼리 연결이 된 것이지 서로의 Socket 객체의 참조값이 동일하게 만들어지고 하는 것은 아니다. 서로 다른 computer이다. (물론 뭐 같을 경우도 있겠지만서도)

 

 이를 보고 파일을 전송하는 것을 생각해보자. Client 측에서 FileInputStream 객체를 통해 socket으로 읽어들인 후, FileOutputStream 객체를 통해 server 측의 socket으로 내보낸다. Server 측에서는 FileInputStream 객체를 통해 socket으로 읽어들인 후, FileOutputStream 객체를 이용해서 파일로 server에 만들어낸다고 할 수 있다. 이렇듯 java에서는 객체를잘 활용해야 한다.

 

- 02

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerMain {
	public static void main(String[] args) {
		// 필요한 객체를 저장할 지역변수 미리 만들기 
		ServerSocket serverSocket=null;
		try {
			// 5000번 통신 port 을 열고 클라이언트의 접속을 기다린다. 
			serverSocket=new ServerSocket(5000);
			/*
			 *  accept() 메소드는 클라이언트가 실제 접속을 할때 까지 리턴하지 않고
			 *  블록킹 되는 메소드 이다.
			 *  클라이언트가 접속을 해오면 Socket 객체의 참조값을 반환하면서 리턴된다.
			 */
			while(true) {
				System.out.println("클라이언트의 Socket 연결 요청을 대기합니다.");
				Socket socket=serverSocket.accept();
				System.out.println("클라이언트가 접속을 했습니다.");
				String clientIp=socket.getInetAddress().getHostAddress();
				System.out.println("접속한 클라이언트의 아이피:"+clientIp);
				// 클라이언트로 부터 읽어들일 (Input) 객체의 참조값 얻어오기
				InputStream is=socket.getInputStream();
				InputStreamReader isr=new InputStreamReader(is);
				BufferedReader br=new BufferedReader(isr);
				// 클라이언트가 전송한 문자열 한줄 읽어들이기 
				String msg=br.readLine();
				System.out.println("메세지:"+msg);
				socket.close();
			}
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(serverSocket!=null)serverSocket.close();				
			}catch(Exception e) {}
		}
	}
}

 마찬가지로 위는 Server, 아래는 Client이다. Server에서 port를 열고 client가 해당 port로 접속을 요청하면 Socket 객체를 만들어 client의 것과 연결한다. 그리고 client 측에서 console 창을 통해 입력해둔 메시지를 OutputStreamWriter 객체를 이용해서 내보낸 것을 server 측에서 InputStreamReader 객체를 이용해서 받아서 console 창에 출력한다.

 

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

public class ClientMain {
	public static void main(String[] args) {
		// 서버에 전송할 문자열을 입력받아서 
		Scanner scan=new Scanner(System.in);
		System.out.println("서버에 전송할 문자열 입력:");
		String msg=scan.nextLine();
		
		Socket socket=null;
		try {
			socket=new Socket("127.0.0.1", 5000);
			System.out.println("Socket 연결 성공!");
			// 문자열을 서버에 전송(출력Output) 하기
			OutputStream os=socket.getOutputStream();
			OutputStreamWriter osw=new OutputStreamWriter(os);
			osw.write(msg);
			osw.write("\r\n");// 개행기호도 출력 (서버에서 줄단위로 읽어낼 예정)
			osw.flush();
			osw.close();
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(socket!=null)socket.close();
			}catch(Exception e) {}
		}
		System.out.println("main 메소드가 종료 됩니다.");
	}
}

 

- 03

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class ClientMain extends JFrame implements ActionListener{
	// 필드
	JTextField tf_msg;
	// 생성자
	public ClientMain() {
		// 레이아웃을 BorderLayout 으로 지정하기 
		setLayout(new BorderLayout());
		
		// 패널
		JPanel panel=new JPanel();
		panel.setBackground(Color.YELLOW);
		// 입력창
		tf_msg=new JTextField(10);
		// 버튼
		JButton sendBtn=new JButton("전송");
		sendBtn.setActionCommand("send");
		// 패널에 입력창과 버튼을 추가
		panel.add(tf_msg);
		panel.add(sendBtn);
		// 프레임의 아래쪽에 패널 배치하기
		add(panel, BorderLayout.SOUTH);
		
		// 버튼에 리스너 등록
		sendBtn.addActionListener(this);
	}	
	
	public static void main(String[] args) {
		// 프레임 객체 생성
		ClientMain f=new ClientMain();
		f.setTitle("채팅창");
		f.setBounds(100, 100, 500, 500);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setVisible(true);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// 전송할 문자열
		String msg=tf_msg.getText();
		
		Socket socket=null;
		try {
			socket=new Socket("127.0.0.1", 5000);
			System.out.println("Socket 연결 성공!");
			// 문자열을 서버에 전송(출력Output) 하기
			OutputStream os=socket.getOutputStream();
			OutputStreamWriter osw=new OutputStreamWriter(os);
			osw.write(msg);
			osw.write("\r\n");// 개행기호도 출력 (서버에서 줄단위로 읽어낼 예정)
			osw.flush();
			osw.close();
		}catch(Exception e2) {
			e2.printStackTrace();
		}finally {
			try {
				if(socket!=null)socket.close();
			}catch(Exception e2) {}
		}
	}
}

 이번 예시는 server 측은 같아서 client측만 가져왔다. 그런데 사실상 거의 차이가 없는 셈이다. 이전에 했던 JFrame 객체를 가지고 만든 창에서 작성한 메시지를 field의 값으로 받아온 후, 이를 OutputStreamWriter 객체를 이용해서 내보내준 것이다.

 - 02 와 - 03의 예시는 각 메시지를 보낼 때 1회용 socket을 사용했다고 할 수 있다. 그렇다면 1회용 socket을 사용하지 않고 문자를 전달하고 받기 위해서는 어떻게 해야할까?

 

- 04

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class ServerMain {
	// static 필드
	static List<ServerThread> threadList=new ArrayList<>();
	
	public static void main(String[] args) {		
		// 필요한 객체를 저장할 지역변수 미리 만들기 
		ServerSocket serverSocket=null;
		try {
			// 5000번 통신 port 을 열고 클라이언트의 접속을 기다린다. 
			serverSocket=new ServerSocket(5000);
			while(true) {
				System.out.println("클라이언트의 Socket 연결 요청을 대기합니다.");
				Socket socket=serverSocket.accept();
				System.out.println("클라이언트가 접속을 했습니다.");
				// 방금 접속한 클라이언트를 응대할 스레드를 시작 시킨다.
				ServerThread thread=new ServerThread(socket);
				thread.start();
				// 생성하고 시작한 스레드의 참조값을 List 에 저장하기 
				threadList.add(thread);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(serverSocket!=null)serverSocket.close();				
			}catch(Exception e) {}
		}
	}
    
	// 내부 클래스로 스레드 객체를 생성할 클래스를 정의한다.
	// static main() 메소드에서 클래스를 사용하기 위해 static 예약어를 붙여서 정의한다.
	public static class ServerThread extends Thread{
		// 필드 
		Socket socket;
		// 클라이언트에게 출력할수 문자열을 있는 객체
		BufferedWriter bw;
		
		// 생성자의 인자로 Socket 객체를 전달받도록 한다.
		public ServerThread(Socket socket) {
			// 생성자의 인자로 전달 받은 Socket 객체의 참조값을 필드에 저장한다. 
			this.socket=socket;
		}
		// 인자로 전달된 문자열을 Socket 을 통해서 출력하는 메소드 
		public void sendMessage(String msg) throws IOException {
			// 반복문 돌면서 모든 스레드의 BufferedWriter 객체를 이용해서
			// 문자열을 전송한다. 
			for(ServerThread tmp:threadList) {
				tmp.bw.write(msg); // 문자열 출력
				tmp.bw.newLine(); // 개행기호 출력
				tmp.bw.flush(); // 방출
			}
		}
		
		// 새로운 작업 단위가 시작되는 run() 메소드 
		@Override
		public void run() {
			try {
				String clientIp=socket.getInetAddress().getHostAddress();
				System.out.println("접속한 클라이언트의 아이피:"+clientIp);
				// 클라이언트로 부터 읽어들일 (Input) 객체의 참조값 얻어오기
				InputStream is=socket.getInputStream();
				InputStreamReader isr=new InputStreamReader(is);
				BufferedReader br=new BufferedReader(isr);
				
				// 클라이언트에게 출력할 수 있는 객체
				OutputStream os=socket.getOutputStream();
				OutputStreamWriter osw=new OutputStreamWriter(os);
				// BufferedWriter 객체의 참조값을 필드에 저장하기 
				bw=new BufferedWriter(osw);
				
				while(true) {
					/*
					 *  클라이언트가 문자열을 한줄 (개행기호와 함께) 보내면
					 *  readLine() 메소드가 리턴 하면서 보낸 문자열을 가지고 온다.
					 *  보내지 않으면 계속 블로킹 되어서 대기하고 있다가
					 *  접속이 끊어지면 Exception 이 발생하거나 혹은 null 이 
					 *  리턴 된다. 
					 *  따라서 null 이 리턴되면 반복문을 빠져 나가게 break 문을 만나도록
					 *  한다.
					 *  실행순서가 try 블럭을 벗어나면 run() 메소드가 리턴하게 되고
					 *  run() 메소드가 리턴되면 해당 스레드는 종료가 된다. 
					 */
					String msg=br.readLine();
					System.out.println("메세지:"+msg);
					// 클라이언트에게 동일한 메세지를 보내는 메소드를 호출한다.
					sendMessage(msg);
					if(msg==null) {// 클라이언트의 접속이 끊겼기 때문에 
						break;// 반복문(while) 을 빠져 나오도록 한다. 
					}
				}
			}catch(Exception e) {
				e.printStackTrace();
			}finally {
				// 접속이 끊겨서 종료 되는 스레드는 List에서 제거한다.
				threadList.remove(this);				
				try {
					if(socket!=null)socket.close();
				}catch(Exception e) {}
			}
		}
	}
}

 이전과는 다르게 ServerMain에서 while 문 안에 ServerThread 객체를 만들었는데, 이 객체를 만드는 class를 뜯어보자.

 

 ServerThread class는 Thread를 extends 하면서 inner class로 만들었다. 그리고 constructor는 Socket 객체를 인자로 전달 받는 방식으로 만들고, 해당 객체의 참조값을 field에 저장하게끔 했다. 또 BufferedWriter 객체를 이용해서 메시지를 방출할 수 있도록 method를 만든다. 이 method는 override 한 run( ) method 내에서 쓰일 것이다.

 

 다음 Thread class를 extends 했기 때문에 run( ) method를 override 해야만 한다. 이 안에 우선 client로 부터 메시지를 전달받을 객체와, 다시 client로 전달할 객체를 만든다. 이는 client로부터 오는 메시지를 받음과 동시에 다시 client로 보내기 위함이다.

 

 BufferedReader 객체의 readline( ) method는 text가 들어오기 전까지 blocking되어 대기한다. (Oracle docs에 의하면 이 method가 terminated 되려면 line feed나 (\n) carriage return(\r)나 \r\n 중에 적어도 하나는 와야한다고 적혀있다.) 후에 client로부터 text가 들어오면 이 값을 만들었던 sendMessage( ) method에 인자로 전달하여 BufferedWriter 객체를 통해 방출한다.

 

 그렇다면 ClientMain은 어떨까? 생성자를 만들면서 ClientThread 객체를 만든다. 이 생성자는 main method 안에서 호출된다. 그리고 ActionListener를 implements 했기 때문에 actionPerformed method를 override하면서 sendMessage( ) 가 동작하게 한다.

 sendMessage( ) method는 전송할 text를 읽어서 BufferedWriter 객체를 통해 server socket의 BufferedReader 객체에서 받게 될 것이다. 이어서 ClientThread 객체를 뜯어보자.

 

 우선 run( ) method를 override 하면서 server로부터 입력받을 수 있는 BufferedReader 객체를 만들어둔다.

 

 그리고 while문 내에 만들어둔 BufferedReader 객체의 readLine( ) method를 통해 전달되는 text를 받고, JTextArea 객체에 그 text를 append한다. 그 다음 줄은 내용이 길어져서 스크롤이 생겼을 때, 자동으로 스크롤이 되면서 가장 하단으로 갈 수 있도록 한다. 즉, 자동으로 JTextArea의 위치를 바로 잡아주는 것이다.

 

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class ClientMain extends JFrame 
			implements ActionListener, KeyListener{
	// 필드
	JTextField tf_msg;
	// 서버와 연결된 Socket 객체의 참조값을 담을 필드
	Socket socket;
	BufferedWriter bw;
	JTextArea area;
	
	// 생성자
	public ClientMain() {
		// 서버에 소켓 접속을 한다.
		try {
			// 접속이 성공되면 Socket 객체의 참조값이 반환된다.
			// 반환되는 객체의 참조값을 필드에 저장해 놓는다. 
			socket=new Socket("127.0.0.1", 5000);
			//서버에 문자열을 출력할
			// BufferedWriter 객체의 참조값을 얻어내서 필드에 저장해 놓는다. 
			OutputStream os=socket.getOutputStream();
			OutputStreamWriter osw=new OutputStreamWriter(os);
			bw=new BufferedWriter(osw);
			// 서버로 부터 메세지를 받을 스레드도 시작을 시킨다.
			new ClientThread().start();
		}catch(Exception e) {// 접속이 실패하면 예외가 발생한다.
			e.printStackTrace();
		}
		
		// 레이아웃을 BorderLayout 으로 지정하기 
		setLayout(new BorderLayout());
		
		// 패널
		JPanel panel=new JPanel();
		panel.setBackground(Color.YELLOW);
		// 입력창
		tf_msg=new JTextField(10);
		// 버튼
		JButton sendBtn=new JButton("전송");
		sendBtn.setActionCommand("send");
		// 패널에 입력창과 버튼을 추가
		panel.add(tf_msg);
		panel.add(sendBtn);
		//프레임의 아래쪽에 페널 배치하기
		add(panel, BorderLayout.SOUTH);
		
		// 버튼에 리스너 등록
		sendBtn.addActionListener(this);
		
		// JTextArea의 참조값을 필드에 저장하기
		area=new JTextArea();
		// 문자열 출력 전용으로 사용하기 위해 편집 불가능하도록 설정 
		area.setEditable(false);
		// 배경색
		area.setBackground(Color.PINK);
		// 스크롤 가능하도록
		JScrollPane scroll=new JScrollPane(area,
				JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
				JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		
		// 프레임의 가운데에 배치하기
		add(scroll, BorderLayout.CENTER);
		
		// 엔터키로 메세지 전송 가능하게 하기 위해
		tf_msg.addKeyListener(this);
		
	}// 생성자 	
	
	public static void main(String[] args) {
		// 프레임 객체 생성
		ClientMain f=new ClientMain();
		f.setTitle("채팅창");
		f.setBounds(100, 100, 500, 500);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setVisible(true);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		sendMessage();
	}
	
	//메세지를 전송하는 메소드
	public void sendMessage() {
		// 전송할 문자열
		String msg=tf_msg.getText();
		try {
			// 필드에 있는 BufferedWriter 객체의 참조값을 이용해서 서버에 문자열 출력하기 
			bw.write(msg);
			bw.newLine();// 개행기호도 출력 (서버에서 줄단위로 읽어낼 예정)
			bw.flush();
		}catch(Exception e2) {
			e2.printStackTrace();
		}
		tf_msg.setText("");
	}
	
	// 서버에서 불특정 시점에 도착하는 메세지를 받을 스레드 
	public class ClientThread extends Thread{		
		@Override
		public void run() {
			try {
				// 서버로 부터 입력 받을수 있는 객체의 참조값 얻어오기 
				InputStream is=socket.getInputStream();
				InputStreamReader isr=new InputStreamReader(is);
				BufferedReader br=new BufferedReader(isr);
				while(true) {
					// 서버로부터 문자열이 전송되는지 대기한다. 
					String msg=br.readLine();
					// JTextArea 에 출력하기
					area.append(msg);
					area.append("\r\n");// 개행 기호도 출력하기 
					// 최근 추가된 글 내용이 보일수 있도록
					int docLength=area.getDocument().getLength();
					area.setCaretPosition(docLength);
					if(msg==null) {
						break;
					}
				}
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	@Override
	public void keyPressed(KeyEvent e) {
		// 눌러진 키의 코드값
		int code=e.getKeyCode();
		if(code == KeyEvent.VK_ENTER) {//만일 엔터키를 눌렀다면 
			sendMessage();
		}
	}

	@Override
	public void keyReleased(KeyEvent e) {
		// TODO Auto-generated method stub		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		// TODO Auto-generated method stub		
	}
}

 

- 05

/*
 * 	 JSON
 * 
 *   - Java Script Object Notation (자바스크립트 객체 표기법을 따르는 문자열)
 *   
 *   - 데이터의 type 
 *   1. { }
 *   2. [ ]
 *   3. "xxx"
 *   4.  10  or  10.1
 *   5. true or false
 *   6. null
 *   
 *   - JSON 예제
 *   
 *   {"num":1, "name":"김구라", "isMan":true, "phone" : null}
 *   
 *   [10, 20, 30, 40, 50]
 *   
 *   ["김구라","해골","원숭이"]
 *   
 *   [{},{},{}]
 *   
 *   {"name":"kim", "friends":["김구라","해골","원숭이"] }
 *   
 * 
 *   메세지의 종류
 *   
 *   1. 일반 대화 메세지  
 *      {"type":"msg","name":"김구라", "content":"안녕하세요"}
 *   2. 누군가 입장 했다는 메세지
 *      {"type":"enter", "name":"김구라"}
 *   3. 누군가 퇴장 했다는 메세지
 *      {"type":"out", "name":"원숭이"}
 *   4. 참여자 목록 메세지
 *      {"type":"members", "list":["김구라","해골","원숭이"]}
 */

 이번 step의 마지막 예시는 이번 step을 시작하기에 앞서서 다운 받았던 JSON을 이용하는 것이다. JSON 문자열이 기본 class인 것은 아니다. JSON 문자열은 Java Script Object Notation의 줄임말로, javascript에서 사용했었던 object와 닮아있다. 필요한 JSON 관련 내용을 import해서 사용한다.

 

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

public class ServerMain {
	// static 필드
	static List<ServerThread> threadList=new ArrayList<>();
	
	public static void main(String[] args) {
		
		// 필요한 객체를 저장할 지역변수 미리 만들기 
		ServerSocket serverSocket=null;
		try {
			// 5000번 통신 port 을 열고 클라이언트의 접속을 기다린다. 
			serverSocket=new ServerSocket(5000);
			while(true) {
				// 클라이언트의 소켓 접속을 기다린다.
				Socket socket=serverSocket.accept();
				// 방금 접속한 클라이언트를 응대할 스레드를 시작 시킨다.
				ServerThread thread=new ServerThread(socket);
				thread.start();
				// 생성하고 시작한 스레드의 참조값을 List 에 저장하기 
				threadList.add(thread);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(serverSocket!=null)serverSocket.close();				
			}catch(Exception e) {}
		}
	}
	// 내부 클래스로 스레드 객체를 생성할 클래스를 정의한다.
	// static main() 메소드에서 클래스를 사용하기 위해 static 예약어를 붙여서 정의한다.
	public static class ServerThread extends Thread{
		// 필드 
		Socket socket;
		// 클라이언트에게 출력할수 문자열을 있는 객체
		BufferedWriter bw;
		// 클라이언트의 대화명을 저장할 필드
		String chatName;
		
		// 생성자의 인자로 Socket 객체를 전달받도록 한다.
		public ServerThread(Socket socket) {
			// 생성자의 인자로 전달 받은 Socket 객체의 참조값을 필드에 저장한다. 
			this.socket=socket;
		}
		// 인자로 전달된 문자열을 Socket 을 통해서 출력하는 메소드 
		public void sendMessage(String msg) throws IOException {
			// 반복문 돌면서 모든 스레드의 BufferedWriter 객체를 이용해서
			// 문자열을 전송한다. 
			for(ServerThread tmp:threadList) {
				tmp.bw.write(msg); // 문자열 출력
				tmp.bw.newLine(); // 개행기호 출력
				tmp.bw.flush(); // 방출
			}
		}
		// 참여자 목록을 얻어내서 Client 에게 출력해주는 메소드
		public void sendChatNameList() {
			JSONObject jsonObj=new JSONObject();
			JSONArray jsonArr=new JSONArray();
			// 스레드 리스트에서 대화명을 순서대로 참조해서 JSONArray 객체에 순서대로 넣기 
			for(int i=0; i<threadList.size(); i++) {
				ServerThread tmp=threadList.get(i);
				jsonArr.put(i, tmp.chatName);
			}
			
			jsonObj.put("type", "members");
			jsonObj.put("list", jsonArr);
			
			try {
				sendMessage(jsonObj.toString());
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		// 새로운 작업 단위가 시작되는 run() 메소드 
		@Override
		public void run() {
			try {
				// 클라이언트로 부터 읽어들일 (Input) 객체의 참조값 얻어오기
				InputStream is=socket.getInputStream();
				InputStreamReader isr=new InputStreamReader(is);
				BufferedReader br=new BufferedReader(isr);
				
				// 클라이언트에게 출력할수 있는 객체
				OutputStream os=socket.getOutputStream();
				OutputStreamWriter osw=new OutputStreamWriter(os);
				// BufferedWriter 객체의 참조값을 필드에 저장하기 
				bw=new BufferedWriter(osw);
				
				while(true) {
					// 클라이언트가 전송하는 문자열을 읽어낸다 
					String msg=br.readLine();
					// 전송된 JSON 문자열을 사용할 준비를 한다.
					JSONObject jsonObj=new JSONObject(msg);
					// type 을 읽어낸다
					String type=jsonObj.getString("type");
					if(type.equals("enter")) {
						// 현재 스레드가 대응하는 클라이언트의 대화명을 필드에 저장한다.
						String chatName=jsonObj.getString("name");
						this.chatName=chatName;
						// 대화명 목록을 보내준다. 
						sendChatNameList();
					}
					// 클라이언트에게 동일한 메세지를 보내는 메소드를 호출한다.
					sendMessage(msg);
					if(msg==null) {// 클라이언트의 접속이 끊어졌기 때문에 
						break;// 반복문(while) 을 빠져 나오도록 한다. 
					}
				}
			}catch(Exception e) {
				e.printStackTrace();
			}finally {
				// 접속이 끊어져서 종료 되는 스레드는 List에서 제거한다.
				threadList.remove(this);
				// this 가 퇴장 한다고 메세지를 보낸다.
				try {
					JSONObject jsonObj=new JSONObject();
					jsonObj.put("type", "out");
					jsonObj.put("name", this.chatName);
					sendMessage(jsonObj.toString());
					// 대화명 목록을 보내준다. 
					sendChatNameList();
					if(socket!=null)socket.close();
				}catch(Exception e) {}
			}
		}
	}
}

 이전 예제에 더해서 대화방 참여자의 이름을 보이게 하고 싶은 것이다. 그래서 Server에서는 client로부터 날아오는 JSON Object를 받아 해당 내용 중에 이름을 받아 ServerThread 객체의 field에 저장하고, 그 각 객체의 이름들을 받아 JSON Object의 list에 담아 sendMessage( ) method를 이용해서 client로 내보낸다.

 

 Thread가 마무리됐을 때도 JSON Object를 이용해서 다른 client에게 해당 thread가 종료되었음을 알리기 위한 부분이다.

 

 Client는 어떨까? Client 객체의 constructor에 server의 socket과 연결 후, 새로운 thread를 시작시키도록 만든다. 그리고 constructor 안에서 만든 JSON Object에 type을 enter로 해서 대화명을 저장 후 server로 넘겨준다. Server에서는 이를 받아 server의 JSON Array에 저장하고 참가자 전체의 목록을 얻어준다. 그리고 다시 그 목록을 client로 보낼 준비를 한다. 그리고 client가 보낼 메시지는 client쪽 sendMessage( ) method에서 만든 JSON Object의 content 항목에 저장해서 server로 넘겨주게 된다.

 

 마지막으로 GUI를 업데이트 하기 위한 method이다. 전달받는 JSON Object의 type이 어떤 것인지에 따라 다르게 동작하도록 설계한 것이다. 여기에서 Vector 라는 친구는 List를 implements 한 것으로, 기능이 좀 더 많아서 좀 더 무겁다고 한다.

 이 예시에서의 가장 큰 문제는 첫 번째 입장한 사람 혼자 뿐이라면 참여자 목록이 항상 등장하는 것이 아니라는 점이다. 이는 server에서의 작업이 완료되기 전 client에서의 작업이 완료되기 때문으로 추정할 수 있다.

 

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.List;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ClientMain extends JFrame 
			implements ActionListener, KeyListener{
	// 필드
	JTextField tf_msg;
	// 서버와 연결된 Socket 객체의 참조값을 담을 필드
	Socket socket;
	BufferedWriter bw;
	JTextArea area;
	// 대화명
	String chatName;
	// JList
	JList<String> jList;
	
	// 생성자
	public ClientMain() {
		// 레이아웃을 BorderLayout 으로 지정하기 
		setLayout(new BorderLayout());
		
		// 패널
		JPanel panel=new JPanel();
		panel.setBackground(Color.YELLOW);
		// 입력창
		tf_msg=new JTextField(10);
		// 버튼
		JButton sendBtn=new JButton("전송");
		sendBtn.setActionCommand("send");
		// 패널에 입력창과 버튼을 추가
		panel.add(tf_msg);
		panel.add(sendBtn);
		// 프레임의 아래쪽에 패널 배치하기
		add(panel, BorderLayout.SOUTH);
		
		// 버튼에 리스너 등록
		sendBtn.addActionListener(this);
		
		// JTextArea의 참조값을 필드에 저장하기
		area=new JTextArea();
		// 문자열 출력 전용으로 사용하기 위해 편집 불가능하도록 설정 
		area.setEditable(false);
		// 배경색
		area.setBackground(Color.PINK);
		// 스크롤 가능하도록
		JScrollPane scroll=new JScrollPane(area,
				JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
				JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		
		// 프레임의 가운데에 배치하기
		add(scroll, BorderLayout.CENTER);
		
		// 엔터키로 메세지 전송 가능하게 하기 위해
		tf_msg.addKeyListener(this);
		
		// String[] 에 JList 공간 확보를 위해 임시 문자열을 넣는다.
		String[] title= {"참여자 목록"}; 
		jList=new JList<String>(title);
		jList.setBackground(Color.GREEN);
		
		// 패널에 JList 를 배치하고 
		JPanel rightPanel=new JPanel();
		rightPanel.add(jList);
		// 패널을 프레임의 동쪽에 배치 
		add(rightPanel, BorderLayout.EAST);
		
		// 대화명을 입력 받아서 필드에 저장한다.
		chatName=JOptionPane.showInputDialog(this, "대화명을 입력하세요");
	
		setTitle("대화명:"+chatName);
		// 서버에 소켓 접속을 한다.
		try {
			// 접속이 성공되면 Socket 객체의 참조값이 반환된다.
			// 반환되는 객체의 참조값을 필드에 저장해 놓는다. 
			socket=new Socket("127.0.0.1", 5000);
			// 서버에 문자열을 출력할
			// BufferedWriter 객체의 참조값을 얻어내서 필드에 저장해 놓는다. 
			OutputStream os=socket.getOutputStream();
			OutputStreamWriter osw=new OutputStreamWriter(os);
			bw=new BufferedWriter(osw);
			// 서버로 부터 메세지를 받을 스레드도 시작을 시킨다.
			new ClientThread().start();
			
			// 내가 입장한다고 서버에 메세지를 보낸다.
			// "{"type":"enter", "name":"대화명"}"
			JSONObject jsonObj=new JSONObject();
			jsonObj.put("type", "enter");
			jsonObj.put("name", chatName);
			String msg=jsonObj.toString();
			// BufferedWriter 객체를 이용해서 보내기 
			bw.write(msg);
			bw.newLine();
			bw.flush();
		}catch(Exception e) {// 접속이 실패하면 예외가 발생한다.
			e.printStackTrace();
		}				
	}//생성자 	
	
	public static void main(String[] args) {
		// 프레임 객체 생성
		ClientMain f=new ClientMain();
		f.setBounds(100, 100, 500, 500);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setVisible(true);				
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		sendMessage();
	}
	
	// 메세지를 전송하는 메소드
	public void sendMessage() {
		// 전송할 문자열
		String msg=tf_msg.getText();
		try {
			// JSONObject 객체를 생성해서 정보를 구성하고 
			JSONObject jsonObj=new JSONObject();
			jsonObj.put("type", "msg");
			jsonObj.put("name", chatName);
			jsonObj.put("content", msg);
			// JSON 문자열을 얻어낸다.
			String json=jsonObj.toString();
			
			// 필드에 있는 BufferedWriter 객체의 참조값을 이용해서 서버에 문자열 출력하기 
			bw.write(json);
			bw.newLine();// 개행기호도 출력 (서버에서 줄단위로 읽어낼 예정)
			bw.flush();
		}catch(Exception e2) {
			e2.printStackTrace();
		}
		tf_msg.setText("");
	}
	
	// 서버에서 불특정 시점에 도착하는 메세지를 받을 스레드 
	public class ClientThread extends Thread{
		
		@Override
		public void run() {
			try {
				// 서버로 부터 입력 받을수 있는 객체의 참조값 얻어오기 
				InputStream is=socket.getInputStream();
				InputStreamReader isr=new InputStreamReader(is);
				BufferedReader br=new BufferedReader(isr);
				while(true) {
					// 서버로부터 문자열이 전송되는지 대기한다. 
					String msg=br.readLine();
					// 메소드를 호출하면서 문자열 전달
					updateTextArea(msg);
					// 최근 추가된 글 내용이 보일수 있도록
					int docLength=area.getDocument().getLength();
					area.setCaretPosition(docLength);
					if(msg==null) {
						break;
					}
				}
			}catch(Exception e) {
				e.printStackTrace();
			}
		}// run()
		
		// JTextArea 에 문자열을 출력하는 메소드 
		public void updateTextArea(String msg) {
			try {
				JSONObject jsonObj=new JSONObject(msg);
				String type=jsonObj.getString("type");
				if(type.equals("enter")) {// 입장 메세지라면
					// 누가 입장했는지 읽어낸다. 
					String name=jsonObj.getString("name");
					area.append("["+name+"] 님이 입장했습니다.");
					area.append("\r\n");
				}else if(type.equals("msg")) {// 대화 메세지라면
					// 누가 
					String name=jsonObj.getString("name");
					// 어떤 내용을
					String content=jsonObj.getString("content");
					// 출력하기
					area.append(name+" : "+content);
					area.append("\r\n");
				}else if(type.equals("out")) {
					// 누가
					String name=jsonObj.getString("name");
					// 출력하기
					area.append("[[ "+name+" ]] 님이 퇴장했습니다.");
					area.append("\r\n");
				}else if(type.equals("members")) {// 대화 참여자 목록이 도착
					// list 라는 키값으로 저장된 JSONArray  객체를 얻어온다. 
					JSONArray arr=jsonObj.getJSONArray("list");
					// 참여자 목록을 저장할 Vector
					Vector<String> list=new Vector<>();
					list.add("참여자 목록");
					// 반복문 돌면서 참여자 목록을 다시 넣어준다.
					for(int i=0; i<arr.length(); i++) {
						String tmp=arr.getString(i);
						list.add(tmp);
					}
					// JList 에 참여자 목록 연결하기 
					jList.setListData(list);
				}
			}catch(JSONException je) {
				je.printStackTrace();
			}
		}		
	}// class ClientThread
	
	@Override
	public void keyPressed(KeyEvent e) {
		// 눌러진 키의 코드값
		int code=e.getKeyCode();
		if(code == KeyEvent.VK_ENTER) {//만일 엔터키를 눌렀다면 
			sendMessage();
		}
	}

	@Override
	public void keyReleased(KeyEvent e) {
		// TODO Auto-generated method stub		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		// TODO Auto-generated method stub		
	}
}

'뒷북 정리 (국비 교육) > java' 카테고리의 다른 글

[Java] step20. String  (0) 2022.05.11
[Java] step19. JDBC  (0) 2022.05.03
[Java] step17. InputOutput  (0) 2022.04.25
[Java] step16. Thread  (0) 2022.04.25
[Java] step15. Swing  (0) 2022.04.25

댓글