암호화 파일 송신 클라이언트
그렇다면 클라이언트를 만들어 볼까요?
닭이 먼저냐 달걀이 먼저냐는 답을 못하지만
클라이언트와 서버는 항상 서버가 먼저입니다.
하지만 이것은 어떤 것을 먼저 실행
시켜야하는 문제이지, 만드는 순서는 아닙니다.
아무래도 클라이언트가 간명하니,
클라이언트로부터 시작하겠습니다.
package filetransfer.client;
import java.net.*;
import java.io.*;
import java.text.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import filetransfer.util;
/**
* 파일을 암호화 채널을 이용해서 서버로 전송합니다.
*/
public class CipherSender {
public static void main(String[] args) {
Socket s = null; // TCP 통신 채널
OutputStream os = null; // TCP 서버로 쓰는 일반 OutputStream
CipherOutputStream cos = null; // TCP 서버로 쓰는 암호화 OutputStream
InputStream is = null; // TCP 서버로부터 읽는 일반 InputStream
InputStream fis = null; // 파일로부터 읽는 일반 InputStream
try {
if (args.length !=6) {
System.out.print("java -classpath CP filetransfer.client.CipherSender ");
System.out.println(" ip-address port id key iv file-name");
System.exit(1);
}
/*
TCP 서버에게 알려줄 클라이언트의 ID와
보낼 파일의 이름, 보낼 파일의 크기를 구합니다.
*/
String userid = args[2]; // TCP 서버에게 알려줄 클라이언트 ID
File f = new File(args[5]); // TCP 서버로 보낼 파일
String name = f.getName(); // TCP 서버로 보낼 파일 이름
long totalbytes = f.length(); // TCP 서버로 보낼 파일 크기
/*
서버로 보내는 첫 데이타는 정확히 100 bytes의 정보입니다.
100 bytes 정보는 10 + 10 + 80 bytes 형대로 구성되어 있습니다.
10 bytes는 클라이언트의 ID입니다.
10 bytes는 클라이언트가 보낼 파일의 크기입니다.
80 bytes는 클라이언트가 보내는 파일의 이름입니다.
*/
StringBuffer sb = new StringBuffer();
sb.append(Util.left(userid,10));
DecimalFormat df = new DecimalFormat("0000000000");
sb.append(df.format(totalbytes));
sb.append(Util.left(name,80));
/* 클라이언트가 서버로 보내는 첫 번째 데이타입니다.*/
String req = sb.toString();
/*
클라이언트가 TCP 서버로 TCP 연결을 시도합니다.
클라이언트는 3초 짜리 Timer를 동작시킵니다.
3초가 흐를때 까지 Timer를 해제하지 않으면
Timer가 클라이언트(Main Thread)에게 인트럽트를 보냅니다.
클라이언트(Main Thread)는 TCP 연결이 되면 Timer를 해제합니다.
*/
System.out.println("FTP Server로 접속중입니다.");
Timer timer = new Timer(Thread.currentThread(),3);
s = new Socket(args[0],Integer.parseInt(args[1]));
timer.release();
/* 클라이언트가 TCP 서버로 클라이언트 ID를 포함한 보낼 파일에
대한 정보를 담은 데이타를 보냅니다.*/
os = s.getOutputStream();
os.write(req.getBytes(),0,100);
System.out.println("FTP Server로부터 사용자인증중입니다.");
/*
서버가 보내는 첫 응답데이타는 정확히 100 bytes의 정보입니다.
100 bytes 정보는 10 + 10 + 80 bytes 형대로 구성되어 있습니다.
10 bytes는 클라이언트에게 보내는 응답코드입니다. 앞 4자리만 사용합니다.
10 bytes는 0으로 초기화 합니다.
80 bytes는 응답메세시입니다.
클라이언트가 보낸 100 bytes 데이타에 따라 적절한 응답코드를 보내며
100 bytes 데이타가 적절하지 않은 경우는 서버는 통신채널을 닫고
종료합니다.
*/
byte[] data = new byte[100];
is = s.getInputStream();
int offset = 0;
while(offset < 100) {
is.read(data,offset,1);
offset++;
}
/*
서버가 보낸 응답데이타를 응답코드와 응답메세지로 얻은 후
응답코드를 분석합니다.
응답코드가 정상이 아니면 응답메세지를 화면에 출력하고 종료합니다.
*/
String rspcode = new String(data,0,4);
String rspmessage = new String(data,20,80);
if (!rspcode.equals("0000")) {
System.out.println(rspmessage);
System.exit(1);
}
/*
데이타를 암호화 채널로 보내기 위한 암호화 엔진을 만듭니다.
암호화 엔진은 DES 알고리듬, CFB8 모드, NoPadding 을 사용합니다.
암호화 키와, Initial Vector를 이용하여 암호화(Enscript Mode)를
위한 엔진을 구성합니다.
*/
Cipher c = Cipher.getInstance("DES/CFB8/NoPadding");
SecretKeyFactory kf = SecretKeyFactory.getInstance("DES");
DESKeySpec spec = new DESKeySpec(args[3].getBytes());
SecretKey key = kf.generateSecret(spec);
IvParameterSpec iv = new IvParameterSpec(args[4].getBytes());
c.init(Cipher.ENCRYPT_MODE,key,iv);
/*
OutputStream os를 암호화 스트림인 CipherOutputStream cos로
변환하여 암호화 채널이 필요할 때는 cos로 씁니다.
*/
cos = new CipherOutputStream(os,c);
/*
TCP 서버로 보낼 파일의 InputStread을 만듭니다.
*/
fis = new FileInputStream(f);
long current = 0; // 현재 전송한 바이트 수
long section = (long)(totalbytes / 10);
int remnant = (int)(totalbytes % 10);
int percent = 0; // 현재 전송한 바이트 비율
System.out.println("파일전송중입니다.");
/* InputStream으로 데이타를 읽은 후 암호화 OutputStream으로 씁니다.*/
int r = 0;
while((r = fis.read()) > 0) {
cos.write(r);
cos.flush();
current++;
/* 클라이언트가 파일의 몇 %정도를 보냈는지 화면에 보여줍니다.*/
if (current % section != 0)
continue;
else
percent = (int)(current / section) * 10;
switch(percent) {
case 10 : System.out.print("10% ");break;
case 20 : System.out.print("20% ");break;
case 30 : System.out.print("30% ");break;
case 40 : System.out.print("40% ");break;
case 50 : System.out.print("50% ");break;
case 60 : System.out.print("60% ");break;
case 70 : System.out.print("70% ");break;
case 80 : System.out.print("80% ");break;
case 90 : System.out.print("90% ");break;
case 100 : System.out.print("100% ");break;
}
}
System.out.println();
System.out.println("전송한 파일크기를 점검하고 있습니다.");
/*
서버에게 보내기로 한 파일크기만큼 데이타를 모두 보냈으므로 서버로부터
확인 데이타를 읽습니다. 확인 데이타를 읽고,쓰는 것은 일반 통신 채널을
이용합니다.
클라이언트가 알려준 파일크기 만큼 서버가 데이타를 읽은 경우
클라이언트에게 100 bytes의 확인데이타를 보냅니다.
확인데이타는 일반 통신 채널로 보냅니다.
클라이언트에게 보내는 확인 데이타는 정확히 100 bytes의 정보입니다.
100 bytes 정보는 10 + 10 + 80 bytes 형대로 구성되어 있습니다.
10 bytes는 클라이언트에게 보내는 응답코드입니다. 앞 4자리만 사용합니다.
10 bytes는 서버가 클라이언트로부터 받은 데이타의 크기입니다.
80 bytes는 서버가 알려주는 응답메세시입니다.
*/
byte[] confirm = new byte[100];
offset = 0;
while(offset < 100) {
is.read(confirm,offset,1);
offset++;
}
rspcode = new String(confirm,0,4);
if (rspcode.equals("0000")) {
/*
클라이언트가 보낸 데이타를 서버가 모두 받았을 경우
서버가 받았다는 데이타의 크기와 실제 서버로 보낸 데이타의 크기를
비교하여 그 결과를 확인 응답 데이타로 서버에 보냅니다.
*/
long rcvedbytes = Long.parseLong(new String(confirm,10,10));
/*
서버가 실제 받은 데이타와 클라이언트가 처음에 보내기로 한 파일크기,
클라이언트가 보낸 실제 크기가 동일한 경우 정상의 응답코드를 보냅니다.
*/
if (rcvedbytes == totalbytes && rcvedbytes == current){
sb = new StringBuffer();
sb.append(Util.left("0000",10));
sb.append(Util.right("",10));
sb.append(Util.left("",80));
os.write(sb.toString().getBytes(),0,100);
System.out.println("파일을 성공적으로 전송했습니다.");
}
/*
서버가 실제 받은 데이타와 클라이언트가 처음에 보내기로 한 파일크기,
클라이언트가 보낸 실제 크기가 같지 않은 경우 적절한 응답코드를
보냅니다. 화면에는 오류 메세지를 보여줍니다.
클라이언트가 응답 확인 데이타에 정상의 응답코드를 보내지 않으면
서버는 받은 데이타를 저장하지 않고 삭제합니다.
*/
else {
System.out.println("전송한 파일크기에 문제가 있습니다.");
System.out.println("Client Sent: " + current + ", Server Received: " +
rcvedbytes);
sb = new StringBuffer();
sb.append(Util.left("0001",10));
sb.append(Util.right("",10));
sb.append(Util.left("",80));
os.write(sb.toString().getBytes(),0,100);
}
}
else {
/*
클라이언트가 보낸 데이타를 서버가 제대로 받지 못한 경우
적절한 응답코드를 보냅니다.
화면에는 오류 메세지를 보여줍니다.
클라이언트가 응답 확인 데이타에 정상의 응답코드를 보내지 않으면
서버는 받은 데이타를 저장하지 않고 삭제합니다.
*/
rspmessage = new String(confirm,20,80);
System.out.println("파일전송을 실패했습니다. " + rspmessage);
sb = new StringBuffer();
sb.append(Util.left("0001",10));
sb.append(Util.right("",10));
sb.append(Util.left("",80));
os.write(sb.toString().getBytes(),0,100);
}
}
catch(Exception e) {
System.out.println("장애가 발생했습니다. 아래의 메세지를 참조하세요.");
e.printStackTrace();
}
finally {
try {
/* 네트워크 및 시스템 자원을 해제합니다.*/
if (is != null)
is.close();
if (os != null)
os.close();
if (cos != null)
cos.close();
if (fis != null)
fis.close();
if (s != null)
s.close();
}
catch(Exception ignore) {
}
}
}
}
예제 14 - 1 CipherSender.java
예제 14 - 1 은 CipherSender라는 암화화 파일전송 프로그램입니다.
인터넷 등 안전하지 않은 네트워크를 이용해서 안전하게 파일을 전송하는 파일 전송
클라이언트입니다. 특징은 대표적인 암호화 알고리즘인 DES를 이용해서 안전하게
네트워크를 통해서 파일을 보낼 수 있다는 건데,무엇보다 아주 간명한 프로토콜을
사용합니다. 네트워크 프로그램에서 아주 유용한 예제가 될것으로 생각합니다.
Util 클래스
CipherSender는 서버로 데이타를 보내는데 아주 정형화된 100 바이트의 정보를 보냅니다.
이 정형화된 정보에는 미리 정해진 길이를 가지는 정보를 3개 포함합니다.
이때 사용하기 편한 Util 클래스를 도입하고 있는데 간단히 소개하겠습니다.
스트링 관련 연산을 할때 도움이 될 것 입니다.
package filetransfer.util;
/**
* 스트링 Utility 클래스입니다.
*/
public class Util {
/**
* 입력 스트링 s를 바이트 배열로 표현했을때의 길이
* @param s 입력 스트링
* @return int 입력 스트링을 바이트 배열 표현했을때의 길이
* 한글 1글자의 길이는 2로 리턴
*/
public static int length(String s) {
if (s == null)
return 0;
else
return s.getBytes().length;
}
/**
* 입력 스트링 s를 바이트 배열로 나타냈을때 길이를 n이라고 하면,
* 만약 n 이 len 보다 크거나 같다면 len 바이트만 리턴합니다.
* 만약 n 이 len 보다 작다면 모자라는 부분을 s의 끝부분에 ' '를 더해서
* len 바이트의 길이를 가지는 스트링을 리턴한다.
* left("abcd",10)의 결과는 "abcd " 이다.
*
* @param s 입력스트링
* @param len 스트링이 반드시 가져야할 바이트 길이
*/
public static String left(String s, int len) {
int c = length(s);
if (c > len) {
byte[] b = s.getBytes();
return new String(b,0,len);
}
else {
int pad = len - length(s);
StringBuffer sb = new StringBuffer();
sb.append(s);
for(int i = 0; i < pad ; i++)
sb.append(" ");
return sb.toString();
}
}
/**
* 입력 스트링 s를 바이트 배열로 나타냈을때 길이를 n이라고 하면,
* 만약 n 이 len 보다 크거나 같다면 len 바이트만 리턴합니다.
* 만약 n 이 len 보다 작다면 모자라는 부분을 s의 첫부분에 '0'를 더해서
* len 바이트의 길이를 가지는 스트링을 리턴한다.
* right("3500",10)의 결과는 "0000003500" 이다.
*
* @param s 입력스트링
* @param len 스트링이 반드시 가져야할 바이트 길이
*/
public static String right(String s, int len) {
int c = length(s);
if (c > len) {
byte[] b = s.getBytes();
return new String(b,0,len);
}
else {
int pad = len - length(s);
StringBuffer sb = new StringBuffer();
for(int i = 0; i < pad ; i++)
sb.append("0");
sb.append(s);
return sb.toString();
}
}
}
예제 14 - 2 Util.java
예제 14 - 2의 Util 클래스는 3개의 static 메소드를 갖고 있는데, 정형화된 정보에
특정한 길이의 일반 문자정보(한글을 포함)를 담을때와 숫자정보를 담을때 유용하게
사용할 수 있습니다.
'자바 Network Programming' 카테고리의 다른 글
TCP 소켓을 이용한 암호화된 파일 전송 프로그래밍 - 1 (0) | 2008.04.07 |
---|---|
UDP Socket-5.One-To-Many 네트워크 프로그램 (0) | 2008.04.02 |
UDP Socket-4.One-To-One 네트워크 프로그램 (0) | 2008.04.02 |
UDP Socket-3.TCP와 UDP의 선택 (0) | 2008.04.02 |
UDP Socket-2.TCP 네트워크 프로그램을 UDP로 구현 (0) | 2008.04.02 |