จากการดำเนินการต่อในบทความต่อเนื่องที่เกี่ยวข้องกับการใช้งานยูทิลิตี้คอนโซล Windows แบบกำหนดเองเราไม่สามารถช่วยได้นอกจากสัมผัส TFTP (Trivial File Transfer Protocol) - โปรโตคอลการถ่ายโอนไฟล์อย่างง่าย
คราวที่แล้ว มาดูทฤษฎีสั้นๆ ดูโค้ดที่ใช้ฟังก์ชันการทำงานคล้ายกับที่ต้องการ แล้ววิเคราะห์ รายละเอียดเพิ่มเติม-อยู่ระหว่างดำเนินการ
ฉันจะไม่คัดลอกและวางข้อมูลอ้างอิง ลิงก์ที่ปกติแล้วจะพบได้ในตอนท้ายของบทความ ฉันจะบอกเพียงว่าโดยพื้นฐานแล้ว TFTP เป็นรูปแบบที่เรียบง่ายของโปรโตคอล FTP ซึ่งการตั้งค่าการควบคุมการเข้าถึงมี ถูกลบออกแล้ว และอันที่จริงไม่มีอะไรอยู่ที่นี่นอกจากคำสั่งสำหรับรับและถ่ายโอนไฟล์ อย่างไรก็ตาม เพื่อให้การใช้งานของเราดูหรูหราขึ้นอีกเล็กน้อยและปรับให้เข้ากับหลักการเขียนโค้ดในปัจจุบัน ไวยากรณ์จึงเปลี่ยนไปเล็กน้อย - นี่ไม่ได้เปลี่ยนหลักการทำงาน แต่อินเทอร์เฟซ IMHO จะกลายเป็นตรรกะมากขึ้นเล็กน้อย และ ผสมผสานด้านบวกของ FTP และ TFTP
โดยเฉพาะอย่างยิ่ง เมื่อเปิดตัว ไคลเอนต์จะร้องขอที่อยู่ IP ของเซิร์ฟเวอร์และพอร์ตที่ TFTP แบบกำหนดเองเปิดอยู่ (เนื่องจากความไม่เข้ากันกับโปรโตคอลมาตรฐาน ฉันจึงถือว่าเหมาะสมที่จะปล่อยให้ผู้ใช้สามารถเลือกพอร์ตได้) หลังจากนั้น การเชื่อมต่อเกิดขึ้นซึ่งเป็นผลมาจากการที่ไคลเอนต์สามารถส่งคำสั่งใดคำสั่งหนึ่ง - รับหรือใส่เพื่อรับหรือส่งไฟล์ไปยังเซิร์ฟเวอร์ ไฟล์ทั้งหมดจะถูกส่งในโหมดไบนารี่เพื่อลดความซับซ้อนของตรรกะ
ในการใช้โปรโตคอล ฉันใช้คลาส 4 แบบดั้งเดิม:
- TFTPClient
- TFTPเซิร์ฟเวอร์
- TFTPClientTester
- TFTPServerTester
เนื่องจากคลาสการทดสอบมีไว้เพื่อการดีบักคลาสหลักเท่านั้น ฉันจะไม่วิเคราะห์คลาสเหล่านั้น แต่โค้ดจะอยู่ในพื้นที่เก็บข้อมูล ลิงก์ไปยังคลาสนั้นอยู่ท้ายบทความ ตอนนี้ฉันจะดูชั้นเรียนหลัก
TFTPClient
งานของคลาสนี้คือการเชื่อมต่อกับเซิร์ฟเวอร์ระยะไกลโดยใช้ IP และหมายเลขพอร์ต อ่านคำสั่งจากสตรีมอินพุต (ในกรณีนี้คือแป้นพิมพ์) แยกวิเคราะห์ ถ่ายโอนไปยังเซิร์ฟเวอร์ และขึ้นอยู่กับว่าคุณ จำเป็นต้องส่งหรือรับไฟล์ ถ่ายโอนหรือรับ
รหัสสำหรับเรียกใช้ไคลเอ็นต์เพื่อเชื่อมต่อกับเซิร์ฟเวอร์และรอคำสั่งจากอินพุตสตรีมจะมีลักษณะดังนี้ ตัวแปรส่วนกลางจำนวนหนึ่งที่ใช้ในที่นี้มีการอธิบายไว้นอกบทความในข้อความฉบับเต็มของโปรแกรม เนื่องจากเรื่องไม่สำคัญฉันจึงไม่อ้างอิงเพื่อไม่ให้บทความมากเกินไป
public void run(String ip, int port)
{
this.ip = ip;
this.port = port;
try {
inicialization();
Scanner keyboard = new Scanner(System.in);
while (isRunning) {
getAndParseInput(keyboard);
sendCommand();
selector();
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
มาดูเมธอดที่ถูกเรียกในบล็อคโค้ดนี้กันดีกว่า:
ที่นี่ไฟล์ถูกส่ง - โดยใช้สแกนเนอร์เรานำเสนอเนื้อหาของไฟล์เป็นอาร์เรย์ของไบต์ซึ่งเราเขียนทีละตัวลงในซ็อกเก็ตหลังจากนั้นเราจะปิดและเปิดใหม่อีกครั้ง (ไม่ใช่วิธีแก้ปัญหาที่ชัดเจนที่สุด แต่ รับประกันการปล่อยทรัพยากร) หลังจากนั้นเราจะแสดงข้อความเกี่ยวกับการถ่ายโอนที่สำเร็จ
private void put(String sourcePath, String destPath)
{
File src = new File(sourcePath);
try {
InputStream scanner = new FileInputStream(src);
byte[] bytes = scanner.readAllBytes();
for (byte b : bytes)
sout.write(b);
sout.close();
inicialization();
System.out.println("nDonen");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
ส่วนของโค้ดนี้อธิบายการดึงข้อมูลจากเซิร์ฟเวอร์ ทุกอย่างเป็นเรื่องเล็กน้อยอีกครั้งมีเพียงโค้ดบล็อกแรกเท่านั้นที่น่าสนใจ เพื่อให้เข้าใจได้อย่างแน่ชัดว่าต้องอ่านจำนวนไบต์จากซ็อกเก็ต คุณจำเป็นต้องทราบว่าไฟล์ที่ถ่ายโอนมีน้ำหนักเท่าใด ขนาดไฟล์บนเซิร์ฟเวอร์จะแสดงเป็นจำนวนเต็มยาว ดังนั้นจึงยอมรับขนาด 4 ไบต์ที่นี่ ซึ่งต่อมาจะถูกแปลงเป็นตัวเลขเดียว นี่ไม่ใช่แนวทาง Java มากนัก แต่ค่อนข้างคล้ายกับ SI แต่สามารถแก้ปัญหาได้
จากนั้นทุกอย่างก็ไม่สำคัญ - เราได้รับจำนวนไบต์ที่ทราบจากซ็อกเก็ตและเขียนลงในไฟล์หลังจากนั้นเราจะแสดงข้อความแสดงความสำเร็จ
private void get(String sourcePath, String destPath){
long sizeOfFile = 0;
try {
byte[] sizeBytes = new byte[Long.SIZE];
for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
{
sizeBytes[i] = (byte)sin.read();
sizeOfFile*=256;
sizeOfFile+=sizeBytes[i];
}
FileOutputStream writer = new FileOutputStream(new File(destPath));
for (int i =0; i < sizeOfFile; i++)
{
writer.write(sin.read());
}
writer.close();
System.out.println("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
หากมีการป้อนคำสั่งอื่นนอกเหนือจาก get หรือ put ลงในหน้าต่างไคลเอ็นต์ ฟังก์ชัน showErrorMessage จะถูกเรียกใช้ เพื่อระบุว่าอินพุตไม่ถูกต้อง เนื่องจากเรื่องไร้สาระ ฉันจะไม่อ้างอิงมัน สิ่งที่น่าสนใจกว่านั้นคือฟังก์ชั่นการรับและแยกสตริงอินพุต เราส่งเครื่องสแกนเข้าไป ซึ่งเราคาดว่าจะได้รับบรรทัดที่คั่นด้วยช่องว่างสองช่องและมีคำสั่ง ที่อยู่ต้นทาง และที่อยู่ปลายทาง
private void getAndParseInput(Scanner scanner)
{
try {
input = scanner.nextLine().split(" ");
typeOfCommand = input[0];
sourcePath = input[1];
destPath = input[2];
}
catch (Exception e) {
System.out.println("Bad input");
}
}
การส่งคำสั่ง—ส่งคำสั่งที่ป้อนจากเครื่องสแกนไปยังซ็อกเก็ตและบังคับให้ส่ง
private void sendCommand()
{
try {
for (String str : input) {
for (char ch : str.toCharArray()) {
sout.write(ch);
}
sout.write(' ');
}
sout.write('n');
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
ตัวเลือกคือฟังก์ชันที่กำหนดการทำงานของโปรแกรมโดยขึ้นอยู่กับสตริงที่ป้อน ทุกอย่างที่นี่ไม่ได้สวยงามมากนัก และเคล็ดลับที่ใช้ก็ไม่ใช่วิธีที่ดีที่สุดในการบังคับออกนอกบล็อกโค้ด แต่สาเหตุหลักคือไม่มีบางสิ่งใน Java เช่น ผู้รับมอบสิทธิ์ใน C# ตัวชี้ฟังก์ชันจาก C++ หรือที่ อย่างน้อยก็ข้ามไปที่น่ากลัวและแย่มากซึ่งทำให้คุณสามารถนำไปใช้ได้อย่างสวยงาม หากคุณรู้วิธีทำให้โค้ดดูหรูหราขึ้นอีกหน่อย ฉันยินดีรับฟังคำวิจารณ์ในความคิดเห็น สำหรับฉันดูเหมือนว่าจำเป็นต้องใช้พจนานุกรม String-delegate ที่นี่ แต่ไม่มีผู้รับมอบสิทธิ์ ...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
TFTPเซิร์ฟเวอร์
ฟังก์ชันการทำงานของเซิร์ฟเวอร์แตกต่างจากฟังก์ชันการทำงานของไคลเอ็นต์โดยรวม เฉพาะในคำสั่งนั้นเท่านั้นที่เข้ามา ไม่ใช่จากแป้นพิมพ์ แต่มาจากซ็อกเก็ต โดยทั่วไปวิธีการบางอย่างจะเหมือนกันดังนั้นฉันจะไม่ให้ แต่จะพูดถึงความแตกต่างเท่านั้น
ในการเริ่มต้นใช้วิธีการรันซึ่งรับพอร์ตเป็นอินพุตและประมวลผลข้อมูลอินพุตจากซ็อกเก็ตในลูปนิรันดร์
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
เมธอด put ซึ่งล้อมเมธอด writeToFileFromSocket ที่เปิดสตรีมการเขียนไปยังไฟล์และเขียนไบต์อินพุตทั้งหมดจากซ็อกเก็ต จะแสดงข้อความที่ระบุว่าการถ่ายโอนเสร็จสมบูรณ์เมื่อการเขียนเสร็จสมบูรณ์
private void put(String source, String dest){
writeToFileFromSocket();
System.out.print("nDonen");
};
private void writeToFileFromSocket()
{
try {
FileOutputStream writer = new FileOutputStream(new File(destPath));
byte[] bytes = sin.readAllBytes();
for (byte b : bytes) {
writer.write(b);
}
writer.close();
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
วิธีการรับดึงไฟล์เซิร์ฟเวอร์ ดังที่ได้กล่าวไว้แล้วในส่วนฝั่งไคลเอ็นต์ของโปรแกรม เพื่อให้ถ่ายโอนไฟล์ได้สำเร็จ คุณต้องทราบขนาดของไฟล์ โดยจัดเก็บไว้ในจำนวนเต็มยาว ดังนั้นฉันจึงแยกมันออกเป็นอาร์เรย์ 4 ไบต์ แล้วถ่ายโอนแบบไบต์ต่อไบต์ ไปที่ซ็อกเก็ตจากนั้นเมื่อได้รับและประกอบมันบนไคลเอนต์เป็นตัวเลขฉันจะถ่ายโอนไบต์ทั้งหมดที่ประกอบเป็นไฟล์อ่านจากสตรีมอินพุตจากไฟล์
private void get(String source, String dest){
File sending = new File(source);
try {
FileInputStream readFromFile = new FileInputStream(sending);
byte[] arr = readFromFile.readAllBytes();
byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
sout.write(bytes[i]);
sout.flush();
for (byte b : arr)
sout.write(b);
}
catch (Exception e){
System.out.println(e.getMessage());
}
};
เมธอด getAndParseInput นั้นเหมือนกับในไคลเอนต์ ข้อแตกต่างเพียงอย่างเดียวคืออ่านข้อมูลจากซ็อกเก็ตแทนที่จะอ่านจากคีย์บอร์ด รหัสอยู่ในพื้นที่เก็บข้อมูล เช่นเดียวกับตัวเลือก
ในกรณีนี้ การเริ่มต้นจะอยู่ในบล็อกโค้ดที่แยกจากกัน เนื่องจาก ภายในการดำเนินการนี้ หลังจากการถ่ายโอนเสร็จสิ้น ทรัพยากรจะถูกปล่อยออกมาและถูกครอบครองอีกครั้ง - อีกครั้งเพื่อป้องกันการรั่วไหลของหน่วยความจำ
private void incialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
เพื่อสรุป:
เราเพิ่งเขียนรูปแบบของเราเองเกี่ยวกับโปรโตคอลการถ่ายโอนข้อมูลแบบธรรมดา และได้ทราบว่ามันควรทำงานอย่างไร โดยหลักการแล้ว ฉันไม่ได้ค้นพบอเมริกาที่นี่และไม่ได้เขียนสิ่งใหม่ ๆ มากนัก แต่ไม่มีบทความที่คล้ายกันเกี่ยวกับHabré และในการเขียนบทความชุดเกี่ยวกับยูทิลิตี้ cmd มันเป็นไปไม่ได้ที่จะไม่แตะต้องมัน
อ้างอิง:
ที่มา: will.com