从协议说起
为了把世界上所有不同类型的计算机和移动设备都连接起来,就必须规定一套全球通用的协议标准。为了网络互联这个目标,互联网协议簇(Internet Protocol Suite)就随之发展而成,互联网协议簇就是通用协议标准。
因为互联网协议包含了很多种协议标准,并不仅仅只是TCP和IP协议,只是这两个相对来说非常重要,所以互联网通信协议也就简称TCP/IP协议。
网络通信协议分层模型
在网络分层模型中,有两种模型:
- TCP/IP 标准:1970年被发明。它提出了分层概念,将网络通信分为四层,分别是数据链接层、网络层、传输层、网际应用层。
- OSI:全称叫开放式系统互联通信参考模型,是国际标准,用来统一各种网络协议,始于上世纪70年代后期。OSI 模型分成了七层,分别为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
两者的关系就好比手机系统的巨头是 iOS 和 Android,它们制定了标准,因为话语强,所以国际标准以它们为核心再添加指定一些其他标准。其中 TCP/IP 就是网络分层模型中的老大,OSI 就像英国女王那样,是皇室象征,是国际标准。
端口
1. 什么是端口?
大白话来讲,端口就像是一个房子的们,是出入这间房子的必经之路,没有其他路。那什么是进程?进程就是正在进行的程序,当我们打开任务管理器,可以看到进程。进程与进程之间需要交流,交流需要收发网络数据,那么就需要端口。端口可以有65536(2^16)个。
2. 端口号
那么既然有这么多端口,那操作系统为了统一管理,就会对端口进行编号,这就是端口号。
端口是通过端口号来标记的,端口号只有整数,范围【0, 65535】
那么端口号是如何分配的呢?端口号不是随意使用的,而是按照一定的规定进行分配。知名端口也就是众所周知的端口号,范围为【0,1023】,它是计算机内部的保留端口,你写的应用最好不要乱用。动态端口是不固定分配某种服务的端口号,动态分配范围为【1024,65535】,你可以随便玩了。
动态分配是只当一个系统进程应用进程需要网络通信时向主机申请一个端口,主机从可用端口号中分配一个供它使用,当这个进程结束时,其占用的端口号同时释放。
TCP
上文提到,进程和进程间的通信需要端口,那么机器和机器间的通信就需要IP:Port。假设我们需要在A电脑的进程发一段数据到B电脑的进程,那么代码我们会使用socket实现。基于Socket的通信方式就2种:TCP和UDP。下图简单的告诉你这2个瓜的区别:突出一点就是TCP可靠,UDP不可靠。早年小马哥的QQ就是基于UDP实现的聊天室。
创建一个连接的代码就下面这样写:
fd = socket(AF_INET,SOCK_STREAM,0);
其中SOCK_STREAM代表的是采用字节流传输数据,其实就是TCP协议。
通过socket调用,我们就创建了一个句柄,其实就是一个32位的整数,通过它我们就可以欢乐地读和写了,比如用bind()绑定IP端口,用connect()发起建连。这里TCP的三次握手我们就不展开了哈,因为它不是我们本文的重点。
成功建立一个fd到fd的链接后,我们就可以使用send()发送数据,recv()接收数据。但是这样一个赤裸裸的TCP连接,可以收发数据,但是远没有这么简单?为啥呢,你就继续往下面读......
TCP是有三个特点,面向连接、可靠(超时和重传保证)、基于字节流。
TCP是什么
这三个特点概括的是非常精辟,但是每个特点都够我们喝一壶的,而今天我们需要关注的是基于字节流这一点。
所谓流就是一个传送带,上面的货物代表的就2个不同类型的包裹:0和1,随着传送带的传输,源源不断的0和1排队从起始端传输到目的端。
你会发现,0和1排队流式传输,居然没有任何边界。举个例子当我们用TCP发送"夏洛"和"特烦恼"的时候,接收端收到的就是"夏洛特烦恼"。你这时有什么想法?
接收端没办法知道你要具体表达什么意思,它没法根据这5个字给你断句。你是想要表达"夏洛"+"特烦恼"还是"夏洛特"+"烦恼"呢?更搞笑的是,如果传送的中途,0和1被掉包了(拜占庭问题),那你还会收到“特烦恼夏洛”。。。
聊这个案例就是为了告诉大家,赤裸裸的TCP是不能直接拿来用的,你需要在这个基础上加入一些自定义的规则,用于区分消息边界,还要有些机制保障类似“我爸是李刚”而不是“我是李刚爸”,这就乱套了。于是我们会把每条要发送的数据都包装一下,比如加入消息头,消息头里写清楚一个完整的包长度是多少,根据这个长度可以继续接收数据,截取出来后它们就是我们真正要传输的消息体。
而这里头提到的消息头,还可以放各种东西,比如消息体是否被压缩过和消息体格式之类的,只要大家都认这个约定,我们就可以欢乐的你组装我拆包,快递就会这样收的不亦乐乎了,这就是所谓的协议。
TCP只是传输的时候大家对齐了的一个标准,至于我想发什么快递给你,我需要自己先打包在标上一个标签,这个标签就是一个附加的头。有了这样一个自定义的头部能力,我们就可以尽情地发不同的快递了。他们可能有区别,但原理都类似。
于是基于TCP,就衍生了非常多的协议,比如HTTP和RPC。
HTTP vs RPC
除了开篇的7层分类,我们还有简单的4层分类协议,如下:
TCP是传输层的协议,而基于TCP加上不同头的包装之后,就衍生出了HTTP和各类RPC协议,它们都只是定义了不同消息格式的应用层协议而已。
HTTP协议(Hyper Text Transfer Protocol),又叫做超文本传输协议。我们浏览一个网站,分享一个链接,都是走的这个协议。我们客户端发送一个Request, 服务端就响应一个Response。
RPC(Remote Procedure Call),远程过程调用。它本身并不是一个具体的协议,而是一种调用方式。
举个例子,我们在一台机器上调用一个本地方法。
res = CallLocalFunc(req)
如果这个方法本地只是一个空壳子,它的真正实现是在另一个台机器上,那咋玩呢?那如果远端服务器暴露出来的一个方法CallremoteFunc,回到我们开篇的IP和端口,如果2台机器都知道彼此的IP和端口,那么我们像调用本地方法那样去调用另外一台机器暴露的函数而屏蔽掉一些网络细节,那岂不美哉?
res = CallremoteFunc(req)
基于这个思路,国内外的几个大牛公司就实现了非常多款式的RPC协议,比如比较有名的Google 的gRPC,Facebook的thrift 还有baidu的brpc
虽然大部分RPC协议底层都使用TCP,但根据我们前文的拆包我们也可以知道不是所有的RPC都需要支持TCP,比如改用UDP或者HTTP,其实也是OK的。
那么既然有HTTP协议,为什么还要有RPC?
其实历史是不断发展的,70年代提出TCP协议后,90年代才开始流行HTTP。而直接使用裸TCP会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有80年代出来的RPC。
所以我们该问的不是既然有HTTP协议为什么要有RPC,而是为什么有RPC还要有HTTP协议。
那么既然有RPC协议,为什么还要有保留HTTP呢?
现在手机和电脑上装的各种联网软件,比如XX三件套,卡脖子的半导体软件,工程制图软件等等,它们都作为客户端(client)需要跟服务端(server)建立连接收发消息,此时都会用到应用层协议,在这种client/server (c/s)架构下,它们可以使用自家造的RPC协议,因为它只管连自己公司的服务器就ok了。
但有个大众的软件不同 - 浏览器(browser),这哥们也有很多种内核,比较有名气的就是IE和chrome,不管他们心是蓝色的还是红色的,它们要既能访问自家公司的服务器(server),还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP就是那个时代用于统一 browser/server (b/s) 的协议。
也就是说在多年以前,HTTP主要用于b/s架构,而RPC更多用于c/s架构。但现在其实已经没分那么清了,b/s和c/s在慢慢融合。很多软件同时支持多端,比如头条、知乎、小红书等等,既要支持网页版,还要支持手机端和PC端,如果通信协议都用HTTP的话,那服务器只用同一套就够了。而这些业务系统的内部,为了让咱们耍玩时,如丝滑般的顺畅,内部通信就要足够的块,不管是表格、图片、还是其他五花八门的数据类型,都要通信,这时RPC就开始退居幕后,再各个微服务之间通信。
那这么说的话,HTTP是不是万能的呢,还用什么RPC?它们区别是啥?
要说区别,就得掂量下他们的能力啦
服务注册和发现
如果要向某个服务器发起请求,你得先建立一条连接(socket),而建立连接的前提是,你得知道IP地址和端口(我们前文提到的2个东东)。这个找到服务对应的IP端口的过程,其实就是服务发现。
服务发现的实现方式也比较多。在HTTP中,比较知名的就是配置一个DNS路由表,给我一个网址,我就可以通过通过这个表(DNS服务)去解析得到它背后的IP地址,默认80端口,配置表的时候也可以自己定义。
而RPC的话,就有些区别,一般会有专门的中间服务去保存服务名和IP信息,比如consul、istio、k8s-discovery或者etcd等等。想要访问某个服务,就去这些中间服务去获得IP和端口信息。由于dns也是服务发现的一种,所以也有基于dns去做服务发现的组件,比如CoreDNS。
可以看出服务发现这一块,两者是有些区别,但都能支持,棋逢对手。
连接方式
以开山大作的HTTP1.1协议为例,它是一个长连接的(对比的就是断连接,一个请求结束,咱们就Bye-Bye了)其默认在建立底层TCP连接之后会一直保持这个连接(keep-alive, 就是每隔个时间咱们电话私聊下, C:你还在吗?,S: 还在的),之后的请求和响应都会复用这条连接。
而RPC协议,也跟HTTP类似,也是通过建立TCP长链接进行数据交互,但不同的地方在于,RPC协议一般还会在客户端建立一个连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。切记不要占着不用,一直吃着锅里还看着碗里的,这样你就会Game Over了。
由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给HTTP加个连接池,比如Java/go都是这么干的。
消息(头+身体)
不管是7层还是4层,各层的消息传输都是都是消息头header和消息体body:header是用于标记一些特殊信息,其中最重要的是连接端的一些信息,比如IP:Port,每一条TCP连接都是由5元组标记(Srcip, Srcport, protocol, Dstip, DstPort)。body则是放我们真正需要传输的消息内容,而这些内容只能是二进制01串(链路层的比特流,字节跳动后还可以有个比特跳动公司),毕竟计算机只认识这玩意。所以TCP传文字、图片和语言都可以,因为都可以转成编码再变成01串。但结构体呢,我们得想个办法将它也转为二进制01串,这样的方案现在也有很多现成的,比如xml, json, protobuf等。结构体呢计算机不认识,我们需要把它转为二进制数组,这个过程就叫序列化,反过来将二进制数组复原成结构体的过程叫反序列化。
对于主流的HTTP1.1,虽然它现在叫超文本协议,支持音频视频,但HTTP设计初是用于做网页文本展示的,所以它传的内容以字符串为主。header和body都是如此。在body这块,它使用json来序列化结构体数据。
打开浏览器,再打开一个页面,在console总可以看到一个请求的详细信息:
可以看到这里面的内容非常的多,没刷新下,页面都包含这么写头,非常浪费带宽。最明显的,像header里的那些信息,如果我们事先约定好就不需要每次都把头中重复的字段来回传,包括数据格式的类型。
而RPC就比较聪明,因为它的数据结构比较灵活,反正序列化+反序列化就可以搞定,选择体积更小的protobuf或其他序列化协议去保存结构体数据更高效,同时也不需要像HTTP那样考虑各种浏览器行为,比如繁琐的1XX、2XX、3XX和讨厌的404。因此性能也会更好一些,这也是在公司内部微服务中抛弃HTTP,选择使用RPC的最主要原因。
上面说的HTTP特指的是现在主流使用的HTTP1.1,HTTP2/HTTP3已经做了很多改进,性能可能比很多小规模的RPC还要好,甚至连gRPC底层都直接用的HTTP2,不同的RPC还内置一个HTTP的服务,比如Brpc
那么你又会问?既然有了HTTP2,还要有RPC协议?
这个我只能说咱们时代还在进度,过去造的轮子不可能给扔掉,那都是花了的。
对比小结
传输协议
- RPC:可以基于TCP协议,也可以基于HTTP协议
- HTTP:基于HTTP协议
传输效率
- RPC:使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
- HTTP:如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理
性能消耗
- RPC:可以基于thrift实现高效的二进制传输
- HTTP:大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能
负载均衡
- RPC:基本都自带了负载均衡策略
- HTTP:需要配置Nginx,HAProxy来实现
服务治理
- RPC:能做到自动通知,不影响上游
- HTTP:需要事先通知,修改Nginx/HAProxy配置
使用场景
RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。