目录
- 前言
- 前置知识
- 一、计算机网络体系结构
- 二、TCP/IP协议族
- 2.1 简介
- *2.2 TCP/IP网络传输中的数据
- 2.3 地址和端口号
- 2.4 小总结
- 三、TCP/UDP特性
- 3.1 TCP特性
- TCP 3次握手
- TCP 4次挥手
- TCP头部结构体
- 3.2 UDP特性
- 四、总结
- 课程内容
- 一、网络通信编程基础知识
- 1.1 什么是Socket
- 1.2 长连接、短连接
- 1.3 网络编程与生活常识类比
- 二、BIO
- 2.1 BIO简介
- 2.2 BIO结合多线程1:普通线程
- 2.3 BIO结合多线程2:线程池
- *2.4 小结
- 三、NIO(未完待续)
- 3.1 NIO简介
- 3.2 与BIO的主要区别
- 学习总结
前言
我是有点怕网络编程的,总有点【谈网色变】的感觉。为了让自己不再【谈网色变】,所以我想过系统学习一下,然后再做个笔记这样,加深一下理解。但是真要系统学习,其实还是要花费不少时间的,所以这里也只是简单的,尽可能地覆盖一下,梳理一些我认为比较迫切需要了解的网络编程相关的知识。
其实单单网络编程跟TCP/IP协议族就可以拿来写一个独立的笔记了,但是为了让知识点更耦合一点,我还是决定写在一起。当然这样也有坏处,那就是知识点比较密集,然后导致笔记篇幅偏长,所以阅读的朋友给点耐心。
另外,为了让知识分层更加清晰一点,基础网络编程跟TCP/IP那一块我还是放在【前置知识】里面。
前置知识
一、计算机网络体系结构
OSI七层网络模型:
我相信大家对OSI七层模型都不陌生,OSI全称:开放系统互连参考模型,是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合制定的开放系统互连参考模型,为开放式互连信息系统提供了一种功能结构的框架。其目的是为异种计算机互连提供一个共同的基础和标准框架,并为保持相关标准的一致性和兼容性提供共同的参考。这里所说的开放系统,实质上指的是遵循 OSI 参考模型和相关协议能够实现互连的具有各种应用目的的计算机系统。(PS:这是一个标准的,参考模型)
它的整体模型图如下:
面试中可能比较常考这一点,建议死记硬背。个人背诵方式是:记每一层首字。即:应表会传网数物、应表会传网数物、应表会传网数物。刚开始有点拗口,多念几遍就记住了。
TCP/IP模型:
严格来说,OSI模型是一种比较复杂且学术化的【参考】模型,实际上我们说的互联网中实际使用的是TCP/IP模型(这是由工程师定义的。这个也是我们后面要着重讲的内容)。
TCP/IP模型在网上又叫五层模型。它跟7层网络模型其实差不多,就是把7层的前3层合在一起表示了。模型图如下:
PS:网上也有一些人把最后2层【数据链路层】和【物理层】合在一起叫做【网络接口层】,最后结论为:TCP/IP为4层网络模型。大家知道有这个东西就好
小总结:
其实无论什么模型,每一层的定义都是建立在低一层提供的服务上的,并且为高一层提供服务。大致来说,我们可以这么映射这个网络模型:
物理层 <—对应—> 网卡、路由器、交换机
数据链路层 <—对应—> 网卡驱动
网络层、传输层 <—对应—> 这一部分已经被操作系统封装了
应用层 <—对应—> 一些通用的网络应用程序,或者自己编写的应用程序
另外,我相信通过对比大家也看到了,OSI七层模型跟TCP/IP五层模型没啥特别大区别的,记住其中一个模型就差不多了。而且,在编程过程中,其实我们通常只需要关注【应用层】就好了,下面几层的封装,其实操作系统早就帮我们封装好了。它就是:Socket
。
二、TCP/IP协议族
2.1 简介
TCP/IP协议族,我希望大家将他拆分成三个部分来理解。
TCP/IP
:Transmission Control Protocol/Internet Protocol 的简写,中译名为【传输控制协议/因特网互联协议】,是 Internet 最基本的协议、Internet 国际互联网络的基础。由网络层的 IP 协议和传输层的 TCP 协议组成。协议采用了 5 层的层级结构。然而在很多情况下,它是利用 IP 进行通信时所必须用到的协议群的统称也就是说,它其实是个协议家族,由很多个协议组成,并且是在不同的层,是互联网的基础通信架构- 协议:协议是经过谈判、协商而制定的共同承认、共同遵守的文件。就可以认为是一种规范、标准
- 族:说明每一层都含有多个的意思
下图是TCP/IP模型,以及对应的常见协议族:
横向对比图:
OSI参考模型 | TCP/IP模型 | TCP/IP协议族 |
应用层 | 应用层 | DNS、HTTP、FTP、Mysql、WebSocket |
表示层 | ||
会话层 | ||
传输层 | 传输层 | TCP、UDP |
网络层 | 网络层 | IP、ICMP、RIP、IGMP |
数据链路层 | 数据链路层 | ARP、RARP、IEEE802.3、CSMA/CD |
物理层 | 物理层 | FE自协商、Manchester、MLT-3、4A、PAM5 |
*2.2 TCP/IP网络传输中的数据
每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息(或者我们可以理解为,数据再每个分层流转的时候都会包裹一层,根据协议标准附带上当前层的一些关键数据),这就是编程语言角度【协议】的本质。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。
网络中传输的数据包由两部分组成:【协议首部】+【上一层传过来的数据】。协议首部结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。
举个简单的例子。我们用用户 A 发送,用户 B 接受来说说明:
1)用户A使用某应用程序在【应用层】处理。首先,应用程序会根据自定义编码处理产生的消息(比如最简单的json序列化),然后交给下面的【传输层】TCP协议处理
2)用户A的【传输层】TCP处理。TCP根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要将【应用层数据】封装为【报文段(segment)】并附加一个 TCP 首部然后交给下面的【网络层】IP协议处理
3)用户A的【网络层】IP模块的处理。IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部生成 IP 数据报(datagram)然后交给下面的【数据链路层】
4)用户A【数据链路层】处理。从 IP 传过来的 IP 包对于数据链路层来说就是数据。给这些数据附加上数据链路层首部封装为链路层帧(frame),生成的链路层帧(frame)将通过物理层传输给接收端
5)用户B【数据链路层】的处理。用户 B 主机收到链路层帧(frame)后,首先从链路层帧(frame)首部找到 MAC 地址判断
是否为发送给自己的包,若不是则丢弃数据;如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如IP、ARP 等。这里的例子则是 IP
6)用户B【网络层】IP 模块的处理。IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP
7)用户B【传输层】TCP 模块的处理。在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据(请记住这个步骤,后面推断三次握手要用到)。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序
8)用户B【应用层】应用程序的处理。接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。
2.3 地址和端口号
在我们网络传输中,有两个概念我们会经常接触,就是所谓的地址跟端口号。地址地址,我估计大部分人想到的是IP地址,却忽略了Mac地址。可是有人知道,为什么计算机里面要设计【Mac地址 + IP地址 + 端口号】吗?
我们知道,IP地址通常是用来唯一标识一台机器的,但我们同样应该知道,不同内网中IP地址是可以重复的,那我怎么知道这个IP地址就是这台机器呢?IP地址又是如何而来的呢?对的,就是Mac地址。
Mac地址
MAC 地址全称叫做媒体访问控制地址,也称为局域网地址(LAN Address),MAC 位址,以太网地址(Ethernet Address)或物理地址(Physical Address),它是由网络设备制造商在生产时就写在硬件内部的(但是这不意味着就不能更改,只是比较麻烦,一般人也不会去改)。它作用于【数据链路层】上
MAC 地址共 48 位(6 个字节)。前 24 位由 IEEE(电气和电子工程师协会)决定如何分配,后 24 位由实际生产该网络设备的厂商自行制定。例如:FF:FF:FF:FF:FF:FF 或 FF-FF-FF-FF-FF-FF
比如,下面是我的电脑Mac地址信息:打开cmd,输入:ipconfig
IP地址
IP地址是啥不用过多介绍了。IP 地址分为:IPv4 和 IPv6,我们通常说的是IPv4。IP 地址是由 32 位的二进制数组成,它们通常被分为 4 个【8 位二进制数】,我们可以把它理解为 4 个字节,格式表示为:A.B.C.D
。其中,A,B,C,D 这四个英文字母表示为 0-255(2^8 - 1)的十进制的整数。例如:192.168.1.1。它作用于【网络层】上
那IP地址跟Mac地址有什么区别和联系呢?
1)对于网络中的一些设备,路由器或者是 PC 及而言, IP 地址的设计是出于拓扑设计出来的,只要在不重复 IP 地址的情况下,它是可以随意更改的;而 MAC 地址是根据生产厂商烧录好的,它一般不能改动的,一般来说,当一台 PC 机的网卡坏了之后,更换了网卡之后 MAC 地址就会变了
2)在前面的介绍里面,它们最明显的区别就是长度不同, IP 地址的长度为 32 位,而Mac地址为 48 位
3)它们的寻址协议层不同。IP 地址应用于 OSI 模型的【网络层】,而Mac地址应用在OSI的【数据链路层】。 数据链路层协议可以使数据从一个节点传递到相同链路的另一个节点上(通过 MAC 地址),而网络层协议使数据可以从一个网络传递到另一个网络上( ARP 根据目的 IP 地址,找到中间节点的 MAC 地址,通过中间节点传送,从而最终到达目的网络)
4)分配依据不同。 IP 地址的分配是基于我们自身定义的网络拓扑, MAC 地址的分配是基于制造商
端口号
端口号的存在不难理解,毕竟无论是Mac地址还是IP地址都只能标识一台机器,那怎么标识机器上特定应用程序呢?就是端口号,所以端口号又可以称为:程序地址。它作用于【传输层】上
一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。
2.4 小总结
1)TCP/IP每一层都有自己的协议,甚至有多种
2)协议是一种规范、约束。用Java WebMVC来说,这种规范就是:前后端约定了,每一次交互必传手机号一样
3)TCP/IP每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息(其实就是【协议】的体现)
4)在下一层的角度看,从上一层收到的包全部都被认为是本层的数据
Q1:为什么端口号只有65535个?
答:因为在TCP、UDP协议报文的定义中,会分别有16位二进制(合计32位4字节)长度来存储元端口号和目的端口号,所以端口个数只能是2^16=65536个。通常0号端口用来表示所有端口,所以实际可以用的端口号有65535个。(详细的TCP、UPD头部结构体定义,见下面的笔记内容【TCP头部结构体】)
Q2:客户端的端口号是多少,如何确定?
答:是的,我们通过客户端连接服务端的时候,也需要端口号的,这一点很容易被大家忽略。然后,客户端的端口号虽然可以指定,但是通常不指定,因为没什么必要。所以,都是由操作系统给自动分配的。并且,分配的范围通常都是大于10000(0-1023通常被知名服务占用了,比如FTP端口是21,HTTP是80)
三、TCP/UDP特性
在讲述TCP/UDP特性之前,我们先给大家声明一个概念。那就是网络通信中的客户端和服务端。声明如下:
- 客户端:主动发起连接的一方为客户端
- 服务端:接收连接请求的一方为服务端
【你】向【我】发起连接,肯定是因为想从【我】这里获得什么,所以【你】是客户端,【我】是服务端(服务提供方),这很合理。其实这才是广义上的客户端、服务端定义。请大家不要狭隘地跟Web开发中的客户端、服务端混淆。
3.1 TCP特性
TCP(Transmission Control Protocol)是面向连接的【全双工】通信协议,通过三次握手建立连接,然后才能开始数据的读写,通讯完成时要拆除连接,由于 TCP 是面向连接的所以只能用于端到端的通讯。
我们知道,TCP相对于UDP最大的特点,就是【可靠性】。那么为了实现【可靠性】,TCP做了什么呢?整体来说大概是2点:【超时重传】和【消息应答确认机制】。那大家有思考过,这两点是如何被实现的吗?其实这个实现方式也并不是特别重要,不过说到这个东西,我突然间意识到一些东西。言归正传。
全双工:即允许数据在两个方向上传输
半双工:虽然数据也能在两个方向上传输,但是一次只能一个方向流动
单工:只允许数据从一个方向上传输
超时重传
超时重传机制中最最重要的就是重传超时(RTO,Retransmission TimeOut)的时间选择(如何确定),由于网络存在波动,所以,用一个固定的时间来测算是一种不经济的方式,TCP通过引入一个叫做RTT(round-trip time)的定义,根据最近的发包、收包时间动态测算、修改重试时间
消息应答机制
顾名思义,就是当收到消息后,给消息一个应答,告诉对方:我收到了。而对于没收到消息应答的一方来说,一段时间后没收到应答就【超时重传】。那在复杂的网络通信中,我可能每秒钟都有好多条消息交互,我怎么知道这一条答复,是对应哪一条请求呢?对的,序号!这个序号也将在TCP三次握手中体现出来(现在回过头来看,RocketMQ的事务可能就是参照这个设计的。当然可能也不是,毕竟这个设计思路很多地方都有用过)
TCP 3次握手
TCP 提供面向有连接的通信传输。【面向有连接】是指在数据通信开始之前先做好两端之间的准备工作。
所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在 socket 编程中,这一过程由客户端执行 connect 来触发,所以网络通信中,发起连接的一方我们称为客户端,接收连接的一方我们称之为服务端。
下面是三次握手的流程图:
流程解读:
1)第一次握手:客户端将请求报文标志位 SYN 置为 1(告诉服务端,此时是在同步),请求报文的 seq(Sequence Number)字段中填入一个随机值 J(表示此时是同步到第几条消息,这个很重要很重要很重要),并将该数据包发送给服务器端,等待服务端的应答确认,紧接着客户端进入SYN_SENT
状态(SYN即同步的意思)
2)第二次握手:服务器端收到数据包后由请求报文标志位SYN=1
知道客户端请求建立连接,服务器端将应答报文标志位 SYN 和 ACK 都置为1(ACK=1
表示此条应答是针对客户端同步的应答;SYN=1
则表示我也想跟你请求同步了【毕竟全双工啊同学们】,双方都交换seq
),应答报文的 ack(Acknowledgment Number)字段中填入 ack=J+1,应答报文的 seq 中填入一个随机值 K(同上面的J一样道理),并将该数据包发送给客户端以确认连接请求,服务器端进入 SYN_RCVD 状态
3)第三次握手:客户端收到应答报文后,检查 ack 是否为 J+1,ACK 是否为 1,如果正确则将第三个报文标志位 ACK 置为 1,ack=K+1,并将该数据包发送给服务器端,服务器端检查 ack 是否为 K+1,ACK 是否为 1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED 状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了
TCP三次握手大家都听过,但是根据我的经验,大部分人都【谈网色变】,甚至搞不清楚三次握手传的参数是什么意思。所以这边再来个【小总结】,希望大家别看了流程就算了。如下:
- 三次握手中的SYN标志,是同步的意思。
SYN=1
表示我要跟你同步了
Q1:为什么要同步?同步什么?
答:主要是同步序号seq,为什么要这个同步这个?你看看【消息应答机制】提到的,在复杂的网络通信中,如果没有序号,如何判断,我这条应答对应的是你这条消息??没有序号,我怎么知道,哪条消息已经接收到了,哪些没有收到,哪些消息又没有重复发送?
Q2:为什么三次握手而不是四次握手,甚至更多次?
答:因为三次就够了,已经完成原始数据同步了,后面再来就没意义了。
- 三次握手的本质:即是通信双方相互告知序列号起始值,并确认对方已经收到了序列号起始值
- 因为是TCP是全双工,所以,双方都要建立请求连接。这个在Java中的体现就是:两边都有一个
socket
对象 - 如果你跟我一样是死记硬背的选手,请记住出现在图中的每一个字段的意思,包括客户端、服务端在握手过程中状态的演变
TCP 4次挥手
其实这个相对来说没那么重要,就算是面试也很少考这个,但终归还是要了解一下的。
四次挥手即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送 4 个包以确认连接的断开。在 socket 编程中,这一过程由客户端或服务端任一方执行 close来触发。
跟握手一样,由于TCP是全双工的,因此,每个方向都必须要单独进行关闭。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。(连接都是双方都请求连接,关闭当然是两边都要关闭)
流程图如下:
我相信你们如果完整看过了【TCP3次握手】的话,这边的流程图难不倒你。FIN
即finish
嘛。老样子,注意状态变化哦。这里可能比较男的地方就是最后一次【挥手】的时候,为什么要进入TIME-WAIT
状态了。哎呀,都是因为【全双工】啊,等待一下,确保那边也关了比较好,另外也确保迟到的消息报文,能被当前应用程序正确的识别并且丢弃。
Q1:【被当前应用程序正确的识别并且丢弃】怎么理解?
答:我们前面介绍端口的时候说过,客户端端口号可以不指定,由系统自动分配的,所以很可能你刚关闭,就被新的其他应用程序客户端占取了这个端口,结果就是:如果没有这个TIME-WAIT状态,就恰好再关闭之前有来自其他端的发送给上一个占用该端口的应用程序。啊,现在这个应用程序就懵了,咋回事啊这条消息?完全“看不懂”啥意思啊!!
TCP头部结构体
为什么要介绍这个结构体?
首先,经过了【2.2 TCP/IP网络传输中的数据】的描述,你应该知道,在网络通信中,每个分层都会裹上一层属于自己的数据(各种协议规定的),这就是编程语言角度【网络协议】的本质。
所以,TCP在传输层中给出的【协议】即如下所示:
这个图是不是很难看懂?给大家一段C语言伪代码,一般人都看得懂啥意思。
// TCP头(首)部信息
struct TCPHead
{int32 port; // 端口号,4字节int32 seq; // 序号,4字节int32 ack; // 确认号,4字节int32 offset; // 偏移数据,4字节int32 crc; // 校验,4字节byte[] data; // 协议数据,边长
}
这里给出这该结构体的定义跟模型图,也是为了方便大家去放飞思想,想象一下其他各层的结构体,以及,又会裹上什么信息呢。
再然后,通过一个简单的面试问题,把之前的知识整合起来
Q1:一台主机上只能保持最多 65535 个 TCP 连接,对吗?
答:肯定是不对的。首先,出于【传输层】的TCP协议,肯定是包含了【网络层】IP层数据的(如果你网络,赶紧回去看看【2.2】)。的虽然我没有贴出来【网络层】IP协议的头部数据,但是你多多少少会知道,肯定需要【源IP地址】跟【目标IP】地址才对。
所以,定义一个【唯一TCP连接】的要素如下:【源 IP 地址】+【目标 IP 地址】+【协议号(协议类型)】+【源端口号】+【目标端口号】
大伙看看上图,不难看出,当【源IP地址】跟【源端口号】变化时,哪怕【目标IP地址】跟【目标端口号】,【协议类型(TCP固定为TCP,UDP固定为UPD)】固定,TCP连接数量也会由于前两者的变化而变化。
3.2 UDP特性
UDP(User Datagram Protocol),中文名是用户数据报协议。是把数据直接发出去,而不管对方是不是在接收,也不管对方是否能接收,也不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员自己选择、比较使用。
UDP还有2个特性,称为:单播和广播。
单播和广播
单播的传输模式,定义为发送消息给一个由唯一的地址所标识的单一的网络目的地。面向连接的协议和无连接协议都支持这种模式。由于通讯不需要连接,所以可以实现广播发送,所谓广播——传输到网络(或者子网)上的所有主机。
我相信大部分初次接触UDP的朋友可能会觉得,UDP这么不可靠,使用应该不多吧?哈,相反,UDP因为没那么多复杂的机制,反而深受大佬们喜爱,甚至可以说,很多大佬们会优先选择UDP,除非迫不得已,真的需要【可靠性】的支持,才使用TCP。
四、总结
OK,【前置知识】中关于网络编程的一些基础知识终于是捋完了。怎么说呢,因为我也没看过更专业、更全的【网络编程基础】相关资料书记,所以,我也不好说我说的是否都是对的,更遑论是否全面。我只是根据,我一直以来学习网络编程以来遇到的问题。不过话说回来,我遇到的这些问题有时候会受限于个人的眼界与技术功底。
共勉
课程内容
正文开始
一、网络通信编程基础知识
1.1 什么是Socket
我在前置知识中稍微提过一嘴,由于Socket
的存在,直接帮我们封装了TCP/IP模型中的后4层,我们只需要关注【应用层】就好了。甚至【应用层】也有很多成熟的产品了,我们往往只需要关注如何写业务,以及如何把它们应用到我们的代码中。言归正传。
Socket
是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,一般由操作系统提供。从设计模式的角度看,Socket
其实就是一个门面模式,它把复杂的 TCP/IP 协议处理和通信缓存管理等等都隐藏在 Socket 接口后面,对用户来说,使用一组简单的接口就能进行网络应用编程,让Socket 去组织数据,以符合指定的协议。主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接。
给大家一个门面模式渲染图感受一下Socket:
改成门面模式后:(不要怀疑下面这个是否合理,如果你去过高端大气上档次的男科医院,就知道什么是VIP服务了)
客户端连接上一个服务端(connect),就会在客户端中产生一个 socket 接口实例,服务端每接受一个客户端连接,就会产生一个 socket 接口实例和客户端的 socket 进行通信,有多个客户端连接自然就有多个 socket 接口实例。它们的通信模型大概如下:
1.2 长连接、短连接
短连接
短连接的大概流程是这样的:连接->传输数据->关闭连接
。典型的如:HTTP。
传统 HTTP 是无状态的,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就断开。也可以这样说:短连接是指 SOCKET 连接发送数据接收完数据后马上断开连接的一种连接。
长连接
长连接的大概流程是这样的:连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接
。典型的如:Mysql连接协议。也可以这样说:长连接指建立 SOCKET 连接后不管是否使用都保持连接的一种连接。
Q:什么时候用长连接,短连接?
答:长连接多用于操作频繁,点对点的通讯,毕竟TCP连接需要三次握手,反复握手是一个很重型的操作。而且从代码角度来说,反复创建Socket对象也不是一个经济的方式。而像 WEB 网站的 HTTP服务按照 HTTP协议规范早期一般都用短链接,毕竟想WEB应用通常都不会频繁交互,或者说每次交互都是短暂的操作。不过值得注意的是,现在的 HTTP协议,Http1.1,尤其是 Http2、Http3 已经开始向长连接演化。
1.3 网络编程与生活常识类比
我们首先来看一个生活中的场景。网络编程可以类比成生活中:心理咨询中心场景。
- 周瑜老师准备开一个心理咨询中心,嘴上光喊没用,只有到工商局注册“东吴心理诊所”并且在图灵大街 888 号挂牌了,才算正式开张。为了开展电话业务,申请了一个电话号码 88888888。
这里就类比于:网络编程中,我们要先在服务端声明一个ServerSocket,绑定指定IP地址跟端口
- 由于疫情经济下滑等原因,诸葛老师为了养家糊口没日没夜的工作,久疾成病,并且有了心理问题。于是打电话过来,周瑜老师接了电话,但是周瑜老师只是一个接待,
这就类比于:网络编程中,ServerSocket只关注于网络连接事件
- 所以周瑜老师不懂心理咨询,于是通过内部分机把电话转给请来的心理医生 A 负责接待诸葛老师,心理医生 A 和诸葛老师通过电话进行沟通,模式一般就是一个人说另个一人听,两者进行沟通交流。
- Fox 老师也来了,周瑜老师接了电话,又把电话转给请来的心理医生 B 负责接待 Fox 老师,心理医生 B 和 Fox 老师也通过电话进行沟通。
4、5就类比于:网络编程中,ServerSocket每接收一个连接事件后,就新建一个Socket与它们交互
还算简单吧。整体来说,在网络通信编程中,我们只需要关注三件事:
1)连接事件。客户端连接服务器,服务器等待和连接
2)读网络数据
3)写网络数据
无论是后面学的BIO也好,还是NIO,又或者什么Netty网络编程,所有模式的通信编程都是围绕着这三件事情进行的。服务端提供 IP 和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
二、BIO
2.1 BIO简介
BIO,意为 Blocking I/O,即:阻塞的 I/O。
BIO 基本上就是我们上面所说的生活场景的直接实现。在 BIO 中类 ServerSocket 负责绑定 IP 地址,启动监听端口,等待客户连接;客户端 Socket 类的实例发起连接操作,ServerSocket接受连接后产生一个新的服务端 socket 实例负责和客户端 socket 实例通过输入和输出流进行通信。
那么,BIO的阻塞体现在哪里呢?我们来看一段简单的BIO代码。
/*** 服务端BIO代码* * @author shen·buffet* @date 2023-10-17* @slogan 听天命,尽人事*/
public class TestServer {final static int PORT = 8585;public static void main(String[] args) throws IOException {// 第一步:创建一个ServerSocketServerSocket serverSocket = createServerSocket();System.out.println("第一步完成。 服务已启动");// 第二步:监听连接事件handleConnect(serverSocket);}/*** 创建一个ServerSocket*/public static ServerSocket createServerSocket() throws IOException {ServerSocket serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(PORT));return serverSocket;}/*** ServerSocket上的事件处理*/public static void handleConnect(ServerSocket serverSocket) throws IOException {int connectCount = 0;try {while (true) {Socket clientSocket = serverSocket.accept();System.out.println("连接事件来了。 当前连接数目=" + (++connectCount));// 第三步:读写网络事件handleClientReadWrite(clientSocket);}} finally {serverSocket.close();}}/*** Client上的读写事件*/public static void handleClientReadWrite(Socket clientSocket) throws IOException {// 实例化与客户端通信的输入输出流ObjectInputStream objectInputStream = new ObjectInputStream(clientSocket.getInputStream());ObjectOutputStream objectOutputStream = new ObjectOutputStream(clientSocket.getOutputStream());// 接收客户端的输出,也就是服务器的输入String userName = objectInputStream.readUTF();System.out.println("客户端读事件来了,消息=" + userName);// 服务器的输出,也就是客户端的输入objectOutputStream.writeUTF("hello," + userName);objectOutputStream.flush();}
}/*** 客户端BIO代码** @author shen·buffet* @date 2023-10-17* @slogan 听天命,尽人事*/
public class TestClient {public static void main(String[] args) throws IOException {//客户端启动必备Socket socket = null;// 实例化与服务端通信的输入输出流ObjectOutputStream output = null;ObjectInputStream input = null;// 服务器的通信地址InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 8585);try {socket = new Socket();socket.connect(addr);System.out.println("连接服务器成功!!");output = new ObjectOutputStream(socket.getOutputStream());input = new ObjectInputStream(socket.getInputStream());System.out.println("客户端准备发送数据....");// 向服务器输出请求output.writeUTF("ZhangShen");output.flush();//接收服务器的输出System.out.println("服务端答复数据来了....");System.out.println(input.readUTF());} finally {if (socket != null) {socket.close();}if (output != null) {output.close();}if (input != null) {input.close();}}}
}
其实这个阻塞,就体现在上面代码中的2点:TestServer#handleConnect
下的serverSocket.accept()
服务端等待客户端连接,以及TestServer#handleClientReadWrite
下的objectInputStream.readUTF()
等待客户端socket发送消息。(大家伙可以从方法点进去,看看方法头上的注释就知道了。通常这种优秀的代码,会在方法头上写清楚注释:这个方法会阻塞)
除了阻塞以外,大家发现没有,这个代码一条线程只能处理一个TCP连接请求,这显然是无法忍受的。所以,BIO在演进的过程中,为了,结合了多线程来实现同时处理多个TCP连接的目的。
如果想要使用我上面的代码实现模拟多条TCP连接失败的场景,需要DEBUG住客户端发送消息那一块,反正就是不要让它进入
finally
块关闭socket
,不然连接都关了,服务端也要申请关闭(TCP全双工特性,还记得吧)。
然后,IDEA用户的话,此时再新增一个Application启动,如下图所示:(整体操作步骤应该不用我手把手再教学了吧…)
2.2 BIO结合多线程1:普通线程
话不多说,先上个代码,这里主要修改了TestServer
类的handleConnect
方法:
/*** ServerSocket上的事件处理*/public static void handleConnect(ServerSocket serverSocket) throws IOException {int connectCount = 0;try {while (true) {Socket clientSocket = serverSocket.accept();System.out.println("连接事件来了。 当前连接数目=" + (++connectCount));// 第三步:读写网络事件new Thread(()->{try {handleClientReadWrite(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}}).start();}} finally {serverSocket.close();}}
就是在调用handleClientReadWrite
的时候,新增一个线程。
好了,如果看过我前面【并发专题】笔记的朋友,或者本身就了解多线程的朋友肯定知道,这么干是有性能瓶颈的。瓶颈造成的原因如下:
- Java线程,目前JDK1.8采用的是【内核线程】方案(其实JDK17也是),所以Java线程与内核线程(内核线程,指的是PC操作系统的线程)是
1:1
关系 - 因为一个PC最多也就开几千条线程,更何况,难道这台PC也不可能只有你一个Java程序在用啊
所以这个方案肯定不行的。先上个该方案的模型图让大家理解一下:
那不行,还有高级一点的方案吗?有的,用线程池咯,试试看
2.3 BIO结合多线程2:线程池
先上代码。这里主要修改了TestServer
类的handleConnect
方法,并且新增了一个线程池:
final static ExecutorService executorService;static {int cores = Runtime.getRuntime().availableProcessors();executorService = Executors.newFixedThreadPool(cores);}/*** ServerSocket上的事件处理*/public static void handleConnect(ServerSocket serverSocket) throws IOException {int connectCount = 0;try {while (true) {Socket clientSocket = serverSocket.accept();System.out.println("连接事件来了。 当前连接数目=" + (++connectCount));// 第三步:读写网络事件executorService.submit(() -> {try {handleClientReadWrite(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}});}} finally {serverSocket.close();}}
虽然我这里的线程池用的是newFixedThreadPool
,不过你们也可以根据需要选择其他API。但是,无论你选择何种线程池API,这始终是一个BIO,并且它的性能受限于线程池。先来个模型图帮助大家理解:
*2.4 小结
一定要看看
一定要看看
一定要看看
BIO小结一:
大家也看到了,BIO使用还是有多种限制的,哪怕是上了多线程也一样。事实上,我相信大家也看得出来,真正让BIO受限的,其实是BIO本身。而且我们开过天眼的啊,知道有NIO这个东西,所以,关于BIO性能瓶颈的解决方案,我们也能预知道。
BIO小结二:
严格来说这里不是BIO小结,而是:Java Socket网络编程小结。
经过了【前置知识】的系统整合,我终于把OSI,TCP/IP网络模型与Java Socket网络编程结合起来理解了。以前我看BIO、NIO、甚至Netty等网络编程的时候,个人遇到的最大的两个问题是:
- 为什么服务端这么多个Socket,什么ServerSocket,然后每次客户端连接进来的时候,还要重新新建一个Socket保存?Socket是什么?我只能死记硬背,真让我回答什么是Socket我也只能含糊其辞说是Java对网络编程的封装了
- 各种流,把我整蒙了。【谈网色变】一定程度上是因为【谈流色变】
当然【谈流色变】这个病我还没去治,现阶段也只是解决了部分疑问。
第一个问题,多个Socket是怎么回事。我在一开始介绍Socket的时候就说过,它是对网络模型后面4层的封装,使得我们无需关心IP、端口是如何被封装的。另外,还记得一条TCP是如何标识唯一的吗?(我在【前置知识 3.1中讲了】)也正是由于这个原因,所以为了维护、标识每个客户端的唯一TCP,使用一个Socket去保存。
BIO小结三:
虽然我们一直说BIO有性能瓶颈,话语中看似有不喜的意思,但并不代表它就没用。因为它使用简单的特性,像Zookeeper等分布式中间件在集群通信方案中,都使采用了BIO。
三、NIO(未完待续)
3.1 NIO简介
NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 BIO 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO 被称为 no-blocking io,也有人叫new io。这无关紧要,只是一个称呼