[JAVA] TCP 소켓통신 파일전송 단계별 구현 - 파일 끊어서 전송하기
[JAVA] TCP 소켓통신 파일전송 단계별 구현 - 파일 끊어서 전송하기 — #JAVA소켓통신 #소켓통신 #소켓통신파일전송 #소켓통신파일 #개발자의도구들 AI스쿨 msa기반 java 백...
#JAVA소켓통신 #소켓통신 #소켓통신파일전송 #소켓통신파일 #개발자의도구들
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 소켓통신 프로젝트를 진행중입니다. 전체적인 목차를 보시려면 여기에서 참고하세요
파일 끊어서 전송하기
이전글을 통해서 fileStream이 어떻게 동작하는지에 대해 배웠고, 직접 보내고 저장까지 해보았습니다. 하지만, 현실적으로 사용하기 어려운 프로토콜이었는데요. 이유는 파일크기를 한번에 할당하여 전송하기 때문에 큰 사이즈의 파일을 전송할 수가 없었습니다.
이번글에서는 큰 파일도 전송가능하도록 파일을 잘라서 보내는 방법에 대해 다뤄보겠습니다.
프로토콜 수정
fileName과 seq를 곁들인
기존 프로토콜
수정 후 프로토콜
기존에 프로토콜에 새로운 정보들을 추가해 봅시다. 프로토콜을 좀 더 간편하게 만들기 위해 fileName사이즈를 4바이트로 고정시켰습니다. (이는 파일 이름이 4바이트 이상이면 전송하지 못하도록 로직을 추가해야합니다)
파일은 잘려서 전송되므로 seq를 4바이트로 할당하여 fileByte앞에 전송합니다.
송신측 클라이언트
fileName, seq추가
먼저 바이트를 찍어보면서 전송하기 위해 작은 바이트를 끊어서 전송해보겠습니다. 89 Byte 크기의 파일 데이터를 10Byte씩 끊어서 전송할 것입니다.
| private void sendTestFile(String receiver, String fileName) { File file = new File("C:\\Users\\yangd\\OneDrive\\바탕 화면\\filetest", fileName); try { FileInputStream fileInputStream = new FileInputStream(file); byte\[\] readByte = new byte\[10\]; int byteRead; int seq = 0; while ((byteRead = fileInputStream.read(readByte)) != -1) { byte\[\] actualBytessRead = Arrays.copyOf(readByte, byteRead); byte\[\] testHeader = FileProcessor.getTestFileHeader(MessageType.TEST, receiver, fileName, seq, actualBytessRead); System.out.println("maded packet: " + Arrays.toString(testHeader)); // System.out.print("\n seq: " + seq + " sending file packet\n"); // System.out.println("testHeader length: " + testHeader.length); dataOutputStream.write(testHeader,0, testHeader.length); dataOutputStream.flush(); seq ++; } // System.out.println("all filePacket be sent"); sendPacket(MessageType.TEST\_SEND\_END, fileName); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); }} |
|---|
이번에는 fileName과 seq도 같이 FileProcessor에게 넘겨줍니다. 10바이트씩 끊어서 전송하며 바이트 전송이 완료되면 seq값을 1증가시켜 계속해서 넘겨줍니다.
마지막으로 TEST\_SEND\_END패킷을 전달하여, 파일 전송이 완료됨을 알려줍니다. 현재는 id가 구현되어 있지 않으니 모든 클라이언트에게 전달될 것이며 fileName 정보를 추가로 주어 해당 파일의 종료를 알립니다.
FileProcessor
fileName, seq추가
FileProcesoor는 이번에 String정보도 함께 받습니다. 서버에서는 String정보를 받지 않고 헤더를 만들기 때문에 이전 버전에서 따로 서버 전용 헤더 메서드를 만들었었습니다.
| public static byte\[\] getTestFileHeader(MessageType messageType, String fileName, int seq, byte\[\] body) { // idlength(4) + id + fileName(4) + seq(4) String onlyName = fileName.substring(0, fileName.lastIndexOf('.')); byte\[\] fileNameByte = onlyName.getBytes(StandardCharsets.UTF\_8); System.out.println("\n fileName Byte: " + Arrays.toString(fileNameByte)); System.out.println("fileNameByteSize: " + fileNameByte.length); if (fileNameByte.length > 4) { throw new FileNameLengthException("FileName length must be under 4"); } int bodyLengthByteSize = 4; int typeByteSize = 4; int fileNameByteSize = 4; int seqByteSize = 4; int fileByteSize = body.length; System.out.println("fileByteSize: " + fileByteSize); ByteBuffer byteBuffer = ByteBuffer.allocate(bodyLengthByteSize + typeByteSize + fileNameByteSize + seqByteSize + fileByteSize); System.out.println("byteBuffer length: " + byteBuffer.capacity()); // body length byteBuffer.putInt(fileNameByteSize + seqByteSize + fileByteSize); byteBuffer.putInt(messageType.ordinal()); byteBuffer.put(fileNameByte); byteBuffer.putInt(seq); byteBuffer.put(body); return byteBuffer.array();} |
|---|
수정 후 프로토콜
만든 프로토콜을 참고하여 ByteBuffer에 차곡 차곡 데이터를 넣어줍니다. 이제 헤더의 bodyLength는 단순 파일 크기 뿐아니라, fileName과 seq길이도 함께 들어있습니다. 이는 수신측에서 파일 길이의 정보를 확인할때 유용하게 사용됩니다. (전체길이 - 4 - 4)
fileName은 체크하여 길이가 4이상인 경우 보내지않도록 예외처리를 하였습니다.
서버측
서버는 변동사항이 딱히 없습니다. 이전글을 참고하세요.
수신측 코드
이제 수신측에서는 단순히 받고 저장하는 것이 아니라, fileManager를 사용하여 데이터를 저장하는 로직도 필요합니다.
| public void run() { long threadId = Thread.currentThread().getId(); System.out.println("myid: "+ threadId); try { System.out.println("\n\[ Request ... \]"); socket.connect(new InetSocketAddress("localhost", tcpClientPort)); System.out.print("\n \[ Success connecting \] \n"); while (true) { OutputStream outputStream = socket.getOutputStream(); dataOutputStream = new DataOutputStream(outputStream); InputStream inputStream = socket.getInputStream(); DataInputStream dataInputStream = new DataInputStream(inputStream); int inAllLength = dataInputStream.readInt(); if(inAllLength > 0) { byte\[\] inLengthByte = new byte\[4\]; dataInputStream.readFully(inLengthByte); int messageLength = Share.readInputLength(inLengthByte); byte\[\] inTypeByte = new byte\[4\]; dataInputStream.readFully(inTypeByte); int typeInt = ByteBuffer.wrap(inTypeByte).getInt(); MessageType inMessageType = MessageType.values()\[typeInt\]; byte\[\] inMessageByte = new byte\[inAllLength - 8\]; dataInputStream.readFully(inMessageByte); String message = Share.readInputMessage(inMessageByte); if (inMessageType == MessageType.TEST) {; storeFile(inMessageByte, messageLength - inLengthByte.length - inTypeByte.length); } if (inMessageType == MessageType.TEST\_SEND\_END) {; combineFile(inMessageByte); } } else if(inAllLength == 0){ // quit break; } } } catch (Exception e) { throw new RuntimeException(e); }} |
|---|
MessageType이 TEST인 경우 storeFile()을, TEST\_SEND\_END인 경우 combineFile()을 실행해야합니다. 파일 저장시에는 파일의 크기를 계산해서 넘겨줍니다.
둘다 파일 매니저를 통해 동작합니다.
| private void storeFile(byte\[\] body, int fileLength) { fileManager.testStore(body, fileLength);}private void combineFile(byte\[\] fileNameByte) { // System.out.println("combinFile start: fileNameByte: " + Arrays.toString(fileNameByte)); byte\[\] totalFileByte = fileManager.getCombineFileTestByte(fileNameByte); //System.out.println("totalFileByte: " + Arrays.toString(totalFileByte)); saveFile(totalFileByte);}// save in pathprivate void saveFile(byte\[\] body) { // has only body(flieName, seq, fileByte\[\])\_ //System.out.print("start to save file/ \n totalData: " + Arrays.toString(body)); try { String randomFileName = UUID.randomUUID().toString() + ".txt"; String directoryPath = "C:\\Users\\yangd\\OneDrive\\바탕 화면\\filetest\\outputPath"; File file = new File(directoryPath, randomFileName); FileOutputStream fileOutputStream = new FileOutputStream(file); fileOutputStream.write(body); fileOutputStream.flush(); System.out.println("File saved successfully: " + file.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); }} |
|---|
FileManager
파일 매니저는 바이트 정보를 가지고 있습니다. 물론 여기서 바이트는 헤더 정보는 없어진 상태입니다. bodyLength는 파일 크기를 유추하는데 중요한 정보라서, 이전단계에서 넘겨받았습니다.
| fileMap = { "fileName": { seq1(Integer} : byte\[\], seq2 : byte\[\], seq3: byte\[\] }} |
|---|
원하는 자료형태는 위와 같습니다. 이를 맞춰서 로직을 추가해주면됩니다.
| public void testStore(byte\[\] body, int fileLength) { ByteBuffer bodyBuffer = ByteBuffer.wrap(body);} |
|---|
ByteBuffer는 바이트를 읽어 다른 형태로 변환하는데도 편리한 메서드를 제공합니다. 참고사항은 ByteBuffer를 읽었다고 해서 스트림처럼 데이터가 삭제되는 것이 아닙니다.
ByteBuffer는 데이터를 읽으면 내부에서 position이 변경됩니다. 0부터시작하여 4바이트를 읽은 후라면, position은 4가 됩니다. 이는 .position()으로 찍어보면 알 수 있습니다.
| public void testStore(byte\[\] body, int fileLength) { //System.out.println("in test Store: Client: filePacket received : " + Arrays.toString(body)); // original body를 들고있다. ByteBuffer bodyBuffer = ByteBuffer.wrap(body); //System.out.println("first bodyBuffer: " + Arrays.toString(bodyBuffer.array())); byte\[\] fileNameByte = new byte\[4\]; bodyBuffer.get(fileNameByte); String fileName = new String(fileNameByte); //System.out.println("store fileName: " + fileName ); //System.out.println("remain bodyBuffer" + Arrays.toString(bodyBuffer.array()) + "\nposition: " + bodyBuffer.position());} |
|---|
fileName으로 사용할 4바이트를 할당 후 ByteBuffer.get(할당할 바이트 배열)로 바이트를 옮겨줍니다. 크기를 4로 지정했으니 이후 position은 4가 됩니다.
| public void testStore(byte\[\] body, int fileLength) { //System.out.println("in test Store: Client: filePacket received : " + Arrays.toString(body)); // original body를 들고있다. ByteBuffer bodyBuffer = ByteBuffer.wrap(body); //System.out.println("first bodyBuffer: " + Arrays.toString(bodyBuffer.array())); byte\[\] fileNameByte = new byte\[4\]; bodyBuffer.get(fileNameByte); String fileName = new String(fileNameByte); //System.out.println("store fileName: " + fileName ); //System.out.println("remain bodyBuffer" + Arrays.toString(bodyBuffer.array()) + "\nposition: " + bodyBuffer.position()); int seq = bodyBuffer.getInt(); //System.out.println("store seq: " + seq); } |
|---|
이후에는 getInt()메서드를 사용하여 seq를 얻어옵니다. 이는 4바이트를 끊어서 읽고 정수로 변환해주는 편리한 메서드입니다.
| public void testStore(byte\[\] body, int fileLength) { //System.out.println("in test Store: Client: filePacket received : " + Arrays.toString(body)); // original body를 들고있다. ByteBuffer bodyBuffer = ByteBuffer.wrap(body); //System.out.println("first bodyBuffer: " + Arrays.toString(bodyBuffer.array())); byte\[\] fileNameByte = new byte\[4\]; bodyBuffer.get(fileNameByte); String fileName = new String(fileNameByte); //System.out.println("store fileName: " + fileName ); //System.out.println("remain bodyBuffer" + Arrays.toString(bodyBuffer.array()) + "\nposition: " + bodyBuffer.position()); int seq = bodyBuffer.getInt(); //System.out.println("store seq: " + seq); byte\[\] fileByte = new byte\[fileLength\]; bodyBuffer.get(fileByte); //System.out.println("store fileByte: " + Arrays.toString(fileByte)); TreeMap<Integer, byte\[\]> seqFileMap; if (!fileMap.containsKey(fileName)) { seqFileMap = new TreeMap<Integer, byte\[\]>(); } else { seqFileMap = fileMap.get(fileName); } seqFileMap.put(seq, fileByte); fileMap.put(fileName, seqFileMap); for (Map.Entry<Integer, byte\[\]> entry : fileMap.get(fileName).entrySet()) { // // System.out.println("Key: " + entry.getKey() + ", Value: " + Arrays.toString(entry.getValue())); }} |
|---|
마지막으로 이전 단계에서 입력받은 fileLength만큼 데이터를 읽어 fileByre그대로 저장합니다. 이후 TreeMap을 이용하여 seq:byte\[\]를 저장하고, fileName을 상위 맵의 key로 하여 TreeMap을 안에 저장해줍니다.
파일저장하기
| public byte\[\] getCombinedFile(String id) throws IllegalAccessException { TreeMap<Integer, byte\[\]> fileSeq = fileMap.get(id); if (fileSeq == null) { throw new IllegalAccessException("ID not found: " + id); } ByteBuffer combinedFile = ByteBuffer.allocate(fileSeq.values().stream().mapToInt(arr -> arr.length).sum()); fileSeq.forEach((seq, bytes) -> combinedFile.put(bytes)); return combinedFile.array();} |
|---|
이후 fileMap을 순회하면서 모든 바이트를 합친 후 리턴합니다.
| private void saveFile(byte\[\] body) { // has only body(flieName, seq, fileByte\[\])\_ //System.out.print("start to save file/ \n totalData: " + Arrays.toString(body)); try { String randomFileName = UUID.randomUUID().toString() + ".txt"; String directoryPath = "C:\\Users\\yangd\\OneDrive\\바탕 화면\\filetest\\outputPath"; File file = new File(directoryPath, randomFileName); FileOutputStream fileOutputStream = new FileOutputStream(file); fileOutputStream.write(body); fileOutputStream.flush(); System.out.println("File saved successfully: " + file.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); }} |
|---|
클라이언트 측으로 totalByte를 옮긴 후 이전글과 동일한 방식으로 저장하면 끝입니다!



