• Java网络编程(二)Socket 套接字(TCP和UDP),以及TCP的回显


    Socket 套接字

    我们软件工作者,着重编写的是应用层的代码,但是发送这个数据,我们就需要将应用层传输到传输层,也就意味着我们需要调用应用层的API,统称为 Socket API。

    套接字的分类:

    1. 流套接字:使用传输层TCP协议
      特点:
      • 有连接:使用 TCP 通信的双方,需要时刻保存对方的相关消息
      • 可靠传输:尽可能的将数据传输过去,如果没有传输过去,自己也知道没有传输,然后通过设定可以重新传输
      • 面向字节流:以字节为传输的基本单位,读写方式更为灵活
      • 全双工:一条路径,双向通信
    2. 数据报套接字:使用传输层UDP协议
      特点:
      • 有连接:使用 UDP 通信的双方,不需要时刻保存对方的相关消息
      • 不可靠传输:只关注是否传输了数据,至于是否传输成功,并不专注
      • 面向数据报:以一个UDP数据报为基本单位
      • 全双工:一条路径,双向通信
    3. 原始套接字:原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
      所有特点自己定义

    什么是全双工和半双工?

    全双工:一条路径,双向通信
    半双工:一条路径,单向通信

    网络传输数据的基本单位:报(Datagram)、包(Packet)、段(Segment)、帧(Frame)
    Socket 对象,相当于系统中Socket文件,这个文件并非对应到硬盘上的某个数据存储区域,而是对应到网卡这个硬件设备

    • 往这个Socket·对象中写数据,相当于通过网卡发送消息
    • 从这个Socket·对象中读数据,相当于通过网卡接收消息

    这个图不是我画,摘抄了网上现有的)
    在这里插入图片描述

    数据报套接字UDP

    java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报 。DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。

    DatagramSocket API:

    1. DatagramSocket 构造方法

    在这里插入图片描述

    1. DatagramSocket 方法:
      在这里插入图片描述

    DatagramPacket API:(DatagramPacket是UDP Socket发送和接收的数据报)

    1. DatagramPacket 构造方法:
      在这里插入图片描述

    2. DatagramPacket 方法:
      在这里插入图片描述
      构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创

    InetSocketAddress API:

    1. InetSocketAddress ( SocketAddress 的子类 )构造方法:
      在这里插入图片描述

    UDP服务器:

    注意:

    • 服务器的端口必须不变,客户端这边则不需要手动指定,系统自动分配
    • socket是文件,也需要关闭
    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

    一旦服务器一起动,调用start方法,就会立即执行到,receive这里,但是如果此时还有没有客户端发来的数据,receive就会阻塞等待,一直持续到有数据发过来。
    细节:网卡这里收到数据,就会进行分用,解析UDP这一层 看到端口号,然后将数据放入接收缓冲区,然后将数据到了参数中的DatagramSocket 对象中

    UDP客户端:

    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

    在这里插入图片描述

    TCP流套接字编程

    ServerSocket API:

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

    1. ServerSocket 构造方法:
      在这里插入图片描述
    2. ServerSocket 方法:
      在这里插入图片描述
      accept:意思就是接受,本质上是三次握手后面的文章会说。

    Socket API:

    Socket 是客户端 Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
    不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

    1. Socket 构造方法:
      在这里插入图片描述
    • host 表示服务器的 IP 地址
    • port 表示服务器的端口
    1. Socket 方法:
      在这里插入图片描述
    • 从InputStream这里读数据,就相当于从网卡接收
    • 往OutputStream这里写数据,就相当于从网卡发送

    TCP的长短连接

    TCP发送数据时,需要先建立连接,而什么时候关闭连接就决定是短连接还是长连接。
    短连接:每次接收数据并返回响应后,都关闭连接。也就是说,短连接只能一次收发。

    • 连接客户端和服务器
    • 对于客户端来说。要发送一个请求,然后接收一个响应
    • 对于服务器来说。会收到一个请求,然后返回一个响应
    • 然后关闭连接

    长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以
    多次收发数据

    • 连接客户端和服务器
    • 可以客户端一直发送请求,并获取服务器的响应
    • 可以服务器一直发送请求,并获取客户端的响应
    • 没有一方主动停止,不关闭

    长连接和短连接的区别:

    • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
    • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
    • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

    实现一个简单回显服务器

    public class TcpEchoServer {
        //serverSocket 就是外场拉客的小哥(类似集合),只有一个
    
        //clientSocket 内场服务的人(),会给每个客服分配一个
        private ServerSocket serverSocket=null;
        //1
    
        public TcpEchoServer(int port) throws IOException {
            serverSocket=new ServerSocket(port);
        }
        public void start() throws IOException {
            ExecutorService executorService= Executors.newCachedThreadPool();
            System.out.println("服务器启动");
            while (true){
                Socket clientSocket=serverSocket.accept();
                //如果直接调用,该方法会影响这个循环的二次执行.导致accept不及时了
                //创建新的线程,用新的线程来调用processConnetion
                //每次来一个新的客户端都搞一个新的线程即可
    /*            Thread t=new Thread(()->{
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
                t.start();*/
                //创建一个线程池,从池子中拿取线程
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            processConnection(clientSocket);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }
        }
        //通过这个方法处理一个链接
        //读取请求
        //根据请求计算响应
        //把响应返回给客户端
        private void processConnection(Socket clientSocket) throws IOException {
            System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
            try(InputStream inputStream=clientSocket.getInputStream();
                OutputStream outputStream=clientSocket.getOutputStream()) {
                //没有这两个也可以,但是代价就是得一个字节一个字节的处理,找到那个是结束符
                //将字节流包装成了字符流
                Scanner scanner=new Scanner(inputStream);
                PrintWriter printWriter=new PrintWriter(outputStream);
                while (true){
                    //3
                    //读取请求
                    if (!scanner.hasNext()){
                        //读取的流到了结尾了
                        System.out.printf("[%s:%d] 客户端下线",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //直接使用scanner读取一段字符串
                    String request=scanner.next();//往后读,一直读到空白符,空格,换行,翻页符....都算空白符
    
                    //5
                    //根据请求计算响应
                    String response=process(request);
                    //把响应返回给客户端
                    printWriter.println(response);
                    //刷新缓冲区
                    printWriter.flush();
                    System.out.printf("[%s:%d] req:%s; resp:%s\n",clientSocket.getInetAddress().toString()
                    ,clientSocket.getPort(),request,response);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }finally {
                clientSocket.close();
            }
        }
        private String process(String request) {
            return request;
        }
    
        public static void main(String[] args) throws IOException {
            TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
            tcpEchoServer.start();
        }
    
    }
    
    • 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
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    在这里插入图片描述
    补充一点:

    硬件的读写速度:

    • 内存 > 硬盘 > 网卡

    读写硬盘和网卡口可以视为 IO 操作。

    • printWriter.println(response);----》写网卡

    因为网卡读写速度慢,如果平凡的写入,读出对于效率太慢了。为了提高IO操作的效率,此时就需要引入一个内存构成的缓冲区。等缓冲区达到一定数量,就统一写入网卡中。

    缓存(cache)!=缓冲区(buffer)

    • 缓存:只能读
    • 缓冲区:可以读也可以写。
  • 相关阅读:
    php+mysql幼儿园早教网站
    在Windows11上安装ubuntu虚拟机
    将 Spring Boot 应用程序部署为 WAR
    紧固螺栓的常见类型有哪些?
    Redis 先写数据库及先写缓存一致性问题
    Tiktok shop api 调试
    『现学现忘』Docker基础 — 22、使用Docker安装Nginx
    【单片机】16-LCD1602和12864和LCD9648显示器
    SCHP(CVPR2019)-人体解析论文阅读
    GPT问答SAP BW
  • 原文地址:https://blog.csdn.net/Cheer_RIO/article/details/132698377