멀티캠퍼스 백엔드 과정40일차[7월 28일] - 네트워크 tcp/udp, 채팅 프로그램

GoldenDusk·2023년 7월 31일
0

네트워크 입출력

  • 카카오가 네트워크 입출력을 기본으로 사는 회사
  • 네트워크는 컴퓨터들을 연결한 환경 입출력은 그 네트워크를 통해 키보드나 그런 것으로 입력을 받아 컴퓨터 화면에 출력해 내는 것이다. 네트워크 입출력은 그러한 여러 컴퓨터를 연결하여 여러 컴퓨터에서 입력을 받아도 컴

1. 네트워크 기초

네트워크

  • 네트워크 : 여러 컴퓨터들을 통신 회선으로 연결한 것
  • LAN: 가정, 회사, 건물, 특정 영역에 존재하는 컴퓨터를 연결한 것
  • WAN: LAN을 연결한 것 = 인터넷[하드웨어적 관점]

서버와 클라이언트

  • 서버 : 서비스를 제공하는 프로그램
  • 클라이언트 : 서비스를 요청하는 프로그램
  • 먼저 클라이언트가 서비스를 요청하고, 서버는 처리 결과를 응답으로 제공

IP 주소

  • IP 주소: 네트워크 어댑터(LAN 카드)마다 할당되는 컴퓨터의 고유한 주소
  • ipconfig(윈도우), ifconfig(맥OS ) 명령어로 네트워크 어댑터에 어떤 IP 주소가 부여되어 있는지 확인
  • 프로그램은 DNS(Domain Name System)를 이용해서 컴퓨터의 IP 주소를 검색

Port 번호

  • 운영체제가 관리하는 서버 프로그램의 연결 번호. 서버 시작 시 특정 Port 번호에 바인딩

2. IP 주소 얻기

InetAddress

  • 자바는 IP 주소를 java.net 패키지의 InetAddress로 표현
  • 로컬 컴퓨터의 InetAddress를 얻으려면 InetAddress.getLocalHost() 메소드를 호출
import java.net.*;
public class InterAddressEx {

	public static void main(String[] args) {
		try {
			InetAddress local = InetAddress.getLocalHost();
			System.out.println("내 컴퓨터 ip 주소 : "+local);
			
			InetAddress[] isArr = InetAddress.getAllByName("www.naver.com");
			for(InetAddress remote : isArr)
				System.out.println("www.naver.com IP주소 : "+remote.getHostAddress());
		}
		catch(Exception e) {}

	}

}

3. TCP 네트워킹

TCP

  • TCP는 연결형 프로토콜로, 상대방이 연결된 상태에서 데이터를 주고 받는 전송용 프로토콜
  • 클라이언트가 연결 요청을 하고 서버가 연결을 수락하면 통신 회선이 고정되고, 데이터는 고정
    회선을 통해 전달. TCP는 보낸 데이터가 순서대로 전달되며 손실이 발생하지 않음
  • ServerSocket은 클라이언트의 연결을 수락하는 서버 쪽 클래스이고, Socket은 클라이언트에서
    연결 요청할 때와 클라이언트와 서버 양쪽에서 데이터를 주고 받을 때 사용되는 클래스
  • 목적 : 신뢰성

  • ServerEx.java

