공부하는 히욤이

[이것이 자바다] Chapter 18 IO 기반 입출력 및 네트워킹 본문

Programming/Java

[이것이 자바다] Chapter 18 IO 기반 입출력 및 네트워킹

히욤이 2021. 5. 9. 07:14

18.1 IO 패키지 소개


  • 자바에서 데이터는 스트림(Stream)을 통해 입출력 됨
  • 스트림은 단일 방향으로 연속적으로 흘러가는 것 => 데이터는 출발지에서 나와 도착지로 들어간다는 개념

 

18.2 입력 스트림과 출력 스트림


  • 입력 스트림(InputStream) : 프로그램이 데이터를 입력 받을 때
  • 출력 스트림(OutputStream) : 프로그램이 데이터를 보낼 때
  • 기본적인 데이터 입출력(IO : Input/Output) API는 java.io 패키지에서 제공
java.io 패키지의 주요 클래스 설명
File 파일 시스템의 파일 정보를 얻기 위한 클래스
Console 콘솔로부터 문자를 입출력하기 위한 클래스
InputStream / OutputStream 바이트 단위 입출력을 위한 최상위 입출력 스트림 클래스
FileInputStream / FileOutputStream
DataInputStream / DataOutputStream
ObjectInputStream / ObjectOutputStream
PrintStream
BufferedInputStream / BufferedOutputStream
바이트 단위 입출력을 위한 하위 스트림 클래스
Reader / Writer 문자 단위 입출력을 위한 최상위 입출력 스트림 클래스
FileReader / FileWriter
InputStreamReader / OutputStreamWriter
PrintWriter
BufferedReader / BufferedWriter
문자 단위 입출력을 위한 하위 스트림 클래스
  • 바이트 기반 스트림은 그림, 멀티미디어, 문자 등 모든 종류의 데이터를 받고 보낼 수 있다.
  • 문자 기반 스트림은 오직 문자만 받고 보낼 수 있다.

 

구분 바이트 기반 스트림 문자 기반 스트림
입력 스트림 출력 스트림 입력 스트림 출력 스트림
최상위 클래스 InputSream OutputStream Reader Writer
하위 클래스(예) XXXInputStream(FileInputStream) XXXOutputStream(FileOutputStream) XXXReader(FileReader) XXXWriter(FileWriter)

 

18.2.1 InputStream

  • 바이트 기반 입력 스트림의 최상위 클래스로 추상 클래스
  • FileInputStream, BufferedInputStream, DataInputStream 클래스 모두 InputStream 클래스를 상속

* InputStream 클래스의 주요 메소드

리턴 타입 메소드 설명
int read( ) 입력 스트림으로부터 1바이트를 읽고 읽은 바이트를 리턴
int read(byte[] b) 입력 스트림으로부터 읽은 바이트들을 매개값으로 주어진 바이트 배열 b에 저장하고 실제로 읽은 바이트 수를 리턴
int read(byte[] b, int off, int len) 입력 스트림으로부터 len개의 바이트만큼 읽고 매개값으로 주어진 바이트 배열 b[off]부터 len까지 저장한다. 실제로 읽은 바이트 수인 len개를 리턴한다. 만약 len개를 모두 읽지 못하면 실제로 읽은 바이트 수를 리턴
void close( ) 사용한 시스템 자원을 반납하고 입력 스트림을 닫음

 

read()메소드

  • 입력 스트림으로부터 1바이트를 읽고 4바이트 int 타입으로 리턴 => 리턴된 4바이트 중 끝의 1바이트에만 데이터가 있음
  • 입력 스트림에서 5개의 바이트가 들어온다면 read()메소드로 1바이트씩 5번 읽을 수 있음
  • 더 이상 입력 스트림으로부터 바이트를 읽을 수 없으면 read() 메소드는 -1을 리턴
InputStream is = new FileInputStream("C:/test.jpg");
int readByte;
while ((readByte = is.read()) != -1) {

}

 

read(byte[] b)메소드

  • 입력 스트림으로부터 매개값으로 주어진 바이트 배열의 길이만큼 바이트를 읽고 배열에 저장해 읽은 바이트 수를 리턴
  • 실제로 읽은 바이트 수가 배열의 길이보다 작을 경우 읽은 수만큼 리턴
  • 입력 스트림으로부터 바이트를 읽을 수 없으면 -1을 리턴
InputStream is = new FileInputStream("C:/test.jpg");
int readByteNo;
byte[ ] readBytes = new byte[100];
while ((readByteNo = is.read(readBytes)) != -1) {

}

 

read(byte[] b, int off, int len)메소드

  • len개의 바이트만큼 읽고 매개 값으로 주어진 바이트 배열 b[off]부터 len개까지 저장한다.
  • 읽은 바이트 수인 len개까지 리턴한다. 실제로 읽은 바이트 수가 len개 보다 작을 경우 실제로 읽은 수만큼만 리턴
  • 바이트를 더 이상 읽을 수 없다면 -1을 리턴
  •  read(byte[] b)와 차이점은 한번에 읽어들이는 바이트 수를 len 매개값으로 조절 할 수 있고, 배열에서 저장이 시작되는 인덱스를 지정할 수 있다.

 

close()메소드

  • InputStream을 더 이상 사용하지 않을 경우 close( ) 메소드를 호출해서 InputStream에서 사용했던 시스템 자원을 풀어줌

 

18.2.2 OutputStream

  • 바이트 기반 출력 스트림의 최상위 클래스로 추상 클래스
  • FileOutputStream, BufferedOutputStream, DataOutputStream 클래스 모두 OutputStream 클래스를 상속

* OutputStream 클래스의 주요 메소드

리턴 타입 메소드 설명
void write(int b) 출력 스트림으로 1바이트를 보낸다(b의 끝 1바이트)
void write(byte[] b) 출력 스트림으로 주어진 바이트 배열 b의 모든 바이트를 보낸다.
void write(byte[] b, int off, int len) 출력 스트림으로 주어진 바이트 배열b[off]부터 len개까지의 바이트를 보낸다.
void flush( ) 버퍼에 잔류하는 모든 바이트를 출력한다.
void close( ) 사용한 시스템 자원을 반납하고 출력 스트림을 닫는다.

 

write(int b)메소드

  • 매개 변수로 주어진 int 값에서 끝에 있는 1바이트만 출력 스트림으로 보냄 (int 타입이라고 4바이트 모두 보내는 것 아님)
OutputStream os = new FileOutputStream("C:/test.txt");
byte[ ] data = "ABC".getBytes();
for(int i = 0; i < data.length; i++) {
	os.write(data[i]); //"A", "B", "C" 하나씩 출력
}

 

write(byte[] b)메소드

  • 매개값으로 주어진 바이트 배열의 모든 바이트를 출력 스트림으로 보냄
OutputStream os = new FileOuputStream("C:/test.txt");
byte[ ] data = "ABC".getBytes;
os.write(data); //"ABC" 모두 출력

 

write(byte[] b, int off, int len)메소드

  • b[off]부터 len개의 바이트를 출력 스트림으로 보냄
OutputStream os = new FileOuputStream("C:/test.txt");
byte[ ] data = "ABC".getBytes;
os.write(data, 1, 2); //"BC" 모두 출력

 

