이자바 - 네트워크 입출력

ezi·2024년 1월 26일

네트워크 : 여러 컴퓨터들을 통신 회선으로 연결한 것
LAN : 특정 영역에 존재하는 컴퓨터를 연결한 것
WAN : LAN을 연결한 것 > 우리가 흔히 말하는 인터넷

IP 주소

컴퓨터의 고유한 주소 : IP 주소
네트워크 어댑터(LAN 카드)마다 할당 됨
DNS를 이용해서 컴퓨터의 IP주소를 검색함
DNS : 도메인 이름, IP를 등록하는 저장소
웹브라우저 : 웹 서버와 통신하는 클라이언트, 사용자가 입력한 도메인 이름으로 DNS에서 IP주소를 검색해 찾은 다음 웹 서버와 연결해서 웹 페이지를 받음

Port 번호

한 대의 컴퓨터에는 다양한 서버들이 동시에 실행되면 클라이언트는 어떤 서버와 통신해야 할지 결정해야 함
IP는 컴퓨터의 네트워크 어댑터까지만 갈 수 있는 정보이기 때문에,
컴퓨터 내부에서 실행되는 서버를 선택하기 위해서는 추가적인 Port번호가 필요함

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

클라이언트 - 데이터 - 네트워크 어댑터(IP주소) - 컴퓨터 ( - 포트번호 - 서버 )

클라이언트도 서버에서 보낸 정볼르 받기 위해서는 Port 번호가 필요한데, 서버와 같이 고정적인 Port 번호에 바인딩하는 것이 아니라 운영체제가 자동으로 부여하는 번호를 사용

이 번호는 클라이언트가 서버로 요청할 때 함께 전송되어 서버가 클라이언트로 데이터를 보낼 때 사용됨

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

로컬 컴퓨터의 InetAddress를 얻고 싶다면

InetAddress ia = InetAddress.getLocalHost();

만약 컴퓨터의 도메인 이름을 알고 있다면 다음 두 개의 메소드를 사용하여 InetAddress 객체를 얻을 수 있음

InetAddress ia = InetAddress.getByName(String domainName);
InetAddress[] iaArr = InetAddress.getAllByName(String domainName);

getByName : DNS 에서 도메인 이름으로 등록된 단 하나의 IP 주소를 가져옴
getAllByName : 등록된 모든 IP 주소를 배열로 가져옴 > 하나의 도메인 이름으로 여러 IP가 등록되어 있는 이유는 클라이언트가 많이 연결되어 있을 경우 서버 부하를 나누기 위해서임

TCP 네트워킹

전송용 프로토콜 : IP 주소로 프로그램들이 통신할 때 약속된 데이터 전송 규약

  • TCP
    연결형 프로토콜 > 상대방이 연결된 상태에서 데이터를 주고 받음
    순서보장, 손실보장
    TCP는 IP와 같이 사용하기 때문에 TCP/IP 라고도 함
    TCP는 웹브라우저가 웹서버에 연결할 때 사용됨, 이메일 전송, 파일 전송 등에도 사용됨
    • 자바
      java.net 패키지에서 ServerSocket과 Socket 클래스를 제공함
      ServerSocket : 클라이언트의 연결을 수락하는 서버쪽 클래스
      Socket : 클라이언트에서 연결 요청할 때와 클라이언트와 서버 양쪽에서 데이터를 주고 받을 때 사용되는 클래스

      ServerSocket을 생성할 때는 바인딩할 Port 번호를 지정해야 함
      서버가 실행되면 클라이언트는 Socket을 이용해서 서버의 IP주소와 Port 번호로 연결 요청을 할 수 있음

      ServerSocket은 accept()로 연결을 수락하고 통신용 Socket을 생성함
      그리고 나서 클라이언트와 서버는 양쪽의 Socket을 이용해서 데이터를 주고 받음

TCP 서버

  1. TCP 서버 프로그램을 개발하려면 우선 ServerSocket 객체를 생성해야 함
    다음은 5001번 Port에 바인딩하는 ServerSocket를 생성하는 코드
    ServerSocket serverSocket = new ServerSocket(5001);

만약 IP 주소 대신 도메인 이름을 사용하고 싶다면, DNS에서 IP 주소를 검색할 수 있도록 생성자 매개값으로 InetSocketAddress 를 제공해야 한다.

Socket socket = new Socket( new InetSocketAddress("domainName", 50001) );

Socket 생성과 동시에 연결 요청을 하지 않고 다음과 같이 기본 생성자로 Socket 을 생성한 후 connect() 메소드로 연결 요청을 할 수도 있다.

socket = new Socket();
socket.connect( new InetSocketAddress("domainName", 50001) );

연결 요청 시 두 가지 예외가 발생할 수 있다.

UnknownHostException 은 IP 주소가 잘못 표기 되었을 떄 발생하고, IOException 은 제공된 IP와 Port 번호로 연결할 수 없을 때 발생한다.

따라서 두 가지 예외를 모두 처리해야 한다.

