본문 바로가기

개발 언어 및 알고리즘 기초/JAVA 기초

[Java] 네트워크 (Network)

자바 공부 중 네트워크에 관한 내용을 정리한 것에 대한 기록이다. 아래 책을 공부 중이다.

신용권, 임경균, 『이것이 자바다』, 한빛미디어(2023), p120-121.

 

 

 네트워크 기초 

 네트워크는 여러 컴퓨터들을 통신 회선으로 연결한 것으로, LAN(Local Area Network)와 WAN(Wide Area Network)가 있다. 우리가 흔히 말하는 인터넷은 WAN에 해당한다. 네트워크에서는 서버와 클라이언트가 데이터를 주고받는다.

네트워크에서 서비스를 제공하는 프로그램을 서버(Server), 서비스를 요청하는 프로그램을 클라이언트(Client)라고 하는데, 클라이언트가 서비스를 요청하면, 서버가 처리하여, 다시 클라이언트에게 처리 결과를 응답하는 식이다.  

 

1. IP 주소

 IP(Internet Protocol)는 컴퓨터의 고유한 주소이다. IP 주소는 LAN 카드마다 할당된다. 이 IP 주소를 통해 프로그램들이 통신할 수 있게 된다.

 만약 연결해야 하는 상대 컴퓨터의 IP를 모른다면 DNS(Domain Name System)에서 도메인 이름으로 IP를 검색할 수 있다. 예를 들어, 네이버 사이트에 접속할 때 도메인 이름인 "www.naver.com"을 이용해서 접속하는 식이다. 도메인 이름 별로 복수개의 IP주소를 가지는 경우도 있는데, 이는 클라이언트가 많이 연결되었을 경우 서버 부하를 나누기 위함이다. 

 

2. Port 번호

 Port 번호는 운영체제가 관리하는 서버 프로그램의 연결 번호로,  한 대의 컴퓨터에서는 다양한 서버 프로그램이 실행되기 때문에 이를 실행할 서버를 선택하기 위해서 Port 번호가 사용된다. 다양한 서버의 통신 요청이 들어오면, 이를 종류 별로 바인딩하여, 그에 맞는 서버에 Port 번호를 통해 연결해 주는 식이다. IP주소는 네트워크 어댑터까지만 갈 수 있는 정보이고, 이 어댑터에서 Port 번호에 바인딩하여 서버에 연결 요청을 하게 된다. 

 

3. IP 주소 얻기

 IP 주소를 얻기 위해서는 java.net 패키지의 InetAddress를 사용한다. IP 주소를 얻기 위한 메소드들은 다음과 같다. 

메소드 이용 코드 설명
InetAddress is = InetAddress.getLocalHost(); 로컬 컴퓨터의 IP 주소 얻기
InetAddress is = InetAddress.getByName(String domainName); 도메인 이름으로 IP 주소 얻기 (하나)
InetAddress[] iaArr = InetAddress.getAllByName(String domainName); 해당 도메인 이름을 가진 모든 IP 주소 얻기

 

4. 전송용 프로토콜(protocol) 

 IP 주소로 프로그램들이 통신할 때에는 약속된 데이터 전송 규약인 '전송용 프로토콜'이 있다. 인터넷에서는 TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)의 두 가지 전송용 프로토콜을 사용한다. 

 TCP와 UDP의 가장 큰 차이점은 고정회선인지 여부이다. TCP는 연결 요청 및 수락 과정을 거쳐 고정 회선을 부여하여 통신한다. 따라서 데이터를 손실 없이 전달할 수 있다. 반면 UDP는 연결 요청 및 수락 과정 없이 여러 회선을 통해 데이터가 전송된다. 따라서 속도가 빠른 대신 데이터 순서 변동 및 손실 등의 문제가 발생할 수 있다. 

 두 전송용 프로토콜에 대한 자세한 내용은 후술한다. 

 

 

 

 

 TCP 네트워킹 

 TCP(Transmission Control Protocol)는 연결형 프로토콜로, 상대방이 연결된 상태에서 데이터를 주고받는다. "클라이언트의 연결 요청 -> 서버의 연결 수락"을 거쳐야 비로소 데이터를 주고받을 수 있고, 고정된 통신 회선을 통해 데이터를 전달하기 때문에 손실이 발생하지 않는다. 

 TCP 네트워킹을 위해 java.net 패키지에서 ServerSocket과 Socket 클래스를 제공한다. 

