计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现

Socket 翻译过来中文含义有(电源)插座;(电器)插口, 插孔等含义, 也就是说在电源领域, 我们将 Socket 称之为插座, 在电器领域, 我们将 Socket 称之为插孔, 那我们是不是就可以理解为在计算机通信领域, 担任插座或者插孔这个含义的 Socket 就定义成了套接字呢?

Socket 套接字

  • 1 概念及分类
  • 2 常用套接字通信模型
    • 2.1 流套接字通信流程
    • 2.2 数据报套接字通信流程
  • 3 常用套接字编程
    • 3.1 TCP 流套接字编程
    • 3.2 UDP 数据报套接字编程
  • 4 关于长短连接的理解
    • 4.1 概念
    • 4.2 区别
  • 5 关于自定义协议的理解
  • 6 补充缓冲区的理解
  • 7 关于 TCP 和 UDP 的经典问题

1 概念及分类

  理论上 Socket 套接字指的是由系统提供的用于网络通信的一种技术, 是基于 TCP / IP 协议的网络通信的基本单元, 也就是说基于 Socket 套接字的网络程序的开发就是网络编程; 看着这段话比较绕, 其实总结一句话: Socket 套接字就是为了实现网络编程的一组 API, 并遵循一定的约定和规则.
关于 Socket 的分类主要有三种, 这三种也是主要针对传输层协议划分的;

  • 流套接字: 使用的是传输层 TCP 协议, TCP 即传输控制协议, 对于字节流来说, 可以理解为传输的数据是基于 IO 流, 其主要特征就是在 IO 流没有关闭的情况下是无边界的数据, 可以多次发送, 也可以分开多次接收;
  • TCP 特性: 有连接 / 可靠传输 / 面向字节流 / 有接收缓冲区 / 有发送缓冲区 / 大小不限;

  • 数据报套接字: 使用传输层 UDP 协议, UDP 即用户数据报协议, 对于数据报而言, 可以简单的理解为传输的数据是一块一块的, 例如一块数据为 100 个字节, 那么发送的这一块数据就是 100 个字节, 并且必须一次发送, 接受也必须一次接收这 100 个字节的数据, 这时候就不能分多次发送或者接收;
  • UDP 特性: 无连接 / 不可靠传输 / 面向数据报 / 有接收缓冲区 / 无发送缓冲区 / 大小受限(一次最多传输 64 k);

  • 原始套接字: 原始套接字使用的便是自定义传输层协议, 主要用于读写内核没有处理的 IP 协议数据, 简单了解即可.
    另外, Java 标准库中主要提供的也是流套接字和数据报套接字, 毕竟 UDP 和 TCP 本身就是传输层中两个最重要的协议.

2 常用套接字通信模型

2.1 流套接字通信流程

在这里插入图片描述

2.2 数据报套接字通信流程

对于 UDP 协议来说, 具有无连接, 面向数据报的特征, 因此每次都是没有建立连接, 并且一次性发送全部数据报, 一次性接收全部的数据报; java 中使用 UDP 协议通信主要基于 DatagramSocket 类来创建数据报套接字, 并使用 DatagramPacket 作为发送或接收的 UDP 数据报, 对于一个服务端而言, 重要的是提供多个客户端的请求处理及相应, 主要流程如下:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第1张图片

3 常用套接字编程

3.1 TCP 流套接字编程

这里我们简单写一个回显服务器当做 来解读流套接字编程用到的方法和逻辑, ServletSocket 是创建 TCP 服务端 Socket 的 API, 主要处理客户端的连接, Socket 主要用来和客户端进行具体的交互, 这里还需要注意 TCP 协议有连接, 类似于打电话; 此处我们实现的是一个长连接版本的服务器, 关于长连接和短连接的区别, 后面会解释.
服务端代码如下:

public class TcpEchoServer {
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void star() throws IOException {
        System.out.println("服务器启动啦!");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
            while (true) {
                String request = bufferedReader.readLine();
                bufferedWriter.write(reponse + "\n");
                bufferedWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request,reponse);
            }
        } catch (IOException e) {
            System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
                    clientSocket.getPort());
        }
    }

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

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.star();
    }
}

代码流程解读:
在这里插入图片描述
客户端代码如下:

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient (String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }
    
    public void start() {
        System.out.println("客户端启动了!");
        Scanner scanner = new Scanner(System.in);
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){
            while (true) {
                System.out.println("--> ");
                String request = scanner.nextLine();
                if("exit".equals(request)) {
                    break;
                }
                bufferedWriter.write(request + "\n");
                bufferedWriter.flush();
                String response = bufferedReader.readLine();
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

代码流程解读:
在这里插入图片描述
运行结果:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第2张图片
注意:

  • TCP 的连接管理是由操作系统内核来管理的, 整体思路就是先描述, 再组织; 描述的主要是通信中的五元组 / 协议类型 / 源 IP 端口 / 目的 IP 端口等, 组织主要是使用一个阻塞队列来组织若干个连接对象;
  • 客户端和服务器建立连接的过程, 完全由内核来进行负责, 应用程序的代码是感知不到的, 当建立成功之后, 内核已经把这个连接对象放到了阻塞队列中了;
  • 代码中的 accept 就是从阻塞队列中取出一个连接对象, 在应用程序中的化身就是 Socket 对象;
  • 如果服务器启动之后么, 没有客户端建立连接, 此时代码中调用 accept 就会阻塞, 阻塞到真正有客户端建立连接为止;
  • 其实这就是一个生产者消费者模型, 后续针对数据的读写都是针对 clientSocket 这个对象进行展开的.

3.2 UDP 数据报套接字编程

  UDP 套接字编程相比于 TCP 编程最大的区别在于发送和接收 UDP 数据报用到的方法是 DatagramSocket(), 其方法和使用我们用例子来解释; 这里我们还是写一个简单回显功能的服务器当做 , 客户端发送什么, 服务端就显示什么.
服务器端代码如下:

public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),
                    0, requestPacket.getLength()).trim();
            String response = process(request);
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length,requestPacket.getSocketAddress());
            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 {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

代码流程解读:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第3张图片
客户端代码:

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

    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while(true) {
            System.out.print("-->");
            String request = scanner.nextLine();
            if(request.equals("exit" +
                    "")) {
                break;
            }
            
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);

            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096 );
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());

            System.out.println(response);
        }
    }

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

