안녕하세요. 명월입니다.
이번 포스트는 파일 전송 모듈에 대해 작성해 보겠습니다.
파일을 전송하는 소스는 많이 사용하면서 제대로 정의되어 있는 소스가 없는 것 같아서 제가 구현해 보았습니다. 물론 고수분들은 이런 게 필요 없겠지요...ㅜㅜ
제가 만들 파일 전송 클래스는 총 7개로 이루어져 있습니다.
package kr.pe.nowonbun.filetransfer;
import java.net.InetSocketAddress;
public class FileTransferAddress extends InetSocketAddress {
private static final long serialVersionUID = 1L;
public FileTransferAddress(String hostname, int port) {
super(hostname, port);
}
public int getPortnumber(){
return super.getPort();
}
}
위 클래스는 InetSocketAddress를 상속받았고, 역할은 클라이언트의 경우 접속 IP, Port 설정을 위해 필요한 클래스 입니다.
package kr.pe.nowonbun.filetransfer;
public class FileTransferBitConverter {
public static final int INTBITSIZE = 4;
public static byte[] getBytes(boolean x) {
return new byte[] { (byte) (x ? 1 : 0) };
}
public static byte[] getBytes(char c) {
return new byte[] {
(byte) (c & 0xff),
(byte) (c >> 8 & 0xff) };
}
public static byte[] getBytes(double x) {
return getBytes(Double.doubleToRawLongBits(x));
}
public static byte[] getBytes(short x) {
return new byte[] {
(byte) (x >>> 8),
(byte) x };
}
public static byte[] getBytes(int x) {
return new byte[] {
(byte) (x >>> 24),
(byte) (x >>> 16),
(byte) (x >>> 8),
(byte) x };
}
public static byte[] getBytes(long x) {
return new byte[] { (byte) (x >>> 56), (byte) (x >>> 48),
(byte) (x >>> 40), (byte) (x >>> 32), (byte) (x >>> 24),
(byte) (x >>> 16), (byte) (x >>> 8), (byte) x };
}
public static byte[] getBytes(float x) {
return getBytes(Float.floatToRawIntBits(x));
}
public static byte[] getBytes(String x) {
return x.getBytes();
}
public static long doubleToInt64Bits(double x) {
return Double.doubleToRawLongBits(x);
}
public static double int64BitsToDouble(long x) {
return (double) x;
}
public boolean toBoolean(byte[] bytes, int index) throws Exception {
if (bytes.length != 1)
throw new Exception(
"The length of the byte array must be at least 1 byte long.");
return bytes[index] != 0;
}
public char toChar(byte[] bytes, int index) throws Exception {
if (bytes.length != 2)
throw new Exception(
"The length of the byte array must be at least 2 bytes long.");
return (char) ((0xff & bytes[index]) << 8 | (0xff & bytes[index + 1]) << 0);
}
public double toDouble(byte[] bytes, int index) throws Exception {
if (bytes.length != 8)
throw new Exception(
"The length of the byte array must be at least 8 bytes long.");
return Double.longBitsToDouble(toInt64(bytes, index));
}
public static short toInt16(byte[] bytes, int index) throws Exception {
if (bytes.length != 8)
throw new Exception(
"The length of the byte array must be at least 8 bytes long.");
return (short) ((0xff & bytes[index]) << 8 | (0xff & bytes[index + 1]) << 0);
}
public static int toInt32(byte[] bytes, int index) throws Exception {
if (bytes.length != 4)
throw new Exception(
"The length of the byte array must be at least 4 bytes long.");
return (int) ((int) (0xff & bytes[index]) << 56
| (int) (0xff & bytes[index + 1]) << 48
| (int) (0xff & bytes[index + 2]) << 40 | (int) (0xff & bytes[index + 3]) << 32);
}
public static long toInt64(byte[] bytes, int index) throws Exception {
if (bytes.length != 8)
throw new Exception(
"The length of the byte array must be at least 8 bytes long.");
return (long) ((long) (0xff & bytes[index]) << 56
| (long) (0xff & bytes[index + 1]) << 48
| (long) (0xff & bytes[index + 2]) << 40
| (long) (0xff & bytes[index + 3]) << 32
| (long) (0xff & bytes[index + 4]) << 24
| (long) (0xff & bytes[index + 5]) << 16
| (long) (0xff & bytes[index + 6]) << 8 | (long) (0xff & bytes[index + 7]) << 0);
}
public static float toSingle(byte[] bytes, int index) throws Exception {
if (bytes.length != 4)
throw new Exception(
"The length of the byte array must be at least 4 bytes long.");
return Float.intBitsToFloat(toInt32(bytes, index));
}
public static String toString(byte[] bytes) throws Exception {
if (bytes == null)
throw new Exception("The byte array must have at least 1 byte.");
return new String(bytes);
}
}
Bitconverter는 숫자를 byte 형식 또는 byte형식을 숫자형식으로 변환시켜주는 Converter입니다. 예전 C#에 있는 클래스인데 아주 유용하게 쓰던 기억이 있어서 Java도 비슷하게 만들어서 가져왔습니다. 이 모듈에서의 역할은 파일의 사이즈를 상대 수신 쪽에 보내주기 위해 byte로 변환하는 클래스입니다.
package kr.pe.nowonbun.filetransfer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.Stack;
public class FileTransferClient extends Socket implements Runnable{
private Thread thread = null;
private InputStream receiver = null;
private OutputStream sender = null;
private Stack<Exception> errorList = null;
private FileTransferAddress address = null;
private File savefile = null;
private FileTransferListener listener = null;
protected FileTransferClient(File savefile,int processSize) throws IOException{
this.savefile = savefile;
}
/**
* 서버 시작 메소드(보내기, 받기 스트림을 선언한다)
*/
protected void serverStart() throws IOException{
thread = new Thread(this);
receiver = getInputStream();
sender = getOutputStream();
errorList = new Stack<Exception>();
thread.start();
}
/**
* 생성자
*/
public FileTransferClient(FileTransferAddress address,File savefile) throws IOException{
this.savefile = savefile;
this.address = address;
thread = new Thread(this);
errorList = new Stack<Exception>();
}
/**
* 접속 메소드
*/
private void connection() throws IOException{
super.connect(address);
receiver = getInputStream();
sender = getOutputStream();
thread.start();
}
/**
* 접속 커넥션 종료 메소드
*/
@Override
public void close() throws IOException{
super.close();
if(listener != null){
listener.connectionClose();
}
}
/**
* 리스너 등록
*/
public void setFileTransferListener(FileTransferListener listener){
this.listener = listener;
}
/**
* 실행 메소드
*/
@Override
public void run() {
byte[] lengthData = null;
int length = 0;
String filename = "";
FileOutputStream out = null;
try{
lengthData = new byte[FileTransferBitConverter.INTBITSIZE];
//파일이름 사이즈를 받는다.
receiver.read(lengthData,0,lengthData.length);
length = FileTransferBitConverter.toInt32(lengthData, 0);
//파일 사이즈가 없으면 종료한다.
if(length == 0){
return;
}
// 다운로드 시작 리스너 호출(이벤트 형식)
if(listener != null){
listener.downloadStart();
}
// 파일 이름 설정
byte[] filenamebyte = new byte[length];
receiver.read(filenamebyte,0,filenamebyte.length);
filename = new String(filenamebyte);
File file = new File(savefile.getPath() + "\\" + filename);
//파일이 있으면 삭제
if(file.exists()) file.delete();
out = new FileOutputStream(file);
//파일 사이즈를 받는다.
receiver.read(lengthData,0,lengthData.length);
length = FileTransferBitConverter.toInt32(lengthData, 0);
//파일 사이즈가 없으면 종료
if(length == 0){
return;
}
//파일 받기 시작
receiveWrite(out,length,listener);
// 다운로드 종료 리스너 호출(이벤트 형식)
if(listener != null){
listener.downloadComplate();
listener.fileSaveComplate(savefile.getPath() + "\\" + filename);
}
} catch (Exception e) {
// 에러가 발생하면 에러 리스너 호출
if(listener != null){
listener.receiveError(e);
}
errorList.push(e);
} finally{
try{
if(isConnected()){
close();
}
if(out != null){
out.close();
}
}catch(Exception ex){
if(listener != null){
listener.receiveError(ex);
}
errorList.push(ex);
}
}
}
public FileTransferAddress getAddress(){
return this.address;
}
/**
* 파일 전송 메소드
*/
public void sendFile(File file) throws FileTransferException,IOException{
// 서버 접속
connection();
// 파라미터 체크
if(file == null) {
throw new FileTransferException("File path not setting");
}
// 전송 파일 체크
if(!file.isFile()) {
throw new FileTransferException("File path not setting");
}
// 접속 체크
if(!isConnected()) {
throw new FileTransferException("Socket is closed");
}
//파일 이름 체크
String filename = file.getName();
if(filename == null){
throw new FileTransferException("File path not setting");
}
FileInputStream in = null;
byte[] databyte = null;
byte[] filenamebyte = filename.getBytes();
try{
// 리스너 업로드 개시 호출
if(listener != null){
listener.uploadStart();
}
in = new FileInputStream(file);
byte[] length = FileTransferBitConverter.getBytes(filenamebyte.length);
//파일 이름 사이즈 전송
sender.write(length,0,FileTransferBitConverter.INTBITSIZE);
//파일 이름 전송
sender.write(filenamebyte,0,filenamebyte.length);
//파일 사이즈 전송
length = FileTransferBitConverter.getBytes((int)file.length());
sender.write(length,0,FileTransferBitConverter.INTBITSIZE);
//파일 전송
databyte = new byte[(int)file.length()];
in.read(databyte,0,databyte.length);
sender.write(databyte,0,databyte.length);
// 리스너 파일 사이즈 호출(이벤트 형식)
if(listener != null){
listener.progressFileSizeAction(databyte.length,filenamebyte.length);
}
// 리스너 업로드 완료 호출
if(listener != null){
listener.uploadComplate();
}
}catch(IOException e){
throw e;
}finally{
in.close();
}
}
/**
* 파일 수신 메소드
*/
private void receiveWrite(FileOutputStream out,int length,FileTransferListener listener)
throws Exception{
//커넥션 체크
if(isClosed()) {
throw new SocketException("socket closed");
}
if(!isConnected()) {
throw new SocketException("socket diconnection");
}
byte[] buffer = new byte[4096];
int progressCount = 0;
while(progressCount < length){
int bufferSize = 0;
while((bufferSize = receiver.read(buffer)) > 0){
out.write(buffer, 0, bufferSize);
progressCount += bufferSize;
// 리스너 파일 수신 진행율 호출
if(listener != null){
listener.progressFileSizeAction(progressCount,length);
}
if(progressCount >= length){
break;
}
}
}
}
public Exception getLastError(){
if(errorList.size() > 0){
Exception e = errorList.pop();
return e;
}else{
return null;
}
}
}
다음은 핵심 클래스입니다. 실제로 이 클래스에서 데이터 전송, 수신, 프로토콜이 정해지는 곳이라고 할 수 있네요.
멀티스레드 환경을 만들기 위해 Runnable을 상속받고 Socket을 상속받아서 사용법을 개선했네요.. 그냥 소켓처럼 사용하면 파일 송수신이 이루어지겠습니다.
package kr.pe.nowonbun.filetransfer;
import java.net.SocketException;
public class FileTransferException extends SocketException {
private static final long serialVersionUID = 1L;
public FileTransferException(){
}
public FileTransferException(String arg){
super(arg);
다음은 예외 클래스입니다. SocketException 을 상속받아 사용하는데 실제 FileTransferException로 throw 구간이 없으니깐 어찌 보면 필요가 없는 클래스 입니다.
package kr.pe.nowonbun.filetransfer;
public interface FileTransferListener {
public void progressFileSizeAction(long complateSize,long filesize);
public void downloadStart();
public void downloadComplate();
public void uploadStart();
public void uploadComplate();
public void fileSaveComplate(String filepath);
public void receiveError(Exception e);
public void connectionClose();
}
FileTransferListener클래스는 FileTransferClient클래스에서 사용되는 이벤트 리스너입니다. 다른 것은 그렇다 치더라고 progressFileSizeAction의 경우에는 파일이 전송 진행사항을 나타내는 함수입니다.
package kr.pe.nowonbun.filetransfer;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Stack;
public class FileTransferServer extends ServerSocket implements Runnable{
private ArrayList<FileTransferClient> clients = null;
private Thread thread = null;
private Stack<Exception> errorList = null;
private File savePath = null;
private int processSize = 1;
private FileTransferServerListener listener = null;
public FileTransferServer(int port,File savePath) throws IOException,FileTransferException{
this(port,savePath,1);
}
/**
* 생성자
*/
public FileTransferServer(int port,File savePath,int processSize) throws IOException,FileTransferException{
super(port);
this.savePath = savePath;
if(!savePath.isDirectory() || !savePath.exists()){
throw new FileTransferException("File Path wrong!");
}
errorList = new Stack<Exception>();
clients = new ArrayList<FileTransferClient>();
this.processSize = processSize;
this.start();
}
/**
* 서버 개시
*/
public void start() throws FileTransferException{
thread = new Thread(this);
thread.start();
}
public File getFile(){
return this.savePath;
}
public void setFileTransferServerListener(FileTransferServerListener listener){
this.listener = listener;
}
/**
* 클라이언트가 접속을 할 때 실행되는 메소드
*/
public FileTransferClient accept() throws FileTransferException,IOException{
if (isClosed())
throw new FileTransferException("Socket is closed");
if (!isBound())
throw new FileTransferException("Socket is not bound yet");
if(this.savePath == null)
throw new FileTransferException("file path wrong!");
FileTransferClient s = new FileTransferClient(this.savePath,this.processSize);
implAccept(s);
s.serverStart();
return s;
}
/**
* 멀티 스레드 환경
*/
@Override
public void run(){
while(true){
try{
FileTransferClient client = this.accept();
clients.add(client);
if(this.listener != null){
this.listener.clientConnection(client);
}
}catch(IOException e){
errorList.push(e);
if(this.listener != null){
this.listener.connectionError(e);
}
}
}
}
public Exception getLastError(){
if(errorList.size() > 0){
Exception e = errorList.pop();
return e;
}else{
return null;
}
}
/**
* 서버 종료
*/
public void close() throws IOException{
for(Socket client : clients){
client.close();
}
super.close();
if(this.listener != null){
this.listener.connectionClose();
}
}
}
FileTransferServer.javas는 서버 클래스입니다. ServerSocket을 상속받아서 인터페이스는 ServerSocket과 같습니다. 기본적으로 생성자 선언(new)을 하면 바로 server대기 상태로 됩니다. 종료 부분이 아직 조금 미흡하네요
사용할 때는 이 종료 부분을 다듬어서 사용해야 하겠습니다.
package kr.pe.nowonbun.filetransfer;
import java.io.IOException;
public interface FileTransferServerListener {
public void clientConnection(FileTransferClient client);
public void connectionError(IOException e);
public void connectionClose();
}
여기까지 파일 전송 모듈을 만들었으면 이번에는 실행을 시켜 보겠습니다.
import java.io.File;
public class ServerMain{
public static void main(String[] args){
try{
FileTransferServer server = new FileTransferServer(9999,new File("D:\\test"));
}catch(Exception e){
e.printStatckTrace();
}
}
}
import java.io.File;
public class ClientMain{
public static void main(String[] args) {
try{
FileTransferAddress address = new FileTransferAddress("127.0.0.1",9999)
File file = new File("d:\\test1");
FileTransferClient client = new FileTransferClient(address,file);
client.sendFile(new File("d:\\t.zip"));
}catch(Exception e){
e.printStackTrace();
}
}
}
파일 전송 모듈을 통해 Client에서 서버쪽으로 t.zip을 전송하였습니다.
여기까지 제가 작성한 부분에서는 잘 되네요..
jar - FileTransfer.jar
소스 -
'Development note > Java' 카테고리의 다른 글
[Java] Gson을 이용한 Json 다루기 (0) | 2019.06.19 |
---|---|
[Java] Base64 인코딩, 디코딩하는 방법 (0) | 2019.06.17 |
[Java] cmd 명령어를 실행하기 위한 방법 (Process 클래스) (0) | 2019.06.05 |
[Java] 콘솔 입력 값을 받는 방법 (System.in) (0) | 2019.06.05 |
[Java] Zip 압축 해제하기 (1) | 2015.06.08 |
[Java] Zip 압축하기 (3) | 2015.06.08 |
[Java] 계산기 프로그램 (1) | 2015.06.07 |
[Java] BitConverter (0) | 2015.06.05 |