Socket 原理和思考

news2025/2/27 21:04:49
众所周知Reactor是一种非常重要和应用广泛的网络编程模式,而Java NIO是Reactor模式的一个具体实现,在Netty和Redis都有对其的运用。而不管上层模式如何,底层都是走的Socket,对底层原理的了解会反哺于上层,避免空中楼阁现象。
所以本文对Socket原理及其中值得关注的点作再次梳理,最终目标还是为了理解Reactor及NIO。

Socket简介


  • Socket用于网络进程间通信,当然单机上不同进程间也行。
  • Socket位于五层网络模型中的应用层和传输层之间,是一种抽象层,也是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,应用层也能更便捷在网络间进行数据传输。
本文对Socket基础原理不做过多介绍,可以参考: https://blog.csdn.net/qq_39208536/article/details/137589718icon-default.png?t=N7T8https://blog.csdn.net/qq_39208536/article/details/137589718

传统Socket编程


虽然现在几乎不用再涉及原生Socket编程,但这些代码对理解原理还是有用的。
Server端:
public class MySocketServer {
    private static ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) throws IOException, InterruptedException {
        //服务端的主线程是用来循环监听客户端请求
        ServerSocket server = new ServerSocket(8686);
        //创建一个服务端且端口为8686
        Socket client = null;
        System.out.println("服务端启动");

        //循环监听
        while (true) {
            //服务端监听到一个客户端请求
            System.out.println("阻塞等待accept....");
            client = server.accept();
            System.out.println(client.getRemoteSocketAddress() + "地址的客户端连接成功!");
            //将该客户端请求通过线程池放入HandlMsg线程中进行处理
            executorService.submit(new HandleMsg(client));
        }
    }

    public static void handle(Socket client) {
        //创建字符缓存输入流
        BufferedReader bufferedReader = null;
        //创建字符写入流
        PrintWriter printWriter = null;
        try {
            //获取客户端的输入流
            bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            //获取客户端的输出流,true是随时刷新
            printWriter = new PrintWriter(client.getOutputStream(), true);
            String inputLine = null;
            long a = System.currentTimeMillis();
            Thread.sleep(1000);

            while ((inputLine = bufferedReader.readLine()) != null) {
                printWriter.println("hello " + inputLine);
            }
            long b = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "线程结束,花费了:" + (b - a) + "ms");
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                bufferedReader.close();
                printWriter.close();
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //一旦有新的客户端请求,创建这个线程进行处理
    private static class HandleMsg implements Runnable {
        //创建一个客户端
        Socket client;
        public HandleMsg(Socket client) {
            this.client = client;
        }
        @Override
        public void run() {
            handle(client);
        }
    }
}
Client端:
public class MySocketClient {
    public static void main(String[] args) throws IOException {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    callServer();
                }
            }).start();
        }
    }

    public static void callServer() throws IOException {
        Socket client = null;
        PrintWriter printWriter = null;
        BufferedReader bufferedReader = null;
        try {
            client = new Socket();
            // 连接超时
            client.connect(new InetSocketAddress("localhost", 8686), 100);
            // 读写超时
//            client.setSoTimeout(10);

            printWriter = new PrintWriter(client.getOutputStream(), true);
            printWriter.println(Thread.currentThread().getName());
            printWriter.flush();

            System.out.println(Thread.currentThread().getName() + " " + "等待服务端消息...");
            bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));            //读取服务器返回的信息并进行输出
            System.out.println(Thread.currentThread().getName() + " " + "来自服务器的信息是:" + bufferedReader.readLine());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            printWriter.close();
            bufferedReader.close();
            client.close();
        }
    }
}

创建Socket的时候操作系统创建了什么


  • Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作,Socket就是该模式的一个实现。Socket即是一种特殊的文件,一些Socket函数就是对其进行的操作。
  • 客户端或服务端Socket创建后,操作系统为其会分配:
    • 文件描述符(区别文件句柄),用于操作Socket,参考:https://blog.csdn.net/tjcwt2011/article/details/122685933 https://zhuanlan.zhihu.com/p/364617329icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/364617329 https://blog.csdn.net/tjcwt2011/article/details/122685933
    • 位于内核的发送缓冲区、接收缓冲区
    • 其他数据结构,暂不讨论。

