[JAVA] 20. 소켓 통신

최재원's avatar
Feb 20, 2025
[JAVA] 20. 소켓 통신

1. 통신의 기본

notion image

1. 통신에 필요한 것

  1. 프로토콜
  1. IP주소
  1. 포트 번호
  1. 버퍼

2. 포트 번호 부여

  • 통신이 필요한 프로세스만 포트 번호를 부여 받는다.

2. 소켓 통신 원리

notion image

1. 서버

1. 요구 사항

  1. 소켓의 포트 번호
  1. 통신 프로토콜(TCP로 고정)

2. 클라이언트

1. 요구 사항

  1. 서버의 소켓 포트 번호
  1. 서버의 IP주소
  1. 통신 프로토콜(TCP로 고정)

3. 소켓 통신 구조

서버 소켓, 소켓

1. 소켓 통신 그림⭐

notion image

2. 멀티스레드를 사용해서 통신하는 법

notion image

4. 방화벽 참고

notion image
notion image
  • 기본적으로 OS는 모든 포트 번호를 닫아 놓는다.
  • 통신이 필요한 포트 번호를 따로 허가해준다.
  • 특정 프로세스가 포트를 요청하면 항상 열어둘 것 인지 사용자에게 물어본다.(방화벽 관리자 권한 허가)

5. 통신 방향

1. 단 방향 Simplex

  • 메시지를 보내고 끝

서버

package ex20.ch01; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; // 단방향 통신 one way public class MyServer01 { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(20000); // 요청만 받는 소켓 System.out.println("서버 소켓이 요청 대기중 입니다. 연결을 시도해 주세요."); Socket socket = serverSocket.accept(); // 프로세스 대기, 포트번호는 랜덤으로 지정해줌 System.out.println("통신 소켓이 연결 되었습니다."); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String body = ""; body = br.readLine(); // 버퍼값 소비, 버퍼가 가득 차면 read 그 전까지 대기, \n까지 읽는다 System.out.println("서버측 Body 확인 : " + body); } catch (IOException e) { throw new RuntimeException(e); } } }

클라이언트

package ex20.ch01; import java.io.*; import java.net.Socket; // 단방향 통신 one way public class MyClient01 { public static void main(String[] args) { try { // localhost라는 이름은 자기자신의 ip주소를 키워드로 만들어 둔것. 숫자로된 ip주소는 외부인터넷으로 나가야 확인 가능 Socket socket = new Socket("localhost", 20000); // 소켓 연결 BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); System.out.println("키보드 입력 대기중..."); String msg = keyboard.readLine(); // \n까지 키보드 입력 대기중 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write(msg); bw.write("\n"); bw.flush(); } catch (IOException e) { throw new RuntimeException(e); } } }

2. 반 이중 방향 Half duplex

  • 메시지를 보내고 응답을 받으면 끝

서버

package ex20.ch02; import java.io.*; import java.net.ServerSocket; import java.net.Socket; // 반이중 방향 통신 (주고 난 뒤 받기) half way public class MyServer02 { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(20000); // 요청만 받는 소켓 System.out.println("서버 소켓이 요청 대기중 입니다. 연결을 시도해 주세요."); Socket socket = serverSocket.accept(); // 프로세스 대기, 포트번호는 랜덤으로 지정해줌 System.out.println("통신 소켓이 연결 되었습니다."); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String reqBody = ""; String respBody = ""; reqBody = br.readLine(); // 버퍼값 소비, 버퍼가 가득 차면 read 그 전까지 대기, \n까지 읽는다 // 프로토콜 if (reqBody.equals("name")) { respBody = "metacoding"; } else if (reqBody.equals("age")) { respBody = "39"; } else { respBody = "error"; } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write(respBody); bw.write("\n"); bw.flush(); } catch (IOException e) { throw new RuntimeException(e); } } }

클라이언트

package ex20.ch02; import java.io.*; import java.net.Socket; // 반이중 방향 통신 (주고 난 뒤 받기) half way public class MyClient02 { public static void main(String[] args) { try { // localhost라는 이름은 자기자신의 ip주소를 키워드로 만들어 둔것. 숫자로된 ip주소는 외부인터넷으로 나가야 확인 가능 Socket socket = new Socket("localhost", 20000); // 소켓 연결 BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); System.out.println("키보드 입력 대기중..."); String reqBody = keyboard.readLine(); // name, age만 프로토콜로 받는다 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write(reqBody); bw.write("\n"); bw.flush(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String respBody = br.readLine(); System.out.println(respBody); } catch (IOException e) { throw new RuntimeException(e); } } }

