在当今这个数字化时代,网络无处不在,它像一张无形的网,将全球的信息和资源紧密相连。无论是日常浏览网页、在线购物,还是企业级的分布式系统、云计算服务,背后都离不开网络编程的支持。而在网络编程的广阔领域中, UDP(用户数据报协议)和 TCP(传输控制协议)作为两大基石,扮演着至关重要的角色。


一、为什么我们需要了解UDP和TCP?

网络编程的核心在于数据的传输与控制,而 UDP 和 TCP 正是实现这一目标的关键协议。它们定义了数据如何在网络中传输,包括如何打包、如何路由、如何接收等。掌握这两种协议的工作原理和特性,对于开发者而言,意味着能够设计出更加稳定、高效、安全的网络通信应用。


二、UDP 和 TCP 的主要区别

2.1、区别总结

UDP:

  1. 无连接
  2. 不可靠传输
  3. 面向数据报
  4. 全双工

TCP:

  1. 有连接
  2. 可靠传输
  3. 面向字节流
  4. 全双工

2.2、有无连接

首先解释第一个,有无连接,这个连接不是真的连接哈,是一种抽象概念上的连接,就好比说,微信大家都用过吧?里面有个微信钱包,你想收红包的话就必须绑定身份证,当你绑定好身份证之后,这个微信也就和你连接了起来,这也是种抽象的连接,这种连接得双方面都承认才好使,就假设说这个化腾老哥一个劲儿的说这个微信号是你的,但是你不承认,就是不绑定你的身份信息,那没用,反之你一个劲的说这个微信号是你的,但是这个微信号已经被别人绑定身份信息了,那也没用。

2.3、传输可不可靠

大家通过这个字面意思应该都很容易理解这个可靠传输和不可靠传输的区别吧?可靠传输并不是说 A 给 B 发的消息百分百能得到哈,这个太难了点,网线断了啊,人家在忙没看到消息啊这些都是 A 这边的不可控因素,可靠传输是指 A 尽可能的把消息传给 B 并且在传输失败的时候, A 可以感知到,或者说在传输成功的时候 A 也能知道自己传输成功了,比如说打电话,这就是一个可靠传输,如果说对面信号不好没听清,A 也是能感知到的,再比如我们平常使用的微信啊,短信啊这些就是不可靠传输,因为在你发消息成功后,不知道对方是否能感知到,但是企业级微信和钉钉这些是可靠传输,因为如果说对方没看消息,那消息旁边会有个未读,这样我们就可以感知到对方是否收到消息了。

2.4、面向数据报/字节流

UDP 是面向数据报的,读写的基本单位是一个UDP的数据报,这个数据报应该怎么来理解呢?等说完字节流,再一起对比讲解

TCP 是面向字节流的,和文件操作类似,都是以“流“的形式传播,传输的单位是字节,所以称为字节流。

那这两者有什么区别呢?就好比说你打水,面向数据报是你使用一个桶来打,一个单位就是一桶,而字节流是指使用水管,然后让水流过去,所以 UDP 的一次传输是有大小限制的,而 TCP 没有。

2.5、全双工

学过计网的大家应该都知道全双工、半双工、单工这些,简单来说,全双工就是 A 可以对 B 发消息,B 可以收到消息,B 也可以对 A 发消息,A 也能收到消息。如下图:

网络编程之 UDP 、TCP 详解_网络编程


三、TCP和UDP自身的优缺点

3.1、UDP 的优缺点

优点:

  • 低延迟:由于无需建立连接,UDP 传输数据的延迟较低。
  • 高效率:UDP 报头的开销比较小,适合快速传输少量数据。
  • 适合广播和多播:UDP 支持广播和多播,非常适合需要同时传输给多个接收方的应用。

缺点:

  • 不可靠传输:UDP 不保证数据的可靠性,数据可能丢失或重复。
  • 无流控制:UDP 没有流控制机制,可能会导致网络拥塞。
  • 无拥塞控制:UDP 不提供拥塞控制,可能对网络资源造成过度占用。

