【网络篇】从零写UDP客户端/服务器:回显程序源码解析

大家好呀
我是浪前
今天讲解的是网络篇的第四章:从零写UDP客户端/服务器:回显程序源码解析

从零写UDP客户端/服务器:回显程序源码解析

UDP 协议特性​

UDP(User Datagram Protocol)作为传输层协议,有着与 TCP 截然不同的特性,在网络通信中扮演着独特的角色,适用于对实时性要求高、能容忍少量数据丢失的场景。​

无连接通信:UDP 在数据传输前,发送方和接收方无需像 TCP 那样进行三次握手建立连接,可直接发送数据。无需复杂的连接建立过程,极大降低了传输延迟。

不可靠传输:它不保证数据一定能到达接收方,也不确保数据的顺序和完整性。若在网络传输中,UDP 数据包丢失或乱序,协议本身不会重传或纠正。

面向数据报:UDP 以独立的数据报为单位传输数据,每个数据报都包含完整的目标地址等信息,可独立传输,服务器收到后直接响应,简单高效。​

全双工通信:同一 UDP Socket 可同时进行数据的发送和接收。在语音通话应用中,双方能同时说话并实时听到对方声音,就是因为 UDP 的全双工特性,保证了语音数据的双向实时传输。​

核心类介绍​

在 Java 的 UDP 网络编程里,DatagramSocket和DatagramPacket是两个关键类,分别负责 socket 通信和数据报的封装传输,相互配合实现 UDP 通信功能。

UDP的socket应该如何使用:

UDP的API主要是提供了两个类:

  1. DatagramSocket
  2. DatagramPacket

1: DatagramSocket

socket: 本质上是操作系统中的一个概念,本质上是一个特殊的文件

这里的socket属于就是把"网卡"这个设备,给抽象成了文件,而进行网络通信最核心的硬件设备就是网卡

往socket文件中写数据,就相当于是通过网卡发送数据

往socket文件中读数据,就相当于是通过网卡接收数据

上面就是把文件操作和网络通信给统一了

在Java中就是使用这个 DatagramSocket 类就是来表示系统内部的socket文件

这个 DatagramSocket 类负责文件读写,也就是借助网卡发送和接收数据

2: DatagramPacket

使用这个类就是来表示一个UDP数据报:

UDP是面向数据报的

每次进行传输,都要以UDP数据报为基本单位

每次传输都只能够传输一个完整的数据报,不可以传输半个数据报,也不可以传输一个半数据报

写一个简单的UDP的客户端/服务器通信的程序:

这个程序没有什么业务逻辑,请求什么,就响应什么

只是单纯滴调用Socket API

让客户端给服务器发送一个请求,请求就是一个从控制台输入的字符串,服务器收到字符串之后,也就会把这个字符串原封不动地返回给客户端,然后客户端再显示出来,

这个程序就是请求什么,就响应什么

这个程序是最简单的网络通信程序,叫做回显服务器

回显服务器

服务器的主要功能:

负责接收客户端的请求,然后根据实际的业务场景来返回不同的响应

有一种服务器是回显服务器:

回显服务器的作用就是客户端发啥请求,回显服务器就立马返回啥请求,没有业务逻辑的

比如:

客户端发送想吃蛋炒饭的请求

服务器就接收到蛋炒饭的请求之后,就返回蛋炒饭的响应

这个回显服务器是网络编程中最简单的程序,相当与网络编程中的"Hello World"

回显服务器的作用

  1. 学会掌握Socket API的基本使用
  2. 学会典型的客户端服务器的工作流程

进行网络编程的第一步

服务器的代码(回显服务器)

我们先来写一个服务器代码:

创建一个DatagramSocket的对象:

注意:

这个对象是在创建在内存中的,直接对内存进行操作就可以影响到网卡

程序启动的同时要关联/绑定一个端口号,这个端口号是专门用来区分主机的

一个主机只能够有一个端口号,同时一个主机中的端口号只能和一个进程进行绑定,

一个端口号和进程A进行了绑定之后,如果进程B也要和这个端口号进行绑定,那么进程B会绑定失败

但是一个进程是可以同时和多个端口号进行绑定的