flush( )와 close( )메소드

  • 출력 스트림은 내부에 작은 버퍼(buffer)가 있어 데이터 출력 전 버퍼에 쌓여있다가 순서대로 출력됨
  • flush( ) 메소드는 버퍼에 잔류하고 있는 데이터를 모두 출력시키고 버퍼를 비우는 역할
  • OutputStream을 더 이상 사용하지 않을 경우에는 close( ) 메소드를 호출해서 OutputStream에서 사용했던 시스템 자원을 풀어줌
OutputStream os = new FileOuputStream("C:/test.txt");
byte[ ] data = "ABC".getBytes;
os.write(data);
os.flush();
os.close();		

 

18.2.3 Reader

  • 문자 기반 입력 스트림의 최상위 클래스로 추상 클래스
  • FileReader, BufferReader, InputStreamReader 클래스는 모두 Reader 클래스를 상속

* Reader 클래스의 주요 메소드

리턴 타입 메소드 설명
int read( ) 입력 스트림으로부터 한 개의 문자를 읽고 리턴한다.
int read(char[ ] cbuf) 입력 스트림으로부터 읽은 문자들을 매개값으로 주어진 문자 배열 cbuf에 저장하고 실제로 읽은 문자 수를 리턴
int read(char[] cbuf, int off, int len) 입력 스트림으로부터 len개의 문자를 읽고 매개값으로 주어진 문자 배열 cbuf[off]부터 len개까지 저장한다. 실제로 읽은 문자 수인 len개를 리턴
void close( ) 사용한 시스템 자원을 반납하고 입력 스트림을 닫음

 

read( )메소드

  • 입력 스트림으로부터 한 개의 문자(2바이트)를 읽고 4바이트 int 타입으로 리턴 => 4바이트 중 끝 2바이트에만 문자 데이터가 있음
  • read( )메소드가 리턴한 int 값을 char 타입으로 변환하면 읽은 문자를 얻을 수 있다.
  • 더 이상 입력 스트림으로부터 문자를 읽을 수 없으면 -1을 리턴
Reader reader = new FileReader("C:/test.txt");
int readData;
while ((readData = reader.read()) != -1){
	char charData = (char) readData;
}

 

read(char[] cbuf, int off, int len)메소드

  • len개의 문자만큼 읽고 매개값으로 주어진 문자 배열 cbuf[off]부터 len개까지 저장
  • 읽은 문자 수인 len개를 리턴
  • 문자를 더 이상 읽을 수 없다면 -1을 리턴
  • read(char[] cbuf)와ㅏ 차이점은 한번에 읽어 들이는 문자 수를 len 매개값으로 조절 할 수 있고 배열에서 저장이 시작되는 인덱스를 지정 가능하다.


close( )메소드

  • Reader를 더 이상 사용하지 않을 경우 close( ) 메소드를 호출해서 Reader에서 사용했던 시스템 자원을 풀어줌

 

18.2.4 Writer

  • 문자 기반 출력 스트림의 최상위 클래스로 추상 클래스
  • FileWriter, BufferedWriter, PrintWriter, OutputStreamWriter 클래스는 모두 Writer 클래스를 상속

* Writer 클래스의 주요 메소드

리턴 타입 메소드 설명
void write(int c) 출력 스트림으로 주어진 한 문자를 보낸다(c의 끝 2바이트)
void write(char[ ] cbuf) 출력 스트림으로 주어진 문자 배열 cnuf의 모든 문자를 보낸다.
void write(char[] cbuf, int off, int len) 출력 스트림으로 주어진 문자 배열b[off]부터 len개까지의 문자를 보낸다.
void write(String str) 출력 스트림으로 주어진 문자열을 전부 보낸다.
void write(String str, int off, int len) 출력 스트림으로 주어진 문자열 off 순번부터 len개까지의 문자를 보낸다.
void flush( ) 버퍼에 잔류하는 모든 문자열을 출력한다.
void close( ) 사용한 시스템 자원을 반납하고 출력 스트림을 닫는다.


write(int c)메소드

  • 매개 변수로 주어진 int 값에서 끝에 있는 2바이트(한 개의 문자)만 출력 스트림으로 보냄 (int 타입이라고 4바이트 모두 보내는 것 아님)
Writer writer = new FileWriter("C:/test.txt");
char[] data = "홍길동."toCharArray();
for(int i = 0; i < data.length; i++) {
	writer.write(data[i]); //"홍", "길", "동"을 하나씩 출력
}

 

write(char[ ] cbuf)메소드

  • 매개값으로 주어진 char[ ]배열의 모든 문자를 출력 스트림으로 보냄
Writer writer = new FileWriter("C:/test.txt");
char[] data = "홍길동."toCharArray();
writer.write(data); //"홍길동"을 모두 출력

 

write(byte[] b, int off, int len)메소드

  • b[off]부터 len개의 문자를 출력 스트림으로 보냄
Writer writer = new FileWriter("C:/test.txt");
char[] data = "홍길동."toCharArray();
writer.write(data, 1, 2); //"길동"만 출력

 

write(String str)와 write(String str, int off, int len) 메소드

  • write(String str)은 문자열 전체를 출력 스트림으로 보냄
  • wrtie(String str, int off, int len)은 주어진 문자열 off 순번부터 len개 까지의 문자를 보냄
  • flush( ) 메소드는 버퍼에 잔류하고 있는 데이터를 모두 출력시키고 버퍼를 비우는 역할
  • OutputStream을 더 이상 사용하지 않을 경우에는 close( ) 메소드를 호출해서 OutputStream에서 사용했던 시스템 자원을 풀어줌
Writer writer = new FileWriter("C:/test.txt");
String data = "안녕 자바 프로그램";
writer.write(data);
writer.flush();
wrtier.close();

 

18.3 콘솔 입출력


  • 콘솔(console) : 시스템을 사용하기 위해 키보드로 입력 받고 화면으로 출력하는 소프트웨어
  • 유닉스나 리눅스 운영체제는 터미널(terminal), windows 운영체제는 명령 프롬포트

 

18.3.1 System.in 필드

  • InputStream 타입의 필드이므로 InputStream 변수로 참조가 가능
InputStream is = System.in;
  • 어떤 키가 입력 되었는지 확인하려면 InputStream의 read( ) 메소드로 한 바이트씩 읽으면 된다.
int asciiCode = is.read();
  • 리턴된 int 값에는 10진수 아스키 코드(ASCII CODE)가 들어있다.
  • 아스키 코드 : 미국표준협회가 컴퓨터에서 문자를 숫자로 매칭하는 방법을 표준화 시킨 것
  • 1byte로 표현되는 256가지의 숫자에 영어 알파벳, 아라비아 숫자, 특수 기호를 매칭
  • 숫자로 된 아스키 코드 대신 키보드에서 입력한 문자를 직접 얻고 싶다면 read()메소드를 읽은 아스키 코드를 char 타입으로 변환하면 된다.
char inputChar = (char) is.read();

 

//콘솔에서 입력한 번호 알아내기

import java.io.IOException;
import java.io.InputStream;

public class SystemInExample1 {

