[이것이 자바다] 19장 정리

자바에서의 소켓통신에 대한 개념을 이해하는데 도움이 되는 문제 풀이를 제공합니다. InetAddress를 이용하여 IP 주소를 얻고, ServerSocket 객체 생성을 통해 TCP 서버 프로그램을 개발할 수 있습니다. 서버와 클라이언트 간의 연결 요청 및 종료는 각각 ServerSocket의 accept() 메소드와 Socket의 close() 메소드를 사용합니다.
Jan 22, 2024
[이것이 자바다] 19장 정리

IPExam.java

package ch19; import java.net.InetAddress; import java.net.UnknownHostException; public class IPExam { public static void main(String[] args) { try { InetAddress local = InetAddress.getLocalHost(); System.out.println(local.getHostAddress()); InetAddress[] naver = InetAddress.getAllByName("www.naver.com"); for(InetAddress i : naver) { System.out.println(i); } } catch (Exception e) { e.printStackTrace(); } } }
 

핵심 키워드

  • 자바는 IP주소를 java.net패키지의 InetAddress로 표현한다. InetAddress를 이용하면 로컬 컴퓨터의 IP 주소를 얻을 수 있고, 도메인 이름으로 DNS에서 검색한 후 IP 주소를 가져올 수도 있다.
 

ServerExam.java

package ch19; import java.io.DataInputStream; import java.io.DataOutputStream; 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 ServerExam { private static ServerSocket serverSocket = null; // 스레드풀 설정(동시 접속자 수 10명으로 제한) private static ExecutorService executorService = Executors.newFixedThreadPool(10); public static void main(String[] args) { System.out.println("------------------------------------------"); System.out.println("서버를 종료하려면 Q 또는 q를 입력해주세요."); System.out.println("------------------------------------------"); start(); Scanner sc = new Scanner(System.in); while (true) { String key = sc.nextLine(); if (key.toLowerCase().equals("q")) break; } sc.close(); stop(); } private static void start() { // 스레드 실행 Thread thread = new Thread() { @Override public void run() { try { serverSocket = new ServerSocket(50001); System.out.println("서버 시작됨."); while (true) { // 50001포트로 들어오는 새로운 유저를 기다림, 연결시 유저 정보 담은 객체 생성 System.out.println("[서버] 연결을 기다림"); Socket socket = serverSocket.accept(); // accept 할때마다 스레드풀의 task에 추가. 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() + "의 연결을 종료함"); } catch (Exception e) { System.out.println("[서버] " + e.getMessage()); } }); } } catch (Exception e) { System.out.println("[서버] " + e.getMessage()); } } }; thread.start(); } private static void stop() { // 스레드 종료 try { serverSocket.close(); } catch (Exception e) { System.out.println("[서버] " + e.getMessage()); } } }

ClientExam.java

package ch19; import java.io.DataInputStream; import java.io.DataOutputStream; import java.net.Socket; public class ClientExam { public static void main(String[] args) { try { Socket socket = new Socket("localhost",50001); System.out.println("[클라이언트] 연결 성공"); String semdMessage = "서버에게 보낼 메시지"; DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); dos.writeUTF(semdMessage); dos.flush(); DataInputStream dis = new DataInputStream(socket.getInputStream()); String receiveMessage = dis.readUTF(); System.out.println("서버로부터 받은 메시지: "+receiveMessage); socket.close(); System.out.println("[클라이언트] 연결 끊음"); } catch (Exception e) { e.printStackTrace(); } } }
 

