不务正业 (1) —— 在应用层模拟实用停等协议

【总目录】http://www.cnblogs.com/tbcaaa8/p/4415055.html

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 extends JFrame{
    private JLabel labelServerPort = new JLabel();
    private JLabel labelClientPort = new JLabel();
    private JTextField textServerPort = new JTextField();
    private JTextField textClientPort = new JTextField();
    private JButton buttonSetPort = new JButton();

    private JTextArea textMessage = new JTextArea();
    private JButton buttonSend = new JButton();

    ......  (与Socekt相关的变量)

    private void initWindow(){
        this.setSize(320, 336);
        this.getContentPane().setLayout(null);
        this.setLocationRelativeTo(null);
        this.setResizable(false);
        this.setTitle("实验一");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.addWindowListener(new WindowListener(){
            @Override
            public void windowClosing(WindowEvent e) {
                ...... (断开连接)
            }
            @Override public void windowClosed(WindowEvent e) { }
            @Override public void windowActivated(WindowEvent e) { }
            @Override public void windowDeactivated(WindowEvent e) { }
            @Override public void windowDeiconified(WindowEvent e) { }
            @Override public void windowIconified(WindowEvent e) { }
            @Override public void windowOpened(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(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                buttonSetPort.setEnabled(false);
                ......  (建立连接)
                buttonSend.setEnabled(true);
            }
        });
        this.add(buttonSetPort);

        textMessage.setLineWrap(true);
        textMessage.setWrapStyleWord(true);
        textMessage.setEditable(false);
        JScrollPane panelMessage = new JScrollPane(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(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                ......  (发送文件)
            }
        });
        this.add(buttonSend);

        this.setVisible(true);
    }

    public Main(){

        super();
        initWindow();

    }

    public static void main(String[] args) {
        new Main();
    }

除滚动条的创建稍显繁琐外,其余代码的含义较为简单,不再一一解释。但需要注意的是,对于窗口而言,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 class Client {
 2     private int port;
 3     private boolean valid = false;
 4     private Socket s = null;
 5     private DataOutputStream dos = null;
 6
 7     private class Create extends Thread{
 8         @Override
 9         public void run(){
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=new DataOutputStream(s.getOutputStream());
19             }catch (Exception e) {
20                 valid=false;
21                 e.printStackTrace();
22             }
23         }
24     }
25
26     public void create(int port){
27         this.port=port;
28         valid=true;
29         new Create().start();
30     }
31
32     ......
33 }

2.2.2 发送数据帧

在发送过程中,程序将重复“发送→等待”的过程,为了防止“等待”过程卡死用户界面,同样需要为数据帧的发送单独创建线程。

数据帧应该包含什么呢?至少应该包含以下内容:

帧类型:用来区分这一帧是数据帧还是ACK帧还是NAK帧

帧序号:取值0或1,在处理超时重传过程中,用于区分是数据帧丢失还是ACK帧丢失的情况

是否为最后一帧:用来确定文件传输合适结束

数据长度:这个不解释了= =

数据:这个也不解释了= =

校验码:用来校验帧的数据部分是否出错

为了尽可能简化程序代码,我们可以大幅度牺牲执行效率——毕竟作为实验课程序,不惜一切代价保证正确性才是重要的。为了使帧的表示尽可能直观,我没有采用字节数组表示帧,而是采用了字符串。帧内不同的部分之间,使用某个特定的间隔符分隔(例如“|”字符),为了确保间隔符不出现在数据部分中,还需要对数据部分进行一定的处理。至于具体的处理方法,就仁者见仁智者见智了,只要保证发送方编码后的数据能够被接收方正确解码即可。在使用字符串表示帧时,数据长度是不必要的,因为可以通过数据两端的间隔符的位置直接计算出数据长度。

 1 public class Client {
 2
 3     ......
 4
 5     private byte[] fileBuffer = null;
 6
 7     private class SendFile extends Thread{
 8         @Override
 9         public void run(){
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<Main.timeOut);
20                     if(server.receiveACK){//对方返回ACK
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()<Main.pCRC32Error)dos.writeUTF(frame[k]+"0");//概率出错
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()<Main.pCRC32Error)dos.writeUTF(frame[k-1]+"0");//错误的CRC
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 boolean sendFile(String path){
47         if(!valid)return false;
48         try {
49             File file = new File(path);
50             long fileSize = file.length();
51             FileInputStream fis = new FileInputStream(file);
52             byte[] buffer = new byte[(int)fileSize];
53             int offset = 0;
54             int numRead = 0;
55             while (offset<buffer.length && (numRead=fis.read(buffer,offset,buffer.length-offset))>=0){
56                 offset += numRead;
57             }
58             fis.close();
59             fileBuffer=buffer;
60             new SendFile().start();
61
62         } catch (Exception e) {
63             e.printStackTrace();
64             return false;
65         }
66         return true;
67     }
68
69     ......
70 }

