2023年8月1日发(作者:)
深⼊理解什么是端⼝(port)每当看到有⼈的简历上写着熟悉 tcp/ip, http 等协议时, 我就忍不住问问他们: 你给我说说, 端⼝是啥吧! 可惜, 很少有⼈能说得让⼈满意... 所以这次就来谈谈端⼝(port), 这个熟悉的陌⽣⼈.在此过程中, 还会谈谈间接层, naming service 等概念, IoC, 依赖倒置等原则以及 TCP协议的⼀些重点知识.常见端⼝在我们的⽇常开发过程中, 特别是后端的开发⼈员, 即便他没有真正理解端⼝的细节, 他还是会听过见过各类的端⼝, 这个东西⼏乎⽆处不在, ⽐如:mysql 缺省⽤的 3306 端⼝,redis 的 6379 端⼝,tomcat 默认⽤的 8080 端⼝,ssh ⽤的 22 端⼝,等等...当然我们最关注的还是 web 相关的端⼝, 涉及的主要为 80 和 443 两个端⼝, 下⾯就来重点说说.端⼝是必须的吗?在本地 web 开发调试过程中, 我们可能都碰到过端⼝, ⽐如或许是/最著名的 8080 端⼝, ⼀般我们会这样去访问本地的 web 程序:localhost:8080但⼀旦 web 程序部署到了正式的⽹站中, 端⼝似乎就消失了, 正式的⽹址中就不需要端⼝了吗?答案是否定的, 在这⾥起作⽤的是缺省值.⽐如你访问我的⽹站: , 这个 url 中似乎没有端⼝, 但其实是有的, 它有⼀个默认值 443, 所以完整的形式实际是这样的::443.你可以通过 Chrome 的开发⼈员调试⼯具看到这⼀点:可以看到, ip 地址后⾯跟着⼀个 443如果你输⼊⼀个错误的端⼝, ⽐如 80, 像这样: :80, 结果就是⽆法访问.但是如果你改成 :80, 它⼜可以访问了.注意, 因为我服务器后台配置了 http ⾃动跳转 https 的 301 重定向, 所以最终浏览器会再次跳转到 :443.注意勾选 'Preserve log' 以保留⽇志, 可以看到第⼀个 80 端⼝的请求会被响应⼀个301 跳转, 并指⽰跳转⽬标, 也即是 Location 字段中的 https 请求, 浏览器接收此跳转指⽰并重新发起 https 请求, 也即是图中第⼆个 的请求. 所以地址栏最终还是会变成 https 的, 特此说明.此时如果你输⼊ :443, 它⼜不能访问了...那么原因是什么呢? 你找到规律了没有?注意⼀个是 http, ⼀个是 https.协议的缺省端⼝当你没有显式的在 url 中输⼊端⼝时, 浏览器实际上会根据所⽤的协议来为你指定⼀个缺省端⼝:如果是 http 协议, 就使⽤ 80 端⼝如果是 https 协议, 就使⽤ 443 端⼝如果你⾃⼰输⼊端⼝呢? 那就⽤你输⼊的端⼝, 你输⼊啥就是啥, 输错了, 访问不了那就是你的责任了, 谁让你瞎搞来着?本来不⽤你劳神的, 你偏要脱裤⼦放屁, 搞不好⾃然就是画蛇添⾜, 弄巧成拙了.⽐如上⾯的⽤了 http 却输⼊了 443, 或者⽤了 https 却输⼊了 80, 就⽆法成功访问了.另外, 如果你胡乱地输⼊⼀个⽐如 9527, :9527, ⾃然也是⽆法访问的, 原因也很简单, 因为我的服务器上根本没有在 9527 端⼝上进⾏监听.即便我有在 9527 端⼝上监听, 提供的也未必是 web 服务, 使⽤的协议可能既不是http, 也不是 https, 所以你⽤浏览器试图去访问也可能会碰壁的.当然了, 我是完全可以在服务器上的 9527 端⼝上再部署⼀个 web 服务的, ⽐如放⼀个 apache或 tomcat server 之类的 web server 监听在那个端⼝上, 再放通防⽕墙, 安全组之类的, 也是可以访问的. 只是我没有这么去做⽽已.那么为啥⼤家都不在那些奇奇怪怪的端⼝上提供 web 服务呢? 原因其实也很简单, 为了⽅便⽤户, 同时也减轻了⽤户的认知负担.其实关于⽤户, 你只要记住两点就好了:1. ⽤户是傻⽠2. ⽤户是懒汉深刻地理解了这⼀点, 你才可能成为⼀个好的程序员(包括但不限于产品经理, 设计师...)其实呀, 何⽌了省略了端⼝呀, 你看看现在的地址栏, 不但 http, https 这些协议省了, 最末尾的斜杠/ 省了, 甚⾄连 www 都省了...是的, 我也帮你们省了 www, 事实上你通过 / 也能访问到, 但如果通过 / 就能访问到, ⼜何苦去再去录⼊三个达不溜呢?必须得承认, 缺省的存在是有很⼤的帮助的, 这其实是进步; 但另⼀⽅⾯, 这些缺省有时也会给不明就⾥的开发⼈员带来了⼀些困惑, 好像端⼝不是必要的, 但其实不是这样的.为什么需要端⼝?那为什么⼀定要端⼝这个东西呢? 它到底起了什么作⽤, 想必很多同学想要了解, 下⾯就来说说为什么, ⽽⼀个⾸先需要了解的概念就是进程间通讯(所谓的 IPC(inter-process communication)进程间通讯(IPC)你在浏览器地址栏输⼊某个⽹站的域名, 然后回车, 就⽣成了⼀次请求, 然后服务器响应你的请求,浏览器再把结果渲染出来, 你就能最终看到到⼀个⽹页.如果你曾经 ping 过⼀个域名, ⽐如你现在 ping 我的域名 , 你就能得到⼀个 ip 地址,118.89.55.54:有了 ip, 浏览器⾃然就能找到我的主机, 但还是有个问题, 我的主机上运⾏着好多的进程, 好多的服务, 除了最常见的 web 服务, 我可能还有 ftp 服务, mysql 服务等等不⼀⽽⾜.简单地讲, 如果⼀个请求只有 ip 地址这⼀信息, 操作系统将不知道把这个请求交给哪个进程去处理, 如果是你来设计整个系统, 你想象⼀下, 是不是这样?如果你仅仅是输⼊域名, 经过 DNS 解析后, 只能得到⼀个 IP 地址.所谓的⼀次请求, 从⼀个⽐较底层的⾓度去看, 就是⼀次进程间的通讯.它可以是 navicat 客户端与 mysql 数据库服务的⼀次通讯, 也可以是 winScp 客户端与 vsftpdFTP 服务的⼀次通讯等等.以上⾯的具体为例, 可以说就是 Chrome 浏览器这个本地操作系统上的进程与我的服务器上的⼀个叫做 Nginx 的进程间的⼀次通讯.那么, 所谓的端⼝, 其实可以简单地视作为进程 ID.当然, 它与进程 ID 还是有不同的, 下⾯再分析, 或者⽬前你可以认为端⼝就是进程 ID的影⼦.也即是说, 如果仅有域名(ip), 是⽆法定位到⼀个进程的, 通讯的发起⽅不但需要给出 ip, 还需要给出端⼝, 只有这样, 服务器才能知道由哪个进程去响应.端⼝, ⼀个间接层那么问题⼜来了, 为什么引⼊端⼝, ⽽不是直接使⽤进程 ID 呢? 这个原因想想也不难明⽩, ⼤概有这么⼏点原因:1. 作为客户端⽆法知道服务端对应进程的 ID2. 服务端对应进程重启后 ID 会改变3. ⼀个⽹站的 web 进程 ID 是这个, 另⼀个⽹站的可能⼜是另⼀个⾃然, 原因是很多的, 我也是随便的列举了⼀些, 你或许还能想到更多. ⽽为了解决这些个问题, 就引⼊了端⼝这⼀间接层(indirection).计算机世界⾥有⼀句名⾔: 任何计算机问题均可通过增加⼀个间接(indirection)层来解决.(Any problem in computer science can be solved with another layer ofindirection. -- David Wheeler)这个名⾔其实还有后⾯⼀句: But what usually will create another problem.(但通常会带来另⼀个问题)这⾥所谓另⼀个问题, ⽐如它会使得层次结构复杂化, 交互效率下降等等. 当然了, 这就是架构师们要去权衡的问题了, 很多时候, 架构就是关于平衡的艺术. 打死都不肯引⼊任何的间接层, 这是⼀个极端; ⽽⼀上来就引⼊好多个间接层, 这⼜是另⼀个极端.如果没有这个间接层, 客户端要与服务端通讯, 就要知道服务端对应进程的 ID, 也即是客户端是依赖于服务端的:显然, 这种模式对于 web 这种⼀个服务端对应⼤量客户端访问的情形是极不适应的,你都不知道有谁可能会来访问你的⽹站! 你根本⽆法告诉它们.⽽有了端⼝这⼀间接层, 对于 web 的情形, 这种依赖被倒置了, 客户端总是把请求发送到 80(或443) 端⼝, 这些成为标准的⼀部分, 并要求服务端反过来去适应, 服务端去监听端⼝的通讯并处理, 变成了⼀种反向依赖.如果⼀个进程想要提供 web 服务, 它启动之后就要去绑定(binding) web 相关的端⼝,如果端⼝已经被其它进程绑定了(即所谓占⽤了), 就会绑定失败; ⼜或者被⾃⾝前⼀个未完全退出的进程占据着, 也会绑定失败, 在开发过程中你可能会遇到类似的问题, ⼀个 web 进程没有关闭, 你⼜试图启动另⼀个, ⽽两者都⽤了相同的端⼝, 就会产⽣冲突.并在其上持续的监听(listen), 同时在有请求到来的时候去响应(response). 这样⼀来, 进程 ID 的问题就消解了:这类似于⼀个接⼝回调, 浏览器只需要⾯向接⼝索取服务, ⽽⽆需知道接⼝服务的具体提供者, 这些细节被端⼝层所封装并隐藏起来了.端⼝这⼀间接层的存在解耦(decouple)了客户端与服务端之间的强依赖, 整个体系变得很灵活.可以把端⼝视作⼀般编程概念中的接⼝(interface), ⽽想 Nginx, apache, tomcat 等等可以认为是这个接⼝的不同实现(Implementation).端⼝与现实世界的⼀个类⽐为加深理解, 可以举⼀个现实世界中的例⼦. 相信⼤家都有过去市民中⼼办事的经历, ⽐如去办理居住证, 护照, 社保等等业务, 你通常会收到⼀个⼩纸条让你去某个窗⼝办理对应业务, 这个窗⼝其实就类似于端⼝了:⽐如 80 窗⼝就对应港澳台通⾏证业务那么你要办港澳台通⾏证, 你就奔向 80 号窗⼝就完了. 你不要去问门⼝保卫处的王⼤爷, 到底是哪位同志办理这个业务.今天可能是⼩明在办理, 隔了⼏天, ⼩明可能受伤了, 流⾎了, ⼜轮到⼩红在那⾥办理,⼜过段时间, ⼩红也出意外了, 流产了, ⼜轮到⼩张在办理, ⼜过段时间, ⼩张被发现在办理业务过程中徇私舞弊, 流放了...等等, 如果此时你的同事问你怎么办港澳台通⾏证, 你需要知道这些个⼈事变动的细节吗? 根本不需要呀, 你只需告诉他去 80 号窗⼝办理就好了...市民中⼼的整个体系, 会确保有个会办理这些业务的⼈员坐在那个窗⼝下⾯, 你唯⼀需要做的, 就是到那个窗⼝下请求服务即可.端⼝与名称服务(naming service)通过上⾯现实世界类⽐的例⼦, 对于端⼝的机制, 相信你已经理解得⽐较深⼊了. ⼴义上讲, 端⼝层也可以视作⼀个 naming service(名称服务), 这与⽐如 spring cloud 中的 eureka ⾥的机制本质上是⼀样的, 只是这个 name 就是⼀个抽象的数字, ⽐如 80. 80 就代表了⼀个 web 服务, Nginx之类的 web server 绑定并监听就相当于把⾃⾝提供的 web 服务注册于其上.DNS 域名系统其实也是 naming service, 你通过 这个名字(name), 就能获取到我所提供给你的⽹页服务.类似的还有 java ⾥的 JNDI 等, 把⼀个名字与⼀个服务关联起来, ⽐如⼀个名字就代表⼀个数据源(数据库连接)之类的.端⼝与 IoC(控制反转)⼴义上, 端⼝的上述机制也是控制反转(Ioc: Inversion of Control)思想的⼀种体现, 如果客户端需要知道服务端的进程 ID, 实际上就被服务端控制了, 毕竟我服务端在哪个 ID 上提供服务, 你就得把你的请求发到相应的 ID 上来;⽽有了端⼝这⼀中间层呢? 作为客户端, 总是把请求发到对应端⼝上, 并要求服务端绑定并监听那些端⼝以及作出响应, 你服务端是反过来被我客户端所控制, 我客户端发到哪个端⼝, 你服务端就要去相应端⼝上监听并响应.⼤家可以体会⼀下这种转变. 这种设计或思想在编程领域其实是特别重要的, 在很多其它地⽅都有体现.因为浏览器总是把 web 请求发到了 80 或 443 端⼝, 这就要求⼀个 web server 进程去监听这些端⼝. ⽐如在我的服务器上, web server 是 Nginx, 它启动之后就会去监听 80 和 443 端⼝, 任何想要访问我的主页的⼈, 并不需要知道我的 Nginx 进程 ID 是啥, 借助于端⼝这⼀间接层, 你就能够与我的 Nginx 进程通讯, 并获取你想要的东西.事实上你可以这么认为, 浏览器实际上只是在与端⼝通讯, 端⼝层再把这些请求委托(delegate)或代理(proxy)给相应的 web server 去处理, 端⼝的⾓⾊就是⼀个中间⼈,⼀个间接层.再论缺省端⼝现在, 我们应该明⽩了, 端⼝是必要的了, 当然, 对最终的⽤户来说, 则不需要知道这些实现的细节,对于他们, 应该遵循最⼩知识原则, 知道得越少越好.如果你⼀定要让⽤户在输⼊ url 的过程中输⼊端⼝, ⼜或者要输⼊个 www 等等, ⽤户就要给你扔过来'⼗万个为什么'了...为什么要加个 443?为什么不是 334, 443是啥意思?为什么⼀会⼉是 80, ⼀会⼉⼜是 443?为什么加个 www, 啥意思?为什么末尾还加个斜杠, 不加会死吗?...惹不起, 惹不起...还记得前⾯说的, ⽤户是笨蛋, ⽤户是懒汉吗?这⾥⼜要引⽤⼀句计算机世界的名⾔了: 程序员和上帝打赌要开发出更⼤更好连傻⽠都会⽤的软件, ⽽上帝却总能创造出更⼤更傻的傻⽠。⽬前为⽌,上帝赢了。Programmers are in a race with the Universe to create bigger and better idiot-proofprograms. The Universe is trying to create bigger and better idiots. So far theUniverse is winning.说句⼼⾥话, 很多时候, ⽤户能记住你的域名就阿弥陀佛了, 你就该烧⾼⾹了, 你还想⽤户记住你的端⼝, 真的想多了...另⼀⽅⾯, 说到这⾥我们应该也能明⽩了, 那就是理论上, web 服务实际上可以构建在任何端⼝之上. ⽐如在本地开发的时候, ⽤户只有你⾃⼰, 那当然你可以随便挑⼀个端⼝, ⽐如 8080, 只要⾃⼰知道就好了或顶多告诉另⼀个与你配合的前端同事.同理, 其它⾮ web 的服务, ⽐如 ftp 服务, 也不⼀定说⾮得在 21 端⼝上等等; mysql 服务的端⼝同样可以调整为 3306 之外的端⼝.⼜或者说, 你想提供⼀个服务, 但只想⼩范围内的⼈知道, 你可以挑⼀个很偏门的端⼝, 这样⼀般⼈只输⼀个域名就没法访问到你的服务了.⽐如有⼈想偷偷提供⼀些服务, 放⼀些⼴淫民群众喜闻乐见的⼩视频啥的...刑法警告,后果⾃负!! 别说我没有提醒你.端⼝与 TCP/UDP 协议前⾯⼀直在说, 什么 3306 端⼝, 80 端⼝, 443 端⼝, 其实严格来说, 端⼝是分 TCP 端⼝和 UDP 端⼝的, 不过多数时候遇到的都是 TCP 端⼝, 但 TCP 80 端⼝和 UDP 80 端⼝是不同的端⼝.UDP 的 80 端⼝, 包括 443 端⼝其实被保留了, ⽬前的 http 协议只构建在 TCP 协议之上.当然, 理论上讲, 在 UDP 上构建 http 也不能说就完全不⾏, 毕竟, ⽆论 UDP 还是 TCP都是构建在 IP 协议之上, 总之呢, 计算机的世界没什么是不可能的, ⽽且似乎真有⼈在做这些尝试, 不过这就属于两⼩母⽜对屁股--⽐较⽜逼的范畴了, 深⽔区了, 咱也不懂, 不多说了.还有⼀点, 对于进程间的端⼝通讯, 实际上是对称的, 也即是说, 服务器的响应也是先回到⼀个客户端的端⼝上.如果你⽤ Windows 10 系统, 可以在 任务管理器 > 性能 > 打开资源监视器 > ⽹络 >TCP 连接, 点击下远程端⼝可以按照从⼩到⼤排列, 通常就可以看到 443 的相关连接了, 可以看到左边有⼀栏本地端⼝, ⼀个 TCP 连接总是有⼀个远程端⼝, ⼀个本地端⼝:当发起⼀个 TCP 连接时, 客户端⾸先⾃⼰先随机挑选⼀个没有被使⽤的端⼝作为服务器响应的接收端⼝, ⽐如 38672. 在⼀个 TCP 的包⾥, ⽆论是握⼿包还是后续的数据包, 包头部分最重要的两个字段, ⼀个就是源端⼝(source port), ⽐如 38672; 另⼀个就是⽬标端⼝(destination port), ⽐如 80, 或者 443.可以这样看, 服务器的响应也是先回到源端⼝, ⽐如 38672 上, 源端⼝再转给最终的进程, ⽐如浏览器.⽽对于⼀个 IP 包, 同样的, 包头部分最重要的两个字段, ⼀个就是源IP(source IP); 另⼀个就是⽬标 IP(destination IP).⽽ TCP 包会作为 IP 包的数据包被打包到 IP 包⾥⾯, 也⼀个 IP 包⾥其实包含了 IP +端⼝.IP 加端⼝再加上端⼝与进程间的关联, 分属两个不同主机间的进程就能通过 TCP(UDP)/IP 协议愉快地进⾏进程间的通讯(IPC)了.当然了, 同⼀个主机间的进程也同样可以利⽤这套机制. 但同⼀个主机间还可以有其它选择, 这个具体看各个操作系统是否提供相关机制及⽀持. ⽽ TCP/IP 属于⼴泛应⽤的标准协议, 从⽽得到了⼴泛⽀持.因为篇幅关系, 关于这样 TCP 协议等的细节, 以及包括 Socket, 连接等概念, 以及虚拟主机, 反向代理等等就不再展开去说, 如果你感兴趣, 欢迎留⾔, 后续会考虑再写⼀些⽂章去介绍.同样因为篇幅的原因以及同时我也不是计算机⽹络及协议⽅⾯的专家, 关于端⼝⽅⾯的, 如果有什么说得不到位, 或不正确的地⽅, 欢迎留⾔指正, 关于端⼝⽅⾯的介绍就到这⾥.
2023年8月1日发(作者:)
深⼊理解什么是端⼝(port)每当看到有⼈的简历上写着熟悉 tcp/ip, http 等协议时, 我就忍不住问问他们: 你给我说说, 端⼝是啥吧! 可惜, 很少有⼈能说得让⼈满意... 所以这次就来谈谈端⼝(port), 这个熟悉的陌⽣⼈.在此过程中, 还会谈谈间接层, naming service 等概念, IoC, 依赖倒置等原则以及 TCP协议的⼀些重点知识.常见端⼝在我们的⽇常开发过程中, 特别是后端的开发⼈员, 即便他没有真正理解端⼝的细节, 他还是会听过见过各类的端⼝, 这个东西⼏乎⽆处不在, ⽐如:mysql 缺省⽤的 3306 端⼝,redis 的 6379 端⼝,tomcat 默认⽤的 8080 端⼝,ssh ⽤的 22 端⼝,等等...当然我们最关注的还是 web 相关的端⼝, 涉及的主要为 80 和 443 两个端⼝, 下⾯就来重点说说.端⼝是必须的吗?在本地 web 开发调试过程中, 我们可能都碰到过端⼝, ⽐如或许是/最著名的 8080 端⼝, ⼀般我们会这样去访问本地的 web 程序:localhost:8080但⼀旦 web 程序部署到了正式的⽹站中, 端⼝似乎就消失了, 正式的⽹址中就不需要端⼝了吗?答案是否定的, 在这⾥起作⽤的是缺省值.⽐如你访问我的⽹站: , 这个 url 中似乎没有端⼝, 但其实是有的, 它有⼀个默认值 443, 所以完整的形式实际是这样的::443.你可以通过 Chrome 的开发⼈员调试⼯具看到这⼀点:可以看到, ip 地址后⾯跟着⼀个 443如果你输⼊⼀个错误的端⼝, ⽐如 80, 像这样: :80, 结果就是⽆法访问.但是如果你改成 :80, 它⼜可以访问了.注意, 因为我服务器后台配置了 http ⾃动跳转 https 的 301 重定向, 所以最终浏览器会再次跳转到 :443.注意勾选 'Preserve log' 以保留⽇志, 可以看到第⼀个 80 端⼝的请求会被响应⼀个301 跳转, 并指⽰跳转⽬标, 也即是 Location 字段中的 https 请求, 浏览器接收此跳转指⽰并重新发起 https 请求, 也即是图中第⼆个 的请求. 所以地址栏最终还是会变成 https 的, 特此说明.此时如果你输⼊ :443, 它⼜不能访问了...那么原因是什么呢? 你找到规律了没有?注意⼀个是 http, ⼀个是 https.协议的缺省端⼝当你没有显式的在 url 中输⼊端⼝时, 浏览器实际上会根据所⽤的协议来为你指定⼀个缺省端⼝:如果是 http 协议, 就使⽤ 80 端⼝如果是 https 协议, 就使⽤ 443 端⼝如果你⾃⼰输⼊端⼝呢? 那就⽤你输⼊的端⼝, 你输⼊啥就是啥, 输错了, 访问不了那就是你的责任了, 谁让你瞎搞来着?本来不⽤你劳神的, 你偏要脱裤⼦放屁, 搞不好⾃然就是画蛇添⾜, 弄巧成拙了.⽐如上⾯的⽤了 http 却输⼊了 443, 或者⽤了 https 却输⼊了 80, 就⽆法成功访问了.另外, 如果你胡乱地输⼊⼀个⽐如 9527, :9527, ⾃然也是⽆法访问的, 原因也很简单, 因为我的服务器上根本没有在 9527 端⼝上进⾏监听.即便我有在 9527 端⼝上监听, 提供的也未必是 web 服务, 使⽤的协议可能既不是http, 也不是 https, 所以你⽤浏览器试图去访问也可能会碰壁的.当然了, 我是完全可以在服务器上的 9527 端⼝上再部署⼀个 web 服务的, ⽐如放⼀个 apache或 tomcat server 之类的 web server 监听在那个端⼝上, 再放通防⽕墙, 安全组之类的, 也是可以访问的. 只是我没有这么去做⽽已.那么为啥⼤家都不在那些奇奇怪怪的端⼝上提供 web 服务呢? 原因其实也很简单, 为了⽅便⽤户, 同时也减轻了⽤户的认知负担.其实关于⽤户, 你只要记住两点就好了:1. ⽤户是傻⽠2. ⽤户是懒汉深刻地理解了这⼀点, 你才可能成为⼀个好的程序员(包括但不限于产品经理, 设计师...)其实呀, 何⽌了省略了端⼝呀, 你看看现在的地址栏, 不但 http, https 这些协议省了, 最末尾的斜杠/ 省了, 甚⾄连 www 都省了...是的, 我也帮你们省了 www, 事实上你通过 / 也能访问到, 但如果通过 / 就能访问到, ⼜何苦去再去录⼊三个达不溜呢?必须得承认, 缺省的存在是有很⼤的帮助的, 这其实是进步; 但另⼀⽅⾯, 这些缺省有时也会给不明就⾥的开发⼈员带来了⼀些困惑, 好像端⼝不是必要的, 但其实不是这样的.为什么需要端⼝?那为什么⼀定要端⼝这个东西呢? 它到底起了什么作⽤, 想必很多同学想要了解, 下⾯就来说说为什么, ⽽⼀个⾸先需要了解的概念就是进程间通讯(所谓的 IPC(inter-process communication)进程间通讯(IPC)你在浏览器地址栏输⼊某个⽹站的域名, 然后回车, 就⽣成了⼀次请求, 然后服务器响应你的请求,浏览器再把结果渲染出来, 你就能最终看到到⼀个⽹页.如果你曾经 ping 过⼀个域名, ⽐如你现在 ping 我的域名 , 你就能得到⼀个 ip 地址,118.89.55.54:有了 ip, 浏览器⾃然就能找到我的主机, 但还是有个问题, 我的主机上运⾏着好多的进程, 好多的服务, 除了最常见的 web 服务, 我可能还有 ftp 服务, mysql 服务等等不⼀⽽⾜.简单地讲, 如果⼀个请求只有 ip 地址这⼀信息, 操作系统将不知道把这个请求交给哪个进程去处理, 如果是你来设计整个系统, 你想象⼀下, 是不是这样?如果你仅仅是输⼊域名, 经过 DNS 解析后, 只能得到⼀个 IP 地址.所谓的⼀次请求, 从⼀个⽐较底层的⾓度去看, 就是⼀次进程间的通讯.它可以是 navicat 客户端与 mysql 数据库服务的⼀次通讯, 也可以是 winScp 客户端与 vsftpdFTP 服务的⼀次通讯等等.以上⾯的具体为例, 可以说就是 Chrome 浏览器这个本地操作系统上的进程与我的服务器上的⼀个叫做 Nginx 的进程间的⼀次通讯.那么, 所谓的端⼝, 其实可以简单地视作为进程 ID.当然, 它与进程 ID 还是有不同的, 下⾯再分析, 或者⽬前你可以认为端⼝就是进程 ID的影⼦.也即是说, 如果仅有域名(ip), 是⽆法定位到⼀个进程的, 通讯的发起⽅不但需要给出 ip, 还需要给出端⼝, 只有这样, 服务器才能知道由哪个进程去响应.端⼝, ⼀个间接层那么问题⼜来了, 为什么引⼊端⼝, ⽽不是直接使⽤进程 ID 呢? 这个原因想想也不难明⽩, ⼤概有这么⼏点原因:1. 作为客户端⽆法知道服务端对应进程的 ID2. 服务端对应进程重启后 ID 会改变3. ⼀个⽹站的 web 进程 ID 是这个, 另⼀个⽹站的可能⼜是另⼀个⾃然, 原因是很多的, 我也是随便的列举了⼀些, 你或许还能想到更多. ⽽为了解决这些个问题, 就引⼊了端⼝这⼀间接层(indirection).计算机世界⾥有⼀句名⾔: 任何计算机问题均可通过增加⼀个间接(indirection)层来解决.(Any problem in computer science can be solved with another layer ofindirection. -- David Wheeler)这个名⾔其实还有后⾯⼀句: But what usually will create another problem.(但通常会带来另⼀个问题)这⾥所谓另⼀个问题, ⽐如它会使得层次结构复杂化, 交互效率下降等等. 当然了, 这就是架构师们要去权衡的问题了, 很多时候, 架构就是关于平衡的艺术. 打死都不肯引⼊任何的间接层, 这是⼀个极端; ⽽⼀上来就引⼊好多个间接层, 这⼜是另⼀个极端.如果没有这个间接层, 客户端要与服务端通讯, 就要知道服务端对应进程的 ID, 也即是客户端是依赖于服务端的:显然, 这种模式对于 web 这种⼀个服务端对应⼤量客户端访问的情形是极不适应的,你都不知道有谁可能会来访问你的⽹站! 你根本⽆法告诉它们.⽽有了端⼝这⼀间接层, 对于 web 的情形, 这种依赖被倒置了, 客户端总是把请求发送到 80(或443) 端⼝, 这些成为标准的⼀部分, 并要求服务端反过来去适应, 服务端去监听端⼝的通讯并处理, 变成了⼀种反向依赖.如果⼀个进程想要提供 web 服务, 它启动之后就要去绑定(binding) web 相关的端⼝,如果端⼝已经被其它进程绑定了(即所谓占⽤了), 就会绑定失败; ⼜或者被⾃⾝前⼀个未完全退出的进程占据着, 也会绑定失败, 在开发过程中你可能会遇到类似的问题, ⼀个 web 进程没有关闭, 你⼜试图启动另⼀个, ⽽两者都⽤了相同的端⼝, 就会产⽣冲突.并在其上持续的监听(listen), 同时在有请求到来的时候去响应(response). 这样⼀来, 进程 ID 的问题就消解了:这类似于⼀个接⼝回调, 浏览器只需要⾯向接⼝索取服务, ⽽⽆需知道接⼝服务的具体提供者, 这些细节被端⼝层所封装并隐藏起来了.端⼝这⼀间接层的存在解耦(decouple)了客户端与服务端之间的强依赖, 整个体系变得很灵活.可以把端⼝视作⼀般编程概念中的接⼝(interface), ⽽想 Nginx, apache, tomcat 等等可以认为是这个接⼝的不同实现(Implementation).端⼝与现实世界的⼀个类⽐为加深理解, 可以举⼀个现实世界中的例⼦. 相信⼤家都有过去市民中⼼办事的经历, ⽐如去办理居住证, 护照, 社保等等业务, 你通常会收到⼀个⼩纸条让你去某个窗⼝办理对应业务, 这个窗⼝其实就类似于端⼝了:⽐如 80 窗⼝就对应港澳台通⾏证业务那么你要办港澳台通⾏证, 你就奔向 80 号窗⼝就完了. 你不要去问门⼝保卫处的王⼤爷, 到底是哪位同志办理这个业务.今天可能是⼩明在办理, 隔了⼏天, ⼩明可能受伤了, 流⾎了, ⼜轮到⼩红在那⾥办理,⼜过段时间, ⼩红也出意外了, 流产了, ⼜轮到⼩张在办理, ⼜过段时间, ⼩张被发现在办理业务过程中徇私舞弊, 流放了...等等, 如果此时你的同事问你怎么办港澳台通⾏证, 你需要知道这些个⼈事变动的细节吗? 根本不需要呀, 你只需告诉他去 80 号窗⼝办理就好了...市民中⼼的整个体系, 会确保有个会办理这些业务的⼈员坐在那个窗⼝下⾯, 你唯⼀需要做的, 就是到那个窗⼝下请求服务即可.端⼝与名称服务(naming service)通过上⾯现实世界类⽐的例⼦, 对于端⼝的机制, 相信你已经理解得⽐较深⼊了. ⼴义上讲, 端⼝层也可以视作⼀个 naming service(名称服务), 这与⽐如 spring cloud 中的 eureka ⾥的机制本质上是⼀样的, 只是这个 name 就是⼀个抽象的数字, ⽐如 80. 80 就代表了⼀个 web 服务, Nginx之类的 web server 绑定并监听就相当于把⾃⾝提供的 web 服务注册于其上.DNS 域名系统其实也是 naming service, 你通过 这个名字(name), 就能获取到我所提供给你的⽹页服务.类似的还有 java ⾥的 JNDI 等, 把⼀个名字与⼀个服务关联起来, ⽐如⼀个名字就代表⼀个数据源(数据库连接)之类的.端⼝与 IoC(控制反转)⼴义上, 端⼝的上述机制也是控制反转(Ioc: Inversion of Control)思想的⼀种体现, 如果客户端需要知道服务端的进程 ID, 实际上就被服务端控制了, 毕竟我服务端在哪个 ID 上提供服务, 你就得把你的请求发到相应的 ID 上来;⽽有了端⼝这⼀中间层呢? 作为客户端, 总是把请求发到对应端⼝上, 并要求服务端绑定并监听那些端⼝以及作出响应, 你服务端是反过来被我客户端所控制, 我客户端发到哪个端⼝, 你服务端就要去相应端⼝上监听并响应.⼤家可以体会⼀下这种转变. 这种设计或思想在编程领域其实是特别重要的, 在很多其它地⽅都有体现.因为浏览器总是把 web 请求发到了 80 或 443 端⼝, 这就要求⼀个 web server 进程去监听这些端⼝. ⽐如在我的服务器上, web server 是 Nginx, 它启动之后就会去监听 80 和 443 端⼝, 任何想要访问我的主页的⼈, 并不需要知道我的 Nginx 进程 ID 是啥, 借助于端⼝这⼀间接层, 你就能够与我的 Nginx 进程通讯, 并获取你想要的东西.事实上你可以这么认为, 浏览器实际上只是在与端⼝通讯, 端⼝层再把这些请求委托(delegate)或代理(proxy)给相应的 web server 去处理, 端⼝的⾓⾊就是⼀个中间⼈,⼀个间接层.再论缺省端⼝现在, 我们应该明⽩了, 端⼝是必要的了, 当然, 对最终的⽤户来说, 则不需要知道这些实现的细节,对于他们, 应该遵循最⼩知识原则, 知道得越少越好.如果你⼀定要让⽤户在输⼊ url 的过程中输⼊端⼝, ⼜或者要输⼊个 www 等等, ⽤户就要给你扔过来'⼗万个为什么'了...为什么要加个 443?为什么不是 334, 443是啥意思?为什么⼀会⼉是 80, ⼀会⼉⼜是 443?为什么加个 www, 啥意思?为什么末尾还加个斜杠, 不加会死吗?...惹不起, 惹不起...还记得前⾯说的, ⽤户是笨蛋, ⽤户是懒汉吗?这⾥⼜要引⽤⼀句计算机世界的名⾔了: 程序员和上帝打赌要开发出更⼤更好连傻⽠都会⽤的软件, ⽽上帝却总能创造出更⼤更傻的傻⽠。⽬前为⽌,上帝赢了。Programmers are in a race with the Universe to create bigger and better idiot-proofprograms. The Universe is trying to create bigger and better idiots. So far theUniverse is winning.说句⼼⾥话, 很多时候, ⽤户能记住你的域名就阿弥陀佛了, 你就该烧⾼⾹了, 你还想⽤户记住你的端⼝, 真的想多了...另⼀⽅⾯, 说到这⾥我们应该也能明⽩了, 那就是理论上, web 服务实际上可以构建在任何端⼝之上. ⽐如在本地开发的时候, ⽤户只有你⾃⼰, 那当然你可以随便挑⼀个端⼝, ⽐如 8080, 只要⾃⼰知道就好了或顶多告诉另⼀个与你配合的前端同事.同理, 其它⾮ web 的服务, ⽐如 ftp 服务, 也不⼀定说⾮得在 21 端⼝上等等; mysql 服务的端⼝同样可以调整为 3306 之外的端⼝.⼜或者说, 你想提供⼀个服务, 但只想⼩范围内的⼈知道, 你可以挑⼀个很偏门的端⼝, 这样⼀般⼈只输⼀个域名就没法访问到你的服务了.⽐如有⼈想偷偷提供⼀些服务, 放⼀些⼴淫民群众喜闻乐见的⼩视频啥的...刑法警告,后果⾃负!! 别说我没有提醒你.端⼝与 TCP/UDP 协议前⾯⼀直在说, 什么 3306 端⼝, 80 端⼝, 443 端⼝, 其实严格来说, 端⼝是分 TCP 端⼝和 UDP 端⼝的, 不过多数时候遇到的都是 TCP 端⼝, 但 TCP 80 端⼝和 UDP 80 端⼝是不同的端⼝.UDP 的 80 端⼝, 包括 443 端⼝其实被保留了, ⽬前的 http 协议只构建在 TCP 协议之上.当然, 理论上讲, 在 UDP 上构建 http 也不能说就完全不⾏, 毕竟, ⽆论 UDP 还是 TCP都是构建在 IP 协议之上, 总之呢, 计算机的世界没什么是不可能的, ⽽且似乎真有⼈在做这些尝试, 不过这就属于两⼩母⽜对屁股--⽐较⽜逼的范畴了, 深⽔区了, 咱也不懂, 不多说了.还有⼀点, 对于进程间的端⼝通讯, 实际上是对称的, 也即是说, 服务器的响应也是先回到⼀个客户端的端⼝上.如果你⽤ Windows 10 系统, 可以在 任务管理器 > 性能 > 打开资源监视器 > ⽹络 >TCP 连接, 点击下远程端⼝可以按照从⼩到⼤排列, 通常就可以看到 443 的相关连接了, 可以看到左边有⼀栏本地端⼝, ⼀个 TCP 连接总是有⼀个远程端⼝, ⼀个本地端⼝:当发起⼀个 TCP 连接时, 客户端⾸先⾃⼰先随机挑选⼀个没有被使⽤的端⼝作为服务器响应的接收端⼝, ⽐如 38672. 在⼀个 TCP 的包⾥, ⽆论是握⼿包还是后续的数据包, 包头部分最重要的两个字段, ⼀个就是源端⼝(source port), ⽐如 38672; 另⼀个就是⽬标端⼝(destination port), ⽐如 80, 或者 443.可以这样看, 服务器的响应也是先回到源端⼝, ⽐如 38672 上, 源端⼝再转给最终的进程, ⽐如浏览器.⽽对于⼀个 IP 包, 同样的, 包头部分最重要的两个字段, ⼀个就是源IP(source IP); 另⼀个就是⽬标 IP(destination IP).⽽ TCP 包会作为 IP 包的数据包被打包到 IP 包⾥⾯, 也⼀个 IP 包⾥其实包含了 IP +端⼝.IP 加端⼝再加上端⼝与进程间的关联, 分属两个不同主机间的进程就能通过 TCP(UDP)/IP 协议愉快地进⾏进程间的通讯(IPC)了.当然了, 同⼀个主机间的进程也同样可以利⽤这套机制. 但同⼀个主机间还可以有其它选择, 这个具体看各个操作系统是否提供相关机制及⽀持. ⽽ TCP/IP 属于⼴泛应⽤的标准协议, 从⽽得到了⼴泛⽀持.因为篇幅关系, 关于这样 TCP 协议等的细节, 以及包括 Socket, 连接等概念, 以及虚拟主机, 反向代理等等就不再展开去说, 如果你感兴趣, 欢迎留⾔, 后续会考虑再写⼀些⽂章去介绍.同样因为篇幅的原因以及同时我也不是计算机⽹络及协议⽅⾯的专家, 关于端⼝⽅⾯的, 如果有什么说得不到位, 或不正确的地⽅, 欢迎留⾔指正, 关于端⼝⽅⾯的介绍就到这⾥.
发布评论