UDP版本服务器

什么是客户端: 主动发起请求的一方. 客户端给服务器发送的数据是 “请求” (Request)
什么是服务器: 被动接受请求的乙方. 服务器给客户端发回的数据"响应" (Response)
但是服务器不知道什么时候客户端会发起请求,服务器只能提前准备好,并且一直等待

下面介绍两个概念

在这里插入图片描述
ip地址:用来识别互联网上的一台主机的位置 ip本质上是一个32位的整数,常用3个点把这个数分成四个部分,每个部分一个字节(0-255),这就是"点分十进制"
端口号:用来区分一台主机上的哪个应用程序 端口号就是一个整数,取值范围是0-65535之间(占两个字节的整数)

一次通信过程中涉及到五个概念(五元组):
源IP:发件人地址
源端口:发件人姓名
目的IP:收件人地址
目的端口:收件人姓名
协议类型

Java中使用SocketAPI
java标准库中提供了两种风格:
[UDP] DatagramSocket:面向数据报(发送接受数据报,必须以一定的数据报为单位进行传输)
[TCP] ServerSocket:面向字节流

下面实现一个最简单的客户端服务器
客户端给服务器发送一个字符串,服务器把这个字符串原封不动的返回(回显服务器 Echo Server)

服务器:

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.DatagramPacket;
public class UdpEchoServer{
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength()).trim();
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s; resp: %s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

注释版:

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

public class UdpEchoServer {
    // 对于一个服务器程序来说, 核心流程也是要分成两步.
    // 1. 进行初始化操作 (实例化 Socket 对象)
    // 2. 进入主循环, 接受并处理请求. (主循环就是一个 "死循环")
    //   a) 读取数据并解析
    //   b) 根据请求计算响应
    //   c) 把响应结果写回到客户端.
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        //new这个Socket对象的时候,就会让当前的socket对象和一个ip地址和一个端口号进行绑定
        //未来客户端就按照这个ip+端口来访问服务器
        //如果在socket的时候没有写ip地址,那么默认的ip地址就是0.0.0.0(特殊ip,表示会关联到这个主机的所有网卡的ip,一个主机可能会有多个网卡)
        socket = new DatagramSocket(port);
        //socket本质上是一个文件,这个文件是网卡的抽象
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            // a) 读取请求并解析.
            //DatagramPacket是UDPSocket发送接收数据的基本单位
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //程序启动之后,马上就能执行到receive操作
            //服务器启动了之后,此时客户端不一定发送了请求
            //所以调用reveive时,客户端可能还没发数据
            //此时receiver操作就会阻塞,一直阻塞到真的有数据过来了为止.
            //当真的有客户端的数据过来了之后,此时receiver就会把收到的数据放到DatagramPacket对象的缓冲区中
            socket.receive(requestPacket);
            //此处是要把请求数据转成一个String(本来请求是一个byte数组,经过new String后转成string,使用trim 方法是因为
            // 用户输入的数据可能远远小于4096,而此处的getLength的带的长度就是4096,通过trim可以删除不必要的空白符)
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength()).trim();
            // b) 根据请求计算响应
            String response = process(request);
            // c) 把响应写回给客户端, 响应数据就是 response, 需要包装成一个 Packet 对象
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requestPacket.getSocketAddress());
            //↑这个最后一个参数就代表这个包要发给谁,此处的address地址就是客户端的地址和端口,这两个信息已经包含在requestPacket内部了;
            socket.send(responsePacket);

            // [这是一个锦上添花的操作] 打印一条请求日志.
            System.out.printf("[%s:%d] req: %s; resp: %s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }

    public String process(String request) {
        // 由于此处是一个 echo server, 请求内容是啥, 响应内容就是啥.
        // 如果是一个更复杂的服务器, 此处就需要包含很多的业务逻辑来进行具体的计算.
        return request;
    }

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

客户端:

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

public class UdpEchoClient {
    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();
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("-> ");
            String request = scanner.nextLine();
            if (request.equals("exit")) {
                break;
            }
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

带注释版:

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

public class UdpEchoClient {
    // 客户端的主要流程分成四步.
    // 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的时候不需要绑定端口号,由操作系统自动分配一个空闲端口,一个端口号通常情况下只能与一个进程绑定
        //如果客户端绑定了端口,那么一个主机上就只能启动一个端口了
        //服务器必须绑定端口.客户端才能访问,客户端才知道去访问谁.
        socket = new DatagramSocket();
    }

    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);
            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 {
        //这是一个环回ip,自己访问自己,服务器和客户端都在同一台主机上,客户端写的服务器ip就是环回ip,如果不在同一个主机上,此处的ip就要写成服务器的ip
        //并且后面的端口要和服务器绑定的端口相匹配
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        // UdpEchoClient client = new UdpEchoClient("47.98.116.42", 9090);
        client.start();
    }
}

站在客户端角度 理解此处通信的五元组:

  • 协议类型:UDP
  • 源IP:客户端的IP(客户端所在的主机IP)
  • 源端口:客户端的端口(操作系统自动分配的端口)
  • 目的ip:服务器的ip(服务器和客户端在同一个主机上,ip就是127.0.0.1)
  • 目的端口:9090(服务器启动的时候绑定的端口)

总结:
客户端的主要流程分成四步.

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

客户端的主要流程分成四步.
4. 从用户这里读取输入的数据.
5. 构造请求发送给服务器
6. 从服务器读取响应
7. 把响应写回给客户端.


实现一个简单的英译汉服务器,体会其中的"根据请求计算响应"

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);

        // 所谓 "有道词典" 实现, 本质上就是个 hash 表.
        // 只不过人家的那个 hash 非常大
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("fuck", "卧槽");
    }

    @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();
    }
}

可以看到,服务器功能不同主要是process内的逻辑不同,这也是一个服务器最关键的逻辑.


Tcp协议有连接,类似于打电话,打通了才能对话
Udp协议无连接,类似于发短信,不管别人看不看,我发就完事了

下面写一下另一组API(TCP的socket API)
写一个简单的回显服务器

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpEchoServer {
    // 1. 初始化服务器
    // 2. 进入主循环
    //   1) 先去从内核中获取到一个 TCP 的连接
    //   2) 处理这个 TCP 的连接
    //     a) 读取请求并解析
    //     b) 根据请求计算响应
    //     c) 把响应写回给客户端
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        // 这个操作和前面的 UDP 类似, 也是要绑定端口号.
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true) {
            // 1) 先从内核中获取到一个 TCP 连接
            Socket clientSocket = serverSocket.accept();
            // 2) 处理这个连接
            processConnection(clientSocket);

            //TCP的连接管理是由操作系统内核管理的
            //所谓的管理就是描述+组织
            //通过通信中的五元组进行描述(协议类型....)
            //组织就是使用一个阻塞队列来组织若干个连接对象

            //客户端和服务器建立连接的过程,完全由内核进行负责,应用程序完全感觉不到
            //当连接建立成功时,内核已经把这个连接对象放到阻塞队列中了.
            //代码中调用的accept就是从阻塞队列中取出一个连接对象(在应用程序中的化身就是socket对象)
            //后续数据读写都是针对clientSocket这个对象来进行展开的

            //如果服务器启动之后,没有客户端建立连接,此时代码调用accept就会阻塞,阻塞到真的有客户端建立连接之后

        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        // 通过 clientSocket 来和客户端交互, 先做好准备工作. 获取到 clientSocket 中的流对象
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            // 进一步的就可以完成后面的逻辑 TODO
            // 1. 读取请求并解析
            // 2. 根据请求计算响应
            // 3. 把响应写回到客户端
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值