对于链路中各类出错情况的模拟也并不复杂:

帧丢失:直接不发送

帧出错:给帧赋予错误的校验码再发送

帧超时:若干秒后发送(代码中未体现)

2.2.3 发送ACK/NAK帧

此处定义:

ACK N:确认之前收到的帧,希望收到帧序号为N的帧

NAK N:帧序号为N的帧出错,请求重传

因此,ACK/NAK帧的帧结构可以非常简单,只需要帧类型和序号N这两部分即可。同样可以使用字符串的形式表示帧。

ACK/NAK帧的发送过程很短,而且不需要等待进一步的回复,因此无需使用单独的线程,可以放在主线程中。代码如下:

 1 public class Client {
 2
 3     ......
 4
 5     public void sendACK(int frameIndex){
 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(int frameIndex){
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 }

同样地,我们需要模拟帧丢失和帧超时的情况。

2.3 Server的设计

Server部分需要完成两个任务:一是创建服务端,二是监听端口接收数据。

2.3.1 创建服务端

注:本小节的理解需要与2.2.1小节相结合

Server的创建同样不复杂,java为我们遮盖了太多的细节,使得编程倾向于傻瓜化。代码含义显而易见,不再做过多解释。

 1 public class Server {
 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(int port){
15         valid=true;
16         try {
17             ss=new ServerSocket(port);
18         } catch (Exception e) {
19             valid = false;
20             e.printStackTrace();
21         }
22
23         Run run=new Run();
24         run.start();
25
26     }
27
28     ......
29 }

2.3.2 监听端口接收数据

回顾2.3.1小节的代码,可以发现在创建服务端创建结束后,有一个新的线程被创建,这个线程就是用来监听端口的。由于对端口的监听会阻塞线程,为了避免主线程被阻塞,为端口监听安排一个独立的线程是必要的。代码如下:

 1 public class Server {
 2
 3     ......
 4
 5     private class Run extends Thread{
 6         @Override
 7         public void run(){
 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 }

这里介绍一下String类的split函数。顾名思义,split函数的作用是对字符串进行分割,返回值为字符串数组,表示分割后的每一部分。其参数为正则式表示的分割规则,在本应用环境下,可以理解为帧的不同部分之间的分隔符。然而之前使用的“|”符号在正则式中有特殊含义,需要通过“\”转义,需要将“|”表示为“\|”。与C/C++类似,“\”字符在java语言的字符串中本身就是转义字符,表示其本身时应双写为“\\”。因此,在使用“|”作为分隔符时,split的参数应写为“\\|”。

FileFrame类只有成员变量没有成员函数,用来表示经过解码后的帧。

2.4 其他

对于上述操作的每一步,留下记录。

作为一种省事的方法,可以在以上每个类的构造函数中,记录用户界面中消息显示控件对应的对象,直接在其中追加内容即可。

这样会导致对用户界面的操作掺杂在整个协议中,不过反正是实验而已管他呢~

3. 连续ARQ与滑动窗口协议

连续ARQ与滑动窗口协议可以在实用停等协议的基础上加以改造。

对于连续ARQ协议:

取消NAK,发生校验错误时等Client超时重传

ACK的含义发生了变化

发送端增加滑动窗口,适当选择窗口大小

对于滑动窗口协议:

取消NAK,发生校验错误时等Client超时重传

ACK的含义发生了变化

增加发送窗口和接收窗口,适当选择窗口大小

建议参考课本P89图示,而不是P91图示

  滑动窗口协议效果示例:

发送端:

接收端:

4. 应对老师检查的小技巧

当老师要求你演示传输文件时,尽量传输文本文件。

一旦程序写渣了,造成接收到的文件不完整,PPT/DOC/ZIP/EXE等文件根本打不开!

而TXT文件依旧可以打开,很难被老师看出瑕疵。

时间: 2024-10-10 06:23:52

不务正业 (1) —— 在应用层模拟实用停等协议的相关文章

计算机网络- 可靠数据传输协议-停等协议的设计与实现

一.所实现停等协议简介 我设计的程序实现的是rdt3.0版本的停等协议,发送端将数据包以0.1交替的顺序发送数据包,当发送0数据包之后开始计时,只有接收到ack0才能继续发送1数据包,如果在没有接收到ack0的时候已经超时,这时候需要重传数据包0: 接收方按照所收到的数据包的编号返回相应的ack,当上一个收到的是数据包0后下一个只有是数据包1才能接收放到接收文件中,如果下一个还是数据包0那么就要丢弃. 二.分组格式 数据报格式: 三.UDP编程的特点 UDP协议是无连接的数据传输格式,所以就不用

基于UDP协议模拟的一个TCP协议传输系统

TCP协议以可靠性出名,这其中包括三次握手建立连接,流控制和拥塞控制等技术. 我写的这个系统基于UDP协议模拟了一个TCP协议,所实现的功能如下: 1.三次握手 2.一个计时器,用来判断传输超时行为 3.快速重传 4.能处理不同的MSS(maximum segment size) 5.没有被接收方确认收到的包会被存在发送方,最大可以存MWS个(Maximum Window size) 6.能处理包丢失的情况 7.接收方一旦收到包,立刻发送确认信息给发送方 8.接收方的超时时间是固定的 9.接收方

ios开发——实用技术篇&amp;XML协议详解

XML的数据协议组成 名词 说明 md5 message-digest algorithm 5 http hypertext transfer protocol xml extensible markup language 交易交互是以http协议作为数据传输协议,这里定义发起交易请求的一端为客户端,客户端需要以http post 数据流(非表单方式)的方式提交交易请求,如下所示: 假设有一个查询指定玩法可销售期信息的交易请求,那么http消息体的内容如下: 1 <?xml version=”1

计算机网络-应用层(2)FTP协议

FTP 使用了两个并行的TCP 连接来传输文件: 控制连接(control connection)用于在两主机之间传输控制信息,如用户标识.口令.改变远程目录的命令以及存放(put)文件.获取(get)文件的命令. 因为FTP协议使用一个独立的控制连接,所以我们也称FTP的控制信息是带外(out-of-band) 传送的.HTTP也可以说是带内(in-band) 发送控制信息的. 数据连接(data connection) 用于实际发送一个文件HTTP 协议是在传输文件的同一个TCP 连接中发送

阿里云云计算认证ACP模拟考试练习题第6套模拟题分享(共10套)

阿里云认证考试包含ACA.ACP.ACE三种认证类型,报名考试最多的是ACP认证考试,本人整理了100道全真阿里云ACP认证考试模拟试题,适合需要参加阿里云ACP认证考试的人复习,模拟练习.此为第6套模拟题分享. 阿里云云计算认证ACP模拟考试练习题6 认证级别 云计算 大数据 云安全 中间件 助理工程师(ACA) 云计算助理工程师认证报名入口 大数据助理工程师认证报名入口 云安全助理工程师认证报名入口 专业工程师(ACP) 云计算工程师认证报名入口 大数据工程师认证报名入口 大数据分析师认证报

telnet客户端模拟浏览器发送请求

telnet 客户端 telnet客户端能够发出请求去连接服务器(模拟浏览器) 使用telnet之前,需要开启telnet客户端 1.进入控制面板 2.进入程序和功能,选择打开或关闭windows功能 3.进入后找到telnet客户端,点击确定 模拟浏览器发出http协议请求 1.打开telnet客户端:进入cmd 2.连接apache服务器 语法:telnet localhost port(telnet localhost 80) 3.数据回显 3.1同时按住:ctrl+右中括号 3.2按下回

GPIO模拟SPI

上次用gpio模拟i2c理解i2c协议,同样的,我用gpio模拟spi来理解spi协议. 我用的是4线spi,四线分别是片选.时钟.命令/数据.数据. 数据在时钟上升沿传递,数据表示的是数据还是命令由命令/数据线决定. 开始条件: void spi_start(void) { gpio_config(GPIO_CS, GPIO_OUTPUT); udelay(SPI_SPEED_DURATION); gpio_set(GPIO_CS, 0);/* start condition */ udela

基于Linux应用层的6LOWPAN物联网网关及实现方法

本发明涉及一种基于Linux应用层的6LOWPAN物联网网关及实现方法,所述物联网网关包括开发平台以及无线射频模块,其实现方法是:所述6LOWPAN物联网网关的以太网网口收到访问6LOWPAN无线传感器网络中节点的数据包,Linux应用层将以太网数据包格式转化成6LOWPAN物联网的格式,然后通过无线射频模块发送出去:同理,Linux应用层同时监听无线射频模块,收到6LOWPAN无线传感器网络中的数据包,所述Linux应用层将数据包转化成以太网数据包格式,再通过以太网网口把该数据包发送出去.本发

Web开发中的网络知识(应用层)

一.网络分层 二.应用层协议--HTTP.DNS和SSH 2.1 http协议 1. 定义:超文本传输协议( Hyper Text Transfer Protocol ),是用于传输诸如HTML的超媒体文档的应用层协议.它被设计用于Web浏览器和Web服务器之间的通信,但也可以用于其它目的. HTTP遵循经典的客户端-服务端模型,客户端打开一个连接以发出请求,然后等待收到服务器端的响应. HTTP是无状态协议,意味着服务器不会在两个请求之间保留任何数据(状态).虽然通常基于TCP / IP层,但