package tcp1;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class ServerEx {
	private static ServerSocket serversocket = null;
	
	public static void main(String[] args) {
		System.out.println("===================");
		System.out.println("서버를 종료하려면 q또는 Q를 입력하고 Enter를 입력하세요");
		System.out.println("===================");
		
		//TCP 서버 시작
		startServer();
		// 키보드 입력
		Scanner input = new Scanner(System.in);
		while(true) {
			String key = input.nextLine();
			if(key.toLowerCase().equals("q")) break;
		}
		input.close();
		//TCP 서버 종료
		stopServer();
	}
	
	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			public void run() {
				//객체 지향은 블록 단위로 코드
				try {
					//TCP 서버를 개발하려면 ServerSocket  생성과 Port 바인딩
					// serversocket.bind(new SocketAddress(50001))로도 사용 가능
					serversocket = new ServerSocket(50001);
					// Port가 이미 다른 프로그램에서 사용 중이라면 BindException이 발생
					System.out.println("[서버] 시작됨");
					
					// 감시하는 리스너 필요
					while(true) {
						System.out.println("\n[서버]연결 요청을 기다리고 있습니다.");
						//연결 수락 1:1 회선을 위해 똑같은 소켓이 필요함, 서버 소켓을 계속 감시 =>accept
						// accept()는 클라이언트가 연결 요청하기 전까지 블로킹(실행 멈춘 상태) 클라이언트의 연결 요청이
						// 들어오면 블로킹이 해제되고 통신용 Socket을 리턴
						Socket socket = serversocket.accept();
						
						//연결된 클라이언트 정보 얻기
						InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
						System.out.println("[서버]"+isa.getHostName() +"의 연결 요청 수락함");
						
						//연결끊기
						socket.close();
						System.out.println("[서버]"+isa.getHostName()+"과의 연결이 종료되었습니다.");
					}
				} catch (IOException e) {
					System.out.println("[서버]"+e.getMessage());
				}
				
			}
			
		};
		
		// 스레드 시작 => 원래는 톰캣역할
		thread.start();
	}
	
	public static void stopServer(){
		try {
			// close() 메소드를 호출해서 Port 번호를 언바이딩해야 서버 종료
			serversocket.close();
			System.out.println("[서버] 종료");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
  • ClientEx.java
package tcp1;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class ClientEx {

	public static void main(String[] args) {
		try {
			//Socket 생성과 동시에 localhost의 50001 port 연결 요청
			Socket socket = new Socket("localhost", 50001);
			System.out.println("[클라이언트]연결 성공");
			
			socket.close();
			System.out.println("[클라이언트]연결 끊음");
		} catch (UnknownHostException e) {
			//ip 표기 방법이 잘못 되었을 때 예외 처리
		}catch (IOException e) {
			// 해당 포트의 서버에 연결할 수 없을 경우 예외 처리
		}

	}

}

예제

  • EchoServer.java
package tcp1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	
	public static void main(String[] args) {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");
		
		//TCP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();
	}	

	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);	
					System.out.println( "[서버] 시작됨");
					
					//연결 수락 및 데이터 통신
					while(true) {
						System.out.println( "\n[서버] 연결 요청을 기다림\n");
						//연결 수락
						Socket socket = serverSocket.accept();
						
						//연결된 클라이언트 정보 얻기
						InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
						System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
						
						//-------------------------------------------------------------------------------
						/*
						//데이터 받기
						InputStream is = socket.getInputStream();
						byte[] bytes = new byte[1024];
						int readByteCount = is.read(bytes);
						String message = new String(bytes, 0, readByteCount, "UTF-8");
						
						//데이터 보내기
						OutputStream os = socket.getOutputStream();
						bytes = message.getBytes("UTF-8");
						os.write(bytes);
						os.flush();
						System.out.println( "[서버] 받은 데이터를 다시 보냄: " + message);
						*/
						//-------------------------------------------------------------------------------
						//데이터 받기
						DataInputStream dis = new DataInputStream(socket.getInputStream());
						String message = dis.readUTF();
						
						//데이터 보내기
						DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
						dos.writeUTF(message);
						dos.flush();
						System.out.println( "[서버] 받은 데이터를 다시 보냄: " + message);
						//-------------------------------------------------------------------------------
						
						//연결 끊기
						socket.close();
						System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음");
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		try {
			//ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			System.out.println( "[서버] 종료됨 ");
		} catch (IOException e1) {}
	}
}
  • EchoClient.java
