Socket 原理和思考

2024-06-20 08:36
文章标签 socket 思考 原理

本文主要是介绍Socket 原理和思考,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

众所周知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);//创建一个服务端且端口为8686Socket 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;}@Overridepublic 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@Overridepublic 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 获取并清除。

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


这篇关于Socket 原理和思考的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1077629

相关文章

关于如何更好管理好数据库的一点思考

本文尝试从数据库设计理论、ER图简介、性能优化、避免过度设计及权限管理方面进行思考阐述。 一、数据库范式 以下通过详细的示例说明数据库范式的概念,将逐步规范化一个例子,逐级说明每个范式的要求和变换过程。 示例:学生课程登记系统 初始表格如下: 学生ID学生姓名课程ID课程名称教师教师办公室1张三101数学王老师101室2李四102英语李老师102室3王五101数学王老师101室4赵六103物理陈

数据库原理与安全复习笔记(未完待续)

1 概念 产生与发展:人工管理阶段 → \to → 文件系统阶段 → \to → 数据库系统阶段。 数据库系统特点:数据的管理者(DBMS);数据结构化;数据共享性高,冗余度低,易于扩充;数据独立性高。DBMS 对数据的控制功能:数据的安全性保护;数据的完整性检查;并发控制;数据库恢复。 数据库技术研究领域:数据库管理系统软件的研发;数据库设计;数据库理论。数据模型要素 数据结构:描述数据库

计算机组成原理——RECORD

第一章 概论 1.固件  将部分操作系统固化——即把软件永恒存于只读存储器中。 2.多级层次结构的计算机系统 3.冯*诺依曼计算机的特点 4.现代计算机的组成:CPU、I/O设备、主存储器(MM) 5.细化的计算机组成框图 6.指令操作的三个阶段:取指、分析、执行 第二章 计算机的发展 1.第一台由电子管组成的电子数字积分和计算机(ENIAC) 第三章 系统总线

GaussDB关键技术原理:高性能(二)

GaussDB关键技术原理:高性能(一)从数据库性能优化系统概述对GaussDB的高性能技术进行了解读,本篇将从查询处理综述方面继续分享GaussDB的高性能技术的精彩内容。 2 查询处理综述 内容概要:本章节介绍查询端到端处理的执行流程,首先让读者对查询在数据库内部如何执行有一个初步的认识,充分理解查询处理各阶段主要瓶颈点以及对应的解决方案,本章以GaussDB为例讲解查询执行的几个主要阶段

【计算机组成原理】部分题目汇总

计算机组成原理 部分题目汇总 一. 简答题 RISC和CICS 简要说明,比较异同 RISC(精简指令集)注重简单快速的指令执行,使用少量通用寄存器,固定长度指令,优化硬件性能,依赖软件(如编译器)来提升效率。 CISC(复杂指令集)包含多样复杂的指令,能一条指令完成多步操作,采用变长指令,减少指令数但可能增加执行时间,倾向于硬件直接支持复杂功能减轻软件负担。 两者均追求高性能,但RISC

MySQL数据库锁的实现原理

MySQL数据库的锁实现原理主要涉及到如何确保在多用户并发访问数据库时,保证数据的完整性和一致性。以下是MySQL数据库锁实现原理的详细解释: 锁的基本概念和目的 锁的概念:在数据库中,锁是用于管理对公共资源的并发控制的机制。当多个用户或事务试图同时访问或修改同一数据时,数据库系统通过加锁来确保数据的一致性和完整性。 锁的目的:解决多用户环境下保证数据库完整性和一致性的问题。在并发的情况下,会

线性回归(Linear Regression)原理详解及Python代码示例

一、线性回归原理详解         线性回归是一种基本的统计方法,用于预测因变量(目标变量)与一个或多个自变量(特征变量)之间的线性关系。线性回归模型通过拟合一条直线(在多变量情况下是一条超平面)来最小化预测值与真实值之间的误差。 1. 线性回归模型         对于单变量线性回归,模型的表达式为:         其中: y是目标变量。x是特征变量。β0是截距项(偏置)。β1

标准分幅下的图幅号转换成经纬度坐标【原理+源代码】

最近要批量的把标准分幅下的图幅号转换成经纬度坐标,所以这两天写了个程序来搞定这件事情。 先举个例子说明一下这个程序的作用。 例如:计算出图幅号I50G021040的经纬度范围,即最大经度、最小经度、最大纬度、最小纬度。 运用我编写的这个程序,可以直接算出来,这个图幅号的经纬度范围,最大经度为115.3125°,最小经度为115.25°,最大纬度为31.167°,最小纬度为31.125°。

SpingBoot原理

配置优先级 SpringBoot配置的优先级从高到低依次为命令行参数、JNDI属性、Java系统属性、操作系统环境变量、外部配置文件、内部配置文件、注解指定的配置文件和编码中直接指定的默认属性。具体如下: 命令行参数:启动应用时,通过命令行指定的参数拥有最高优先级。例如,使用--server.port=8081会直接改变应用程序的端口,无论在什么配置文件中定义过该值。JNDI属性:这些属性由当

HashMap 的工作原理及其在 Java 中的应用?

在Java的数据结构中,HashMap是最常见且最重要的一个数据结构之一。HashMap是Java集合框架中的一部分,它存储的是键值对(Key-value)映射,也就是说,你可以通过键(Key)找到对应的值(Value)。让我们来详细地看一下HashMap的工作原理。 HashMap的工作原理 HashMap内部有一个数组,数组中的每个元素又是一个链表。当我们将一个键值对存入HashM