핵심 키워드

  • IP 주소로 프로그램들이 통신할 때는 약속된 데이터 전송 규약이 있다. 이것을 전송용 프로토콜이라고 부른다. 인터넷에서 전송용 프로토콜은 TCP와 UDP가 있다.
  • TCP 서버 프로그램을 개발하려면 우선 ServerSocket 객체를 생성해야 한다.
    • 만약 서버 컴퓨터에 여러 개의 IP가 할당되어 있을 경우, 특정 IP에서만 서비스를 하고 싶다면 InetSocketAddress의 첫 번째 매개값으로 해당 IP를 주면 된다.
    • ServerSocket serverSocket = new ServerSocket (50001);
    • 만약 Port가 이미 다른 프로그램에서 사용 중이라면 BindException이 발생한다. 이 경우에는 다른 Port로 바인딩하거나 Port를 사용 중인 프로그램을 종료하고 다시 실행하면 된다.
    • ServerSocket이 생성되었다면 연결 요청 수락을 위해 accept() 메소드를 실행해야 한다.
    • Socket socket = serverSocket.accept();
    • 만약 리턴된 Socket을 통해 연결된 클라이언트의 IP 주소와 Port 번호를 얻고 싶다면 방법은 getRemoteSocketAddress() 메소드를 호출해서 InetSocketAddress를 얻은 다음 getHostName()과 getPort() 메소드를 호출하면 된다.
    • InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress(); String clientIp = isa.getHostName(); String portNo = isa.getPort();
    • 서버를 종료하려면 ServerSocket의 close() 메소드를 호출한다.
    • serverSocket.close();
  • 클라이언트가 서버에 연결 요청을 하려면 Socket 객체를 생성할 때 생성자 매개값으로 서버 IP 주소와 Port 번호를 제공하면 된다. 만약 IP 주소 대신 도메인 이름을 사용하고 싶다면, 생성자 매개값으로 InetAddress를 제공해야 한다.
    • Socket socket = new Socket("IP", 50001);
    • 연결 요청 시 두 가지 예외가 발생할 수 있다. UnknownHostException은 IP주소가 잘못 표기되었을 때 발생하고, IOException은 제공된 IP와 Port 번호로 연결할 수 없을 때 발생한다.
    • 서버와 연결된 후에 클라이언트에서 연결을 끊고 싶다면 Socket의 close() 메소드를 사용한다.
    • socket.close();
  • 일반적으로 서버는 다수의 클라이언트와 통신을 한다. 만약 서버에 동시 요청 처리에 대한 코드를 작성하지 않는다면, 먼저 연결한 클라이언트의 요청 처리 시간이 길어질수록 다음 클라이언트의 요청 처리 작업이 지연될 수 밖에 없다.
    • 따라서 accept()와 receive()를 제외한 요청 처리 코드를 별도의 스레드에서 작업하는 것이 좋다.
    • 스레드를 처리할 때 주의할 점은 클라이언트의 폭증으로 인한 서버의 과도한 스레드 생성을 방지해야 한다는 것이다. 그래서 스레드풀을 사용하는 것이 바람직하다.
      • 스레드풀은 작업 처리 스레드 수를 제한해서 사용하기 때문에 갑작스런 클라이언트 폭증이 발생해도 크게 문제가 되지 않는다. 다만 작업 큐의 대기 작업이 증가되어 클라이언트에서 응답을 늦게 받을 수도 있다.
 

UDPServerExam.java

