socket会话是如何进行的?

不管是TCP通信还是UDP通信,会话的端点一定由IP+端口(套接字)构成,通信时也是根据这个来定位对方,虽说如此,架设应用的时候需要考虑的地方往往没有这么简单。

应用跨过NAT设备时,只要有一点点网络基础,就会知道需要在设备上做端口映射将内网服务器的服务发布出来。比如,外网接口上,发布一个内网的服务(192.168.10.10的TCP8080)到外网的TCP80(假设外网地址是1.1.1.1),这时对客户端来讲,只需要访问1.1.1.1:80,请求会被转发到真实服务器192.168.10.10的TCP8080上。

但是真实服务器上,这个应用应该怎么部署呢?好像这个问题听着不太像一个值得关注的问题,该装什么依赖就装什么依赖,该装什么服务端程序就装什么服务端程序,或者装上web容器,startup脚本一跑,应用一启,齐活。流程看上去简单到根本谈不上有什么流程,但这中间却实实在在存在着一些弯弯绕绕的细节。

先发布一个服务看看。

暂时先把场景从web应用换成一个socket应用,比如Redis。安装好redis-server应用后,只要在本机通过redis-cli命令连接服务器就可以登陆进去了,但是这中间到底哪些设置在起作用,使得redis-cli命令能连上去?它们之间的通信细节是怎样的?从另一台服务器连接过来能正常连通吗?

通过查询可知,redis的默认配置是监听在127.0.0.1,从外部无法访问。

127.0.0.1代表什么?

很多人,即使对网络方面的概念没有了解过,也知道127.0.0.1就是本地地址,但其实这个说法并不准确,准确的说法是,127.0.0.1是一个专门用于回环接口(loopback)的保留地址。

  • 回环接口专用的保留地址并不止127.0.0.1,事实上它包含了127.0.0.0/8整个网段;
  • 回环接口完全可以使用任意其它有效IP,127.0.0.0/8只是RFC里特地保留用于回环地址的网段;
  • 不同回环地址之间并不互通。
(随意ping一个127开头的地址)

回环地址又是什么东西?

好像越扯越远了,回环地址即回环接口(loopback)的地址。

回环接口不同于真实网卡所代表的物理接口,它是一个软件层面的虚拟接口,发送到回环接口的数据,左手进,右手还从这个接口出来,正因为如此,外部将无法直接访问这个地址,因为从外部来看,并没有一个可达的真实存在的直连链路到达这个接口。相反地,真实网卡就是一个可直连的物理接口,真实存在。

当然,可直达并不意味着可以连通,从网络层、会话层、直到应用层之间,协议协商到哪一层,哪一层才可以真正的“连通”,比如:

  • 虽然用同一根网线连接了两台设备,但IP不在同一个网段,那么在链路上这两台设备是“连通”的,但在网络层上这两个网段是不通的;
  • 进一步,两台设备IP也在同一层,也可以互相PING通,但是A的服务并没有开启,从B访问A的服务,将无法建立传输层连接,即表现为从底层一直到网络层都是“连通”的,但是在传输层不“连通”;
  • 再进一步,A的服务也启动了,但是TCP连接建立以后,服务端与客户端通全部是加密过的,服务端发过来的信息客户端完全无法理解,形成鸡同鸭讲的局面,这就是底层可以“连通”,应用层无法“连通”。
  • * 以上流程未考虑防火墙的影响;
  • * OSI参考模型的七层划分单纯是为了分层而分层,五六层与七层的分层边界极为模糊,不用太刻意去理解,更具实际意义的是TCP/IP的分层模型。

再回到redis-server,刚才查下来,它的发布地址是127.0.0.1,按上面的解释,这个服务器将只有本机可以访问,其它主机无法连接,截图里测试的结果也证实了这一点。

对了,还有localhost。

localhost准确地讲,确实是地址,但不是IP地址,它只是一个主机名(hostname),不过约定俗成,大家都把localhost这个“域名”解析到127.0.0.1(通过本地hosts文件),访问localhost时,会经过解析动作,才真正和127.0.0.1建立联系,所以,在hosts文件里写上一条127.0.0.1 www.abc.com,效果其实和localhost一样。如果某个服务禁止域名解析,localhost就没法和127.0.0.1划等号了。

没结束,还有一个东西也和“本机”有关。

