打开抖音互联网会发生什么(作业)丨青训营笔记

238 阅读4分钟

这是我参与「第三届青训营-后端场」笔记创作活动的的第5篇笔记。

1. UDP socket实现ack,感知丢包重传

1.1 要求

  1. 学会 UDP socket 编程
  2. 先从简单的 ack 学习,客户端等待 ack 再发包
  3. 什么时候客户端认为是丢包?
  4. 重传怎么考虑效率?
  5. 能不能不阻塞只传丢掉的中间的段?

1.2 概念

UDP协议提供的服务不同于TCP协议的端到端服务,UDP面向非连接,属于不可靠连接,UDP套接字在使用前不需要进行连接。实际上,UDP协议只实现了两个功能:

  • 在IP协议的基础上添加了端口
  • 对传输数据过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。

1.3 UDP的通信建立的步骤

1.3.1 客户端

UDP客户端首先向被动等待联系的服务器发送一个数据报文。一个典型的UDP客户端要经过三个步骤:

  1. 创建一个DatagramSocket实例,可以有选择对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器发送来的数据。
  2. 使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信。
  3. 通信完成后,调用DatagramSocket实例的close()方法来关闭套接字。

1.3.2 服务端

由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外UDP服务器为所有通信使用同一套接字,这点与TCP不同,TCP为每个成功返回的accept()方法创建一个新的套接字。一个典型的UDP服务端需要经过以下三步操作:

  1. 创建一个DatagramSocket实例,指定本地端口号,并且可以有选择的指定本地地址,此时服务端已经准备好从任何客户端接收数据报文;
  2. 使用DatagramSocket实例的recive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端地址,这样就知道了回复信息应该发送到什么地方。
  3. 使用DatagramSocket实例的send()方法向服务端返回DatagramPacket实例

1.4 SocketClientUDP

UDP程序在receive()方法处阻塞,直到收到一个数据报文或者等待超时。由于UDP协议是不可靠的协议,如果数据报在传输过程中发生丢失,那么程序将会一直阻塞在reveive()方法处,这样客户端将永远都收不到服务端发送回来的数据,但是又没有任何提示。为了避免这个问题,在客户端使用Socket类的setSoTimeout()方法来指定receive()方法最长阻塞时间,并制定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。

//非多线程
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPClient {
    private static final int TIMEOUT = 5000; //设置接收数据的超时时间,传到后面的方法中——单位ms
    private static final int MAXNUM = 5;
    public static void main(String args[]) throws IOException {
        UDPClient client = new UDPClient();
        for(int i = 0; i < 10; ++ i) {
            client.run(i);
        }
    }

    public void run(int id) throws IOException { //每次发送都重新创建一些socket?是否有必要?
        String str_send = "Hello UDP Server, I'm client" + String.valueOf(id);//发送的字符串
        byte[] buf = new byte[1024];//接收数据

        DatagramSocket ds = new DatagramSocket(9000);//客户端9000端口监听接收到的数据
        InetAddress loc = InetAddress.getLocalHost(); //获取本机ip地址

        //定义用来发送数据的DatagramPacket实例
        DatagramPacket dp_send = new DatagramPacket(str_send.getBytes(), str_send.length(), loc, 3000); //发送到本地3000端口

        //定义接收数据的DatagramPacket实例
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024); //读到的数据存放在buf中,length表示读入的长度,不能超过buf的长度

        //数据发向本地的3000端口
        ds.setSoTimeout(TIMEOUT);//设置超时时间,接收数据阻塞的最长时间
        int tries = 0; //重发数据的次数
        boolean receivedResponse = false;//是否接收到数据的标志位

        //直到接收到数据,或者重发次数达到预定值,则退出循环
        while (!receivedResponse && tries < MAXNUM) {
            ds.send(dp_send);
            try{
                //接收从服务端发送回来的数据
                ds.receive(dp_receive);
                //如果接收到的数据不是来自(发送的)目标地址,则抛出异常
                if(!dp_receive.getAddress().equals(loc)){
                    throw new IOException("Received packet from an unknow source");
                }
                receivedResponse = true;
            } catch (InterruptedIOException e){
                tries += 1;
                System.out.println(String.valueOf(id) + ". Time out, " + (MAXNUM - tries) + " more tries...");
            }
        }

        if(receivedResponse) {
            //如果收到数据,则打印出来
            System.out.println("Client received data from server: ");
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) + " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();
            System.out.println(str_receive);
            //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数
            //所以这里要将dp_receive的内部消息长度重新置为1024
            dp_receive.setLength(1024);
        } else {
            //如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息
            System.out.println("No reponse -- give up.");
        }
        ds.close();
    }
}

1.5 SocketServerUDP

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
    public static void main(String[] args) throws IOException {
        String str_send = "Hello UDP client ";
        byte[] buf = new byte[1024];

        //服务端在3000端口监听接收到的数据
        DatagramSocket ds = new DatagramSocket(3000);

        //接收从客户端发送过来的数据
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024);
        System.out.println("Server is on, waiting for client to send data......");
        boolean f = true;
        while (f) {
            //服务器端接收来自客户端发送过来的数据
            ds.receive(dp_receive);
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) +
                    " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();
            System.out.println(str_receive);

            //数据发送到客户端的9000接口
            String str_send0 = str_send + str_receive.charAt(28);
            DatagramPacket dp_send = new DatagramPacket(str_send0.getBytes(), str_send0.length(), dp_receive.getAddress(), 9000);
            ds.send(dp_send);
            dp_receive.setLength(1024);
        }
        ds.close();
    }
}

