19. 네트워크 입출력
19.1 네트워크 기초
네트워크는 여러 컴퓨터를 통신회선으로 연결한 것을 말한다.
LAN은 가정 회사 건물 특정영역에 존재하는 컴퓨터를 연결한 것이고
WAN은 LAN을 연결한 것이다. WAN이 우리가 흔히 말하는 인터넷이다.
19.1.1 서버와 클라이언트
네트워크에서 유무선으로 컴퓨터가 연결되어 있다면 실제러 데이터를 주고받는 행위는 프로그램들이 한다.
서비스를 제공하는 프로그램을 일반저긍로 서버라고 부르고
서비스를 요청하는 프로그램을 클라이언트 라고 부른다.
인터넷에서 두 프로그램이 통신하기 위해서는 먼저 클라이언트가 서비스를 요청하고 서버는 처리결과를 응답으로 제공해준다.
19.1.2 IP주소
컴퓨터에 고유한 주소가 있다. 이것이 바로 IP주소 이다.
IP주소는 LAN카드마다 할당된다. 만약 컴퓨터에 두개의 네트워크 어댑터가 장착되어있다면 두개의 IP주소를 할당 받을 수 있다.
프로그램은 DNS(Domain Name System)를 이용해서 컴퓨터의 IP주소를 검색한다.
DNS는 도메인 이름으로 IP를 등록하는 저장소이다. 대중에게 서비스를 제공하는 대부분의 컴퓨터는 다음과 같이 도메인 이름으로 IP를 DNS에 미리 등록한다.
웹브라우저는 웹서버와 통신하는 클라이언트로 사용자가 입력한 도메인 이름으로 DNS에서 IP주소를 검색해 찾은 다음 웹서버와 연결해서 웹페이지를 받는다.
19.1.3 Port번호
한대의 컴퓨터에는 다양한 서버 프로그램들이 실행될 수있다.
예를들어 웹서버, 데이터베이스 관리시스템 FTP서버 등이 하나의 IP주솔르 갖는 컴퓨터에서 동시에 실행될 수 있다.
이 경우 클라이언트는 어떤 서버와 통신해야할지 결정해야한다.
IP는 컴퓨터의 네트워크 어댑터까지만 갈 수 있는 정보이기때문에 컴퓨터 내부에서 실행하는 서버를 선택하기 위해서는 추가적인 Port번호가 필요하다.
Port는 운영체제가 관리하는 서버프로그램의 연결번호이다. 서버는 시작할때 특정 Port번호에 바인딩한다.
클라이언트도 서버에 보낸 정보를 받기 위해서 Port번호가 필요한데 서버와 같이 고정적인 Port번호에 바인딩하는게 아니라 운영체제가 자동으로 부여하는 번호를 사용한다.
이번호는 클라이언트가 서버로 요청할때 함께 전송되어 서버가 클라이언트로 데이터를 보낼때 사용된다.
프로그램에서 사용할 수 있는 Port번호는 사용목적에 따라 3가지 범위를 가진다.
Well Know Port Numbers | 01023 | 국제인터넷주소관리기구(ICANN)이 특정 애플리케이션용으로 미리예약한 Port49151 | 회사에서 등록해서 사용할수 있는 Port
Registered Port Numbers | 1024
Dynamic Or Private Port Numbers | 49152~65535| 운영체제가 부여하는 동적 Port 또는 개인적인 목적으로 사용할수 있는 Port
19.2 IP주소 얻기
자바는 IP주소를 java.net 패키지의 InetAddress로 표현한다.
InetAddress를 이용하면 로컬 컴퓨터의 IP주솔르 얻을 수 있고 도메인 이름으로 DNS를 검색한 후 IP주소를 가져올 수도 있다
로컬 컴퓨터의 InetAddress를 얻고 싶다면 InetAddress.getLocalHost()메소드를 호출하면된다.
InetAddress ia = InetAddress.getLocalHost();
만약 컴퓨터의 도메인 이름을 알고있다면 getByName()메소드르 사용하면된다.
하나의 도메인 이름으로 여러 IP가 등록되어 있는 경우도 있는데 클라이언트가 많이 연결되엇을경우 서버 부하를 나누기 위해서이다.
InetAddress ia = InetAddress.getByName(String domainName);
InetAddress[] iaArr = InetAddress.getAllByName(String domainName);
이 메소드를로부터 얻은 InetAddress객체에서 IP주소를 얻으려면 getHostAddress()메소드를 호출해야한다.
리턴값은 문자열로된 IP주소이다.
String ip = InetAddress.getHostAddress();
네이버와 내 IP주소를 얻어보자.
public class InetAddressExample {
public static void main(String[] args) {
try {
InetAddress local = InetAddress.getLocalHost();
System.out.println("내 컴퓨터 IP주소: " + local.getHostAddress());
InetAddress[] iaArr = InetAddress.getAllByName("www.naver.com");
for (InetAddress remote : iaArr) {
System.out.println("www.naver.com IP주소: " + remote.getHostAddress());
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}19.3 TCP 네트워킹
IP주소로프로그램들이 통신할때는 약속된 데이터 전송규약이 있다.
이것을 전송용 프로토콜이라고 부른다. 인터넷에서 프로토콜은 TCP와 UDP가 있다.
TCP는 연결형 프로토콜로 상대방이 연결된 상태에서 데이터를 주고받는다.
클라이언트가 연결 요청을 하고 서버가 연결 수락을 하면 통신 회선이 고정되고 데이터는 고정회선을 통해 전달된다.
그래서 TCP는 보낸 데이터가 순서대로 전달되며 손실이 발생하지 않는다.
TCP는 IP와 함께 사용하기 때문에 TCP/IP라고도 한다. TCP는 웹브라우저가 웹서버에 연결할때 사용되며 이메일전송 파일전송 DB연동에도 사용된다.
자바는 TCP네트워킹을 위해 java.net 패키지에서 ServerSocket과 Socket클래스를 제공하고 있다.
ServerSocket은 클라이언트의 연결을 수락하는 서버쪽 클래스이고
Socket은 클라이언트에서 연결 요청할때와 클라이언트와 서버 양쪽에서 데이터를 주고받을때 사용되는 클래스이다.
ServerSocket을 생성할때는 바인딩할 Port번호를 지정해야한다.
서버가 실행되면 클라이언트는 Socket을 이용해서 서버의 IP주소와 Port번호로 연결요청을 할 수 있다.
ServerSocket은 accept() 메소드로 연결 수락을 하고 통신용 Socket을 생성한다.
그리고 나서 클라이언트와 서버는 양쪽의 Socket을 이용해서 데이터를 주고받는다.
19.3.1 TCP서버
TCP서버 프로그램을 개발하려면 우선 ServerSocket객체를 생성해야한다.
생성자에 포트번호를 넣고 Port에 바인딩해야한다.
ServerSocket serverSocket = new ServerSocket(50001);
ServerSocket을 생성하는 또 다른 방법은 기본 생성자로 객체를 생성하고 Port바인딩을 위해 bind()메소드를 호출하는 것이다.
만약 서버 컴퓨터에 여러 개의 IP가 할당되어 있을 경우
특정 IP에서만 서비스를 하고 싶다면 InetSocketAddress의 첫번째 매개값으로 해당 IP를 주면된다.
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(50001));
serverSocket.bind(new InetSocketAddress("xxx.xxx.xxx.xxx",50001));
만약 Port가 이미 다른 프로그램에서 사용중이라면 BindException이 발생한다.
이 경우는 다른 포트로 바인딩하거나 사용중인 프로그램을 종료하고 다시 실행하면된다.
ServerSocket이 생성되었다면 연결 요청 수락을 위해 accpet()메소드를 실행해야한다.
accept()는 클라이언트가 연결 요청하기 전까지 블로킹된다. 블로킹이란 실행을 멈춘상태라는 뜻이다.
클라이언트의 연결 요청이 들어오면 블로킹이 해제되고 통신용 Socket을 리턴한다.
Socket socket = serverSocket.accpet();
만약 리턴된 Socket을 통해 연결된 클라이언트의 IP주소와 Port번호를 얻고 싶다면
getRemoteSocketAddress()메소드르 호출해서 InetSocketAddress를 얻고 getHostName()과 getPort 메소르를 호출하면 된다.
InetSocketAddress isa = (InetSocketAddress) socketgetRemoteSocketAddress();
String clientIp = isa.getHostName();
String portNo = isa.getPort();
서버를 종료하려면 serverSocket.close()를 해서 포트번호를 언바인딩해야핟나.
그래야 다른 프로그램에서 해당 포트번호를 재사용할 수 있다.
public class ServerExample {
private static ServerSocket serverSocket = null;
public static void main(String[] args) {
System.out.println("---------------------------------------");
System.out.println("서버 종료하려면 q또는 Q를 입력하고 엔터");
System.out.println("---------------------------------------");
// TCP서버시작
startServer();
// 키보드 입력
Scanner sc = new Scanner(System.in);
while (true) {
String key = sc.nextLine();
if (key.toLowerCase().equals("q")) {
break;
}
}
sc.close();
// TCP서버종료
stopServer();
}
public static void startServer() {
// 작업스레드 정의
Thread thread = new Thread(() -> {
try {
// ServerSocket생성 및 Port바인딩
serverSocket = new ServerSocket(50001);
System.out.println("[서버] 시작됨");
// 서버는 요청을 계속 기다려야함.
while (true) {
System.out.println("\n[서버] 연결 요청을 기다림\n");
// 연결 수락
Socket socket = serverSocket.accept();
// 연결된 클라이언트 정보얻기
InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
System.out.println("[서버]" + isa.getHostName() + "의 연결 요청을 수락함");
// 연결끊기
socket.close();
System.out.println("[서버]" + isa.getHostName() + "의 연결을 끊음");
}
} catch (IOException e) {
System.out.println("[서버]" + e.getMessage());
}
});
// 스레드 시작
thread.start();
}
public static void stopServer() {
try {
// serverSocket을 닫고 포트 언바인딩
serverSocket.close();
System.out.println("[서버] 종료됨");
} catch (IOException e) {
e.printStackTrace();
}
}
}19.3.2 TCP클라이언트
클라이언트가 서버에 연결 요청을 하려면 Socket객체를 생성할때 생성자 매개값으로 서버 IP주소와 Port번호를 제공하면된다.
로컬 컴퓨터에서 실행하는 서버로 연결 요청을 할 경우에는 IP주소대선 localhost를 사용할 수 있다.
IP주소 대신 도메인 이름이라면 DNS에서 IP주소를 검색할 수 잇도록 생성자 매개값으로 InetAddress를 제공해야한다.
Socket socket = new Socket("IP", 50001);
Socket socket = new Socket(new InetAddress.getByName("domainName", 50001));
Socket생성과 동시에 연결요청을 하지 않고 기본생성자로 Socket생성후 connet()메소드로 연결요청을 할 수도 있다.
socket = new Socket();
scoekt.connect(new InetSocketAddress("domainName", 50001));
연결요청시 IP주소가 잘못표기되엇을때 UnknownHostException예외가 발생하고
제공된 IP와 Port번호로 연결할 수 없을때 IOException이 발생한다.
연결을 끊고싶다면 socket.close();
public class ClientExample {
public static void main(String[] args) {
try {
// localhost 50001포트로 연결요청
Socket socket = new Socket("localhost", 50001);
System.out.println("[클라이언트] 연결 성공");
// Socket 닫기
socket.close();
System.out.println("[클라이언트] 연결 끊음");
} catch (UnknownHostException e) {
// IP 표기방법이 잘못되엇을 경우
} catch (IOException e) {
// 해당 포트의 서버에 연결할수 없는 경우
}
}
}19.3.3 입출력 스트림으로 데이터 주고받기
클라이언트가 연결 요청(connect())를 하고 서버가 연결수락(accept())햇다면
양쪽의 Socket객체로부터 각가가 입력스트림과 출력스트림을 얻을 수 있다.
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
상대방에게 데이터를 보낼때에는 보낼 데이터를 byte[]배열로 생성하고
이것을 매개값으로 write()메소드를 호출하면된다.
다음 코드는 문자열로부터 UTF-8로 인코딩한 바이트배열을 얻어내고 write()메소드로 전송한다.
String data = "보낼데이터";
byte[] bytes = data.getBytes("UTF-8");
OutputStream os = socket.getOutputStream();
os.write(bytes);
os.flush();
문자열을 더 간편하게 보내고 싶다면 보조스트림인 DataOutputStream을 연결해서 사용하면된다.
Stringn data = "보낼데이터";
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(data);
dos.flush();
데이터를 받기 위해서는 받은 데이터를 저장할 byte[]배열을 하나 생성하고
이것을 매개값으로 read()메소드를 호출하면된다.
받는 데이터가 문자열이라면 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");
문자열을 더 간편하게 받고 싶다면 보조스트림인 DataInputStream을 연결해서 사용하면된다.
단, 이방법은 상대방이 DataInputStream으로 문자열을 보낼때만 가능하다.
DataInputStream dis = new DataInputStream(socket.getInputStream());
String data = dis.readUTF();
받은 메시지를 다시 돌려보내는 에코 서버 - 클라이언트
서버 연결요청 수락 후 - 끊기전에 할일을 넣으면된다.
//서버 데이터 받기
InputStream is = socket.getInputStream();
byte[] receivebytes = new byte[1024]; //1kbyte
int readByteCount = is.read(receivebytes);
String message = new String(receivebytes, 0, readByteCount, "UTF-8");
//서버 데이터 보내기
OutputStream os = socket.getOutputStream();
byte[] sendbyte = message.getBytes("UTF-8");
os.write(sendbyte);
os.flush();
System.out.println("[서버] 받은데이터를 다시 보냄: " + message);//클라이언트 데이터 보내기
String sendMessage = "나는 자바가 싫어";
OutputStream os = socket.getOutputStream();
byte[] sendBytes = sendMessage.getBytes("UTF-8");
os.write(sendBytes);
os.flush();
System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);
//클라이언트 데이터 받기
InputStream is = socket.getInputStream();
byte[] receivebytes = new byte[1024];
int readByteCount = is.read(receivebytes);
String receiveMessage = new String(receivebytes, 0, readByteCount, "UTF-8");
System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);보조스트림을 사용하면 다음과 같다.
//서버 데이터 받기
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);//클라이언트 데이터 보내기
String sendMessage = "나는 자바가 싫어";
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(sendMessage);
dos.flush();
System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);
//클라이언트 데이터 받기
DataInputStream dis = new DataInputStream(socket.getInputStream());
String receiveMessage = dis.readUTF();
System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);19.4 UDP네트워킹
UDP는 발신자가 일방적으로 수신자에게 데이터를 보내는 방식이다.
TCP처럼 연결 요청 및 수락 과정이 없기 때문에 TCP보다 데이터 전송속도가 상대적으로 빠르다.
고정회선이아니라 여러회선을 통해 데이터가 전송되기 때문에 특정 회선 속도에 따라 데이터가 순서대로 전달되지 않거나
잘못된 회선으로 인해 데이터 손실이 발생할 수 있다.
하지만 실시간 영상 스트리밍에서 한 컷의 영상이 손실되더라도 영상은 계속되서 수신되므로 문제가 되지 않는다.
따라서 데이터전달의 신뢰성보다 속도가 더 중요하다면 UDP를 사용하고 데이터 전달의 신뢰성이 중요하다면 TCP를 사용해야한다.
java.net 패키지에서 DatagramSocket과 DatagramPacket클래스로 통신한다.
전자가 발신점과 수신점이고 후자는 주고받는 데이터이다.
19.4.1 UDP서버
UDP서버를 위한 DatagramSocket객체를 생성할때 포트번호를 생성자 매개값으로 제공해야한다.
DatagramSocket datagramSocket = new DatagramSocket(50001);
UDP서버는 클라이언트가 보낸 DatagramPacket을 항상 받을 준비를 해야한다.
이 역할을 하는 메소드가 recive()이다. recive()메소드는 데이터를 수신할때까지 블로킹되고 데이터가 수신되면 매개값으로 주어진 DatagramPacket에 저장한다.
DatagramPacket의 첫번째 매개값은 수신된 데이터를 저장할 배열이다.
두번째 매개값은 수신할 수 있는 최대 바이트 수이고 보통 첫번째 바이트배열의 크기를 준다.
DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1204);
datagramPacket.receive(receivePacket);
recive()메소드가 실행된후 수신된 데이터와 바이트 수를 얻는 방법은 다음과 같다.
byte[] bytes = receivePacket.getData();
int num = receivePacket.getLength();
읽는 데이터가 문자열이라면 String생성자를 이용해서 문자열을 얻을 수 있다.
String data = new String(bytes,0,num, "UTF-8");
서버가 클라이언트에게 처리 내용을 보내려면 클라이언트 IP주소와 포트번호가 필요하다.
receive()로 받은 DatagramPacket에서 얻을 수 있다. getSocketAddress()메소드를 호출하면 SocketAddress객체를 얻을 수 있다.
SocketAddress socketAddress = receivePacket.getSocketAddress();
이렇게 얻은 SocketAddress 객체는 클라이언트로 보낼 DatagramPacket을 생성할때 네번째 매개값으로 사용된다.
DatagramPacket 생성자의 첫번째 매개값은 바이트 배열이고 두번째는 사직인덱스 세번째는 보낼 바이트 수이다.
String data = "처리내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(bytes, 0, bytes.length, socketAddress);
보낼때는 DatagramSocket의 send()메소드, 끌땐 close()하면된다.
public static void startServer() {
// 작업 스레드 정의
Thread thread = new Thread(() -> {
try {
// datagramSocket 생성 및 포트바인딩
datagramSocket = new DatagramSocket(50001);
System.out.println("[서버] 시작됨");
// 계속 받고있어야함
while (true) {
// 클라이언트가 구독하고 싶은 뉴스 주제 얻기
DatagramPacket receivePacket = new DatagramPacket(new byte[1024], 1024);
datagramSocket.receive(receivePacket);
String newsKind = new String(receivePacket.getData(), 0, receivePacket.getLength(), "UTF-8");
// 클라이언트의 IP와 PORT얻기
SocketAddress socketAddress = receivePacket.getSocketAddress();
// 10개의 뉴스를 클라이언트로 전송
for (int i = 1; i <= 10; i++) {
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());
}
});
thread.start();
}19.4.2 UDP클라이언트
UDP클라이언트는 서버에 요청을 보내고 그 결과를 받는 역할을 한다.
UDP클라이언트르 위한 DatagramSocket객체는 기본생성자로 생성한다 포트번호는 자동으로 부여되니 지정할 필요가 없다.
DatagramSocket datagramSocket = new DatagramSocket();
요청을 보내기 위한 DatagramPacket은 다음과 같이 생성한다.
첫번째 매개값은 보낼 바이트배열 두번째 매개값은 보낼 바이트 수
세번째 매개값은 UDP서버의 ip와 포트정보를 가지고 있는 InetSocketAddress 객체이다.
String data = "요청 내용";
byte[] bytes = data.getBytes("UTF-8");
DatagramPacket sendPacket = new DatagramPacket(bytes, bytes.length, new InetSocketAddress("localhost", 50001);
DatagramSocket의 send()하면 받아진다.
UDP서버에서 처리결과가 언제 올지 모르므로 항상 받을 준비를 하기 위해 recevice상태에서 기다리는 것은 같다.
public class NewsClient {
public static void main(String[] args) {
// Datagram Socket생성
try {
DatagramSocket datagramSocket = new DatagramSocket();
System.out.println("[클라이언트] 연결 성공");
// 구독하고 싶은 뉴스 주제 보내기
String data = "정치";
byte[] bytes = data.getBytes("UTF-8");
InetSocketAddress isa = new InetSocketAddress("localhost", 50001);
DatagramPacket sendPacket = new DatagramPacket(bytes, bytes.length, isa);
datagramSocket.send(sendPacket);
System.out.println("[클라이언트] 뉴스받기 대기");
while (true) {
// 뉴스받기
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);
if (news.contains("뉴스10")) {
break;
}
}
// 닫기
datagramSocket.close();
System.out.println("[클라이언트] 연결종료");
} catch (Exception e) {
System.out.println("[클라이언트]" + e.getMessage());
}
}
}2023.03.07 후기
네트워크파트를 공부하기전 나는 의문을 가졌다.
ip와 Port번호가 같으면 하나의 서버만 가질수잇고 port번호로 서버프로그램을 나눠야함 정도는 기억하고 잇엇다.
그러면 학원에서 다같이 배울때 모두 ip주소가 같을텐데 같은공유기를 쓰니 그럼 어떻게 회선을 구분하나?
port번호를 사람마다 다르게 하나라고 생각하고 있엇다.
그런데 IP번호가 인터넷마다 같은것이아니라 LAN카드마다 다르다.
LAN카드는 컴퓨터마다 다르게 꼽혀잇으니 모두 ip주소가 다를 것이다라는 것을 이해하게 되었다.
'기초단계 > JAVA' 카테고리의 다른 글
| 2023.03.10 Java 복습 (0) | 2023.03.10 |
|---|---|
| 2023.03.09 Java 복습 (0) | 2023.03.10 |
| 2023.03.03 Java 복습 (0) | 2023.03.06 |
| 2023.02.28 -2 Java 복습 (0) | 2023.02.28 |
| 2023.02.28 - 1 Java복습 (0) | 2023.02.28 |