클래스 설명 주요 메소드
ServerSocket  클라이언트의 연결을 수락하는 서버 쪽 클래스
Socket accept() 클라이언트의 연결 요청 수락하며 Socket 생성
void bind(InetSocketAddress(ip, port번호) Port 번호 바인딩, 생성자에서도 바인딩 가능
void close() Port 번호를 언바인딩하여 서버 종료
Socket
 클라이언트에서 연결 요청할 때와 클라이언트와 서버 양쪽에서 데이터를 주고 받을 때 사용되는 클래스
void connect(SocketAddress sa) 연결 요청. 생성자에 매개변수로 주어 바로 연결 요청도 가능.
SocketAddress getRemoteSocketAddress() Soket을 통해 클라이언트 정보 얻기
InputStream getInputStream() InputStream 얻기
OutputStream getOutputStream() OutputStream 얻기
void close() 클라이언트에서 연결 끊기

 

 

 

1. TCP 서버 

 TCP 서버 프로그램 개발을 위해서는 클라이언트의 연결을 수락해주는 서버 쪽 클래스인 ServerSocket 클래스를 사용해야 한다. ServerSocket 클래스를 이용하여 TCP 서버 프로그램을 구현하는 과정을 차례로 기술하고자 한다. 

 

  • ServerSocket 객체 생성
// 객체 생성과 동시에 Port 바인딩
ServerSocket serverSocket = new ServerSocket(50001);

// 기본 생성자로 객체 생성하고 bind() 메소드로 Port 바인딩
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(50001));

// 특정 IP에서만 서비스 하고 싶은 경우
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("xxx.xxx.xxx.xxx", 50001));

 

 ServerSocket 객체를 생성할 때는 Port 번호를 위와 같이 지정해주어야 하는데, 만약 Port가 이미 다른 프로그램에서 사용 중이라면 BindException이 발생한다. 이 경우에는 다른 Port로 바인딩하거나, 사용 중인 프로그램을 종료하고 다시 실행해줘야 한다. 

 

  • 클라이언트 연결 수락

 TCP 서버는 서버에서 연결을 수락해야 데이터를 주고받을 수 있다. ServerSocket이 연결을 수락하면서 통신용 Socket을 생성하게 되어 이를 통해 데이터를 주고받을 수 있게 된다. 이를 위한 코드는 아래와 같다. 

Socket socket = serverSocket.accept();

 

  • Socket을 활용한 클라이언트와의 통신

 클라이언트의 연결을 수락하는 것에 까지 성공했다면, 이제 비로소 데이터를 주고 받을 준비가 되었다. 아래는 클라이언트의 정보를 얻고, 데이터를 주고받은 예시 코드이다. 클라이언트와 데이터를 주고받을 때에는 socket으로부터 InputStream과 OutputStream을 얻어서 이를 통해서 데이터를 주고받는다. 

// 연결된 클라이언트 정보 얻기
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
String clientIp = isa.getHostName();
String portNo = isa.getPort();

// 데이터 받기
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int readByteCount = is.read(bytes);

// 데이터 보내기
OutputStream os = socket.getOutputStream();
os.write(bytes);
os.flush();

 

 Socket으로부터 얻은 InputStream과 OutputStream 역시 보조 스트림과 연결해서도 사용 가능하다. 특히 특정한 데이터 타입을 주고받기 위해서 DataInput/OutputStream을 많이 사용한다. 

 

  • 서버 종료

 서버를 종료하려면 close() 메소드를 이용하여 Port 번호를 언바인딩해야 한다. 이렇게 언바인딩 한 후에는 Port 번호를 재사용할 수 있다.

