这是我参与「第三届青训营-后端场」笔记创作活动的的第5篇笔记。
1. UDP socket实现ack,感知丢包重传
1.1 要求
- 学会 UDP socket 编程
- 先从简单的 ack 学习,客户端等待 ack 再发包
- 什么时候客户端认为是丢包?
- 重传怎么考虑效率?
- 能不能不阻塞只传丢掉的中间的段?
1.2 概念
UDP协议提供的服务不同于TCP协议的端到端服务,UDP面向非连接,属于不可靠连接,UDP套接字在使用前不需要进行连接。实际上,UDP协议只实现了两个功能:
- 在IP协议的基础上添加了端口
- 对传输数据过程中可能产生的数据错误进行了检测,并抛弃已经损坏的数据。
1.3 UDP的通信建立的步骤
1.3.1 客户端
UDP客户端首先向被动等待联系的服务器发送一个数据报文。一个典型的UDP客户端要经过三个步骤:
- 创建一个DatagramSocket实例,可以有选择对本地地址和端口号进行设置,如果设置了端口号,则客户端会在该端口号上监听从服务器发送来的数据。
- 使用DatagramSocket实例的send()和receive()方法来发送和接收DatagramPacket实例,进行通信。
- 通信完成后,调用DatagramSocket实例的close()方法来关闭套接字。
1.3.2 服务端
由于UDP是无连接的,因此UDP服务端不需要等待客户端的请求以建立连接。另外UDP服务器为所有通信使用同一套接字,这点与TCP不同,TCP为每个成功返回的accept()方法创建一个新的套接字。一个典型的UDP服务端需要经过以下三步操作:
- 创建一个DatagramSocket实例,指定本地端口号,并且可以有选择的指定本地地址,此时服务端已经准备好从任何客户端接收数据报文;
- 使用DatagramSocket实例的recive()方法接收一个DatagramPacket实例,当receive()方法返回时,数据报文就包含了客户端地址,这样就知道了回复信息应该发送到什么地方。
- 使用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));
}
}