package tcp1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class EchoClient {
	public static void main(String[] args) {
		try {
			//Socket 생성과 동시에 localhost의 50001 포트로 연결 요청;
			Socket socket = new Socket("localhost", 50001);

			System.out.println( "[클라이언트] 연결 성공");

			//---------------------------------------------------------------------------
			/*
 			//데이터 보내기
 			String sendMessage = "나는 자바가 좋아~~";
 			OutputStream os = socket.getOutputStream();
 			byte[] bytes = sendMessage.getBytes("UTF-8");
 			os.write(bytes);
 			os.flush();
 			System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);

 			//데이터 받기
 			InputStream is = socket.getInputStream();
 			bytes = new byte[1024];
			int readByteCount = is.read(bytes);
			String receiveMessage = new String(bytes, 0, readByteCount, "UTF-8");
			System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);
			*/
			//---------------------------------------------------------------------------
			//데이터 보내기
			String sendMessage = "나는 자바가 좋아~~";
			DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
			dos.writeUTF(sendMessage);
			dos.flush();
			System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);
			
			//데이터 받기
			DataInputStream dis = new DataInputStream(socket.getInputStream());
			String receiveMessage = dis.readUTF();
			System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);
			//---------------------------------------------------------------------------
			
			//연결 끊기
			socket.close();
			System.out.println("[클라이언트] 연결 끊음");
		} catch(Exception e) {
		}
	}
}

4. UDP 네트워크 방식

UDP

  • 발신자가 일방적으로 수신자에게 데이터를 보내는 방식. TCP처럼 연결 요청 및 수락 과정이 없기
    때문에 TCP보다 데이터 전송 속도가 상대적으로 빠름
  • 데이터 전달의 신뢰성보다 속도가 중요하다면 UDP를 사용하고, 데이터 전달의 신뢰성이
    중요하다면 TCP를 사용
  • DatagramSocket은 발신점과 수신점에 해당, DatagramPacket은 주고받는 데이터에 해당
  • 주거니 받거니만 하면 됨
  • 목적 : 속도, 많은 사람들에게 빠르게 보냄

  • NewServer.java
package udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.Scanner;

public class NewsServer {
	private static DatagramSocket datagramSocket = null;
	
	public static void main(String[] args) throws Exception {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");		
		
		//UDP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();		
	}	
		
	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//DatagramSocket 생성 및 Port 바인딩
					datagramSocket = new DatagramSocket(50001);
					System.out.println( "[서버] 시작됨");
					
					while(true) {
						//클라이언트가 구독하고 싶은 뉴스 주제 얻기
						DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
						datagramSocket.receive(receivePacket);
						String newsKind = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
						
						//클라이언트의 IP와 Port 얻기
						SocketAddress socketAddress = receivePacket.getSocketAddress();
						
						//10개의 뉴스를 클라이언트로 전송
						for(int i=1; i<=10; i++) {
							String data = newsKind + ": 뉴스" + i;
							byte[] bytes = data.getBytes("UTF-8");
							DatagramPacket sendPacket = new DatagramPacket(bytes, 0, bytes.length, socketAddress);
							datagramSocket.send(sendPacket);
						}
					}
				} catch (Exception e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}			
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		//DatagramSocket을 닫고 Port 언바인딩
		datagramSocket.close();
		System.out.println( "[서버] 종료됨 ");
	}
}
  • NewsClient.java
package udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

public class NewsClient {
	public static void main(String[] args) {
		try {
			//DatagramSocket 생성
			DatagramSocket datagramSocket = new DatagramSocket();
			
			//구독하고 싶은 뉴스 주제 보내기
			String data = "정치";
			byte[] bytes = data.getBytes("UTF-8");
			DatagramPacket sendPacket = new DatagramPacket(
				bytes, bytes.length, 	new InetSocketAddress("localhost", 50001)
			);
			datagramSocket.send(sendPacket);

			while(true) {
				//뉴스 받기
				DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
				datagramSocket.receive(receivePacket);
				
				//문자열로 변환
				String news = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
				System.out.println(news);
				
				//10번째 뉴스를 받았을 경우 while 문 종료
				if(news.contains("뉴스10")) {
					break;
				}
			}
			
			//DatagramSocket 닫기
			datagramSocket.close();
		} catch(Exception e) {
		}
	}
}