	public static void main(String[] args) throws IOException {
		System.out.println("==메뉴==");
		System.out.println("1. 예금 조회");
		System.out.println("2. 예금 출금");
		System.out.println("3. 예금 입금");
		System.out.println("4. 종료 하기");
		System.out.print("메뉴를 선택하세요:");

		InputStream is = System.in; //키보드 입력 스트림 얻기
		char inputChar = (char)is.read();  //아스키 코드를 읽고 문자로 리턴
		switch (inputChar) {
		case '1': {
			System.out.println("예금 조회를 선택하셨습니다.");
			break;
		}
		case '2': {
			System.out.println("예금 출금을 선택하셨습니다.");
			break;
		}
		case '3': {
			System.out.println("예금 입금을 선택하셨습니다.");
			break;
		}
		case '4': {
			System.out.println("종료 하기을 선택하셨습니다.");
			break;
		}
		
		}
	}
}
  • InputStream의 read() 메소드는 1바이트만 읽기 때문에 1바이트의 아스키 코드로 표현되는 숫자, 영어, 특수문자는 읽을 수 있지만 한글과 같이 2바이틀를 필요로 하는 유니코드는 read()메소드로 읽을 수 없음
  • 한글을 얻기 위해서는 read(byte[] b)나 read(byte[] b, int off, int len) 메소드로 전체 입력된 내용을 바이트 배열로 받고, 이 배열을 이용해서 String 객체를 생성하면 된다.
byte[] byteData = new byte[15];
int readByteNo = System.in.read(byteData);
// readByteNo : 읽은 바이트 수 byteData : 실제로 읽은 바이트 수 저장
  • 변환할 문자열은 바이트 배열의 0번 인덱스에서 시작해서 ㅇ릭은 바이트수 -2만큼이다 => Enter키에 해당하는 마지막 두 바이트를 제외하기 위해서
String strData = new String(byteData, 0, readByteNo-2);
// byteData : 바이트 배열, 0: 시작 인덱스, readByteNo-2 : 읽은 바이트수 -2

 

// 콘솔에서 입력한 한글 알아내기

import java.io.IOException;
import java.io.InputStream;

public class SystemInExample2 {

	public static void main(String[] args) throws IOException {
		InputStream is = System.in;
		
		byte[] datas = new byte[100];
		
		System.out.print("이름:");
		int nameBytes = is.read(datas);
		String name = new String(datas, 0, nameBytes-2);
		
		System.out.print("하고 싶은 말:");
		int commentBytes = is.read(datas);
		String comment = new String(datas, 0, commentBytes-2);
		
		System.out.println("입력한 이름 : " + name);
		System.out.println("입력한 하고 싶은 말 : " + comment);

	}

}

 

18.3.2 System.out 필드

  • 콘솔로 데이터를 출력하기 위해서 사용
  • out은 PrintStream 타입의 필드
OutputStream os = System.out;
  • 1개의 바이트를 출력하려면 OutputStream의 writer(int b) 메소드를 이용 => 바이트 값은 아스키 코드
  • OutputStream의 write(int b)의 메소드는 1바이트만 보낼 수 있기 때문에 1바이트로 표현 가능한 숫자, 영어, 특수문자는 출력이 가능하지만, 2바이트로 표현되는 한글은 출력 할 수 없음
  • 한글을 출력하기 위해서는 한글을 바이트 배열로 얻은 다음, write(byte[ ] b)나 write(byte[] b, int off, int len) 메소드로 콘솔로 출력
String name = "홍길동";
byte[] nameBytes = name.getBytes();
os.write(nameBytes);
os.flush();

 

// 연속된 숫자, 영어, 한글 출력

import java.io.IOException;
import java.io.OutputStream;

public class SystemOutExample {

	public static void main(String[] args) throws IOException {
		OutputStream os = System.out;
		
		for (byte b = 48; b < 58; b++) {
			os.write(b); //아스키 코드 48에서 57까지의 문자 출력
		}
		
		os.write(10); //라인 피드 10을 입력하면 다음 행으로 넘어감
		
		for (byte b = 97; b < 123; b++) {
			os.write(b); //아스키 코드 97에서 122까지의 문자 출력
		}
		
		os.write(10);
		
		String hangul = "가나다라마바사아자차카파타하";
		byte[] hangulBytes = hangul.getBytes();
		os.write(hangulBytes);
		
		os.flush();

	}

}
  • out은 원래 PrintStream 타입의 필드이므로 print() 또는 println()메소드를 사용하면 더 쉽게 콘솔에 출력 가능

 

18.3.3 Console 클래스

  • Console 객체를 얻기 위해서 System의 정적 메소드인 console()을 호출 하면 된다.
Console console = System.console();
  • 이클립스에서 실행하면 System.console()메소드는 null을 리턴하기 때문에 반드시 cmd에서 실행해야 한다.
리턴 타입 메소드 설명
String readLine() Enter키를 입력하기 전의 모든 문자열을 읽음
char[] readPassword() 키보드 입력 문자를 콘솔에 보여주지 않고 문자열을 읽음

 

18.3.4 Scanner 클래스

  • java.util 패키지의 Scanner 클래스를 이용하면 콘솔로부터 기본 타입의 값을 바로 읽을 수 있다.
Scanner scanner = new Scanner(System.in);
  • Scaaner가 콘솔에서만 사용되는 것은 아니고 생성자 매개값에 File, InputStream, Path 등과 같이 다양한 입력 소스를 지정할 수 있음
리턴 타입 메소드 설명
boolean nextBoolean() boolean(true/false) 값을 읽음
byte nextByte() byte 값을 읽음
short nextShort() short 값을 읽음
int nextInt() int 값을 읽음
long nextLong() long 값을 읽음
float nextFloat() float 값을 읽음
double nextDouble() double 값을 읽음
String nextLine() String 값을 읽음

 

18.4 파일 입출력


18.4.1 File 클래스

  • IO 패키지(java.io)에서 제공하는 File 클래스는 파일 크기, 파일 속성, 파일 이름 등의 정보를 얻어내는 기능과 파일 생성 및 삭제 가능을 제공
  • 디렉토리를 생성하고 디렉토리에 존재하는 파일 리스트를 얻어내는 기능도 있다.
  • 파일의 데이터를 읽고 쓰는 기능은 지원하지 않음 => 파일의 입출력은 스트림을 사용해야 함

* C:\temp 디렉토리의 file.txt 파일을 File 객체로 생성하는 코드

File file = new File("C:\\temp\\file.txt");
File file = new ile("C:/temp/file.txt");
  • File 객체를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아님
  • 생성자 매개값으로 주어진 경로가 유효하지 않더라도 컴파일 에러나 예외가 발생하지 않음
  • File 객체를 통해 해당 경로에 실제로 파일이나 디렉토리가 있는지 확인하려면 exists( ) 메소드를 호출 할 수 있음
  • 디렉토리나 파일이 파일 시스템에 존재한다면 true, 존재하지 않는다면 false를 리턴
boolean isExists = file.exists();

 

  • exitsts() 메소드의 리턴 값이 false 라면 createNewFile( ), mkdir( ), mkdirs( ) 메소드로 파일 또는 디렉토리를 생성 할 수 있다.
리턴 타입 메소드 설명
boolean createNewFile( ) 새로운 파일을 생성
boolean mkdir( ) 새로운 디렉토리를 생성
boolean mkdirs( ) 경로상에 없는 모든 디렉토리를 생성
boolean delete( ) 파일 또는 디렉토리 삭제

 

  • 파일 또는 디렉토리가 존재할 경우 다음 메소드를 통해 정보를 얻을 수 있다.