serverSocket.close();

 

 

2. TCP 클라이언트 

 클라이언트는 연결 요청 시와 데이터를 주고 받을 때 사용하는 Socket 클래스를 이용하여 서버와 소통한다. Socket을 이용하여 TCP 클라이언트를 구현하는 과정을 차례로 기술해보려 한다. 

 

  • Socket 객체 생성 및 연결 요청
// IP 주소와 Port 번호로 객체 생성
Socket socket = new Socket("IP", 50001);

// IP 주소 대신 도메인 이름 사용
Socket socket = new Socket(new InetAddress.getByName("domainName", 50001));

// 기본 생성자로 Socket 생성 후 connect 메소드로 연결 요청
Socket socket = new Socket();
socket.connect(new InetSocketAddress("domainName", 50001));

 

 위와 같이 연결 요청을 할 때는 UnknownHostException(IP 주소가 잘못 표기되었을 때), IOException(제공된 IP와 Port 번호로 연결할 수 없을 때)이 발생할 수 있다. 

 

  • 서버와의 통신

 서버에서 했던 것과 마찬가지로 입출력 스트림을 이용하여 데이터를 주고받게 된다. 아래 예는 서버에서의 예와는 달리 보조 스트림인 DataInputStream과 DataOutputStream을 이용하여 String 데이터를 주고받는다.

// 데이터 보내기
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
String sendMessage = "나는 자바가 좋아";
dos.writeUTF(sendMessage);
dos.flush();

// 데이터 받기
DataInputStream dis = new DataInputStream(socket.getInputStream());
String receiveMessage = dis.readUTF();

 

 

  • 클라이언트에서 연결 끊기
socket.close();

 

 

 

 UDP 네트워킹 

 UDP(User Datagram Protocol)는 발신자가 일방적으로 수신자에게 데이터를 보내는 방식으로, TCP에 비해 속도는 빠르지만 정확도는 떨어진다는 특징을 가진 프로토콜이다. 연결 요청 및 수락 과정 없이 발신자가 일방적으로 수신자에게 데이터를 보내기 때문에 속도가 빠르다는 이점을 가진다. 하지만 고정이 회선이 아닌 여러 회선을 통해 데이터가 전송되기 때문에 회선 간 속도 차이로 인해 데이터가 순서대로 전달되지 않거나 잘못된 회선을 사용하여 데이터가 손실되는 등의 문제점이 발생할 수 있다. 따라서 UDP는 주로 실시간 영상 스트리밍 같이 한 컷 정도의 손실이 있더라도 무관하지만 속도가 중요한 프로그램에 사용된다. 

 UDP 네트워킹을 위해서 java.net 패키지에서 DatagramSocket과 DatagramPacket 클래스를 제공한다. 

클래스 설명 주요 메소드
DatagramSocket  발신점과
수신점

void receive(DatagramPacket receivePakcet) 데이터를 수신할 때까지 블로킹되고, 데이터가 수신되면 매개값으로 주어진 DatagramPacke에 저장
void send(DatagramPacket sendPacket) sendPacket을 보냄
void close() UDP 서버 종료
DatagramPacket  주고 받는
데이터

byte[] getData() 수신된 데이터를 byte 배열로 리터
int getLength() 수신된 바이트 배열의 길이를 리턴
SocketAddress getSocketAddress() 클라이언트 정보(IP, Port 번호 등)가 담긴 SocketAddress 객체 리턴

 

 

1. UDP 서버 

 UDP 서버 프로그램 개발을 위해서는 클라이언트와의 연결을 위한 DatagramSocket클라이언트에서 받은 데이터를 저장하고 보낼 데이터를 생성할 DatagramPakcet 객체를 생성해야 한다. 이러한 과정은 다음과 같다. 

 

  • DatagramSocket 객체 생성
