Preface
이번 장은 네트워크와 관련된 내용들이다.
네트워크의 개념 자체는 생활코딩 강의를 통해 공부한 적이 있어 쉽게 이해할 수 있었지만, 이를 활용해 직접 코드를 작성하는 것은 쉽지 않은 것 같다.
다음 장을 공부하기 전에 직접 간단한 채팅 프로그램을 구현해볼까 한다.
5. 네트워크 기초
- 네트워크: 여러 대의 컴퓨터를 통신 회선으로 연결한 것
1) 지역 네트워크: 특정 영역에 존재하는 컴퓨터를 통신 회선으로 연결한 것
2) 인터넷: 지역 네트워크를 통신 회선으로 연결한 것
- 서버: 서비스를 제공하는 프로그램
→ 클라이언트가 요청하는 내용을 처리해주고, 응답을 클라이언트로 보낸다.
- 클라이언트: 서비스를 받는 프로그램
- 두 프로그램이 통신하기 위해선 연결을 요청하는 역할과 수락하는 역할이 필요하다.
- 클라리언트/서버(C/S: client/server) 모델은 한 개의 서버와 다수의 클라이언트로 구성 되는 것이 일반적이다.
→ P2P(peer to peer) 모델: 두 개의 프로그램이 서버인 동시에 클라이언트 역할을 하는 모델로, 먼저 접속을 시도한 쪽이 클라이언트가 된다.
- IP(Internet Protocol): 컴퓨터의 고유한 주소
→ 네트워크 어댑터(Lan 카드)마다 할당되므로, 네트워크 어댑터 수만큼 IP 주소를 할당할 수 있다.
- 연결할 상대방 컴퓨터의 IP 주소를 모르면 프로그램들은 통신을 할 수 없다.
- 프로그램은 DNS(Domain Name System)를 이용해서 연결할 컴퓨터의 IP 주소를 찾는다.
→ 대부분의 서버는 DNS에 도메인 이름으로 IP를 등록해놓으므로 사용자는 도메인 이름만 알면 해당 IP 주소로 접근할 수 있다.
- 한 개의 컴퓨터에는 다양한 서버 프로그램들이 실행될 수 있으므로, 클라이언트는 어떤 서버와 통신해야 할지 결정해야 한다.
- 포트
1) 포트 번호: 컴퓨터 내에서 실행하는 서버를 선택하기 위한 추가적인 정보
2) 포트 바인딩: 서버가 시작할 때 고정적인 포트 번호를 가지는 것
- 클라이언트도 서버에서 보낸 정보를 받기 위해 포트 번호가 필요하다.
→ 고정적인 번호가 아닌, OS에서 자동으로 부여하는 동적 포트 번호를 사용하며, 이는 클라이언트가 서버로 연결 요청을 할 때 전송되어 서버가 클라이언트로 데이터를 보낼 때 사용된다.
- 자바는 IP 주소를 java.net.InetAddress 객체로 표현한다.
- InetAddress: 로컬 컴퓨터의 IP주소 및 도메인 이름을 DNS에서 검색한 후 IP 주소를 가져오는 기능을 제공
1) InetAddress.getLocalHost( ) 메소드: 로컬 컴퓨터의 InetAddress를 리턴한다.
InetAddress ia = InetAddress.getLocalHost();
2) 외부 컴퓨터의 도메인 이름을 알고 있는 경우 InetAddress 객체를 얻는 방법
//매개값으로 준 도메인 이름으로 DNS에서 단 하나의 IP 주소를 얻어 IndetAddress를 생성하고 리턴
InetAddress ia = InetAddress.getByName(String host);
//DNS에 등록된 모든 IP 주소를 얻어 InetAddress[] 배열로 리턴
InetAddress iaArr[] = InetAddress.getAllByName(String host);
//InetAddress 객체에서 IP 주소를 리턴
String ip = InetAddress.getHostAddress();
package ch18;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressExample {
public static void main(String[] args) {
try {
InetAddress local = InetAddress.getLocalHost();
System.out.println("my computer IP address: " + local.getHostAddress());
InetAddress[] iaArr = InetAddress.getAllByName("www.naver.com");
for (InetAddress remote : iaArr) {
System.out.println("IP address for www.naver.com: " + remote.getHostAddress());
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
6. TCP 네트워킹
- TCP(Transmission Control Protocol): 연결 지향적 프로토콜
→ 클라이언트와 서버가 연결된 상태에서 데이터를 주고받는 프로토콜
1) 데이터를 정확하고 안정적으로 전달
2) 데이터를 보내기 전에 반드시 연결이 형성되어야 함
3) 고정된 통신 선로가 최단선이 아닐 경우 상대적으로 UDP보다 데이터 전송 속도가 느림
- TCP 서버의 역할: 클라이언트가 연결 요청을 하면 ServerSocket은 연결을 수락하고 통신용 Socket을 만든다.
1) 클라이언트가 연결 요청을 해오면 연결을 수락하는 것: java.net.ServerSocket 클래스
2) 연결된 클라이언트와 통신하는 것: java.net.Socket 클래스
- 바인딩 포트: 클라이언트가 접속할 포트
1) 서버는 고정된 포트 번호에 바인딩해서 실행하므로, ServerSocket을 생성할 때 포트 번호 하나를 지정해야 한다.
2) ServerSocket은 클라이언트가 연결 요청을 하면 accept( ) 메소드로 연결 수락을 하고 통신용 Socket을 생성한다.
- ServerSocket을 얻는 방법
1) 생성자에 바인딩 포트를 대입하고 객체를 생성
ServerSocket serverSocket = new ServerSocket(5001);
2) 디폴트 생성자로 객체를 생성하고 포트 바인딩을 위해 bind( ) 메소드를 호출하는 것
→ bind( ) 메소드의 매개값은 포트 정보를 가진 InetSocketAddress이다.
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(5001));
3) 서버 PC에 멀티 IP가 할당되어 있을 경우 특정 IP로 접속할 때만 연결 수락을 하고 싶은 경우
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("IP주소", 5001));
- ServerSocket을 생성할 때 해당 포트가 이미 다른 프로그램에서 사용중이면 BindException이 발생한다.
1) 다른 포트로 바인딩한다.
2) 다른 프로그램을 종료하고 다시 실행한다.
- accept( ) 메소드는 클라이언트가 연결 요청하기 전까지 블로킹된다.
→ 블로킹: 스레드가 대기 상태가 되는 것
- 클라이언트가 연결 요청을 하면 accept( )는 클라이언트와 통신할 Socket을 만들고 리턴한다.
→ accept( )에서 블로킹되어 있을 때 ServerSocket에 close( ) 메소드를 호출하면 SocketException이 발생하므로 예외 처리가 필요하다.
try {
Socket socket = serverSocket.accept();
} catch(Exception e) {}
- 연결된 클라이언트의 IP와 포트 정보를 알고 싶다면 Socket의 getRemoteSocketAddress( ) 메소드를 호출해 SocketAddress를 얻으면 된다.
→ 실제로 리턴되는 것은 InetSocketAddress 객체이므로 타입 변환을 할 수 있다.
InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
- InetSocketAddress의 메소드
리턴 타입 | 메소드명 | 설명 |
String | getHostName( ) | 클라이언트 IP 리턴 |
Int | getPort( ) | 클라이언트 포트 번호 리턴 |
String | toString( ) | "IP:포트번호" 형태의 문자열 리턴 |
- 더 이상 클라이언트 연결 수락이 필요 없으면 close( ) 메소드를 호출해 포트를 언바인딩 시켜야 한다.
- 반복적으로 accept( ) 메소드를 호출해 다중 클라이언트 연결을 수락하는 코드
package ch18;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerExample {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", 5001));
while (true) {
System.out.println("[waiting connection]");
Socket socket = serverSocket.accept();
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
System.out.println("[connection is permited]" + isa.getHostName());
}
} catch (Exception e) {
}
if (!serverSocket.isClosed()) {
try {
serverSocket.close();
System.out.println("[quit serverSocekt]");
} catch (IOException e1) {
}
}
}
}
- Socket 객체를 생성함과 동시에 연결 요청을 하라면 생성자의 매개값으로 서버의 IP 주소와 바인딩 포트 번호를 제공하면 된다.
1) 외부 서버에 접속하려면 localhost 대신 정확한 IP를 입력하면 된다.
2) IP 대신 도메인 이름만 알고 있다면 도메인 이름을 IP 주소로 번역해야 하므로 InetSocketAddress 객체를 이용하는 방법을 사용해야 한다.
try {
Socket socket = new Socket("localhost", 5001); //방법1
Socket socket = new Socket(new InetSocketAddress("localhost", 5001)); //방법2
} catch(UnknownHostException e) {
//IP 표기 방법이 잘못된 경우
} catch(IOException e) {
//해당 포트의 서버에 연결할 수 없는 경우
}
- 기본 생성자로 Socket을 생성한 후 connect( ) 메소드로 연결 요청을 할 수도 있다.
socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 5001));
- 연결 요청 예외
1) UnknownHostException: 잘못 표기된 IP 주소를 입력한 경우 발생
2) IOException: 주어진 포트로 접속할 수 없을 경우 발생
- 연결된 후 클라이언트 프로그램을 종료하거나 강제적으로 연결을 끊고 싶을 땐 close( ) 메소드를 호출하면 된다.
→ IOException 예외 처리가 필요하다.
- localhost 5001 포트로 연결을 요청하는 코드
package ch18;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
public class ClientExample {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket();
System.out.println("[requiring connection]");
socket.connect(new InetSocketAddress("localhost", 5001));
System.out.println("[connected]");
} catch (Exception e) {
}
if (!socket.isClosed()) {
try {
socket.close();
System.out.println("[quit connection]");
} catch (IOException e1) {
}
}
}
}
- 클라이언트가 연결 요청을 한 후 서버가 수락했다면, 양쪽의 Socket 객체로부터 각각 입력 스트림과 출력 스트림을 얻을 수 있다.
- Socket으로부터 InputStream과 OutputStream을 얻는 코드
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputstream();
- 상대방에게 데이터를 보내기 위해선 보낼 데이터를 byte[ ] 배열로 생성한 후, 이것을 매객밧으로 OutputStream의 write( ) 메소드를 호출하면 된다.
- 상대방이 보낸 데이터를 받기 위해선 받은 데이터를 저장할 byte[ ] 배열을 하나 생성한 후, 이것을 매개값으로 InputStream의 read( ) 메소드를 호출하면 된다.
- 데이터를 주고받는 코드
package ch18;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
public class ClientExample2 {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket();
System.out.println("[requiring connection]");
socket.connect(new InetSocketAddress("localhost", 5001));
System.out.println("[connected]");
byte bytes[] = null;
String message = null;
OutputStream os = socket.getOutputStream();
message = "Hello Server";
bytes = message.getBytes("UTF-8");
os.write(bytes);
os.flush();
System.out.println("[sending data success]");
InputStream is = socket.getInputStream();
bytes = new byte[100];
int readByteCount = is.read(bytes);
message = new String(bytes, 0, readByteCount, "UTF-8");
System.out.println("[receiving data success]" + message);
os.close();
is.close();
} catch (Exception e) {
}
if (!socket.isClosed()) {
try {
socket.close();
System.out.println("[quit connection]");
} catch (IOException e2) {
}
}
}
}
package ch18;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerExample2 {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", 5001));
while (true) {
System.out.println("[waiting connection]");
Socket socket = serverSocket.accept();
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
System.out.println("[connection is permited]" + isa.getHostName());
byte bytes[] = null;
String message = null;
InputStream is = socket.getInputStream();
bytes = new byte[100];
int readByteCount = is.read(bytes);
message = new String(bytes, 0, readByteCount, "UTF-8");
System.out.println("[receiving data success]" + message);
OutputStream os = socket.getOutputStream();
message = "Hello client";
bytes = message.getBytes("UTF-8");
os.write(bytes);
os.flush();
System.out.println("[sending data success]");
is.close();
os.close();
socket.close();
}
} catch (Exception e) {
}
if (!serverSocket.isClosed()) {
try {
serverSocket.close();
System.out.println("[quit serverSocekt]");
} catch (IOException e2) {
}
}
}
}
- InputStream의 read( ) 메소드를 호출하면 상대방이 데이터를 보내기 전까지는 블로킹된다.
- read( ) 메소드가 블로킹 해제되고 리턴되는 경우
1) 상대방이 데이터를 보냄
2) 상대방이 정상적으로 Socket의 close( ) 를 호출
3) 상대방이 비정상적으로 종료
→ 상대방이 정상적으로 연결을 끊었을 경우와 비정상적으로 종료했을 경우 모두 예외 처리를 해서 이쪽도 Socket을 닫기 위해 close( ) 메소드를 호출해야 한다.
- ServerSocket과 Socket은 동기(블로킹) 방식으로 구동된다.
→ accept( ), connect( ), read( ), write( ) 메소드는 별도의 작업 스레드를 생성해서 병렬적으로 처리하는 것이 좋다.
- 클라이언트의 폭증으로 인해 서버의 과도한 스레드 생성을 방지하려면 스레드풀을 사용하는 것이 바람직하다.
7. UDP 네트워킹
- UDP(User Datagram Protocol): 비연결 지향적 프로토콜
→ 데이터를 주고받을 때 연결 절차를 거치지 않고, 발신자가 일방적으로 데이터를 발신하는 방식
1) TCP보다 빠른 전송을 할 수 있지만, 데이터 전달의 신뢰성은 떨어진다.
2) 먼저 보낸 패킷이 느린 선로를 통해 전송될 경우 나중에 보낸 패킷보다 늦게 도착할 수도 있다.
3) 일부 패킷은 잘못된 선로로 전송되어 잃어버릴 수도 있다.
- 데이터 전달의 신뢰성보다 속도가 중요한 경우엔 UDP를, 데이터의 신뢰성이 중요한 경우엔 TCP를 사용한다.
- 자바의 UDP 프로그래밍
1) java.net.DatagramSocket: 발신점과 수신점에 해당하는 클래스
2) java.net.DatagramPacket: 주고받는 패킷 클래스
- DatagramSocket 객체 생성
DatagramSocket datagramSocket = new DatagramSocket();
- 보내고자 하는 데이터를 byte[ ] 배열로 생성한다.
- DatagramPacket 생성자의 첫 번째 매개값은 보낼 데이터인 byte[ ] 배열, 두 번째 매개값은 byte[ ] 배열에서 보내고자 하는 항목 수, 세 번째 매개값은 수신자 IP와 포트 정보를 가지고 있는 SocketAddress이다.
→ SocketAddress는 추상 클래스이므로 하위 클래스인 InetSocketAddress 객체를 생성해서 대입하면 된다.
- DatagramPacket을 생성했다면, 이것을 매개값으로 DatagramSocket의 sent( ) 메소드를 호출하면 수신자에게 데이터가 전달된다.
- 더 이상 보낼 데이터가 없을 경우엔 close( ) 메소드를 호출해 DatagramSocket을 닫는다.
- 발신자 프로그램 코드
package ch18;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class UdpSendExample {
public static void main(String[] args) throws Exception {
DatagramSocket datagramSocket = new DatagramSocket();
System.out.println("[start sending]");
for (int i = 0; i < 3; i++) {
String data = "message" + i;
byte byteArr[] = data.getBytes("UTF-8");
DatagramPacket packet = new DatagramPacket(byteArr, byteArr.length,
new InetSocketAddress("localhost", 5001));
datagramSocket.send(packet);
System.out.println("[numbers of sending data]: " + byteArr.length + "bytes");
}
System.out.println("[quit sending]");
datagramSocket.close();
}
}
- 수신자로 사용할 DatagramSocket 객체는 바인딩할 포트 번호를 매개값으로 지정하고 생성해야 한다.
DatagramSocket datagramSocket = new DatagramSocket(5001);
- DatagramSocket이 생성되었다면 receive( ) 메소드를 호출해서 패킷을 읽을 준비를 해야 한다.
- DatagramPacket 생성자의 첫 번째 매개값은 읽은 패킷 데이터를 저장할 바이트 배열, 두 번째 매개값은 읽을 수 있는 최대 바이트 수로 첫 번째 바이트 배열의 크기와 같거나 작아야 한다.
- 패킷을 읽은 후엔 DatagramPacket의 getData( ) 메소드로 데이터가 저장된 바이트 배열을 얻을 수 있다.
1) getLength( )를 호출해 읽은 바이트 수를 얻을 수 있다.
2) 인코딩된 문자열을 받았다면 디코딩해서 문자열을 얻으면 된다.
- 수신자가 발신자에게 응답 패킷을 보내려면 발신자의 IP와 포트를 알아야 한다.
→ DatagramPacket의 getSocketAddress( )를 호출하면 SocketAddress 객체를 얻을 수 있다.
- 수신자는 작업 스레드를 생성해서 receive( ) 메소드를 반복적으로 호출해야 한다.
- 작업 스레드를 종료시키려면 receive( ) 메소드가 블로킹된 상태에서 DatagramSocket의 close( )를 호출하면 된다.
→ receive( ) 메소드에서 SocketException이 발생하므로 예외 처리 코드에서 작업 스레드를 종료시키면 된다.
- 수신자 프로그램 코드
package ch18;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpReceiveExample extends Thread {
public static void main(String[] args) throws Exception {
DatagramSocket datagramSocket = new DatagramSocket(5001);
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("[start receiving]");
try {
while (true) {
DatagramPacket packet = new DatagramPacket(new byte[100], 100);
datagramSocket.receive(packet);
String data = new String(packet.getData(), 0, packet.getLength(), "UTF-8");
System.out.println("[contents of reveiving data: " + packet.getSocketAddress() + "] " + data);
}
} catch (Exception e) {
System.out.println("[quit receiving]");
}
}
};
thread.start();
Thread.sleep(10000);
datagramSocket.close();
}
}
'Java > 이것이 자바다' 카테고리의 다른 글
Java 기본 개념 (0) | 2023.09.13 |
---|---|
(Fin) NIO 기반 입출력 및 네트워킹 (0) | 2023.05.23 |
IO 기반 입출력 (0) | 2023.05.12 |
이자바 16장(스트림과 병렬 처리) 확인문제 (0) | 2023.05.07 |
스트림과 병렬 처리 (0) | 2023.05.07 |
댓글