22. 网络编程(1)——UDP 协议

Java
264
0
0
2022-11-28

网络编程需要依靠Socket API,在java标准库中有两种风格: 1.(UDP)DatagramSocket:面向数据报(发送接收数据,必须以一定的数据报为单位进行传输) 2.(TCP)ServerSocket:面向字节流

UDP和TCP就是传输层的两个最重要的协议

UDP

实现一个最简单的服务器(回显服务器 echo server),客户端给服务器发送一个字符串,服务器把这个字符串返回显示出来

对于一个服务器程序,核心流程分成两步

1.进行初始化操作 2,进入主循环,接收并处理请求(主循环就是死循环) a)读取数据并解析 b)根据请求计算响应 c)把响应结果写回到客户端

服务器:

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

/**
 * UDP 服务器
 */
public class UdpEchoServer {
    //对于一个服务器程序,核心流程分成两步 
    //1.进行初始化操作 
    //2,进入主循环,接收并处理请求(主循环就是死循环) 
    //  a)读取数据并解析 
    //  b)根据请求计算响应 
    //  c)把响应结果写回到客户端 
    private DatagramSocket socket = null;
    //DatagramSocket 本质上是一个文件,这个文件是网卡的抽象

    //构造方法 
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
        //new的时候就会让socket对象和一个端口号和一个IP地址关联在一起(绑定端口) 
        //未来的客户端就按照这个IP和端口号来访问服务器 
        //如果在构造socket的时候没有写IP,就是 0.0.0.0(会关联到这个主机的所有网卡IP) 
        //IP是决定互联网的某个主机的位置,port是决定数据交给这个主机的哪个位置
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //  a)读取数据并解析 
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //new byte[4096],4096  相当于关联了缓冲区 
            // DatagramPacket是发送和接收数据的基本单位
            socket.receive(requestPacket);
            //程序启动之后马上就能执行到receive 
            //大多时候调用receive的时候,客户端还没有发送请求,这时receive就会阻塞,当真的有客户端数据过来之后,就会把收到的数据放入缓冲区 
            String request = new String(requestPacket.getData(), 0,requestPacket.getLength()).trim();
            //此处需要把请求数据转成string(本来是byte[]) 
            //requestPacket.getData()获取到缓冲区,也就是byte数组,然后从0开始,到缓冲区长度处结束 
            //.trim():用户实际发送的数据可能远远小于4096,此时getLength()获取到的都是4096,此时就可以通过trim来去掉一些空白空间

            //  b)根据请求计算响应 
            String response = process(request);
            //  c)把响应结果写回到客户端 
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requestPacket.getSocketAddress());
            //response.getBytes():响应数据就是response,需要包装成一个Packet对象 
            //request.getBytes().length:获取长度,得到的是字符数 
            //requestPacket.getSocketAddress():指定当前数据发给谁,这个方法就把IP端口号全部获取到,就设置到了responsePacket里面
            socket.send(responsePacket);

            //以下部分可省略
            System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }

    private String process(String request) {
        //由于此处是一个回显服务器,所以只需要原分不动的返回 
        //但是如果是其他复杂的服务器,就需要在这里有更多的逻辑 
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

对于一个客户端程序,核心流程分成两步

1.从用户这里读取输入的数据 2.构造一个请求发送给服务器 3.从服务器读取响应 4.把响应写回给客户端

客户端:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

/**
 * UDP 客户端
 */
public class UdpEchoClient {
    //客户端的主要流程分为4步 
    //1.从用户这里读取输入的数据 
    //2.构造一个请求发送给服务器 
    //3.从服务器读取响应 
    //4.把响应写回给客户端

    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    //需要在启动客户端的时候指定需要连接哪个服务器 
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        this.serverIP = serverIP;
        this.serverPort = serverPort;
        socket = new DatagramSocket();//客户端创建socket的时候不需要绑定端口号,由操作系统自动分配一个空闲端口
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1.读取用户输入的数据
            System.out.print("->");//提示符:提示用户输入字符 
            String request = scanner.nextLine();//作为请求 
            if (request.equals("exit")){
                //结束 
                break;
            }
            
            //2.构造一个请求发送给服务器 
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIP), serverPort);
            //InetAddress.getByName(serverIP), serverPort: 要把数据报发给哪个服务器:指定IP和端口号
            socket.send(requestPacket);

            //3.从服务器读取响应 
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength()).trim();
            
            //4.把响应写回给客户端
            System.out.println(response);
        }
    }


    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        //"127.0.0.1":是环回IP,因为此时客户端和服务器在一台主机上 
        //9090:这个端口要和服务器绑定的端口相匹配
        client.start();
    }
}

img

img

如果想要完成较为复杂的逻辑,就可以通过继承,重写process方法实现 例如现在想要完成词典翻译的服务器:

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

//实现一个翻译功能
public class UdpDictServer extends UdpEchoServer {

    private Map<String,String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("find","找到");
    }

    @Override 
    public String process(String request) {
        return dict.getOrDefault(request,"这超出了我的知识范围");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}