为什么?

因为每一个端口号对应了一个DatagramSocket对象,如果一个进程中有多个DatagramSocket对象的话,就可以和多个端口号进行绑定

如下图所示:

同时我们在创建DatagramSocket对象的时候必须要手动指定一个端口号

在运行一个服务器程序的时候,也要手动指定端口号

DatagramSocket对象的代码创建如下:

java 复制代码
package netWork;  
  
import java.net.DatagramSocket;  
import java.net.SocketException;  
  
public class Server {  
    private DatagramSocket socket = null;  
  
    public Server(int port) throws SocketException{  
        socket = new DatagramSocket(port);  
    }  
  
      
}

上述的代码抛出的异常为SocketException

这个异常是在创建DatagramSocket对象时,当主机的端口号已经被其他进程绑定的时候会抛出的异常

定义服务器启动方法:

接下来定义一个start方法来作为服务器启动的方法:

start方法的执行逻辑如下所示:

在start方法中会有一个while循环:

由于服务器每天从客户端那里接收到的请求有很多,

所以服务器会每时每刻都在不停地运行,每循环一次,就是服务器在接收到请求,返回响应的过程

在每次while循环中,会经历下面三个步骤 :

  1. 服务器读取客户端发来的请求,解析请求
  2. 服务器根据请求来计算响应(回显服务器没有这一步)
  3. 服务器向客户端返回响应
    代码如下:
java 复制代码
public void start(){  
    System.out.println("服务器启动");  
    while(true){  
        //每次循环,都是一次服务器在接收请求,返回响应的过程  
        //1.读取请求,解析请求  
        socket.receive();  
        //2.根据请求计算响应(回显服务器不需要这一步)  
        //3. 返回响应  
                  
}  
}

第一步:读取请求,进行解析,

socket.receive()

上面代码中的这个receive方法中需要填写一个DatagramPacket类型的参数,

这个参数是一个输出型参数,这个参数在文件IO中也涉及到了,实际上在DatagramPacket内部就会包含一个字节数组,如下所示

java 复制代码
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

这个字符数组会保存收到的消息正文,这个消息正文就是应用层数据包,也就是UDP数据报载荷部分,这个载荷空间的大小可以灵活设置

将这个参数传入socket.receive()中后,会抛出一个异常:

IOException : //网络编程,读写socket本质就是IO

java 复制代码
public void start() throws IOException {  
    System.out.println("服务器启动");  
    while(true){  
        //每次循环,都是一次服务器在接收请求,返回响应的过程  
        //1.读取请求,解析请求  
        DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);  
        socket.receive(requestPacket);  
        //2.根据请求计算响应(回显服务器不需要这一步)  
        //3. 返回响应  
    }  
}

手动创建内存空间:

当收到数据的时候,需要搞一个内存空间来保存这个数据

所以上面的requestPacket对象是用来承载从网卡那里读到的数据

但是由于在DatagramPacket的内部是不可以自行分配内存空间的,所以需要手动把内存空间创建好

这里创建了一个字节数组,这个字节数组就是真正的用来承载数据的内存空间

然后再交给DatagramPacket处理:

之后receive就会从这个requestPacket对象中读取数据,然后把读取到的数据填充到socket对象中去

此处receive就可以从网卡中读取一个UDP数据报

这个UDP数据报就是被放进了requestPacket对象中

其中UDP数据报的载荷部分被放进了requestPacket内置的字节数组中,同时,UDP的报头部分和收到的数据源IP,源IP端口都会被保存在 requestPacket的其他属性中

所以我们requestPacket还可以知道数据是从哪里来的(源IP源端口)

如果执行到receive的时候,还没有客户端发来请求,那么此时receive就没有可以读取的数据,此时receive就会发生阻塞,一直阻塞到客户端发来请求为止

将读取到的数据转成字符串:

此时的receive会读取到一个字节数组,

当receive读取完毕之后,数据是以二进制的形式存储到DatagramPacket中

要想能够把这里的数据给显示出来,就需要把这个二进制数据转化为字符串

所以,此时的读的字节数组必须要先转成(字符串)String之后,才方便后续的逻辑处理:

java 复制代码
String request = new String(requestPacket.getData(),0,requestPacket.getLength());

基于字节数组构造String,字节数组里面保存的内容不一定就是二进制数据,也可能是文本数据

而字符串(String) 不仅可以保存二进制数据,还可以保存文本数据,所以需要将这个字节数组转成字符串 :

注意:

在getLength()中获取的字节数组的有效数据的长度不一定就是4096

这个4096是这个字节数组的最大长度,

而getLength()获取到的结果是收到的数据的真实长度,即发送方这一次实际发送了多少个数据

比如:

如果这一次收到的数据长度是10,那么这个getLength()获取到的就是10。

所以我们这里构造字符串是使用有效数据长度来进行构造,不能使用字节数组的最大长度来构造

以上就是把一个请求转化为字符串了

封装分用:

网路通信过程中涉及到"封装和分用":

只有当应用层调用传输层提供的API的时候,才会把这个数据给读取到;

数据来到服务器时,会经由物理层,一层层分用到应用层:

在传输层中:会给每一个socket对象都分配一个缓冲区(这个缓冲区在操作系统内核里)

每次网卡收到一个数据都是经由层层分用,解析好之后,最终放到缓冲区里

在应用层的应用程序调用receive就是从这个缓冲区里面拿走一个数据

这个本质上就是生产者-消费者模型,而此处给socket对象分配的缓冲区就是阻塞队列

所以从客户端传过来的数据 不是存在socket文件中,而是存在socket对象中的一个内存缓冲区的阻塞队列中

第二步:根据请求来构造响应:

java 复制代码
String response = process(request);

这个代码要根据请求构造响应,通过这个process方法来构造响应

java 复制代码
public  String process(String request) {  
    return request;  
}

由于此处是回显服务器,所以只需要单纯滴返回这个请求就可以了

第三步: 把响应返回给客户端:

1: 构造一个响应对象DatagramPacket作为响应对象

同时由于UDP是无连接的,所以UDP不会保存要发给谁

所以就需要在每次发送的时候,重新指定,数据要发送到哪里去

所以在这个响应对象(数据报)中需要指定数据内容,也要指定数据具体要发送给谁。

java 复制代码
//构造一个DatagramPacket作为响应对象  
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
response.getBytes().length);

刚刚的socket对象还没有构造完毕,在构造时还需要指定一个socketAddress进去:

java 复制代码
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
response.getBytes().length,requestPacket.getAddress(),requestPacket.getPort());

这个requestPacket.getAddress()和requestPacket.getPort()

方法会获取到一个IP和一个端口号

这个IP和端口号是和服务器通信的对端的客户端的IP和 端口号

这个IP和端口号是从这个requestPacket这个客户端数据包中获取的

同时在代码中的response.getBytes().length获取到的是字节

在进行网络传输的时候,一定是使用字节来进行传输的

而response.length() 获取到的是字符,如果全是英文,那么字节和字符的个数一样,但是如果有中文,那么此时字节和字符的个数就不一样了。

为什么要获取到这个客户端的IP和端口号?

这里是把客户端(请求)中的源IP和源端口,作为响应的目的IP和目的端口

此时就可以做到把消息返回给客户端的效果了

此时我们的响应对象就构造好了,只需要将这个responsePacket作为参数使用send方法传递出去即可:

socket.send(responsePacket);

上述代码中,可以看到UDP 是无连接通信,UDP socket自身不保存对端(客户端)的IP和端口号 :

这个IP和端口号是在数据包中有一个,同时在代码中也没有"建立连接"和"接受连接"的操作

这个是直接读取请求,若没有请求,则阻塞等待,若有请求,则对请求进行解析,然后根据请求构造响应,最后返回响应

所谓的UDP不可靠传输目前代码中没有体现:

而UDP的面向数据报有体现: 上述代码中的send和receive的参数接收都是以DatagramPacket为单位进行发送和接收的

UDP的全双工在代码中也有体现:

一个socket既可以发送又可以接收,就叫做全双工

最后在代码中进行一个打印日志的操作:

