额外知识点
软件结构
-
C/S结构
全称为Client/Server结构,指客户端和服务器结构。常见的程序有QQ、微信、LOL等软件(即需要下载安装的软件)
-
B/S结构
全称为Browser/Server结构,指浏览器和服务器结构。常见的浏览器有IE、谷歌等。(即不需要下载安装,在浏览器中直接可以访问使用)
不论是C/S结构还是B/S结构,都离不开网络的支持(即网络编程)
网络编程
- 定义
在网络通信协议下,不同计算机上运行的程序可以进行数据传输(即让两台计算机可以进行数据交互)。比如:发微信
- 网络编程三要素
-
IP地址
设备在网络中的地址,是唯一标识
-
端口
应用程序在设备中唯一的标识
-
协议
数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
IP
-
IP定义
全称为:“互联网协议地址”,也称IP地址,是分配给上网设备的数字标签。常见的IP分类为:IPv4和IPv6
-
IP分类
IPv4: 是互联网协议的第四个版本,是互联网上广泛使用的网络层协议之一。它是TCP/IP协议族的核心协议之一,用于在网络上识别和定位设备,并且为它们提供唯一的地址。
写法: IPv4地址通常由四个十进制数构成,每个数的取值范围是0到255,四个数之间以点号(.)分隔。
IPv4地址是一个32位的二进制数,通常以点分十进制的形式表示,如192.168.0.1。这些地址被用来标识网络上的设备,类似于街道地址用于标识房屋。IPv4地址被分为网络地址和主机地址两部分,网络地址用于标识网络,而主机地址则用于标识网络中的具体设备。
IPv6: 是互联网协议的第六个版本,是IPv4的后继版本,旨在解决IPv4中地址空间有限的问题。IPv6采用128位地址,相比IPv4的32位地址,IPv6拥有更加庞大的地址空间,可以为全球范围内的设备提供足够的唯一地址。
写法: Pv6地址通常由8组16位的十六进制数构成,每组之间以冒号(:)分隔
由于IPv4的地址空间是有限的,总共有约42亿个可用的地址。然而,由于互联网的快速发展和设备的爆炸性增长,IPv4地址已经不足以满足需求。因此,IPv6(Internet Protocol version 6)被设计出来扩展地址空间,并逐渐取代IPv4成为下一代互联网协议。IPv6采用128位地址,拥有更广阔的地址空间,可以支持更多的设备连接到互联网上。
- 常用命令—在cmd中
命令 | 解释 |
---|---|
ipconfig | 查看本机IP地址 |
ping IP | 检查网络是否连通(即检查你的电脑与你想要连接的那台电脑两者之间的网络是否畅通) |
ipconfig
ping IP
-
特殊IP地址
127.0.0.1:回送地址(即本地回环地址),可以代表本地的IP地址,一般用来测试使用
该IP地址代表连接自己的电脑
InetAddress类
Java提供了InetAddress类来方便我们对IP地址的获取和操作,它表示Internet协议(IP)地址
-
注意
该类没有构造方法(一般没有构造方法的类都会提供一个静态方法来返回该类的对象供操作者使用)
静态方法 | 解释 |
---|---|
public static InetAddress getByName(String host) | 返回指定主机名对应的 InetAddress 对象 |
普通方法 | 解释 |
public String getHostName() | 获取此IP地址的主机名。 |
public String getHostAddress() | 返回文本表示中的IP地址字符串。 |
注意:
字符串参数
host
,代表要获取 IP 地址的主机名或 IP 地址字符串。如果host
参数是一个有效的主机名,则该方法将返回该主机名对应的 IP 地址;如果host
参数是一个有效的 IP 地址字符串,则该方法将返回该 IP 地址本身。如果无法解析host
参数,则该方法将抛出UnknownHostException
异常。
主机名一般查看步骤:桌面→电脑右键→属性,弹出的设备规格中的设备名称即为主机名,如图所示
public class TestOne {
public static void main(String[] args) throws UnknownHostException {
InetAddress address = InetAddress.getByName("DESKTOP-KICELNQ");
String hostName = address.getHostName();
System.out.println("主机名为:" + hostName);
String ip = address.getHostAddress();
System.out.println("ip为:" + ip);
}
}
端口
-
定义
是一个通信端点,用于标识特定的应用程序或服务。
是应用程序在设备中的唯一标识
-
端口号
是一个16位的整数,取值范围从0到65535。其中0~1023之间的端口号用于一些知名网络服务或应用,所以我们自己使用1024以上的端口号就可以了
端口号被用来区分同一台设备上运行的不同网络应用程序或服务(即一个端口号只能被一个应用程序使用),使得网络数据能够正确地路由到目标应用程序。
协议
-
定义
计算机网络中,连接和通信的规则被称为网络通信协议
-
协议分类
UDP协议和TCP协议
UDP协议
-
定义
用户数据报协议(User Datagram Protocol)
UDP是面向无连接通信协议
速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据
-
优缺点
- 优点
- 低延迟、速度快: UDP协议不需要建立连接和维护状态信息,因此具有较低的通信延迟。适用于对实时性要求较高的应用场景,如实时音视频传输、在线游戏等。
- 轻量级: 适用于数据量较小、传输速度要求较高的场景。
- 简单: 适用于对系统资源要求较低的场景。
- 支持广播和多播: UDP协议支持向多个目标地址同时发送数据,适用于一对多通信的场景,如视频直播、多播文件传输等。
- 缺点
- 不可靠性: UDP协议不提供数据的可靠传输,数据包可能会丢失、重复、乱序等,需要应用层自行处理数据的丢失和错误,增加了应用程序的复杂性。
- 无拥塞控制: UDP协议不提供拥塞控制机制,无法根据网络负载情况调整发送速率,容易造成网络拥塞和数据包丢失。
- UDP 协议的数据包大小有限制: 大多数操作系统对 UDP 数据包的大小都有一定的限制,通常是在64KB到64MB之间。
- 无流量控制: UDP协议不提供流量控制机制,发送端可能会以过快的速度发送数据,导致接收端无法及时处理,造成数据包丢失或缓冲区溢出。
- 优点
UDP通信程序
发送端
-
用到的类构造器
DatagramSocket
构造方法解释 public DatagramSocket()
创建一个 UDP 套接字( DatagramSocket
对象),并在操作系统中自动分配一个可用的端口号,以便在该端口上接收和发送 UDP 数据包。注意:
客户端UDP套接字通常用于向服务器端发送数据报,但它也可以接收来自服务器端的数据包。这取决于UDP通信的需求来设计和实现UDP套接字的发送和接收功能。
DatagramPacket
构造方法解释 public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
构造一个数据报包,用于将长度为 length
的数据包发送到指定主机上的指定端口号。 (即创建一个DatagramPacket
(数据报包)对象,用于在 UDP 通信中封装要发送或接收的数据及其相关信息。)注意:
byte[] buf
:用于存储要发送或接收的数据。数据报包中的数据将从这个字节数组中读取或写入。
int length
:是一个整数,表示要发送或接收的数据的长度。在发送数据时,通常设置为要发送数据的实际长度;在接收数据时,通常设置为接收缓冲区的大小,用于指定接收的数据长度。(即指定字节数组中要发送数据的长度)
InetAddress address
:是一个 InetAddress 对象,表示要发送或接收数据的目标主机的 IP 地址。
int port
:是一个整数,表示要发送或接收数据的目标主机的端口号。 -
方法
DatagramSocket
方法解释 public void send(DatagramPacket p)
从此套接字发送数据报包 public void close()
关闭此数据报套接字。 DatagramPacket
方法解释 public int getLength(DatagramPacket p)
返回要发送的数据的长度或接收的数据的长度。 -
发送端发送数据步骤
- 创建发送端的
DatagramSocket
对象 - 创建一个
DatagramPacket
(数据报包)对象,封装要发送的数据及其相关信息 - 调用DatagramSocket对象的方法发送数据
- 释放资源
- 创建发送端的
public class TestTwo {
public static void main(String[] args) throws Exception {
//第一步:创建发送端对象
DatagramSocket ds = new DatagramSocket();
//第二步:创建数据并把数据打包
//第一个参数byte字节数组
String s = "你好啊,你好不好";
byte[] bytes = s.getBytes();//将你要发送的数据打包成字节数组
//第三个参数:要发送的目标主机IP地址
InetAddress address = InetAddress.getByName("10.201.196.231");//此处以自己本机IP试验
//第四个参数:要发送数据的目标主机中的端口号
int port = 1000;
DatagramPacket dp = new DatagramPacket(bytes, 8, address, port);
//第三步:调用DatagramSocket类中的方法发送数据
ds.send(dp);
//第四步:释放资源
ds.close();
}
}
由运行截图可知运行后没有任何结果,原因如下:
UDP是面向无连接通信协议,所以不论发送端是否与接收端创建了连接,发送端都可以发送数据包,不管接收端是否能够收到,所以若想接收端收到数据包则需要写出接收端接收数据的代码
接收端
-
构造器
DatagramSocke
t构造方法解释 public DatagramSocket(int port)
创建一个 UDP 套接字( DatagramSocket
对象),并将其绑定到本地主机指定的端口号上,以便在该端口上接收和发送 UDP 数据包。DatagramPacket
构造方法解释 public DatagramPacket(byte[] buf, int length)
构造一个数据报包,用于接收长度为 length
数据包。 -
方法
DatagramSocket
方法解释 public void receive(DatagramPacket p)
从此套接字接收数据报包。 public void close()
关闭此数据报套接字。 DatagramPacket
方法解释 public int getLength(DatagramPacket p)
返回要发送的数据的长度或接收的数据的长度。 -
接收端接受数据的步骤
- 创建接收端
DatagramSocket
对象 - 创建一个
DatagramPacket
(数据报包)对象,封装要接收的数据及其相关信息 - 调用
DatagramSocket
对象的方法接收数据 - 解析数据包并把数据在控制台显示
- 释放资源
- 创建接收端
public class TestTwo {
public static void main(String[] args) throws Exception {
//第一步:创建接收端DatagramSocket对象
//此处参数表示接收端从10000端口号接收数据的(因为发送数据时是将数据发送到了10000端口号),若无参数则代表从一个随机端口接收数据
DatagramSocket ds = new DatagramSocket(10000);
//第二步:创建一个 `DatagramPacket` (数据报包)对象,封装要接收的数据及其相关信息
//第一个参数:byte数组
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//第三步:调用DatagramSocket对象的方法接收数据
ds.receive(dp);
//第四步:解析数据包并把数据在控制台显示
byte[] data = dp.getData();
int length = dp.getLength();//返回接收到的数据长度,这样在控制台显示时就不会把字节数组中没有数据的部分以空格显示出来了
System.out.println(new String(data, 0, length));//将字节中的内容变为字符串打印出来
/*
第四步可写为
int length = dp.getLength();
System.out.println(new String(bytes, 0, length));
*/
//第五步:释放资源
ds.close();
}
}
- 注意
- 运行时必须先运行接收端在运行发送端
- 若接收端在启动之后没有接收到数据则会陷入阻塞状态(即死等)
- 在接收数据时可调用
getLength()
方法,表示所接收到的数据字节长度,这样在打印时就不会有空格 - 可以不创建data字节数组,因为接收的数据最后是存放在了创建的
DatagramPacket
(数据报包)对象的第一个参数字节数组bytes
中
练习
按照下面要求实现程序
UDP发送数据:数据来源于键盘录入,直到输入的数据是886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
- 发送端
public class Sender {
public static void main(String[] args) throws Exception {
Scanner input = new Scanner(System.in);
System.out.println("请输入要发送的数据:");
DatagramSocket ds = new DatagramSocket();
InetAddress address = InetAddress.getByName("10.201.196.231");
int port = 10000;
String s;
byte[] bytes;
while (! "886".equals(s = input.nextLine())) {
bytes = s.getBytes();
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
ds.send(dp);
}
ds.close();
}
}
- 接收端
public class Receiver {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket(10000);
while (true) {
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
ds.receive(dp);
int length = dp.getLength();
System.out.println(new String(bytes, 0, length));
}
}
}
UDP三种通信方式
- 三种通信方式
- 单播—一对一
- 将数据包从一个发送方传输到一个特定的接收方。已经学过的上述内容即为单播
- 组播—一对多
- 向特定的一组主机发送数据包,而不是整个网络
- 组播允许一个发送方将数据发送到一个特定的多播组地址,所有加入了该组的主机都可以接收到该数据包。与单播(Unicast)和广播(Broadcast)不同,组播是一对多的通信模式,其中一个发送方可以同时将数据发送给多个接收方。
- 广播 —一对所有
- 将数据包发送到同一网络中的所有主机。
- 在广播通信中,发送方发送的数据包将目标地址设置为广播地址,即网络中的所有主机都能接收到该数据包。
- 广播通常用于向局域网内的多个主机发送相同的信息,如网络发现、服务发现等。
- 单播—一对一
UDP组播
UDP组播示例一:使用public void joinGroup(InetAddress mcastaddr)
方法加入组播(已过时)
-
与单播的异同
- 相同
- 都只有一个发送端,且发送端都需要指定一个IP
- 不同
- 单播的接收端只有一个,而组播的接收端有一组(这一组中可能有多台计算机)
- 单播中发送端指定的IP是接收端的IP,而组播中指定的IP是组播地址(即若将不同的计算机绑定到该组播地址就可接收到发送端的数据,该组播地址有 范围:
224.0.0.0
至239.255.255.255
,其中224.0.0.0
至224.0.0.255
为预留的组播地址(即操作系统用的组播地址),所以我们只能用224.0.1.0
开始往后的组播地址)
- 相同
-
发送端发送数据步骤(与单播发送端步骤类似,但有两处更改)
- 创建发送端的
DatagramSocket
对象 - 创建一个
DatagramPacket
(数据报包)对象,封装要发送的数据及其相关信息(注意:与单播不同的是创建的数据报包对象的第三个参数此时为组播地址,而不是单播中单个计算机的IP地址) - 调用DatagramSocket对象的方法发送数据(注意:与单播不同的是在组播中,数据发送给了组播地址;而单播中是发给指定IP的计算机)
- 释放资源
- 创建发送端的
public class SenderOne {
public static void main(String[] args) throws IOException {
//第一步:创建发送端的DatagramSocket对象
DatagramSocket ds = new DatagramSocket();
//第二步:创建一个 `DatagramPacket` (数据报包)对象,封装要发送的数据及其相关信息
String s = "你好 组播";
byte[] bytes = s.getBytes();//将要发送的数据打包成字节数组
InetAddress address = InetAddress.getByName("224.0.1.0");//要发送的目标组播地址
int port = 10000;//要发送数据的一组目标主机中的端口号
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
//第三步:调用DatagramSocket对象的方法发送数据
ds.send(dp);
//第四步:释放资源
ds.close();
}
}
-
接收端接收数据步骤(与单播发送端步骤类似,但有两处更改)
-
创建接收端
MulticastSocket
对象构造器 解释 public MulticastSocket(int port)
创建多播套接字并将其绑定到特定端口。( port
是一个整数,表示MulticastSocket
实例将要绑定的端口号。当创建MulticastSocket
实例时,它会绑定到指定的端口上,以便于接收组播数据。) -
把当前计算机绑定到一个组播地址(表示添加到这一组中)
用到的 MulticastSocket
中的方法解释 public void joinGroup(InetAddress mcastaddr)
加入组播组。参数为InetAddress类的对象。该方法已过时 public void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf)
加入指定接口的指定组播组。(推荐) -
创建一个
DatagramPacket
(数据报包)对象,封装要接收的数据及其相关信息 -
调用
MulticastSocket
对象的方法public void reveive(DatagramPacket dp)
接收数据 -
解析数据包并把数据在控制台显示
-
释放资源
-
public void joinGroup(InetAddress mcastaddr)
public class ReceiverOne {
public static void main(String[] args) throws IOException {
//第一步:创建接收端`MulticastSocket`对象
//此处参数表示组播从10000端口号接收数据,若无参数则代表从一个随机端口接收数据
MulticastSocket ms = new MulticastSocket(10000);
//第二步:把当前计算机绑定到一个组播地址
ms.joinGroup((InetAddress.getByName("224.0.1.0")));
//第三步:创建一个 DatagramPacket(数据报包)对象,封装要接收的数据及其相关信息
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//第四步:调用MulticastSocket对象的方法接收数据
ms.receive(dp);
//第五步:解析数据包并把数据在控制台显示
int length = dp.getLength();
System.out.println(new String(bytes, 0, length));
//第六步:释放资源
ms.close();
}
}
UDP组播示例二:使用public void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf)
方法加入组播(推荐)
- 参数1:SocketAddress为抽象类,所以需要使用其子类的对象(其子类为
InetSocketAddress
)
InetSocketAddress 构造器 | 解释 |
---|---|
public InetSocketAddress(int port) | 创建一个套接字地址,其中IP地址是通配符地址,端口号是指定值。 |
public InetSocketAddress(String hostname, int port) | 根据主机名和端口号创建套接字地址。 |
public InetSocketAddress(InetAddress addr, int port) | 根据IP地址和端口号创建套接字地址。 |
- 参数2:NetworkInterface类没有构造方法(一般没有构造方法的类都会提供一个静态方法来返回该类的对象供操作者使用)
NetworkInterface类中用到的静态方法 | 解释 |
---|---|
public static NetworkInterface getByIndex(int index) | 根据索引获取网络接口。 |
public static NetworkInterface getByInetAddress(InetAddress addr) | 搜索具有绑定到其的指定Internet协议(IP)地址的网络接口的便捷方法。 |
public static NetworkInterface getByName(String name) | 搜索具有指定名称的网络接口。 |
- 注意
- 发送端、接收端步骤均不变
- 发送端代码不变,接收端加入组播时的代码改变
发送端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class SendOne {
public static void main(String[] args) throws Exception{
DatagramSocket ds = new DatagramSocket();
Scanner input = new Scanner(System.in);
System.out.print("请输入要发送的内容:");
String content = input.nextLine();
InetAddress ia = InetAddress.getByName("224.0.1.0");
int port = 10000;
DatagramPacket dp = null;
while(true){
byte[] bytes = content.getBytes();
if (!"886".equals(content)) {
dp = new DatagramPacket(bytes, bytes.length, ia, port);
ds.send(dp);
} else {
dp = new DatagramPacket(bytes, bytes.length, ia, port);
ds.send(dp);
break;
}
System.out.print("请再次输入要发送的内容:");
content = input.nextLine();
}
ds.close();
}
}
接收端
import java.net.*;
public class ReceiveOne {
public static void main(String[] args) throws Exception {
//第一步:创建接收端`MulticastSocket`对象
//此处参数表示组播从10000端口号接收数据,若无参数则代表从一个随机端口接收数据
MulticastSocket ms = new MulticastSocket(10000);
//第二步:把当前计算机绑定到一个组播地址
InetAddress ia = InetAddress.getByName("224.0.1.0");
InetSocketAddress isa = new InetSocketAddress(ia, 10000);
NetworkInterface ni = NetworkInterface.getByInetAddress(InetAddress.getLocalHost());
ms.joinGroup(isa, ni);
//第三步:创建一个 DatagramPacket(数据报包)对象,封装要接收的数据及其相关信息
DatagramPacket dp = null;
while (true) {
byte[] bytes = new byte[1024];
dp = new DatagramPacket(bytes, bytes.length);
//第四步:调用MulticastSocket对象的方法接收数据
ms.receive(dp);
//第五步:解析数据包并把数据在控制台显示
String s = new String(dp.getData(), 0, dp.getLength());
if ("886".equals(s)) {
System.out.println(s);
break;
} else System.out.println(s);
}
System.out.println("聊天结束");
//第六步:释放资源
ms.close();
}
}
UDP广播
-
与单播的异同
- 相同
- 都只有一个发送端,且发送端都需要指定一个IP
- 不同
- 相同
-
发送端发送数据步骤
- 创建发送端的
DatagramSocket
对象 - 创建一个
DatagramPacket
(数据报包)对象,封装要发送的数据及其相关信息(注意:与单播不同的是创建的数据报包对象的第三个参数此时为广播地址(固定为255.255.255.255
),而不是单播中单个计算机的IP地址) - 调用DatagramSocket对象的方法发送数据(注意:与单播不同的是在广播中,数据发送给了广播地址;而单播中是发给指定IP的计算机)
- 释放资源
- 创建发送端的
public class SenderTwo {
public static void main(String[] args) throws IOException {
//第一步:创建发送端的DatagramSocket对象
DatagramSocket ds = new DatagramSocket();
//第二步:创建一个 `DatagramPacket` (数据报包)对象,封装要发送的数据及其相关信息
String s = "你好 广播";
byte[] bytes = s.getBytes();//将要发送的数据打包成字节数组
InetAddress address = InetAddress.getByName("255.255.255.255");//要发送的目标广播地址
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
//第三步:调用DatagramSocket对象的方法发送数据
ds.send(dp);
//第四步:释放资源
ds.close();
}
}
- 接收端接收数据步骤—与单播接收端步骤一模一样
- 创建接收端
DatagramSocket
对象 - 创建一个
DatagramPacket
(数据报包)对象,封装要接收的数据及其相关信息 - 调用
DatagramSocket
对象的方法接收数据 - 解析数据包并把数据在控制台显示
- 释放资源
- 创建接收端
public class ReceiverTwo {
public static void main(String[] args) throws Exception {
//第一步:创建接收端DatagramSocket对象
DatagramSocket ds = new DatagramSocket(10000);
//第二步:创建一个DatagramPacket(数据报包)对象,封装要接收的数据及其相关信息
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//第三步:调用DatagramSocket对象的方法接收数据
ds.receive(dp);
//第四步:解析数据包并把数据在控制台显示
int length = dp.getLength();
System.out.println(new String(bytes, 0, length));
//第五步:释放资源
ds.close();
}
}
- 例题示例
发送端
import java.net.DatagramSocket;
import java.net.*;
import java.util.Scanner;
public class SenderOne {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket();
Scanner input = new Scanner(System.in);
System.out.print("请输入要发送的内容:");
String content = input.nextLine();
InetAddress ia = InetAddress.getByName("255.255.255.255");
DatagramPacket dp = null;
while (true) {
byte[] bytes = content.getBytes();
int length = bytes.length;
if (!"886".equals(content)) {
dp = new DatagramPacket(bytes, length, ia, 10000);
} else {
dp = new DatagramPacket(bytes, length, ia, 10000);
ds.send(dp);
break;
}
ds.send(dp);
System.out.print("请再次输入要发送的内容:");
content = input.nextLine();
}
ds.close();
}
}
接收端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ReceiveOne {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket(10000);
byte[] bytes = new byte[1024];
DatagramPacket dp = null;
while (true) {
dp = new DatagramPacket(bytes, bytes.length);
ds.receive(dp);
String contentR = new String(dp.getData(), 0, dp.getLength());
if ("886".equals(contentR)) {
System.out.println("收到的消息:" + contentR);
break;
} else System.out.println("收到的消息:" + contentR);
}
System.out.println("聊天结束");
ds.close();
}
}
TCP协议
-
定义
传输控制协议(Transmission Control Protocol)
TCP是面向连接的通信协议,是一种可靠的网络协议,它在通信的两端各建立一个Socket对象
速度慢,没有大小限制,数据安全
- 特点
- 通信之前要保证连接已经建立
- 通过Socket产生IO流来进行网络通信
TCP三次握手与四次挥手
三次握手
-
定义
是 TCP(Transmission Control Protocol)建立连接的过程中使用的一种协议。在客户端和服务器之间建立 TCP 连接时,需要进行三次握手来确保双方都能够正常通信。
-
三次握手步骤
- 客户端向服务器端发出连接请求,等待服务器确认
- 服务器向客户端返回一个响应,告诉客户端收到了请求
- 客户端向服务器端再次发出确认信息,连接建立
四次挥手
-
定义
是 TCP(Transmission Control Protocol)断开连接的过程中使用的一种协议。在客户端和服务器之间断开 TCP 连接时,需要进行四次挥手来确保双方都能够正常结束连接。
-
四次挥手步骤
- 客户端向服务器发出取消连接请求
- 服务器向客户端返回一个响应,表示收到客户端的取消请求
- 当服务器将最后的数据处理完毕之后,服务器会向客户端发出确认取消信息
- 客户端再次向客户端发出确认取消信息,连接取消
-
问题
为什么四次挥手比三次握手多一个步骤?
因为在四次握手之前,客户端与服务器端已经建立了连接,若想断开连接就需要等到服务器端将数据处理完毕之后在断开,所以在客户端向服务器端发送取消连接请求时,服务器端就多了一个步骤(即处理数据),当处理完毕后会向客户端发出是否确认取消连接的信息,若是则取消连接
TCP通信程序
发送端(客户端)
- 用到的类构造器
Socket 类构造方法 | 解释 |
---|---|
public Socket(String host, int port) | 创建流套接字并将其连接到指定主机上的指定端口号。(即创建一个新的 Socket 实例,并连接到指定主机和端口上。) |
注意:
host
:要连接的目标主机的主机名或 IP 地址
port
:要连接的目标主机的端口号当创建 Socket 实例时,它会尝试连接到指定主机和端口上的服务器。如果连接成功,就可以通过这个 Socket 实例进行数据的发送和接收。如果连接失败,则会抛出
UnknownHostException
(如果指定的主机名无效)或IOException
(如果发生 I/O 错误)等异常。
- 方法
Socket 类方法 | 解释 |
---|---|
public OutputStream getOutputStream() | 返回此套接字的输出流。(即获取与该Socket 关联的输出流,以便通过这个输出流向连接的远程主机发送数据。) |
public void shutdownOutput() | 关闭套接字的输出流,意味着无法再从该套接字发送数据。但仍然可以通过该套接字的输入流接收来自另一端的数据。 |
public void close() | 关闭此套接字。(此时即无法从该套接字发送数据也无法通过该套接字的输入流接收来自另一端的数据) |
注意:
public OutputStream getOutputStream()
:
- 在客户端程序中,可以使用
getOutputStream()
获取一个OutputStream
实例,然后通过该输出流向服务器端发送数据。在服务器端程序中,同样可以使用这个方法获取一个OutputStream
实例,然后通过该输出流向客户端发送数据。- 一旦获取了输出流,就可以使用输出流的
write
方法将数据写入到流中,然后数据就会通过底层的网络连接发送到远程主机。发送的数据可以是字节数组、字符串或者任何其他可以转换为字节流的数据类型。- 在使用完输出流后,应该关闭输出流以释放资源。通常建议使用
try-with-resources
语句或者在finally
块中关闭输出流,以确保在程序结束时正确释放资源,防止资源泄露。
- 发送端发送数据步骤
- 创建客户端Socket对象并与指定服务端连接
- 客户端创建对象并连接服务器,此时是通过三次握手协议保证跟服务器之间的连接
- 获取输出流(
OutputStream
),写数据- 这里所获取的输出流为字节输出流(
OutputStream
对象),因为针对客户端来说,数据是往外写的,所以是输出流 - 写数据利用的是
OutputStream
类中的write
方法
- 这里所获取的输出流为字节输出流(
- 释放资源------字节输出流及Socket对象均要释放
- 释放资源时客户端会向服务器端的read()方法传递一个结束标记的动作,以此来让read()方法感知到已读到数据末尾(或在输出数据的代码后加上
shutdownOutput()
方法,也是让read()方法感知已读到数据末尾) - Socket对象释放资源是通过四次挥手协议保证连接终止
- 释放资源时客户端会向服务器端的read()方法传递一个结束标记的动作,以此来让read()方法感知到已读到数据末尾(或在输出数据的代码后加上
- 创建客户端Socket对象并与指定服务端连接
public class SenderOne {
public static void main(String[] args) throws IOException {
//第一步:创建一个Socket对象并与指定服务端连接
Socket socket = new Socket("10.201.196.231", 10000);
//第二:获取IO流(输出流),写数据
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
//第三步:释放资源
os.close();
socket.close();
}
}
运行出错原因:
TCP协议在传输数据之前就要保证连接的建立,所以在发送端发送数据的第一步结束时就会跟服务器去尝试建立连接,但由于此时未写服务器端代码,所以无法连接也就无法运行发送端代码
接收端(服务端)
- 用到的类构造器
ServerSocket 类构造方法 | 解释 |
---|---|
public SeverSocket(int port) | 创建绑定到指定端口的服务器套接字(创建一个新的 ServerSocket 实例,并指定服务器端监听的端口号。) |
注意:
port
:服务器将要监听的端口号。当创建 ServerSocket 实例时,它会绑定到指定的端口上,以便于监听客户端的连接请求。- 在创建 ServerSocket 实例时可能会抛出一些异常,比如
IOException
,表示在创建 ServerSocket 实例的过程中可能会发生 I/O 错误
- 方法
ServerSocket 类方法 | 解释 |
---|---|
public Socket accept() | 侦听对此套接字的连接并接受它。 (即用于接受客户端的连接请求,并返回一个 Socket 实例,代表与客户端建立的连接。) |
Socket类方法 | 解释 |
public InputStream getInputStream() | 返回此套接字的输入流。(即获取与该 Socket 关联的输入流,以便从连接的远程主机接收数据。) |
- 接收端接收数据步骤
- 创建服务器端的ServerSocket对象
- 监听客户端连接,返回一个
Socket
对象------public Socket accept()
- 执行
accept
方法后,服务器端会死等客户端的连接(即执行accept方法后会陷入阻塞状态来等待客户端的连接) - 若有客户端连接则返回一个Socket对象;若没有则陷入阻塞状态
- 执行
- 获取输入流(
InputStream
),读数据,并把数据显示在控制台- 针对服务器端来说,数据是往里读的,所以是输入流
- 读数据利用的是输入流
InputStream
类中的read()
方法InputStream
类中的read()
方法是一个阻塞方法,它用于从输入流中读取数据,并返回一个字节的数据。该方法不会自动添加结束标记,若客户端不进行资源释放,它会一直阻塞等待直到有数据可读- read()方法读到结束标记的条件(满足其一即可):第一,客户端资源释放;第二,读到流的末尾
- 释放资源------字节输入流对象及
ServerSocket
对象均要释放
public class ReceverOne {
public static void main(String[] args) throws IOException {
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
//第三步:获取输入流InputStream对象
InputStream is = socketAccept.getInputStream();
//读数据并将数据显示在控制台
int b;
while ((b = is.read()) != -1) {
System.out.print((char) b);
}
/* 可写为
//读数据
byte[] bytes = is.readAllBytes();
//将数据显示在控制台
System.out.println(new String(bytes, 0, bytes.length));
*/
//第四步:释放资源
ss.close();
is.close();
}
}
练习
- 例题1如图
注意:
发送中文数据时使用转换流进行数据的处理和发送,不能直接使用字节流。在该例子中客户端向服务器端发送的是英文,可以用字节流;服务器端收到消息后向客户端发送的是中文,则使用字符缓冲流
代码实现
客户端
public class SenderTwo {
public static void main(String[] args) throws IOException {
//客户端发消息
Socket socket = new Socket("10.201.196.231", 10000);
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
//os.close();客户端若在发送数据后直接关流则会导致整个socket无法使用,即之后的代码无法执行
socket.shutdownOutput();
//客户端接收消息---此时客户端为服务器端
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is)); //将字节流转换成字符流
String line;
while ((line = br.readLine()) != null) {
System.out.print(line);
}
//释放资源
socket.close();
os.close();
is.close();
br.close();
}
}
服务器端
public class ReceverTwo {
public static void main(String[] args) throws IOException {
//服务器端接收消息
ServerSocket ss = new ServerSocket(10000);
Socket socketAccept = ss.accept();
InputStream is = socketAccept.getInputStream();
int b;
while ((b = is.read()) != -1) {
System.out.print((char) b);
}
//服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("你谁啊");
bw.newLine();
bw.flush();
//释放资源
ss.close();
is.close();
socketAccept.close();
os.close();
bw.close();
}
}
- 例题2
客户端:将本地文件上传到服务器并接收服务器的反馈
服务器:接收客户端上传的文件,上传完毕后给出反馈
客户端步骤
- 创建客户端Socket对象并与指定服务端连接
- 利用缓冲输入流将本地文件读到内存
- 获取输出流(
OutputStream
),并利用缓冲输出流将数据写到网络中 - 接收客户端反馈
- 释放资源
public class SenderThree {
public static void main(String[] args) throws Exception{
//第一步:创建客户端Socket对象并与指定服务端连接
Socket socket = new Socket("10.201.196.231", 10000);
//第二步:利用字节缓冲输入流流将本地文件读到内存
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("大数据.png"));
//第三步:获取输出流并写数据
OutputStream os = socket.getOutputStream();
//利用字节缓冲输出流将内存中的数据传到服务器端
BufferedOutputStream bos = new BufferedOutputStream(os);
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
bos.flush();//刷新缓冲区
socket.shutdownOutput();//给服务器中的read()方法一个结束标记
//第四步:客户端接收消息---此时客户端为服务器端
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is)); //将字节流转换成字符流
String line;
while ((line = br.readLine()) != null) {
System.out.print(line);
}
//第五步:释放资源
socket.close();
bis.close();
/*
bis.close();
bos.close();
os.close();
br.close();
is.close();
socket.close();
*/
}
}
服务器端步骤
- 创建服务器端的
ServerSocket
对象 - 监听客户端连接,返回一个
Socket
对象------public Socket accept()
- 获取输入流(
InputStream
),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件 - 给客户端反馈
- 释放资源
public class ReceverThree {
public static void main(String[] args) throws Exception{
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
//第三步:获取输入流(InputStream),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件
InputStream is = socketAccept.getInputStream();
//将网络中的数据读到内存---即从客户端读取数据
BufferedInputStream bis = new BufferedInputStream(is);
//将内存中的数据写到 本地文件,实现永久化存储
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("hello1.png"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
System.out.println("已上传完毕");
//第四步:服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("上传成功");
bw.newLine();
bw.flush();
//第五步:释放资源
bos.close();
socketAccept.close();
ss.close();
/*
bis.close();
bos.close();
bw.close();
is.close();
socketAccept.close();
ss.close();
*/
}
}
服务端优化
TCP通信中我们写的服务端代码都是只能使用一次,客户端在发送一次数据之后若想再次发送数据则会出错(即服务器只能处理一个客户端请求),由此引入了服务器优化
循环优化
将练习中例题2的服务器端代码放入循环中,以此来保证服务器端一直处于运行状态,即可时刻处理客户端请求,代码如下:
public class ReceverThree {
public static void main(String[] args) throws Exception{
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
while (true) {
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
//第三步:获取输入流(InputStream),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件
InputStream is = socketAccept.getInputStream();
//将网络中的数据读到内存---即从客户端读取数据
BufferedInputStream bis = new BufferedInputStream(is);
//将内存中的数据写到 本地文件,实现永久化存储
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("hello1.png"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
System.out.println("已上传完毕");
//服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("上传成功");
bw.newLine();
bw.flush();
bos.close();
socketAccept.close();
/*
bis.close();
bos.close();
bw.close();
is.close();
socketAccept.close();
*/
}
}
}
- 进阶版
发送端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class SendOne {
public static void main(String[] args) throws IOException {
Socket socket = null;
while (true) {
//创建客户端的Socket对象并与指定的服务器端连接
socket = new Socket("127.0.0.1", 10000);
//利用字节缓冲流将本地文件读取到内存中
Scanner input = new Scanner(System.in);
System.out.print("请输入文件地址:");
String fileAddress = input.nextLine();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileAddress));
//将本地文件从内存中传输到服务器端
OutputStream os = socket.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(os);
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.flush();
socket.shutdownOutput();
//接收服务器端的反馈
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println("服务器返回消息:" + line);
}
bis.close();
System.out.println("是否继续发送文件:");
String iss = input.nextLine();
if (!iss.equals("y")) break;
}
System.out.println("停止与服务器的连接");
socket.close();
}
}
服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class ReceiveOne {
public static void main(String[] args) throws Exception {
//创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
Scanner input = new Scanner(System.in);
//剩余部分进入循环:来持续接收客户端传输的文件
while (true) {
Socket socket = ss.accept();
//接收客户端传输的文件
InputStream is = socket.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
//保存到服务器本地
System.out.print("请输入保存的位置:");
String fileSaveLocation = input.nextLine();
FileOutputStream fos = new FileOutputStream(fileSaveLocation);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.flush();
System.out.println("已接收客户端传输的文件");
//反馈给客户端
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);
bw.write("文件已上传完毕");
bw.newLine();
bw.flush();
//释放资源
fos.close();
bos.close();
os.close();
osw.close();
bw.close();
socket.close();
}
}
}
UUID优化(UUID类)
循环优化虽然解决了服务器端只能处理一次客户端请求的问题,但是客户端在多次向服务器端上传文件时,服务器端只会保存一个已经命名好的文件,不会重新出现一个文件。比如:在利用客户端向服务器端上传一个图片时,不管客户端上传多少次,服务器端始终只有一个文件,如图所示
产生该问题的原因是:服务器端在接收文件时,文件路径与名称是已经被确定了的,所以在多次上传文件时,系统会自动把第一次的文件给覆盖了
为了解决该问题,引入了UUID优化
-
定义
UUID类会自动生成一个随机且唯一的UUID对象
-
作用
生成唯一标识符,可以用于标识应用程序中的对象、实体或事件,而不需要全局唯一性的中心注册器、
-
特点
UUID是一个128位的数字,通常以32个十六进制数字表示,形如
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
,其中每个 x 是一个十六进制数字(0-9 或 a-f),M 和 N 分别代表 UUID 的版本号和变体。使用UUID类可以方便地生成唯一标识符,特别是在需要唯一标识符而无法依赖于集中式的注册机构时
-
注意
UUID类生成的对象并不是不可能重复的,只是说重复的几率非常低
-
方法
方法 | 解释 |
---|---|
public static UUID randomUUID() | 生成一个符合标准格式(UUID标准格式)的UUID实例,它是一个128位的数字,通常以32个十六进制数字表示,形如 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx ,其中每个 x 是一个十六进制数字(0-9 或 a-f),而 M 和 N 分别代表 UUID 的版本号和变体。 |
public String toString() | 返回一个表示UUID的String 对象。(UUID对象通常以32个十六进制数字的形式表示,例如 550e8400-e29b-41d4-a716-446655440000 。而toString() 方法会返回这个字符串表示形式,因此可以直接用于输出或者将UUID转换为字符串以存储或传输。) |
public class UuidOne {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
String s = uuid.toString().replace("-", " ");
System.out.println(s);
}
}
练习中例题2优化后的代码如下:
public class ReceverThree {
public static void main(String[] args) throws Exception{
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
while (true) {
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
//第三步:获取输入流(InputStream),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件
InputStream is = socketAccept.getInputStream();
//将网络中的数据读到内存---即从客户端读取数据
BufferedInputStream bis = new BufferedInputStream(is);
//将内存中的数据写到 本地文件,实现永久化存储
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(UUID.randomUUID().toString() + ".jpg"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
System.out.println("已上传完毕");
//服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("上传成功");
bw.newLine();
bw.flush();
bos.close();
socketAccept.close();
/*
bis.close();
bos.close();
bw.close();
is.close();
socketAccept.close();
*/
}
}
- 进阶版
客户端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class SendOne {
public static void main(String[] args) throws IOException {
Socket socket = null;
while (true) {
//创建客户端的Socket对象并与指定的服务器端连接
socket = new Socket("127.0.0.1", 10000);
//利用字节缓冲流将本地文件读取到内存中
Scanner input = new Scanner(System.in);
System.out.print("请输入文件地址:");
String fileAddress = input.nextLine();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileAddress));
//将本地文件从内存中传输到服务器端
OutputStream os = socket.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(os);
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.flush();
socket.shutdownOutput();
//接收服务器端的反馈
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println("服务器返回消息:" + line);
}
bis.close();
System.out.println("是否继续发送文件:");
String iss = input.nextLine();
if (!iss.equals("y")) break;
}
System.out.println("停止与服务器的连接");
socket.close();
}
}
服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.UUID;
public class ReceiveOne {
public static void main(String[] args) throws Exception {
//创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
Scanner input = new Scanner(System.in);
//剩余部分进入循环:来持续接收客户端传输的文件
while (true) {
Socket socket = ss.accept();
//接收客户端传输的文件
InputStream is = socket.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
//保存到服务器本地
System.out.print("请输入保存的位置:");
String SaveLocation = input.nextLine();
System.out.println("请输入文件后缀");
String houzhui = input.nextLine();
//给定UUID
UUID uuid = UUID.randomUUID();
String uuidString = uuid.toString();
String fileSaveLocation = SaveLocation + uuidString + houzhui;
FileOutputStream fos = new FileOutputStream(fileSaveLocation);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.flush();
System.out.println("已接收客户端传输的文件");
//反馈给客户端
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);
bw.write("文件已上传完毕");
bw.newLine();
bw.flush();
//释放资源
fos.close();
bos.close();
os.close();
osw.close();
bw.close();
socket.close();
}
}
}
多线程优化
使用while(true)
循环优化时虽然可以让服务器处理多个客户端请求,但是无法同时跟多个客户端进行通信,为解决该问题引入了多线程优化
例题2服务器端代码实现
多线程代码
public class ThreadServerSocket implements Runnable{
private Socket socketAccept;
public ThreadServerSocket (Socket socket) {
this.socketAccept = socket;
}
@Override
public void run() {
BufferedOutputStream bos = null;
try {
//第三步:获取输入流(InputStream),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件
InputStream is = socketAccept.getInputStream();
//将网络中的数据读到内存---即从客户端读取数据
BufferedInputStream bis = new BufferedInputStream(is);
//将内存中的数据写到 本地文件,实现永久化存储
bos = new BufferedOutputStream(new FileOutputStream(UUID.randomUUID().toString() + ".jpg"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
System.out.println("已上传完毕");
//服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("上传成功");
bw.newLine();
bw.flush();
/*
bis.close();
bos.close();
bw.close();
is.close();
socketAccept.close();
*/
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (bos != null) {
try {
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (socketAccept != null) {
try {
socketAccept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务器端代码
public class ReceverThree {
public static void main(String[] args) throws Exception{
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
while (true) {
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
ThreadServerSocket threadServerSocket = new ThreadServerSocket(socketAccept);
new Thread(threadServerSocket).start(); //当监听到一个客户端线程时就立马开启一个服务器端线程
}
}
}
线程池优化
加入多线程后服务器虽然可以同时处理多个客户端请求,但是资源消耗太大,为解决该问题引入了线程池优化
-
线程池定义
是一种管理和复用线程的机制,它可以避免重复创建和销毁线程,从而减少了线程创建和销毁的开销,提高了系统的性能和资源利用率。
-
线程池特点
- 降低资源消耗:通过复用线程,减少了频繁创建和销毁线程的开销,从而降低了系统的资源消耗,包括 CPU 和内存资源。
- 提高响应速度:由于线程池中的线程是预先创建的,可以立即执行任务,而不需要等待线程创建,从而提高了任务的响应速度。
- 提高系统吞吐量:线程池可以限制线程的数量,避免系统过度并发导致资源竞争和性能下降,从而提高了系统的吞吐量。
- 减少任务排队时间:线程池可以限制任务的数量,当任务提交速度超过处理速度时,可以对任务进行排队,避免了系统资源的过度消耗和性能下降。
- 提高代码可维护性:通过使用线程池,可以将线程的管理和维护抽象为线程池的管理和维护,使得代码更加清晰和易于维护。
-
线程池几种优化方式
- 选择合适的线程池参数:包括核心线程数、最大线程数、线程空闲时间、任务队列等参数,根据任务的特性和系统的负载情况来调整参数,以达到最佳的性能和资源利用率。
- 合理选择线程池类型:Java提供了多种类型的线程池,包括固定大小线程池、可缓存线程池、定时执行线程池等,根据任务的特性和系统的需求来选择合适的线程池类型。
- 避免阻塞和死锁:在任务执行过程中,要避免对共享资源的阻塞和死锁,通过合理的同步机制和资源管理来保证系统的稳定性和可靠性。
- 监控和调优:定期监控线程池的状态和性能指标,包括线程池的活跃线程数、任务队列长度、任务执行时间等指标,根据监控结果来调整线程池的参数和配置,以达到最佳的性能和效率。
多线程代码
public class ThreadServerSocket implements Runnable{
private Socket socketAccept;
public ThreadServerSocket (Socket socket) {
this.socketAccept = socket;
}
@Override
public void run() {
BufferedOutputStream bos = null;
try {
//第三步:获取输入流(InputStream),并利用缓冲输入流将网络中的数据读到内存,并把文件保存到本地文件
InputStream is = socketAccept.getInputStream();
//将网络中的数据读到内存---即从客户端读取数据
BufferedInputStream bis = new BufferedInputStream(is);
//将内存中的数据写到 本地文件,实现永久化存储
bos = new BufferedOutputStream(new FileOutputStream(UUID.randomUUID().toString() + ".jpg"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
System.out.println("已上传完毕");
//服务器端发消息---此时服务器端为客户端
OutputStream os = socketAccept.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));//将字符流转换成字节流
bw.write("上传成功");
bw.newLine();
bw.flush();
/*
bis.close();
bos.close();
bw.close();
is.close();
socketAccept.close();
*/
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (bos != null) {
try {
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (socketAccept != null) {
try {
socketAccept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
服务器端代码
public class ReceverThree {
public static void main(String[] args) throws Exception{
//第一步:创建服务器端的ServerSocket对象
ServerSocket ss = new ServerSocket(10000);
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//线程池中核心线程数量
10,//线程池中线程总数量
60,//临时线程空闲时间
TimeUnit.SECONDS,//临时线程控线时间的单位
new ArrayBlockingQueue<>(5),//阻塞队列即允许线程排队的个数
Executors.defaultThreadFactory(),//创建线程的方式---默认
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略---即线程排队中第5个之后的线程就不要了并抛出一个异常
);
while (true) {
//第二步:监听客户端连接,返回一个Socket对象
Socket socketAccept = ss.accept();
ThreadServerSocket threadServerSocket = new ThreadServerSocket(socketAccept);
//当客户端请求服务器端时会自动调用线程池中的线程进行处理,若客户端使用完毕则会将线程自动归还到线程池中
pool.submit(threadServerSocket);
}
}
}
日志
记录程序在运行过程中的任何点滴并进行永久式存储
-
定义
是一种记录应用程序运行时状态、行为和错误信息的重要机制。通过使用日志,开发人员可以更容易地调试和监视应用程序的运行情况。Java提供了几种日志工具和框架,其中最常见的是Java标准库中的
java.util.logging
(性能低)以及第三方库如Log4j
(有大bug)、Logback
(推荐)等。 -
日志与输出语句区别
输出语句 日志技术 取消日志 需要修改代码,灵活性差 不需修改代码,修改配置文件,灵活性好 输出位置 只能是控制台 可以将日志信息写到文件或数据库中 多线程 和业务代码处于一个线程中,所以若输出语句较多的话会影响程序性能 多线程方式记录日志,不影响业务代码的性能 -
日志技术特点
- 控制日志信息输送的目的地(即是日志信息是输送到控制台还是文件等位置)
- 控制每一条日志的输出格式
- 可以定义每一条日志信息的级别,来更加细致地控制日志的生成过程
- 可以通过一个配置文件来灵活配置,不需要反复修改应用代码
日志技术------Logback
-
Logback
介绍Logback是基于
slf4j
的日志规范实现的框架,性能比之前的log4j
要好 -
Logback技术模块
logback-core
:该模块为其他两个模块提供基础代码,必须有logback-classic
:该模块完整实现了slf4j
API的模块logback-access
:该模块与Tomcat
和Jetty
等Servlet
容器集成,以提供HTTP访问日志功能
-
Logback
使用步骤-
Step1:导入
Logback
的相关jar包-
右键项目→新建文件,名为lib→将
Logback
jar
包复制进去即可
-
全选所有jar包右键→Add as library→OK
-
查看jar包是否导入成功的两种方式
-
看jar包前是否有展开的小箭头,如图所示,导入成功
-
File→Project Structure→Libraries,可看到导入成功,如图所示
-
-
-
Step2: 编写
Logback
配置文件- 在工程的src下导入
logback.xml
文件,如图所示
- 在工程的src下导入
-
Step3: 在代码中获取日志对象
-
定义
Logger
类对象的成员变量,如图所示(注意:该Logger
类必须是org.slf4j
包下的Logger
类) -
该
Logger
类对象是通过LoggerFactory
类下的public static Logger getLogger(Class<?> clazz)
方法获取(注意:LoggerFactory
类也是org.slf4j
包下的)- 该静态方法解释:用于获取一个与特定类相关联的日志记录器(即
LoggerFactory
将检查与给定类名关联的日志记录器是否已存在。如果不存在,则创建一个新的日志记录器,并将其与该类名相关联。如果已存在,则返回现有的日志记录器实例。) Class<?> clazz
:表示要与日志记录器关联的类。通过提供一个类作为参数,可以在日志中标识出特定的类或模块,以便更好地组织和查看日志。Logger
:返回与给定类关联的日志记录器实例。该日志记录器将用于在代码中记录消息和异常。
代码如下:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Scanner; public class TestOne { //获取日志对象 private static final Logger LOGGER = LoggerFactory.getLogger(TestOne.class); public static void main(String[] args) { Scanner input = new Scanner(System.in); //打印日志------类似于写输出语句 System.out.println("请输入您的姓名:"); String name = input.nextLine(); LOGGER.info("用户输入的姓名为:" + name); System.out.println("请输入您的年龄:"); String age = input.nextLine(); try { int ageInt = Integer.parseInt(age); LOGGER.info("用户输入的年龄格式正确,年龄为:" + age); } catch (NumberFormatException e) { LOGGER.info("用户输入的年龄格式错误,为:" + age); } } }
如运行截图所示,日志内容会在运行界面显示出来,同时也会保存到当地你所保存的位置,如下图所示
- 该静态方法解释:用于获取一个与特定类相关联的日志记录器(即
-
-
Step4: 按照级别设置记录日志信息
-
-
如系统上线后只想记录一些错误日志信息或者不想记录日志了,应该怎么办
此时可以通过设置日志的输出级别来控制哪些日志信息输出或者不输出
-
日志输出级别
从低到高为:
All
<Trace
<Debug
<Info
<Warn
<Error
<Fatal
<OFF
,默认为Debug
,忽略大小写,举例说明如下:LOGGER.info("info级别的日志信息")
LOGGER.error("error级别的日志信息")
注意:
All
代表打开全部日志信息,即全部的日志都要
OFF
代表关闭全部日志信息,即全部的日志都不要 -
级别日志作用
Debug
级别:开发过程中打印一些基本信息,可以查看程序的流程,平时主要用于调试程序。项目上线后一般会把该级别的日志给取消掉Info
级别:打印一些感兴趣或者重要的信息Warn
级别:警告,给程序员提示的Error
级别:打印一些已经发生错误的信息(这些错误信息不会影响系统的运行)Fatal
级别:是非常严重的错误,会导致程序的崩溃停止 -
日志作用
作用:将开发中不同的日志信息进行分类,只输出大于等于该级别的日志信息
日志logback.xml
文件解析
- logback.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE:表示当前的日志信息是可以输出到控制台的
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 输出流对象 默认System.out改为 System.err-->
<target>System.out</target>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
%msg:日志信息,%n是换行符-->
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</Pattern>
<!-- 设置字符集
<charset>UTF-8</charset>
-->
</encoder>
</appender>
<!-- File是输出的方向通向文件的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!--日志输出路径-->
<file>F:/rizhi/idea/data.log</file>
<!-- 指定日志文件拆分和压缩规则 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 通过指定压缩文件名称来确定分割文件方式 -->
<fileNamePattern>F:/rizhi/idea/data-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
<!--文件拆分大小-->
<maxFileSize>1MB</maxFileSize>
<!--日志文件保留单位数
<maxHistory>15</maxHistory>
-->
</rollingPolicy>
</appender>
<!--
level:用来设置日志输出级别(即打印级别),大小写无关,
从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF,默认为Debug
<root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
-->
<!-- 设置日志输出级别,-->
<root level="ALL">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
- 第一部分------日志在控制台上的设置
<!--
CONSOLE:表示当前的日志信息是可以输出到控制台的
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 输出流对象 默认System.out改为 System.err-->
<target>System.out</target> <!--代表在控制台中以输出语句的形式进行打印-->
<encoder>
<!--设置在控制台打印出来时的格式-->
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:输出当前日志的级别(-5代表从左显示5个字符宽度)
%c:代表日志是当前哪个类中打印的,包括包名和类名
%thread:当前线程的名字
%msg:日志信息,对应的内容为LOGGER.级别("括号中的内容")
%n是换行符
-->
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</Pattern>
<!-- 设置字符集
<charset>UTF-8</charset>
-->
</encoder>
</appender>
- 第二部分解析------日志在文件中的设置
<!-- File是输出的方向通向文件的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset><!--默认文件保存格式为utf-8-->
</encoder>
<!--日志文件输出路径-->
<file>F:/rizhi/idea/data.log</file>
<!-- 指定日志文件拆分和压缩规则 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 通过指定压缩文件名称来确定分割文件方式 -->
<fileNamePattern>F:/rizhi/idea/data-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
<!--文件拆分大小-->
<maxFileSize>1MB</maxFileSize>
<!--日志文件保留单位数
<maxHistory>15</maxHistory>
-->
</rollingPolicy>
</appender>
- 第三部分------日志开关
<root level="ALL"><!--level表示日志的级别,此处为ALL,表示日志级别>=ALL的日志才有权力执行下列代码-->
<appender-ref ref="CONSOLE"/><!--表示日志可以打印在控制台-->
<appender-ref ref="FILE"/><!--表示日志可以保存在文件中-->
</root>
比如我现在将日志开关中的日志级别设置为INFO
级别(即:<root level="INFO">
),并在测试类利用debug级别输出一条语句,由于debug级别小于INFO级别,所以就不会显示在控制台,如下代码所示
public class TestOne {
//获取日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(TestOne.class);
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
//打印日志------类似于写输出语句
System.out.println("请输入您的姓名:");
String name = input.nextLine();
LOGGER.info("用户输入的姓名为:" + name);
System.out.println("请输入您的年龄:");
String age = input.nextLine();
try {
int ageInt = Integer.parseInt(age);
LOGGER.debug("用户输入的年龄格式正确,年龄为:" + age);
} catch (NumberFormatException e) {
LOGGER.info("用户输入的年龄格式错误,为:" + age);
}
}
}
如运行截图所示,此时输入年龄后,日志信息并不会打印出来