리턴 타입 메소드 설명
boolean canExecute( ) 실행 할 수 있는 파일인지 여부
boolean canRead( ) 읽을 수 있는 파일인지 여부
boolean canWrite( ) 수정 및 저장할 수 있는 파일인지 여부
String getName( ) 파일의 이름을 리턴
String getParent( ) 부모 디렉토리를 리턴
File getParentFile( ) 부모 디렉토리를 File 객체로 생성한 후 리턴
String getPath( ) 전체 경로를 리턴
boolean isDirectory( )  디렉토리인지 여부
boolean isFile( ) 파일인지 여부
boolean isHidden( ) 숨김 파일인지 여부
long lastModified( ) 마지막 수정 날짜 및 시간을 리턴
long length( ) 파일의 크기를 리턴
String[ ] list( ) 디렉토리에 포함된 파일 및 서브디렉토리 목록 전부를 String 배열로 리턴
String[ ] list(FilenameFilter filter) 디렉토리에 포함된 파일 및 서브디렉토리 목록 중에 FilenameFilter에 맞는 것만 String 배열로 리턴
File[ ] listFiles( ) 디렉토리에 포함된 파일 및 서브 디렉토리 목록 전부를 File 배열로 리턴
File[ ] listFiles(FilenameFile filter) 디렉토리에 포함된 파일 및 서브 디렉토리 목록 중에 FilenameFilter에 맞는 것만 File 배열로 리턴
// File 클래스를 이용한 파일 및 디렉토리 정보 출력

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;

public class FileExample {

	public static void main(String[] args) throws URISyntaxException, IOException {
		File dir = new File("C:/Temp/Dir");
		File file1 = new File("C:/Temp/file1.txt");
		File file2 = new File("C:/Temp/file2.txt");
		File file3 = new File(new URI("file:///C:/Temp/file3.txt")); //파일 경로를 URI 객체로 생성해서 매개값으로 제공해도 됨
		
		if (dir.exists() == false) {
			dir.mkdirs();
		}
		if (file1.exists() == false) {
			file1.createNewFile();
		}
		if (file2.exists() == false) {
			file2.createNewFile();
		}
		if (file3.exists() == false) {
			file3.createNewFile();
		}
		
		File temp = new File("C:/Temp");
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd   a   HH:mm");
		File[] contents = temp.listFiles();
		System.out.println("날짜           시간        형태                   크기           이름");
		System.out.println("---------------------------------------------------------------");
		for (File file : contents) {
			System.out.print(sdf.format(file.lastModified()));
			
			if (file.isDirectory()) {
				System.out.print("\t<DIR>\t\t\t"+file.getName());
			} else {
				System.out.print("\t<DIR>\t\t\t"+file.length() + "\t" + file.getName());
			}
			System.out.println();
		}

	}

}

 

18.4.2 FileInputStream

  • 파일로부터 바이트 단위로 읽어들일 때 사용하는 바이트 기반 입력 스트림
  • 바이트 단위로 읽기 때문에 그림, 오디오, 비디오, 텍스트 파일 등 모든 종류의 파일을 읽을 수 있음
// 첫번째 방법
FileInputStream fis = new FileInputStream("C:/Temp/image.gif");

// 두번째 방법
File file = new File("C:/Temp/image.gif");
FileInputStream fis = new FileInputSream(file);

 

18.4.3 FileOutputStream

  • 바이트 단위로 데이터를 파일에 저장할 때 사용하는 바이트 기반 출력 스트림
  • 바이트 단위로 저장하기 때문에 그림, 오디오, 비디오, 텍스트 등 모든 종류의 데이터를 파일로 저장할 수 있다.
// 첫번째 방법
FileOutputStream fos = new FileOutputStream("C:/Temp/image.gif");

// 두번째 방법
File file = new File("C:/Temp/image.gif");
FileOutputStream fos = new FileOutputStream(file);
  • 파일이 이미 존재 할 경우, 데이터를 출력하면 파일을 덮어쓰게 되므로 기존의 파일 내용은 사라짐
  • 기존의 파일 내용 끝에 데이터를 추가할 경우에는 FileOutputStream 생성자의 두번째 매개값을 true로 주면 됨
FileOutputStream fos = new FileOutputStream("C:/Temp/data.txt", true);
FileOutputStream fos = new FileOutputStream(file, true);

 

18.4.4 FileReader

  • 텍스트 파일을 프로그램으로 읽어들일 때 사용하는 문자 기반 스트림
  • 문자 단위로 읽기 때문에 텍스트가 아닌 그림, 오디오, 비디오 등의 파일은 읽을 수 없음
// 방법1
FileReader fr = new FileReader("C:/Temp/file.txt");

// 방법2
File file = new File("C:/Temp/file.txt");
FileReader fr = new FileReader(file);
  • FileReader 객체가 생성될 때 파일과 직접 연결 되는데 만약 파일이 존재하지 않으면 FileNotFoundException이 발생
  • 한 문자를 읽기 위해서 read() 메소드를 사용하거나 읽은 문자를 char 배열에 저장하기 위해서 read(char[] cbuf) 또는 read(char[] cbuf, int off, int len) 메소드를 사용

 

18.4.5 FileWriter

  • 텍스트 데이터를 파일에 저장할 때 사용하는 문자 기반 스트림
  • 문자 단위로 저장하기 때문에 텍스트가 아닌 그림, 오디오, 비디오 등의 파일은 저장할 수 없음
// 방법1
FileWriter fw = new FileWriter("C:/Temp/file.txt");

// 방법2
File file = new File("C:/Temp/file.txt");
FileWriter fw = new FileWriter(file);
  • FileWriter를 생성하면 지정된 파일이 이미 존재할 경우 그 파일을 덮어쓰게 되므로 기존의 파일 내용은 사라짐
  • 기존의 파일 내용 끝에 데이터를 추가 할 경우에는 FileWriter 생성자에 두번째 매개값으로 true를 주면 됨
FileWriter fw = new FileWriter("C:/Temp/data.txt", true);
FileWriter fw = new FileWriter(file, true);
  • 한 문자를 저장하기 위해서는 write() 메소드를 사용하고 문자열을 저장하기 위해서는 write(String str) 메소드를 사용

 

18.5 보조 스트림


  • 보조스트림이란 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림임
  • 보조스트림은 자체적으로 입출력을 수행할 수 없기 때문에 입력소스와 바로 연결되는 InputStream, FileInputStream, Reader, FileReader, 출력소스와 바로 연결되는 OutputStream, FileOutputStream, Writer, FileWriter 등에 연결해서 입출력 수행
  • 문자 변환, 입출력 성능 향상, 기본 데이터 타입 입출력, 객체 입출력 등의 기능을 제공

 

보조스트림 변수 = new 보조스트림(연결스트림)

// 콘솔 입력 스트림을 문자 변환 보조 스트림인 InputStreamReader에 연결하는 코드
InputStream is = System.in;
InputStreamReader reader = new InputStreamReader(is);

// 보조 스트림은 또 다른 보조 스트림에도 연결되어 스트림 체인을 구성할 수 있음
// 문자 변환 보조 스트림인 InputStreamReader를 다시 성능 향상 보조 스트림인 BufferedReader에 연결하는 코드
InputStream is = System.in;
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);

 

