• javaEE -8(9000字详解网络编程)


    一:网络编程基础

    1.1 网络资源

    所谓的网络资源,其实就是在网络中可以获取的各种数据资源,而所有的网络资源,都是通过网络编程来进行数据传输的。

    网络资源包括:视频资源,图片资源,文本资源等等

    用户在浏览器中,打开在线视频网站,如在 b 站看视频,实质是通过网络,获取到网络上的一个视频资源,与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
    在这里插入图片描述
    那么我们怎么将这些网络资源在不同的设备上进行传输呢?答案是网络编程

    1.2 网络编程

    网络编程:指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
    在这里插入图片描述
    当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

    对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。

    但是我们一定要明确我们的目的是提供网络上不同主机,基于网络来传输数据资源:

    • 进程A:编程来获取网络资源
    • 进程B:编程来提供网络资源

    1.3网络编程中的基本概念

    1.3.1 发送端和接收端

    在一次网络数据传输时:

    • 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
    • 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
    • 收发端:发送端和接收端两端,也简称为收发端。

    注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。

    在这里插入图片描述

    1.3.2 请求和响应

    一般来说,获取一个网络资源,涉及到两次网络数据传输:

    • 第一次:请求数据的发送
    • 第二次:响应数据的发送。

    好比在快餐店点一份炒饭:先要发起请求:点一份炒饭,
    再有快餐店提供的对应响应:提供一份炒饭

    在这里插入图片描述

    1.3.3 客户端和服务端

    • 服务端:提供服务的一方进程,称为服务端
    • 客户端:获取服务的一方进程,称为客户端。

    对于服务来说,一般有两种:

    1. 客户端获取服务资源

    在这里插入图片描述

    1. 客户端保存资源在服务端

    在这里插入图片描述

    1.3.4 常见的客户端服务端模型

    最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

    1. 客户端先发送请求到服务端
    2. 服务端根据请求数据,执行相应的业务处理
    3. 服务端返回响应:发送业务处理结果
    4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

    在这里插入图片描述

    二:Socket套接字

    因为应用层获得数据需要通过传输层传输数据,所以我们要通过应用层调用传输层,而传输层也为应用层提供了 socket api,用于应用层调用传输层,

    这个api有两组:

    1. 基于UDP的api,
    2. 基于TCP的api,

    因为TCP和UDP的差别很大,所以这两组api的差别也很大。

    2.1 TCP 和 UDP

    TCP,即Transmission Control Protocol(传输控制协议)

    UDP,即User Datagram Protocol(用户数据报协议)

    TCP 和 UDP都是传输层协议,但是TCP比UDP复杂,因为TCP要保证可靠性,同时又尽可能的提高性能,因为TCP的机制,不管TCP怎么提高效率,TCP还是不如UDP的

    2.2 TCP和UDP特点解析

    • TCP的特点是:有连接,可靠传输,面向字节流,全双工
    • UDP的特点是:无连接,不可靠传输,面向数据报,全双工

    下面说说这些特点的含义是什么:

    1. 有连接 vs 无连接:

    有连接是指在数据传输之前,发送方与接收方需要先建立一个可靠的通信链路,以确保双方能够互相通信。在传输过程中,双方需要维护一定的状态信息。

    而无连接则是指在数据传输之前,发送方和接收方之间不需要建立可靠的通信链路。每个数据包都是独立传输的,不关心之前或之后的数据包是否丢失。

    1. 可靠传输 vs 不可靠传输:

    可靠传输是指数据在传输过程中会被发送方和接收方进行确认,并进行丢失、错误、重复等错误处理,确保数据的完整性和正确性。发送方会等待接收方的确认信息,如果没有得到确认,会重新发送数据。

    不可靠传输则是指数据在传输过程中不会进行确认,不保证数据的完整性和正确性。

    1. 面向字节流 vs 面向数据报:

    面向字节流是指数据在传输过程中被看作是一连串的无结构的字节流,发送方按照字节流进行传输,接收方按照相同的字节流进行接收,不考虑数据的边界或分段。

    面向数据报则是指数据在传输过程中被分割成数据报的形式进行传输,每个数据报都包含了数据的完整信息,接收方根据数据报的边界进行接收和处理。

    1. 全双工 vs 半双工:

    全双工是指发送方和接收方能够同时进行数据的发送和接收,可以进行双向通信,彼此不会干扰对方,发送方和接收方之间的通信是完全独立的。例如:在电话通信中,发送方和接收方可以同时说话和倾听。

    半双工则是指发送方和接收方不能同时进行数据的发送和接收,每个时刻只能进行单向通信。发送方和接收方之间的通信是互斥的。例如:在对讲机通信中,一个人说话时,另一个人只能倾听。

    三:UDP数据报套接字编程

    3.1 DatagramSocket API

    DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。

    DatagramSocket 构造方法:

    方法签名方法说明
    DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口
    DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口

    DatagramSocket 方法:

    方法签名方法说明
    void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
    void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送)
    void close()关闭此数据报套接字

    3.2 DatagramPacket API

    DatagramPacket是UDP Socket发送和接收的数据报。

    DatagramPacket 构造方法:

    方法签名方法说明
    DatagramPacket(byte[] buf, int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组中,接收指定长度
    DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组中,从0到指定长度。address指定目的主机的IP和端口号

    DatagramPacket 方法:

    方法签名方法说明
    InetAddress.getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
    InetAddress.getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
    DatagramPacket.getData()获取数据报中的数据

    构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建。

    3.3 InetSocketAddress API

    InetSocketAddress ( SocketAddress 的子类 )构造方法:

    方法签名方法说明
    InetSocketAddress(InetAddress addr, int port)创建一个Socket地址,包含IP地址和端口号

    3.4 案例演示

    一发一收(无响应)

    3.4.1 UDP服务端

    package org.example.udp.demo1;
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.util.Arrays;
    public class UdpServer {
      //服务器socket要绑定固定的端口
      private static final int PORT = 8888;
      
      public static void main(String[] args) throws IOException {
        // 1.创建服务端DatagramSocket,指定端口,可以发送及接收UDP数据报
        DatagramSocket socket = new DatagramSocket(PORT);
        
        //不停的接收客户端udp数据报
        while (true){
        
          // 2.创建数据报,用于接收客户端发送的数据
          byte[] bytes = new byte[1024];//1m=1024kb, 1kb=1024byte, UDP最多64k(包含UDP首部8byte)
    
          DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
          System.out.println("------------------------------------------------
    ---");
          System.out.println("等待接收UDP数据报...");
          
          // 3.等待接收客户端发送的UDP数据报,该方法在接收到数据报之前会一直阻塞,接收到数据报以后,DatagramPacket对象,包含数据(bytes)和客户端ip、端口号
          socket.receive(packet);
          System.out.printf("客户端IP:%s%n",packet.getAddress().getHostAddress());
          System.out.printf("客户端端口号:%s%n", packet.getPort());
          System.out.printf("客户端发送的原生数据为:%s%n", Arrays.toString(packet.getData()));
          System.out.printf("客户端发送的文本数据为:%s%n", new String(packet.getData()));
       }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    运行后,服务端就启动了,控制台输出如下:


    等待接收UDP数据报…

    可以看出,此时代码是阻塞等待在 socket.receive(packet) 代码行,直到接收到一个UDP数据报。

    3.4.2 UDP客户端

    package org.example.udp.demo1;
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    public class UdpClient {
    
      // 服务端socket地址,包含域名或IP,及端口号
      private static final SocketAddress ADDRESS = new InetSocketAddress("localhost", 8888);
      public static void main(String[] args) throws IOException {
      
        // 4.创建客户端DatagramSocket,开启随机端口就行,可以发送及接收UDP数据报
        DatagramSocket socket = new DatagramSocket();
        
        // 5-1.准备要发送的数据
        byte[] bytes = "hello world!".getBytes();
        
        // 5-2.组装要发送的UDP数据报,包含数据,及发送的服务端信息(服务器IP+端口号)
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length,ADDRESS);
    
        // 6.发送UDP数据报
        socket.send(packet);
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:


    等待接收UDP数据报…
    客户端IP:127.0.0.1
    客户端端口号:57910
    客户端发送的原生数据为:[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33,
    0, 0, 0, …此处省略很多0]
    客户端发送的文本数据为:hello world!


    等待接收UDP数据报…

    从以上可以看出,发送的UDP数据报,在接收到以后(假设接收的数据字节数组长度为N,假设发送的数据字节数组长度为M):

    1. 如果N>M,则接收的byte[]字节数组中会有很多初始化byte[]的初始值0,转换为字符串就是空白字符;
    2. 如果N

    要解决以上问题,就需要发送端和接收端双方约定好一致的协议,如规定好结束的标识或整个数据的长度。

    四:TCP流套接字编程

    4.1 ServerSocket API

    ServerSocket 是创建TCP服务端Socket的API。

    ServerSocket 构造方法:

    方法签名方法说明
    ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口。

    ServerSocket 方法:

    方法签名方法说明
    Socket accept()开始监听创建时绑定的端口,有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
    void close()关闭此套接字

    4.2 Socket API

    Socket 构造方法:

    方法签名方法说明
    Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

    Socket 方法:

    方法签名方法说明
    getInetAddress()返回套接字所连接的地址
    getInputStream()返回此套接字的输入流
    getOutputStream()返回此套接字的输出流

    4.3 案例演示

    一发一收(短连接)

    4.3.1 TCP服务端

    package org.example.tcp.demo1;
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    public class TcpServer {
    
      //服务器socket要绑定固定的端口
      private static final int PORT = 8888;
      public static void main(String[] args) throws IOException {
      
        // 1.创建一个服务端ServerSocket,用于收发TCP报文
        ServerSocket server = new ServerSocket(PORT);
        
        // 不停的等待客户端连接
        while(true) {
          System.out.println("---------------------------------------------------");
          System.out.println("等待客户端建立TCP连接...");
          
          // 2.等待客户端连接,注意该方法为阻塞方法
          Socket client = server.accept();
          System.out.printf("客户端IP:%s%n",client.getInetAddress().getHostAddress());
          System.out.printf("客户端端口号:%s%n", client.getPort());
          
          // 5.接收客户端的数据,需要从客户端Socket中的输入流获取
          System.out.println("接收到客户端请求:");
          InputStream is = client.getInputStream();
          
          // 为了方便获取字符串内容,可以将以上字节流包装为字符流
          BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
          String line;
          
          // 一直读取到流结束:TCP是基于流的数据传输,一定要客户端关闭Socket输出流才表示服务端接收IO输入流结束
          while ((line = br.readLine()) != null) {
            System.out.println(line);
         }
         
          // 6.双方关闭连接:服务端是关闭客户端socket连接
          client.close();
       }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行后,服务端就启动了,控制台输出如下:


    等待客户端建立TCP连接…

    可以看出,此时代码是阻塞等待在 server.accept() 代码行,直到有新的客户端申请建立连接。

    4.3.2 TCP客户端

    package org.example.tcp.demo1;
    import java.io.*;
    import java.net.Socket;
    public class TcpClient {
    
      //服务端IP或域名
      private static final String SERVER_HOST = "localhost";
      
      //服务端Socket进程的端口号
      private static final int SERVER_PORT = 8888;
      public static void main(String[] args) throws IOException {
      
        // 3.创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
        Socket client = new Socket(SERVER_HOST, SERVER_PORT);
        
        // 4.发送TCP数据,是通过socket中的输出流进行发送
        OutputStream os = client.getOutputStream();
        
        // 为了方便输出字符串作为发送的内容,可以将以上字节流包装为字符流
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
        
        // 4-1.发送数据:
        pw.println("hello world!");
        
        // 4-2.有缓冲区的IO操作,真正传输数据,需要刷新缓冲区
        pw.flush();
        
        // 7.双方关闭连接:客户端关闭socket连接
        client.close();
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    客户端启动后会发送一个"hello world!" 的字符串到服务端,在服务端接收后,控制台输出内容如下:


    等待客户端建立TCP连接…
    客户端IP:127.0.0.1
    客户端端口号:51118
    接收到客户端请求:
    hello world!


    等待客户端建立TCP连接…

    以上客户端与服务端建立的为短连接,每次客户端发送了TCP报文,及服务端接收了TCP报文后,双方都会关闭连接。

    4.4 TCP中的长短连接

    TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

    • 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。

    也就是说,短连接只能一次收发数据。

    • 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。

    也就是说,长连接可以多次收发数据。

    五:Socket编程注意事项

    在这里插入图片描述

    在这里插入图片描述

    1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机。
    2. 注意目的IP和目的端口号

    5.1端口被占用的问题

    如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:

    在这里插入图片描述

    此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:

    • 在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid。如以下命令显示了8888进程的pid

    在这里插入图片描述

    • 在任务管理器中,通过pid查找进程
      在这里插入图片描述

    解决端口被占用的问题:

    • 如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B
    • 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。

    六:再谈协议

    以上我们实现的UDP和TCP数据传输,除了UDP和TCP协议外,程序还存在应用层自定义协议,可以想想分别都是什么样的协议格式。

    对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:

    • 客户端发送请求和服务端解析请求要使用相同的数据格式。
    • 服务端返回响应和客户端解析响应也要使用相同的数据格式。
    • 请求格式和响应格式可以相同,也可以不同。

    约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。

    可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。

    5.1 封装/分用 vs 序列化/反序列化

    对发送数据时的数据包装动作来说:

    • 如果是使用知名协议,这个动作也称为封装
    • 如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。

    接收数据时的数据解析动作来说:

    • 如果是使用知名协议,这个动作也称为分用
    • 如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象
  • 相关阅读:
    @ApiImplicitParams这个注解的作用
    Tuskr 用例平台 快捷操作栏(基于 油猴)
    算法查找——二分查找
    僵尸进程的处理方法
    2024年全国青少信息素养大赛python编程复赛集训第十二天编程题分享
    计算机中的一些基本概念(速度,比特,门,电路图)
    实现真正的高性能高并发的上亿级别秒杀系统!
    DBT 项目建立
    软件测试——用例篇
    谷粒学院16万字笔记+1600张配图(五)——讲师管理前端
  • 原文地址:https://blog.csdn.net/weixin_73232539/article/details/134020374