3. 전 이중 방향 Full duflex

  • 메시지를 보내면서 응답을 받는 것

서버

package ex20.ch03; import java.io.*; import java.net.ServerSocket; import java.net.Socket; // full duplex way public class MyServer03 { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(20000); // 요청만 받는 소켓 System.out.println("서버 소켓이 요청 대기중 입니다. 연결을 시도해 주세요."); Socket socket = serverSocket.accept(); // 프로세스 대기, 포트번호는 랜덤으로 지정해줌 System.out.println("통신 소켓이 연결 되었습니다."); // 1. 쓰기 분신 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); new Thread(() -> { while (true) { try { String reqBody = keyboard.readLine(); bw.write(reqBody); bw.write("\n"); bw.flush(); System.out.println("나 : " + reqBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); // 2. 읽기 분신 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); new Thread(() -> { while (true) { try { String respBody = br.readLine(); System.out.println("받은 메시지: " + respBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); } catch (IOException e) { throw new RuntimeException(e); } System.out.println("메인 스레드 종료"); } }

클라이언트

package ex20.ch03; import java.io.*; import java.net.Socket; // 반이중 방향 통신 (주고 난 뒤 받기) full duplex public class MyClient03 { public static void main(String[] args) { try { // localhost라는 이름은 자기자신의 ip주소를 키워드로 만들어 둔것. 숫자로된 ip주소는 외부인터넷으로 나가야 확인 가능 Socket socket = new Socket("localhost", 20000); // 소켓 연결 System.out.println("소켓에 연결되었습니다."); BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); // 쓰기 분신 new Thread(() -> { while (true) { try { String reqBody = keyboard.readLine(); bw.write(reqBody); bw.write("\n"); bw.flush(); System.out.println("나 : " + reqBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 읽기 분신 new Thread(() -> { while (true) { try { String respBody = br.readLine(); System.out.println("받은 메시지: " + respBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); } catch (IOException e) { throw new RuntimeException(e); } } }

4. 통신 muliplex

서버

package ex20.ch04; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; // multiflex (채팅) public class MyServer04 { private List<ManagerThread> threads = new ArrayList<>(); class ManagerThread extends Thread { private String username; private Socket socket; private BufferedReader br; private BufferedWriter bw; public ManagerThread(Socket socket) { this.socket = socket; try { this.br = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); this.bw = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream())); } catch (IOException e) { throw new RuntimeException(e); } } public void setUsername() { try { this.username = this.br.readLine(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void run() { send("당신의 아이디를 전달해 주세요"); setUsername(); send("당신의 아이디는 : " + this.username); while (true) { try { String msg = this.br.readLine(); // 파싱 & 프로토콜은 서비스다 // 1. 파싱 String[] split = msg.split(":"); // All:msg | s1:msg String protocol = split[0]; String body = split[1]; // 2. 프로토콜 if (protocol.equals("ALL")) { for (ManagerThread thread : threads) { thread.send(body); } } else { for (ManagerThread thread : threads) { if (protocol.equals(thread.getUsername())) { thread.send(body); } } } } catch (Exception e) { } } } private String getUsername() { return this.username; } public void send(String msg) { try { this.bw.write(msg); this.bw.write("\n"); this.bw.flush(); } catch (IOException e) { throw new RuntimeException(e); } } } public MyServer04() { try { ServerSocket serverSocket = new ServerSocket(20000); while (true) { Socket socket = serverSocket.accept(); ManagerThread managerThread = new ManagerThread(socket); this.threads.add(managerThread); managerThread.start(); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) { new MyServer04(); } }

절차

  1. 소켓 스레드 리스트 생성
  1. 서버 소켓 생성
  1. 소켓 연결 요청 대기
  1. 담당 스레드 생성
    1. 스레드의 이름, 소켓, 쓰기버퍼, 읽기버퍼 생성
  1. 스레드 리스트에 저장
  1. 스레드 실행
    1. run() 실행
    2. 버퍼읽기 대기
    3. 파싱
    4. 프로토콜 확인
    5. 대상지정
    6. 메시지 날리기

클라이언트