5. 서버의 동시 요청 처리

  • 일반적으로 서버는 다수의 클라이언트와 통신. 서버는 클라이언트들로부터 동시에 요청을 받아서 처리하고, 처리 결과를 개별 클라이언트로 보내줌
  • 스레드를 처리할 때 클라이언트의 폭증으로 인한 서버의 과도한 스레드 생성을 방지하기
    위해 스레드풀사용하는 것이 바람직
  • accept()와 receive()를 제외한 요청 처리 코드를 별도의 스레드에서 작업

threadpool

TCP EchoServer 동시 요청 처리

  • EchoServer.java
package threadpool;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EchoServer {
	private static ServerSocket serverSocket = null;
	private static ExecutorService executorService = Executors.newFixedThreadPool(10);
	
	public static void main(String[] args) {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");
		
		//TCP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
		//TCP 서버 종료
		stopServer();		
	}	

	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//ServerSocket 생성 및 Port 바인딩
					serverSocket = new ServerSocket(50001);	
					System.out.println( "[서버] 시작됨\n");
					
					//연결 수락 및 데이터 통신
					while(true) {
						//연결 수락
						Socket socket = serverSocket.accept();
						
						executorService.execute(() -> {
							try {
								//연결된 클라이언트 정보 얻기
								InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
								System.out.println("[서버] " + isa.getHostName() + "의 연결 요청을 수락함");
									
								//데이터 받기
								DataInputStream dis = new DataInputStream(socket.getInputStream());
								String message = dis.readUTF();
								
								//데이터 보내기
								DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
								dos.writeUTF(message);
								dos.flush();
								System.out.println( "[서버] 받은 데이터를 다시 보냄: " + message);								
								
								//연결 끊기
								socket.close();
								System.out.println("[서버] " + isa.getHostName() + "의 연결을 끊음\n");
							} catch(IOException e) {
							}
						});
					}
				} catch(IOException e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		try {
			//ServerSocket을 닫고 Port 언바인딩
			serverSocket.close();
			executorService.shutdownNow();
			System.out.println( "[서버] 종료됨 ");
		} catch (IOException e1) {}
	}
}

UDP NewsServer 동시 요청 처리

  • NewsServer.java
package threadpool;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewsServer {
	private static DatagramSocket datagramSocket = null;
	private static ExecutorService executorService = Executors.newFixedThreadPool(10);
	
	public static void main(String[] args) throws Exception {
		System.out.println("--------------------------------------------------------------------");
		System.out.println("서버를 종료하려면 q 를 입력하고 Enter 키를 입력하세요.");
		System.out.println("--------------------------------------------------------------------");		
		
		//UDP 서버 시작
		startServer();
		
		//키보드 입력
		Scanner scanner = new Scanner(System.in);
		while(true) {
			String key = scanner.nextLine();
			if(key.toLowerCase().equals("q")) {
				break;
			}
		}
		scanner.close();
		
        //TCP 서버 종료
        stopServer();		
	}	
		
	public static void startServer() {
		//작업 스레드 정의
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					//DatagramSocket 생성 및 Port 바인딩
					datagramSocket = new DatagramSocket(50001);
					System.out.println( "[서버] 시작됨");
					
					while(true) {
						//클라이언트가 구독하고 싶은 뉴스 종류 얻기
						DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
						datagramSocket.receive(receivePacket);
						
						executorService.execute(() -> {
							try {
								String newsKind = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
							
								//클라이언트의 IP와 Port 얻기
								SocketAddress socketAddress = receivePacket.getSocketAddress();
								
								//10개의 뉴스를 클라이언트로 전송
								for(int i=1; i<=10; i++) {
									String data = newsKind + ": 뉴스" + i;
									byte[] bytes = data.getBytes("UTF-8");
									DatagramPacket sendPacket = new DatagramPacket(bytes, 0, bytes.length, socketAddress);
									datagramSocket.send(sendPacket);
								}
							} catch (Exception e) {
							}
						});
					}
				} catch (Exception e) {
					System.out.println("[서버] " + e.getMessage());
				}
			}			
		};
		//스레드 시작
		thread.start();
	}
		
	public static void stopServer() {
		//DatagramSocket을 닫고 Port 언바인딩
		datagramSocket.close();
		executorService.shutdownNow();
		System.out.println( "[서버] 종료됨 ");
	}
}

