[Java] 파일 전송 예제


Development note/Java  2015. 6. 11. 00:31

안녕하세요. 명월입니다.


이번 포스트는 파일 전송 모듈에 대해 작성해 보겠습니다.


파일을 전송하는 소스는 많이 사용하면서 제대로 정의되어 있는 소스가 없는 것 같아서 제가 구현해 보았습니다. 물론 고수분들은 이런 게 필요 없겠지요...ㅜㅜ

제가 만들 파일 전송 클래스는 총 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

소스 -

FileTransfer.zip