package ch19; 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 UDPServerExam { private static DatagramSocket datagramSocket = null; private static ExecutorService executorService = Executors.newFixedThreadPool(10); public static void main(String[] args) { System.out.println("------------------------------------------"); System.out.println("서버를 종료하려면 Q 또는 q를 입력해주세요."); System.out.println("------------------------------------------"); start(); Scanner sc = new Scanner(System.in); while (true) { String key = sc.nextLine(); if (key.toLowerCase().equals("q")) break; } sc.close(); stop(); } private static void start() { // 스레드 시작 Thread thread = new Thread() { @Override public void run() { try { datagramSocket = new DatagramSocket(50001); // 포트 바인딩 System.out.println("[서버] UDP 서버 시작됨"); while (true) { DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024); // DatagramPacket의 사이즈 // 설정 datagramSocket.receive(receivePacket); executorService.execute(()->{ try { String newsKind = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8"); SocketAddress socketAddress = receivePacket.getSocketAddress(); // 클라이언트의 ip와 포트 get for (int i = 1; i <= 5; i++) { // 뉴스를 클라이언트로 전송(TCP와 달리 응답 기다리지 않고 보냄) 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()); } }); } } catch (Exception e) { System.out.println("[서버]: " + e.getMessage()); } } }; thread.start(); } private static void stop() { datagramSocket.close(); // 포트 언바인딩 System.out.println("[서버] UDP 서버 종료됨"); } }

UDPClientExam.java

package ch19; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.util.Scanner; public class UDPClientExam { public static void main(String[] args) { Scanner scan = new Scanner(System.in); try { // DatagramSocket 생성 DatagramSocket datagramSocket = new DatagramSocket(); while (true) { // 전송할 주제 보내기 System.out.print("구독할 뉴스 주제를 입력하세요. 종료를 원하면 q 입력 > "); String data = scan.nextLine(); if (data.toLowerCase().equals("q")) { System.out.println("클라이언트를 종료합니다."); break; } byte[] bytes = data.getBytes("UTF-8"); DatagramPacket sendPacket = new DatagramPacket(bytes, bytes.length, new InetSocketAddress("localHost", 50001)); datagramSocket.send(sendPacket); while(true) { // 데이터 받기(UDP이므로 요청 보내자마자 데이터 받을 준비 안하면 데이터를 못받음) 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); // 원하는 만큼 받고 while문 종료 if (news.contains("뉴스5")) { System.out.println(); break; } } } // DatagramSocket 닫기 datagramSocket.close(); } catch (Exception e) { } } }
 

핵심 키워드

  • UDP는 발신자가 일방적으로 수신자에게 데이터를 보내는 방식으로, TCP처럼 연결 요청 및 수락 과정이 없기 때문에 TCP보다 데이터 전송 속도가 상대적으로 빠르다.
    • UDP는 TCP처럼 고정 회선이 아니라 여러 회선을 통해 데이터가 전송되기 때문에 특정 회선의 속도에 따라 데이터가 순서대로 전달되지 않거나 잘못된 회선으로 인해 데이터 손실이 발생할 수 있다.
    • 따라서 데이터 전달의 신뢰성보다 속도가 중요하다면 UDP를 사용하고, 데이터 전달의 신뢰성이 중요하다면 TCP를 사용해야 한다.
  • 자바는 UDP 네트워킹을 위해 java,net 패키지에서 DatagramSocket과 DatagramPacket 클래스를 제공한다.
  • UDP 서버를 위한 DatagramSocket 객체를 생성할 때에는 바인딩할 Port 번호를 생성자 매개값으로 제공해야 한다.
    • DatagramSocket datagramSocket = new DatagramSocket(50001);
    • UDP 서버는 클라이언트가 보낸 DatagramPacket을 항상 받을 준비를 해야 한다. receive() 메소드는 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다.
    • DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024); datagramSocket.receive(receivePacket);
    • DatagramPacket 생성자의 첫 번째 매개값은 수신될 데이터를 저장할 배열이고 두 번째 매개값은 수신할 수 있는 최대 바이트 수이다. 보통 첫 번째 바이트 배열의 크기를 준다.
    • byte[] bytes = receivePacket.getData(); int num = receivePacket.getLength;
    • 읽은 데이터가 문자열이라면 String 생성자를 이용해서 문자열을 얻을 수 있다.
    • String data = new String(bytes, 0, num, "UTF-8");
    • UDP 서버가 클라이언트에세 처리 내용을 보내려면 클라이언트 IP 주소와 Port 번호가 필요한데, 이것은 receive()로 받은 DatagramPacket에서 얻을 수 있다. getSocketAddress() 메소드를 호출하면 정보가 담긴 SocketAddress 객체를 얻을 수 있다.
    • SocketAddress socketAddress = receivePacket.getSocketAddress();
    • 이렇게 얻은 SocketAddress 객체는 다음과 같이 클라이언트로 보낼 DatagraPacket을 생성할 때 네 번째 매개값으로 사용된다. DatagraPacket의 첫 번째 매개값은 바이트 배열, 두 번째는 시작 인덱스, 세 번째는 보낼 바이트 수이다.
    • String data = "처리 내용"; byte[] bytes = data.getBytes("UTF-8"); DatagraPacket sendPacket = new DatagraPacket( bytes, 0, bytes.length, socketAddress );
    • DatagraPacket을 클라이언트로 보낼 때는 DatagramSocket의 send() 메소드를 사용한다.
    • datagramSocket.send( sendPacket );
    • 더 이상 UDP 클라이언트의 데이터를 수신하지 않고 UDP 서버를 종료하고 싶을 경우에는 DatagramSocket의 close() 메소드를 사용한다.
    • datagramSocket.close();
  • UDP 클라이언트는 서버에 요청 내용을 보내고 그 결과를 받는 역할을 한다. UDP 클라이언트를 위한 DatagramSocket 객체는 기본 생성자로 생성한다. Port 번호는 자동으로 부여되기 때문에 따로 지정할 필요가 없다.
    • DatagramSocket datagramSocket = new DatagramSocket();
    • 요청 내용을 보내기 위해서는 DatagramPacket을 사용한다. DatagramPacket 생성자의 첫 번째 매개값은 바이트 배열이고, 두 번째 매개값은 바이트 배열에서 보내고자 하는 바이트 수이다. 세 번째 매개값은 UDP 서버의 IP와 Port 정보를 가지고 있는 InetSocketAddress 객체이다.
    • String data = "요청 내용"; byte[] bytes = data.getBytes("UTF-8"); DatagramPacket sendPacket = new DatagramPacket( bytes, bytes.length, new InetSocketAddress("localhost", 50001) );
    • 생성된 DatagramPacket을 매개값으로 해서 DatagramSocket의 send() 메소드를 호출하면 UDP 서버로 DatagramPacket이 전송된다.
    • datagramSocket.send(sendPacket);
    • UDP 서버에서 처리 결과가 언제 올지 모르므로 항상 받을 준비를 하기 위해 receive() 메소드를 호출한다. receive()메소드는 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다. 이 부분은 UDP 서버와 같다.
    • 더 이상 UDP 서버와 통신할 필요가 없다면 DatagramSocket을 닫기 위해 close() 메소드를 호출한다.
    • datagramSocket.close();
 

JsonExam.java

package ch19; import java.io.FileWriter; import java.io.Writer; import java.nio.charset.Charset; import org.json.JSONArray; import org.json.JSONObject; public class JsonExam { public static void main(String[] args) throws Exception { Person winter = new Person( "winter", "한겨울", 25, true, new Tel("02-123-1234", "010-1234-1324"), new String[] { "java", "C", "C++" } ); Person summer = new Person( "summer", "한여름", 25, true, new Tel("02-123-1234", "010-1234-1324"), new String[] { "Python", "C", "C++" } ); JSONObject obj1 = createJSON(winter); JSONObject obj2 = createJSON(summer); JSONObject root = new JSONObject(); JSONArray people = new JSONArray(); people.put(obj1); people.put(obj2); root.put("people", people); Writer writer = new FileWriter("C:/Temp2/people.json", Charset.forName("UTF-8")); writer.write(root.toString()); // 내용물을 파일로 쓰고 싶다면 toString 사용. writer.flush(); writer.close(); } private static JSONObject createJSON(Person person) { JSONObject obj = new JSONObject(); obj.put("id", person.getId()); // put으로 데이터 입력 가능. 처음이 키, 뒤가 밸류. obj.put("name", person.getName()); obj.put("age", person.getAge()); obj.put("student", person.isStudent()); JSONObject inner = new JSONObject(); inner.put("home", person.getTel().getHome()); inner.put("mobile", person.getTel().getMobile()); obj.put("tel", inner); // 객체도 객체에 넣을 수 있다. JSONArray skill = new JSONArray(); skill.put(person.getSkill()[0]); skill.put(person.getSkill()[1]); skill.put(person.getSkill()[2]); obj.put("skill", skill); return obj; } }

Tel.java

package ch19; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Tel { private String home; private String mobile; }

Person.java

package ch19; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Person { private String id; private String name; private int age; private boolean student; private Tel tel; private String[] skill; }

JSONParsingExam.java

package ch19; import java.io.BufferedReader; import java.io.FileReader; import java.nio.charset.Charset; import org.json.JSONArray; import org.json.JSONObject; public class JSONParsingExam { public static void main(String[] args) throws Exception { // 파일로부터 JSON 읽기 BufferedReader br = new BufferedReader(new FileReader("C:/Temp2/people.json", Charset.forName("UTF-8"))); String people = br.readLine(); br.close(); // JSON 파싱 JSONObject obj = new JSONObject(people); JSONArray arr = obj.getJSONArray("people"); // 메소드 실행 Person person1 = parsePerson(arr.getJSONObject(0)); Person person2 = parsePerson(arr.getJSONObject(1)); System.out.println(person1); System.out.println(person2); } private static Person parsePerson(JSONObject obj) { // 객체 속성 정보 읽기 JSONObject tel = obj.getJSONObject("tell"); // 배열 속성 정보 읽기 JSONArray skill = obj.getJSONArray("skill"); String[] arr = new String[skill.length()]; for (int i = 0; i < skill.length(); i++) { arr[i] = skill.getString(i); } // 속성 정보 읽기 return new Person(obj.getString("id"), obj.getString("name"), obj.getInt("age"), obj.getBoolean("student"), new Tel(tel.getString("home"), tel.getString("mobile")), arr); } }
 

핵심 키워드

  • 네트워크로 전달되는 데이터가 복잡할수록 구조화된 형식이 필요하다. 네트워크 통신에서 가장 많이 사용되는 데이터 형식은 JSON이다.
    • 두 개 이상의 속성이 있는 경우에는 객체{ }로 표기하고, 두 개 이상의 값이 있는 경우에는 배열[ ]로 표기한다.
    • { "id":"winter", "name":"한겨울", "age":25, "student":true, "tell":{"mobile":"010-1234-1324","home":"02-123-1324"}, "skill":["java","c","c++"] }
    • JSONObject 클래스는 JSON 객체 표기를 생성하거나 파싱할 때 사용한다.
    • JSONArray 클래스는 JSON 배열 표기를 생성하거나 파싱할 때 사용한다.
 

결론

해당 문제를 풀면서 자바에서의 소켓통신과 서버의 동시 요청 처리, JSON 파일 작성에 대한 개념을 이해할 수 있었다.
Share article

More articles

See more posts

👨🏻‍💻DriedPollack's Blog