网络通信协议
通信协议
所谓通信协议,是指通信双方在进行数据交换时必须遵守的规则和约定。这些规则确保了双方能够有效地进行通信,实现信息的交换和资源共享。通信协议定义了传输时的数据格式、控制信息以及传输顺序和速度等,确保双方能够“说同样的语言”,从而成功地进行通信。
从产生"用户数据"到真正使用"通信电缆"将数据发送出去这个阶段,需要将用户数据进行各种封装与设计。而如何封装和设计,这里有两种模型可以选择:
-
TCP/IP模型 : 设计4个层次,来封装"用户数据"。 简单、方便、易用, 而被广泛使用。
-
OSI模型 : 设计了7个层次,来封装"用户数据"。该模型过于理想化,未能在因特网上进行广泛推广
通信协议有很多种。
-
应用层中,有HTTP、FTP、TELNET、DNS等。
-
传输层中,有TCP、UDP。
-
网络层中,有IP、ICMP、ARP、RARP等。
我们在这里举个例子,比如用户想要消息到另一个客户端,采用了HTTP、TCP、IP这些协议,以及调用了以太网接口。这个过程如下:
网络接口层
网络接口层,也叫数据链路层,该层的主要功能就是将准备要传输的数据的字节序列(0和1的序列),划分成数据帧从一个节点传输到临近的另一个节点,这些节点是通过MAC来唯一标识的。
-
封装成帧: 把网络层数据报加头和尾,封装成帧,帧头中包括源MAC地址和目的MAC地址。
-
透明传输:零比特填充、转义字符。
-
可靠传输: 在出错率很低的链路上很少用,但是无线链路WLAN会保证可靠传输。
-
差错检测(CRC):接收者检测错误,如果发现差错,丢弃该帧。
网络层
网络层最常用的通信协议,就是IP协议了。所有的TCP,UDP,ICMP,IGMP的数据都基于IP协议的数据格式进行传输的。在数据链路层中我们一般通过MAC地址来识别不同的节点,而在网络层我们也要有一个类似的地址标识,这就是IP地址。
IP是 Internet Protocol (网络互连协议),在计算机中,使用IP地址来描述一个上网终端的唯一的地址编号。分为 IPv4 和 IPv6。
IPv4 : 使用4个字节(32位)来描述一个IP地址,由四部分组成,每一部分一个字节。
IPv6 : 使用6个字节(48位)来描述一个IP地址,由六部分组成,每一部分一个字节。
32位IP地址分为网络位和地址位,这样做可以减少路由器中路由表记录的数目,有了网络地址,就可以限定拥有相同网络地址的终端都在同一个范围内,那么路由表只需要维护一条这个网络地址的方向,就可以找到相应的这些终端了。
IP 地址的分类:
A类: 1.0.0.1 ~ 126.255.255.254 :保留给政府机构
B类: 128.0.0.1 ~ 191.255.255.254 :分配给大中型企业
C类: 192.0.0.1 ~ 223.255.255.254 :分配给任何有需要的个人
D类: 224.0.0.1 ~ 239.255.255.254 :用于组播
E类: 240.0.0.1 ~ 255.255.255.254 :用于实验
IP协议头部
这里只说一个生存时间(TTL),这个不是一个时间的概念,实际上是指可以中转多少个路由器的意思,每经过一个路由器,TTL会减少 1,直到变成0 则丢弃该包。这个字段的最大值也就是255,也就是说一个协议包也就在路由器里面穿行255次就会被抛弃了,根据系统的不同,这个数字也不一样,一般是32或者是64
IP 作为整个 TCP/IP 协议族中至关重要的协议,主要负责将数据包发送给最终的目标计算机,为上层协议提供无状态、无连接、不可靠的服务。
-
无状态:无状态是指 IP 通信双方是不同步传输数据的状态信息。所有 IP 数据报的发送、传输和接收都是相互独立。
-
无连接:无连接是指 IP 通信双方都不长久的维持对方的任何信息。上层协议每次发送数据的时候,都需要明确指出对方的 IP 地址。
-
不可靠:不能保证 IP 数据报准确到达接收端,它只承诺尽最大努力交付。IP 模块一旦检测到数据报发送失败,就通知上层协议,而不会试图重传。
传输层
TCP/UDP都是传输层的协议,但是两者具有不同的特性,同时也具有不同的应用场景,下面以图表的形式对比分析。
TCP | UDP | |
可靠性 | 可靠 | 不可靠 |
连接线 | 面向连接 | 无连接 |
报文 | 面向字节流 | 面向报文 |
效率 | 低 | 高 |
双工性 | 全双工 | 一对一,一对多,多对一,多对多 |
流量控制 | 滑动窗口 | 无 |
拥塞控制 | 慢开始,拥塞避免,快重传,快恢复 | 无 |
应用场景 | 对效率要求低,对准确性要求高,有连接需求的场景 | 对效率要求高,对准确性要求低 |
面向报文
面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小
面向字节流
面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。
TCP和UDP协议的一些应用
应用层协议 | 应用场景 | 传输层 |
SMTP | 电子邮件 | TCP |
TELNET | 远程终端连接 | TCP |
HTTP | 万维网 | TCP |
FTP | 文件传输 | TCP |
DNS | 域名转换 | UDP |
TFTP | 文件传输 | UDP |
SNMP | 网络管理 | UDP |
NFS | 远程文件服务器 | UDP |
网络编程概述
其实,所谓的网络编程,就是编写程序,实现让同一个网络中的机器可以进行数据的传递,实现通信。
Java是 Internet 的语言,它从语言级上提供了对网络应用程序的支持。
Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制,并且Java 实现了一个跨平台的网络库,因此程序员面对的是一个统一的网络编程环境,很容易开发常见的网络应用程序。
网络编程必备的条件
如果要实现两台机器之间实现通信,必须要满足几点要求:
1、需要知道对方的 IP 地址。
2、需要知道对方的哪一个端口号来做数据的接收。
3、通信的双方,需要遵循相同的通信协议。
IP地址和端口号
端口是设备与外界进行通信的数据的出口。端口号的范围: [0,65535]。
常见的端口占用:
3306: MySQL
1521 :Oracle
8080 :Tomcat
80:HTTP
区别:
- IP地址(Internet Protocol Address)是一个独特的标识符,用于在网络中标识设备(如计算机、服务器、路由器等)。它可以是 IPv4(如 192.168.1.1)或 IPv6(如 2001:0db8:85a3:0000:0000:8a2e:0370:7334)格式。IP地址的主要功能是确定设备的位置,以便在网络上进行数据传输。
- 主要用于网络层,负责寻址和路由,帮助数据包找到目的地。每个连接到网络的设备必须有一个唯一的 IP地址,以便其他设备能够通过这个地址与它进行通信。
- 通常用字符串表示,IPv4格式为四组数字(每组0到255),用点分隔(如 192.168.1.1);IPv6格式为八组十六进制数字,用冒号分隔(如 2001:0db8:85a3:0000:0000:8a2e:0370:7334)。
- 连接到网络上的设备,如:
192.168.1.10
、10.0.0.5
。
- 端口号是一个 16 位的数字(范围在 0 到 65535),用于标识网络服务或应用程序。在同一台设备上,多个应用程序可以通过不同的端口号进行通信。例如,HTTP 服务通常使用端口 80,HTTPS 使用端口 443,FTP 使用端口 21。
- 主要用于传输层,负责识别设备上的特定服务或应用程序。通过组合 IP地址和端口号,可以确定特定设备上的特定应用程序(例如,通过
192.168.1.1:80
可以访问该 IP 地址下的 HTTP 服务)。- 仅为一个整数,通常在 IP 地址之后以冒号形式表示(例如,192.168.1.1:8080)。
- 如果你在浏览器中输入
http://192.168.1.10:8080
,这里的192.168.1.10
是目标设备的 IP地址,而8080
是指定的服务端口号。
IP地址和端口号一起构成了一个完整的网络地址,用于唯一标识在网络上的一个特定服务。例如,一个 web 服务器的地址可以表示为
192.168.1.10:80
,其中192.168.1.10
是 IP 地址,80
是端口号。
TCP/IP程序编程
C/S架构模型
CS架构由客户端和服务器两个部分组成,通常采用C/S模式进行通信。其中,客户端负责向用户提供界面和交互功能,而服务器则负责存储和处理数据。在这种架构中,客户端和服务器之间通过网络进行通信,客户端向服务器发出请求,服务器响应并返回相应的结果。
目前,CS架构已被广泛应用于各种场景中,如Web应用程序、电子邮件、数据库管理等,并且具有可扩展性、可靠性和安全性等优点。
TCP利用Socket(套接字)接口来实现C/S模型的网络程序开发,其早已被广泛的采用。通常,主动发起通信的应用程序属于客户端。而服务器则是等待通信请求,当服务器收到客户端的请求,执行需要的运算然后向客户端返回结果。
如我们使用QQ软件时,我们电脑上安装的就是一个客户端软件,而腾讯必需运行一个服务器。
Socket套接字介绍
socket被称为套接字,用于描述IP地址和端口号。主机上一般运行了多个服务软件,同时提供多种服务,每种服务都会打开一个Socket,并绑定到一个端口上,不同的端口对应不同的服务。
Socket和ServerSocket类位于java.net包中,ServerSocket用于服务端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,两个Socket构成一个通信管道。操作这个实例,完成所需要的通信。
套接字的基本操作有七个基本操作:
-
连接到远程主机
-
绑定到端口
-
接收从远程机器来的连接请求
-
监听到达的数据
-
发送数据
-
接收数据
-
关闭连接。
Socket编程步骤
1)服务端编程步骤如下:
-
调用 ServerSocket(int port) 创建一个服务器端套接字,并绑定到指定端口上。
-
调用 accept(),监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字。
-
调用 Socket类的 getOutputStream 和 getInputStream 获取输出流和输入流,开始网络数据的发送和接收。
-
最后关闭通信套接字。
2)客户段编程步骤如下:
-
创建 Socket。根据指定的 IP和port构造 Socket 类对象,并发送连接请求。如服务器端响应,则建立客户端到服务器的通信线路。
-
通过Socket获取与服务器的通信流。 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流。
-
按照一定的协议对 Socket进行读/写操作。通过输入流读取服务端响应的信息,通过输出流将信息发送给服务端。
-
关闭 Socket。断开客户端到服务器的连接,释放线路
3)流连接
-
客户端和服务器端的套接字对象诞生以后,必须进行输入、输出流的连接。
-
套接字调用 close()可以关闭双方的套接字连接,只要一方关闭连接,就会导致对方发生IOException异常。
三个常用类的API
InetAddress
InetAddress,是一个用来描述IP地址的类;常用的两个子类,分别是Inet4Address 、Inet6Address 。
InetAddress的实例对象由一个IP地址和可能与之对应的主机名(域名)组成
互联网中的主机地址有两种表现形式:
域名: www.baidu.com
IP地址: 110.242.68.4
域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。
- static InetAddress getLocalHost()
返回本地主机的地址。
- static InetAddress getByName(String host)
确定主机名称的IP地址。
- String getHostAddress()
返回文本显示中的IP地址字符串。
- String getHostName()
获取此IP地址的主机名。
//获取本机的信息InetAddress local = InetAddress.getLocalHost();System.out.println("主机名:"+local.getHostName());System.out.println("主机地址:"+local.getHostAddress());System.out.println("地址的字节数组形式:"+ Arrays.toString(local.getAddress()));//注意: getByName(String ip),该方法会主动与指定的IP地址发送请求连接,如果连接成功则会获取一些网络信息。// 如果该ip在互联网上不存在,则报异常:UnknownHostExceptionInetAddress byName = InetAddress.getByName("www.baidu.com");System.out.println("主机名:"+byName.getHostName());System.out.println("主机地址:"+byName.getHostAddress());System.out.println("地址的字节数组形式:"+ Arrays.toString(byName.getAddress()));
客户端编程:Socket
Socket是一个用来描述TCP中的客户端的类
常用构造器
- Socket(String ip,int port)
- Socket(InetAddress ip,int port)
ip是指要连接的服务端的IP
port是指要连接的服务端的端口号
常用方法
- InetAddress getLocalAddress()
返回对方Socket中的IP的InetAddress对象
- int getLocalPort()
返回本地Socket中的端口号
- InetAddress getInetAddress()
返回对方Socket中IP地址
- int getPort()
返回对方Socket中的端口号
- void close() throws IOException
关闭Socket,释放资源
- InputStream getInputStream() throws IOException
获取与Socket相关联的字节输入流,用于从Socket中读数据。
- OutputStream getOutputStream() throws IOException
获取与Socket相关联的字节输出流,用于向Socket中写数据。
try {//定义客户端的Socket,向服务端发送请求Socket client = new Socket(InetAddress.getByName("192.168.30.50"),10086);System.out.println(client);// 服务端的IP,Port,还有自己的端口号(系统随机分配的)System.out.println("服务端的端口号:"+client.getPort());System.out.println("服务端的IP:"+client.getInetAddress().getHostName());//如何向服务端发送信息呢? 通过套接字的输出流对象OutputStream outputStream = client.getOutputStream();PrintWriter pw = new PrintWriter(new OutputStreamWriter(outputStream,"utf-8"),true);//发送信息,调用输出方法while (true){try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}pw.println("是你吗?服务端");}} catch (IOException e) {e.printStackTrace();}}
服务端编程: ServerSocket
常用构造器:
- ServerSocket(int port)
通过指定一个端口号,来规定服务端某一个通信服务功能。
常用方法:
- Socket accept() throws IOException
用来接收客户端的请求,如果获得了请求操作,则返回与那个客户端相关的Socket套接字对象,如果没有获取请求操作前,具有阻塞效果。
注意:如果客户端莫名的死掉了,则与服务端断开连接了,服务端这边针对于那个客户端的socket就会断开,然后抛出异常
- void close()throws IOException
关闭监听Socket
- InetAddress getInetAddress()
返回此服务器套接字的本地地址
- int getLocalPort()
返回此套接字在其上监听的端口号
- SocketAddress getLocalSocketAddress()
返回此套接字绑定的端点的地址
- void setSoTimeout(int timeout) throws SocketException
设置accept()方法等待连接的时间为timeout毫秒。若时间已到,还没有客户端连接,则抛出InterruptedIOException异常,accept()方法不再阻塞,该倾听Socket可继续使用。若timeout值为0,则表示accept()永远等待。该方法必须在倾听Socket创建后,在accept()之前调用才有效。
BufferedReader br = null;try {//定义一个通信服务端口,用来搜集客户端的信息ServerSocket server = new ServerSocket(10086);//用于规定阻塞方法accept()的等待时间,如果超时,则抛异常,不再等待。server.setSoTimeout(10000);InetAddress inetAddress = server.getInetAddress();System.out.println("主机名:"+inetAddress.getHostName());System.out.println("主机地址:"+inetAddress.getHostAddress());System.out.println("地址的字节数组形式:"+ Arrays.toString(inetAddress.getAddress()));int localPort = server.getLocalPort();System.out.println("该通信服务端绑定的端口号:"+localPort);SocketAddress localSocketAddress = server.getLocalSocketAddress();System.out.println("本套接字绑定的Ip和端口号:"+localSocketAddress);Socket accept = server.accept(); //只有获取到请求连接时,才会继续向下执行。(阻塞效果)System.out.println("---服务端说:已经有一个客户端连接上了--");System.out.println("客户端的端口号:"+accept.getPort());System.out.println("客户端的IP:"+accept.getInetAddress().getHostName());//如何获取客户端发送过来的数据: 调用与那个客户端相关的套接字来获取输入流InputStream inputStream = accept.getInputStream();br = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));String line = null;while((line = br.readLine())!=null){ //注意,此时的readline()是有阻塞效果的System.out.println(line);}System.out.println("------over-----------");} catch (IOException e) {e.printStackTrace();}finally {try {br.close();} catch (IOException e) {e.printStackTrace();}}
传输层的TCP连接
三次握手
三次握手的原文是 three-way handshake
,整个名词的可以翻译为:需要三个步骤才能建立握手/连接的机制。当然,三次握手也可以叫 three-message handshake
,通过三条消息来建立的握手/连接。
进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的 初始化序列号(Init Sequense Number, ISN
) 为后面的可靠性传输做准备。
-
SYN
:连接请求/接收报文段 -
seq
:发送的第一个字节的序号 -
ACK
:确认报文段 -
ack
:确认号。希望收到的下一个数据的第一个字节的序号
刚开始客户端处于 Closed
的状态,而服务端处于 Listen
状态:
CLOSED :没有任何连接状态
LISTEN :侦听来自远方 TCP 端口的连接请求
1)第一次握手:客户端向服务端发送一个连接请求报文。报文中有SYN = 1,和 seq = x(x是随机的初始化序列号ISN,表示本报文段所发送的数据的第一个字节的序号)。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。这个三次握手中的开始。表示客户端想要和服务端建立连接。此时客户端处于 SYN_Send
状态。
SYN-SENT
:同步已发送状态
2)第二次握手:服务器收到客户端的 SYN 报文之后,如何同意连接,则发出确认报文。确认报文中有SYN = 1,ACK = 1,同时把 x + 1 作为确认号 ack 的值,并且指定自己的初始化序列号 ISN(y),即图中的 seq = y。这个报文也不能携带数据,但是同样要消耗一个序号。这个报文带有SYN(建立连接)和ACK(确认)标志,询问客户端是否准备好。此时服务器处于 SYN_REVD
的状态。
SYN-RECEIVED
:同步收到状态
3)第三次握手:客户端收到服务器端的确认报文后,还会再向服务器发送一个 确认报文。确认报文中有ACK = 1 ,同时把服务器的 ISN + 1 作为 ack 的值(ack = y + 1),表示已经收到了服务端发来的的 SYN 报文。并指明此时客户端的序列号 seq = x + 1(表示第二个报文)。
TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。这里客户端表示我已经准备好。
此时客户端处于 Establised
状态。服务器收到 ACK 报文之后,也处于 Establised 状态
,至此,双方建立起了 TCP 连接。
ESTABLISHED
:已建立连接状态
为什么要三次握手
三次够用了,不需要四次。但是二次握手就建立连接的话,有可能在网络波动的情况下让服务端产生连接错觉。
举例:已失效的连接请求报文段。
client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server
本来这已经是一个失效的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。
假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求
但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server的很多资源就没白白浪费掉了
采用三次握手就是为了防止这种情况的发生,server会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用。
连接中
建立连接后,两台主机就可以相互传输数据了。如下图所示:
1)主机A初始seq为1200,滑动窗体为100,向主机B传递数据的过程。
2)假设主机B在完全成功接收数据的基础上,那么主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。因此按如下的公式确认 Ack 号:
Ack号 = Seq号 + 传递的字节数 + 1 (这是在完全接受成功的情况下)
3)主机A获得B传来的ack(1301)后,开始发送seq为1301,滑动窗体为100的数据。
......
与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。上面说了,主机B完全成功接收A发来的数据才是这样的,如果存在丢包该如何。
下面分析传输过程中数据包丢失的情况,如下图所示:
上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试
重传数据。为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。
上面也只是一种可能,比如数据1250丢失,那么Ack返回的就是1250。
四次挥手
第一次挥手
客户端发送请求,请求断开连接,请求报文里有
-
FIN = 1 (表示数据已经完成了数据的发送,没有新的数据要发送了),
-
seq = u(等于前面已经传送过来的数据的最后一个字节的序号加1),
此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手
服务端收到客户端的请求断开连接的报文后,需要向客户端发送一个确认报文,该报文里有ACK = 1 ,ack = u + 1,seq = v(自己的序列号).
此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。
这个时候,客户端不能给服务器发送信息报文,只能接收。但是服务器要是还有信息要传给客户端,仍然能传送。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次挥手
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN = 1,ACK = 1,ack=u+1,seq=w(在半关闭状态,服务器很可能又发送了一些数据,序列化发生了变化)。
此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手
客户端接收到服务器的连接释放报文后,需要向服务器发送确认报文。
ACK=1,ack=w+1,seq=u+1(自己的序列号)。
此时,客户端就进入了TIME-WAIT(时间等待)状态。
注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
通信协议
TCP协议
是一个传输层的通信协议,是一个安全的,面向连接的通信协议。所谓的安全,指的是在数据传递的过程中,不会出现数据丢失的问题。 特点:安全的、面向连接。
TCP/IP 因其传输控制协议(TCP)和网络互联协议(IP)而得名。实际上是一组协议,其包含多个具有不同功能且互为关联的协议。
TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即网络接口层、IP层、传输层和应用层。
TCP与UDP的区别
运输层协议中有两个非常重要的协议:
-
传输控制协议TCP(Transmission Control Protocol)
-
用户数据报协议UDP(User Datagram Protocol)
TCP是面向连接的运输层协议。即应用进程(或程序)在使用TCP协议之前,必须先建立TCP连接,在传输完毕后,释放已经建立的连接。利用TCP协议进行通信的两个应用进程,一个是服务器进程。另一个是客户进程。
UDP是面向无连接的运输层协议。即应用进程(或程序)在使用UDP协议之前,不必先建立连接。自然,发送数据结束时也没有连接需要释放。因此,减少了开销和发送数据之前的时延。