Socket传输数据经历的过程


网卡也是有缓冲区的,暂不讨论。

阻塞、非阻塞与同步、异步的关系


看了很多文章对这两组概念解释和对比,说的太复杂了,其实没必要,两句话就能说清楚。
首先,对于读数据recv或read(写数据同理,没写出来),分两个阶段:
  1. 等待数据可读。
  2. 系统调用讲数据从内核拷贝到用户空间。
然后,再对比那两组概念:
  • 阻塞、非阻塞是对于等待数据可读、可写时,是否死等;
  • 同步、异步是对于数据在用户空间和内核传递时,是否等待其完成;
结合这四种LinuxIO模型对比(一般讨论LinuxIO模型会有五种,其中信号驱动IO用得太少,暂不讨论。
可以得出结论: 阻塞IO、非阻塞IO、多路复用都属于同步IO!区别于异步IO
注意:我们之前说的复习Socket还是为了进一步学习NIO和Reactor模式,这里有几点需要区分原生Socket和NIO
  • 原生Socket在创建的时候也可以指定为阻塞或非阻塞模式。原生非阻塞Socket编程较复杂,比如可能需要循环判断send和recv的数据量是否完整,故一般不会轻易挑战。
  • 原生Socket也是可以直接编程实现多路复用的,参考: SOCKET编程与复用 | YuYoung's Blog
  • NIO底层实现也是操作的原生Socket,可以看作是对以上两点的包装,使用NIO来操作非阻塞IO就方便多了。

发送缓冲区和接收缓冲区


 1,send在本质上并不是向网络上发送数据,而是将应用层发送缓冲区的数据 拷贝到内核缓冲区 中,至于数据什么时候会从网卡缓冲区中真正的发到网络中,要根据TCP/IP协议栈的行为来确定。recv在本质上并不是从网络上收取数据,而是将 内核缓冲区中的数据拷贝到 应用程序的缓冲区中,也就是说从网络接收数据时,TCP/IP协议栈会把数据收下来放在内核的接收缓冲区内。
2,如果接收缓冲区一直满着堆积,没有recv读取,网卡缓冲区也满,网络发过来的数据怎么存?
只有当接收网络报文的速度大于应用程序读取报文的速度时,可能使读缓存达到了上限,这时这个缓存使用上限才会起作用。所起作用为:丢弃掉新收到的报文,防止这个TCP连接消耗太多的服务器资源。同样,当应用程序发送报文的速度大于接收对方确认ACK报文的速度时,写缓存可能达到上限,从而使send方法阻塞或失败。
3,当待发送(拷贝)的数据的长度大于发送缓冲区的长度,是如何发送的?
一次send调用,但TCP/IP协议栈可能会分多帧发送,参考:https://blog.csdn.net/aflyeaglenku/article/details/73614292
4,recv和send不一定是一一对应的,也就是说并不是send一次,就一定recv一次就接收完,有可能send一次,recv多次才接收完,也有可能send多次,一次recv就接收完了。  

缓冲区可读、可写的判断条件


1,接收低水位和发送低水位

每个套接字有一个接收低水位和一个发送低水位。他们由select函数使用。

  • 接收低水位标记:让select返回“可读”时接收缓冲区中所需的数据量。对于TCP默认值为1。
  • 发送低水位标记:让select返回“可写”时发送缓冲区中所需的可用空间。对于TCP,其默认值常为2048。

2,引用《Unix网络编程》中的可读可写条件

当满足下列条件之一时,一个套接字准备好读:

  • 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于 0 的值(也就是返回准备好读入的数据)。我们可以使用 SO_RCVLOWAT 套接字选项设置该套接字的低水位标记。对于 TCP 和 UDP 套接字而言,其默认值为 1。
  • 该连接的读半部关闭(也就是接收了 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回 0 (也就是返回 EOF)。
  • 该套接字是一个监听套接字且已完成的连接数不为 0。对这样的套接字的 accept 通常不会阻塞。
  • 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回 -1(也就是返回一个错误),同时把 errno 设置成确切的错误条件。这些待处理错误也可以通过指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除。

当满足下列条件之一时,一个套接字准备好写:

  • 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且要求该套接字已连接(TCP)或者不需要连接(UDP)。这意味着如果我们把这样的套接字设置为非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接收的字节数)。我们可以使用 SO_SNDLOWAT 套接字选项来设置该套接字的低水位标记。对于 TCP 和 UDP 套接字而言,其默认值通常为 2048。
  • 该连接的写半部关闭,对这样的套接字的写操作将产生 SIGPIPE 信号。
  • 使用非阻塞式 connect 的套接字已建立连接,或者已经以失败告终。
  • 其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回 -1(也就是返回一个错误),同时把 errno 设置成确切的错误条件。这些待处理的错误也可以通过指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除。

当缓冲区满了时,发送或接收数据会怎样?


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1841447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

npm install 安装不成功,node-sass缺失,提示python环境缺失的解决办法

npm install 安装不成功的原因 是因为缺少python的环境 解决方法&#xff1a; 1、去官网下载 https://www.python.org/downloads/release&#xff0c;注意安装3.6版本以上还是会有问题&#xff0c;建议安装3.6版本以上的&#xff0c;我选择安装的是3.9.6&#xff0c;对应的下载…

Git pull下来时合并分支

执行git pull时报错如下 解决方法&#xff1a; VSCode git中手动选取分支&#xff0c;完成合并 这一步&#xff0c;也可以在README.md中进行Tutorial&#xff0c; 上面出现冲突后&#xff0c;README.md会被更改成下述形式&#xff0c; If you have questions, please <…

支持 MKV、MP4、AVI、MPG 等格式视频转码器

一、简介 1、一款开源的视频转码器&#xff0c;适用于 Linux、Mac 和 Windows。它是一个免费的工具&#xff0c;由志愿者们开发&#xff0c;可以将几乎所有格式的视频转换为现代、广泛支持的编码格式。你可以在官网上下载该应用或源代码。该软件支持 MKV、MP4、AVI、MPG 等格式…

[Linux] 系统管理

全局配置文件 用户个性化配置 配置文件的种类 alias命令和unalias命令 进程管理 进程表

SAP PP学习笔记22 - 生产订单(制造指图)的元素1

前面几章讲了PP 里面生产计划的各种策略以及策略的Customize。 SAP PP学习笔记20 - 复习总结一下MTS&#xff0c;MTO&#xff0c;ATO的各种生产策略-CSDN博客 SAP PP学习笔记21 - 计划策略的Customize&#xff1a;策略组 &#xff1e; 策略 &#xff1e; 需求类型 &#xff1…

windows系统中开发的GO程序生成docker镜像并部署到阿里云服务(linux系统)的操作说明

本文简述将go程序生成docker镜像的操作方法&#xff0c;以及如何部署到阿里云服务。其中go程序在windows系统中开发&#xff0c;阿里云服务的操作系统为linux&#xff08;centos7.9&#xff09;&#xff0c;以下为流程示意图&#xff1a; 一、window系统中开发go程序 程序实现…

利用JAVA语言调用GLM-4接口实战指南

一、什么是API接口 API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&#xff09;是一种软件接口&#xff0c;它定义了不同应用程序之间如何相互通信、交互。API接口分为很多种&#xff0c;常见的有Web API&#xff0c;数据库API&#xff0c;操…

windows文件及文件夹加密后无法解除加密

windows加密文件及文件夹的方法 window EFS加密 1、右键点击文件夹选择“属性”&#xff0c;在属性常规窗口中点击“高级”。 2、勾选“加密内容以便保护数据”&#xff0c;点击“确定”。 3、选择加密范围&#xff0c;点击“确定”即可加密。 使用这种方法加密之后文件夹将只…

赋能AI未来,景联文科技推出高质量亿级教育题库、多轮对话以及心理大模型数据

当前&#xff0c;大模型正如雨后春笋般不断涌现&#xff0c;不断推动着大模型产业的应用实践进入加速发展的新阶段。 景联文科技是AI数据服务公司&#xff0c;提供海量优质大模型数据集&#xff0c;涵盖文本、图像、视频、音频等多类型数据&#xff0c;致力于为不同训练阶段的算…

Stable Diffusion 3 大模型文生图实践

windows教程2024年最新Stable Diffusion本地化部署详细攻略&#xff0c;手把手教程&#xff08;建议收藏!!)_stable diffusion 本地部署-CSDN博客 linux本地安装教程 1.前期准备工作 1&#xff09;创建conda环境 conda create --name stable3 python3.10 2&#xff09;下…

【C++ | 友元(friend)】友元函数、友元类、友元成员函数详解及例子代码

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

「iOS」UI——无限轮播图实现与UIPageControl运用

「OC」UI 文章目录 「OC」UI无限轮播图的实现以及UIPageControl的实际运用明确要求简单滚动视图的实现UIPageControl的实现设置NSTimer实现自动移动补充实现 进行无限滚动视图的修改思路实现 完整代码展示 无限轮播图的实现以及UIPageControl的实际运用 明确要求 我们要实现一…

LabVIEW与数字孪生

LabVIEW与数字孪生技术在工业自动化、智慧城市、医疗设备和航空航天等领域应用广泛&#xff0c;具备实时数据监控、虚拟仿真和优化决策等特点。开发过程中需注意数据准确性、系统集成和网络安全问题&#xff0c;以确保数字孪生模型的可靠性和有效性。 经典应用&#xff1a;LabV…

算法人生(23):跟着“生成对抗网络”思维走出“拖延”

生成对抗网络&#xff08;GANs&#xff09;是一种深度学习模型&#xff0c;其核心思想是通过两个神经网络——生成器和判别器的对抗过程来学习数据分布&#xff0c;进而生成新的、类似真实数据的样本。它基本原理基于一个博弈论框架&#xff0c;其中生成器尝试生成尽可能逼真的…

Solkane 冷媒性能计算软件-管路计算

下载 制冷管道设计 制冷管路的压降会降低制冷量&#xff0c;增大功耗。但不同部分的管路允许的压降的数量级是不同的。 制冷管路的压降不是唯一的考虑因素&#xff0c;制冷剂的流速往往比压降更重要。 制冷系统中&#xff0c;压缩机、阀、汽液分离器或其他附件上的连接件的尺…

地下管线管网三维建模系统MagicPipe3D

地下管网是保障城市运行的基础设施和“生命线”。随着实景三维中国建设的推进&#xff0c;构建地下管网三维模型与地上融合的数字孪生场景&#xff0c;对于提升智慧城市管理至关重要&#xff01;针对现有三维管线建模数据差异大、建模交互弱、模型效果差、缺乏语义信息等缺陷&a…

swagger下载文件名中文乱码、swagger导出文件名乱码、swagger文件导出名称乱码、解决swagger中文下载乱码bug

文章目录 一、场景描述&#xff1a;swagger导出文件名称乱码二、乱码原因三、解决方法3.1、方法一、在浏览器中输入地址下载3.2、方法二、swagger升级为2.10.0及以上 四、可能遇到的问题4.1、DocumentationPluginsManager.java:152 一、场景描述&#xff1a;swagger导出文件名称…

时间复杂度的相关概念

1. 统计时间增长趋势 时间复杂度分析统计的不是算法运行时间&#xff0c;而是算法运行时间随着数据量变大时的增长趋势&#xff0c;也就是算法运行时间与输入数据的关系。 // 算法 A 的时间复杂度&#xff1a;常数阶 function algorithm_A(n) {console.log(0); } // 算法 B 的…

反激开关电源EMI电路选型及计算

EMI &#xff1a;开关电源对电网或者其他电子产品的干扰 EMI &#xff1a;传导与辐射 共模电感的滤波电路&#xff0c;La和Lb就是共模电感线圈。这两个线圈绕在同一铁芯上&#xff0c;匝数和相位都相 同(绕制反向)。 这样&#xff0c;当电路中的正常电流&#xff08;差模&…

快速搭建Jenkins自动化集成cicd工具

一、简介 jenkins是一款优秀的自动化持续集成运维工具&#xff0c;可以极大的简化运维部署的步骤。 传统的项目部署需要手动更换最新的项目代码&#xff0c;然后打包并运行到服务器上。 使用Jenkins可以自动化实现&#xff0c;当代码编写完成并提交到git后&#xff0c;Jenki…