DatagramSocket datagramSocket = new DatagramSocket(50001);

 

  • 클라이언트로부터 데이터 받기

 클라이언트로부터 데이터를 전달받아 저장하기 위해서는 우선 전달받을 Packet을 생성해야 한다. Packet을 생성할 때는 생성자에 데이터를 저장할 바이트 배열과 전달받을 크기(주로 바이트 배열의 크기)를 매개변수로 입력한다. 

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

 

 데이터를 전달받을 패킷을 생성했다면, 클라이언트로부터 데이터를 전달받을 준비를 해야 한다. 앞서 언급했듯 UDP 서버는 연결 요청이나 수락 과정 없이 일방적으로 발신자가 수신자에게 데이터를 보내기 때문에 UDP 서버는 클라이언트가 보낸 DatagramPacket을 항상 받을 준비를 해야 한다. 이 역할은 receive() 메소드가 한다. 데이터를 수신할 때까지 블로킹 되다가, 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다. 

datagramSocket.receive(receivePacket);

 

 이렇게 수신받은 packet으로부터 byte 배열을 얻어서 확인할 수 있다.

byte[] bytes = receivePacket.getData();	// 수신된 데이터
int num = receivePacket.getLength();	// 수신된 데이터의 길이

String data = new String(bytes, 0, num, "UTF-8");	// 데이터를 string으로

 

  • 클라이언트에게 데이터 보내기

 반대로 UDP 서버가 클라이언트에게 처리 내용을 보내려면 클라이언트 IP 주소와 Port 번호가 필요하다. 이는 앞선 과정에서 클라이언트에게 받은 DatagramPacket 객체로부터 얻을 수 있다. 

SocektAddress socketAddress = receivePacket.getSocketAddress();

 

 클라이언트 정보를 얻었으면, 이제 클라이언트에게 보낼 DatagramPacket 객체를 생성할 수 있다. 전송할 DatagramPacket을 생성할 때는 앞서 수신할 packet을 생성했던 것과는 달리, 보낼 내용을 담은 바이트 배열과 클라이언트의 정보를 담은 socketAddress를 생성자의 매개 변수로 주어야 한다. 

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

 

 마지막으로 생성한 DatagramPacket을 보내주면 클라이언트에게 데이터를 전송하는 과정이 마무리된다.

datagramSocket.send(sendPacket);

 

  • 클라이언트와의 연결 끊기

  마지막으로 클라이언트의 데이터를 더 이상 수신하지 않고 싶으면 UDP 서버를 종료하면 된다.

datagramSocket.close();

 

 

 

2. UDP 클라이언트 

 UDP 클라이언트는 서버에 요청 내용을 보내고 결과를 받는 역할을 한다. 클라이언트 역시 서버와 동일하게 서버와의 연결을 위한 DatagramSocket요청 내용과 받은 결과의 내용을 저장할 DatagramPacket 객체를 이용한다. 

 

  • DatagramSocket 객체 생성

 클라이언트를 위한 DatagramSocket은 별도의 Port 번호 없이 기본 생성자로 생성하여, Port 번호를 자동으로 부여받는다. 

DatagramSocket datagramSocket = new DatagramSocket();

 

  • 서버에 요청 내용 보내기

 요청 내용을 보내기 위해서는 우선 요청 내용을 담은 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(datagramPacket);

 

 

  • 서버에서 온 결괏값 받기

 앞서 서버에서 클라이언트로부터 언제 요청이 올지 모르니 항상 대기 상태로 준비했던 것처럼 클라이언트 역시 서버에서 언제 결과가 올지 모르니 항상 받을 준비를 해야 한다. 결괏값을 수신하는 과정은 서버와 동일하다.

DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
datagramSocket.receive(receivePacket);
String news = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");

 

  • 서버와의 연결 끊기

 더 이상 서버와 통신하지 않아도 된다면, 서버와의 연결을 끊는다. 