package ex20.ch04; import java.io.*; import java.net.Socket; // 반이중 방향 통신 (주고 난 뒤 받기) full duplex public class MyClient04 { public static void main(String[] args) { try { // localhost라는 이름은 자기자신의 ip주소를 키워드로 만들어 둔것. 숫자로된 ip주소는 외부인터넷으로 나가야 확인 가능 Socket socket = new Socket("192.168.0.99", 20000); // 소켓 연결 System.out.println("소켓에 연결되었습니다."); BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); // 쓰기 분신 new Thread(() -> { while (true) { try { String reqBody = keyboard.readLine(); bw.write(reqBody); bw.write("\n"); bw.flush(); System.out.println("나 : " + reqBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 읽기 분신 new Thread(() -> { while (true) { try { String respBody = br.readLine(); System.out.println("받은 메시지: " + respBody); } catch (IOException e) { throw new RuntimeException(e); } } }).start(); } catch (IOException e) { throw new RuntimeException(e); } } }

6. 소켓 통신 숙제

1. 요구 사항

  • ch02 반 이중 통신을 이용하라(스레드 필요 없음)
  • 서버, 클라이언트 1:1 구조
  • HashMap 만들기(서버)
  • 프로토콜 GET, POST, PUT, DELETE
  • 클라이언트는 메시지를 아래와 같이 보낸다
GET:name -> HashMap에서 name의 값을 찾아서 클라이언트에게 전달. 없으면 -> 404 전달 POST:age/20 -> HashMap에 age키 20값 저장. -> 클라이언트에게 ok 전달 PUT:name/홍길동 -> HashMap에 name키 홍길동값 변경. 없으면 -> 404 전달 DELETE:phone -> HashMap에서 phone키 삭제. 없으면 -> 404 전달

서버