18.5.1 문자 변환 보조 스트림

  • 소스 스트림이 바이트 기반 스트림이면서 입출력 데이터가 문자라면 Reader와 Writer로 변환해서 사용하는 것을 고려해야 한다  => Reader와 Writer는 문자 단위로 입출력하기 때문에 바이트 기반 스트림보다는 편리하고 문자셋의 종류를 지정할 수 있기 때문에 다양한 문자를 입출력 할 수 있음

InputStreamReader

  • 바이트 입력 스트림에 연결되어 문자 입력 스트림인 Reader로 변환시키는 보조 스트림

// 콘솔 입력을 위한 InputStream을 Reader 타입으로 변환
InputStream is = System.in;
Reader reader = new InputStreamReader(is);

// 파일 입력을 위한 FileInputStream을 Reader 타입으로 변환
FileInputStream fis = new FileInputStream("C:/Temp/file.txt");
Reader reader = new InputStreamReader(fis);
// 콘솔에서 한글 입력받기

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;


public class InputStreamReaderExample {

	public static void main(String[] args) throws IOException {
		InputStream is = System.in; //InputStream 바이트 기반  
		Reader reader = new InputStreamReader(is); //Reader 문자 기반
		
		int readCharNo;
		char[] cbuf = new char[100];
		while ((readCharNo = reader.read(cbuf)) != -1) {
			String data = new String(cbuf, 0, readCharNo);
			System.out.println(data);
		}
	}
}

 

OutputStreamReader

  • 바이트 출력 스트림에 연결되어 문자 출력 스트림인 Writer로 변환시키는 보조 스트림

// 파일 출력을 위한 FileOutputStream을 Writer 타입으로 변환

FileOutputStream fos = new FileOutputStream("C:/Temp/file.txt");
Writer writer = new OutputStreamWriter(fos);
// 파일로 출력하기

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class OutputStreamWriterExample {

	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("C:/Temp/file.txt");
		Writer writer = new OutputStreamWriter(fos);
		
		String data = "바이트 출력 스트림을 문자 출력 스트림으로 변환";
		writer.write(data);
		
		writer.flush();
		writer.close();
		System.out.println("파일 저장이 끝났습니다.");
	}
}

 

18.5.2 성능 향상 보조 스트림

  • 바이트 기반 스트림에는 BufferedInputStream, BufferedOutputStream이 있고 문자 기반 스트림에는 BuffereReader와 BufferedWriter가 있다.

BufferedInputStream과 BufferedReader

  • BufferedInputStream은 바이트 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedReader는 문자 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedInputStream과 BufferedReader는 입력 소스로부터 자신의 내부 버퍼 크기만큼 데이터를 미리 읽고 버퍼에 저장해 둔다.
BufferedInputSream bis = new BufferedInputStream(바이트 입력 스트림);
BufferedReader br = new BufferedReader(문자 입력 스트림);

 

BufferedOutputStream과 BufferedWriter

  • BufferedOutputStream은 바이트 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedWriter는 문자 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림
  • BufferedOutputStream과 BufferedWriter는 프로그램에서 전송한 데이터를 내부 버퍼에 쌓아 두었다가 버퍼가 꽉차면 버퍼의 모든 데이터를 한꺼번에 보낸다.
BufferedOutputSream bos = new BufferedOutputStream(바이트 출력 스트림);
BufferedWriter bw = new BufferedWriter(문자 출력 스트림);

 

18.5.3 기본 타입 입출력 보조 스트림

  • 바이트 스트림은 바이트 단위로 입출력하기 때문에 자바의 기본 데이터 타입인 boolean, char, short, int, long, float, double 단위로 입출력 할 수 없다.
  • DataInputStream과 DataOutputStream 보조 스트림을 연결하면 기본 데이터 타입으로 입출력 가능
DataInputStream dis = new DataInputStream(바이트 입력 스트림);
DataOutputStream dos = new DataOutputStream(바이트 출력 스트림);
DataInputStream DataOutputStream
boolean readBoolean() void writeBoolean(boolean v)
byte readByte() void writeByte(int v)
char readChar() void writeChar(int v)
double readDouble() void writeDouble(double v)
float readFloat() void writeFloat(float v)
int readInt() void writeInt(int v)
long readLong() void writeLong(long v)
short readShort() void wrtieShort(int v)
String readUTF() void wrtieUTF(String str)
  • 데이터 타입의 크기가 모두 다름으로 DataOutputStream으로 출력한 데이터를 다시 DataInputStream으로 읽어 올 때 출력한 순서와 동일한 순서로 읽어야 한다.

 

18.5.4 프린터 보조 스트림

  • PrintStream과 PrintWriter는 프린터와 유사하게 출력하는 print(), println() 메소드를 가지고 있는 보조 스트림임
  • PrintStream은 바이트 출력 스트림과, PrintWriter는 문자 출력 스트림과 연결 된다.
PrintStream ps = new PrintStream(바이트출력스트림);
PrintWriter pw = new PrintWriter(문자 출력 스트림);
  • PrintStream과 PrintWriter는 println()과 print() 이외에 printf()도 제공한다.
  • printf()메소드는 형식화된 문자열을 출력할 수 있도록 하기 위해 제공되는 메소드
더보기

%[argument_index$] [flags] [width] [.precision] conversion

   매개값의 순번            -, 0     전체자릿수 소수자릿수   변환 문자

  • %와 conversion(변환문자)는 필수적으로 작성하고 그 이외의 항목은 생략 가능
형식화된 문자 설명 출력형태
정수 %d 정수 123
%6d 6자리 정수, 왼쪽 빈자리 공백 ___123
%-6d 6자리 정수, 오른쪽 빈자리 공백 123___
%06d 6자리 정수, 왼쪽 빈자리 0 채움 000123
실수 %10.2f 소수점 이상 7자리, 소수점 이하 2자리, 왼쪽 빈자리 공백 ____123.45
%-10.2f 소수점 이상 7자리, 소수점 이하 2자리, 오른쪽 빈자리 공백 123.45____
%010.2f 소수점 이상 7자리, 소수점 이하 2자리, 왼쪽 빈자리 0 채움 0000123.45
문자열 %s 문자열 abc
%6s 6자리 문자열, 왼쪽 빈자리 공백 ___abc
%-6s 6자리 문자열, 오른쪽 빈자리 공백 abc___
날짜 %tF %tY-%tM-%td 2010-01-06
%tY 4자리 년 2010
%ty 2자리 년 10
%tm 2자리 월 01
%td 2자리 일 06
%tH 2자리 시(0~23) 08
%tl 시(0~12) 8
%tM 2자리 분 06
%tS 2자리 초 24
특수
문자
/t 탭(tab)  
/n 줄바꿈  
%% % %

 

18.5.5 객체 입출력 보조 스트림

  • 객체는 문자가 아니기 때문에 바이트 기반 스트림으로 출력해야 함
  • 객체를 출력하기 위해서는 객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데, 이것을 객체 직렬화(serialization)라고 함
  • 반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을수도 있는데, 입력 스트림으로 부터 읽어들인 연속적인 바이트를 객체로 복원하는 것을 역직렬화(deserializtion)

 