datagramSocket.close();

 

 

 

 

 스레드풀을 이용한 다수의 클라이언트 요청 처리 

 일반적으로 서버는 다수의 클라이언트와 통신한다. 하지만 이렇게 되며 먼저 연결한 클라이언트의 요청 처리 시간이 길어질수록 다음 클라이언트의 요청 처리 작업이 지연될 수 있다. 따라서 accept()와 receive()를 제외한 요청 처리 코드는 별도의 스레드에서 작업하는 것이 좋다. 

 하지만 만약 클라이언트가 폭증하면 서버가 과도한 스레드를 생성할 가능성이 있다. 따라서 이를 방지하기 위해 스레드풀을 사용하는 것이 바람직하다. 다만 작업 큐의 대기 작업이 증가되어 클라이언트에서 응답을 늦게 받을 가능성을 감수해야 한다.

 

 TCP 서버에서 스레드풀을 사용하는 예시는 아래와 같다. 

Socket socket = serverSocket.accept();	// accept는 스레드풀 밖에서 진행

executorService.execute(() -> {	// 스레드풀에서 나머지 과정 진행
    // 클라이언트 정보 얻기
    // 데이터 받기
    // 데이터 봬기
    // 연결 끊기
});

 

 UDP 서버에서 스레드풀을 사용하는 예시는 아래와 같다.

datagramSocket.receive(receivePacket);

executorService.execute(() -> {
	// 클라이언트 정보 얻기
    // 클라이언트에 데이터 전송
});

 

 

 JSON 데이터 형식 

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

 

 Java에서 JSON 데이터 형식을 만들 때는 라이브러리를 사용한다. 내가 공부하는 책에서 안내해 준 가장 많이 사용하는 라이브러리는 아래 링크였다. 아래로 스크롤하여 " Click here if you just want the latest release jar file."을 클릭하면 된다.

https://github.com/stleary/JSON-java?tab=readme-ov-file

 

GitHub - stleary/JSON-java: A reference implementation of a JSON package in Java.

A reference implementation of a JSON package in Java. - stleary/JSON-java

github.com

 

 책에서는 이클립스를 사용하고 있어, 이클립스에서 라이브러리를 어떻게 설정하는지를 설명했지만, 나는 Intellij를 사용하고 있기 때문에 따로 검색하여 설정해 주었다. 내가 참고한 글은 아래와 같다.

https://inpa.tistory.com/entry/IntelliJ-%F0%9F%92%BD-%EC%9E%90%EB%B0%94-%EC%99%B8%EB%B6%80-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EA%B0%84%EB%8B%A8-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0

 

💽 IntelliJ - 외부 jar 라이브러리 간단 추가하기

인텔리제이 자바 라이브러리 추가 방법 현재 자바 프로젝트에 쓰일 외부 라이브러리를 인텔리제이 IDE를 통해 추가하는 방법이다. 1. 파일(File) → 프로젝트 구조(Project Structure) 2. 모듈(Modules) →

inpa.tistory.com

 

 

 이렇게 JSON 라이브러리까지 설정해주고 나면 클래스들을 이용하여 JSON 데이터 형식을 사용할 수 있게 된다. JSON은 아래와 같은 형식으로 각각 JSONObject와 JSONArray 클래스를 이용하여 사용된다. 각각의 클래스의 객체를 만들고 put() 메소드를 이용하여 속성 또는 항목을 추가하게 된다.

표기 대상 형식 value로 가능한 것 사용 클래스
객체 표기 {
    "속성명": 속성값,
    "속성명": 속성값,
    ...
}
속성값으로 가능한 것
- 문자열, 숫자, boolean
- 객체 { ... }
- 배열
JSONObject
배열 표기 [ 항목, 항목, ... ] 항목으로 가능한 것
- 문자열, 숫자, boolean
- 객체 { ... }
- 배열
JSONArray

 

728x90