第9章 网络编程
9.1 网络通信协议
通过计算机网络可以实现多台计算机连接,但是不同计算机的操作系统和硬件体系结构不同,为了提供通信支持,位于同一个网络中的计算机在进行连接和通信时必须要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交互。
网络通信协议有很多种,目前应用最广泛的是TCP/IP协议(Transmission Control Protocol/Internet Protocol,传输控制协议/英特网互联协议)、UDP协议(User Datagram Protocol,用户数据报协议)、ICMP协议(Internet Control Message Protocol,Internet 控制报文协议)和其他一些协议的协议组。
本章中所学的网络编程知识,主要就是基于TCP/IP协议中的内容。在学习具体的内容之前,首先来了解一下TCP/IP 协议。TCP/IP(又称TCP/IP协议簇)是一组用于实现网络互连的通信协议,其名称来源于该协议簇中两个重要的协议(TCP协议和IP协议)。
基于TCP/IP的参考模型将协议分成四个层次。
TCP/IP协议中的四层分别是链路层、网络层、传输层和应用层,每层分别负责不同的通信功能,接下来针对这四层进行详细地讲解。
● 链路层:也称为网络接口层,该层负责监视数据在主机和网络之间的交互。事实上,TCP/IP本身并未定义该层的协议,而由参与互连的各网络使用自己的物理层和数据链路层协议与TCP/IP的网络互联层进行连接。
● 网络层:也称网络互联层,是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
● 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
● 应用层:主要负责应用程序的协议,如HTTP协议、FTP协议等。
9.1.1 IP地址和端口号
要想使网络中的计算机能够进行通信,必须为每台计算机指定一个标识号,通过这个标识号指定接收数据的计算机或者发送数据的计算机。在TCP/IP协议中,这个标识号就是IP地址,它可以唯一标识一台计算机。目前,IP地址广泛使用的版本是IPv4,它由4个字节大小的二进制数来表示,如:00001010000000000000000000000001。由于二进制形式表示的IP地址非常不便记忆和处理,因此通常会将IP地址写成十进制的形式,每个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “10.0.0.1”。
随着计算机网络规模的不断扩大,对IP地址的需求也越来越多,IPv4这种用4个字节表示的IP地址将面临使用枯竭的局面。为解决此问题,IPv6 便应运而生。IPv6使用16个字节表示IP地址,它所拥有的地址容量约是IPv4的8×1028倍,达到2128个(算上全零的),这样就解决了网络地址资源数量不足的问题。
IP地址由两部分组成,即“网络.主机”的形式,其中网络部分表示其属于互联网的哪一个网络,是网络的地址编码,主机部分表示其属于该网络中的哪一台主机,是网络中一个主机的地址编码,二者是主从关系。
IP地址总共分为5类,常用的有3类,介绍如下。
● A类地址:由第一段的网络地址和其余三段的主机地址组成,范围是1.0.0.0到127.255.255.255
● B类地址:由前两段的网络地址和其余两段的主机地址组成,范围是128.0.0.0到191.255.255.255
● C类地址:由前三段的网络地址和最后一段的主机地址组成,范围是192.0.0.0到223.255.255.255
另外,还有一个回送地址127.0.0.1,指本机地址,该地址一般用来测试使用,例如:ping 127.0.0.1来测试本机TCP/IP是否正常。
通过IP地址可以连接到指定计算机,但如果想访问目标计算机中的某个应用程序,还需要指定端口号。在计算机中,不同的应用程序是通过端口号区分的。端口号是用两个字节(16位的二进制数)表示的,它的取值范围是0~65535,其中,0~1023之间的端口号由操作系统的网络服务所占用,用户的普通应用程序需要使用1024以上的端口号,从而避免操作系统服务端口号被其他应用或服务所占用。
接下来通过一个图例描述IP地址和端口号的作用。
9.1.2 InetAddress
在Java中,提供了一个与IP地址相关的InetAddress类,该类用于封装一个IP地址,并提供了一系列与IP地址相关的方法,InetAddress类的一些常用方法如下表。
方法声明 | 功能描述 | |
---|---|---|
InetAddress getByName(String host) | 参数host表示指定的主机,该方法用于在给定主机名的情况下确定主机的 IP 地址 | |
InetAddress getLocalHost() | 创建一个表示本地主机的InetAddress对象 | |
String getHostName() | 得到IP地址的主机名,如果是本机则是计算机名,不是本机则是主机名,如果没有域名则是IP地址 | |
Boolean isReachable(int timeout) | 判断指定的时间内地址是否可以到达 | |
String getHostAddress() | 得到字符串格式的原始 IP 地址 |
上表列举了InetAddress的五个常用方法。其中,前两个方法用于获得该类的实例对象,第一个方法用于获得表示指定主机的InetAddress对象,第二个方法用于获得表示本地的InetAddress对象。通过InetAddress对象便可获取指定主机名,IP地址等。
接下来通过一个案例来演示InetAddress常用方法的使用。
1 import java.net.InetAddress;
2 public class Example01 {
3 public static void main(String[] args) throws Exception {
4 InetAddress localAddress = InetAddress.getLocalHost();
5 InetAddress remoteAddress = InetAddress. getByName("www.itcast.cn");
6 System.out.println("本机的IP地址:" + localAddress.getHostAddress());
7 System.out.println("itcast的IP地址:" + remoteAddress.getHostAddress());
8 System.out.println("3秒是否可达:" + remoteAddress.isReachable(3000));
9 System.out.println("itcast的主机名为:" + remoteAddress.getHostName());
10 }
11 }
上述代码中,第4行代码获取本机的IP地址并打印,第5~6行代码获取主机名为传智教育【官网】-好口碑IT职业教育,好口碑IT培训机构,一样的教育,不一样的品质地址,第7行代码获取itcast的主机地址。第8行代码判断3秒是否可到达主机。第9行代码用于获取itcast的主机名。
9.1.3 UDP与TCP协议
协议是定义的通信规则,一般有TCP协议和UDP协议。通过TCP/IP结构,我们知道传输层的两个重要的高级协议分别是UDP和TCP,其中,UDP是User Datagram Protocol的简称,称为用户数据报协议;TCP是Transmission Control Protocol的简称,称为传输控制协议。
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输,例如视频会议使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
UDP的交互过程如下图。
TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务器端发出连接请求,每次连接的创建都需要经过“三次握手”。第一次握手,客户端向服务器端发出连接请求,等待服务器确认;第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;第三次握手,客户端再次向服务器端发送确认信息,确认连接。
TCP连接的整个交互过程如下图。
由于TCP协议的面向连接特性,它可以保证传输数据的安全性,是一个被广泛采用的协议。例如,在下载文件时,如果数据接收不完整,将会导致文件数据丢失而不能被打开,因此,下载文件时必须采用TCP协议。
9.2 UDP通信
前面介绍了UDP是一种面向无连接的协议,因此,在通信时发送端和接收端不用建立连接。UDP通信的过程就像是货运公司在两个码头间发送货物一样,在码头发送和接收货物时都需要使用集装箱来装载货物。UDP通信也是一样,发送和接收的数据也需要使用“集装箱”进行打包,为此Java提供了一个DatagramPacket类。然而运输货物只有“集装箱”是不够的,还需要有“码头”。同理,在程序中,要实现通信只有DatagramPacket数据包也是不行的,它也需要一个“码头”。为此,Java还提供了一个DatagramSocket类。
通过DatagramPacket类和DatagramSocket类发送数据的过程如下图。
9.2.1 DatagramPacket
DatagramPacket类用于封装UDP通信中发送或者接收的数据。想要创建一个DatagramPacket对象,首先需要了解一下它的构造方法。在创建发送端和接收端的DatagramPacket对象时,使用的构造方法有所不同,接收端的构造方法只需要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要接收存放了发送数据的字节数组,还需要指定发送端IP地址和端口号。
DatagramPacket类用于封装UDP通信中发送或者接收的数据。想要创建一个DatagramPacket对象
(1)DatagramPacket(byte[] buf,int length)
使用该构造方法在创建DatagramPacket对象时,指定了封装数据的字节数组和数据的大小,没有指定IP地址和端口号。很明显,这样的对象只能用于接收端,不能用于发送端。因为发送端一定要明确指出数据的目的地(IP地址和端口号),而接收端不需要明确知道数据的来源,只需要接收到数据即可。
(2)DatagramPacket(byte[] buf,int length,InetAddress addr,int port)
使用该构造方法在创建DatagramPacket对象时,不仅指定了封装数据的字节数组和数据的大小,还指定了数据包的目标IP地址(addr)和端口号(port)。该对象通常用于发送端,因为在发送数据时必须指定接收端的IP地址和端口号,就好像发送货物的集装箱上面必须标明接收人的地址一样。
(3)DatagramPacket(byte[] buf,int offset,int length)
该构造方法与第一个构造方法类似,同样用于接收端,只不过在第一个构造方法的基础上,增加了一个offset参数,该参数用于指定接收到的数据在放入buf缓冲数组时是从offset处开始的。
(4)DatagramPacket(byte[] buf,int offset,int length,InetAddress addr,int port)
该构造方法与第二个构造方法类似,同样用于发送端,只不过在第二个构造方法的基础上,增加了一个offset参数,该参数用于指定一个数组中发送数据的偏移量为offset,即从offset位置开始发送数据。
上面已经讲解了DatagramPacket的构造方法,接下来对DatagramPacket类中的常用方法进行详细的讲解。
方法声明 | 功能描述 |
---|---|
InetAddress getAddress() | 该方法用于返回发送端或者接收端的IP地址,如果是发送端的DatagramPacket对象,就返回接收端的IP地址,反之,就返回发送端的IP地址 |
int getPort() | 该方法用于返回发送端或者接收端的端口号,如果是发送端的DatagramPacket对象,就返回接收端的端口号,反之,就返回发送端的端口号 |
byte[] getData() | 该方法用于返回将要接收或者将要发送的数据,如果是发送端的DatagramPacket对象,就返回将要发送的数据,反之,就返回接收到的数据 |
int getLength() | 该方法用于返回接收或者将要发送数据的长度,如果是发送端的DatagramPacket对象,就返回将要发送的数据长度,反之,就返回接收到数据的长度 |
9.2.2 DatagramSocket
使用DatagramSocket类的实例对象就可以发送和接收DatagramPacket数据包在创建发送端和接收端的DatagramSocket对象时,使用的构造方法也有所不同,下面对DatagramSocket类中常用的构造方法进行讲解。
(1)DatagramSocket()
该构造方法用于创建发送端的DatagramSocket对象,在创建DatagramSocket对象时,并没有指定端口号,此时,系统会分配一个没有被其他网络程序所使用的端口号。
(2)DatagramSocket(int port)
该构造方法既可用于创建接收端的DatagramSocket对象,又可以创建发送端的DatagramSocket对象,在创建接收端的DatagramSocket对象时,必须要指定一个端口号,这样就可以监听指定的端口。
(3)DatagramSocket(int port,InetAddress addr)
使用该构造方法在创建DatagramSocket对象时,不仅指定了端口号,还指定了相关的IP地址。该对象的使用适用于计算机上有多块网卡的情况,在使用时可以明确规定数据通过哪块网卡向外发送和接收哪块网卡的数据。由于计算机中针对不同的网卡会分配不同的IP,因此在创建DatagramSocket对象时需要通过指定IP地址确定使用哪块网卡进行通信。
上面我们讲解了DatagramSocket的常用构造方法,接下来对DatagramSocket类中的常用方法进行详细的讲解。
方法声明 | 功能描述 |
---|---|
void receive(DatagramPacket p) | 该方法用于将接收到的数据填充到DatagramPacket数据包中,在接收到数据之前会一直处于阻塞状态,只有当接收到数据包时,该方法才会返回 |
void send(DatagramPacket p) | 该方法用于发送DatagramPacket数据包,发送的数据包中包含将要发送的数据、数据的长度、远程主机的IP地址和端口号 |
void close() | 关闭当前的Socket,通知驱动程序释放为这个Socket保留的资源 |
9.2.3 UDP网络程序
前面两个小节讲解了DatagramPacket和DatagramSocket的相关知识,接下来通过一个案例来学习一下它们在程序中的具体用法。要实现UDP通信需要创建一个发送端程序和一个接收端程序。很明显,在通信时只有接收端程序先运行,才能避免发送端发送数据时找不到接收端,而造成数据丢失的问题。因此,首先需要完成接收端程序的编写。接收端程序如下。
package com.javase.text9;import java.net.*;
// 接收端程序
public class Receiver {public static void main(String[] args) throws Exception {byte[] buf = new byte[1024]; // 创建一个字节数组,用于接收数据// 定义一个DatagramSocket对象,监听的端口号为8954DatagramSocket ds = new DatagramSocket(8954);// 定义一个DatagramPacket对象,用于接收数据DatagramPacket dp = new DatagramPacket(buf, buf.length);System.out.println("等待接收数据");ds.receive(dp); // 等待接收数据,如果没有数据则会阻塞// 调用DatagramPacket的方法获得接收到的信息//包括数据的内容、长度、发送的IP地址和端口号String str = new String(dp.getData(), 0, dp.getLength()) +"from "+ dp.getAddress().getHostAddress() + ":" + dp.getPort();System.out.println(str); // 打印接收到的信息ds.close();// 释放资源}
}
创建了一个接收端程序,用来接收数据。其中,第6行代码创建了一个DatagramSocket对象,并指定其监听的端口号为8954,这样发送端就能通过这个端口号与接收端程序进行通信。第9行代码在创建DatagramPacket对象时传入一个大小为1024个字节的数组用来接收数据,第11行代码调用DatagramPacket对象的receive()方法接收到数据以后,数据会填充到DatagramPacket中,第14~15行代码是通过DatagramPacket的相关方法可以获取接收到的数据的内容、长度、发送的IP地址和端口号等信息,第17行代码是释放资源。
从运行结果可以看到,程序运行后,程序一直处于停滞状态,这是因为DatagramSocket的receive()方法在运行时会发生阻塞,只有接收到发送端程序发送的数据时,该方法才会结束这种阻塞状态,程序才能继续向下执行。
实现了接收端程序之后,接下来还需要编写一个发送端的程序。
package com.javase.text9;import java.net.*;
//发送端程序
public class Sender {public static void main(String[] args) throws Exception {// 创建一个DatagramSocket对象DatagramSocket ds = new DatagramSocket(3000);String str = "hello world"; // 要发送的数据byte[] arr = str.getBytes(); //将定义的字符串转为字节数组//创建一个要发送的数据包,数据包包括发送的数据,//数据的长度,接收端的IP地址以及端口号DatagramPacket dp = new DatagramPacket(arr, arr.length,InetAddress.getByName("localhost"), 8954);System.out.println("发送信息");ds.send(dp); // 发送数据ds.close(); // 释放资源}
}
上述代码中,创建了一个发送端程序,用来发送数据。在创建DatagramPacket对象时需要指定目标IP地址和端口号,而且端口号必须要和接收端指定的端口号一致,这样调用DatagramSocket的send()方法才能将数据发送到对应的接收端。
在接收端程序阻塞的状态下,运行发送端程序,接收端程序就会收到发送端发送的数据而结束阻塞状态,并打印接收的数据。
在创建发送端的DatagramSocket对象时,可以不指定端口号,而发送端程序中指定端口号目的就是为了每次运行时接收端的getPort()方法的返回值都是一致的,否则发送端的端口号由系统自动分配,接收端的getPort()方法的返回值会每次都不同。
M脚下留心: UDP程序所使用的端口号被占用时运行异常
需要注意的是,运行接收端程序有时会出现一种异常,如下图。
上图所示异常是因为在一台计算机中,一个端口号上只能运行一个程序,而我们编写的UDP程序所使用的端口号已经被其他的程序占用。遇到这种情况时,可以在命令行窗口输入“netstat”命令来查看当前计算机端口占用情况,netstat命令运行结果如右图。
上图显示了所有正在运行的应用程序及它们所占用的端口号。想要解决端口号占用的问题,只需关掉占用端口号的应用程序或者使用一个未被占用的端口号重新运行程序即可。
9.2.4 多线程的UDP网络程序
在上一节中,分别实现了发送端程序和接收端程序,当接收端程序阻塞的状态下,运行发送端程序,接收端程序就会收到发送端发送的数据而结束阻塞状态,完成程序运行。实际上,发送端可以无限发送数据,接收端也可以一直接收数据,例如,聊天程序发送端可以一直发消息,接收端也可以一直接收消息,因此发送端和客户端都是多线程的。
接下来通过一个案例演示使用UDP通信方式实现多线程的UDP网络程序。
package com.javase.text9;import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Example04 {public static void main(String[] args) {new Receive().start();new Send().start();}
}
class Receive extends Thread {public void run() {try {//创建socket相当于创建码头DatagramSocket socket = new DatagramSocket(6666);//创建packet相当于创建集装箱DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);while(true) {socket.receive(packet);//接收货物byte[] arr = packet.getData();int len = packet.getLength();String ip = packet.getAddress().getHostAddress();System.out.println(ip + ":" + new String(arr,0,len));}} catch (IOException e) {e.printStackTrace();}}
}
class Send extends Thread {public void run() {try {//创建socket相当于创建码头DatagramSocket socket = new DatagramSocket();Scanner sc = new Scanner(System.in);while(true) {String str = sc.nextLine();if("quit".equals(str))break;DatagramPacket packet = new DatagramPacket(str.getBytes(),str.getBytes().length, InetAddress.getByName("127.0.0.1"), 6666);socket.send(packet);//发货}socket.close();} catch (IOException e) {e.printStackTrace();}}
}
上述代码中,第10~28行代码使用多线程的方法创建了一个接收端程序,第17~23行代码通过在接收端的while循环中调用receive()方法,不停地接收发送端发送的请求,当与发送端建立连接后,就会开启一个新的线程,该线程会去处理发送端发送的数据,而主线程仍处于继续等待状态;第29~49行代码使用多线程的方法创建的一个发送端程序,第35~43行代码通过在发送端的while循环中调用的send()方法,不停的发送数据。
【案例9-1】 模拟微信聊天
如今微信已经成为人们生活中必不可少的一款社交软件。本案例要求编写一个程序模拟微信聊天功能。在实现案例时,要求使用多线程与UDP通信完成消息的发送和接收。
(1)第一要知道用什么技术实现,通过上述任务描述可知此任务是使用多线程与UDP通信相关知识实现的。要实现图中的聊天窗口界面。首先需要定义一个实现微信聊天功能的类,类中需要定义访问微信聊天的输出语句,从而获取输入的发送端端口号、接收端端口号以及实现发送和接收功能的方法。
(2)实现发送数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取发送数据的端口号,并在实现run()的方法中,编写发送数据的方法。
(3)实现接收数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取接收数据的端口号,并在实现run()的方法中,编写显示接收到的数据的方法。
(4)创建完所有的类与方法后,运行两次程序,同时开启两个窗口来实现聊天功能。
1 import java.util.Scanner;2 public class Room {3 public static void main(String[] args) {4 System.out.println("微信聊天欢迎您!");5 Scanner sc = new Scanner(System.in);6 System.out.print("请输入您的微信号登录:");7 int sendPort = sc.nextInt();8 System.out.print("请输入您要发送消息的微信号:");9 int receivePort = sc.nextInt();10 System.out.println("微信聊天系统启动!!"); 11 //发送操作12 new Thread(new SendTask(sendPort), "发送端任务").start();13 //接收操作14 new Thread(new ReceiveTask(receivePort), "接收端任务").start();15 }16 }
1 import java.net.DatagramPacket;2 import java.net.DatagramSocket;3 import java.net.InetAddress;4 import java.util.Scanner;5 public class SendTask implements Runnable {6 private int sendPort; // 发数据的端口号7 // 构造方法8 public SendTask(int sendPort) {9 this.sendPort = sendPort;10 }11 @Override12 public void run() {13 try {14 // 1. 创建DatagramSocket对象15 DatagramSocket ds = new DatagramSocket();16 // 2.输入要发送的数据17 Scanner sc = new Scanner(System.in);18 while (true) {19 String data = sc.nextLine();// 获取键盘输入的数据20 // 3.封装数据到 DatagramPacket对象中21 byte[] buf = data.getBytes();22 DatagramPacket dp = new DatagramPacket(buf, buf.length,23 InetAddress.getByName("127.0.0.255"),sendPort);24 // 4.发送数据25 ds.send(dp);26 }27 } catch (Exception e) {28 e.printStackTrace();29 }30 }31 }
1 import java.net.DatagramPacket;2 import java.net.DatagramSocket;3 public class ReceiveTask implements Runnable{4 private int receivePort;// 接收数据的端口号5 public ReceiveTask(int receivePort) {6 this.receivePort = receivePort;7 }8 @Override9 public void run() {10 try {11 // 1.DatagramSocket对象12 DatagramSocket ds = new DatagramSocket(receivePort);13 // 2.创建DatagramPacket对象14 byte[] buf = new byte[1024];15 DatagramPacket dp = new DatagramPacket(buf, buf.length);16 // 3.接收数据17 while (true) {18 ds.receive(dp);19 // 4.显示接收到的数据20 String str = new String(dp.getData(), 0, 21 dp.getLength());22 System.out.println("收到" + 23 dp.getAddress().getHostAddress()24 + "--发送的数据--" + str);25 }26 } catch (Exception e) {27 e.printStackTrace();28 }29 }30 }
9.3 TCP通信
TCP通信同UDP通信一样,也能实现两台计算机之间的通信,但TCP通信的两端需要创建socket对象。UDP通信与TCP通信的区别在于,UDP中只有发送端和接收端,不区分客户端与服务器端,计算机之间可以任意地发送数据;而TCP通信是严格区分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户端,并且服务器端程序需要事先启动,等待客户端的连接。
Java提供了两个用于实现TCP程序的类,一个是ServerSocket类,用于表示服务器端;一个是Socket类,用于表示客户端。通信时,首先要创建代表服务器端的ServerSocket对象,创建该对象相当于开启一个服务,此服务会等待客户端的连接;然后创建代表客户端的Socket对象,使用该对象向服务器端发出连接请求,服务器端响应请求后,两者才建立连接,开始通信。
整个通信过程如下图。
9.3.1 ServerSocket
通过前面的学习可知,在开发TCP程序时,首先需要创建服务器端程序。java.net包提供了一个ServerSocket类,该类的实例对象可以实现一个服务器端的程序。通过查阅API文档可知,ServerSocket类提供了多种构造方法。
(1)ServerSocket()
ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket对象不与任何端口绑定,这样的ServerSocket对象创建的服务器端没有监听任何端口,不能直接使用,还需要继续调用bind(SocketAddress endpoint)方法将其绑定到指定的端口号上,才可以正常使用。
(2)ServerSocket(int port)
使用该构造方法在创建ServerSocket对象时,可以将其绑定到一个指定的端口号上(参数port就是端口号)。端口号可以指定为0,此时系统就会分配一个还没有被其他网络程序所使用的端口号。由于客户端需要根据指定的端口号来访问服务器端程序,因此端口号随机分配的情况并不常用,通常都会让服务器端程序监听一个指定的端口号。
(3)ServerSocket(int port, int backlog)
该构造方法就是在第二个构造方法的基础上,增加了一个backlog参数。该参数用于指定在服务器忙时,可以与之保持连接请求的等待客户数量,如果没有指定这个参数,默认为50。
(4)ServerSocket(int port, int backlog, InetAddress bindAddr)
该构造方法就是在第三个构造方法的基础上,增加了一个bindAddr参数,该参数用于指定相关的IP地址。该构造方法的使用适用于计算机上有多块网卡和多个IP的情况,使用时可以明确规定ServerSocket在哪块网卡或IP地址上等待客户的连接请求。显然,对于一般只有一块网卡的情况,就不用专门的指定了。
除了构造方法,ServerSocket还提供了其他常用方法,如下表。
方法声明 | 功能描述 |
---|---|
Socket accept() | 该方法用于等待客户端的连接,在客户端连接之前会一直处于阻塞状态,如果有客户端连接,就会返回一个与之对应的Socket对象 |
InetAddress getInetAddress() | 该方法用于返回一个InetAddress对象,该对象中封装了ServerSocket绑定的IP地址 |
boolean isClosed() | 该方法用于判断ServerSocket对象是否为关闭状态,如果是关闭状态则返回true,反之则返回false |
void bind(SocketAddress endpoint) | 该方法用于将ServerSocket对象绑定到指定的IP地址和端口号,其中参数endpoint 封装了IP 地址和端口号 |
ServerSocket对象负责监听某台计算机的某个端口号,在创建ServerSocket对象后,需要继续调用该对象的accept()方法,接收来自客户端的请求。当执行了accept()方法之后,服务器端程序会发生阻塞,直到客户端发出连接请求时,accept()方法才会返回一个Socket对象用于和客户端实现通信,程序才能继续向下执行。
9.3.2 Socket
ServerSocket对象可以实现服务器端程序,但只实现服务器端程序还不能完成通信,此时还需要一个客户端程序与之交互,为此Java提供了一个Socket类,用于实现TCP客户端程序。通过查阅API文档可知,Socket类同样提供了多种构造方法。
(1)Socket()
使用该构造方法在创建Socket对象时,并没有指定IP地址和端口号,也就意味着只创建了客户端对象,并没有去连接任何服务器。通过该构造方法创建对象后还需调用connect(SocketAddress endpoint)方法,才能完成与指定服务器端的连接,其中参数endpoint用于封装IP地址和端口号。
(2)Socket(String host, int port)
使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。
(3)Socket(InetAddress address, int port)
该构造方法在使用上与第二个构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。
除了构造方法,Socket还提供了很多其他方法,如下表。
方法声明 | 功能描述 |
---|---|
int getPort() | 该方法返回一个int类型对象,该对象是Socket对象与服务器端连接的端口号 |
InetAddress getLocalAddress() | 该方法用于获取Socket对象绑定的本地IP地址,并将IP地址封装成InetAddress类型的对象返回 |
void close() | 该方法用于关闭Socket连接,结束本次通信。在关闭Socket之前,应将与Socket相关的所有的输入输出流全部关闭,这是因为一个良好的程序应该在执行完毕时释放所有的资源 |
InputStream getInputStream() | 该方法返回一个InputStream类型的输入流对象,如果该对象是由服务器端的Socket返回,就用于读取客户端发送的数据,反之,用于读取服务器端发送的数据 |
OutputStream getOutputStream() | 该方法返回一个OutputStream类型的输出流对象,如果该对象是由服务器端的Socket返回,就用于向客户端发送数据,反之,用于向服务器端发送数据 |
接下来通过一张图描述服务器端和客户端的数据传输。
9.3.3 简单的TCP网络程序
接下来通过一个TCP通信的案例来进一步学习ServerSocket、Socket类的用法。要实现TCP通信需要创建一个服务器端程序和一个客户端程序,为了保证数据传输的安全性,首先需要实现服务器端程序。服务器端程序实现如下。
package com.javase.text9;import java.io.*;
import java.net.*;
public class Server {public static void main(String[] args) throws Exception {new TCPServer().listen(); // 创建TCPServer对象,并调用listen()方法}
}
// TCP服务器端
class TCPServer {private static final int PORT = 7788; // 定义一个端口号public void listen() throws Exception { // 定义一个listen()方法,抛出异常ServerSocket serverSocket = new ServerSocket(PORT);// 调用ServerSocket的accept()方法接收数据Socket client = serverSocket.accept();OutputStream os = client.getOutputStream();// 获取客户端的输出流System.out.println("开始与客户端交互数据");// 当客户端连接到服务器端时,向客户端输出数据os.write(("传智播客欢迎你!").getBytes());Thread.sleep(5000);// 模拟执行其他功能占用的时间System.out.println("结束与客户端交互数据");os.close();client.close();}
}
上述代码中,第9~24行代码封装了一个TCP服务端的方法,第12行代码创建ServerSocket对象时指定了端口号(7788),第14代码调用ServerSocket对象的accept()方法用于接收数据,第15行代码使用OutputStream获取客户端的输出流,第19行代码使用线程的sleep()方法使线程休眠5000毫秒,用于模拟执行其他功能占用的时间,最后在第21~22行代码中分别使用OutputStream与Socket的close()方法关闭了OutputStream与Socket。
从运行结果可以看出,控制台中的光标一直在闪动,这是因为accept()方法发生阻塞,程序暂时停止运行,直到有客户端来访问时才会结束这种阻塞状态。这时该方法会返回一个Socket类型的对象用于表示客户端,通过该对象获取与客户端关联的输出流并向客户端发送信息,同时执行Thread.sleep(5000)语句模拟服务器执行其他功能占用的时间。最后,调用Socket对象的close()方法将通信结束。
package com.javase.text9;import java.io.*;
import java.net.*;
public class Client {public static void main(String[] args) throws Exception {new TCPClient().connect();// 创建TCPClient对象,并调用connect()方法}
}
//TCP客户端
class TCPClient {private static final int PORT = 7788; // 服务器端的端口号public void connect() throws Exception {//创建一个Socket并连接到给出地址和端口号的计算机Socket client = new Socket(InetAddress.getLocalHost(), PORT);InputStream is = client.getInputStream(); // 得到接收数据的流byte[] buf = new byte[1024]; // 定义1024个字节数组的缓冲区int len = is.read(buf); // 将数据读到缓冲区中System.out.println(new String(buf, 0, len));// 将缓冲区中的数据输出client.close(); // 关闭Socket对象,释放资源}
}
上述代码中,第9~20行代码封装了一个TCP客户端的方法,第13行代码创建了一个Socket并连接到给出地址给端口号的计算机,第14行代码使用InputStream接收得到的数据流,第15行代码定义1024个字节数组的缓冲区,第16行代码将InputStream接收到的数据读到缓冲区中,最后在第18行代码中使用Socket的close()方法关闭Socket。
在客户端创建的Socket对象与服务器端建立连接后,通过Socket对象获得输入流读取服务器端发来的数据,并打印出如上图所示的结果。
同时服务器端程序会结束阻塞状态,并在控制台中打印出“开始与客户端交互数据”,然后向客户端发出数据“传智播客欢迎你!”,在休眠5秒钟后会在控制台打印出“结束与客户端交互数据”,此时,本次通信才结束。
9.3.4 多线程的TCP网络程序
实际上,很多服务器端程序都是允许被多个应用程序访问的,例如,门户网站可以被多个用户同时访问,因此服务器都是多线程的。下面就通过一个图例来表示多个用户访问同一个服务器。
在上图中,服务器端为每个客户端创建一个对应的Socket,并且开启一个新的线程使两个Socket建立专线进行通信。接下来根据上图所示的通信方式对9.3.3节的服务器端程序进行改进。
import java.io.*;
import java.net.*;
public class Server {public static void main(String[] args) throws Exception {new TCPServer().listen(); // 创建TCPServer对象,并调用listen()方法}
}
// TCP服务器端
class TCPServer {private static final int PORT = 7788; // 定义一个静态常量作为端口号public void listen() throws Exception {// 创建ServerSocket对象,监听指定的端口ServerSocket serverSocket = new ServerSocket(PORT); // 使用while循环不停的接收客户端发送的请求while (true) {// 调用ServerSocket的accept()方法与客户端建立连接final Socket client = serverSocket.accept();// 下面的代码用来开启一个新的线程new Thread() { public void run() {OutputStream os; // 定义一个输出流对象try {os = client.getOutputStream(); // 获取客户端的输出流System.out.println("开始与客户端交互数据");os.write(("中国人民欢迎你!").getBytes());Thread.sleep(5000); // 使线程休眠5000毫秒System.out.println("结束与客户端交互数据");os.close(); // 关闭输出流client.close(); // 关闭Socket对象} catch (Exception e) {e.printStackTrace();}};}.start();}}
}
在上述代码中,第9~37行代码使用多线程的方式创建了一个服务器端程序。第15~35行代码通过在while循环中调用accept()方法,不停地接收客户端发送的请求,当与客户端建立连接后,就会开启一个新的线程,该线程会去处理客户端发送的数据,而主线程仍处于继续等待状态。
为了验证服务器端程序是否实现了多线程,首先运行服务器端程序,之后运行三个客户端程序,当运行第一个客户端程序时,服务器端马上就进行数据处理,打印出“开始与客户端交互数据”,再运行第二、和第三个客户端程序,会发现服务器端也立刻做出回应,三个客户端会话结束后分别打印各自结束信息,如下图所示。
【案例9-2】 字符串反转
在使用软件或浏览网页时,我们总会查询一些数据,查询数据的过程其实就是客户端与服务器交互的过程。我们(客户端)将查询信息发送给服务器,服务器接收到查询消息之后,进行处理,将查询结果返回给我们(客户端)。本案例要求编写一个程序模拟客户端与服务器的交互,客户端向服务器传递一个字符串(键盘录入) ,服务器将字符串反转后写回,客户端再次接读取到的是反转后的字符串。本案例要求使用多线程与TCP通信相关知识完成数据交互。
(1)根据任务描述可以知道该程序用TCP通信技术实现,所以第一条就是定义客户端,键盘录入数据定义Scanner来实现,然后创建客户端指定IP地址和端口号,之后获取输出流,与输入流,最后将字符串写到服务器并将反转后的结果读出来打印在控制台。
(2)实现服务端的代码编写,首先创建服务端绑定客户端的端口号,并用Server的accept()方法接受客户端的请求。
(3)服务端定义run()方法实现之后获取输入输出流,将客户端发送过来的数据读取出来并采用链式编程的思想将字符串反转后返回到客户端。
1 import java.io.BufferedReader;2 import java.io.IOException;3 import java.io.InputStreamReader;4 import java.io.PrintStream;5 import java.net.Socket;6 import java.net.UnknownHostException;7 import java.util.Scanner;8 public class client {9 public static void main(String[] args) throws UnknownHostException, 10 IOException {11 //创建键盘录如对象12 Scanner sc = new Scanner(System.in); 13 //创建客户端,指定ip地址和端口号 14 Socket socket = new Socket("127.0.0.1", 54321); 15 BufferedReader br = new BufferedReader(new 16 InputStreamReader(socket.getInputStream())); //获取输入流17 //获取输出流18 PrintStream ps = new PrintStream(socket.getOutputStream());19 //将字符串写到服务器去20 ps.println(sc.nextLine()); 21 System.out.println(br.readLine()); //将反转后的结果读出来 22 socket.close();23 }24 }
1 import java.io.BufferedReader;2 import java.io.IOException;3 import java.io.InputStreamReader;4 import java.io.PrintStream;5 import java.net.ServerSocket;6 import java.net.Socket;7 public class server {8 public static void main(String[] args) throws IOException {9 ServerSocket server = new ServerSocket(54321);10 System.out.println("服务器启动,绑定54321端口");11 while(true) {12 final Socket socket = server.accept(); //接受客户端的请求13 new Thread() { //开启一条线程14 public void run() {15 try {16 BufferedReader br = new BufferedReader(new 17 InputStreamReader18 socket.getInputStream())); //获取输入流19 PrintStream ps = new PrintStream20 (socket.getOutputStream());//获取输出流21 //将客户端写过来的数据读取出来22 String line = br.readLine()23 line = new StringBuilder(line).24 reverse().toString(); //链式编程25 ps.println(line); //反转后写回去26 socket.close();27 } catch (IOException e) { 28 e.printStackTrace();29 }30 }31 }.start();32 }33 }34 }
【案例9-3】 上传文件
在日常工作生活中,我们总会将工作成果或生活照片等上传到某一个软件,其实这个上传过程就是将数据保存到了软件服务端。本案例要求编写一个程序模拟向服务端上传文件,在本地机器中输入一个路径,将该路径下的文件上传到服务端D盘中名称为upload的文件夹中。在上传时,把客户端的IP地址加上count标识作为上传后文件的名称,即IP(count)的形式。其中,count随着文件的增多而增大,如127.0.0.(1).jpg、127.0.0.(2).jpg。本案例要求使用多线程与TCP通信相关知识完成数据上传。
(1)根据任务描述中使用TCP通信的知识实现文件上传功能可知,要实现此功能,需要定义一个服务器接收文件的程序和 一个客户端上传文件的程序。
(2)首先要编写服务器端程序来接收文件。服务器端需要使用ServerSocket对象的accept()方法接收客户端的请求,由于一个服务器可能对于多个客户端,所以当客户端与服务器端简历连接后,服务器需要单独开启一个新的线程来处理与客户端的交互,这时需要在服务器端编写开启新线程的方法。在新线程的方法中,需要获取客户端的端口号,并且使用输入输出流来传输文件到指定的目录中。
(3)编写客户端的功能代码,客户端功能的实现,因为是用户自己输入上传文件。所以要定义键盘录入。录入后需要使用Socket类来创建客户对象,并通过输入输出流来定义指定的文件。
(4)最后我们启动程序,先启动服务端程序,再运行客户端程序来测试上传的结果。
1 import java.io.File;2 import java.io.FileOutputStream;3 import java.io.InputStream;4 import java.io.OutputStream;5 import java.net.ServerSocket;6 import java.net.Socket;7 public class FileServer {8 public static void main(String[] args) throws Exception {9 //创建ServerSocket对象10 ServerSocket serverSocket = new ServerSocket(10001); 11 while (true) {12 // 调用accept()方法接收客户端请求,得到Socket对象13 Socket s = serverSocket.accept();14 // 每当和客户端建立Socket连接后,单独开启一个线程处理和客户端的交互15 new Thread(new ServerThread(s)).start();16 }17 }18 }19 class ServerThread implements Runnable {20 // 持有一个Socket类型的属性21 private Socket socket; 22 // 构造方法中把Socket对象作为实参传入23 public ServerThread(Socket socket) { 24 this.socket = socket;25 }26 public void run() {27 // 获取客户端的IP地址28 String ip = socket.getInetAddress().getHostAddress(); 29 // 上传图片个数30 int count = 1; 31 try {32 InputStream in = socket.getInputStream();33 // 创建上传图片目录的File对象34 File parentFile = new File("D:\\upload\\"); 35 // 如果不存在,就创建这个目录36 if (!parentFile.exists()) { 37 parentFile.mkdir();38 }39 // 把客户端的IP地址作为上传文件的文件名40 File file = new File(parentFile, ip + "(" + count + 41 ").jpg");42 while (file.exists()) {43 // 如果文件名存在,则把count++44 file = new File(parentFile, ip + "(" + (count++) + 45 ").jpg");46 }47 // 创建FileOutputStream对象48 FileOutputStream fos = new FileOutputStream(file);49 // 定义一个字节数组50 byte[] buf = new byte[1024]; 51 // 定义一个int类型的变量len,初始值为052 int len = 0; 53 // 循环读取数据54 while ((len = in.read(buf)) != -1) { 55 fos.write(buf, 0, len);56 }57 // 获取服务端的输出流58 OutputStream out = socket.getOutputStream();59 // 上传成功后向客户端写出“上传成功”60 out.write("上传成功".getBytes()); 61 // 关闭输出流对象62 fos.close(); 63 // 关闭Socket对象64 socket.close(); 65 } catch (Exception e) {66 throw new RuntimeException(e);67 }68 }
1 import java.io.FileInputStream;2 import java.io.InputStream;3 import java.io.OutputStream;4 import java.net.Socket;5 import java.util.Scanner;6 public class FileClient {7 public static void main(String[] args) throws Exception {8 // 创建客户端Socket9 Socket socket = new Socket("127.0.0.1", 10001);10 // 获取Socket的输出流对象11 OutputStream out = socket.getOutputStream(); 12 // 创建FileInputStream对象13 System.out.println("请输入你要上传文件的路径:");14 Scanner sc =new Scanner(System.in);15 String upload = sc.nextLine();16 if(!upload.isEmpty()){17 FileInputStream fis = new FileInputStream(upload);18 // 定义一个字节数组19 byte[] buf = new byte[1024];20 // 定义一个int类型的变量len21 int len; 22 // 循环读取数据23 while ((len = fis.read(buf)) != -1) { 24 out.write(buf, 0, len);25 }26 // 关闭客户端输出流27 socket.shutdownOutput();28 // 获取Socket的输入流对象29 InputStream in = socket.getInputStream(); 30 // 定义一个字节数组31 byte[] bufMsg = new byte[1024];32 // 接收服务端的信息33 int num = in.read(bufMsg); 34 String Msg = new String(bufMsg, 0, num);35 System.out.println(Msg);36 // 关键输入流对象37 fis.close(); 38 // 关闭Socket对象39 socket.close(); 40 }else {41 System.out.println("对不起请您输入文件路径后再上传!!!");42 }43 }44 }45 }
9.4 本章小结
本章讲解了Java网络编程的相关知识。首先简要介绍了网络通信协议的相关知识,然后着重介绍了与UDP网络编程相关的DatagramSocket、DatagramPacket类,并通过两个的案例实现了UDP通信。最后讲解了TCP网络编程中相关的ServerSocket、Socket类,并通过两个案例实现了TCP通信。通过对本章的学习,读者能够了解网络编程相关的知识,并能够掌握UDP网络程序和TCP网络程序的编写。
相关文章:
第9章 网络编程
9.1 网络通信协议 通过计算机网络可以实现多台计算机连接,但是不同计算机的操作系统和硬件体系结构不同,为了提供通信支持,位于同一个网络中的计算机在进行连接和通信时必须要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵…...
Python setattr函数
在Python编程中,setattr()函数是一个有用且灵活的内置函数,用于设置对象的属性值。它可以在运行时动态地设置对象的属性,无论是新建对象还是已有对象。本文将深入探讨setattr()函数的用法、语法、示例代码,并探讨其在实际编程中的…...
[C#]winform制作仪表盘好用的表盘控件和使用方法
【仪表盘一般创建流程】 在C#中制作仪表盘文案(通常指仪表盘上的文本、数字或指标显示)涉及到使用图形用户界面(GUI)组件,比如Windows Forms、WPF (Windows Presentation Foundation) 或 ASP.NET 等。以下是一个使用W…...
探索弗洛姆的思想:人类本质与爱的哲学
探索弗洛姆的思想:人类本质与爱的哲学 摘要:艾里希弗洛姆是20世纪著名的德裔美国哲学家和精神分析学家,他的理论在心理学、社会学和哲学领域均产生了广泛影响。弗洛姆的核心思想围绕人的本性、自由发展以及爱的重要性展开,强调了人…...
【碎片知识点】安装Linux系统 VMware与kali
天命:VMware就是可以运行操作系统的载体,kali就是Linux的其中一个分支 天命:Linux有两个分支版本:centos与ubuntu,kali底层就是ubuntu(所有Linux用起来都差不多,没啥区别) 天命&…...
Android 车载应用开发之SystemUI 详解
一、SystemUI SystemUI全称System User Interface,直译过来就是系统级用户交互界面,在 Android 系统中由SystemUI负责统一管理整个系统层的 UI,它是一个系统级应用程序(APK),源码在/frameworks/base/packages/目录下,而不是在/packages/目录下,这也说明了SystemUI这个…...
C# CAD-Xdata数据 添加(一)
运行环境Visual Studio 2022 c# cad2016 一、XData(扩展数据)特定代码值 XData(扩展数据)特定代码值 XData通过一系列DXF组码(DxfCode)存储不同类型的数据,包括但不限于ASCII字符串、已注册应…...
【NLP】MHA、MQA、GQA机制的区别
Note LLama2的注意力机制使用了GQA。三种机制的图如下: MHA机制(Multi-head Attention) MHA(Multi-head Attention)是标准的多头注意力机制,包含h个Query、Key 和 Value 矩阵。所有注意力头的 Key 和 V…...
nginx upstream server主动健康监测模块添加https检测功能
1 缘起 前面的《nginx upstream server主动健康检测模块ngx_http_upstream_check_module 使用和源码分析》系列已经分析了ngx_http_upstream_check_module的实现原理,并且在借助这个模块的框架实现了一个udp健康检测的新功能。 但是ngx_http_upstream_check_mod…...
OCP的operator——(4)用户任务:使用Operator创建etcd集群
文章目录 环境在namespace中安装Operator先决条件使用Web console从OperatorHub安装删除 使用CLI从OperatorHub安装从已安装的Operator创建应用使用Operator创建etcd集群报错从web console debug从命令行debug分析 参考 环境 RHEL 9.3Red Hat OpenShift Local 2.32 在namespa…...
win7自带截图工具保存失效解决办法
今日发现一台远航技术的win7中自带的截图工具使用时正常,保存图片时没有弹出保存位置的对话窗口,无法正常保存图片。解决方案如下: 1、进入注册表编辑器。开始-搜索程序和文件-输入 regedit 按下回车键,打开注册表; 2、…...
Android14之Android Rust模块编译语法(一百八十七)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...
分布式文件系统 SpringBoot+FastDFS+Vue.js【三】
分布式文件系统 SpringBootFastDFSVue.js【三】 七、创建后台--分角色管理7.1.创建后台数据库表7.2.创建实体类7.2.1.Admin7.2.2.Menu7.2.3.MenuBean7.2.4.Role7.2.5.RoleMenu 7.3.编辑配置文件application.yml7.4.编写工具类7.4.1.AuthContextHolder7.4.2.HttpUtils7.4.3.Stri…...
【深度学习每日小知识】全景分割
全景分割 全景分割是一项计算机视觉任务,涉及将图像或视频分割成不同的对象及其各自的部分,并用相应的类别标记每个像素。与传统的语义分割相比,它是一种更全面的图像分割方法,传统的语义分割仅将图像划分为类别,而不…...
机器人能否返回原点
657. 机器人能否返回原点 在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束。 移动顺序由字符串 moves 表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右&a…...
Mysql5.6忘记密码,如何找回(windows)
mysql5.6安装 第一步:关闭正在运行的数据库服务 net stop mysql第二步:在my.ini文件当中的[mysqld] 任意一个位置放入 skip-grant-tables第三步:启动mysql服务 net start mysql第四步:服务启动成功后就可以登录了,…...
算法训练营day29, 贪心算法3
import ( "sort" ) // 1005. K 次取反后最大化的数组和 func largestSumAfterKNegations(nums []int, k int) int { //先从小到大排序 sort.Ints(nums) sum : 0 //将数组中负数转为正数 for i : 0; i < len(nums); i { if nums[i] < 0 && k > 0 …...
164基于matlab的奇异值分解、小波降噪、zoom细化
基于matlab的奇异值分解、小波降噪、zoom细化。程序已调通,可直接运行。 164 奇异值分解 小波降噪 zoom细化 (xiaohongshu.com)...
每日OJ题_算法_递归③力扣206. 反转链表
目录 力扣206. 反转链表 解析代码 力扣206. 反转链表 206. 反转链表 LCR 024. 反转链表 难度 简单 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,…...
【Linux】指令 【whereis】
whereis是一个用于查找特定文件在文件系统中的位置的命令。 主要查找二进制文件或帮助文件,适用于那些不属于常规文件系统的特殊文件。 查找ping命令的二进制文件,你可以使用如下命令: whereis -b ping查看命令的帮助文件,可以添…...
牛客网SQL进阶128:未完成试卷数大于1的有效用户
官网链接: 未完成试卷数大于1的有效用户_牛客题霸_牛客网现有试卷作答记录表exam_record(uid用户ID, exam_id试卷ID, st。题目来自【牛客题霸】https://www.nowcoder.com/practice/46cb7a33f7204f3ba7f6536d2fc04286?tpId240&tqId2183007&ru%2…...
GitHub的使用操作
记得看目录哦! 1. 创建仓库2. 下载desktop3. 把创建的库克隆到本地4. 文件拷贝到本地仓库![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/7171ac6c4ca14e3b8d22717121f79c9e.png)5. 在网址后面加/compare进行比较6. 给系统添加功能 1. 创建仓库 2. 下载…...
智慧公厕管理软件
随着城市化的不断推进,城市公共设施逐渐完善,其中智慧公厕的建设也在不断提速。智慧公厕作为城市基础设施的重要组成部分,对城市卫生水平提升有着不可忽视的作用。而智慧公厕管理软件更是智慧公厕管理的基础,是公共厕所智慧化管理…...
【30秒看懂大数据】数据中台
知幽科技是一家专注企业数字/智化,围绕数据价值应用的一站式数智化解决方案的咨询公司,也包括了为企业提供定制化数据培训,力求做企业最好的数智化决策伙伴。 点击上方「蓝字」关注我们 30秒看懂大数据专栏 让您在有限的碎片化时间…...
【UI自动化测试技术】自动化测试研究:Python+Selenium+Pytest+Allure,详解UI自动化测试,了解元素交互的常用方法(精)(三)
导言 在之前的文章里,我们一起学习了定位方式、等待机制等内容。相信通过之前的学习,你已经掌握了WEB自动化的一些入门知识,具备了编写代码的一些基础知识和能力。这篇文章,让我们一起学习一下模拟键盘事件。 在实际的项目当中&a…...
GPT-4带来的思想火花
GPT-4能够以其强大的生成能力和广泛的知识储备激发出众多思想火花。它能够在不同的情境下生成新颖的观点、独特的见解和富有创意的解决方案,这不仅有助于用户突破思维定势,还能促进知识与信息在不同领域的交叉融合。 1.GPT-4出色的创新思考和知识整合能…...
使用倒模耳机壳UV村脂胶液制作舞台监听耳返入耳式耳机壳有哪些优点?
使用倒模耳机壳UV树脂胶液制作舞台监听耳返入耳式耳机壳有很多优点,具体如下: 高音质表现:通过倒模工艺制作的耳机壳能够更好地贴合耳朵,减少声音散射和反射,提高声音的清晰度和质感。这对于舞台监听来说非常重要&…...
html从零开始8:css3新特性、动画、媒体查询、雪碧图、字体图标【搬代码】
css3新特性 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width, …...
HAL库 STM32驱动W25QXX驱动例程
HAL库 STM32驱动W25QXX驱动例程 📍驱动程序参考:《STM32CubeMX | 基于STM32使用HAL库W25Q128驱动程序》🔑 驱动方式:硬件SPI方式和SPI DMA方式。🔖适用于:W25X系列/Q系列芯片:W25Q80、W25Q16、W25Q32、 W25…...
C#入门及进阶|数组和集合(九):Stack类
在C#中,通过类Stack来封装对栈的操作,使得对栈的操作变得非常简单和容易理解。 栈是按照“后进先出”的原则来操作元素。 栈集合常用的属性和方法: 属性说明Count获取 Stack 中包含的元素数。方法说明Peek返回位于栈顶部的对象但不将其移除。Po…...
算法训练day31贪心算法理论基础Leetcode455分发饼干376摆动序列53最大子序和
贪心算法理论基础 文章链接 代码随想录 (programmercarl.com) 说实话贪心算法并没有固定的套路。最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。 面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可&…...
Java与JavaScript同源不同性
Java是目前编程领域使用非常广泛的编程语言,相较于JavaScript,Java更被人们熟知。很多Java程序员想学门脚本语言,一看JavaScript和Java这么像,很有亲切感,那干脆就学它了,这也间接的帮助了JavaScript的发展…...
【JavaEE】spring boot快速上手
SpringBoot快速上手 文章目录 SpringBoot快速上手Maven会出现的一个官方bug创建完项目之后常用的的三个功能依赖管理Maven仓库中央仓库本地仓库国内源配置私服 springboot项目创建什么是springspring boot项目的创建Hello Worldweb服务器 SpringMVC什么是SpringWebMVC什么是MVC…...
【数据结构】16 二叉树的定义,性质,存储结构(以及先序、后序、中序遍历)
二叉树 一个二叉树是一个有穷的结点集合。 它是由根节点和称为其左子树和右子树的两个不相交的二叉树组成的。 二叉树可具有以下5种形态。 性质 一个二叉树第i层的最大结点数为 2 i − 1 2^{i-1} 2i−1, i ≥ 1 i \geq 1 i≥1 每层最大结点可以对应完美二叉树(…...
GPT SOVITS项目 一分钟克隆 (文字输出)
步骤流程:(首先使用UVR 提取人声文件,然后按下面步骤进行) 注意这里提交的音频是参考的音频...
python34-Python列表和元组之加法
列表和元组支持加法运算,加法的和就是两个列表或元组所包含的元素的总和。 需要指出的是,列表只能和列表相加;元组只能和元组相加;元组不能直接和列表相加。 如下代码示范了元组和列表的加法运算。 # !/usr/bin/env python# -*- coding: utf-8 -*-# T…...
不做程序员了(转岗半年后对程序员岗位的思考)
不做程序员了(转岗半年后对程序员岗位的思考) 前言 好久没有更新了,已经久到CSDN的小编来问我为什么不更了。原因是我半年前转岗了,不再做程序员了,由程序员变为了产品经理。废话不多说,换个视角来给大家…...
DS:八大排序之直接插入排序、希尔排序和选择排序
创作不易,感谢三连支持!! 一、排序的概念及运用 1.1 排序的概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起 来的操作。稳定性&…...
【MySQL】-21 MySQL综合-8(MySQL默认值+MySQL非空约束+MySQL查看表中的约束)
MySQL默认值MySQL非空约束MySQL查看表中的约束 MySQL默认值在创建表时设置默认值约束在修改表MySQL默认值在创建表时设置默认值约束在修改表时添加默认值约束删除默认值约束删除默认值约束 MySQL非空约束在创建表时设置非空约束在修改表时添加非空约束删除非空约束 MySQL查看表…...
力扣hot3--并查集+哈希
第一想法是排个序然后遍历一遍,but时间复杂度就超啦 并查集居然与哈希结合了() 已经好久没用过并查集了,,,我们用哈希表f_node中来记录原结点的父节点,其中key是原结点,value是父节点…...
微信网页版能够使用(会顶掉微信app的登陆)
一、文件结构 新建目录chrome新建icons,其中图片你自己找吧新建文件manifest.json新建文件wx-rules.json 二、文件内容 对应的png你们自己改下 1、manifest.json {"manifest_version": 3,"name": "wechat-need-web","author…...
word软件中硬件图像加速有什么用处?禁用硬件图形加速(G)会影响word文档中插入图片的分辨率吗?
问题描述:word软件中硬件图像加速有什么用处?禁用硬件图形加速(G)会影响word文档中插入图片的分辨率吗? 问题解答: 在 Microsoft Word 中,硬件图形加速主要用于提高图形元素的渲染速度和性能,特别是处理大…...
.NET Core MongoDB数据仓储和工作单元模式封装
前言 上一章我们把系统所需要的MongoDB集合设计好了,这一章我们的主要任务是使用.NET Core应用程序连接MongoDB并且封装MongoDB数据仓储和工作单元模式,因为本章内容涵盖的有点多关于仓储和工作单元的使用就放到下一章节中讲解了。仓储模式(R…...
lua:有关表访问的metamethod
针对在两种正常状态:表的不存在的域的查询和修改,Lua也提供了改变 tables的行为的方法。 index metamethod 我们可以通过index元方法来实现访问table内部不存在的域时人为操控返回数据。 比如以下测试代码: local set {1,2,3} setmetata…...
【MySQL】索引事务
MySQL索引事务 1. 索引1.1 概念1.2 作用1.3 使用场景1.4 使用1.5 案例 2. 事务2.2 事物的概念2.3 使用 3. 内容重点总结 1. 索引 1.1 概念 索引是一种特殊的文件,包含着对数据表里所有记录的引用指针。可以对表中的一列或多列创建索引, 并指定索引的类…...
ChatGPT重大升级:能自动记住用户的习惯和喜好,用户有权决定是否共享数据给OpenAI
OpenAI刚刚宣布了ChatGPT的一项激动人心的更新! OpenAI在ChatGPT中新加了记忆功能和用户控制选项,这意味着GPT能够在与用户的互动中记住之前的对话内容,并利用这些信息在后续的交谈中提供更加相关和定制化的回答。 这一功能目前正处于测试阶…...
CSS设置盒子阴影
语法 box-shadow: *h-shadow v-shadow blur spread color* inset; 注释: box-shadow向框添加一个或多个阴影. 该属性是由逗号分隔的阴影列表,每个阴影由2-4个长度值、可选的颜色值及可选的inset关键词来规定。省略长度的值是0。 外阴影 a、给元素右边框和下边框加外阴影——把…...
文件夹删不掉,显示在另一个文件中打开怎么办
问题: 一、想要删掉这个文件夹,却因为文件夹中的文件打开了删不掉,这里我因为做的测试,所以是知道打开了什么 二、一般情况下文件比较多时,是不知道打开了什么的,长这个样子 解决: 一、打开任…...
阿里云香港云服务器租用_BGP多线网络_CN2高速线路测试
阿里云香港服务器中国香港数据中心网络线路类型BGP多线精品,中国电信CN2高速网络高质量、大规格BGP带宽,运营商精品公网直连中国内地,时延更低,优化海外回中国内地流量的公网线路,可以提高国际业务访问质量。阿里云服务…...
C# 异步方法的使用场景
我一直认为C#的异步方法只是一堆华而不实的东西,坑特别多,比起直接自建线程也没有任何优势。 直到有一天,一个需求场景,让我再次想到了C#的异步方法。 需求场景如下:需要写一个程序控制机械臂完成各种动作。每个动作要…...