3.2、TCP 的优缺点

优点:

  • 可靠性高:TCP 提供数据重传、顺序保证和错误检测,保证数据的可靠传输。
  • 流量控制和拥塞控制:TCP 通过控制滑动窗口和拥塞控制算法,优化网络资源的使用、避免网络拥塞。
  • 面向连接:TCP 的连接管理机制保证了同系的呢稳定性和可靠性。

缺点:

  • 开销大:TCP 头部文件信息偏多,传输开销较大。
  • 延迟高:TCP 的连接建立和维护过程增加了传输延迟。
  • 复杂性:TCP 的实现和管理较为复杂。

四、服务器的代码实现

4.1、基于 UDP 协议实现回显服务器

首先我们要想实现 UDP ,得先了解怎么把一个要求变成一个数据报,因为 UDP 传输的基本单位就是一个数据报。这就得先了解我们的 DatagramPacket 类了,这是 Java 本身提供的一个 API ,这个 API 可以接收数据报和发送数据报,还有一个创建服务器实例的 API DatagramSocket 下面是代码实现:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UDPServer {
    private DatagramSocket socket = null;

    public UDPServer(int port) throws SocketException {
        // 首先得创建一个服务器实例,端口号的话是要自己指定的,不然随机分配的话客户端那边不好访问
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        // 添加一个日志
        System.out.println("My UDP 服务器启动!!!");
        // 服务器要一直不停的工作
        while (true) {
            // 首先建立一个空的 DatagramSocket 数据报,然后把客户端的请求数据报装到这个数组里面
            DatagramPacket keHuDuan = new DatagramPacket(new byte[4096], 4096);
            // 接收客户端发送的请求数据报,这时候这个请求还是个数据报,我们得解析出请求
            socket.receive(keHuDuan);
            // 解析请求
            String qingQiu = new String(keHuDuan.getData(), 0, keHuDuan.getLength());
            // 对客户端的请求做出响应
            String xiangYing = process(qingQiu);
            // 然后再把我们的响应打包成一个数据报返还给客户端那边
            // 需要传递的参数:传递的内容,内容的长度以及传给谁??
            DatagramPacket packet = new DatagramPacket(xiangYing.getBytes(StandardCharsets.UTF_8), // 传递的内容
                    xiangYing.length(), // 内容的长度
                    keHuDuan.getSocketAddress());// 得到客户端的 ip 地址
            socket.send(packet);
            // 最后打印一个日志,证明我们传好了
            System.out.printf("\n客户端端口号[%d]\n请求:%s; 响应:%s\n",
                    keHuDuan.getPort(),
                    qingQiu,
                    xiangYing
            );
        }
    }

    public String process(String request) {
        return request;
    }

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

4.2、基于 TCP 协议实现回显服务器

想实现一个 TCP 协议的服务器,首先得知道我们 TCP 创建服务器实例的 API ServerSocket ,然后我们还得知道,在我们 TCP 读取请求和发送响应的时候,用的是 InputStream 和 OutputStream 这两个文件操作,并且发送的时候,也是打包成 PrintWriter 这个文件,所以我前面提到了 和文件操作类型 这个关键词,并且还加粗了,就是这个原因,下面是代码实现。

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

public class TCPServer {
    private ServerSocket serverSocket= null;

    public TCPServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("My TCP 服务器启动!!!");
        while (true) {
            // 首先得到客户端的请求
            Socket lianJie = serverSocket.accept();
            // 再处理请求
            processLianJie(lianJie);
        }
    }

    private void processLianJie(Socket lianJie) {
        // 打印一个日志
        System.out.printf("[%s:%d] 客户端上线!\n",
                lianJie.getInetAddress().toString(),
                lianJie.getPort()
                );
        // 读取客户端请求
        try (
                InputStream inputStream = lianJie.getInputStream();
                OutputStream outputStream = lianJie.getOutputStream();
                ) {
            while (true) {
                // 直接使用 Scanner 来读取请求
                Scanner sc = new Scanner(inputStream);
                if (!sc.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线!\n",
                            lianJie.getInetAddress().toString(),
                            lianJie.getPort()
                    );
                    break;
                }
                // 拿到连接之后先读取客户端的请求
                String qingQiu = sc.next();
                String xiangYing = process(qingQiu);
                // 处理好之后直接打包成文件发过去
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(xiangYing);
                // 还得记得关闭这个文件操作
                printWriter.flush();
                // 打印一个日志
                System.out.printf("客户端ip及端口号[%s:%d]\n输入: %s, 响应: %s\n",
                        lianJie.getInetAddress().toString(),
                        lianJie.getPort(),
                        qingQiu,
                        xiangYing
                        );
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String process(String qingQiu) {
        return qingQiu;
    }

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

但是我们这个代码还是有些小问题的哈,因为 TCP 和 UDP 的服务器的处理方式不同,UDP 是不需要和客户端建立连接的,所以在处理客户端请求的时候就不会出现阻塞,而 TCP 就不一样了,和一个客户端连接之后,就会阻塞,一直到这个线程下了,才会解除阻塞,对于这种情况,我们可以使用多线程解决。所以,更进的代码如下:

package boKe.myTCP;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TCPServer {
    private ExecutorService service = Executors.newCachedThreadPool();
    private ServerSocket serverSocket= null;

    public TCPServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("My TCP 服务器启动!!!");
        while (true) {
            // 首先得和客户端建立连接
            Socket lianJie = serverSocket.accept();
            // 再处理请求
            service.submit(() -> {
                processLianJie(lianJie);
            });
        }
    }

    private void processLianJie(Socket lianJie) {
        // 打印一个日志
        System.out.printf("[%s:%d] 客户端上线!\n",
                lianJie.getInetAddress().toString(),
                lianJie.getPort()
                );
        // 读取客户端请求
        try (
                InputStream inputStream = lianJie.getInputStream();
                OutputStream outputStream = lianJie.getOutputStream();
                ) {
            while (true) {
                // 直接使用 Scanner 来读取请求
                Scanner sc = new Scanner(inputStream);
                if (!sc.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线!\n",
                            lianJie.getInetAddress().toString(),
                            lianJie.getPort()
                    );
                    break;
                }
                // 拿到连接之后先读取客户端的请求
                String qingQiu = sc.next();
                String xiangYing = process(qingQiu);
                // 处理好之后直接打包成文件发过去
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(xiangYing);
                // 还得记得关闭这个文件操作
                printWriter.flush();
                // 打印一个日志
                System.out.printf("客户端ip及端口号[%s:%d]\n输入: %s, 响应: %s\n",
                        lianJie.getInetAddress().toString(),
                        lianJie.getPort(),
                        qingQiu,
                        xiangYing
                        );
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String process(String qingQiu) {
        return qingQiu;
    }

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

五、客户端代码实现

5.1、基于 UDP 协议实现客户端

大家还记得两个 API 吗?DatagramSocketDatagramPacket 前者是接收和发送请求的,后者是打包成数据报的,废话不多说,直接上代码,如下:

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class UDPClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    public UDPClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        // 创建一个 DatagramSocket ,并让系统自动分配端口号
        this.socket = new DatagramSocket();
    }

    private void start() throws IOException {
        // 打印日志
        System.out.println("My UDP 客户端启动!!!");
        Scanner sc = new Scanner(System.in);
        // 想一直输入,一直看结果
        while (true) {
            System.out.print("请输入->");
            String qingQiu = sc.next();
            // 打包成一个 DatagramPacket 数据报,发送的内容,发送的目的地端口和 id
            DatagramPacket packet = new DatagramPacket(
                    qingQiu.getBytes(StandardCharsets.UTF_8)
                    , qingQiu.getBytes().length,
                    InetAddress.getByName(serverIp),
                    serverPort
            );
            // 通过 send 方法发送出去
            socket.send(packet);
            // 然后再接收服务器的响应
            DatagramPacket xiangYingShuJuBao = new DatagramPacket(new byte[4096], 4096);
            socket.receive(xiangYingShuJuBao);
            String xiangYing = new String(xiangYingShuJuBao.getData(), 0, xiangYingShuJuBao.getLength());
            System.out.println(xiangYing);
        }
    }

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

写到这里,就可以试着启动我们的 UDP 服务器,然后再启动我们的 UDP 客户端来试试了,运行结果如下:

网络编程之 UDP 、TCP 详解_客户端_02

5.2、基于 TCP 协议实现客户端

和 TCP 的客户端差不多,只要理解清楚用如何接收请求和发送请求就非常好做,代码如下:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TCPClient {
    private Socket socket = null;

    public TCPClient(String serverIp, int serverPort) throws IOException {
        this.socket = new Socket(serverIp, serverPort);
    }

    private void start() {
        // 打印一个日志
        System.out.println("My TCP 客户端启动!!!");
        Scanner sc = new Scanner(System.in);
        try (
                InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream();
        ) {
            while (true) {
                // 先输入你的请求
                System.out.print("请输入->");
                String qingQiu = sc.nextLine();
                // 打包成 PrintWriter
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(qingQiu);
                printWriter.flush();
                // 然后接收服务器的响应
                Scanner xiangYingSc = new Scanner(inputStream);
                String xiangYing = xiangYingSc.next();
                System.out.println(xiangYing);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

下面是运行结果,可以开多个客户端访问这个服务器

网络编程之 UDP 、TCP 详解_客户端_03


六、总结

在这篇博客中,我们详细探讨了网络编程的基础知识,特别是 UDP(用户数据报协议)和 TCP(传输控制协议)的核心概念及其应用。这两种协议作为互联网通信的基石,对于开发者设计稳定、高效和安全的网络应用至关重要。

6.1、了解 UDP 和 TCP 的必要性

通过深入了解 UDP 和 TCP 的工作原理和特性,开发者可以更好地设计和优化网络通信应用。无论是低延迟、高效率的 UDP 还是可靠性高、具有流量和拥塞控制机制的 TCP,每种协议都有其独特的优势和适用场景。

6.2、UDP 和 TCP 的主要区别

我们总结了 UDP 和 TCP 在有无连接、传输可靠性、面向数据报还是字节流、以及全双工等方面的主要区别。UDP 无需连接、传输效率高但不可靠,适用于对实时性要求高的应用;而 TCP 则需建立连接、传输可靠,但延迟较高,适用于需要保证数据完整性和顺序的应用。

6.3、优缺点分析

深入分析了 UDP 和 TCP 各自的优缺点,帮助开发者在实际应用中选择合适的协议。UDP 的低延迟、高效率和适合广播、多播的特性,使其在实时音视频传输和在线游戏中得到广泛应用。而 TCP 的高可靠性、流量控制和拥塞控制,使其成为网页浏览、文件传输和电子邮件等应用的首选。

6.4、服务器和客户端代码实现

通过详细的代码实例,我们展示了如何在 Java 中实现基于 UDP 和 TCP 的服务器和客户端应用程序。具体步骤包括:

  • UDP 服务器和客户端:使用 DatagramSocketDatagramPacket 类实现回显服务器和客户端,展示了如何接收和发送数据报。
  • TCP 服务器和客户端:使用 ServerSocketSocketInputStreamOutputStream 类实现回显服务器和客户端,并通过多线程处理多个客户端连接,展示了如何读取和发送字节流。

七、结语

网络编程作为现代软件开发的重要组成部分,其核心在于数据的传输与控制。通过深入理解和掌握 UDP 和 TCP 协议的工作原理和实现方法,开发者能够设计出更为高效、稳定和可靠的网络通信应用。在未来的开发过程中,我们应根据应用需求选择合适的协议,灵活运用其优缺点,以达到最佳的通信效果。

希望这篇博客能够帮助读者更好地理解网络编程的基础知识,并在实际开发中有所应用。感谢大家的阅读,期待与各位在评论区交流讨论