java编程 停等协议_在应用层模拟实用停等协议
本文允许转载,转载请保留全文!
1. 背景说明
本文章来源于近期需要提交的《计算机网络》课程实验。
教材使用高等教育出版社出版的陈鸣编著的《计算机网络:原理与实践》一书。
实验分为3部分,分别需要在应用层模拟实用停等协议、连续ARQ协议和滑动窗口协议,实现文件的传输。端与端之间的通信使用Socket完成。
语言可以任选,出于简单,本文以java为例,仅介绍使用停等协议的实现,其他内容由同学们自己探索吧。强烈不推荐MFC,除非想把自己玩死。
注:本人对网络兴趣不大,上课睡觉时间远远长于听课时间,故不对文章的正确性做任何保证,代码仅供参考。
2. 模拟实用停等协议的详细思路
本程序仅仅是一个课堂实验而已,故没有在程序结构上花太多心思,基本上算是想到哪儿写到哪儿,所以代码可能有一些奇♂怪的地方。
为了能在单机状态下进行端到端的通信,每个进程即作为Client,又作为Server。在演示时,同时打开两个进程,为两个进程的Server设置不同的端口号,分别由对方进程的Client进行连接,并将IP地址使用127.0.0.1,即可实现单机状态下进程之间的通信。
程序划分为6个类:
Main:主要用于显示用户界面,完成与用户的交互;同时定义了程序中的全局常量。
Client:用于向对方进程的Server发送消息。
Server:用于监听端口,接受对方进程的Client发送的消息。
Encode:对文件进行编码,使之满足某种自己定义的帧格式。
Decode:对接收到的帧进行解码,得到帧的信息及帧中的数据。
FileFrame:简单的结构,用于表示Decode解码后得到的数据。
2.1 用户界面的设计
实验指导书中用户界面设计的非常复杂,给程序设计带来了额外的负担,其实大可不必。用户界面中只有以下部分是必须的:
端口设置部分:程序运行时,需要设置本进程接收数据的端口,供本进程Server监听对方进程Client发送的数据。同样的,也需要设置对方Server的端口,供本进程Client向对方发送数据。因此,这部分需要两个文本框和一个按钮,文本框用于接受用户输入的端口号,按钮用于确认端口并建立连接。此外,可以添加2个静态文本,用于提示用户。
信息显示部分:需要一个文本框,用于记录并显示当前进程每一次发送/接收时的详细状态。否则老师根本不知道你的程序在做什么。
发送文件部分:只需要一个按钮,文件路径与实验核心目的无关,故固定于代码中即可。
除此之外,实验指导书中用户界面的其他部分都是可有可无的。对于实验而言,没有必要把时间浪费在窗口设计上。设计效果如下图所示:
在java中实现图形化界面的方法还是比较多的,比如形形色色的eclipse插件。由于本人懒得找插件配环境,就直接使用java中的Swing编写。缺点很明显:窗口和每一个控件都需要完全使用代码来定义。鉴于本程序窗口非常简单,所以工作量还是可以容忍的。
要想让用户界面在程序的Main类中完成,必须让Main类继承JFrame类,JFrame位于javax.swing包中。代码如如下形式:
public class Main extendsJFrame{private JLabel labelServerPort = newJLabel();private JLabel labelClientPort = newJLabel();private JTextField textServerPort = newJTextField();private JTextField textClientPort = newJTextField();private JButton buttonSetPort = newJButton();private JTextArea textMessage = newJTextArea();private JButton buttonSend = newJButton();
...... (与Socekt相关的变量)private voidinitWindow(){this.setSize(320, 336);this.getContentPane().setLayout(null);this.setLocationRelativeTo(null);this.setResizable(false);this.setTitle("实验一");this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);this.addWindowListener(newWindowListener(){
@Overridepublic voidwindowClosing(WindowEvent e) {
...... (断开连接)
}
@Overridepublic voidwindowClosed(WindowEvent e) { }
@Overridepublic voidwindowActivated(WindowEvent e) { }
@Overridepublic voidwindowDeactivated(WindowEvent e) { }
@Overridepublic voidwindowDeiconified(WindowEvent e) { }
@Overridepublic voidwindowIconified(WindowEvent e) { }
@Overridepublic voidwindowOpened(WindowEvent e) { }
});
labelServerPort.setBounds(0, 4, 64, 24);
labelServerPort.setText("接收端口:");this.add(labelServerPort);
textServerPort.setBounds(64, 4, 48, 24);this.add(textServerPort);
labelClientPort.setBounds(128, 4, 64, 24);
labelClientPort.setText("发送端口:");this.add(labelClientPort);
textClientPort.setBounds(192, 4, 48, 24);this.add(textClientPort);
buttonSetPort.setBounds(256, 4, 64, 24);
buttonSetPort.setText("确定");
buttonSetPort.setHorizontalAlignment(SwingConstants.CENTER);
buttonSetPort.addActionListener(newActionListener(){
@Overridepublic voidactionPerformed(ActionEvent e) {
buttonSetPort.setEnabled(false);
...... (建立连接)
buttonSend.setEnabled(true);
}
});this.add(buttonSetPort);
textMessage.setLineWrap(true);
textMessage.setWrapStyleWord(true);
textMessage.setEditable(false);
JScrollPane panelMessage= newJScrollPane(textMessage);
panelMessage.setBounds(0, 32, 320, 240);this.add(panelMessage);
buttonSend.setBounds(0, 276, 320, 24);
buttonSend.setText("发送文件");
buttonSend.setHorizontalAlignment(SwingConstants.CENTER);
buttonSend.setEnabled(false);
buttonSend.addActionListener(newActionListener(){
@Overridepublic voidactionPerformed(ActionEvent e) {
...... (发送文件)
}
});this.add(buttonSend);this.setVisible(true);
}publicMain(){super();
initWindow();
}public static voidmain(String[] args) {newMain();
}
View Code
除滚动条的创建稍显繁琐外,其余代码的含义较为简单,不再一一解释。但需要注意的是,对于窗口而言,this.setVisible(true);是必须存在的,且必须位于整个窗口初始化的最后一句,否则会引起窗口显示不全的问题。
2.2 Client的设计
Client部分需要完成3个任务:一是建立连接,二是发送数据帧,三是发送ACK/NAK。严格来讲,ACK与NAK的发送是Server该做的事,此处为了方便,将所有的发送操作并入Client部分。
2.2.1 建立连接
注:本小节的理解需要与2.3.1小节相结合
对于Client而言,建立连接并不困难,只需要Socket s=new Socket("127.0.0.1",端口号);即可。然而这里面却有着一些小细节需要注意。
当执行上述代码时,如果对应主机的相应端口没有打开,会导致Socket创建失败,抛出ConnectException异常。两个相互通信的进程,无论谁先打开端口,都会存在如下问题:先打开端口的进程的Client无法连接到另一个进程的Server,而后打开端口的进程的Client却可以顺利连接到另一个进程的Server。Socket创建失败会导致接下来的通信无法进行,因此当Socket创建失败时,需要通过循环不停的重新尝试创建,直至成功。如果这一过程不放到单独的线程中运行,那么在此期间主线程将一直被占用,导致先打开端口的进程的用户界面卡住,直到后打开端口的进程打开端口。
与MFC不同,java的线程创建较为简单:首先需要用一个类(类名假定为A)继承Thread类,然后重载run函数。run函数即为新线程的入口。当需要创建线程并运行时,使用new A().start();即可。需要注意的是,新线程的创建必须调用start()函数,调用之前重载的run()函数将无法创建新线程。
对于成功创建的Socket,可以使用DataOutputStream进行更高层次的封装,将Socket操作简化为文件读写的形式。
本部分代码如下:
1 public classClient {2 private intport;3 private boolean valid = false;4 private Socket s = null;5 private DataOutputStream dos = null;6
7 private class Create extendsThread{8 @Override9 public voidrun(){10 boolean success=false;11 try{12 while(!success){13 try{14 s=new Socket("127.0.0.1",port);15 success=true;16 }catch(ConnectException e){ }17 }18 dos=newDataOutputStream(s.getOutputStream());19 }catch(Exception e) {20 valid=false;21 e.printStackTrace();22 }23 }24 }25
26 public void create(intport){27 this.port=port;28 valid=true;29 newCreate().start();30 }31
32 ......33 }
View Code
2.2.2 发送数据帧
在发送过程中,程序将重复“发送→等待”的过程,为了防止“等待”过程卡死用户界面,同样需要为数据帧的发送单独创建线程。
数据帧应该包含什么呢?至少应该包含以下内容:
帧类型:用来区分这一帧是数据帧还是ACK帧还是NAK帧
帧序号:取值0或1,在处理超时重传过程中,用于区分是数据帧丢失还是ACK帧丢失的情况
是否为最后一帧:用来确定文件传输合适结束
数据长度:这个不解释了= =
数据:这个也不解释了= =
校验码:用来校验帧的数据部分是否出错
为了尽可能简化程序代码,我们可以大幅度牺牲执行效率——毕竟作为实验课程序,不惜一切代价保证正确性才是重要的。为了使帧的表示尽可能直观,我没有采用字节数组表示帧,而是采用了字符串。帧内不同的部分之间,使用某个特定的间隔符分隔(例如“|”字符),为了确保间隔符不出现在数据部分中,还需要对数据部分进行一定的处理。至于具体的处理方法,就仁者见仁智者见智了,只要保证发送方编码后的数据能够被接收方正确解码即可。在使用字符串表示帧时,数据长度是不必要的,因为可以通过数据两端的间隔符的位置直接计算出数据长度。
1 public classClient {2
3 ......4
5 private byte[] fileBuffer = null;6
7 private class SendFile extendsThread{8 @Override9 public voidrun(){10 try{11 //server是服务端的实例,服务端收到数据后设置相应状态量,供客户端查询
12 server.receiveACK=true;13 server.receiveNAK=false;14 server.nextFrameIndex=0;15
16 String[] frame=Encode.encodeFile(fileBuffer);//由字节数组编码得到字符串格式的帧
17 int k=0;//当前发送的帧在数组frame中的下标
18 while(true){19 while(!server.receiveACK && !server.receiveNAK && System.currentTimeMillis()-lastSendTime
21 if(k==frame.length)break;//所有帧发送完毕,接收最后一个ACK
22 server.receiveACK=server.receiveNAK=false;23 if(Main.random.nextDouble()>Main.pLoseFrame){//概率丢帧
24 if(Main.random.nextDouble()
25 else dos.writeUTF(frame[k]);//发送下一帧
26 dos.flush();27 }28 ++k;29 }else{//对方返回NAK 或者 超时
30 server.receiveACK=server.receiveNAK=false;31 if(Main.random.nextDouble()>Main.pLoseFrame){//丢帧
32 if(Main.random.nextDouble()
33 else dos.writeUTF(frame[k-1]);//重发上一帧
34 dos.flush();35 }36 }37 lastSendTime=System.currentTimeMillis();38 }39 } catch(Exception e) {40 e.printStackTrace();41 }42
43 }44 }45
46 public booleansendFile(String path){47 if(!valid)return false;48 try{49 File file = newFile(path);50 long fileSize =file.length();51 FileInputStream fis = newFileInputStream(file);52 byte[] buffer = new byte[(int)fileSize];53 int offset = 0;54 int numRead = 0;55 while (offset=0){56 offset +=numRead;57 }58 fis.close();59 fileBuffer=buffer;60 newSendFile().start();61
62 } catch(Exception e) {63 e.printStackTrace();64 return false;65 }66 return true;67 }68
69 ......70 }
View Code
对于链路中各类出错情况的模拟也并不复杂:
帧丢失:直接不发送
帧出错:给帧赋予错误的校验码再发送
帧超时:若干秒后发送(代码中未体现)
2.2.3 发送ACK/NAK帧
此处定义:
ACK N:确认之前收到的帧,希望收到帧序号为N的帧
NAK N:帧序号为N的帧出错,请求重传
因此,ACK/NAK帧的帧结构可以非常简单,只需要帧类型和序号N这两部分即可。同样可以使用字符串的形式表示帧。
ACK/NAK帧的发送过程很短,而且不需要等待进一步的回复,因此无需使用单独的线程,可以放在主线程中。代码如下:
1 public classClient {2
3 ......4
5 public void sendACK(intframeIndex){6 if(valid){7 try{8 if(Main.random.nextDouble()>Main.pLoseFrame){9 dos.writeUTF("ack|"+frameIndex);10 dos.flush();11 }12 } catch(Exception e) {13 e.printStackTrace();14 }15 }16 }17
18 public void sendNAK(intframeIndex){19 if(valid){20 try{21 if(Main.random.nextDouble()>Main.pLoseFrame){22 dos.writeUTF("nak|"+frameIndex);23 dos.flush();24 }25 } catch(Exception e) {26 e.printStackTrace();27 }28 }29 }30
31 }
View Code
同样地,我们需要模拟帧丢失和帧超时的情况。
2.3 Server的设计
Server部分需要完成两个任务:一是创建服务端,二是监听端口接收数据。
2.3.1 创建服务端
注:本小节的理解需要与2.2.1小节相结合
Server的创建同样不复杂,java为我们遮盖了太多的细节,使得编程倾向于傻瓜化。代码含义显而易见,不再做过多解释。
1 public classServer {2
3 public boolean receiveACK=true;4 public boolean receiveNAK=false;5 public int nextFrameIndex=0;//已经确认之前的帧,希望收到的帧的序号
6
7 private boolean valid = false;8 private ServerSocket ss = null;9 private Client client = null;10
11 private FileOutputStream fos = null;12 private int idx=1;//当前接收的帧是文件的第idx部分
13
14 public void create(intport){15 valid=true;16 try{17 ss=newServerSocket(port);18 } catch(Exception e) {19 valid = false;20 e.printStackTrace();21 }22
23 Run run=newRun();24 run.start();25
26 }27
28 ......29 }
View Code
2.3.2 监听端口接收数据
回顾2.3.1小节的代码,可以发现在创建服务端创建结束后,有一个新的线程被创建,这个线程就是用来监听端口的。由于对端口的监听会阻塞线程,为了避免主线程被阻塞,为端口监听安排一个独立的线程是必要的。代码如下:
1 public classServer {2
3 ......4
5 private class Run extendsThread{6 @Override7 public voidrun(){8 try{9 if(!valid)return;10 Socket s =ss.accept();11 DataInputStream dis = new DataInputStream(s.getInputStream());//读取数据,每次读取完毕会抛出EOFException异常
12 int expectedFrameIndex=0;//希望收到帧的帧序号,取值总是0或1
13 while(valid){14 try{15 String frame=dis.readUTF();16 if(frame==null || frame.equals(""))continue;//这一句并不是特别必要
17
18 String frameType=Decode.getType(frame);//对帧进行初步解码,得到帧的类型
19
20 if (frameType.equals("file")){//数据帧
21 FileFrame ff=Decode.decodeFile(frame);//对数据帧进行进一步解码,得到帧的具体内容
22 if(expectedFrameIndex==ff.frameIndex){23 if(ff.CRC32){//如果帧校验正确
24 ++idx;25 if(fos==null)fos= new FileOutputStream("file");//第一帧,创建新文件
26 fos.write(ff.data);//将收到的帧存入文件
27 if(!ff.hasNext){ fos.close(); fos=null; idx=1; }//最后一帧,关闭文件
28 expectedFrameIndex=1-expectedFrameIndex;29 client.sendACK(1-ff.frameIndex);30 }else{31 client.sendNAK(ff.frameIndex);32 }33 }else{//帧的序号错误,代表收到了重复帧。通常是由于ACK丢失或数据帧超时引起Client重传,丢弃即可
34 client.sendACK(1-ff.frameIndex);//虽然丢弃,仍要ACK通知Client,否则Client死循环
35 }36 }else if (frameType.equals("ack")){37 nextFrameIndex=Integer.parseInt(frame.split("\\|")[1]);38 receiveACK=true;39 }else if (frameType.equals("nak")){40 nextFrameIndex=Integer.parseInt(frame.split("\\|")[1]);41 receiveNAK=true;42 }43 } catch(EOFException e){ }44 }45 } catch(Exception e) {46 e.printStackTrace();47 }48
49 }50 }51 }
View Code
这里介绍一下String类的split函数。顾名思义,split函数的作用是对字符串进行分割,返回值为字符串数组,表示分割后的每一部分。其参数为正则式表示的分割规则,在本应用环境下,可以理解为帧的不同部分之间的分隔符。然而之前使用的“|”符号在正则式中有特殊含义,需要通过“\”转义,需要将“|”表示为“\|”。与C/C++类似,“\”字符在java语言的字符串中本身就是转义字符,表示其本身时应双写为“\\”。因此,在使用“|”作为分隔符时,split的参数应写为“\\|”。
FileFrame类只有成员变量没有成员函数,用来表示经过解码后的帧。
2.4 其他
对于上述操作的每一步,留下记录。
作为一种省事的方法,可以在以上每个类的构造函数中,记录用户界面中消息显示控件对应的对象,直接在其中追加内容即可。
这样会导致对用户界面的操作掺杂在整个协议中,不过反正是实验而已管他呢~
3. 连续ARQ与滑动窗口协议
连续ARQ与滑动窗口协议可以在实用停等协议的基础上加以改造。
对于连续ARQ协议:
取消NAK,发生校验错误时等Client超时重传
ACK的含义发生了变化
发送端增加滑动窗口,适当选择窗口大小
对于滑动窗口协议:
取消NAK,发生校验错误时等Client超时重传
ACK的含义发生了变化
增加发送窗口和接收窗口,适当选择窗口大小
建议参考课本P89图示,而不是P91图示
滑动窗口协议效果示例:
发送端:
接收端:
java编程 停等协议_在应用层模拟实用停等协议相关推荐
- JAVA编程习题及答案_完美版
JAVA编程习题及答案_完美版 原创 lingwu7 最后发布于2017-08-15 20:01:12 阅读数 10796 收藏 发布于2017-08-15 20:01:12 版权声明:本文为博主原创 ...
- java编程控制电脑硬件_如何快速学习AP计算机中的Java编程?
AP Computer Science A (APCSA)考试,需要掌握Java编程语言.对于刚学习Java的同学,都应该了解Java编程语言的知识体系结构. 多编程零基础的高中生,在自学一段时间内, ...
- java编程能做什么_学习Java编程能做什么工作?
Java作为编程语言界最时髦流行的元老之一,现今在软件市场上也是宠儿,被各大企业广泛应用到生产中.在各种行业.各个企业的业务管理平台,如门户网站等许多方面都占据了主导地位.吸引着越来越多学习Java的 ...
- 想学java编程从哪入手_初学编程从哪方面入手?
感谢邀请! 从最初的机器语言到汇编语言,再到百花齐放的高级语言,编程语言种类繁多,多达上百种.当然每一种语言都有自己特殊的用途.例如,java,php专门用来显示网页:Perl更适合文本处理:C语言被 ...
- 自学个JAVA编程有什么用_怎样自学java编程
4 怎么学java 多线程需要理解机理 很多Java程序员热衷于多线程程序编写,认为是对逻辑能力的挑战.其实在大量应用中根本就不需要编写多线程程序,或者说大多数编写应用程序的程序员不会去写多线程程序. ...
- 对接物联网设备tcp协议_什么是物联网?常见IoT协议最全讲解
本文介绍物联网基础知识:什么是物联网,以及常见的物联网协议. 一.什么是物联网? 物联网(Internet of Things)这个概念读者应该不会陌生.物联网的概念最早于1999年被提出来,曾被称为 ...
- 一图读懂开源协议_一张经典图,开源协议比较
部分内容来自维基 MPLMozilla公共许可证 版本 2.0 发布日期 2012年1月3日 是 是 (MPL 2.0預設與GPL 2+.LGPL 2.1+.AGPL 3+相容) 与其他协议代码链接 ...
- java smi s开发指导_【存储入门系列】SMI-S协议简介
SMI-S协议简介 介绍 SMI-S(Storage Management Initiative Specification存储管理主动)是SNIA(全球网络存储工业协会)发起并主导,众多存储厂商共同 ...
- java编程实现素数环_结对编程(JAVA实现)
项目成员:黄思扬(3117004657).刘嘉媚(3217004685) 二.PSP表格 PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟) Pla ...
- java编程题身高排队_编程初学者入门4_从键盘输入5个人的身高(米),求他们的平均身...
编程初学者入门4_从键盘输入5个人的身高(米),求他们的平均身 编程初学者入门4_从键盘输入5个人的身高(米),求他们的平均身高(米).(C的没什么问题,试着用Java写一下,Java的Scanner ...
最新文章
- 选项卡示例代码###
- java中 wait()和sleep()的差异
- mysql数据库重做日志文件_mysql数据库重做日志
- 洛谷P1551 亲戚题解
- (二十五)深度学习目标检测:RCNN
- 使用 jQuery 和 KnockoutJS 开发在线股票走势图应用
- HAproxy配置文件操作
- JAVA 项目中使用 H2 数据库
- DBUtils详细介绍+实例
- Vue使用Echarts控件实现图表设计
- 疑难杂症共济失调怎么治疗恢复?
- 软件工程第二次作业(王伟东)
- 【FLASH存储器系列六】SPI NOR FLASH芯片使用指导之二
- 织梦cms模板下载:集团企业通用织梦模板
- 计算机系统要素高清pdf,计算机系统要素:从零开始构建现代计算机[PDF][43.21MB]...
- ProSpec BCA-1人重组 (CXCL13)说明书
- 【PRINCE2总述】
- 自动驾驶--Deep3DBox
- 小软件发布:百度ai文字识别免费截图版
- 面经/字节跳动,面试流程及问题分享(附答案)
热门文章
- 2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_2-1微服务项目的搭建-SpringBoot搭建多模块项目一...
- 阶段3 2.Spring_08.面向切面编程 AOP_4 spring基于XML的AOP-配置步骤
- 阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第4节 等待唤醒机制_1_线程状态概述...
- day25 在继承的背景下属性查找的顺序、组合、多态与接口、鸭子类型
- radio按钮样式美化和checkbox按钮样式美化
- NSLog的各种打印格式
- hdu acmsteps 2.1.3 Cake
- 面向对象编程时,十条原则:
- Vue使用Axios实现http请求以及解决跨域问题
- 如何将浮点数点左边的数每三位添加一个逗号,如12000000.11转化为『12,000,000.11』...