6. JSON 데이터 형식

JSON(javaScript Object Notation)

  • 네트워크로 전달하는 데이터 형식
  • 두 개 이상의 속성이 있으면 객체 {}로 표기, 두 개 이상의 이 있으면 배열[]로 표기
  • json을 사용하기 위해서는 라이브러리 추가
  • JSON 관련 GITHUB

{
"속성명":속성값,
"속성명":속성값,
"tel" :{"home" : "02-123-1234", "mobile" :"010-1234-1234"},
"skill" : {}
}

  • CreateJsonEx.java : 서버에서 사용하는 방식
package json;

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;

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

// 회원가입에 필요한 데이터를 JSON으로 만들기
public class CreateJsonEx {

	public static void main(String[] args) throws IOException{
		
		//JSON 객체 생성 순서 상관없음
		JSONObject root = new JSONObject();
		
		//생성한 JSON객체에 단순 속성 추가
		root.put("id", "winter");
		root.put("name", "한바다");
		root.put("age", 25);
		root.put("student", true);
		
		//생성한 JSON객체에 객체 속성 추가
		JSONObject tel = new JSONObject();
		tel.put("home", "02-123-1234");
		tel.put("moblie", "010-1233-1234");
		root.put("tel", tel);
		
		// 배열 속성 추가
		JSONArray skill = new JSONArray();
		skill.put("java");
		skill.put("c");
		skill.put("c++");
		skill.put("python");
		root.put("skill", skill);
		
		//String JSON 확인하기
		String json = root.toString();
		
		//파일로 저장
		Writer writer = new FileWriter("C:/temp/member.json", Charset.forName("UTF-8"));
		writer.write(json);
		writer.flush();
		writer.close();
	}

}
  • JSONObject : JSON 객체 표기를 생성하거나 파싱할 때 사용
  • JSONArray : JSON 배열 표기를 생성하거나 파싱할 때 사용
  • RESTful API 서버 만들 시 필수라 잘 알아 둘 것
  • ParseJsonEx.java: 프론트에서 많이 쓰이는 방법

package json;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;

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

public class ParseJsonEx {

	public static void main(String[] args) throws IOException{
		// 1. C:/temp/member.json 파일을 읽기(빨대 생성)
		BufferedReader br = new BufferedReader(new FileReader("C:/Temp/member.json", Charset.forName("UTF-8")));
		
		//2. 읽어온 내용을 String 문자열로 저장
		String json = br.readLine();
		
		//3. 빨대 제거
		br.close();
		
		// 4. 문자열에 저장된 내용에서 속성 정보를 읽어서 출력
		JSONObject root = new JSONObject(json);
		
		System.out.println("id : "+root.getString("id"));
		System.out.println("name : "+root.getString("name"));
		System.out.println("age : "+root.getInt("age"));
		System.out.println("student : "+root.getBoolean("student"));
		
		// 5. 객체 속성 정보 읽기
		JSONObject tel = root.getJSONObject("tel");
		System.out.println("home : " +tel.getString("home"));
		System.out.println("mobile : " +tel.getString("moblie"));
		
		// 5. 배열 속성 정보 읽기 Obeject 타입임 조심
		JSONArray skill = root.getJSONArray("skill");
		System.out.println("skill : ");
		for(Object s : skill) {
			System.out.print(s.toString()+",");
		}
		System.out.println();
		
		for(int i=0; i<skill.length(); i++) {
			System.out.print(skill.get(i)+" ");
		}
	}

}

