16. 网络编程
学习目标:
- 掌握UDP编程
- 掌握TCP编程
所谓的网络编程,就是实现互联网络中的不同计算机上运行程序间的数据交换。Java网络编程,自然就是实现两个Java进程之间的数据交换。(启动一个main方法就是启动一个Java进程)
对于Java程序员而言,日常工作几乎不会直接涉及到底层网络协议和实现细节,网络编程的原理无论是出于面试还是实际开发的需求,都谈不上重要知识点。所以本章节的内容以实现功能为主,无意讲太多计算机网络的基础知识和概念。甚至,我会尽量排除这些知识和概念,我们一起来专注于Java代码本身。
网络编程三要素
对于实现Java网络编程,我们仅需要知道,两台不同计算机上的进程实现通信交互数据的条件,一般我们把这个概念称之为**“网络通信三要素”**。
这三个要素是:
- IP地址(InetAddress): 是计算机网络中主机的唯一标识,可以用来唯一确定一台主机。比如在一个局域网下,会给每台计算机分配一个IP地址,这个IP地址指向唯一的一台主机。
- 端口号(port):光有IP地址确定主机还不够,因为网络编程实现的是两个进程间的通信。端口号用来在网络编程中唯一确定一个(Java)进程。
- 传输协议(protocol):有了IP地址和端口号,就可以确定进行通信的主机和进程了。现在只需要双方都遵循一定的通信规则,就可以实现通讯了。这就是传输协议。常见的传输协议是TCP和UDP。
举个例子:
我想找一个人聊天
第一个条件:要先找到这个人(IP地址)
第二个条件:这个人要能正常听到我说话,耳朵。(端口号)
第三个条件:对方只能听懂汉语,那我就必须说汉语。(传输协议)
于是对于两台计算机的一次数据传输通信,就可以大致的、通俗的做以下描述:
A主机作为发送端首先要明确接收端的IP地址和端口号,其上的一个进程(比如Java进程)确认好要传输的数据,然后根据传输协议的不同,选择不同的方式进行传输。
在这里,我们就具体使用两种不同的传输协议来讲解Java网络编程,它们就是:
- UDP协议,该协议进行数据传输是一种面向无连接的、不可靠的,但效率更高的传输方式。它的最大特点是,要求传输的过程中将数据封装成数据包然后进行传输。
- TCP协议,该协议进行数据传输是一种面向连接的、稳定可靠的,但效率稍低的传输方式。它的最大特点是,需要先建立连接再进行传输,并且传输的过程通过IO流的形式出现(所以TCP网络编程需要使用Java IO)。
UDP编程
传输原理
发送端步骤
- 创建发送端的socket对象
- 把要发送的数据封装成数据报包
- send方法发送数据报包
- 释放资源close
接收端步骤
- 创建接收端的socket对象
- 创建用于接收的数据报包
- receive方法接收数据
- 解析数据报包
- 释放资源close
DatagramSocket
此类表示用来发送和接收数据报包的套接字。
构造方法
DatagramSocket(int port) 创建数据报套接字并将其绑定到本地主机上的指定端口。
成员方法
void | receive(DatagramPacket p) 从此套接字接收数据报包。 |
---|---|
void | send(DatagramPacket p) 从此套接字发送数据报包。 |
DatagramPacket
此类表示数据报包。
构造方法
用于发送的
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号
用于接收的
DatagramPacket(byte[] buf, int offset, int length) 构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。
成员方法
byte[] | getData() 返回数据缓冲区。 |
---|---|
int | getLength() 返回将要发送或接收到的数据的长度。 |
int | getOffset() 返回将要发送或接收到的数据的偏移量。 |
案例
v1 发送端发送消息,接收端接收并打印
package _22network.com.cskaoyan.udp.v1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* @description: 接收端
* @author: 景天
* @date: 2022/10/21 15:50
**/
public class Receiver {
public static void main(String[] args) throws IOException {
// - 创建接收端的socket对象
DatagramSocket datagramSocket = new DatagramSocket(9999);
//- 创建用于接收的数据报包
byte[] bytes = new byte[1024];
// DatagramPacket(byte[] buf, int offset, int length)
// 构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。
DatagramPacket receivePacket = new DatagramPacket(bytes, 0, bytes.length);
//- receive方法接收数据
System.out.println("receive before");
datagramSocket.receive(receivePacket);
System.out.println("receive after");
//- 解析数据报包
// | byte[] | getData() 返回数据缓冲区。 |
//| int | getLength() 返回将要发送或接收到的数据的长度。 |
//| int | getOffset() 返回将要发送或接收到的数据的偏移量。 |
byte[] data = receivePacket.getData();
int offset = receivePacket.getOffset();
int length = receivePacket.getLength();
String s = new String(data, offset, length);
System.out.println("接收到了:" + s);
//- 释放资源close
datagramSocket.close();
}
}
package _22network.com.cskaoyan.udp.v1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* @description: 发送端
* @author: 景天
* @date: 2022/10/21 15:50
**/
/*
v1 发送端发送消息,接收端接收并打印
*/
public class Sender {
public static void main(String[] args) throws IOException {
// - 创建发送端的socket对象
//DatagramSocket(int port)
// 创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramSocket datagramSocket = new DatagramSocket(8888);
//- 把要发送的数据封装成数据报包
String s = "hello udp";
byte[] bytes = s.getBytes();
InetAddress targetIP = InetAddress.getByName("127.0.0.1");
int port = 9999;
// DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
// 构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号
DatagramPacket sendPacket =
new DatagramPacket(bytes, 0, bytes.length, targetIP, port);
//- send方法发送数据报包
datagramSocket.send(sendPacket);
//- 释放资源close
datagramSocket.close();
}
}
v2 使用工具类优化v1
package utils;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @description:
* @author: 景天
* @date: 2022/10/21 16:02
**/
public class NetworkUtils {
// 用于获取发送的数据报包
public static DatagramPacket getSendPacket(String msg,String ip,int port) throws UnknownHostException {
// 把数据封装成包
byte[] bytes = msg.getBytes();
InetAddress targetIP = InetAddress.getByName(ip);
// 创建用于发送的数据报包
DatagramPacket sendPacket =
new DatagramPacket(bytes, 0, bytes.length, targetIP, port);
//最终 return 装满了数据的一个包
return sendPacket;
}
// 用于获取接收的数据报包
public static DatagramPacket getReceivePacket() {
byte[] bytes = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(bytes, 0, bytes.length);
// return 一个空的数据报包
return receivePacket;
}
// 用于解析数据报包的方法
public static String parseMsg(DatagramPacket receivePacket) {
byte[] data = receivePacket.getData();
int offset = receivePacket.getOffset();
int length = receivePacket.getLength();
String s = new String(data, offset, length);
// 最终返回包里的数据 String
return s;
}
}
v3 发送端接收端相互发送
package _22network.com.cskaoyan.udp.v3;
import utils.NetworkUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Scanner;
/**
* @description:
* @author: 景天
* @date: 2022/10/21 16:26
**/
public class Receiver {
public static void main(String[] args) throws IOException {
// 创建接收端的socket对象
DatagramSocket datagramSocket = new DatagramSocket(9999);
// 创建Scanner对象
Scanner scanner = new Scanner(System.in);
// while
while (true) {
// 接收逻辑
// 创建用于接收的数据报包
DatagramPacket receivePacket = NetworkUtils.getReceivePacket();
// receive
datagramSocket.receive(receivePacket);
//解析
String msg = NetworkUtils.parseMsg(receivePacket);
// 打印
System.out.println("接收到了来自:"+receivePacket.getSocketAddress()+
"的消息: " +msg);
// 发送逻辑
// 接收键盘数据
String s = scanner.nextLine();
// 把要发送的数据封装成数据报包
DatagramPacket sendPacket =
NetworkUtils.getSendPacket(s, "127.0.0.1", 8888);
// send
datagramSocket.send(sendPacket);
}
}
}
package _22network.com.cskaoyan.udp.v3;
import utils.NetworkUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Scanner;
/**
* @description: 发送端
* @author: 景天
* @date: 2022/10/21 16:20
**/
/*
v3 发送端接收端相互发送
*/
public class Sender {
public static void main(String[] args) throws IOException {
// 创建发送端的socket对象
DatagramSocket datagramSocket = new DatagramSocket(8888);
// 创建scanner对象
Scanner scanner = new Scanner(System.in);
// while循环
while (true) {
// 发送逻辑
// 先接收键盘数据
String s = scanner.nextLine();
// 把要发送的数据封装成数据报包
DatagramPacket sendPacket =
NetworkUtils.getSendPacket(s, "127.0.0.1", 9999);
// send
datagramSocket.send(sendPacket);
// 接收逻辑
// 创建用于接收的数据报包
DatagramPacket receivePacket = NetworkUtils.getReceivePacket();
// receive接收
datagramSocket.receive(receivePacket);
// 解析
String msg = NetworkUtils.parseMsg(receivePacket);
// 打印
System.out.println("接收到了来自:"+receivePacket.getSocketAddress()+
"的消息: " +msg);
}
}
}
v4 使用多线程优化v3
思路:
定义发送任务 SendTask 专门用来发送消息
定义成员变量
DatagramSocket datagramSocket;
String ip;
int port;
定义接收任务 receiveTask 专门用来接收消息
定义成员变量
DatagramSocket datagramSocket;
OnePerson:
创建用于发送的线程
创建用于接收的线程
AnotherPerson:
创建用于发送的线程
创建用于接收的线程
package _22network.com.cskaoyan.udp.v4;
import utils.NetworkUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* @description: 接收任务
* @author: 景天
* @date: 2022/10/21 16:49
**/
public class ReceiveTask implements Runnable {
// 定义成员变量
DatagramSocket datagramSocket;
public ReceiveTask(DatagramSocket datagramSocket) {
this.datagramSocket = datagramSocket;
}
@Override
public void run() {
// 只接收消息
while (true) {
// 创建用于接收的数据报包
DatagramPacket receivePacket = NetworkUtils.getReceivePacket();
// receive接收
try {
datagramSocket.receive(receivePacket);
// 解析
String msg = NetworkUtils.parseMsg(receivePacket);
// 打印
System.out.println("接收到了来自:"+receivePacket.getSocketAddress()+
"的消息: " + msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package _22network.com.cskaoyan.udp.v4;
import utils.NetworkUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* @description: 发送任务
* @author: 景天
* @date: 2022/10/21 16:45
**/
public class SendTask implements Runnable{
// 定义成员变量
DatagramSocket datagramSocket;
String ip;
int port;
public SendTask(DatagramSocket datagramSocket, String ip, int port) {
this.datagramSocket = datagramSocket;
this.ip = ip;
this.port = port;
}
@Override
public void run() {
// 只发送消息
// 创建Scanner对象
Scanner scanner = new Scanner(System.in);
while (true) {
// 键盘接收数据
String s = scanner.nextLine();
// 封装成数据报包
try {
DatagramPacket sendPacket = NetworkUtils.getSendPacket(s, ip, port);
// send
datagramSocket.send(sendPacket);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package _22network.com.cskaoyan.udp.v4;
import java.net.DatagramSocket;
/**
* @description:
* @author: 景天
* @date: 2022/10/21 16:52
**/
public class OnePerson {
public static void main(String[] args) throws Exception{
DatagramSocket datagramSocket = new DatagramSocket(8888);
// 创建用于发送的线程
//创建用于接收的线程
// start
new Thread(new SendTask(datagramSocket, "127.0.0.1", 9999)).start();
new Thread(new ReceiveTask(datagramSocket)).start();
}
}
package _22network.com.cskaoyan.udp.v4;
import java.net.DatagramSocket;
/**
* @description:
* @author: 景天
* @date: 2022/10/21 16:52
**/
public class AnotherPerson {
public static void main(String[] args) throws Exception{
DatagramSocket datagramSocket = new DatagramSocket(9999);
// 创建用于发送的线程
//创建用于接收的线程
// start
new Thread(new SendTask(datagramSocket, "127.0.0.1", 8888)).start();
new Thread(new ReceiveTask(datagramSocket)).start();
}
}
TCP编程
传输原理
客户端步骤(Client)
- 创建客户端Socket对象
- 从socket中获取输入输出流
- 利用输出输出流进行读写操作
- 释放资源close
服务端步骤(Server)
- 创建服务端的socket对象(ServerSocket)
- 通过accept建立连接, 得到socket对象
- 从socket中得到输入输出流
- 利用输入输出流进行读写操作
- 释放资源
Socket
此类实现客户端套接字
构造方法
Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号。
成员方法
InputStream | getInputStream() 返回此套接字的输入流。 |
---|---|
OutputStream | getOutputStream() 返回此套接字的输出流。 |
void | shutdownOutput() 禁用此套接字的输出流。 |
---|---|
Socket半关闭 |
ServerSocket
此类实现服务器套接字
构造方法
ServerSocket(int port) 创建绑定到特定端口的服务器套接字。
成员方法
Socket | accept() 侦听并接受到此套接字的连接。 |
---|---|
案例
v1 客户端发送消息,服务端接收并打印
package _22network.com.cskaoyan.tcp.v1;
import java.io.OutputStream;
import java.net.Socket;
/**
* @description: 客户端
* @author: 景天
* @date: 2022/10/21 17:31
**/
/*
v1 客户端发送消息,服务端接收并打印
*/
public class Client {
public static void main(String[] args) throws Exception{
//- 创建客户端Socket对象
// Socket(String host, int port)
// 创建一个流套接字并将其连接到指定主机上的指定端口号。
Socket socket = new Socket("127.0.0.1", 12306);
//- 从socket中获取输入输出流
// getOutputStream() 返回此套接字的输出流。
OutputStream out = socket.getOutputStream();
//- 利用输出输出流进行读写操作
out.write("hello tcp".getBytes());
//- 释放资源close
out.close();
}
}
package _22network.com.cskaoyan.tcp.v1;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description: 服务端
* @author: 景天
* @date: 2022/10/21 17:31
**/
public class Server {
public static void main(String[] args) throws Exception{
// - 创建服务端的socket对象(ServerSocket)
// ServerSocket(int port)
// 创建绑定到特定端口的服务器套接字。
ServerSocket serverSocket = new ServerSocket(12306);
//- 通过accept建立连接, 得到socket对象
System.out.println("accept before");
Socket socket = serverSocket.accept();
System.out.println("accept after");
//- 从socket中得到输入输出流
// getInputStream() 返回此套接字的输入流。
InputStream in = socket.getInputStream();
//- 利用输入输出流进行读写操作
byte[] bytes = new byte[1024];
int readCount = in.read(bytes);
String s = new String(bytes, 0, readCount);
System.out.println(s);
//- 释放资源
socket.close();
serverSocket.close();
}
}
v2 多个客户端发送,服务端接收(多线程处理)
package _22network.com.cskaoyan.tcp.v2;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @description: 客户端
* @author: 景天
* @date: 2022/10/21 17:43
**/
/*
v2 多个客户端发送,服务端接收(多线程处理)
*/
public class Client {
public static void main(String[] args) throws Exception{
// 创建客户端socket对象
Socket socket = new Socket("127.0.0.1", 8888);
Scanner scanner = new Scanner(System.in);
// 循环
while (true) {
// 键盘接收数据
String s = scanner.nextLine();
// 从socket中获取输出流
OutputStream out = socket.getOutputStream();
// write
out.write(s.getBytes());
}
}
}
package _22network.com.cskaoyan.tcp.v2;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @description: 服务端
* @author: 景天
* @date: 2022/10/21 17:43
**/
public class Server {
public static void main(String[] args) throws Exception{
// 创建服务端socket对象
ServerSocket serverSocket = new ServerSocket(8888);
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 循环
while (true) {
// accept方法建立连接
Socket socket = serverSocket.accept();
//new Thread(new ConnectTask(socket)).start();
// 提交任务
pool.submit(new ConnectTask(socket));
}
}
}
class ConnectTask implements Runnable{
// 定义成员变量
Socket socket;
public ConnectTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
while (true) {
// 得到socket对象
// 从socket中获取输入流
try {
InputStream in = socket.getInputStream();
// read
byte[] bytes = new byte[1024];
int readCount = in.read(bytes);
// 打印
String s = new String(bytes, 0, readCount);
System.out.println("接收到了来自" + Thread.currentThread().getName() +
socket.getInetAddress() + ":" + socket.getPort() + s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
v3 客户端发送对象(序列化),服务端接收
package _22network.com.cskaoyan.tcp.v3;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @description:
* @author: 景天
* @date: 2022/10/22 9:46
**/
/*
v3 客户端发送对象(序列化),服务端接收
*/
public class Client {
public static void main(String[] args) throws Exception{
// 创建客户端的socket对象
Socket socket = new Socket("127.0.0.1", 11111);
// 创建学生对象
Student student = new Student("张三", 20);
// 获取输出流 OutputStream
OutputStream outputStream = socket.getOutputStream();
// 创建序列化流对象ObjectOutputStream
ObjectOutputStream out = new ObjectOutputStream(outputStream);
// writeObject()
out.writeObject(student);
// 释放资源close
out.close();
socket.close();
}
}
package _22network.com.cskaoyan.tcp.v3;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description:
* @author: 景天
* @date: 2022/10/22 9:46
**/
public class Server {
public static void main(String[] args) throws Exception{
// 创建服务端的socket对象
ServerSocket serverSocket = new ServerSocket(11111);
// accept建立练级 得到socket对象
Socket socket = serverSocket.accept();
// 从socket中获取输入流InputStream
InputStream inputStream = socket.getInputStream();
// 创建反序列化流对象ObjectInputStream
ObjectInputStream in = new ObjectInputStream(inputStream);
// readObject()
Student student = (Student) in.readObject();
// 打印
System.out.println(student);
// close
socket.close();
serverSocket.close();
}
}
package _22network.com.cskaoyan.tcp.v3;
import java.io.Serializable;
/**
* @description:
* @author: 景天
* @date: 2022/10/22 9:47
**/
public class Student implements Serializable {
private static final long serialVersionUID = -1049823664553329306L;
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
v4 客户端上传文件到服务端
思路:
package _22network.com.cskaoyan.tcp.v4;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* @description:
* @author: 景天
* @date: 2022/10/22 10:07
**/
/*
客户端上传文件到服务器
*/
public class Client {
public static void main(String[] args) throws Exception{
// 创建客户端的socket对象
Socket socket = new Socket("127.0.0.1", 8888);
// 创建自己的输入流对象
FileInputStream in = new FileInputStream("D:\\a.jpg");
//FileInputStream in = new FileInputStream("D:\\b.txt");
// 从socket中获取输出流
OutputStream out = socket.getOutputStream();
// 边读边写
byte[] bytes = new byte[1024];
int readCount;
while ((readCount = in.read(bytes)) != -1) {
out.write(bytes,0,readCount);
}
System.out.println("while end");
// void shutdownOutput()
// 禁用此套接字的输出流。
socket.shutdownOutput();
// 循环结束 文件上传完成
// 接收来自服务端的反馈消息
// 从socket获取输入流
InputStream in2 = socket.getInputStream();
byte[] bytes1 = new byte[1024];
System.out.println("read before");
int readCount2 = in2.read(bytes1);
System.out.println("read after");
System.out.println(new String(bytes1,0,readCount2));
// close
in.close();
socket.close();
}
}
package _22network.com.cskaoyan.tcp.v4;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description:
* @author: 景天
* @date: 2022/10/22 10:07
**/
public class Server {
public static void main(String[] args) throws Exception{
// 创建服务端的socket对象
ServerSocket serverSocket = new ServerSocket(8888);
// 创建自己的输出流对象
FileOutputStream out = new FileOutputStream("server_a.jpg");
//FileOutputStream out = new FileOutputStream("server_a.txt");
// accept建立连接 得到socket对象
Socket socket = serverSocket.accept();
// 从socket中获取输入流
InputStream in = socket.getInputStream();
// 边读边写
int readCount;
byte[] bytes = new byte[1024];
while ((readCount = in.read(bytes)) != -1) {
out.write(bytes,0,readCount);
}
System.out.println("while end");
// 循环结束 保存完成
// 从socket中获取输出流
// 给客户端一个反馈消息
OutputStream out2 = socket.getOutputStream();
out2.write("文件已经上传成功".getBytes());
// close
out.close();
socket.close();
serverSocket.close();
}
}
常见异常
- java.net.ConnectException: Connection refused 先启动了Client会造成这个异常
- java.net.BindException: Address already in use: JVM_Bind 端口号被占用(换个端口)
三次握手四次挥手(了解)
三次握手
三次握手是TCP连接建立过程中的一种握手协议,目的是在客户端和服务器之间建立可靠的连接。
三次握手的过程如下:
- 第一次握手(SYN):客户端向服务器发送一个TCP数据包,其中包含一个特殊的标志位“SYN”(synchronize),表示请求建立连接。同时,客户端会设置一个随机的初始序列号。
- 第二次握手(SYN-ACK):服务器收到客户端的SYN请求后,会向客户端发送一个确认数据包,其中包含“SYN”和“ACK”(acknowledge)标志位,表示同意建立连接。服务器也会设置一个随机的初始序列号。
- 第三次握手(ACK):客户端收到服务器的SYN-ACK确认包后,会向服务器发送一个包含ACK标志位的数据包,表示客户端已经收到了服务器的确认。此时,双方都确认了初始序列号,TCP连接被成功建立。
四次挥手
四次挥手是TCP连接终止过程中的一种挥手协议,目的是在客户端和服务器之间安全地终止连接。四次挥手的过程如下:
- 第一次挥手(FIN):客户端决定关闭与服务器的连接时,会向服务器发送一个包含“FIN”(finish)标志位的数据包,表示客户端已经完成了数据传输,请求关闭连接。
- 第二次挥手(ACK):服务器收到客户端的FIN请求后,会向客户端发送一个包含ACK标志位的确认数据包,表示已经收到客户端的关闭请求。此时,客户端到服务器的连接已经关闭,但服务器到客户端的连接仍然打开。
- 第三次挥手(FIN):当服务器完成所有数据传输后,会向客户端发送一个包含FIN标志位的数据包,表示服务器也准备好关闭连接了。
- 第四次挥手(ACK):客户端收到服务器的FIN请求后,会向服务器发送一个包含ACK标志位的确认数据包,表示已经收到服务器的关闭请求。此时,服务器到客户端的连接也关闭。客户端等待一个预定的时间(2MSL,最长报文段生存时间的两倍)后,关闭整个TCP连接。
小结
三次握手和四次挥手是为了在客户端和服务器之间建立和终止可靠的连接。在这些过程中,双方通过交换带有特定标志位的数据包来传达自己的意图和状态。
为了更好地理解三次握手和四次挥手,可以将其视为一种对话:
三次握手(建立连接)
- 客户端:“你好,服务器,我想建立连接。”(发送SYN包)
- 服务器:“你好,客户端,我同意建立连接。”(发送SYN-ACK包)
- 客户端:“谢谢,服务器,我知道你同意建立连接了。”(发送ACK包)
四次挥手(终止连接)
- 客户端:“你好,服务器,我已经完成数据传输,请求关闭连接。”(发送FIN包)
- 服务器:“你好,客户端,我收到了你的关闭请求。”(发送ACK包)
- 服务器:“你好,客户端,我也已经完成数据传输,准备好关闭连接了。”(发送FIN包)
- 客户端:“谢谢,服务器,我知道你准备好关闭连接了。”(发送ACK包)
在实际应用中,TCP协议的三次握手和四次挥手过程可以确保双方在建立和关闭连接时达成共识,以便在网络通信中传输可靠的数据。
更详细的图解,可以参考一篇网络博客:详解三次握手和四次挥手_十九万里的博客