用了socket就一定要走网络协议栈吗?

用了socket就一定要走网络协议栈吗?

我们经常犯一种错误,学一样新东西的时候,我们根本不知道它的用途和存在的意义是啥,这样学了就忘,无法内化为自己的能力。

就比方说大学的时候学线性代数的时候,大家都学得很枯燥很痛苦,根本不知道这玩意能跟什么联系起来,以为根本用不上,后来才知道,原来这玩意可以用在机器学习上。

所以说,脱离实际应用场景学东西会让人很痛苦。

因此,为了更好的聊今天的话题,我们假设有这么个场景。

你是一个后端开发,你手上有两类服务,分为golang服务(rpc服务)和python服务(http服务)(当然也可以替换为java和C++,总之是两个不同语言类型的服务)。

公司主推go服务,基于go实现了非常多的基础组件,其中最重要的是微服务rpc框架,以及基于框架实现了限流,熔断、流量染色balabala等一系列服务治理的功能。

头疼的是python服务没能用上这些组件,怎么办?

我猜你第一时间想到的是将python服务翻译成golang服务。

那么问题来了,不是所有功能都能一比一进行翻译的,比如说有些python服务里用了一些机器学习的库,go里可不支持。

那这时候转念一想,如果能将python代码转成动态链接库.so文件),让go去调用动态链接库不就好了吗,理论上可行,需要将python服务中核心的代码拿出来抽成一个函数,再通过动态链接库暴露出去。

然而,python服务是基于一个成熟的web框架gunicorn去做开发的,像什么日志,打点都做好了,你并不想过多的去改动python服务里的代码。

那么问题来了,有没有一种更好的方法?

有,你听说过sidecar吗?

基于sidecar的服务架构

sidecar,又叫边车。我猜大家应该在各种抗战剧里看到过。

就是一辆摩托车边上加装的一个带轮车座。看下面这个图,你能秒懂。

左边的边车和右边的摩托车共同组成一辆小三轮。

离开边车,右边的摩托车本身也能跑,但是组装上一个边车,它能做更多的事情,比如多带点东西。

我们可以借鉴这个概念,用在文章开头的案例里,将golang服务作为python服务的边车,将两者共同组成一个新的服务

python服务本身是个http服务,监听127.0.0.1:8080地址。

golang作为一层皮,使用微服务rpc框架对外提供rpc功能,而对内,只做一件事,那就是将rpc请求转发到Python服务的127.0.0.1:8080地址上。

这样就做到了python服务不需要怎么改,就能享受到golang服务的微服务框架的服务治理能力。

但这里其实还有个问题,之前写过的《断网了能ping通127.0.0.1吗?》中提到过127.0.0.1是本地回环地址

如果本机的A应用进程将数据发向本地回环地址,那么数据会过一遍内核网络协议栈的封包拆包逻辑,再回到本机的B应用进程。

有没有办法,可以省略这部分“不必要”的逻辑?

有,我们可以用unix domain socket.

unix domain socket是什么?

unix domain socket,简称uds,也是socket的一种,只不过yan割了网络协议栈的功能,专门用于本机进程间通信

如果你用过docker,那你肯定见过下面这个报错。一般常见于你本地没启动docker守护进程,但却尝试使用docker的命令。

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

这里面就涉及到unix://var/run/docker.sock其实就是uds,它用于docker守护进程和docker容器之间的通信。大家知道有这回事就好,不再展开。

正常情况下想要建立网络连接。服务端的伪代码大概长这样。