1.4和1.5节所写代码完成了1.1的前四个要求。

1.6 满足要求5

1.6.1 Server

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class Server {
    DatagramSocket ds;
    void serve(int port) {
        while(true) {
            try {
                ds = new DatagramSocket(port);
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                ds.receive(packet);
                handler(packet);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(ds != null) {
                    ds.close();
                }
            }
        }
    }

    void handler(DatagramPacket packet) throws IOException, InterruptedException {
        double d = Math.random();
        if(d > 0.4) {
            if(d > 0.8) {
                Thread.sleep(1000);
            }
            String s = new String(packet.getData(), packet.getOffset(), packet.getLength());
            Packet pkt = Packet.getPacketObject(s);
            System.out.println("Receive:" + s);
            pkt.ack = true;
            InetAddress address = packet.getAddress();
            int port = packet.getPort();
            System.out.println(address);
            System.out.println(port);
            byte[] data = Packet.getJsonObject(pkt).getBytes();
            DatagramPacket pkts = new DatagramPacket(data, data.length, address, port);
            ds.send(pkts);
        } else {
            String s = new String(packet.getData(), packet.getOffset(), packet.getLength());
            System.out.println("Discard:"+s);
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.serve(9000);
    }
}

1.6.2 Client

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

public class Client {
    private static final int TIMEOUT = 5000; //设置接收数据的超时时间,传到后面的方法中——单位ms
    private static final int PORT = 9000; //监听端口
    Map<Integer, Boolean> cMap = new ConcurrentHashMap<>();//ConcurrentHashMap并发安全:可见性、顺序性、原子性
    private AtomicBoolean done = new AtomicBoolean(false);
    DatagramSocket socket;

    Client(int n) {
        for(int i = 0; i < n; ++ i) {
            cMap.put(i, false);
        }
    }

    class SenderThread extends Thread{
        public void run() {
            while (true) {
                for(Map.Entry<Integer, Boolean> entry : cMap.entrySet()) {
                    if(entry.getValue() == false) {
                        String str = "{'id':"+entry.getKey()+",'ack':false,'buf':'hello'}";
                        byte[] data = str.getBytes(StandardCharsets.UTF_8);
                        InetAddress loc = null;
                        try {
                            loc = InetAddress.getLocalHost();
                        } catch (UnknownHostException e) {
                            e.printStackTrace();
                        }
                        if(loc == null) continue;
                        DatagramPacket packet = new DatagramPacket(data, data.length, loc, PORT);
                        //packet.setData(data);//这句话不需要吧?
                        if(socket!=null) {
                            try {
                                socket.send(packet);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }

                try {
                    Thread.sleep(TIMEOUT);//超时重传
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                boolean ok = true;
                for(boolean v : cMap.values()) {
                    ok = ok & v;
                }
                done.set(ok);
                if(done.get()) return;
            }
        }
    }

    class ReceiverThread extends Thread{
        public void run() {
            byte[] buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            while (true) {
                if (socket != null) {
                    try {
                        socket.receive(packet);
                        String s = new String(packet.getData(), packet.getOffset(), packet.getLength());
                        Packet pkt = Packet.getPacketObject(s);
                        if (pkt.ack) {
                            cMap.put(pkt.id, true);
                            System.out.println(s);
                        }
                    } catch (IOException e) {
                        //当发送线程全部发送完之后,并且判断所有的信息已经被服务端响应,会关闭连接,此时会产生异常
                        break;
                    }
                }
            }
        }
    }

    public static void main(String args[]) {
        Client client = new Client(10);
        String hostname = "localhost";
        if(args.length > 0) {
            hostname = args[0];
        }
        try {
            client.socket = new DatagramSocket();
            SenderThread sender = client.new SenderThread();
            sender.start();
            Thread receiver = client.new ReceiverThread();
            receiver.start();
            while(!client.done.get()){}
            client.socket.close();
        } catch (SocketException e) {
            e.printStackTrace();
        }
        return;
    }
}

1.6.3 Packet

import com.google.gson.Gson;

public class Packet {
    public int id;
    public boolean ack;
    public String buf;
    public Packet() {}

    public Packet(int id, boolean ack, String buf) {
        this.id = id;
        this.ack = ack;
        this.buf = buf;
    }

    public static Packet getPacketObject(String json) {
        return new Gson().fromJson(json, Packet.class);
    }

    public static String getJsonObject(Packet pkt) {
        return new Gson().toJson(pkt);
    }

    @Override
    public String toString() {
        return "Packet{" +
                "id=" + id +
                ", ack=" + ack +
                ", buf='" + buf + ''' +
                '}';
//        String str="ID:"+id+"\nACK:"+ack+"\nBUF:"+buf;
//        return str;
    }

    public static void main(String[] args) {
        Packet pkt = new Packet(1, false, "hello");
        String s = Packet.getJsonObject(pkt);
        System.out.println(s);
        System.out.println(Packet.getPacketObject(s));
    }
}

参考资料

www.cnblogs.com/yongdaimi/p…