java 复制代码
//打印日志:  
System.out.printf("[%s:%d] req: %s,resp: %s\n",requestPacket.getAddress().toString(),  
        requestPacket.getPort(), request, response);

之后撰写一个main方法即可:

java 复制代码
public static void main(String[] args) throws IOException {  
    Server server = new Server(9090);  
    server.start();  
}

上述服务器的代码编写完毕,

下面是回显服务器的完整代码:

java 复制代码
package netWork;  
  
import javax.xml.crypto.Data;  
import java.io.IOException;  
import java.net.*;  
import java.sql.SQLOutput;  
  
public class UdpServer {  
    //创建一个DatagramSocket对象,是后续进行网卡的基础  
    private DatagramSocket socket = null;  
  
    public UdpServer(int port)  throws SocketException{  
        //下面是手动指定端口  
        socket = new DatagramSocket(port);  
  
        //下面这么写就是系统自动分配端口  
        //socket = new DatagramSocket();  
    }  
  
    //程序的主方法,通过这个方法来启动服务器  
    public void start() throws IOException {  
        System.out.println("服务器启动");  
        //一个服务器要不停滴运行,所以需要一个while循环来进行操作,一个服务器程序是要长时间运行的  
        //为了保证客户端随时来,随时可以响应,  
        while(true){  
            //1.第一步,读取请求并解析  
            DatagramPacket requestPacket = new DatagramPacket(new byte[4090],4090);  
            socket.receive(requestPacket);  
            //将请求转化为字符串  
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());  
  
            //2.根据请求计算响应  
            //这个步骤是服务器最核心的一个步骤,  
            String response = process(request);  
  
  
            //3. 把响应返回给客户端  
            //使用一个响应对象  DatagramPacket  往响应对象中构造刚才的数据,再通过send返回  
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,  
                    requestPacket.getSocketAddress());  

			//使用send方法传入参数进行发送
			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 {  
        UdpServer server = new UdpServer(9090);  
        server.start();  
    }  
}

为什么上述的代码中没有出现close()?

socket也是一个文件,不进行关闭的话,会造成文件资源泄露

什么是文件资源泄露?

你一直申请,但是一直都没有进行close,没有进行释放,结果到最后,你想用的时候,发现用不了了,就是文件资源泄露。

为什么这里不写close()方法,也不会出现文件资源泄露呢?

因为socket是文件描述符表中的一个表项.

每次打开一个文件,就会占用一个位置,文件描述符是在PCB(进程)上的,是跟随进程的

这个socket在整个程序过程中一直都在使用,不可以提前释放,不可以提前关闭

当socket不使用的时候,此时整个程序也要结束了

当进程结束时,文件描述符表也会跟随着进程的结束被销毁,就可能发生泄露问题了。

总结:

不会泄露的原因是因为socket会随着进程销毁的过程中,被系统自动回收了

什么时候会出现泄露?

代码中频繁地打开文件,但是不关闭,在一个进程的运行过程中,不断积累打开的文件,逐渐消耗掉了文件描述符表里的内容,最后内容会被消耗光,就出现了泄露

但是如果进程的生命周期很短,打开一下就关闭了,也就不会出现泄露了

所以文件资源泄露的问题在服务器上经常出现,因为服务器的进程生命周期很长,要一直运行

泄露的问题在客户端上很少出现,因为客户端的进程的生命周期很短,客户端打开之后用一下就直接关闭了

客户端的代码:

接下来我们去 编写客户端的代码:

注意:服务器需要手动指定端口号

但是客户端不需要手动指定端口号,不手动指定也有端口号,
因为系统会自动给客户端分配一个空闲的端口号

为什么服务器必须要自己手动指定一个端口号?
因为服务器要保证端口号是固定不变的

因为只有在服务器代码中手动指定一个端口号

才能保证端口始终是固定的,如果不手动指定,服务器依赖系统自动分配端口号,就会导致服务器每次开机重启后,系统自动分配的端口号就发生了改变 。

如果服务器的端口号发生了改变,那么客户端就可能会找不到这个服务器在哪里了,所以服务器的端口号必须要在代码中手动指定

那么为什么客户端中的端口号不需要手动指定, 可以通过系统自动分配呢?

