电话:13485538018
关闭
您当前的位置:首页 > 职场资讯 > 面试秘籍

Socket网络编程面试:一文搞懂TCP/UDP内核sock结构设计

来源:网络整理 时间:2026-06-30 作者:佚名 浏览量:

inet_connection_sock指得就是面向连接的sock, 它是在inet_sock的基础之上, 加入了面向连接的协议里与之相关的字段。比如说, 像accept队列、数据包分片大小以及握手失败重试次数等。尽管当下我们一提到面向连接的协议, 所指的便是TCP, 然而在设计方面, linux是需要支持扩展其他面向连接的新协议的。

tcp_sock, 它可是那种彻头彻尾正儿八经的tcp协议专门所专用的sock结构, 其在inet_connection_sock的基础之上额外又加入了tcp自身特有的滑动窗口、拥塞避免等一系列功能。同样地, udp协议也会存在着一个专门所专用的数据结构, 它的名字叫做udp_sock。

现在, 此套数据结构既定, 将其与硬件网卡予以对接处理, 如此, 则网络传输之恩能用得以成就了。

提供socket层

能够想象得出, 这里面的代码必定极为复杂, 与此同时还对网卡硬件进行了操作, 这需要较高的操作系统权限, 再鉴于性能与安全方面的考量, 所以决定把它放置在操作系统内核之中。

倘若网络传输功能被构建于内核之中, 那么处于用户空间的应用程序若要运用这部分功能, 该采取什么样的举措呢?

采用不重复制造轮子的准则, 这是容易做到的事情, 我们会把这部分功能抽象为一个个简易的接口。往后别的人只要调用这些接口, 就能驱使我们编写好了的那一大串繁杂的数据结构去传送数据。

那么, 问题出现了, 怎样把这部分功能予以暴露, 如何能让其他程序员更加便利地去使用它?

跟远端服务端进程进行数据的收发可被抽象成 ‘读和写’, 操作文件同样也能够被抽象成 '读和写', 恰好有那么一句话, 即"linux里所有都是文件", 因而我们干脆, 把内核的sock封装成文件就行。在创建sock之际同时创建一个文件, 文件具备一个句柄fd, 简单来讲就是文件系统里的身份证号码, 借助它能够唯一确定是哪一个sock。

实际上是指包含在这个表达式sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)中的sock_fd, 而说的这个文件句柄叫fd。

把句柄向用户予以暴露, 随后用户能够如同对文件句柄开展操作那般去操控这个sock句柄。于用户空间之中对这个句柄实施操作, 文件系统会把操作导向内核sock结构。

是的,操作这个特殊的文件就相当于操作内核里对应的sock。

通过文件找到sock

得到sock_fd句柄之后, 我们得给出些许接口方法。这是为了让用户能更便利地实现特定网络编程功能。我们罗列了这些接口, 发觉要有send()、recv()、bind()、listen()还有connect()这些, 至此我内核网络传输功能就算是设计好了。

目前是不是有了眼熟的感觉, 置于上方的那些接口方法, 实际上恰是由socket所提供出来的接口。

所以呀, 要说的是, socket实际上呢就是个代码库或者接口层, 它处在内核以及应用程序的中间位置, 提供出来若干高度封装好了的接口, 从而让我们能够使用内核网络传输功能。

基于sock实现网络传输功能

来到此处,我们理应是明晰了的。我们平常所编写的应用程序之中, 编写在代码里头虽说运用了socket达成了收发数据包的功能, 然而实际真正执行网络通信功能的, 并非应用程序, 而是linux内核了。这就好比应用程序借助socket所提供的接口, 把网络传输的这一部分工作外包给了linux内核。

这听闻起来是否类似我们最为熟知的前后端分离的服务架构呢, 虽说如此表述并非十分严谨, 然而看起来linux仿佛被划分成了应用程序以及内核这两个服务。内核恰似后端, 它暴露了好些个api接口, 其中有一类便是socket的send()以及recv()这类方法。应用程序则如同前端, 负责调用内核所提供的接口以达成想要实现的功能。

进程通过socket调用内核功能

看到这里,我担心大家会有点混乱,来做个小的总结。

在操作系统内核空间当中, 实现网络传输功能的结构是sock, 基于不一样的协议以及应用场景, 会被泛化为各种各样类型的xx_sock, 它们与硬件相结合, 一同达成了网络传输功能。为了把这部分功能展现给用户空间的应用程序来使用, 所以引入了socket层, 与此同时将sock嵌入到文件系统的框架之内, sock就变成了一个特殊的文件, 用户能够在用户空间运用文件句柄, 也就是socket_fd来操控内核sock的网络传输能力。

此socket_fd乃为int类型之数字, 当下回转去瞧socket之汉译, 套接字, 吾将其领会作一套用以进行互连之数字, 这般是否便觉尤为合情合理了啦。

网络分层与基于sock实现网络传输功能

socket如何实现网络通信

上面关于怎么实现网络通信功能这一块一笔带过了。