ObjectInputStream, ObjectOutputStream

  • 객체를 입출력할 수 있는 두개의 보조 스트림인 ObjectInputStream과 ObjectOutputStream을 제공
  • ObjectOutputStream은 바이트 출력 스트림과 연결되어 객체를 직렬화
  • ObjectInputStream은 바이트 입력 스트림과 연결되어 객체를 역직렬화
ObjectInputStream ois = new ObjectInputStream(바이트 입력 스트림);
ObjectOutputStream oos = new ObjectOutputStream(바이트 출력 스트림);
  • ObjectOutputStream으로 객체를 직렬화하기 위해서 wrtieObject() 메소드를 사용
  • ObejctInputStream의 readObject() 메소드는 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 생성한다.

 

직렬화가 가능한 클래스(Serializable)

  • 자바는 Serializable 인터페이스를 구현한 클래스만 직렬화 할 수 있도록 제한
  • Serializable 인터페이스는 필드나 메소드가 없는 빈 인터페이스지만, 객체를 직렬화 할 때 private 필드를 포함ㅎ나 모든 필드를 바이트로 변환해도 좋다는 표시 역할을 한다.
public class XXX implements Serializable { }
  • 객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생성자 및 메소드는 직렬화에 포함되지 않음
  • 역직렬화 할 때에는 필드의 값만 복원 됨
  • 필드선언에 static 또는 transient가 붙어 잇을 경우에는 직렬화가 되지 않음

 

serialVersionUID 필드

  • 직렬화된 객체를 역직렬화 할 때는 직렬화 했을때와 같은 클래스를 사용해야 함
  • 클래스의 이름이 같더라도 클래스의 내용이 변경되면, 역직렬화는 실패하며 예외가 발생 함
  • serialVersionUID는 같은 클래스임을 알려주는 식별자 역할을 함
  • Serializable 인터페이스를 구현한 클래스를 컴파일하면 자동적으로 serialVersionUID 정적 필드가 추가 됨
  • 클래스를 재컴파일하면 serialVersionUID의 값이 달라짐

 

writeObject()와 readObject() 메소드

  • writeObject() 메소드는 직렬화될 때 자동으로 호출되고, readObject()메소드는 역직렬화 될 때 자동으로 호출
  • 접근 제한자가 private가 아니면 자동 호출되지 않기 때문에 반드시 private를 붙여줘야 함

 

18.6 네트워크 기초


  • 네트워크는 여러 대의 컴퓨터를 통신 회선으로 연결한 것

18.6.1 서버와 클라이언트

  • 서버(server) : 서비스를 제공하는 프로그램
  • 클라이언트(client) : 서비스를 받는 프로그램
  • 인터넷에서 두 프로그램이 통신하기 위해서는 연결을 요청하는 역할과 연결을 수락하는 역할이 필요함
  • 클라이언트는 서비스를 받기 위해 연결을 요청하고 서버는 연결을 수락하여 서비스를 제공한다.
  • 서버는 클라이언트가 요청(request)하는 내용을 처리해주고 응답(response)를 클라이언트로 보낸다.

  • 클라이언트/서버 모델은 한개의 서버와 다수의 클라이언트로 구성되는 것이 보통이나 두개의 프로그램이 서버인 동시에 클라이언트 역할을 하는 p2p(peer to peer) 모데돌 있다.
  • p2p 모델에서는 먼저 접속을 시도한 컴퓨터가 클라이언트가 됨

 

18.6.2 IP주소와 포트(Port)

  • IP(Internet Protocol) : 컴퓨터의 고유한 주소
  • IP주소는 xxx.xxx.xxx.xxx와 같은 형식으로 표현
  • xxx는 0~255 사이의 정수
  • 프로그램은 DNS(Domain Name System)을 이용해서 연결한 컴퓨터의 IP주소를 찾는다.
더보기

[DNS]

도메인 이름        : 등록된 IP주소

www.naver.com   : 222.122.1995.5

  • 포트(port) 번호 ; 컴퓨터 내에서 실행하는 서버를 선택하기 위해서는 추가적인 정보가 필요함
  • 포트 바인딩(binding) : 서버는 시작 할 때 고정적인 포트번호를 가지고 실행
  • 클라이언트도 서버에서 보낸 정보를 받기 위해 포트 번호가 필요한데, 서버와 같이 고정적인 포트 번호가 아닌 운영체제가 자동으로 부여하는 동적 포트 번호를 사용
  • 통적 포트 번호는 클라이언트가 서버로 연결 요청을 할 때 전송되어 서버가 클라이언트로 데이터를 보낼 때 사용 됨
구분명 범위 설명
Well Know Prot Numbers 0~1023 국제인터넷주소관리기구(ICANN)가 특정 애플리케이션용으로 미리 예약한 포트
Registered Port Numbers 1024~49151 회사에서 등록해서 사용할 수 있는 포트
Dynamic Or Private Port Numbers 49152~65535 운영체제가 부여하는 동적 포트 또는 개인적인 목적으로 사용할 수 있는 포트

 

18.6.3 InetAddress로 IP 주소 얻기

  • Java는 IP주소를 java.net.InetAddress 객체로 표현 함
  • InetAddress는 로컬 컴퓨터의 IP주소 뿐만 아니라 도메인 이름을 DNS에서 검색한 후 IP 주소를 가져오는 기능을 제공
  • 로컬 컴퓨터의 InetAddress를 얻고 싶다면 InetAddress.getLocalHost()메소드를 호출 하면 됨
InetAddress is = InetAddress.getLocalHost();
  • 외부 컴퓨터의 도메인 이름을 알고 있다면 두개의 메소드를 이용해 InetAddress 객체를 얻으면 됨
