从Linux内核探索 Socket 的本质

news2024/12/25 22:38:58

目录

一、引言

二、Socket 的概念

三、Socket 的使用场景

四、Socket 的设计

五、提供 Socket 层

六、Socket 如何实现网络通信

(一)建立连接

(二)数据传输

七、Socket 怎么实现“继承”

八、总结


一、引言

相信大家刚开始学 Socket 的时候,都跟我一样,对 Socket 的概念很模糊。本文将从一个初学者的角度开始聊起,让大家了解 Socket 是什么以及它的原理和内核实现。

二、Socket 的概念

 Socket 就如同我们日常生活中的插头与插座的连接关系。在网络编程中,Socket 是一种实现网络通信的接口或机制。 想象一下,插头插入插座后,电流得以流通,实现了能量的传递。而在网络世界里,当一个程序使用 Socket 与另一台机子建立“连接”时,就如同插头成功插入了插座,数据能够在两者之间进行流通和交换。

例如,当我们在网上聊天时,发送方的程序通过 Socket 将消息发送出去,接收方的程序通过对应的 Socket 接收这些消息。又比如在下载文件时,下载程序通过 Socket 与提供文件的服务器建立连接,从而能够获取到所需的文件数据。总之,它是网络通信的端点,用于在不同的计算机进程之间进行通信,而计算机中通过五元组:协议类型、源IP地址、源端口号、目标IP地址、目标端口号,通过五元组来唯一

三、Socket 的使用场景

我们想要将数据从 A 电脑的某个进程发到 B 电脑的某个进程。如果需要确保数据能发给对方,就选可靠的 TCP 协议;如果数据丢了也没关系,就选择不可靠的 UDP 协议。初学者一般首选 TCP。
这时就需要用 socket 进行编程,首先创建关于 TCP 的 socket:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock_fd == -1) {
        std::cerr << "Failed to create socket" << std::endl;
        return 1;
    }

    // 后续代码...

    return 0;
}

这个方法会返回 sock_fd,它是 socket 文件的句柄。
对于服务端,得到 sock_fd 后,依次执行 bind()listen()accept() 方法,等待客户端的连接请求;对于客户端,得到 sock_fd 后,执行 connect() 方法向服务端发起建立连接的请求,此时会发生 TCP 三次握手。
连接建立完成后,客户端可以执行 send() 方法发送消息,服务端可以执行 recv() 方法接收消息,反之亦然。

四、Socket 的设计

现在我们抛开socket,重新设计一个内核网络传输功能。我们想要将数据从 A 电脑的某个进程发到 B 电脑的某个进程,从操作上来看,就是发数据给远端和从远端接收数据,也就是写数据和读数据。
但这里有两个问题:

  1. 接收端和发送端可能不止一个,因此需要用 IP 和端口做区分,IP 用来定位是哪台电脑,端口用来定位是这台电脑上的哪个进程。
  2. 发送端和接收端的传输方式有很多区别,如可靠的 TCP 协议、不可靠的 UDP 协议,甚至还需要支持基于 icmp 协议的 ping 命令。

为了支持这些功能,需要定义一个数据结构 sock,在 sock 里加入 IP 和端口字段。这些协议虽然各不相同,但有一些功能相似的地方,可以将不同的协议当成不同的对象类(或结构体),将公共的部分提取出来,通过“继承”的方式复用功能。