Linux服务配置里经常会用到一个叫.sock的文件,比如mysql的mysql.sock,php-fpm的php-fpm.sock,supervisor的supervisor.sock……它其实是一种叫Unix domain socket的东西,不光看着名字里也带socket,作用也是一样,也是用于通信,不同的是它是基于IPC的进程间通信,而不是基于TCP/IP的网络通信。

比如,在mysql服务器本机使用mysql命令的时候,往往就是用它。

另一个露面比较多的场景就是nginx与php-fpm之间的fastcgi通信,要么让php-fpm在tcp/ip上监听,要么让它在php-fpm.sock上监听;同时,nginx的fastcgi请求也要发到相同的位置,即两边都是tcp/ip,或者两边都是.sock,甚至在nginx的默认配置里,也能经常看到为.php预留的配置段。

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    也可改为
        #    fastcgi_pass   unix:/run/php/php7.4-fpm.sock;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

怎么让别人也能访问到?

一句话:把服务器发布到本机网卡上。

通过上面所讲的回环接口/物理接口的区别可以知道,要真正访问到服务器上的服务,需要客户端到服务器的每一层协议都能“连通”,但发布到127.0.0.1所代表的回环接口,客户端到服务器在最底层就不通的。所以必须发布到别人能访问到的接口上,即本机物理网卡,同时防火墙放行对应的端口,即可让别人访问到服务。

还是用redis举例,之前bind 127.0.0.1的时候,外部主机无法访问,这次bind到192.168.2.22上再试试(这个时候127.0.0.1又不通了,因为服务没发布到回环接口上)。

[root@docker2 redis-6.0.5]# grep ^bind redis.conf
bind 192.168.2.22

多个网卡怎么办?

一句话答案:用哪个/哪些网卡提供服务,就Bind对应网卡上的那个/那些IP,或者直接Bind 0.0.0.0(请尽量不要将服务绑定到0.0.0.0)。

一个网卡就相当于服务器与外界交流的一个大门,来这间房子的道路有多条,对应就有多个大门,想在哪个大门接待访客,就让服务发布到哪个大门。

在开篇第一幅截图里,有一个监听地址是0.0.0.0:22的SSHD服务,这个0.0.0.0又是什么?0.0.0.0这个IP的掩码是0,非常特殊,通过0位长的掩码计算下来,其实它代表所有IPV4地址的总集。比如用在默认路由里,会将所有未能匹配到路由表条目的出站流量指到一个特定接口或者下一跳地址,用的就是0.0.0.0/0这个网段来进行地址匹配。也因为如此,默认路由的优先级要低于其它路由,因为它的匹配长度是0,只要有匹配长度大于0的路由条目,都会优先于默认路由选择。

既然它代表所有IPV4地址,那么再回头看Bind 0.0.0.0这条配置,就能明白它表示在所有地址上监听,再和本机IP取个交集,Bind 0.0.0.0就代表在所有接口上监听了。也同样是因为这个原因,0.0.0.0并不是一个特定的IP地址,只能作为监听地址使用,不能使用在客户端主动连接的地址上。

需要特别注意的地方是:

  • 如果服务只供本机使用,请Bind 127.0.0.1;
  • 如果服务器对外,请Bind 具体的IP;
  • 多网卡场景尽量不要使用Bind 0.0.0.0的配置。

这是因为Bind 127.0.0.1时,外部无法访问,可避免由于不必要的暴露带来的安全威胁,尤其是数据库、缓存之类的服务,一旦暴露,将变成一颗威力十足的不定时炸弹。

稍微复杂点的服务架构,数据库和应用会分离开,但往往会处在同一个局域网中,这时不可避免地会需要对外发布数据库服务,这种情况下,建议通过网段隔离的方式将应用和数据隔离到不同网络,条件允许的情况下,数据库服务器可以直接与外网断开。

端口冲突导致应用无法启动

这是个很常见的故障了,如果启动服务碰到Address already in use之类的提示,检查一下是不是有其它程序有占用了端口,有的时候服务未正常关闭也会有残留进程占用端口导致新启动服务失败的情况发生。

相同的端口,如果所使用的IP地址不同,仍然可以正常绑定,比如服务器有ip1, ip2, ip3,完全可以在ip1:8080, ip2:8080, ip3:8080, 127.0.0.1:8080上发布四个不同的服务,不过如果应用程序使用0.0.0.0进行绑定,将直接占用掉所有可用IP,使得其它ip包括127.0.0.1的对应端口都无法被其它程序使用,这也是使用0.0.0.0的弊端。