现在我们来聊聊。

这么一套sock的架构实际上极为繁杂。就拿最为常用的TCP协议当作例子, 简要去知晓一下它是怎样达成网络传输功能的。

我将它分为两阶段,分别是建立连接和数据传输。

建立连接

对于TCP,要传数据,就得先在客户端和服务端中间建立连接。

于客户端里, 代码去执行那socket所提供的connect(sockfd"ip:port")此方法之际, 会借由sockfd句柄寻觅到对应的文件, 接着依据文件当中的信息指向内核的sock结构。凭借这个sock结构主动展开三次握手。

TCP三次握手

连接若在服务端握手次数尚未抵达“三次”的情形下, 便称作半连接, 而顺利完成三次握手的连接, 则叫做全连接。半连接队列与全连接队列将会用来存放它们, 而这两个队列在你执行listen()方法之际会被创建妥当。当服务端执行accept()方法时期, 一条全连接便会从全连接队列当中被取出。

半连接队列和全连接队列

至此,连接就算准备好了,之后,就可以开始传输数据。

既都称作队列, 然而半连接队列实际上是个哈希表, 并且全连接队列实际上是个链表。

数据传输

要达成发送以及接收数据的功能目的, sock结构体之中附带了一个发送缓冲区, 还有一个接收缓冲区, 虽说称作缓冲区, 然而实际上它就是一个链表, 在这个链表上面悬挂着一个个预备要发送或者接收的数据。

在应用执行send()方法来发送数据之际,同样也会借由sock_fd句柄寻觅到对应的文件, 依据文件所指向的sock结构, 寻得此sock结构中所带的发送缓冲区, 会把数据放置于发送缓冲区, 接着结束流程, 而内核依照自身心情来判定何时将这份数据发送出去。

收下数据的流程同样相仿, 在数据被送至linux内核之后, 数据并非即刻就给予应用程序, 而是先被放置于接收缓冲区里, 数据安静地待着, 卑微地等候应用程序于某个时刻执行recv()方法来取走它。

sock的发送和接收缓冲区

IP以及端口实际上并非处于sock之下, 而是存在于inet_sock之下, 上面如此进行绘制仅仅是用于简化。

那么此时问题就出现了, 数据的发送是由应用程序主动去发起的, 对于这点大家都是不存在任何问题的。

那数据接收这一行为该如何进行? 数据是从远端发送过来的这种情况, 要怎样去完成告知以及给予应用程序这一系列动作?

这就需要用到等待队列。

sock内的等待队列

当你应用进程去执行recv()这个方法呀, 是在尝试获取接收缓冲区的数据之时, 处于阻塞场景的状态下。

recv时无数据进程进入等待队列

有时, 你会瞧见多个进程借由fork的途径, 进行了对同一个socket_fd的listen操作。于内核之中, 它们俱为同一个sock, 多个进程执行listen()之后, 皆急切等待连接接入,故而都把自身的进程信息登记到这个socket_fd所对应的内核sock的等待队列里。倘若此时实实在在来了一个连接, 该唤醒等待队列中的哪一个进程去接收连接呢? 这个问题的答案相当有意思。

惊群效应

看到这里,问题又来了。

实施服务端 listen 操作之际, 如此数量众多的数据抵达同一个 socket , 究竟是凭借怎样的方式去分辨多个客户端的呢?

以TCP作为例子, 服务端在执行listen方法之后, 就会等候客户端发送数据过来。客户端发送来的数据包之上会有源IP地址以及端口, 还有目的IP地址以及端口, 这四个元素共同构成一个四元组, 能够用来唯一标记一个客户端。

实际上讲四元组这种说法谈不上更为严谨, 鉴于在整个进程当中还存在超多别的信息内容, 那样描述也能够归纳为五元组。然而大体上去领会就行, 就如此这般。

四元组

服务端会去创建出, 一个全新的内核sock, 然后利用四元组生成, 一个hash key, 接着要把它放置进去, 成为到一个hash表里头的部分。

四元组映射成hash键

当接下来再有消息传入之际, 利用消息所附带的四元组去生成hash key , 接着依据这个hash key到那个hash表里再次取出相应的sock就得。故而可以说服务端是凭借四元组用来区分多个客户端的。

多个hash_key对应多个客户端

sock怎么实现"继承"

最后遗留一个问题。

众人皆知, linux内核借助C语言得以实现, 然而, 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;
}

内存布局

总结

微信扫一扫分享资讯
相关推荐
暂无相关推荐
客服服务热线
13485538018
24小时服务
微信公众号
手机浏览

CopyrightC 2009-2025 All Rights Reserved 版权所有 芜湖人才网 本站内容仅供参考,不承担因使用信息、外部链接或服务中断导致的任何直接或间接责任,风险自担。如有侵权,请联系删除,联系邮箱:ysznh@foxmail.com 鄂ICP备2025097818号-15

地址: EMAIL:qlwl@foxmail.com

Powered by PHPYun.

用微信扫一扫