int main() { //1.创建网络的通信对象 listen_fd = socket(AF_INET,SOCK_STREAM,0); //2.绑定服务器的IP地址 bind(listen_fd, "127.0.0.1:8080"); //3.把服务器socket,设置为监听模式 listen(listen_fd); //4.接收客户端的连接请求 client_fd = accept(listen_fd); //5.接收网络数据 n = recv(client_fd, &msg) return 0; }

对应的客户端伪代码长这样。

int main(){ //创建套接字 sock = socket(AF_INET, SOCK_STREAM, 0); // 主动发起建立连接 connect(sock, "127.0.0.1:8080"); // 写数据到接收方 ret = send(sock, "hello word"); }

只要客户端那执行创建的socket执行 connect( ) 方法,两端就会开始进行TCP三次握手建立连接。然后使用**send()**方法写数据,**recv()**方法读数据。

如果要改成uds,只需要改两个地方。

  1. 将客户端和服务器创建socket时的AF_INET改为AF_UNIX.
  2. 将连接的地址从127.0.0.1:端口号改为某个文件地址,比如**/tmp/xiaobai.sock**.

是的,这就好了。

改动量极小

而且以上的伪代码是C语言的,其他语言也类似。

这时候问题就来了,实际上我在项目中是直接用了成熟的框架,它们都将socket这一层给包起来了,我碰不到socket这一层的接口,怎么改造呢?

这个就更好办了,成熟的框架基本上都支持uds的功能,一般只需要很少量的配置就可以搞定,搜一下,很容易就能找到答案。比如pythongunicorn框架只需要将具体的IP地址替换为unix+某个文件地址就好了。

// gunicorn_config.py bind = ['unix:/tmp/xiaobai.sock']

unix domain socket内核实现

这时候问题很多的小明肯定就要问了。

用了unix domain socket之后,能用wireshark抓包吗?

哈哈哈哈哈。

不能。

为啥?这涉及到它的内核实现。

在此之前,我们需要看下用127.0.0.1通信时的内核实现是怎么样的,对比一下你就知道为什么uds这么快,以及为什么不能抓包了

127.0.0.1通信时的内核实现

以我们项目中最常见的HTTP1.1为例,它底层依赖的是TCP协议

用TCP进行数据传输,会先进行三次握手,然后才是应用进程进行收发数据。

三次握手的阶段,客户端执行connect()发出第一次握手后,服务端收到数据会创建一个半连接放到半连接队列中,然后回客户端第二次握手,客户端发出第三次握手后服务端会从半连接队列中取出前面那个连接,创建全连接并放入到全连接队列中。然后等待服务端代码执行accept()方法将全连接取出。这之后数据就可以进入收发数据阶段。

在收发数据阶段,以send()为例,数据会从应用进程拷贝到内核发送方sock的发送缓冲区,进入传输层加入TCP头,再进入网络层后加入IP头,到数据链路层加入mac头,然后推入一个叫input_pkt_queue的链表中,触发软中断,专门处理软中断的ksoftirqd会将数据包从input_pkt_queue取出,然后再层层拆包给回到传输层,再找到接收方的sock并将数据包放到它的接收缓冲区中。应用程序执行recv(),就能获取到数据。

不知道sock是什么的,可以看下我之前写的《socket到底是什么》

uds通信的内核实现。

还是以TCP为例。

在握手阶段,客户端执行connect()后并不发出握手消息,而是根据unix:/tmp/xiaobai.sock里的文件地址,直接找到这个文件背后的listen sock,然后将数据塞到这个listen sock接收缓冲区中。

注意,这里根本没有什么半连接队列和全连接队列。

服务端执行accept()方法时就直接从listen sock接收缓冲区中拿数据,再创建一个新的sock,这个新sock和客户端的sock互相指向对方,这样两者就可以通过指针很容易找到对方,从此就用这两个sock互相向对方收发数据。

在收发数据阶段,以send()为例,数据会从应用进程进入到到内核后,根本就不进发送方sock的发送缓冲区,而是通过发送方sock直接找到接收方的sock,直接将数据放到接收方sock的接收缓冲区中。此时,应用程序执行recv(),就能获取到数据。

对比下上面两个流程和与回环地址(127.0.0.1)通信的区别,你品,你细品。

uds是不是省下了很多操作。

所以它的性能会比用回环地址通信要好。

那么为什么不能抓包呢?这是因为wireshark抓包这个动作,其实发生在数据链路层。用了uds,数据根本没经过数据链路层,因此也就不可能会被抓到了。

用了UDS后,选择TCP和UDP还有区别吗?

先说结论,是有区别的。

这里分为用法上的区别内核实现上的区别。它们都有区别。

用法上的区别

以下是创建uds时,选择"TCP或UDP"的代码。

// 创建一个"udp"的unix domain socket socket(AF_UNIX, SOCK_DGRAM, 0) // 创建一个"tcp"的unix domain socket socket(AF_UNIX, SOCK_STREAM, 0)

选择"TCP"时,写代码的时候要比"UDP"多使用两个接口,分别是listen()accept()

其实这个倒还好,我相信大家关心的并不是这个,而是内核实现上它们有区别不。

内核实现上的区别

注意上面提到TCP和UDP的时候,我是加了双引号的,是因为到了uds中,就已经没有TCP和UDP的说法了,只有数据流(stream)和数据报(dgram)的区别。

数据流,意味着发给内核底层的数据就跟水进入水管一样,内核根本不知道什么时候是个头,没有明确的边界

数据报,可以类比为一件件快递进入传送管道一样,内核很清楚拿到的是几件快递,快递和快递之间边界分明

UDS在内核实现上对数据流和数据报做了区分实现。

但除此之外,两者从代码大逻辑上没太大区别。

比如,以前在走网络协议栈的时候,TCP比UDP多了不少重传和拥塞控制这一大堆可靠性保证的代码。现在用了uds后不走网络协议栈了,就算选了"TCP"也不再有这些繁琐的逻辑。

为什么呢?

以前做各种可靠性保证的逻辑,目的就是为了让发送端的数据能安然无恙的抵达接收端接收缓冲区

而用了UDS后,都直接将发送端的数据塞到接收端的接收缓冲区了,那还要啥可靠性保证代码。

还有哪些使用场景

基于uds的使用场景有很多。我再举个例子,假设你在一个中台性质的部门打工,你需要开发一个平台性质的功能,假设这个功能是视频转码,这时候业务方需要在里面开发定制化的功能插件,比如A业务方需要在转码的过程中加个自己的logo,B业务方想要加个定制水印。

你会怎么办?

我们最容易想到的方案是,在一个进程内,每个业务方一个插件,平台代码将这些插件类串联起来,这样功能肯定就能跑起来了。

但问题就来了,这个插件类,是你来开发还是业务方来开发。

  1. 如果是你来开发,你就沦为了业务方的小外包,业务方要改点需求你就得跟着干活,岂不累死?
  2. 如果是业务方来开发,在一个进程内,如果A业务方写了个空指针,进程就得崩,影响了B业务方,你的服务崩了,这口锅你背不背?

那么问题就来了,有没有更好的方案?

有,多进程加uds,通过多进程的方式将代码串联起来,像下面这样。

多个业务方写的插件都以进程的方式启动,你写的平台代码只负责将这些进程串联起来,其中一个进程挂了并不影响其他进程。你也不需要被业务方催着改代码,利用uds做进程间通信,服务性能也比用回环地址的时候要更好。

岂不美哉?

那么回到文章的标题,用了socket就一定要走网络协议栈吗?

答案是不一定,比如unix domain socket就根本不走网络协议栈。

总结

  1. unix domain socket也是socket的一种,只不过它用于本机内的进程间通信。
  2. 跟走localhost通信比起来,用unix domain socket通信省下了网络协议栈的逻辑,因此性能会更好。
  3. 这篇文章通过两个案例来说明unix domain socket的应用场景,我相信你未来一定能用上。

最后留个问题。

文章开头的案例中,以前python服务作为一个pod部署在k8s中的时候,由于暴露的是python服务的端口,因此python服务挂了,k8s能感知到并重新启动。

但现在加入golang服务作为sidecar之后,暴露的是golang服务的端口,如果此时python服务挂了,k8s是不是就感知不到这一事件了?那该怎么办呢?

#网络#
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
小白debug
作者分享
出现 timeout 问题时如何正确甩锅
40
之前部署博客的时候,需要先搞一台云服务器,搭了nginx后还要整个https证书,各种配置,阿里云上面的免费证书还是一年过期,过期还得重新手动申请。 买的服务器带宽是真·小水管。ping一下域名直接暴露云服务器ip,一挨打就得趴下。想接个迷人又危险的cdn隐藏下真实ip,又怕钱包扛不住爆刷。 就挺难受的。 最近尝试了下cloudflare,免费版完全够用,而且一次性解决了上面的问题。对我来说,最香的是http域名直接接入后就能自动生成https证书并访问,再也不用一年一整了。 【不是广告,我还不配。。。】
44
我也工作好些年了,但一直都是做后端相关的工作,前端几乎0基础。[捂脸][捂脸] 最近几个月在接触学习前端相关的内容,看了不少vue的课程。 但很多课程都是讲的很基础,做不了项目。或者版本滞后,学起来有些脱节。😂 于是尝试跟着鱼总最近的OJ项目从0开始敲代码,确实全程干货,很适合新手上路[机智][机智] “学到东西不能输出为项目那等于白学”,于是倒腾了个小的demo项目。【避免广告嫌疑,就不发链接了】 虽然还很demo,但能肉眼看到自己学到新东西就已经很开心了。 真心推荐刚进星球或者在星球里长期观望的老哥们跟着鱼总的项目学一遍[奸笑][奸笑]
28
通过一些案例重新学习golang net/http
13
#自我介绍# 小白 大家好,很高兴认识大家。我是golang后端开发,业余时间搞搞抖音/B站/公众号「小白debug」,大家叫我小白就好,比较社恐,接下来向各位大佬多多学习。 个人情况: 之前在盛大游戏做过游戏,在字节做的golang后端开发,资深搬砖打工人。 有机会可以跟大家聊聊面试跳槽,职业发展这块内容。
30
下载 APP