【JAVA基础之网络编程】UDP和TCP协议以及三次握手和四次挥手的过程

🔥作者主页小林同学的学习笔录

🔥mysql专栏小林同学的专栏

目录

[1. 网络编程](#1. 网络编程)

[1.1 概述](#1.1 概述)

[1.2 网络编程的三要素](#1.2 网络编程的三要素)

[1.2.1 IP地址](#1.2.1 IP地址)

[1.2.2 InetAddress](#1.2.2 InetAddress)

[1.2.3 端口和协议](#1.2.3 端口和协议)

[1.3 UDP协议](#1.3 UDP协议)

[1.3.1 UDP发送数据](#1.3.1 UDP发送数据)

[1.3.2 UDP接收数据](#1.3.2 UDP接收数据)

[1.4 TCP协议](#1.4 TCP协议)

[1.4.1 TCP协议实例](#1.4.1 TCP协议实例)

[1.4.2 三次握手,四次挥手](#1.4.2 三次握手,四次挥手)


1. 网络编程

1.1 概述

概述:在网络通信协议下,不同计算机上运行的程序,进行数据的传输

java.net包中可以看见常见的网络应用程序API

常见的软件架构:

C/S:Client / Server 客户端 / 服务器

  • 需要用户下载并安装客户端程序,在远程有一个服务器程序
  • 优缺点:
    • 画面可以做的比较精美,用户体验好(不需要网络传输,数据来源于安装包)
    • 需要开发客户端,也需要开发服务端
    • 用户需要下载和更新的时候太麻烦

B/S:Browser / Server 浏览器 / 服务器

  • 只需要一个浏览器,通过访问不同的网址实现操作程序,客户访问不同的服务器
  • 优缺点:
    • 不需要开发客户端,只需要页面 + 服务器
    • 用户不需要下载,打开浏览器就能使用
    • 如果应用过大,用户体验将受到影响(因为数据进行网络传输效率比较低)

1.2 网络编程的三要素

IP:设备在网络中的地址,是唯一标识

端口号:应用程序在设备中的唯一标识

协议:数据在网络中的传输规则,常见的协议有UDP,TCP,HTTP,HTTPS,FTP

1.2.1 IP地址

IP地址分为两大类:

  • IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是"11000000 10101000 00000001 01000010",这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号"."分隔不同的字节。于是,上面的IP地址可以表示为"192.168.1.66"。IP地址的这种表示法叫做"点分十进制表示法",这显然比1和0容易记忆得多,最多有2^32次方个IP

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,每一组用分号隔开,这样就解决了网络地址资源数量不够的问题,最多有2^128次方个IP、

DOS常用命令:

  • ipconfig:查看本机IP地址

  • ping IP地址:检查网络与目标主机是否能连通

特殊IP地址:

  • 127.0.0.1:是回送地址,可以代表本机地址LocalHost,一般用来测试使用

疑问:假设192.168.1.100是我的电脑IP,那么这个IP跟127.0.0.1是一样吗?

不一样,192.168.1.100和127.0.0.1不是一样的IP地址。192.168.1.100是局域网内的私有IP地址,用于在局域网中标识设备。而127.0.0.1是本地回环地址,用于在同一台设备内部进行通信。当你的计算机尝试连接127.0.0.1时,它实际上是在尝试与自己通信,而不是与网络上的其他设备通信。
疑问:公网地址(万维网使用)和私有地址(局域网使用)的区别?

公网地址和私有地址之间的主要区别在于它们的可访问性和范围。公网地址是全球唯一的IP地址,用于在互联网上唯一标识设备和进行通信。私有地址则是在局域网内使用的地址,不会在互联网上进行路由,因此不能直接从互联网上访问。私有地址用于在局域网内部进行通信,而通过路由器进行网络地址转换(NAT),可以允许多个设备共享单个公网IP地址来访问互联网。

1.2.2 InetAddress

InetAddress 是 Java 编程语言中用于表示 IP 地址的类。它提供了一种将 IP 地址和主机名相互转换的方式。通过 InetAddress 类,可以实现网络通信中的主机名解析、IP 地址解析等功能。

成员方法:

1.2.3 端口和协议

  • 端口

    • 设备上应用程序的唯一标识
  • 端口号

    • 用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
  • 协议

    • 计算机网络中,连接和通信的规则被称为网络通信协议
  • UDP协议

    • 用户数据报协议(User Datagram Protocol)

    • UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

    • 由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输

    • 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据

    • 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议

  • TCP协议

    • 传输控制协议 (Transmission Control Protocol)

    • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过"三次握手"

    • 速度慢,没有大小限制,数据安全

    • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠

      第一次握手,客户端向服务器端发出连接请求,等待服务器确认

      第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求

      第三次握手,客户端再次向服务器端发送确认信息,确认连接

    • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

1.3 UDP协议

1.3.1 UDP发送数据

构造方法:

成员方法:

发送数据的步骤

  • 创建发送端的Socket对象(DatagramSocket)

  • 创建数据,并把数据打包(DatagramPacket)

  • 调用DatagramSocket对象的方法发送数据(send)

  • 关闭发送端

代码演示:

java 复制代码
public class Send {
    public static void main(String[] args) throws IOException {
        /**
         * 创建发送端的Socket对象(DatagramSocket)
         * 创建数据,并把数据打包(DatagramPacket)
         * 调用DatagramSocket对象的方法发送数据(send)
         * 关闭发送端
         */
        //这里参数如果没有端口号,系统会自动分配一个端口号
        DatagramSocket socket = new DatagramSocket(10000);

        String str = "龙颜大怒666";
        byte[] bytes = new byte[1024];
        bytes = str.getBytes();

        DatagramPacket packet = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("127.0.0.1"),10086);
        socket.send(packet);

        socket.close();
    }
}

1.3.2 UDP接收数据

构造方法:

成员方法:

接收数据的步骤

  • 创建接收端的Socket对象(DatagramSocket)

  • 创建一个数据包,用于接收数据(DatagramPacket)

  • 调用DatagramSocket对象的方法接收数据(receive)

  • 解析数据包,并把数据在控制台显示

  • 关闭接收端

代码演示:

java 复制代码
public class Receive {
    public static void main(String[] args) throws IOException {
        /**
         * 创建接收端的Socket对象(DatagramSocket)
         * 创建一个数据包,用于接收数据(DatagramPacket)
         * 调用DatagramSocket对象的方法接收数据(receive)
         * 解析数据包,并把数据在控制台显示
         * 关闭接收端
         */

        //注意:这里的端口号一定要与发送端中的数据包端口一致,不然会收不到数据
        DatagramSocket socket = new DatagramSocket(10086);

        byte[] bytes = new byte[1024];
        DatagramPacket packet = new DatagramPacket(bytes,bytes.length);

        //这个方法会一直等待发送端发送信息过来,直到拿到数据才会取消阻塞
        socket.receive(packet);

        byte[] data = packet.getData();
        InetAddress address = packet.getAddress();
        int port = packet.getPort();
        System.out.println("数据:" + new String(data,0,packet.getLength()) + " 主机IP:" + address + " 端口号:" + port);

        socket.close();
    }
}

输出结果:

数据:龙颜大怒666 主机IP:/127.0.0.1 端口号:10000

1.4 TCP协议

1.4.1 TCP协议实例

代码演示:

java 复制代码
public class Client {
    public static void main(String[] args) throws IOException {
        //创建socket对象
        //细节:在创建对象同时会连接服务端
        //如果连接不上代码会报错
        Socket socket = new Socket("127.0.0.1",10000);
        //创建socket的输出流通道
        OutputStream outputStream = socket.getOutputStream();
        //写入数据
        outputStream.write("你好呀".getBytes());
        //关闭资源
        socket.close();
        outputStream.close();
    }
}
java 复制代码
public class Server {
    public static void main(String[] args) throws IOException {
        //这里的端口号要跟客户端的Socket保持一致
        ServerSocket serverSocket = new ServerSocket(10000);
        //这里会阻塞等待客户端发送信息
        Socket socKet = serverSocket.accept();
        //通过socket获取输入流通道
        InputStream inputStream = socKet.getInputStream();
        //解决中文乱码问题
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        int len;
        while ((len = bufferedReader.read()) != -1) {
            System.out.print((char) len);
        }
        //关闭资源
        serverSocket.close();
        socKet.close();
    }
}

1.4.2 三次握手,四次挥手

三次握手:为了确保连接的建立

四次挥手:确保连接断开,且数据处理完毕

1.5 综合练习

1.5.1 多发多送

java 复制代码
public class Test01 {
    public static void main(String[] args) throws IOException {
        /**
         * 客户端:多次发送数据
         * 服务端:接收多次数据,并打印
         */
        Socket socket = new Socket("127.0.0.1",10002);

        OutputStream outputStream = socket.getOutputStream();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("请输入你要发送的信息:");
            String str = scanner.nextLine();
            //用户输入886表示退出
            if("886".equals(str)){
                break;
            }
            outputStream.write(str.getBytes());
        }
        //关闭资源
        outputStream.close();
        socket.close();
    }
}




public class TestServer01 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10002);

        Socket socket = serverSocket.accept();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        int len;
        while((len = bufferedReader.read()) != -1){
            System.out.print((char)len);
        }
        //关闭资源
        socket.close();
        serverSocket.close();
    }
}

1.5.2 接收并反馈

java 复制代码
public class Test02 {
    public static void main(String[] args) throws IOException {
        /**
         * 客户端:发送一条数据,接收服务端反馈的消息并打印
         * 服务端:接收数据并打印,再给客户端反馈信息
         */
        Socket socket = new Socket(InetAddress.getLocalHost(),10003);
        Scanner scanner = new Scanner(System.in);
        OutputStream os = socket.getOutputStream();

        System.out.println("请输入要发给服务端的信息:");
        String str = scanner.nextLine();
        os.write(str.getBytes());

        //细节:这里需要一个结束标记,服务端那边读取才知道结束
        socket.shutdownOutput();

        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        int len;
        while ((len = isr.read()) != -1){
            System.out.print((char)len);
        }
        
        socket.close();
    }
}



public class TestServer02 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10003);

        Socket socket = serverSocket.accept();

        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        int len;
        //细节:
        //read方法从连接通道读取数据
        //但是需要一个结束标记循环才会停止
        //否则,程序就会一直停在read方法这里,等待读取下面的数据
        while ((len = isr.read()) != -1){
            System.out.print((char)len);
        }

        OutputStream os = socket.getOutputStream();
        os.write("收到客户端的信息".getBytes());

        socket.close();
        serverSocket.close();
    }
}

1.5.3 上传练习

java 复制代码
public class Test03 {
    public static void main(String[] args) throws IOException {
        /**
         * 案例需求:
         * 客户端:数据来自于本地文件,接收服务器反馈
         * 服务器:接收到的数据写入本地文件,给出反馈
         */
        Socket socket = new Socket("127.0.0.1", 10000);

        byte[] bytes = new byte[1024];
        int len;

        //这种方式效率比较低
        //FileInputStream fis = new FileInputStream("D:\\img\\0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg");
        //OutputStream os = socket.getOutputStream();

        //缓存流可以降低磁盘IO次数
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("source/0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

        //发送数据
        while ((len = bis.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //设置结束标记
        socket.shutdownOutput();

        //接收数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        while ((len = isr.read()) != -1){
            System.out.print((char)len);
        }

        socket.close();
    }
}




public class TestServer03 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10000);

        Socket socket = serverSocket.accept();
        InputStream is = socket.getInputStream();
        byte[] bytes = new byte[1024];
        int len;

        //效率低
        //FileOutputStream fos = new FileOutputStream(new File("D:\\test\\a.jpg"));


        //这里需要解决文件名重复问题,导致原先的文件会被后面的覆盖,用UUID来解决
        //System.out.println(UUID.randomUUID());
        //72e165ae-98ad-4cd4-80e9-c9f86b910461,我们一般看到的效果是没有"-"的,需要处理一下
        String name = UUID.randomUUID().toString().replace("-", "");
        //BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\a.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\" + name + ".jpg"));

        //边读边写
        while((len = is.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //发送数据
        OutputStream os = socket.getOutputStream();
        os.write("收到数据啦".getBytes());

        socket.close();
        serverSocket.close();
    }
}

1.5.4 服务器改写成多线程,以及线程优化

服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。

优化方案一:使用循环

弊端:第一个用户正在上传数据,第二个用户就来访问了,此时第二个用户是无法成功上传的。

所以,使用多线程改进

优化方案二:使用循环 + 多线程

每来一个用户,就开启多线程处理

java 复制代码
public class Test04 {
    public static void main(String[] args) throws IOException {
        /**
         * 案例需求:
         * 客户端:数据来自于本地文件,接收服务器反馈
         * 服务器:接收到的数据写入本地文件,给出反馈
         */
        Socket socket = new Socket("127.0.0.1", 10000);

        byte[] bytes = new byte[1024];
        int len;

        //这种方式效率比较低
        //FileInputStream fis = new FileInputStream("D:\\img\\0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg");
        //OutputStream os = socket.getOutputStream();

        //缓存流可以降低磁盘IO次数
        BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("source/0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

        //发送数据
        while ((len = bis.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //设置结束标记
        socket.shutdownOutput();

        //接收数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        while ((len = isr.read()) != -1){
            System.out.print((char)len);
        }

        socket.close();
    }
}



public class TestServer04 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10000);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                3,//核心线程数量
                16,//线程池总大小
                60,//空闲时间
                TimeUnit.SECONDS,//空闲时间(单位)
                new ArrayBlockingQueue<>(2),//队列
                Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象
                new ThreadPoolExecutor.AbortPolicy()//阻塞队列
        );

        while (true) {
            //等待客户端连接
            Socket socket = serverSocket.accept();

            //一个用户对应一条线程
            //new Thread(new MyRunnable(socket)).start();

            //线程池进行优化
            poolExecutor.submit(new MyRunnable(socket));
        }
    }
}



public class MyRunnable implements Runnable {
    private Socket socket;

    public MyRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len;

            //效率低
            //FileOutputStream fos = new FileOutputStream(new File("D:\\test\\a.jpg"));


            //这里需要解决文件名重复问题,导致原先的文件会被后面的覆盖,用UUID来解决
            //System.out.println(UUID.randomUUID());
            //72e165ae-98ad-4cd4-80e9-c9f86b910461,我们一般看到的效果是没有"-"的,需要处理一下
            String name = UUID.randomUUID().toString().replace("-", "");
            //BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\a.jpg"));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("img\\" + name + ".jpg"));

            //边读边写
            while ((len = is.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }

            //发送数据
            OutputStream os = socket.getOutputStream();
            os.write("上传成功".getBytes());

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
相关推荐
测开小菜鸟几秒前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
萧鼎1 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸1 小时前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农1 小时前
Python 继承、多态、封装、抽象
开发语言·python
^velpro^1 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋31 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花1 小时前
【JAVA基础】Java集合基础
java·开发语言·windows