详解:TCP/IP五层(四层)协议模型
一.五层(四层)模型
1.概念
TCP/IP协议模型分为五层:物理层、数据链路层、网络层、传输层和应用层。这五层每一层都依赖于其下一层给它提供的网络去实现需求。
1)物理层:这是最基本的一层,也是最接近硬件的一层。其规定了网络通信中的一些硬件设施要求。
2)数据链路层:完成两个相邻设备之间的如何进行通信。如通过网线把电脑链接到路由器/交换机上,这里的电脑与路由器/交换机就是相邻的设备。
3)网络层:完成两个任意设备之间的如何进行通信,考虑的是通讯中间的过程是怎么样的。
4)传输层:完成两个任意设备之间的如何进行通信,考虑的是通讯的起点和终点。
5)应用层:网络编程实现我们想要的效果(需求)。
举个形象例子:网购。我们可以将网购送货分成各个层次。首先最基础的,运货要有路,这个公路就是最基础的“物理层”。再说,快递员将货物运输到各个转运中心,这就是两个相邻的设备中间的通讯,是“数据链路层”。快递公司给快递小哥规划路线,让他怎么走,考虑的是中间的过程,这是“网络层”。网购商家只管填上发货地和收货地,其他不用管,不关心快递小哥是走哪几个转运中心的,只管能不能到收货地,这就是“传输层”。最后,快递到我们手里了,我们怎么使用是我们决定的,正如应用层代码是我们自己实现的,实现的是什么由我们自己决定,这就是“应用层”。
透过上面的例子大家应该对这五层模型是什么有了更深刻的印象了。
回答一下为什么有人说五层,有人说四层。说四层其实是将最下层的物理层和数据链路层合并成一个,与硬件设备直接相关,只是说法不同,实际上都是一样的。
2.网络设备所在分层
1)主机:通过应用程序满足网络通信的需求,涉及 物理层 -> 应用层。
2)路由器:组建局域网,进行网络数据包转发,涉及 物理层 -> 网络层。
3)交换机:对路由器接口的拓展,涉及 物理层 -> 数据链路层。
二.传输层协议
传输层有两个核心协议,一个是TCP,一个是UDP。
1.UDP
1)特点:无连接,不可靠传输,面向数据报,全双工。
UDP不保存通讯对端的信息,这就是无连接。UDP发出数据后直接就不管了,一点不可靠。UDP在读写数据的时候,是以一个数据报为单位去读写的,注意一次必须读写一个数据报,半个不行,因此不存在粘包问题(这个在下面的TCP会解释)。全双工的意思的能读也能写,与之对应的是半双工,只可以读或只可以写。
2)报文格式:
源端口号和目的端口号这两个好理解,就是起点和终点。
UDP长度指的是整个UDP的长度,包括报头和载荷,长度这个位置最多存储两个字节的数据,也就是整个UDP长度最长是64kb。如果我们传输的数据超过64kb,比较大,这个时候我们就要考虑拆包了传输了。
校验和用来验证数据是否发生修改。这个校验和不是用来保证数据安全的,而是用来防止数据在运输的过程中发生比特翻转的现象。比特翻转就是数据的比特位1变成0,0变成1。
UDP校验和使用了CRC(循环冗余校验)的方法,把每个字节都当作整数进行累加,不管溢出,直至最终,得到校验和。
数据在发出端计算一次校验和,再放入报文中,传给目的端,目的端再算一次校验和,看看两个是不是一样。如果是一样,只能确定可能没有发生比特翻转;而不一样,肯定是发生比特翻转了。
UDP在发现校验和不同时只会丢弃,不会重发,如果要重发要我们自己写代码实现。
2.TCP
1)特点:有连接,可靠传输,面向字节流,全双工。
TCP会保存对端的信息,这就是有连接。TCP有两个核心机制保证其是可靠传输。TCP在读写数据的时候是以字节为单位的,支持任意长度,因此存在粘包问题。TCP支持读也支持写。
2)报文格式:
TCP报文内容与TCP的核心机制有关,具体各个部分是什么在下面解释。这里只是补充下面没有提到的。
16位紧急指针(URG)在标志位处有,用来跳过前面的数据,直接从某一个开始读。
PSH(催促标志位)发送方给接收方的数据中带有这个标志,接收方会尽快的将这个数据read。
3.TCP十大核心机制
1)确认应答
在日常生活中,我们怎判断我们叫了某个人他有没有听见,他回应一声不就行了。
TCP也是这么想的,我们在发送数据后,需要对方给一个应答报文(acknowledge,简称ack)。我们收到了这个ack后,就知道了目的端已经收到数据了。这就是TCP是可靠传输的一大原因。
但是在数据传输时可能会出现先发后到的情况。我们在发送数据后,这个数据会经过多个路由器/交换机。每个数据走的路线不同,可能先发的数据走的“路”比较长,花的时间更长,导致“来的比较完”。
针对这种情况,TCP会对载荷中的每个字节进行编号,32位序号就是载荷部分第一个字节的序号,序号连续递增。
32位确认序号的目的就是告诉发送者,我已经收到了那些数据,下次发送从确认序号这个位置开始发。32位确认序号的值就是收到的数据载荷的最后一位+1。注意,这个32位确认序号只在应答报文中生效。
那怎么才能知道这个TCP是应答报文?这就是上面标志位(下图)管的了。
可以看到第二个表示位是ACK,也就是应答报文的意思,如果这一位是 1 ,说明这个报文就是应答报文。
说回来,有了序号之后,应用程序会通过socketAPI读到正确的顺序,不用担心先发后到的情况了。
2)超时重传
超时重传是针对丢包问题进行的处理。丢包问题是不可避免的客观情况。
TCP规定了一个超时时间阈值,这个阈值不是固定不变的,是动态变化的。如果超过这个阈值就是触发重传,同时延长这个时间阈值。但是这个重传和时间阈值是有上限的,超过这个上限后就是放弃传输。
一次信息传输可能有两种丢包情况:
情况一:源端口发送的数据丢失,这个时候直接重传就可以了。
情况二:目的端发送的ack丢失,源端口迟迟没有收到ack,会认为是自己发送的数据丢失,重发一次。这时目的端会收到相同的数据,TCP有一个接收缓存区,数据会先到缓存区,如果发现数据已经存在了就丢弃,如果没有就放入。
3)连接管理
连接分为建立连接和断开连接。
TCP建立连接是通过“三次握手”来实现的。
syn(synchronized,同步)表示的是同步报文,在上图表示为中有,如果syn是1的话,表示这个报文是同步报文。当然,一个报文可以既是同步报文也是应答报文。
建立连接的过程:1. A先给B发一个同步报文。2. B收到后会给A发一个同步报文同时发一个应答报文,这两个报文可以分开,但没必要,因为这两个都是内核负责的,可以保证同一时机。3. A收到同步报文和应答报文后会发给B一个应答报文。
三次握手的作用:1.探一探网络的通信链路是否通畅,这个网络可靠传输的前提条件。2.验证双发的发送能力和接收能力是不是正常。3.协商关键信息,比如通讯序号从几开始。
TCP断开连接是通过“四次挥手”来实现的。
FIN(finish,完成),表示发送方已经没有数据要发送了,请求断开连接,FIN位为1表示FIN报文。FIN不是由内核负责,而是与我们写的程序有关,代码中调用socket.close或进程结束时才会发送。
断开连接的过程:1.A给B发一个FIN,请求断开连接。2.B给A一个ACK,表示收到A的请求。3.等到B的逻辑执行完了,给A发送FIN,B请求断开连接。4.A给B发一个ACK,表示收到B的请求。
下图是整个连接的全过程
介绍一下上图中出现的部分状态的含义:
ESTABLISHED:连接完成,可以发送数据了。
CLOSE_WAIT:被发起FIN的一方进入该状态,表示等待程序调用close方法。
TIME_WAIT:主动发起FIN的一方进入该状态,表示等待对方结束。主动发起方不会一直等待对方发FIN,而是由一个等待上限,上限时间是2*MSL(网络上两个任意节点传输过程中消耗的最大时间)。
4)滑动窗口
前面说TCP采用一问一答的方法来进行数据传输,这个其实效率比较低。如果我们一下发好几个,不用等待应答,这样就可以提高效率。我们一下发出好几个后,返回一个应答,就再发送下一条。这个不就是滑动窗口嘛。
窗口越大,批量发的数据越多,效率就越高。但是窗口不能无限大,太大了会影响可靠性。
滑动窗口丢包的情况:
这个问题不大,超时后重传。
正如上图说的那样,1001-2000的数据丢了后,主机B没有收到这个数据,其会再应答报文的确认序号哪里一直返回1001。主机A连续3次收到相同的确认序号时会意识到1001-2000这部分数据丢失了,会重发1001-2000。已经发的2001-7000受不受影响呢?答案是不受影响,为什么?
前面说了,在读数据的时候会按顺序读数据,读到1001,发现没有,队列堵塞,后面的数据一直堵在哪里,等到1001-2000来了后才继续读。这个队列不是一个纯粹的队列,我们发的每个数据都是有序号的,可以根据这个序号来准确的算出数据应该放的位置,如果没有就空着这个位置。
这种重传的方式叫快速重传,即只传丢了的数据,不传其他数据。注意快速重传是在滑动窗口下出现的,不要弄错情况。
5)流量控制
前面说了窗口越大,效率越高,但窗口过大,就是影响可靠性。为了提高效率的同时保证传输可靠性,TCP对流量进行了控制。
TCP有一个接收缓冲区,里面有一些待处理的数据。进入缓冲区的速度取决于发送方发送的速度,出缓冲区的速度取决于应用程序读取的速度。TCP可以根据处理数据的速度,反馈给发送方,限制它的发送速度。
怎么做到的?滑动窗口的大小会动态变化。在TCP的报文中有一栏就是滑动窗口的大小。
这个窗口的大小等于 接收缓冲区剩余空间的大小。剩的多了,就多放进来一点;剩的少了,就少放一点呗。
一旦发现返回的窗口的大小是0,发送方就会暂停发送,过一段时间发送一个窗口探测包,“问问”接收方的接收缓存区有没有空间,有空间了,继续传输数据。
6)拥塞控制
上面说的流量控制是依据接收的处理能力进行限制。而拥塞控制是依据传输链路的转发能力进行限制的。它通过不断试验的方法区找到一个合适的窗口大小,大了就减,小了就加,说白了就是“面多加水,水多加面”。
拥塞机制的工作过程:
先慢启动,再指数增长,再线性增长。发现丢包了,窗口减小,回到新的阈值处。
7)延时应答
默认情况,发送方发送数据后会立即返回ack,但是如果我们延时发送ack,效率会提高。
如果延时发送,接收缓冲区的数据会被处理更多,这个时候返回ack,会返回更大的滑动窗口大小。我们知道窗口越大,效率越高,这样延时发送就会提高效率。
所有包都可以延时应答吗?不是。有数量限制,每隔N个包就要应答一次。也有时间限制,超过最大延迟时间就应答一次。
8)捎带应答
基于延时应答,TCP可以将上次的ack捎并带回,这样将两个包放在一起传输,提高了效率。
9)面向字节流
前面在介绍TCP的特点的时候就提到了一点,因为TCP是面向字节流传输的,支持任意长度,因此就有粘包问题。
什么是粘包问题?粘包问题粘的是应用层数据包,各个包之间由于没有长度限制,而且是面向字节流,不是数据报,因此有时候会分不清字节要读到哪里停止,会出现多读或漏读的情况。粘包问题在TCP层面是无解的,我们要在应用层方面区解决这个问题。定义好应用层的协议,明确包之间的边界。
10)异常情况的处理
TCP通讯过程中会出现的特殊情况。
情况一: 进程崩溃/主机关机
本质上与主动退出没有区别
情况二: 主机断电/网线断开
分为接收方断电和发送方断电。
接收方断电:发送方发送的数据没有ack返回,超时重传后还没有返回。重传到一定次数会触发“重置TCP连接”,发送方会主动发一个复位报文(RST),如果还没有用,发送方就会单方面释放连接。
发送方断电:发送方断电后,接收方此时判断不出来发送方是断电了还是暂时没有数据发送。等待一段时间后接收方会发送一个特殊报文—“心跳包”,这个报文不携带数据,只是为了触发ack,跟上面的窗口探测一样。如果不跳了,就发一个复位报文,如果还不好使,就单方面释放连接。
三.网络层协议
1.IP协议基础
概念:用于唯一标识网络中的每台计算机。我们可以在cmd中查看ip地址:ipconfig。
IP地址的表示形式:点分十进制 xx.xx.xx.xx,每个十进制数的范围是0—255。
IP地址的组成:网络地址+主机地址。
同一个局域网当中网络地址必须相同,主机地址必须不同。相邻的局域网中网络地址必须不同,主机地址随意。
Ipv4是由4个字节(32位)表示,而I,Pv6是由16个字节(128位)表示。
IP协议的作用主要是下面两个:
1)地址管理,用来标识网络上的某个设备的位置。
2)路由选择,在两个通信的节点之间,规划出一个合理的路径。
这是以前使用的IPv4地址分类,现在不用了:
各类表示范围:
那现在的IPv4怎么分网络号和主机号呢?
打开ipconfig:
看到下面的子网掩码,255的表示网络地址位,0表示主机地址位。
特殊的IP地址:
1)将IP地址中的主机地址全部设成0就成了网络号,代表这个局域网
2)将IP地址中的主机地址全部设为1,就成为了广播地址,用于给同一个链路中相互连接的所有主机发送数据包
3)127.*的IP地址用于本机环回(loop back)测试,通常是127.0.0.1
2.IP协议报文
1)4位版本:IPv4或IPv6
2)4位首部长度:因为报头有选项这一栏,所以报头长度也是变长的
3)8位服务类型:决定了IP地址的工作方式,3位优先权字段(已弃用)+ 4位TOS字段 + 1位保留字段(必须设置为0)。4位TOS字段分别表示:最小延时,最大吞吐量,最高可靠性,最小成本。注意这四个TOS字段是不共存的,只能是其中一个,要根据实际情况选择。
4)16位总长度(字节数):报头+载荷的长度
5)16位标识、3位标志,13位片偏移:
IP协议内置了拆包组包的功能,拆完了包我们要给包一个标识,要不然怎么组包,怎么知道谁跟谁是一组的。16位标识位就是给包一个标识,拆出来的包都是一个标识,组包的时候就将相同标识的包组在一起。3位标志用来标志有没有触发拆包操作,并且记录这个包是不是最后一个包。13位片偏移描述了先后顺序,偏移小的放在前面,偏移大的放在后面
6)8位生存空间(TTL):一个IP数据报能在网络上传输的最大时间,单位是次数,IP数据报每经过一个路由器就TTL就减小一次
7)8位协议:标识传输层使用什么协议
3.NAT机制
NAT机制,即网络地址转换,是当今网络时间解决IPv4不够用的最重要的方式。
其将说有IP分成两类:一是公网/外网,另一是私网/内网。公网IP是唯一的,但是私网IP在不同的局域网中是可以重复的。
如上面的例子,运营商路由器具有NAT功能,能将我的设备的IP地址进行转换:192.168.100.1->100.1.1.1。
可能会有多个设备对运营商的服务器发送请求,但是到了运营商的路由器都会被转换成同一个IP
服务器收到请求后会返回给运营商路由器。但是运营商怎么知道这个哪一个设备发送的请求呢?
运营商在地址转换时会将两个地址存入一个记录映射关系的表格,记录替换前的地址和替换后的地址,同时也会记录端口。NAT设备进行转换的时候可以修改端口号,这一就避免了端口相同找不到源地址的情况。
4.路由选择
这里介绍路由选择的简单模型:探索式。
网络环境是非常复杂的,路由器没有办法将所有的网络信息都存储,只存储周围的网络情况。当数据包到了某个路由器,就会匹配这个路由器的路由表。路由表中记录了这个路由器周围的设备的IP地址是什么,以及记录每个设备要通过哪个口转发过去。
如果目的IP地址刚好匹配到了路由表中的记录,就直接转发到对应的口。
如果没有匹配到,路由表会有一个特殊的表项——下一跳,指向的设备就是上一级路由器所在的位置。
四.数据链路层
1.以太网
以太网帧格:
1)目的地址和源地址这里存储的地址不是IP地址,而是mac地址(物理地址)。
想看这个地址要在cmd中输入:ipconfig /all
2)类型是用来确定载荷的数据格式的
3)CRC:帧尾,校验和
4)ARP:根据IP地址,得到对应的mac地址。具体做法:通过广播地址,发送ARP数据报,询问周围的网络设备的IP地址和mac地址,路由器会构建出一个映射表来存储这些IP地址和ARP地址
五.应用层
1.DNS
可以认为是应用层的一个协议也可以认为是一个系统。这个系统就是域名解析系统(域名是什么在后面的网络基础知识中有)。
最初DNS通过一个hosts文件,这个文件中存储域名和IP地址的映射关系。我们在访问某个网站的时候,会先查询DNS服务器,把域名对应的IP拿到,再访问服务器。
现在全世界的服务器这么多,我们都访问DNS服务器,服务器会承受海量的并发量,容易挂。
这里有两个方法去解决:
1)缓存,我们再DNS服务器找到IP地址后会存储再缓存中,下次访问这个域名就不用去DNS服务器查找了
2)DNS服务器有很多,存储原始数据的是根服务器,各个网络运营商可以搭建镜像服务区。
2.InetAddress类
常用方法:
方法 | 说明 |
getLocalHost | 获取本机InetAddress对象 |
getByName | 根据主机名/域名 获取InetAddress对象 |
getHostName | 获取InetAddress对象的主机名 |
getHostAddress | 获取InetAddress对象的地址 |
代码示例:
//1.获取本机的InetAddress对象
InetAddress localHost = InetAddress.getLocalHost();
//输出设备名称和IP
System.out.println(localHost);
//2.根据主机名 获取InetAddress对象
InetAddress host1 = InetAddress.getByName("LAPTOP-RPIOC01F");
System.out.println(host1);
//3.根据域名 获取InetAddress对象
InetAddress host2 = InetAddress.getByName("www.bilibili.com");
System.out.println(host2);
//4.通过 InetAddress 对象,获取对应的地址
String hostAddress = host2.getHostAddress();
System.out.println(hostAddress);
//5.根据 InetAddress 对象,获取对应主机名/域名
String hostName = host2.getHostName();
System.out.println(hostName);
3.TCP网络通信
1)Socket
Socket开发网络应用程序被广泛采用,以至于成为事实上的标准。通信的两端都要有Socket,是两台机器间通信的端点。网络通信其实就是Socket间的通信。
Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
怎么理解这个Socket呢?可以从它的英文原意入手,socket->插座、插口。它就是一个插口,中间连接一个数据通道。
2)使用字节流
代码示例:
//客户端
//1.连接服务器(ip,端口),如果连接成功返回Socket对象
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
System.out.println("客户端返回:"+socket.getClass());
//2.连接上后,生成Socket对象,通过socket.getOutputStream()得到和socket对象关联的输出流对象
OutputStream outputStream = socket.getOutputStream();
//3.通过输出流,写入数据到数据通道
outputStream.write("hello".getBytes());
//4.设置结束标记
socket.shutdownOutput();
//5.关闭流对象
outputStream.close();
socket.close();
System.out.println("客户端退出");
//服务端
//1. 在本机的9999端口监听,等待连接
//要求在本机没有其他服务在监听9999
//这个 ServerSocket 通过accept() 返回多个Socket
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("服务端在9999端口监听,等待中。。。");
//2.当没有客户端连接9999端口时,程序会阻塞,等待连接
// 如果有客户端连接,则会返回Socket对象,程序继续
Socket socket = serverSocket.accept();
//3.通过 读取客户端写入到数据通道的数据
InputStream inputStream = socket.getInputStream();
//4.IO读取
byte[] bf=new byte[1024];
int readLen=0;
while((readLen=inputStream.read(bf))!=-1){
System.out.println(new String(bf,0,readLen));
}
//5.关闭流
inputStream.close();
socket.close();
serverSocket.close();
3)使用字符流传输文件
代码示例:
//客户端
//1.连接服务端,得到Socket对象
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
//2.创建读取磁盘文件的输入流
String path="C:\\JavaNet\\Java.png";
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
//将文件内容读入数组中
byte[] bytes=StreamUtils.streamToByteArray(bis);
//3.传输到服务端
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
bos.write(bytes);
bis.close();
socket.shutdownOutput(); //设置写入数据结束
//4.接收消息
InputStream inputStream = socket.getInputStream();
byte[] b=new byte[1024];
int readLen=0;
while ((readLen=inputStream.read(b))!=-1){
System.out.println(new String(b,0,readLen));
}
//5.关闭流
inputStream.close();
bos.close();
socket.close();
//服务端
//获得端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端在8888端口等待...");
//等待连接
Socket socket = serverSocket.accept();
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
//拿到文件
byte[] bytes=StreamUtils.streamToByteArray(bis);
String path="C:\\JavaNet\\pic.png";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(path));
bos.write(bytes);
bos.close();
//发送信息
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("已收到图片");
writer.flush(); //刷新内容到数据通道
socket.shutdownOutput();
System.out.println("已收到图片");
//关闭资源
writer.close();
bis.close();
socket.close();
serverSocket.close();
4)补充
使用缓冲流一定注意要刷新数据到数据管道,否则就白写了。
在写入完后一定要记得设置写入数据结束。
4.UDP网络通信编程
1)基本流程
核心的两个类/对象 DatagramSocket 与 DatagramPacket;建立发送端,接收端;发送数据前,建立数据包DatagramPacket对象;调用DatagramSocket的发送接收方法;最后关闭DatagramSocket。
2)应用
代码示例:
//发送端
//1.创建DatagramSocket对象,准备在9998接收数据
DatagramSocket socket = new DatagramSocket(9998);
//2.将需要发送的数据,封装到对象
byte[] data="秘制小汉堡".getBytes();
//说明:封装 DatagramPacket 对象,data ,长度,主机IP,端口
DatagramPacket packet =
new DatagramPacket(data, data.length, InetAddress.getByName("10.40.91.181"),9999);
socket.send(packet);
socket.close();
System.out.println("B结束了");
//接收端
//1.创建一个DatagramSocket对象,准备在9999接收数据
DatagramSocket socket = new DatagramSocket(9999);
//2.构建一个 DatagramPacket对象,准备接收数据
byte[] buf=new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
//3.准备接收数据,将通过网络传输的DatagramPacket对象填充到packet对象
//有数据就会接收,没数据就会阻塞
System.out.println("正在等待连接");
socket.receive(packet);
//4.可以把packet 进行拆包,取出数据,并显示
int length=packet.getLength(); //实际接收的数据字节长度
byte[] data=packet.getData();
String regStr = new String(data, 0, length);
System.out.println(regStr);
//关闭资源
socket.close();
System.out.println("A结束");
六.网络的基础概念
1.网络通信
两台设备之间通过网络实现数据传输,将数据通过网络从一台设备传输到另一台设备。
在java.net包下提供了一系列的类或接口,供程序员使用完成网络通信。
2.网络
两台或多台设备通过一定物理设备连接起来构成了网络。
根据网络的覆盖范围可分为下面几类:
1)局域网:覆盖范围最小,仅仅覆盖一个教室
2)城域网:覆盖范围较大,可以覆盖一个城市
3)广域网:覆盖范围最大,可以覆盖全国
3.域名和端口号
域名概念:将IP地址映射成域名,也就是将IP地址变成了网站。IP地址一堆数字,太难记了,转变成域名后方便记忆。
端口号概念:用于标识计算机上某个特定的网络程序。我们可以通过端口号来访问某一个域名下的某个服务。ip定位主机,端口定位服务。比如说我们访问了B站的域名,要想连上网站服务,就需要一个端口号来实现。
端口号就好像是房子的门,如果我们想要进入房子就要通过门。一个房子可能有多个门,同样一个主机也有多个端口号,每个端口号都对应着其自身的业务。
端口号表示形式:以整数的形式,范围0—65535(2个字节表示端口)
避免使用0—1024端口,因为0—1024都被占用了。常见的网络程序端口号:tomcat:8080 mysql:3306 Oracle:1521 sqlserver:1433。
原文地址:https://blog.csdn.net/lllsure/article/details/145148115
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!