于是,定义了一些数据结构:

  • sock 是最基础的结构,维护一些任何协议都有可能会用到的收发数据缓冲区。
    在 Linux 内核 2.6 相关的源码中,sock 结构体的定义可能类似于:
    struct sock {
        // 相关字段
        struct sk_buff_head sk_receive_queue; // 接收数据缓冲区
        struct sk_buff_head sk_write_queue;  // 发送数据缓冲区
        // 其他可能的字段
    };
    
  • inet_sock 特指用了网络传输功能的 sock,在 sock 的基础上还加入了 TTL、端口、IP 地址这些跟网络传输相关的字段信息。比如 Unix domain socket,用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈。
    可能的定义:
    struct inet_sock {
        struct sock sk; // 继承自 sock
        __be32 port;    // 端口
        __be32 saddr;   // IP 地址
        // 其他相关字段
    };
    
  • inet_connection_sock 是指面向连接的 sock,在 inet_sock 的基础上加入面向连接的协议里相关字段,比如 accept 队列、数据包分片大小、握手失败重试次数等。虽然现在提到面向连接的协议就是指 TCP,但设计上 Linux 需要支持扩展其他面向连接的新协议。
    例如:
    struct inet_connection_sock {
        struct inet_sock inet; // 继承自 inet_sock
        struct request_sock_queue accept_queue; // accept 队列
        // 其他相关字段
    };
    
  • tcp_sock 就是正儿八经的 TCP 协议专用的 sock 结构,在 inet_connection_sock 基础上还加入了 TCP 特有的滑动窗口、拥塞避免等功能。同样 UDP 协议也会有一个专用的数据结构,叫 udp_sock
    大概如下:
    struct tcp_sock {
        struct inet_connection_sock icsk; // 继承自 inet_connection_sock
        // TCP 特有的字段,如滑动窗口、拥塞避免等相关字段
    };
    

有了这套数据结构,将它跟硬件网卡对接一下,就实现了网络传输的功能。

五、提供 Socket 层

由于这里面的代码复杂,还操作了网卡硬件,需要较高的操作系统权限,再考虑到性能和安全,于是将它放在操作系统内核里。
为了让用户空间的应用程序使用这部分功能,将这部分功能抽象成简单的接口,将内核的 sock 封装成文件。创建 sock 的同时也创建一个文件,文件有个文件描述符 fd,通过它可以唯一确定是哪个 sock。将fd暴露给用户,用户就可以像操作文件句柄那样去操作这个 sock 。

struct file{
    //文件相关的字段
    .....
    void *private_data; //指向sock
}

创建socket时,其实就是创建了一个文件结构体,并将private_data字段指向sock。


有了 sock_fd 句柄后,提供了一些接口,如 send()recv()bind()listen()connect() 等,这些就是 socket 提供出来的接口。

所以说,socket 其实就是个代码库或接口层,它介于内核和应用程序之间,提供了一堆接口,让我们去使用内核功能,本质上就是一堆高度封装过的接口。

我们平时写的应用程序里代码里虽然用了socket实现了收发数据包的功能,但其 实真正执行网络通信功能的,不是应用程序,而是linux内核。

在操作系统内核空间里,实现网络传输功能的结构是sock,基于不同的协议和应用场景,会被泛化为各种类型的xx_sock,它们结合硬件,共同实现了网络传输功能。为了将这部分功能暴露给用户空间的应用程序使用,于是引入了socket层,同时将sock嵌入到文件系统的框架里,sock就变成了一个特殊的文件,用户就可以在用户空间使用文件句柄,也就是socket_fd来操作内核sock的网络传输能力。

六、Socket 如何实现网络通信

以最常用的 TCP 协议为例,实现网络传输功能分为建立连接和数据传输两个阶段。

(一)建立连接

在客户端,执行 socket 提供的 connect(sockfd, "ip:port") 方法时,会通过 sockfd 句柄找到对应的文件,再根据文件里的信息指向内核的 sock 结构,通过这个 sock 结构主动发起三次握手。
在服务端,握手次数还没达到“三次”的连接叫半连接,完成好三次握手的连接叫全连接,它们分别会用半连接队列和全连接队列来存放,这两个队列会在执行 listen() 方法的时候创建好。当服务端执行 accept() 方法时,就会从全连接队列里拿出一条全连接。

虽然都叫队列,但半连接队列其实是个哈希表,而全连接队列其实是个链表。

在 Linux 内核 2.6 版本的源码中,相关的代码实现可能位于网络子系统的部分。例如,建立连接的过程可能涉及到 tcp_connect() 等函数。