客户端的端口号让系统随机分配,系统会去分配给客户端一个空间中可用的端口号
如果是手动指定端口号,那么就无法确定这个端口号是不是可控的,有没有被别的进程占用

为什么服务器的端口号就不怕被别的进程占用呢?

因为服务器这个机器是在程序员手中的,程序员对于服务器上有哪些端口号是可用的一清二楚

但是客户端是在用户手中的,有千千万万个用户,上面的环境也千差万别,程序员无法得知端口号是否被占用,如果强行手动指定客户端的端口号就会导致端口绑定失败.

所以程序员手中的服务器的端口号是可以手动指定的,但是在用户手中的客户端的端口号是不能手动指定的,只能靠系统自动分配一个空闲的端口号

在构造方法中,由于UDP自身不会保存对端的信息,所以就需要在应用程序中,把对端的情况给记录下来,在构造方法中主要记录的就是对端的IP和端口,也就是目的IP和目的端口:

java 复制代码
  
public class Client {  
    //首先要创建socket对象,但是此处不需要手动指定端口号  
    DatagramSocket socket = null;  
  
    //构造方法:要传输服务器IP(目的IP)和服务器端口(目的端口)  
    public Client(String serverIp, int serverPort) throws SocketException {  
        socket = new DatagramSocket();  
    }  
}

接下来创建start方法来启动客户端:

在这个start方法中,依然是使用一个循环来不停滴发送请求:

在循环中一共要做四件事情:

  1. 从控制台中读取请求数据
  2. 构造请求并发送
  3. 读取服务器的响应
  4. 把响应显示到控制台上
java 复制代码
public void start(){  
    System.out.println("客户端启动");  
    Scanner scanner = new Scanner(System.in);  
    while(true){  
        System.out.println("-> ");  
        //1. 从控制台中读取请求数据   
        //2. 构造请求并发送  
        //3. 读取服务器的响应  
        //4. 把响应显示到控制台上  
    }  
}

第一步: 从控制台中读取数据,作为请求:

java 复制代码
String request = scanner.next();

这里从控制台读取请求,使用scanner读取字符串,最好使用next来读取,而不是使用nextLine

因为如果使用nextLine读取,可能会读取不到空格

nextLine遇到空格就自动作为分隔符了,所以需要手动输入换行符

使用enter来控制,由于enter键不仅仅会产生\n,还会产生其他的字符,就会导致当前的这个读取到的内容会出问题

而使用next其实是以"空白符"作为分隔符,包括但不限于换行,回车,空格,制表符,垂直制表符

总结:

如果从控制台读取内容,就使用next()
如果是从文件读取内容,那么使用next()和nextLine()都可以

第二步: 把请求的内容构造成一个DatagramPacket对象,在对象中保存数据,长度, 目的IP和目的端口:

java 复制代码
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),  
        request.getBytes().length, InetAddress.getByName(serverIp),serverPort);

然后将这个对象发给服务器:

java 复制代码
socket.send(requestPacket);

OK,此时客户端已经向服务器发送了请求,那么接下来就只需要去读取服务器返回的响应即可:

第三步: 尝试读取服务器返回的响应:

此时我们也是需要先构造一个空的DatagramPacket对象来接收响应的数据:

java 复制代码
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);

接下来使用这个responsePacket对象对响应进行接收即可:

java 复制代码
socket.receive(responsePacket);

第四步: 把响应转换成字符串,并显示出来:

java 复制代码
//4. 把响应转化成字符串,并显示到控制台上  
String response = new String(responsePacket.getData(),0,responsePacket.getLength());  
System.out.println(response);

上述的start方法就结束了,最后再补上一个main方法即可:

java 复制代码
  
public static void main(String[] args) throws IOException {  
    Client client = new Client("127.0.0.1",9090);  
    client.start();  
}

综上所述:客户端的完整代码如下所示:

java 复制代码
package netWork;  
  
import java.io.IOException;  
import java.net.*;  
import java.util.Scanner;  
  
public class Client {  
    //首先要创建socket对象,但是此处不需要手动指定端口号  
    DatagramSocket socket = null;  
  