代码流程解读:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第4张图片运行结果:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第5张图片

整体逻辑:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第6张图片
注意:

  • DatagramPacket 是 UDP socket 发送和接收数据的基本单位, socket 对象本质上是一个文件, 这个文件是网卡的抽象, 可以理解为 “一切皆文件”;
  • 再次强调 IP 是用来决定互联网上的某个主机的位置, port 是决定数据交给这个主机上的哪个程序, 这里的关系不是 “一一对应” 的关系, 只是要配合使用, 同一个 IP 上面的 n 个服务器程序就有 n 个端口号;
  • 程序启动之后, 马上就能执行到 receive 操作, 服务器启动之后, 客户端并没有接着发送请求, 客户端什么时候发送请求并不能确定, 大概率的情况是调用 receive 的时候客户端还没发任何数据, 此时的 receive 操作就会阻塞, 一直阻塞到真的有数据过来为止, 此处的阻塞时间完全不可预期; 当真的有客户端传过数据来的时候, 此时的 receive 就会把收到的数据放到 DatagramPacket 对象的缓冲区中.

4 关于长短连接的理解

4.1 概念

  • 短连接: 一个连接中, 客户端和服务器之间只交互一次, 交互完毕就断开连接; 每次接收到数据并返回响应后, 都关闭连接;
  • 长连接: 一个连接中, 客户端和服务器之间交互 n 次, 直到满足一定的条件才断开, 因此长连接的效率会更高, 会避免反复建立连接和断开连接的过程; 总之, 不关闭连接, 一直保持连接状态, 双方不停的收发数据.

4.2 区别

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

5 关于自定义协议的理解

其实上面 TCP 的演示就用到了自定义协议, 当客户端发送请求的时候我们加上了 “\n”, 然后又在服务器读取的时候约定了 readLine(), 这里的按行写和按行读这样的过程就是一种最简单的自定义协议, 当然我们也可以约定为其他形式. 如下所示:
计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第7张图片

6 补充缓冲区的理解

 设想一下, 如果一次往 IO 设备中写一个字节, 分 100 次写的效率远远低于一次往 IO 设备中写 100 个字节分一次性写完的效率, 这样算的话效率差距就近乎 100 倍; 因此如果让写操作先把数据写到内存中, 然后当内存中数据达到一定程度时, 再统一写入到 IO 设备中;
操作 IO 设备程序的效率很大程度上取决于程序真正访问 IO 设备的次数, 因此缓冲区存在的真正意义就是为了减少访问 IO 设备的次数.

计算机网络(第五弹) --- Socket 套接字详细介绍及套接字通信流程和编程的具体实现_第8张图片

7 关于 TCP 和 UDP 的经典问题


(1) 如何基于 UDP 协议实现可靠传输???

虽然问的是 UDP, 但实际操作起来还是 TCP 相关知识!!!

  • 实现确认应答机制, 把每个数据接收到之后都要反馈一个 ACK, 这里不是内核返回的, 而是应用程序自定义一个 ack 包发送回去;
  • 设立实现序号和确认序号, 实现去重;
  • 实现超时重传;
  • 实现连接管理, 三次握手建立连接和四次挥手断开连接;
  • 为了提高效率可以实现滑动窗口, 并且还要限制滑动窗口, 实现流量控制和拥塞控制.

(2) 什么样的场景适合使用 TCP, 什么样的场景适合 UDP???

  • 如果需要可靠性肯定首选 TCP;
  • 如果传输的单个数据报比较长还是需要使用 TCP;
  • 如果特别注重效率优先考虑 UDP, 如机房内部的主机之间的通信, 往往传输的数据量比较大并且带宽充裕, 丢包的概率不大, 尤其是在当下微服务环境下特别需要 UDP;
  • 如果需要广播优先考虑 UDP; 一份数据可以同时发给多个主机, UDP 自身是支持广播的, 而 TCP 自身并不支持广播.

(3) 像 CF, LOL, 王者荣耀这一类游戏传输的时候优先使用 TCP 还是 UDP??

很有可能既不是 TCP 也不是 UDP; 因为传输层的协议不只是只有这两种协议, 还有很多的传输层协议, 有的协议就可以尽可能的兼顾到可靠性和效率.

你可能感兴趣的:(计算机网络,Java,计算机网络,网络)