(二)数据传输

为了实现发送和接收数据的功能,sock 结构体里带了一个发送缓冲区和一个接收缓冲区,其实就是个链表,上面挂着一个个准备要发送或接收的数据。
当应用执行 send() 方法发送数据时,会通过 sock_fd 句柄找到对应的文件,根据文件指向的 sock 结构,找到这个 sock 结构里带的发送缓冲区,将数据放到发送缓冲区,然后结束流程,内核看心情决定什么时候将这份数据发送出去。


接收数据流程也类似,当数据送到 Linux 内核后,先放在接收缓冲区中,等待应用程序执行 recv() 方法来拿。
当应用进程执行 recv() 方法尝试获取(阻塞场景下)接收缓冲区的数据时,如果有数据,取走就好;如果没数据,就会将自己的进程信息注册到这个 sock 用的等待队列里,然后进程休眠。如果这时候有数据从远端发过来了,数据进入到接收缓冲区时,内核就会取出 sock 的等待队列里的进程,唤醒进程来取数据。

当多个进程通过 fork 的方式 listen 了同一个 socket_fd,在内核它们都是同一个 sock,多个进程执行 listen() 之后,都会将自身的进程信息注册到这个 socket_fd 对应的内核 sock 的等待队列中。在 Linux 2.6 以前,会唤醒等待队列里的所有进程,但最后其实只有一个进程会处理这个连接请求,其他进程又重新进入休眠,会消耗一定的资源,这就是惊群效应。在 Linux 2.6 之后,只会唤醒等待队列里的其中一个进程,这个问题被修复了。

服务端 listen 的时候,那么多数据到一个 socket 怎么区分多个客户端的?以 TCP 为例,服务端执行 listen 方法后,会等待客户端发送数据来。客户端发来的数据包上会有源 IP 地址和端口,以及目的 IP 地址和端口,这四个元素构成一个四元组,可以用于唯一标记一个客户端。服务端会创建一个新的内核 sock,并用四元组生成一个 hash key,将它放入到一个 hash 表中。下次再有消息进来的时候,通过消息自带的四元组生成 hash key 再到这个 hash 表 里重新取出对应的 sock 就好了。

七、Socket 怎么实现“继承”

Linux 内核是 C 语言实现的,而 C 语言没有类也没有继承的特性,是通过结构体里的内存是连续的这一特点来实现“继承”的效果。将要继承的“父类”,放到结构体的第一位,然后通过结构体名的长度来强行截取内存,这样就能转换结构体,从而实现类似“继承”的效果。


例如:

struct tcp_sock {
    /* inet_connection_sock has to be the first member of tcp_sock */
    struct inet_connection_sock inet_conn;
    // 其他字段
};

struct inet_connection_sock {
    /* inet_sock has to be the first member! */
    struct inet_sock icsk_inet;
    // 其他字段
};

// sock 转为 tcp_sock
static inline struct tcp_sock *tcp_sk(const struct sock *sk) {
    return (struct tcp_sock *)sk;
}

八、总结

  • socket 中文套接字,可理解为一套用于连接的数字。
  • sock 在内核,socket_fd 在用户空间,socket 层介于内核和用户空间之间。
  • 在操作系统内核空间里,实现网络传输功能的结构是 sock,基于不同的协议和应用场景,会被泛化为各种类型的 xx_sock,它们结合硬件,共同实现了网络传输功能。为了将这部分功能暴露给用户空间的应用程序使用,于是引入了 socket 层,同时将 sock 嵌入到文件系统的框架里,sock 就变成了一个特殊的文件,用户就可以在用户空间使用文件句柄,也就是 socket_fd 来操作内核 sock 的网络传输能力。
  • 服务端可以通过四元组来区分多个客户端。
  • 内核通过 C 语言“结构体里的内存是连续的”这一特点实现了类似继承的效果。

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

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

相关文章

[Zer0pts2020]Can you guess it?1