채팅 서버와 클라이언트 구현

  • TCP 네트워킹을 이용해서 채팅 서버와 클라이언트를 구현

  • 채팅 서버

    • 실행 시 서버를 먼저 돌리고 ChatClient 실행

  • ChatServer는 채팅 서버 실행 클래스로 클라이언트의 연결 요청을 수락하고 통신용SocketClient를 생성하는 역할
    package chatting;
    
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Scanner;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import org.json.JSONObject;
    
    // 채팅서버 실행 클래스 : 1) 클라이언트의 연결 요청을 수락하고 2)통신용 SocketClient를 생성한느 역할
    public class ChatServer {
    	// 필드
    	ServerSocket serverSocket;
    	ExecutorService threadPool = Executors.newFixedThreadPool(100); // 100개의 클라이언트가 동시에 채팅 가능하도록
    	// chatRoom 통신용 SocketClient[비대면 공간 Map에 모여있는 느낌]를 관리하는
    	// 동기화(메세지가 다른 사람에게 똑같이 혹은 1: 1 채팅 가능한)된 Map
    	Map<String, SocketClient> chatRoom = Collections.synchronizedMap(new HashMap<>());
    
    	// 서버 시작 기능(메서드)
    	public void start() throws IOException {
    		// 서버 시작을 위해서는 서버 소켓 필요 50002 포트번호 바인드 되면서 서버 시작
    		serverSocket = new ServerSocket(50002);
    		System.out.println("[서버] 시작됨");
    
    		Thread thread = new Thread(() -> {
    			try { // accept()메소드로 연결이 수락된 요청에 대해서, 통신용 SocketClient를 반복해서 생성
    					// 무한루프
    				while (true) {
    					// 리스너 감시자 필요 : accept 필요
    					Socket socket = serverSocket.accept();
    					// 소켓을 넘겨줌
    					SocketClient sc = new SocketClient(this, socket);
    				}
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    
    		});
    		// 알아서 움직임[서버가 끝나기 전까지는 lifecycle 유지]
    		thread.start();
    	}
    
    	// 클라이언트 연결시 : SocketClient 생성 및 추가 기능(메소드)
    	// 채팅룸에 추가하는 메소드
    	public void addSocketClient(SocketClient socketClient) {
    		String key = socketClient.chatName + "@" + socketClient.clientIp;
    		chatRoom.put(key, socketClient);
    		System.out.println("입 장 : " + key);
    		System.out.println(" 현재 채팅자 수 : " + chatRoom.size() + "\n");
    	}
    
    	// 클라이언트 연결 종료 SocketClient 제거하는 기능(메서드)
    	public void removeSocketClient(SocketClient socketClient) {
    		String key = socketClient.chatName + "@" + socketClient.clientIp;
    		chatRoom.remove(key);
    		System.out.println("퇴 장 : " + key);
    		System.out.println(" 현재 채팅자 수 : " + chatRoom.size() + "\n");
    	}
    
    	// 모든 클라이언트들에게 메세지 보내는 기능(메서드)
    	public void sendToAll(SocketClient sender, String message) {
    		JSONObject root = new JSONObject();
    		root.put("clientIp", sender.clientIp);
    		root.put("chatName", sender.chatName);
    		root.put("message", message);
    
    		String json = root.toString();
    
    		Collection<SocketClient> socketClients = chatRoom.values();
    		// json을 보내줌 한명 한명 보내줌
    		for (SocketClient sc : socketClients) {
    			// sc는 SocketClient임 sc. 붙어있는 것은 거기 클래스에서 작업
    			if (sc == sender)
    				continue;
    			sc.send(json);
    		}
    	}
    
    	// 서버 종료
    	public void stop() {
    		try {
    			serverSocket.close();
    			threadPool.shutdown();
    			chatRoom.values().stream().forEach(sc -> sc.close());
    			System.out.println("[서버] 종료");
    
    		} catch (IOException e) {
    			// TODO: handle exception
    		}
    	}
    
    	// main도 하나의 스레드, 하나하나 실행시켜주는 메인
    	public static void main(String[] args) {
    		try {
    			ChatServer chatServer = new ChatServer();
    			chatServer.start();
    
    			System.out.println("--------------------------------------------------------------------");
    			System.out.println("서버를 종료하려면 q나 Q를 입력하고 Enter 키를 입력하세요.");
    			System.out.println("--------------------------------------------------------------------");
    
    			// 키보드 입력
    			Scanner scanner = new Scanner(System.in);
    			while (true) {
    				String key = scanner.nextLine();
    				if (key.toLowerCase().equals("q")) {
    					break;
    				}
    
    			}
    			scanner.close();
    
    			chatServer.stop(); // 서버 종료
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    }
  • ChatClient
    • 단일 클래스 ChatClient는 채팅 서버로 연결을 요청하고, 연결된 후에는 제일 먼저 대화명을 보내며 다음 서버와 메시지를 주고 받음

    • 채팅 클라이언트 실행 클래스

    • ChatServer에 연결 요청

    • SocketClient와 1:1로 통신

      package chatting;
      
      import java.io.DataInputStream;
      import java.io.DataOutput;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.net.Socket;
      
      import org.json.JSONObject;
      
      // 클라이언트와 1:1 통신 역할
      public class SocketClient {
      	
      	//필드
      	ChatServer chatServer; //ChatServer()메소드를 호출하기
      	Socket socket; //연결을 끊을 때 필요
      	DataInputStream dis; // 문자열을 읽기 위한 보조 스트림
      	DataOutput dos; //문자열을 보내기 위한 보조 스트림
      	String clientIp; //클라이언트의 ip
      	String chatName; //클라이언트의 대화명
      	
      	SocketClient(){}
      	SocketClient(ChatServer cs, Socket socket){
      		try {
      			this.chatServer = cs;
      			this.socket = socket;
      			// input 스트림 생성
      			this.dis = new DataInputStream(socket.getInputStream());
      			//output 스트림 생성
      			this.dos = new DataOutputStream(socket.getOutputStream());
      			// ip 얻어오기
      			InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
      			// 클라이언트 ip 넘겨주기
      			this.clientIp = isa.getHostName();
      			receive();
      			
      			
      		} catch (Exception e) {
      			// TODO: handle exception
      		}
      	}
      	
      	//중요 : 클라이언트가 보낸 JSON 메세지를 받기 and 읽기 기능
      	public void receive() {
      		chatServer.threadPool.execute(()-> {
      			try {
      				while(true) {
      					String receiveJson = dis.readUTF();
      					
      					JSONObject jsonObject = new JSONObject(receiveJson);
      					String command = jsonObject.getString("command");
      					
      					switch(command) {
      					case "incomming": //입장했을 때
      						this.chatName = jsonObject.getString("data");
      						chatServer.sendToAll(this, "들어오셨습니다.");
      						chatServer.addSocketClient(this); //this란 clientsocket임
      						break;
      						
      					case "message": //메세지
      						String message = jsonObject.getString("data");
      						chatServer.sendToAll(this, message);
      						break;
      					}
      					
      				}
      				
      			} catch (IOException e) {
      				chatServer.sendToAll(this, "나가셨습니다."); //퇴장
      				// chat서버 삭제
      				chatServer.removeSocketClient(this);
      			}
      		});
      	}
      		
      		// JSON메소드 보내기
      		public void send(String json) {
      			try {
      				dos.writeUTF(json);
      				((DataOutputStream)dos).flush();
      			} catch (Exception e) {
      				e.printStackTrace();
      			}
      		}
      		
      		// 연결 종료
      		public void close() {
      			try {
      				socket.close();
      			} catch (IOException e) {
      				e.printStackTrace();
      			}
      		}
      	
      	}
  • 소켓 클라이언트
    • ChatClient와 1:1로 통신

      package chatting;
      
      import java.io.DataInputStream;
      import java.io.DataOutputStream;
      import java.io.IOException;
      import java.net.Socket;
      import java.util.Scanner;
      
      import org.json.JSONObject;
      
      // 채팅서버로 연결을 요청하고 연결된 후에는 제일 먼저 대화명을 보낸다. 그런 다음 서버와 메세지를 주고 받는다.
      public class ChatClient {
      	//필드
      	Socket socket;
      	DataInputStream dis;
      	DataOutputStream dos;
      	String chatName;	
      	
      	//메소드: 서버 연결
      	public  void connect() throws IOException {
      		socket = new Socket("localhost", 50002);
      		dis = new DataInputStream(socket.getInputStream());
      		dos = new DataOutputStream(socket.getOutputStream());
      		System.out.println("[클라이언트] 서버에 연결됨");		
      	}	
      	//메소드: JSON 받기
      	public void receive() {
      		Thread thread = new Thread(() -> {
      			try {
      				while(true) {
      					String json = dis.readUTF();
      					JSONObject root = new JSONObject(json);
      					String clientIp = root.getString("clientIp");
      					String chatName = root.getString("chatName");
      					String message = root.getString("message");
      					System.out.println("<" + chatName + "@" + clientIp + "> " + message);
      				}
      			} catch(Exception e1) {
      				System.out.println("[클라이언트] 서버 연결 끊김");
      				System.exit(0);
      			}
      		});
      		thread.start();
      	}	
      	//메소드: JSON 보내기
      	public void send(String json) throws IOException {
      		dos.writeUTF(json);
      		dos.flush();
      	}	
      	//메소드: 서버 연결 종료
      	public void unconnect() throws IOException {
      		socket.close();
      	}	
      	//메소드: 메인
      	public static void main(String[] args) {		
      		try {			
      			ChatClient chatClient = new ChatClient();
      			chatClient.connect();
      			
      			Scanner scanner = new Scanner(System.in);
      			System.out.println("대화명 입력: ");
      			chatClient.chatName = scanner.nextLine();
      			
      			JSONObject jsonObject = new JSONObject();
      			jsonObject.put("command", "incoming");
      			jsonObject.put("data", chatClient.chatName);
      			String json = jsonObject.toString();
      			chatClient.send(json);
      			
      			chatClient.receive();			
      			
      			System.out.println("--------------------------------------------------");
      			System.out.println("보낼 메시지를 입력하고 Enter");
      			System.out.println("채팅를 종료하려면 q를 입력하고 Enter");
      			System.out.println("--------------------------------------------------");
      			while(true) {
      				String message = scanner.nextLine();
      				if(message.toLowerCase().equals("q")) {
      					break;
      				} else {
      					jsonObject = new JSONObject();
      					jsonObject.put("command", "message");
      					jsonObject.put("data", message);
      					json = jsonObject.toString();
      					chatClient.send(json);
      				}
      			}
      			scanner.close();
      			chatClient.unconnect();
      		} catch(IOException e) {
      			System.out.println("[클라이언트] 서버 연결 안됨");
      		}
      	}
      }

    회고

    밀린 포스팅 다 올렸다!! 이제 당일에 올리는 걸 목표로 하자!! 오늘(월요일)은 휴강이다. 거의 한달 반만에 제대로 휴식 가지는 중.. 휴식하면서 회고록이랑 8월 목표 포스팅해야겠다.

profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다. 참고로 워드프레스는 아직 수정중

0개의 댓글