    private String serverIp;  
    private int serverPort;  
  
    //构造方法:要传输服务器IP(目的IP)和服务器端口(目的端口)  
    public Client(String serverIp, int serverPort) throws SocketException {  
        this.serverIp = serverIp;  
        this.serverPort = serverPort;  
        //下面就是自动分配一个端口号  
        socket = new DatagramSocket();  
    }  
  
    public void start() throws IOException {  
        System.out.println("客户端启动");  
        Scanner scanner = new Scanner(System.in);  
        while(true){  
            System.out.println("-> ");  
            //1. 从控制台中读取请求数据  
            String request = scanner.next();  
  
            //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);  
  
            //4. 把响应转化成字符串,并显示到控制台上  
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());  
            System.out.println(response);  
  
        }  
    }  
  
    public static void main(String[] args) throws IOException {  
        Client client = new Client("127.0.0.1",9090);  
        client.start();  
    }  
  
}

通信过程:

此时客户端和服务器就可以相互配合,完成通信过程:

步骤如下:

  1. 先启动服务器
  2. 再启动客户端
  3. 在客户端中编写hello
  4. 然后就可以在服务器中看见

下面是 客户端的界面展示:

下面是服务器的界面展示:

执行过程:

  1. 第一步:服务器启动,进入while循环,执行到receive这里时发生阻塞(此时客户端还没有发送请求)
  2. 第二步:客户端开始启动: 也会进入while循环,执行scanner.next,并且在这里阻塞,直到用户在控制台输入,当用户输入字符串之后,next就会返回,从而构造请求数据并发送出来
  3. 第三步:客户端发送出数据之后,在服务器那边,就会从receive中返回数据,进一步的解析请求为字符串,执行process操作,执行send操作。 此时的客户端也会继续往下执行,执行到receive,等待服务器的响应
  4. 客户端收到服务器返回的数据之后,就会从receive中返回,执行这里的打印操作,也就把响应给显示出来了
  5. 服务器完成一次循环之后,就又会执行到receive,重新进入阻塞
  6. 客户端完成一次循环之后,就又会执行到scanner.next,重新进入阻塞

我们重点要理解网络程序的交互逻辑

刚刚的两个程序都是在一个主机上的,没有实现跨主机通信的效果

能否让同学使用客户端程序来访问老师的服务器代码呢?

如果我的服务器就在我的电脑上,此时,你是不可以直接访问的,除非老师和同学的电脑都在同一个局域网下,即同一个路由器下,才可以

但是还有一种方式" 云服务器"

有了这个,就可以访问老师的电脑了,因为老师的电脑没有公网IP,但是云服务器有公网IP

jar包是java打包的一种基本方式

把刚才的UDP服务器部署到云服务器上,进一步的,就可以让大家来访问了

之后可以调整一下客户端的代码,让客户端访问云服务器上的服务程序,就只需要把IP地址换成云服务器的IP即可

相关推荐
搬码临时工2 分钟前
路由器转发规则设置方法步骤,内网服务器端口怎么让异地连接访问的实现
服务器·网络·智能路由器·内网穿透·端口映射·外网访问
落笔画忧愁e8 分钟前
数据通信学习笔记之OSPF的基础术语
网络·笔记·学习
终身学习基地38 分钟前
第七篇:linux之基本权限、进程管理、系统服务
linux·运维·服务器
安顾里43 分钟前
LInux平均负载
linux·服务器·php
unlockjy1 小时前
Linux——进程优先级/切换/调度
linux·运维·服务器
成工小白1 小时前
【Linux】详细介绍进程的概念
linux·运维·服务器
wayuncn2 小时前
双卡 4090 服务器租用:释放强算力的新选择
运维·服务器
YGGP2 小时前
【每日八股】复习计算机网络 Day4:TCP 协议的其他相关问题
网络·tcp/ip·计算机网络
桃花岛主702 小时前
WebSocket是h5定义的,双向通信,节省资源,更好的及时通信
网络·websocket·网络协议
爱的叹息2 小时前
本地(NAS/服务器)与云端(Docker/Kubernetes)部署详解与对比
服务器·docker·kubernetes