打开题目 看到信息随便输入一个数&#xff0c;显示错误 查看源代码 看到php代码&#xff0c;代码审计 <?php include config.php; // FLAG is defined in config.php if (preg_match(/config\.php\/*$/i, $_SERVER[PHP_SELF])) { exit("I dont know what you are t…

以node / link文件表征的道路网络-----dijkstra算法yyds-----基于南京公路公开数据做路径规划(上)

前文已经基于公开数据&#xff0c;获得了南京的全域高速公路的路网数据&#xff0c;这些以node / link文件表征的道路网络不仅延续了osm地图中所包含的经纬度、名称、容量等信息 &#xff0c;还包含了一个重要的道路等级字段 “link_type_name”。 交通部门一般以高速公路、国…

ThinkPHP的SQL注入漏洞学习

目录 漏洞环境 漏洞概要 函数学习 call_user_func函数 mplode函数 漏洞分析 漏洞修复 攻击总结 漏洞环境 漏洞存在于 Builder 类的 parseData 方法中。由于程序没有对数据进行很好的过滤&#xff0c;将数据拼接进 SQL 语句&#xff0c;导致 SQL注入漏洞 的产生。 漏洞…

Shell参考 - Linux Shell 训练营

出品方<Linux.cn & 阿里云开发者学堂> 一&#xff0c;Linux 可以划分为以下四个部分&#xff1a; 1. 应用软件 2. 窗口管理软件 Unity Gnome KDE 3. GNU 系统工具链 Software- GNU Project - Free Software Foundation 4. Linux 内核 二&#xff0c;什么是shell 1. L…

一款免费开源电脑流量监控软件,电脑流量统计工具!

TrafficMonitor是一个开源的网络速度监控工具&#xff0c;它能够在Windows平台上以悬浮窗的形式显示当前的网速、CPU和内存使用情况。该工具支持多种显示模式&#xff0c;包括悬浮窗和任务栏显示&#xff0c;并且允许用户更换不同的皮肤来自定义外观样式。此外&#xff0c;Traf…

【MySQL】数据库基础(库的操作)

目录 一、MySQL安装、连接、修改密码操作 二、库的操作 2.1 创建数据库 2.2 字符集和校验规则 2.3 操控数据库 2.4 修改数据库 2.5 删除数据库 2.6 数据库的备份和恢复 2.7 查看连接情况 前情提要&#xff1a; 我的服务器操作系统是Ubuntu20.04&#xff0c;安装的是M…

eNSP 华为远程访问路由器

华为远程访问路由器 前提&#xff1a;主机能与路由器通信就行&#xff0c;如果不同网段就配路由协议&#xff0c;这里直接模拟直连通信 Cloud&#xff1a; R&#xff1a; <Huawei>sys [Huawei]sys R [R]int g0/0/0 [R-GigabitEthernet0/0/0] [R-GigabitEthernet0/0/0]i…

Vue50 todolist自定义事件版本

代码 MyFooter.vue <template><div class"todo-footer" v-show"total"><label><!-- <input type"checkbox" :checked"isAll" change"checkAll"/> --><input type"checkbox" v…

【html】颜色随机产生器(补充包)

上一篇文章我们讲了如何制作一个通过滑动产色纯色背景的网页&#xff0c;今天&#xff0c;我们对那个网页进行一个补充&#xff0c;&#xff08;&#xff09; 因为很多人在设计网页的时候没有颜色的灵感这个时候我们我们就可以考虑通过随机产生一种颜色并且能够实时看到效果的…

Delphi5实现密码、姓名生成器

文章目录 目的效果图密码生成器类类定义成员函数 点击“密码生成”事件名字生成器类类的成员功能概述注意点 点击“姓名生成”事件点击“清空”事件“导出txt”事件“备注”输入框画图软件完整代码 目的 写这个程序的目的是生成一个密码和用于快递的名字&#xff08;生成密码和…

keepalived详细讲解