package ex20.homework; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class Server { public static void main(String[] args) { // 저장소 생성 Map<String, String> map = new HashMap<>(); try { // 서버 준비 ServerSocket serverSocket = new ServerSocket(20000); System.out.println("✅ 소켓 서버 준비 완료."); Socket socket = serverSocket.accept(); System.out.println("🆗 소켓 서버 연결됨."); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); while (true) { // 읽기 대기 System.out.println("🔄 요청 대기중..."); String msg = br.readLine(); // 파싱 String[] parsedData = msg.split(":"); if (parsedData.length < 2) { bw.write("400"); bw.write("\n"); bw.flush(); continue; } String protocol = parsedData[0].toUpperCase(); String body = parsedData[1]; //프로토콜 if (protocol.equals("GET")) { String value = map.get(body); if (value != null) { bw.write(value); bw.write("\n"); bw.flush(); } else { bw.write("404"); bw.write("\n"); bw.flush(); } } else if (protocol.equals("POST")) { String[] keyAndValue = body.split("/"); map.put(keyAndValue[0], keyAndValue[1]); bw.write("ok"); bw.write("\n"); bw.flush(); } else if (protocol.equals("PUT")) { String[] keyAndValue = body.split("/"); Object result = map.replace(keyAndValue[0], keyAndValue[1]); if (result == null) { bw.write("404"); bw.write("\n"); bw.flush(); } else { bw.write("ok"); bw.write("\n"); bw.flush(); } } else if (protocol.equals("DELETE")) { Object result = map.remove(body); if (result == null) { bw.write("404"); bw.write("\n"); bw.flush(); } else { bw.write("ok"); bw.write("\n"); bw.flush(); } } else { bw.write("400"); bw.write("\n"); bw.flush(); } System.out.println("❗ 응답 완료."); } } catch (IOException e) { throw new RuntimeException(e); } } }
notion image

클라이언트

package ex20.homework; import java.io.*; import java.net.Socket; public class Client { public static void main(String[] args) { try { // 클라이언트 준비 Socket socket = new Socket("localhost", 20000); BufferedReader keyBoard = new BufferedReader(new InputStreamReader(System.in)); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); while (true) { // 요청 메시지 작성 System.out.println("🔄 요청 메시지를 작성해 주세요..."); String reqMsg = keyBoard.readLine(); // 요청 메시지 보내기 bw.write(reqMsg); bw.write("\n"); bw.flush(); // 응답 메시지 읽기 String resMsg = br.readLine(); System.out.println(resMsg); System.out.println("❗ 요청 완료."); } } catch (IOException e) { throw new RuntimeException(e); } } }
notion image

2. 풀이

1. 코드의 결과를 잘 모를 경우

Test 파일을 만들어 부분 테스트를 해보자

  1. Map 자료형의 메서드 결과값을 잘 모를 경우
package ex20.ch05; import java.util.HashMap; import java.util.Map; public class MyTest { public static void main(String[] args) { Map<String, Object> map = new HashMap<>(); String respBody = (String) map.get("name"); System.out.println(respBody); } }
notion image
  1. String 자료형의 split메서드의 결과값을 잘 모를 경우
package ex20.ch05; import java.util.HashMap; import java.util.Map; public class MyTest02 { public static void main(String[] args) { String reqBody = "GET~name"; String[] method = reqBody.split(":"); System.out.println(method.length); System.out.println(method[0]); } }
notion image

2. 유효성 검사를 꼭 하자

서버

// 유효성 검사 try { msg = reqBody.split(":")[1]; switch (method) { case "GET": respBody = doGet(msg); break; case "POST": respBody = doPost(msg); break; case "PUT": respBody = doPut(msg); break; case "DELETE": respBody = doDelete(msg); break; default: respBody = "404"; } } catch (Exception e) { respBody = "프로토콜이 잘못됐어요. GET:key, POST:key/value, PUT:key/value, DELETE:key 중 하나를 사용해 주세요."; }

클라이언트

// 유효성 검사 while (true) { reqBody = keyIn.readLine(); if (reqBody.startsWith("GET:") || reqBody.startsWith("POST:") || reqBody.startsWith("PUT:") || reqBody.startsWith("DELETE:")) break; else System.out.println("GET:, POST:, PUT:, DELETE:"); }

3. 각 처리 과정은 메서드로 분리

// name static private String doGet(String msg) { String respBody = (String) datas.get(msg); return respBody != null ? respBody : "404"; } // age/20 static private String doPost(String msg) { String key = msg.split("/")[0]; String value = msg.split("/")[1]; datas.put(key, value); return "ok"; } // name/홍길동 static private String doPut(String msg) { return ""; } // phone static private String doDelete(String msg) { return ""; }

3. 풀이를 보고 수정함

서버

package ex20.ch05; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class MyServer { // 저장소 생성 static Map<String, Object> map = new HashMap<>(); static String doGet(String body) { return map.get(body) != null ? (String) map.get(body) : "404"; } static String doPost(String body) { try { String key = body.split("/")[0]; String value = body.split("/")[1]; map.put(key, value); } catch (Exception e) { return "404"; } return "ok"; } static String doPut(String body) { try { String key = body.split("/")[0]; String value = body.split("/")[1]; map.replace(key, value); } catch (Exception e) { return "404"; } return "ok"; } static String doDelete(String body) { return map.remove(body) != null ? "ok" : "404"; } public static void main(String[] args) { try { // 서버 준비 ServerSocket serverSocket = new ServerSocket(20000); System.out.println("✅ 소켓 서버 준비 완료."); Socket socket = serverSocket.accept(); System.out.println("🆗 소켓 서버 연결됨."); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String respBody = ""; while (true) { // 읽기 대기 System.out.println("🔄 요청 대기중..."); String msg = br.readLine(); // 파싱 String[] parsedData = msg.split(":"); String protocol = parsedData[0]; String body = ""; //프로토콜 try { body = parsedData[1]; switch (protocol) { case "GET": respBody = doGet(body); break; case "POST": respBody = doPost(body); break; case "PUT": respBody = doPut(body); break; case "DELETE": respBody = doDelete(body); break; default: respBody = "404"; } } catch (Exception e) { respBody = "프로토콜이 잘못됐어요. GET:key, POST:key/value, PUT:key/value, DELETE:key 중 하나를 사용해 주세요."; } bw.write(respBody); bw.write("\n"); bw.flush(); System.out.println("❗ 응답 완료."); } } catch (IOException e) { throw new RuntimeException(e); } } }

클라이언트

package ex20.ch05; import java.io.*; import java.net.Socket; public class MyClient { public static void main(String[] args) { try { // 클라이언트 준비 Socket socket = new Socket("localhost", 20000); BufferedReader keyIn = new BufferedReader(new InputStreamReader(System.in)); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String reqMsg = ""; while (true) { // 유효성 검사 while (true) { // 요청 메시지 작성 System.out.println("🔄 요청 메시지를 작성해 주세요..."); reqMsg = keyIn.readLine(); if (reqMsg.startsWith("GET:") || reqMsg.startsWith("POST:") || reqMsg.startsWith("PUT:") || reqMsg.startsWith("DELETE:")) break; else System.out.println("GET:, POST:, PUT:, DELETE: 로 시작해 주세요."); } // 요청 메시지 보내기 bw.write(reqMsg); bw.write("\n"); bw.flush(); // 응답 메시지 읽기 String resMsg = br.readLine(); System.out.println(resMsg); System.out.println("❗ 요청 완료."); } } catch (IOException e) { throw new RuntimeException(e); } } }

Share article

jjack1