InetAddress is = InetAddress.getByName(String host); //단 하나의 IP주소를 얻어와 InetAddress를 생성하고 리턴
InetAddress[] iaArr = InetAddress.getAllByName(String host); // 연결 클라이언트가 많은 회사의 경우 하나의 도메인 이름에 여러 개의 컴퓨터 IP를 등록해서 운영
// IP 주소 얻기

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressExample {
	public static void main(String[] args) {
		
		try {
			InetAddress ia = InetAddress.getLocalHost();
			System.out.println("내 컴퓨터 IP 주소 : " + ia.getHostAddress());
			
			InetAddress[] iaArr = InetAddress.getAllByName("www.naver.com");
			for (InetAddress inetAddress : iaArr) {
				System.out.println("www.naver.com IP 주소 : " + inetAddress.getHostAddress());
			}
			
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
	}
}

 

18.7 TCP 네트워킹


  • TCP(Transmission Control Protocol) : 연결 지향적 프로토콜
  • 연결 지향적 프로토콜 : 클라이언트와 서버가 연결된 상태에서 데이터를 주고받는 프로토콜
  • 클라이언트가 연결 요청을 하고, 서버가 연결을 수락하면 통신 선로가 고정되고, 모든 데이터는 고정된 통신 선로를 통해서 순차적으로 전달 됨
  • 데이터를 정확하고 안정적으로 전달 함
  • 데이터를 보내기 전 반드시 연결이 형성되어야 하고 고정된 통신 선로가 최단선이 아닐 경우 상대적으로 UDP보다 데이터 전송 속도가 느릴 수 있음
  • TCP 네트워킹을 위해 java.net.ServerSocket과 java.net.Socket 클래스를 제공

 

18.7.1 ServerSocket과 Socket용도

  • TCP 서버의 역할
    1. 클라이언트가 연결 요청을 해오면 연결을 수락하는 것 => java.net.ServerScoet 클래스가 담당
    2. 연결된 클라이언트와 통신하는 것 => java.net.Socket 클래스가 담당
  • 클라이언트가 연결을 요청해오면 ServerSocket은 연결을 수락하고 통신용 Socket을 생성 함

  • 서버는 클라이언트가 접속할 포트를 가지고 있어야 하는데 이 포트가 바인딩 포트임
  • 서버는 고정된 포트 번호에 바인딩해서 실행하므로, ServerSocket을 생성할 때 포트 번호 하나를 지정해야 한다.
  • 서버가 실행되면 클라이언트는 서버의 IP주소와 바인딩 포트 번호로 Socket을 생성해 연결 요청을 할 수 있다.
  • ServerSocket은 클라이언트가 연결 요청을 해오면 accpet() 메소드로 연결 수락을 하고 통신용 Socket을 생성함
  • 클라이언트와 서버는 각각의 Socket을 이용해서 데이터를 주고 받음

 

18.7.2 ServerSocket 생성과 연결 수락

  • ServerSocket을 얻는 방법
    1. 생성자에 바인딩 포트를 대입하고 객체를 생성
ServerSocket serverSocket = new ServerSocket(5001);

         2. 디폴트 생성자로 객체를 생성하고 포트 바인딩을 위해 bind() 메소드를 호출

            bind() 메소드의 매개값은 포트 정보를 가진 InetSocketAddress이다.

ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetAddress(5001));
  • ServerSocket을 생성할 때 해당 포트가 이미 사용중이라면 BindException 발생
  • accpet() 메소드는 클라이언트가 연결 요청하기 전까지 블로킹(스레드가 대기 상태) 됨
  • 연결된 클라이언트의 IP와 포트 정보를 알고 싶으면 Socket의 getRemoteSocketAddress() 메소드를 호출해서 SocketAddress를 얻으면 된다. => 실제 리턴되는 것은 InetSocketAddress 객체이므로 타입 변환 해야 함
InetSocketAddress socketAddress = (InetSocketAddress) socekt.getRemoteSocketAddress();

 

* InetSocketAddress 리턴타입과 메소드

리턴 타입 메소드명(매개변수) 설명
String getHostName() 클라이언트 IP 리턴
int getPort() 클라이언트 포트 번호 리턴
String toString() "IP:포트번호" 형태의 문자열 리턴

 

  • 클라이언트 연결 수락이 필요없으면 ServerSocket의 close()메소드를 호출해서 포트를 언바인딩 시켜야 함
serverSocket.close();

 

