网络编程--上篇
网络编程–上篇
本文只做归纳总结。
1.基础知识
①网络模型
这里引用一下菜鸟教程网站的介绍和图片,为了使不同计算机厂家生产的计算机能够相互通信,以便在更大的范围内建立计算机网络,国际标准化组织(ISO)在1978年提出了"开放系统互联参考模型",即著名的OSI/RM模型(Open System Interconnection/Reference Model)。它将计算机网络体系结构的通信协议划分为七层。除了标准的OSI七层模型以外,常见的网络层次划分还有TCP/IP四层协议以及TCP/IP五层协议:(如下图所示-来自菜鸟教程)
抛出一个问题:为什么要分层?
②协议
在上述的各层级中,有各种各样的协议,在计算机网络中,我们常见的协议有:IP协议,TCP协议,UDP协议,HTTP协议,WebSocket协议,HTTPS协议,FTP协议,DNS协议,SSH协议等等。
IP协议:网络层
互联网的基础协议,负责将数据从源主机通过路由传输到目标主机,通过IP 地址(如192.168.1.1)唯一标识网络中的设备。我们常说的IP地址在仅仅是网络层中用于标识一个节点(或者网络设备的接口)。网络标识唯一节点,便于数据包转发。
- IPv4:32 位地址(如
192.168.1.100
),地址空间约 43 亿,面临枯竭(通过 NAT 技术扩展)。 - IPv6:128 位地址(如
2001:0db8:85a3:0000:0000:8a2e:0370:7334
),地址空间几乎无限。
所有互联网通信的基础(如网页访问、文件传输都依赖 IP 寻址);路由器通过 IP 协议实现跨网络的数据转发
TCP协议 和 UDP协议 【传输层】
tcp:面向连接的可靠传输协议,确保数据从源应用到目标应用的有序、无丢失传输。当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。【一种面向连接的、可靠的、基于字节流的传输层通信协议】
UDP:无连接的不可靠传输协议,以高效率、低延迟为核心,不保证数据到达或顺序。 UDP协议全称是用户数据报协议。
他们二者的各自的特点:
TCP | UDP |
---|---|
面向连接:发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。 | 面向无连接,首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了 |
仅支持单播传输,只能进行点对点的数据传输,不支持多播和广播传输方式 | 有单播,多播,广播的功能,UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式 |
面向字节流,不保留报文边界的情况下以字节流方式进行传输 | UDP是面向报文的 |
可靠传输 | 不可靠性 |
提供拥塞控制 | 头部开销小,传输数据报文时是很高效的。 |
Http和WebSocket协议 【应用层】
HTTP(超文本传输协议)和WebSocket都是应用层协议,但它们在设计目标、通信方式和适用场景上有显著差异,同时也存在一定联系。
特性 | HTTP | WebSocket |
---|---|---|
通信模式 | 单向请求-响应,客户端发起请求,服务器响应后连接关闭(HTTP/1.1支持持久连接,但仍是客户端主动) | 全双工双向通信,连接建立后,服务器和客户端可随时主动发送数据 |
连接生命周期 | 短连接(每次请求需新建连接)或长连接(如HTTP/1.1的Keep-Alive,但需定期维护) | 持久连接,一次握手后长期保持,适合高频实时交互 |
头部开销 | 头部较大(如包含Cookie、缓存头等),每次请求需携带完整头部 | 头部极简(仅2-10字节),数据传输效率高 |
数据格式 | 纯文本(如HTML、JSON),需遵循特定MIME类型 | 支持二进制和文本数据,可传输任意格式(如Blob、ArrayBuffer) |
适用场景 | 静态资源加载、API调用、表单提交等 | 实时聊天、在线游戏、股票行情、协作编辑等需要低延迟双向通信的场景【实时】 |
上述两个协议的联系:
1.握手协议兼容
WebSocket通过HTTP协议完成握手升级:
- 客户端发送
HTTP Upgrade
请求(包含Sec-WebSocket-Key
等头部) - 服务器响应
101 Switching Protocols
状态码 - 此后通信切换为WebSocket协议(基于TCP,使用
ws://
或wss://
地址)
2.应用层协议层级
两者均位于TCP/IP协议栈的应用层,但WebSocket提供了更高效的双向通信机制,可视为HTTP的补充而非替代。
3.共用基础设施
- 均可通过防火墙、代理服务器
- 都支持TLS加密(HTTPS与WSS)
Https协议【应用层】
安全套接字层超文本传输协议(Hyper Text Transfer Protocol over Secure Socket Layer)。以安全为目标的HTTP通道,简单讲是HTTP的安全版本,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
http和https的区别
对比维度 | http | https |
---|---|---|
协议性质 | 明文传输,不加密 | 基于 SSL/TLS 加密协议,数据加密传输 |
默认端口 | 80 | 443 |
安全性 | 低,数据易被窃取、篡改或监听 | 高,通过加密、身份验证和完整性校验保证安全 |
证书要求 | 无需 CA 证书 | 需要申请 CA(证书颁发机构)签名的数字证书(或自签名证书,仅限测试) |
加密方式 | 无加密 | 使用 对称加密(如 AES)和 非对称加密(如 RSA)结合,通过证书验证身份 |
URL 前缀 | http:// | https:// |
性能影响 | 无加密开销,速度略快 | 存在加密 / 解密开销,首次连接时延迟略高(可通过 TLS 1.3 等优化) |
- HTTP 适合对安全性要求低的场景,但存在数据泄露风险。
- HTTPS 通过加密和证书机制保障安全,是现代网站的标准配置,尤其适用于涉及用户隐私或交易的场景。
- HTTPS 的核心优势:防止数据篡改、身份伪造和信息泄露,建立用户信任。
FTP 协议【应用层】
用于在网络中上传或下载文件的协议,支持交互式文件管理(如创建目录、重命名文件)
DNS 协议(域名系统)【应用层】
将人类可读的域名(如baidu.com
)解析为机器可读的 IP 地址(如14.215.177.38
)的分布式数据库系统
SSH协议(安全外壳协议) 应用层(基于 TCP)
用于加密远程登录和命令执行的协议,替代不安全的 Telnet,确保通信过程中数据不被窃取或篡改。
比如说我们平时管理员通过 SSH 登录 Linux 服务器,执行系统维护命令;
②网络连接
大家耳熟能详的“TCP三次握手、四次挥手”:计算机网络中的三次握手和四次挥手是TCP协议建立和终止连接的核心机制,确保数据传输的可靠性。
一些标志:
【ACK】:用以指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收的报文段的确认;
【RST】:用以指示连接的强制拆除,当接收到错误连接时会发送RST位置为1的报文;
【SYN】:用以指示连接的建立,该位为1的报文表示希望建立连接;
【FIN】:用以指示连接的终止,该位为1的报文表示希望断开连接;
【seq】:序列号
首先建立连接:三次握手,目的是为了确保客户端和服务器双方具备双向通信能力,同步初始序列号(ISN);
- SYN(客户端 → 服务器)
- 客户端发送SYN报文(SYN=1,seq=x),进入
SYN-SENT
状态。 - 作用:请求建立连接,携带初始序列号
x
。
- 客户端发送SYN报文(SYN=1,seq=x),进入
- SYN-ACK(服务器 → 客户端)
- 服务器收到SYN后,回复SYN-ACK报文(SYN=1,ACK=1,seq=y,ack=x+1),进入
SYN-RCVD
状态。 - 作用:确认客户端的SYN,并发送自己的初始序列号
y
。
- 服务器收到SYN后,回复SYN-ACK报文(SYN=1,ACK=1,seq=y,ack=x+1),进入
- ACK(客户端 → 服务器)
- 客户端回复ACK报文(ACK=1,seq=x+1,ack=y+1),进入
ESTABLISHED
状态。 - 服务器收到后也进入
ESTABLISHED
状态,连接建立。 - 作用:确认服务器的SYN,双方确认通信正常。
- 客户端回复ACK报文(ACK=1,seq=x+1,ack=y+1),进入
可以看出,ack标志是接收到的序列号+1
问题1:为什么需要三次?
原因有几个,但是主要原因是防止旧的重复连接初始化造成混乱,假设只有两次握手,客户端发送一个旧的SYN包(比如因网络延迟滞留的包)到服务器,服务器收到后,直接回复SYN-ACK并分配资源,进入连接状态。如果客户端发现这是一个旧的连接请求(比如网络等原因导致已超时),直接忽略服务器的SYN-ACK,导致服务器一直进入连接状态等待,浪费资源【TCP连接需要双方分配资源,在只有两次握手的情况下,服务器在收到SYN后立即分配资源,但客户端可能因超时或放弃连接,导致服务器资源浪费】。所以,第三次握手时,客户端必须回复ACK确认服务器的SYN,确保双方资源分配是同步的,如果客户端收到的是服务器的旧的SYN-ACK,会发送RST(重置)包终止连接,避免服务器误开连接。
第二个原因是确保双方通信能力对称,第一次握手(客户端→服务器),可以让服务器确认客户端的发送能力正常,自己的接收能力正常;第二次握手(服务器→客户端),可以让客户端确认服务器的发送和接收能力正常,自己的接收能力正常;第三次握手(客户端→服务器),可以让服务器确认客户端的接收能力正常,自己的发送能力正常。
举个例子,比如两个人打电话:经过三次确认后,双方确认通信正常,开始对话
A打给B:“喂,听得到吗?”(SYN)
B回答:“听到了,你能听到我吗?”(SYN-ACK)
A说:“能听到!”(ACK)
然后是断开连接:四次挥手,目的是为了双方安全关闭连接,确保所有数据传输完成。
- FIN(主动关闭方 → 被动关闭方)
- 主动方(如客户端)发送FIN报文(FIN=1,seq=u),进入
FIN-WAIT-1
状态。 - 作用:声明不再发送数据,但可接收数据。
- 主动方(如客户端)发送FIN报文(FIN=1,seq=u),进入
- ACK(被动方 → 主动方)
- 被动方(如服务器)回复ACK(ACK=1,seq=v,ack=u+1),进入
CLOSE-WAIT
状态。 - 主动方收到后进入
FIN-WAIT-2
状态。 - 作用:确认FIN,允许被动方继续发送剩余数据。
- 被动方(如服务器)回复ACK(ACK=1,seq=v,ack=u+1),进入
- FIN(被动方 → 主动方)
- 被动方处理完数据后,发送FIN报文(FIN=1,ACK=1,seq=w,ack=u+1),进入
LAST-ACK
状态。 - 作用:声明被动方不再发送数据。
- 被动方处理完数据后,发送FIN报文(FIN=1,ACK=1,seq=w,ack=u+1),进入
- ACK(主动方 → 被动方)
- 主动方回复ACK(ACK=1,seq=u+1,ack=w+1),进入
TIME-WAIT
状态,等待2MSL(最大报文生存时间)。 - 被动方收到后关闭连接,主动方等待2MSL后关闭。
- 作用:确保被动方收到ACK,防止报文丢失导致重传FIN。
- 主动方回复ACK(ACK=1,seq=u+1,ack=w+1),进入
问题1:为什么需要四次?
TCP连接是双向的(客户端和服务器可以同时发送和接收数据),因此关闭连接时,双方需要独立关闭各自的发送通道,主动关闭方(如客户端)关闭自己的发送通道,同时,被动关闭方(如服务器)也要关闭自己的发送通道。
因为被动关闭方的剩余数据需要处理,所以其第二次挥手(ACK)和第三次挥手(FIN)必须分开发送,服务器收到客户端的FIN
时,可能仍有数据未发送完毕,需要先回复ACK
确认关闭请求,然后继续发送剩余数据,最后再发送自己的FIN
。若合并为ACK+FIN
,会导致服务器无法发送剩余数据,破坏数据完整性。
我们与三次握手做一下对比:在三次握手中,服务器的SYN
和ACK
可以合并发送(SYN-ACK
),因为此时没有数据需要处理。但是在四次挥手中,ACK
和FIN
的发送存在时间差(等待数据处理),因此无法合并。
但是嚯,凡是都有但是,如果被动关闭方没有剩余数据需要发送,理论上可以将第二次挥手(ACK
)和第三次挥手(FIN
)合并,变为三次交互。比如说服务器收到FIN
后,立即回复ACK+FIN
,客户端回复ACK
。由于TCP协议标准要求严格处理数据完整性,因此我们还是要默认仍按四次挥手设计,确保兼容所有场景。
问题2:主动关闭方,为什么要有TIME_WAIT这个状态?为啥是等待2MSL?
TIME_WAIT状态是主动关闭连接的一方(发送最后一个ACK后)必须经历的状态,其核心目的是确保连接的可靠终止和避免新旧连接的数据混淆。如果在第四次握手中,主动关闭方最后一次ACK丢失,被动关闭方会在超时后重传FIN,有TIME_WAIT这个状态的话,主动关闭方在TIME_WAIT期间仍能接收该FIN,并重新发送ACK,确保被动关闭方正常关闭。如果没有TIME_WAIT这个状态,主动关闭方最后一次ACK丢失的话,服务端就会一直在 LAST_ACK 状态下等待。
旧连接数据混淆是什么意思呢?假设在没有TIME_WAIT这个状态的情况下A — B连接了,端口是8080,然后现在A发起关闭,经过了三次挥手了,A发起第四次挥手之后直接关闭了,由于网络延迟原因,这个第四次挥手的ACK包要过一段时间才会到B。现在C想要来连接B,同样端口8080,连接的时候,上次关闭连接第四次挥手的ACK包恰好在这个时候到B了,这不就是与旧连接数据混淆了嘛。
MSL 的单位是时间,TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
2. Java中的网络
Java 网络编程是指使用 Java 语言开发基于网络通信的应用程序,主要基于 TCP/IP 协议栈,支持从底层套接字编程到高层 Web 服务的多种网络通信方式。此外,Java 主要关注 传输层(TCP/UDP)和 应用层(HTTP、FTP 等)。
2.1 BIO模型
BIO(Blocking IO) 又称同步阻塞IO,一个客户端由一个线程来进行处理
在net包下面,一些与网络有关的类:
InetAddress
表示 IP 地址(IPv4 或 IPv6);
InetAddress.getByName(String host):根据主机名或文本形式的 IP 创建地址对象。
InetAddress.getLocalHost():获取本机地址。
getHostAddress()/getHostName():分别返回字符串形式的 IP 和主机名。
ServerSocket
面向连接的服务器套接字,用于监听并接受客户端的 TCP 连接。
关键方法:
new ServerSocket(int port):在指定端口监听。
accept():阻塞等待并返回客户端 Socket。
setSoTimeout(int timeout):设置 accept() 的超时时间。
工作模式:主线程循环 accept(),新连接交给工作线程或线程池处理。
Socket
面向连接的客户端套接字,用于 TCP(流式)通信
常见构造:
new Socket(String host, int port):直接建立到目标服务器的连接。
new Socket(InetAddress addr, int port)。主要方法:
getInputStream()/getOutputStream():拿到网络读写的字节流。
close():关闭连接。
与 ServerSocket 配合,实现点对点的请求-响应模型。
UDP 相关:DatagramSocket
/ DatagramPacket
提供无连接、不可靠的报文(数据报)通信:
DatagramPacket封装要发送或接收的报文数据及其目的地址、端口。构造时指定字节数组、长度,接收时自动填充。
DatagramSocket,用于发送(send(packet))和接收(receive(packet)) DatagramPacket。也可通过 new DatagramSocket(port) 绑定本地端口。
MulticastSocket继承自 DatagramSocket,用于 IP 组播(将报文发给一组订阅者)。
joinGroup(InetAddress mcastaddr):加入组播组。
leaveGroup(InetAddress mcastaddr):离开组播组。NetworkInterface表示本地网络接口,如以太网卡、Wi-Fi 适配器等。
NetworkInterface.getByName(String name)、getNetworkInterfaces():列出本机所有网络接口。可查询接口状态、IP 地址列表、MAC 地址等信息。
URL
/ URLConnection
用于简单的基于 URL 的网络资源访问。
URL url = new URL("http://example.com/index.html");
URLConnection conn = url.openConnection();
通过 conn.getInputStream()
读取远程资源;可设置请求头、超时、缓存等。
具体子类:HttpURLConnection
(HTTP 协议);JarURLConnection
(访问 JAR 内部资源)
URI
表示统一资源标识符,比 URL
更通用/轻量。支持对各部分(scheme、host、path、query、fragment)进行解析与重组。此外还可以从 URI
生成 URL
,也可做路径规范化/相对路径解析。
简单客户端服务端示例:
// server.java
public class NetServer {public static void main(String[] args) {// ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());ExecutorService pool = Executors.newFixedThreadPool(2);try ( ServerSocket socket = new ServerSocket(9999) ) {while ( true ) {Socket client = socket.accept();pool.execute(new Handler(client));}} catch (IOException e) {throw new RuntimeException(e);} finally {pool.shutdown();}}private static class Handler implements Runnable {private Socket client;public Handler(Socket client) {this.client = client;}@Overridepublic void run() {if ( client == null ) return;try {while ( true ) {byte[] bytes = new byte[256];int read = client.getInputStream().read(bytes);if ( read != -1 ) {String message = new String(bytes, 0, read);if ( "bye".equals(message) ) break; // 断开连接System.out.println("客户端发来消息:" + message);client.getOutputStream().write("hello, 我是服务器".getBytes());} else {break;}}} catch (IOException e) {throw new RuntimeException(e);} finally {try {if ( client != null ) client.close();} catch (IOException e) {throw new RuntimeException(e);}}}}
}
// client.java
public class NetClient {public static void main(String[] args) {Scanner in = new Scanner(System.in);try ( Socket client = new Socket("localhost", 9999) ) {// 第一个线程写东西给服务器Thread w = new Thread(() -> {try {while (true) {String msg = in.nextLine();client.getOutputStream().write(msg.getBytes());client.getOutputStream().flush();if ( "bye".equals(msg) ) break;}} catch (IOException e) {throw new RuntimeException(e);}});w.start();// 第二个线程读取服务器发来的消息Thread r = new Thread(() -> {while (true) {try {byte[] bytes = new byte[256];int read = client.getInputStream().read(bytes);if ( read == -1 ) break;System.out.println("【服务器】:" + new String(bytes, 0, read));} catch (IOException e) {throw new RuntimeException(e);}}});r.start();w.join(); r.join();} catch (IOException | InterruptedException e) {throw new RuntimeException(e);} finally {in.close();}}
}
2.2 NIO模型
…。。。我再写的话,也只能是对他拙劣的模仿了,看他的文章,犹如久旱逢甘霖,他乡遇故知。
建议直接看这篇文章吧,我认为无人出其右了。深入理解BIO、NIO、AIO线程模型
地址(电脑打开更佳):https://blog.csdn.net/qq_45076180/article/details/112698579
同时这篇文章也值得一看:「操作系统」什么是用户态和内核态?为什么要区分
地址(电脑打开更佳):https://blog.csdn.net/u014571143/article/details/129660010
2.3.1 零拷贝技术
**零拷贝(Zero-copy)**技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。
为什么这里要介绍一下该技术呢?那是因为在传统的数据传输过程通常需要经历多次内存拷贝。在传输数据的过程中,会从从磁盘读取数据,将数据从内核空间拷贝到用户空间,再从用户空间拷贝到应用程序的内存中。这些额外的拷贝会消耗大量的CPU资源和内存带宽,降低数据传输的效率。零拷贝就是为了避免这些不必要的数据拷贝,能够将数据直接传输到目标内存区域,以提高数据传输的效率。如下图(文件传输)所示
上图中的DMA是什么呢?从上图可以看到如果这四次拷贝,都要需要 CPU 亲自参与搬运数据的过程,并且这个过程,CPU 是不能做其他事情的,当数据量很大的时候,CPU忙得过来吗。所以就有了DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输。简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
但是 DMA 有其局限性,DMA 仅仅能用于设备之间交换数据时进行数据拷贝,但是设备内部的数据拷贝还需要 CPU 进行,例如 CPU 需要负责内核空间数据与用户空间数据之间的拷贝(内存内部的拷贝),如上图中的磁盘设备与内存、网卡设备与内存,他们在电脑中是不同的设备。
如上图【文件传输】,我仅仅是想要发送一份数据,却经历了四次上下文切换、四次数据拷贝,这样的效率肯定是不行的,所以就要在上下文切换和拷贝次数上面做文章了。
那么,零拷贝并不是没有拷贝数据,而是 CPU 不再全程负责数据拷贝时的搬运工作、尽可能减少用户态/内核态的切换次数以及CPU拷贝的次数。
零拷贝技术的具体实现方式有很多,例如:
- sendfile
- mmap
- splice
- 直接 Direct I/O
sendfile:它主要是用到了DMA、传递文件描述符代替数据拷贝: 如下图所示
工作流程大致如下【从上图可以看出,只有两次上下文切换,然后CPU是0次拷贝】
- DMA从磁盘拷贝数据到内核缓冲区。
- 将内核缓冲区的数据描述符(地址、长度)直接传递给Socket缓冲区【可能会有CPU拷贝】。
- DMA将数据从内核缓冲区拷贝到网卡。
注意的是:只有网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术才可以通过传递文件描述符的方式避免内核空间内的一次 CPU 拷贝
mmap:mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。这样就减少了一次用户态和内核态的CPU拷贝,但是在内核空间内仍然有一次CPU拷贝。
splice: splice() 的主要优势在于减少了数据拷贝的次数和数据在用户空间和内核空间之间的来回传输,从而显著提高了数据传输的效率。但它也有一些局限,它的两个文件描述符参数中有一个必须是管道设备。
Direct I/O: 顾名思义,数据直接存储在用户空间中,绕过了内核
end.参考
- 菜鸟教程 – 【https://www.runoob.com/w3cnote/summary-of-network.html#_label0】
- https://blog.csdn.net/ymb615ymb/article/details/123449588 【tcp、udp】
- https://blog.csdn.net/weixin_45321483/article/details/117968774 【https协议】
- https://blog.csdn.net/spade_Kwo/article/details/119464901 【详解 TCP 连接的“三次握手”与“四次挥手” ---- (好文)建议读者阅读】
- https://blog.csdn.net/Allen_Adolph/article/details/107117156 【TCP协议的流程图解 ---- (好文)建议读者阅读】
- https://blog.csdn.net/qq_45076180/article/details/112698579 【深入理解BIO、NIO、AIO线程模型---- **(好文)**建议读者阅读】
- https://www.cnblogs.com/xiaolincoding/p/13719610.html 【零拷贝】
- https://segmentfault.com/a/1190000044068914#item-4-4 【零拷贝–思否】