[JAVA] TCP 소켓통신 파일전송 원하는 소켓에만 보내기
[JAVA] TCP 소켓통신 파일전송 원하는 소켓에만 보내기 — #JAVA소켓통신 #소켓통신 #소켓통신파일전송 #소켓통신파일 #개발자의도구들 AI스쿨 msa기반 java 백...
#JAVA소켓통신 #소켓통신 #소켓통신파일전송 #소켓통신파일 #개발자의도구들
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 소켓통신 프로젝트를 진행중입니다. 전체적인 목차를 보시려면 여기에서 참고하세요
들어가기에 앞서
이전글을 통해 파일을 분할하여 보내보았습니다. 이를 위해서 fileName을 프로토콜에 추가하였는데요. 추가된 fileName은 클라이언트의 fileManager가 가지고있는 fileMap의 키로 사용되어 병합과정에서 유용하게 쓰였습니다.
프로젝트가 완성된건 아니라서 글에는 언급하지 않는 여러가지 많은 문제들이 있습니다. 가령 동일한 fileName이 들어오면 어떻게 처리할 것인지와 같은 문제들이죠.
하지만, 글을 통해서 전달하고자하는 바는, 완벽한 구현보다는 어떤 원리로 데이터가 보내지고 해석하는지와 프로토콜로 통신하는 것에 익숙함을 주는것 입니다. 따라서 자잘한 문제들은 여력이 생기면 뒤에 해결하는 식으로 해볼까 합니다.
원하는 소켓에만 보내기
이번에는 원하는 recevierId를 따로 받아 원하는 소켓에만 파일을 전송하도록 만들어 볼까 합니다. 구현하고자하는 로직은 이렇습니다.
| 🚀 원하는 id에 원하는 파일을 전송하도록한다.파일 분할전송송신 -> 서버 : id, fileName전송서버 -> 수신 : recevier\_id 추출 : 해당 소켓에만 file데이터 전송(with fileName)수신: fileName과 그에 해당하는 seq-file쌍을 저장파일 전송 완료1. 송신 -> 서버 : id, fileName전송 = fileName에 해당하는 파일 송신이 모두 완료됨을 알림2. 서버 -> 수신 : recevier\_id 추출 : 해당 소켓에만 file데이터 전송(with fileName)완료를 알림3. 수신: fileName을 추출하여 저장된 fileMap에서 해당 파일을 전부 합쳐서 원하는 경로에 저장 |
|---|
가장 우선적으로 프로토콜을 수정해 봅시다.
프로토콜 v3
FileStream에 대한 이해
기존 프로토콜의 문제점
이전글에 사용된 프로토콜인데 여기서 id를 4바이트로 고정시켰습니다. 사실 의도한바는 4바이트 이하로 고정시키는 것이였습니다. 하지만, 4바이트 이하인 바이트가 추가되는 경우 0을 채워주는 로직을 따로 구현해야 합니다. 이전에 구현되지 않아서 로직을 추가하기 보다는 새롭게 프로토콜을 수정하였습니다.
이번글의 목적인 recevierId를 포함시켰고, 이제 fileName이 몇 바이트든 상관없이, 프로토콜 내에서 몇 바이트인지 길이를 적어주었습니다.
송신자 측
| private void sendFile(byte\[\] receiver, byte\[\] fileName) { // home pc String filePath = "C:\\Users\\user\\Desktop\\BEmetoring\\file\_test"; String filePath = "C:\\Users\\yangd\\OneDrive\\바탕 화면\\filetest"; File file = new File(filePath, new String(fileName)); try { DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); FileInputStream fileInputStream = new FileInputStream(file); byte\[\] readByte = new byte\[10\]; String onlyName = new String(fileName).split("\\.")\[0\]; int receiverLengthSize = Integer.BYTES; int fileNameSize = Integer.BYTES; int seqSize = Integer.BYTES; int byteRead; int seq = 0; while ((byteRead = fileInputStream.read(readByte)) != -1) { byte\[\] actualRead = Arrays.copyOf(readByte, byteRead); // idlength(4) + id + fileNameLength(4) + fileName + seq + filebyte ByteBuffer bodyBuffer = ByteBuffer.allocate(receiverLengthSize + receiver.length + fileNameSize + onlyName.getBytes().length + seqSize + actualRead.length); System.out.println("bodybuffer size: " + bodyBuffer.capacity()); bodyBuffer.putInt(receiver.length); bodyBuffer.put(receiver); bodyBuffer.putInt(onlyName.getBytes().length); bodyBuffer.put(onlyName.getBytes()); bodyBuffer.putInt(seq); bodyBuffer.put(actualRead); System.out.println("bodyBuffer: (not head)" + Arrays.toString(bodyBuffer.array())); // 여기서 문제 -> 할당하는 부분 다시 byte\[\] packet = HeaderMaker.makeHeader(MessageType.FILE, bodyBuffer.array()); System.out.println("made packet: " + Arrays.toString(packet)); dataOutputStream.write(packet,0, packet.length); dataOutputStream.flush(); seq ++; } System.out.println("all filePacket be sent"); ByteBuffer endBuffer = ByteBuffer.allocate(receiverLengthSize + receiver.length + fileNameSize + onlyName.getBytes().length); endBuffer.putInt(receiver.length); endBuffer.put(receiver); endBuffer.putInt(onlyName.getBytes().length); endBuffer.put(onlyName.getBytes()); byte\[\] endPacket = HeaderAdder.addHeader(MessageType.FILE\_END, endBuffer.array()); System.out.println("endPacket: " + Arrays.toString(endPacket)); dataOutputStream.write(endPacket, 0, endPacket.length); dataOutputStream.flush(); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); }} |
|---|
sendFile에 입력으로필요한 receiverId, fileName은 사용자의 입력에서 가져옵니다. 이번에는 클라이언트용, 서버용 헤더 추가 로직을 만들지 않고, HeaderAdder 클래스를 따로 만들어 양쪽다 사용가능 하도록 만들었습니다.(아래 참고)
이를 위해서 byte\[\]를 입력 받아야하기 때문에, 클라이언트 측에서는 idLength, id, fileLength, fileName, seq, file 모두 바이트배열로 통합하여 HeaderAdder를 호출합니다.
마지막에는 FILE\_END 패킷을 보내는데, 여기에 id와 fileName 정보가 포함되어 있습니다.
HeaderAdder
| package org.example;import java.nio.\;public class HeaderAdder { public static byte\[\] addHeader(MessageType messageType, byte\[\] body) { // bodyLength + bodytype + body int bodyLength = body.length; ByteBuffer resultBuffer = ByteBuffer.allocate(Integer.BYTES \ 2 + bodyLength); resultBuffer.putInt(bodyLength); resultBuffer.putInt(messageType.ordinal()); resultBuffer.put(body); return resultBuffer.array(); } public static byte\[\] addOnlyTypeHeader(MessageType messageType) { ByteBuffer resultBuffer = ByteBuffer.allocate(Integer.BYTES \* 2); resultBuffer.putInt(0); resultBuffer.putInt(messageType.ordinal()); return resultBuffer.array(); }} |
|---|
매개변수를 MessageType과 byte\[\]로 받기 때문에 클라이언트, 서버 둘다 사용이 가능합니다.
서버 측
| private void sendFile(byte\[\] body) { System.out.println("\nStrat sendFile!!"); //System.out.println("Test packet Received!!" + Arrays.toString(body)); ByteBuffer byteBuffer = ByteBuffer.wrap(body); int idLengthSize = 4; int idLength = byteBuffer.getInt(); byte\[\] idByte = new byte\[idLength\]; byteBuffer.get(idByte); System.out.println(Arrays.toString(body)); String receiveId = new String(idByte, StandardCharsets.UTF\_8); byte\[\] packet = new byte\[body.length - idLengthSize - idLength\]; byteBuffer.get(packet); System.out.println("remian body: " + Arrays.toString(packet)); Socket receiverSocket = idManager.getSocketById(receiveId); ClientHandler receiverHandler; synchronized (handlerLock) { receiverHandler = handlerMap.get(receiverSocket); } receiverHandler.sendFile(packet);} |
|---|
서버는 전체 패킷에서 bodyLength와 messageType을 추출한 후 sendFileEnd()를 실행합니다. 즉 sendFileEnd에는 8바이트에 해당하는 데이터가 날라간 상태입니다.
body\[\]에는 다음 정보가 들어있습니다.
여기서 idLength와 id를 추출해서 수신측 id에만 남은 데이터를 보냅니다. 수신측에 전달될 데이터는 아래와 같습니다.
FILE\_END 처리
| private void sendFileEnd(byte\[\] body) { // body = idLength id fileName System.out.println("sendFile body in end: " + Arrays.toString(body)); ByteBuffer byteBuffer = ByteBuffer.wrap(body); // idlength(4) + id + fileNameLength(4) + fileName + seq + filebyte int idLengthSize = Integer.BYTES; int idLength = byteBuffer.getInt(); byte\[\] idByte = new byte\[idLength\]; byteBuffer.get(idByte); String receiverId = new String(idByte, StandardCharsets.UTF\_8); System.out.println("receiver id: " + receiverId); byte\[\] remainBody = new byte\[body.length - idLengthSize - idByte.length\]; byteBuffer.get(remainBody); System.out.println("remianBody(with filename)" + Arrays.toString(remainBody)); Socket receiverSocket = idManager.getSocketById(receiverId); ClientHandler receiverHandler; synchronized (handlerLock) { receiverHandler = handlerMap.get(receiverSocket); } receiverHandler.sendPacket(MessageType.FILE\_END, remainBody);} |
|---|
FILE\_END처리하는 것도 FILE방식과 비슷 합니다. id를 추출하고 해당 id 소켓만 열어 데이터를 보냅니다.
수신측에는 이 데이터만이 들어갈겁니다. 수신측은 messageType을 보고 end 끝났음을 알며 조각난 데이터들을 조립하여 저장할 것 입니다.
ClientHandler측
클라이언트와 연결된 소켓의 입,출력을 관리합니다. 서버에서 sendPacket 명령을 받으면 HeaderAdder를 호출해 헤더를 추가 후 데이터를 전송합니다.
| public void sendPacket(MessageType messageType, byte\[\] body){ try { byte\[\] packet = HeaderAdder.addHeader(messageType, body); DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream()); dataOutputStream.writeInt(packet.length); dataOutputStream.write(packet, 0, packet.length); dataOutputStream.flush(); } catch (IOException e) { throw new RuntimeException(e); }} |
|---|
수신 측
MessageType = FILE 일때
파일을 분할해서 받으면 fileManager를 따로 두어 데이터를 저장합니다. 이때 fileName을 key로 두고 TreeMap을 만들어 seq-file byte\[\]쌍으로 저장해둡니다.
treeMap에 저장되는 정보 {seq: fileByte}
| fileMap = { "fileName": { seq1(Integer} : byte\[\], seq2 : byte\[\], seq3: byte\[\] }} |
|---|
| public class FileManager{ private final Client client; private final HashMap<String, TreeMap<Integer, byte\[\]>> fileMap = new HashMap<>(); public FileManager(Client client) { this.client = client; System.out.println("Run File\_handler"); } // fileMap = {FileName: {seq: byte\[\]}} public void store(byte\[\] body) { System.out.println("\nin tore: Client: filePacket received : " + Arrays.toString(body)); // original body를 들고있다. ByteBuffer bodyBuffer = ByteBuffer.wrap(body); // fileNameSize(4) + fileName(n) + seq(4) + file(10) int fileNameLengthSize = Integer.BYTES; int fileNameLength = bodyBuffer.getInt(); byte\[\] fileNameByte = new byte\[fileNameLength\]; bodyBuffer.get(fileNameByte); String fileName = new String(fileNameByte); System.out.println("store fileName: " + fileName ); int seqSize = Integer.BYTES; int seq = bodyBuffer.getInt(); System.out.println("store seq: " + seq); byte\[\] fileByte = new byte\[body.length - fileNameLengthSize - fileNameLength - seqSize\]; bodyBuffer.get(fileByte); System.out.println("store fileByte: " + Arrays.toString(fileByte)); TreeMap<Integer, byte\[\]> seqFileMap; if (fileMap.containsKey(fileName)) { seqFileMap = fileMap.get(fileName); System.out.println("seqFileMap 가져옴 "); } else { seqFileMap = new TreeMap<>(); System.out.println("seqFileMap 새로생성"); } System.out.println("seqFileMap 만들거나 가져옴"); seqFileMap.put(seq, fileByte); fileMap.put(fileName, seqFileMap); System.out.println("fileMap에 값 추가 완료 "); }} |
|---|
기존에 있다면, fileMap에서 가져온 TreeMap에 저장해야한다는 것을 잊지마세요!
MessageType = FILE\_END 일때
클라이언트는 FILE\_END메세지가 오면, fileManger를 호출하고 fileName을 추출하여 file들을 combine합니다. combine된 파일을 최적적으로 저장합니다.
sever -> filemanger -> server
Sever
| private void combineFile(byte\[\] fileName) { ByteBuffer fileNameBuffer = ByteBuffer.wrap(fileName); int fileNameLength = fileNameBuffer.getInt(); byte\[\] fileNameByte = new byte\[fileNameLength\]; fileNameBuffer.get(fileNameByte); byte\[\] totalFile = fileManager.getCombineFile(fileNameByte); saveFile(totalFile);} |
|---|
FileManger
| public byte\[\] getCombineFile(byte\[\] fileNameByte) { String fullFileName = new String(fileNameByte, StandardCharsets.UTF\_8); String fileName = fullFileName.split("\\.")\[0\]; TreeMap<Integer, byte\[\]> seqFileMap = fileMap.get(fileName); int totalSize = 0; for (Map.Entry<Integer, byte\[\]> entry : seqFileMap.entrySet()) { totalSize += entry.getValue().length; } System.out.println("totalSize: " + totalSize); ByteBuffer totalBuffer = ByteBuffer.allocate(totalSize); for (byte\[\] fileByte : seqFileMap.values()) { totalBuffer.put(fileByte); } return totalBuffer.array();} |
|---|
fileName정보를 이용해 fieMap의 fileName에 접근합니다. 이후 TreeMap을 순서대로 조합하고 서버에 넘겨줍니다.
server
| private void saveFile(byte\[\] body) { try { String randomFileName = UUID.randomUUID() + ".txt"; String directoryPath = "yourpath"; 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(); }} |
|---|
지정된 경로에 랜덤한 이름으로 파일이 저장됩니다.