// 연결 수락

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 생성
			serverSocket = new ServerSocket();
			serverSocket.bind(new InetSocketAddress("localhost", 5001));

			while (true) {
				System.out.println("[연결 기다림]");

				//클라이언트 연결 수락
				Socket socket = serverSocket.accept();
				InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
				System.out.println("[연결 수락함] " + isa.getHostName());
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		// serverSocket이 닫혀있지 않을 경우
		try {
			if (!serverSocket.isClosed()) {
				serverSocket.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

 

18.7.3 Socket 생성과 연결 요청

  • 클라이언트가 서버에 연결을 요청하려면 java.net.Socket을 이용해야 함
  • Socket 객체를 생성함과 동시에 연결 요청을 하려면 생성자의 매개값으로 서버의 IP 주소와 바인딩 포트 번호를 제공하면 됨
// 로컬 PC의 5001 포트에 연결 요청하는 코드

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 생성과 동시에 연결 요청을 하지 않고 기본 생성자로 Socket을 생성 한 후, connect() 메소드로 연결 요청 가능
socket = new Socket();
socket.connect( new InetAddress("localhost", 5001) );
  • UnknownHostException은 잘못 표기된 IP주소를 입력했을 경우 예외 발생
  • IOException은 주어진 포트로 접속할 수 없을 때 예외 발생
  • Socket 생성자 및 connect() 메소드는 서버와 연결이 될 때 까지 블로킹 됨
// 연결 요청

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 생성
			socket = new Socket();
			System.out.println("[연결 요청]");
			
			// 연결 요청
			socket.connect(new InetSocketAddress("localhost", 5001));
			System.out.println("[연결 성공]");
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		
		// 연결이 되어있을 경우
		if (!socket.isClosed()) {
			try {
				socket.close();
			} catch (Exception e) {
				
			}
		}
	}

}

 

18.7.4 Socket 데이터 통신

  • 클라이언트가 연결 요청(connect()) 하고 서버가 연결 수락(accpet()) 했다면, 양쪽의 Socket 객체로부터 각각 입력 스트림(InputStream)과 출력 스트림(OutputStream)을 얻을 수 있음
// 입력 스트림 얻기
InputStream is = socket.getInputStream();

// 출력 스트림 얻기
OutputStrem os = socket.getOutputStream();

 

 

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

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

 

* 상대방이 보낸 데이터를 받기 위해서는 받은 데이터를 저장할 byte[] 배열을 하나 생성하고, 이것을 매개값으로 해서 InputStream의 read()메소드를 호출

byte[] byteArr = new byte[100];
InputStream inputStream = socket.getInputStream();
int readByteCount = inputStream.read(byteArr);
String data = new String(byteArr, 0, readByteCount, "UTF-8");

 

* 데이터 받고 보내기

// 데이터 받고 보내기 Client

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;


public class ClientExample {
	public static void main(String[] args) {
		Socket socket = null;
		
		try {
			
			// socket 생성
			socket = new Socket();
			System.out.println("[연결 요청]");
			
			// 연결 요청
			socket.connect(new InetSocketAddress("localhost", 5001));
			System.out.println("[연결 성공]");
			
			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("데이터 보내기 성공");
			
			// 상대방이 보낸 데이터를 받기 위한 코드
			InputStream is = socket.getInputStream();
			bytes = new byte[100];
			int readByteCount = is.read(bytes);
			message = new String(bytes, 0, readByteCount, "UTF-8");
			System.out.println("[데이터 받기 성공] : " + message);
			
			os.close();
			is.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		
		// 연결이 되어있을 경우
		if (!socket.isClosed()) {
			try {
				socket.close();
			} catch (Exception e) {
				
			}
		}
	}

}
// 데이터 받고 보내기 Server

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 ServerExample {
	public static void main(String[] args) {

		ServerSocket serverSocket = null;

		try {
			// serverSocket 생성
			serverSocket = new ServerSocket();
			serverSocket.bind(new InetSocketAddress("localhost", 5001));

			while (true) {
				System.out.println("[연결 기다림]");

				//클라이언트 연결 수락
				Socket socket = serverSocket.accept();
				InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
				System.out.println("[연결 수락함] " + 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("[데이터 받기 성공] : " + message);
				
				// 데이터 보내기 위한 코드
				OutputStream os = socket.getOutputStream();
				message = "Hello Client";
				bytes = message.getBytes("UTf-8");
				os.write(bytes);
				os.flush();
				System.out.println("[데이터 보내기 성공]");
				
				is.close();
				os.close();
				socket.close();
				
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		// serverSocket이 닫혀있지 않을 경우
		try {
			if (!serverSocket.isClosed()) {
				serverSocket.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

데이터를 받기위해 InputStream의 read()메소드를 호출하면 상대방이 데이터를 보내기 전까지는 블로킹(blocking) 되는데 read() 메소드가 블로킹 해제되고 리턴 되는 경우는 세가지 임

블로킹이 해제되는 경우 리턴값
상대방이 데이터를 보냄 읽은 바이트 수
상대방이 정상적으로 Socket의 close()를 호출 -1
상대방이 비정상적으로 호출 IOException 발생

 

18.7.5 스레드 병렬 처리

  • 서버 어플리케이션은 지속적으로 클라이언트와 연결 수락 기능을 해야 하는데, 입출력이 블로킹되면 이 작업을 할 수 없게 됨 => accept(), connect(), read(), write()는 별도의 작업 스레드를 생성해서 병렬적으로 처리하는 것이 좋음
  • 클라이언트의 폭증으로 인해 서버의 과도한 스레드 생성을 방지하려면 스레드풀을 사용하는 것이 바람직함

 

18.7.6 채팅 서버 구현

 

18.8 UDP 네트워킹


  • UDP(User Datagram Protocol)은 비연결 지향적 프로토콜
  • 비연결 지향적 프로토콜 : 데이터를 주고 받을 때 연결 절차를 거치지 않고, 발신자가 일방적으로 데이터를 발신하는 방식
  • 연결 과정이 없기 때문에 TCP 보다는 빠른 전송을 할 수 있지만 데이터 전달의 신뢰성은 떨어짐
  • 발신자가 데이터 패킷을 순차적으로 보내더라도 이 패킷들은 서로 다른 통신 선로를 통해 전달 될 수 있음
  • 먼저 보낸 패킷이 느린 선로를 통해 전송될 경우 나중에 보낸 패킷보다 늦게 도착할수도 있음
  • 일부 패킷은 잘못된 선로로 전송되어 유실 가능
  • 데이터 전달의 속도가 중요한 프로그램은 UDP 사용
  • 데이터 전달의 신뢰성이 중요한 프로그램은 TCP 사용
  • UDP 프로그래밍을 위해 java.net.DatagramSocket과 java.net.DatagramPacket 클래스를 제공
  • DatagramSocket은 발신점과 수신점에 해당하는 클래스
  • DatagramPacket은 주고받는 패킷 클래스

 

18.8.1 발신자 구현

  • DatagramSocket 객체 생성과 보내고자 하는 데이터를 byte[] 배열로 생성
DatagramSocket datagramSocket = new DatagramSocket();
byte[] byteArr = data.getBytes("UTF-8");

 

  • 데이터와 수신자 정보를 담고 있는 DatagramPacket 생성
  • DatagramPacket의 첫번째 매개값은 보낼 데이터인 byte[] 배열, 두번째 매개값은 byte[] 배열에서 보내고자 하는 항목 수, 전체 항목을 보내려면 length 값을 대입하면 됨, 세번째 매개값은 수신자 IP와 포트 정보를 가지고 있는 SocketAddress임
  • SocketAddress는 추상 클래스이므로 하위 클래스인 InetSocketAddress객체를 생성해서 대입하면 됨
byte[] byteArr = data.getBytes("UTF-8");
DatagramPacket packet = new DatagramPacket(byteArr, byteArr.length, new InetSocketAddress("localhost",5001));

DatagramSocket의 send() 메소드를 호출하면 수신자에게 데이터가 전달 됨

datagramSocket.send(packet);

 

* 발신자 프로그램 코드

// 발신자

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 datagramSocket = new DatagramSocket();
		System.out.println("[발신 시작]");

		for (int i = 1; i < 3; i++) {
			String data = "메시지" + i;
			byte[] byteArr;
			byteArr = data.getBytes("UTF-8");
			//DatagramPacket 생성
			DatagramPacket packet = new DatagramPacket(byteArr, byteArr.length, new InetSocketAddress("localhost", 5001));
			//DatagramPacket 전송
			datagramSocket.send(packet);
			System.out.println("[보낸 바이트 수]:" + byteArr.length +"bytes");
		}

		System.out.println("[발신 종료]");
		datagramSocket.close();

	}
}

 

18.8.2 수신자 구현

  • 수신자로 사용할 DatagramSocket 객체는 바인딩할 포트 번호를 매개값으로 지정하고 생성해야 함
DatagramSocket datagramSocket = new DatagramSocket(5001);

 

  • DatagramSocket이 생성되면 receive() 메소드를 호출해서 패킷을 읽을 준비 한다.=> receive() 메소드는 패킷을 받을 때까지 블로킹 되고, 패킷이 도착하면 매개값으로 주어진 DatagramPack에 패킷 내용을 저장함
datagramSocket.receive(datagramPacket);

 

  • 패킷 내용을 저장할 DatagramPacket 객체는 다음과 같이 생성첫번째 매개값은 읽은 패킷 데이터를 저장할 바이트 배열, 두번째 매개값은 읽을 수 있는 최대 바이트 수로 첫번째 바이트 배열의 크기와 같거나 작아야 함
DatagramPacket datagramPacket = new DatagramPacket(new byte[100], 100);

 

  • receive() 메소드로 패킷을 읽었다면 DatagramPacket의 getData()메소드로 데이터가 저장된 바이트 배열을 얻어낼 수 있다.
  • getLength()를 호출해서 읽은 바이트 수를 얻을 수 있다.
String Data = new String(packet.getData(), 0, packet.getLength(), "UTf-8");

 

  • 수신자가 패킷을 받고 나서 발신자에게 응답 패킷을 보내고 싶다면 발신자의 IP와 포트를 알아야 함
  • => DatagramPacket의 getSocketAddress()를 호출하면 발신자의 SocketAddress 객체를 얻어 낼 수 있어, 발신자에게 응답 패킷을 보낼 때 send() 메소드에서 이용 가능
SocketAddress socketAddress = packet.getSocketAddress();
  • 수신자는 항상 데이터를 받을 준비를 해야 하므로 작업 스레드를 생성해 receive()메소드를 반복적으로 호출해야 함

* 수신자 프로그램 코드

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPReceiveExample extends Thread{
	public static void main(String[] args) throws Exception {

		//5001번 포트에서 수신하는 DatagramSocket 생성
		DatagramSocket datagramSocket = new DatagramSocket(5001);

		Thread thread = new Thread() {
			@Override
			public void run() {
				System.out.println("[수신 시작]");

				try {
					while (true) {
						//DatagramPacket 수신
						DatagramPacket datagramPacket = new DatagramPacket(new byte[100], 100);
						datagramSocket.receive(datagramPacket);

						String data = new String(datagramPacket.getData(), 0, datagramPacket.getLength(), "UTf-8");
						System.out.println("[받는 내용: " + datagramPacket.getSocketAddress()+"]" + data);
					}
				} catch (Exception e) {
					System.out.println("[수신 종료]");
				}
			}
		};
		thread.start();

		Thread.sleep(10000);
		datagramSocket.close();

	}

}

 

 

 

<해당 포스트는 교육 목적을 위해 '이것이 자바다 신용권의 Java 프로그래밍 정복' 의 도서와 한빛미디어 유튜브를 참고하여 개인적으로 정리한 것으로 모든 내용의 출처와 저작권은 신용권과 한빛미디어에 있습니다.>