keepalived&#xff1a; Keepalived是一个基于VRRP&#xff08;‌Virtual Router Redundancy Protocol&#xff0c;‌虚拟路由冗余协议&#xff09;‌协议实现的LVS&#xff08;‌Linux Virtual Server&#xff09;‌服务高可用方案。‌它的主要作用是进行虚拟路由的故障切换&…

算法打卡 Day24(二叉树)-二叉搜索树的最近公共祖先 + 二叉搜索树中的插入操作 + 删除二叉搜索树中的节点

文章目录 Leetcode 235-二叉搜索树的最近公共祖先题目描述解题思路 Leetcode 701-二叉搜索树中的插入操作题目描述解题思路 Leetcode 450-删除二叉搜索树中的节点题目描述解题思路 Leetcode 235-二叉搜索树的最近公共祖先 题目描述 https://leetcode.cn/problems/lowest-comm…

江协科技STM32学习笔记(第10章 SPI通信)

第10章 SPI通信 10.1 SPI通信协议 10.1.1 SPI通信 SPI&#xff08;Serial Peripheral Interface&#xff09;是由Motorola公司开发的一种通用数据总线&#xff1b; 串行外设接口&#xff1b; I2C无论是软件还是软件电路&#xff0c;设计的都还是比较复杂的&#xff0c;硬件…

xss靶场详解

目录 1.第一题 2.第二题 3.第三题 4.第四题 5.第五题 6.第六题 7.第七题 8.第八题 1.第一题 在源码script标签里边&#xff0c;innerhtml是用于访问或修改 HTML 元素内的 HTML 内容的&#xff0c;这里是访问spaghet这个元素的&#xff0c;并通过括号里面的东西搜索当前…

【问题解决3】【已解决】Cannot determine path to‘tools.jar‘libraryfor17

前几天在IDEA运行JAVA项目时&#xff0c;出现这个报错。 这是因为是这个笔记本上安装的IDEA版本是“IntelliJ IDEA 2020.3.1”&#xff0c;与JDK17版本不兼容&#xff0c;这种情况下要想使得IDEA版本与JDK版本兼容&#xff0c;就需要升级IDEA版本或者使用JDK较低版本&#xff…

专题---自底向上的计算机网络(计算机网络相关概述)

目录 计算机网络相关概述 物理层 数据链路层 网络层 运输层 应用层 网络安全 1.计算机网络相关概述&#xff08;具体细节http://t.csdnimg.cn/NITAW&#xff09; 什么是计算机网络&#xff1f; 计算机网络是将一个分散的&#xff0c;具有独立功能的计算机系统&#x…

FusionSphere虚拟机网络不通

虚拟机侧 1、通过控制台Console或者VNC登录虚拟机。 获取VNC的token链接&#xff0c;因为token有超时失效&#xff0c;该链接获取后长时间不用要重新获取。 # nova get-vnc-console vmid novnc 2、登录VNC控制台之后&#xff0c;检查网卡和IP地址是否up, ARP学习是否正常…

Postman 问题汇总

1 postman Error: SSL Error: UNABLE_TO_VERIFY_LEAF_SIGNATURE 根因 SSL校验失败&#xff0c;可以在postman设置中关闭ssl校验&#xff0c;自测对ssl无要求。 解决方法 在postman设置中关闭ssl校验&#xff1a;

树莓派智能语音助手之聊天机器人-RASA

我的树莓派目前已经会“说”&#xff08;《树莓派智能语音助手之TTS - pyttsx3 espeak》&#xff09;&#xff0c;也能“听”&#xff08;《树莓派智能语音助手之ASR2 – sherpa-ncnn》&#xff09;了。接下来&#xff0c;就要让它能够和我们对话起来&#xff0c;即会“聊天”…

python 获取pdf文件中的超链接

pip install pymupdf pip install fitzimport fitz # PyMuPDFdef get_pdf_links(pdf_path):# 打开PDF文件document fitz.open(pdf_path)links []for page_num in range(len(document)):page document[page_num]# 获取当前页面的链接for link in page.get_links():links.app…