서버와 연결된 후에 클라이언트에서 연결을 끊고 싶다면 Socket의 close(0 메소드를 다음과 같이 호출하면 된다.

socket.close();
  1. ServerSocket이 생성되었다면 연결 요청을 수락하기 위해 accpet() 메소드를 실행해야 함
    accpet()는 클라이언트가 연결 요청하기 전까지 블로킹(실행을 멈춘 상태)됨
    클라이언트의 요청이 들어오게 되면 블로킹이 해제되고 통신용 Socket을 리턴한다.
    Socket Socket = serverSocket.accept();

TCP 클라이언트

클라이언트가 서버에 연결 요청을 하려면 Socket 객체를 생성할 때 생성자 매개값으로 서버 IP 주소와 Port 번호를 제공하면 된다.
로컬 컴퓨터에서 실행하는 서버로 연결 요청을 할 경우에는 IP 주소 대신 localhost 를 사용할 수 있다.

Socket socket = new Socket("IP", 50001);

만약 IP 주소 대신 도메인 이름을 사용하고 싶다면, DNS에서 IP 주소를 검색할 수 있도록 생성자 매개값으로 InetSocketAddress 를 제공해야 한다.

Socket socket = new Socket( new InetSocketAddress("domainName", 50001) );

Socket 생성과 동시에 연결 요청을 하지 않고 다음과 같이 기본 생성자로 Socket 을 생성한 후 connect() 메소드로 연결 요청을 할 수도 있다.

socket = new Socket();
socket.connect( new InetSocketAddress("domainName", 50001) );

연결 요청 시 두 가지 예외가 발생할 수 있다.
UnknownHostException 은 IP 주소가 잘못 표기 되었을 떄 발생하고, IOException 은 제공된 IP와 Port 번호로 연결할 수 없을 때 발생한다.

따라서 두 가지 예외를 모두 처리해야 한다.

서버와 연결된 후에 클라이언트에서 연결을 끊고 싶다면 Socket의 close(0 메소드를 다음과 같이 호출하면 된다.

socket.close();

입출력 스트림으로 데이터 주고 받기

클라이언트가 연결 요청(connect())을 하고 서버가 연결 수락(accept())했다면, 양쪽의 Socket 객체로부터 각각 입력 스트림과 출력 스트림을 얻을 수 있다.

다음은 Socket 으로부터 InputStream과 OutputStream 을 얻는 코드이다.

InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();

상대방에게 데이터를 보낼 때에는 보낼 데이터를 byte[] 배열로 생성하고, 이것을 매개값으로 해서 OutputStream의 write() 메소드를 호출하면 된다.

String data = "보낼 데이터";
byte[] bytes = data.getBytes("UTF-8");
OutputStream os = socket.getOutputStream();
os.write(bytes);
os.flush();

데이터를 받기 위해서는 받은 데이터를 저장할 byte[] 배열을 하나 생성하고, 이것을 매개값으로 해서 InputStream의 read()메소드를 호출하면 된다.
read() 메소드는 읽은 데이터를 byte[] 배열에 저장하고 읽은 바이트 수를 리턴한다.
받는 데이터가 문자열이라면 다음과 같이 byte[] 배열을 UTF-8 로 디코딩해서 문자열로 얻을 수 있다.

byte[] bytes = new byte[1024];
InputStream is = socket.getInputStream();
int num = is.read(bytes);
String data = new String(bytes, 0, num, "UTF-8");

UDP 네트워킹

UDP 는 발신자가 일방적으로 수신자에게 데이터를 보내는 방식으로, TCP 처럼 연결 요청 및 수락 과정이 없기 때문에 TCP 보다 데이터 전송 속도가 상대적으로 빠르다.

UDP는 TCP 처럼 고정 회선이 아니라 여러 회선을 통해 데이터가 전송되기 때문에 특정 회선의 속도에 따라 데이터가 순서대로 전달되지 않거나 잘못된 회선으로 인해 데이터 손실이 발생할 수 있다.

하지만 실시간 영상 스트리밍에서 한 것의 영상이 손실되더라도 영상은 계속해서 수신되므로 문제가 되지는 않는다.

따라서 데이터 전달의 신뢰성보다 속도가 중요하다면 UDP를 사용하고, 데이터 전달의 신뢰성이 중요하다면 TCP를 사용해야 한다.

자바는 UDP 네트워킹을 위해 java.net 패키지에서 DatagramSocket과 DatagramPacket 클래스를 제공한다.
DatagramSocket은 발신점과 수신점에 해당하고, DatagramPacket은 주고 받는 데이터에 해당한다.

UDP 서버

UDP 서버를 위한 DatagramSocket 객체를 생성할 때에는 바인딩할 Port 번호를 생성자 매개값으로 제공해야 한다.

DatagramSocket datagramSocket = new DatagramSocket(50001);

UDP 서버는 클라이언트가 보낸 DatagramPacket을 항상 받을 준비를 해야 한다.
이 역할을 하는 메소드가 receive() 이다. receive() 메소드는 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다.

DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
datagramSocket.receive(receivePacket);

DatagramPacket 생성자의 첫 번째 매개값은 수신된 데이터를 저장할 배열이고, 두 번째 매개값은 수신할 수 있는 최대 바이트 수이다. 보통 첫 번째 바이트 배열의 크기를 준다.
receive() 메소드가 실행된 후 수신된 데이터와 바이트 수를 얻는 방법은 다음과 같다.

byte[] bytes = receivePacket.getData();
int num = receivePacket.getLength();

읽은 데이터가 문자열이라면 다음과 같이 String 생성자를 이용해서 문자열을 얻을 수 있다.

String data = new String(bytes, 0, num, "UTF-8");

반대로 UDP 서버가 클라이언트에게 처리 내용을 보내려면 클라이언트 IP 주소와 Port 번호가 필요한데, 이것은 receive() 로 받은 DatagramPacket 에서 얻을 수 있다.

SocketAddress socketAddress = receivePacket.getSocketAddress();

이렇게 얻은 SocketAddress 객체는 다음과 같이 클라이언트로 보낼 DatagramPacket 을 생성할 때 네 번째 매개값으로 사용된다.
DatagramPacket 생성자의 첫 번째 매개값은 바이트 배열이고 두번째는 시작 인덱스, 세 번째는 보낸 바이트 수이다.

String data = "처리 내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket( bytes, 0, bytes.length, socketAddress);

DatagramPacket 을 클라이언트로 보낸 때는 DatagramSocket의 send() 메소드를 이용한다.

datagramSocket.send( sendPacket );

더 이상 UDP 클라이언트의 데이터를 수신하지 않고 UDP 서버를 종료하고 싶을 경우에는 다음과 같이 DatagramSocket 의 close() 메소드를 호출하면 된다.

datagramSocket.close();

UDP 클라이언트

UDP 클라이언트는 서버에 요청 내용을 보내고 그 결과를 받는 역할을 한다.
UDP 클라이언트를 위한 DatagramSocket 객체는 기본 생성자로 생성한다. Port 번호는 자동으로 부여되기 때문에 따로 지정할 필요가 없다.

DatagramSocket datagramSocket = new DatagramSocket();

요청 내용을 보내기 위한 DatagramPacket을 생성하는 방법은 다음과 같다.

String data "요청 내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(
	bytes, bytes.length, new InetSocketAddress("localhost", 50001)
);

DatagramPacket 생성자의 첫 번째 매개값은 바이트 배열이고, 두 번째 매개값은 바이트 배열에서 보내고자 하는 바이트 수이다.
세번째 매개값은 UDP 서버의 IP 와 Port 정보를 가지고 있는 InetSocketAddress 객체이다.

생성된 DatagramPacket 을 매개값으로 해서 DatagramSocket의 send() 메소드를 호출하면 UDP 서버로 DatagramPacket이 전송된다.

datagramSocket.send(sendPacket);

더 이상 UDP 서버와 통신할 필요가 없다면 DatagramSocket을 닫기 위해 close() 메소드를 다음과 같이 호출한다.

datagramSocket.close();

서버의 동시 요청 처리

일반적으로 서버는 다수의 클라이언트와 통신을 한다.
서버는 클라이언트로부터 동시에 요청을 받아서 처리하고, 처리 결과를 개별 클라이언트로 보내줘야 한다.

accept()와 receive() 를 제외한 요청 처리 코드를 별로의 스레드에서 작업하는 것이 좋은데 주의할 점은 클리이언트의 폭증으로 인한 서버의 과도한 스레드 생성을 방지해야 한다는 것이다. 그래서 스레드풀을 사용하는 것이 바람직하다.
하지만 스레트풀에서는 작업 큐의 대기 작업이 증가되어 클라이언트에서 응답을 늦게 받을 수 있다.

JSON 데이터 형식
네트워크로 전달하는 데이터가 복잡할수록 구조화된 형식이 필요하다.
네트워크 통신에서 가장 많이 사용되는 데이터 형식은 JSON(javasctript Object Notation)이다.

JSON 에서는 두 개 이상의 속성이 있는 경우에는 객체 {} 로 표기하고, 두 개 이상의 값이 있는 경우에는 배열 [] 로 표기한다.


{
  "id": "winter",
  "name": "한겨울",
  "age": 25,
  "student": true,
  "tel": { "home" : "02-1234-5678", "mobile": "010-1234-5678" },
  "skill": [ "java", "c", "c++" ]
}

JSON 을 문자열로 직접 작성할 수 있지만 대부분은 라이브러리를 이용해서 생성한다.

다음은 JSON 표기법과 관련된 클래스들이다.

클래스 용도
JSONObject JSON 객체 표기를 생성하거나 파싱할 때 사용
JSONArray JSON 배열 표기를 생성하거나 파싱할 때 사용
JSON에서 속성 순서는 중요하지 않기 때문에 추가한 순서대로 작성되지 않아도 상관없다.
그리고 줄바꿈 처리가 되지 않는데, 오히여 네트워크 전송량을 줄여주기 때문에 더 좋다.

profile
차곡차곡

0개의 댓글