写在前面的(偶然get到一个新技能 ❤️ 哈哈O(∩_∩)O哈哈~ )
❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️ ❤️
一、网络通信协议
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
网络通信协议有多种,目前应用最广泛的是TCP/IP协议(Transmission Control Protocol / Internet Protocol 传输控制协议 / 因特网互联协议),它是一个包括TCP协议、IP协议、UDP协议(User Datagram Protocol)、ICMP协议(Internet Control Message Protocol)和其他一些协议的协议组。
TCP / IP 协议的层次结构比较简单,共分为四层,分别是:应用层、传输层、网络层、链路层,每层负责不同的通信功能。
- 链路层:链路层用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、双绞线提供的驱动。
- 网络层:网络层是整个TCP / IP 协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
- 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
- 应用层:主要负责应用程序的协议,如HTTP协议、FTP协议等。
<1.1> IP地址和端口号
<1.1.1> IP地址
IP地址可以唯一标识一台计算机。目前,IP地址广泛使用的版本是IPv4,它是由4个字节大小的二进制数来表示。 为了便于记忆和处理,通常会将IP地址写成十进制的形式,每个字节用一个十进制数字(0~255)表示,数字间用符号“.”隔开,例如:10.0.0.1。
注意:
随着计算机网络规模的不断扩大,对IP地址的需求也越来越多,IPv4这种使用4个字节表示的IP地址面临枯竭,因此IPv6便应运而生。IPv6使用16个字节表示IP地址,它所拥有的地址容量是IPv4的 8*10^28倍,这样就解决了网络地址资源数量不足的问题。
<1.1.2> 端口号
通过IP地址可以连接到指定计算机,但如果想访问目标计算机中的某个应用程序,还需要指定端口号。在计算机中,不同的应用程序是通过端口号区分的。端口号是用两个字节(16位的二进制数)表示的,它的取值范围是0~65535 , 其中,0 ~1023之间的端口用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,避免端口号被另外一个应用或服务所占用。
<1.1.3> IP地址和端口号的作用
<1.2> InetAddress
JDK中提供了一个 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对象便可获取指定主机名,IP地址等。
eg:
public static void main(String[] args) throws IOException {
InetAddress localAddress = InetAddress.getLocalHost();
InetAddress remoteAddress = InetAddress.getByName("hao.360.com");
System.out.println("本机的IP地址:"+localAddress.getHostAddress());//本机的IP地址:10.211.27.244
System.out.println("本机的主机名"+localAddress.getHostName());//本机的主机名DESKTOP-ERQ4IIQ
System.out.println("hao.360.com的IP地址:"+remoteAddress.getHostAddress());//hao.360.com的IP地址:111.206.66.62
System.out.println("3秒是否可达:"+remoteAddress.isReachable(3000));//3秒是否可达:true
System.out.println("hao.360.com的主机名为:"+remoteAddress.getHostName());//hao.360.com的主机名为:hao.360.com
}
注意:
getHostName()方法用于得到某个主机的域名,如果创建的InetAddress对象是用主机名创建的,则将该主机名返回;否则,将根据IP地址反向查找对应的主机名,如果找到则将其返回,否则返回IP地址。
<1.3> UDP和TCP协议
<1.3.1> UDP协议
传输层的两个重要的协议,分别是UDP和TCP协议,其中UDP是User Datagram Protocol 的简称,称为用户数据报协议,TCP是Transmission Control Protocol 的简称,称为传输控制协议。
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输。 例如,视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
UDP的交换过程图:
<1.3.2> TCP协议
TCP协议是面向连接的通信协议,即在数据传输前先在发送端和接收端建立逻辑连接,然后再传输数据,==它提供了两台计算机之间可靠无差错的数据传输。==在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过 “三次握手”。 第一次握手,客户端向服务端发出连接请求,等待服务器确认;第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;第三次握手,客户端再次向服务器端发送确认信息,确认连接。
服务端与客户端连接:
由于TCP协议的面向连接特性,它可以保证传输数据的安全性,所以是一个被广泛采用的协议,例如在下载文件时,如果数据接收不完整,将会导致文件数据丢失而不能被打开,因此,下载文件时必须采用TCP协议。
二、UDP通信
UDP通信的过程就像是货运公司在两个码头间发送货物一样。在码头发送和接收货物时都需要使用集装箱来装载货物,UDP通信也一样,发送和接收数据也需使用“集装箱”进行打包。为此JDK中提供了一个DatagramPacket类,该类的实例对象就相当于一个集装箱,用于封装UDP通信中发送或者接收的数据。
<2.1> DatagramPacket类
在创建发送端和接收端的DatagramPacket对象时,使用的构造方法有所不同,接收端的构造方法只需要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要接收存放了发送数据的字节数组,还需要指定发送端的IP地址和端口号。
<2.1.1> DatagramPacket的构造方法
- DatagramPacket(byte [ ] buf , int length ) :使用该构造方法在创建 DatagramPacket对象时,指定了封装数据的字节数组和数据的大小,没有指定IP地址和端口号。很明显,这样的对象只能用于接收端,不能用于发送端。因为发送端一定要明确指出数据的目的地(IP地址和端口号),而接收数据端不需要明确知道数据的来源,只需要接收到数据即可。
- DatagramPacket(byte [ ] buf , int length , InetAddress addr , int port ):使用该构造方法在创建 DatagramPacket对象时,不仅指定了封装数据的字节数组和数据的大小,还指定了数据包的目标IP地址(addr)和端口号(port)。该对象通常用于发送端,因为在发送数据时必须指定接收端的IP地址和端口号,就好像发送货物的集装箱上面必须标明接收人的地址一样。
- DatagramPacket(byte [ ] buf , int offset , int length):该构造方法与第一个构造方法类似,同样用于接收端,只不过在第一个构造方法的基础上,增加了一个offset参数,该参数用于指定接收到的数据在放入buf缓冲数组时是从offset处开始的。
- DatagramPacket(byte [ ] buf , int offset , int length , InetAdderess addr , int port):该构造方法与第二个构造方法类似,同样用于发送端,只不过在第二个构造方法的基础上,增加了一个offset参数,该参数用于指定一个数组中发送数据的偏移量为offset,即从offset位置开始发送数据。
<2.1.2> DatagramPacket类中的常用方法
- InetAddress getAddress():该方法用于返回发送端或者接收端的IP地址,如果是发送端的DatagramPacket对象,就返回接收端的IP地址,反之,就返回发送端的IP地址。
- int getPort():该方法用于返回发送端或者接收端的端口号,如果是发送端的DatagramPacket对象,就返回接收端的端口号,反之就返回发送端的端口号。
- byte [ ] getData ():该方法用于返回将要接收或者将要发送的数据,如果是发送端的DatagramPacket对象,就返回将要发送的数据,反之,就返回接受到的数据。
- int getLength():该方法用于返回接收或者将要发送数据的长度,如果是发送端的DatagramPacket 对象,就返回将要发送的数据长度,反之,就返回接收到数据的长度。
<2.2> DatagramSocket类
DatagramPacket数据包的作用就如同是“集装箱”,可以将发送端或者接收端的数据封装起来。然而,运输货物只有“集装箱”是不够的,还需要码头。在程序中需要实现通信只有DatagramPacket数据包也同样不行,为此,JDK提供了一个DatagramSocket类。DatagramSocket类的作用就类似于码头,使用这个类的实例对象就可以发送和接收DatagramPacket数据包。
发送数据的过程如图所示:
<2.2.1> DatagramSocket类中常用的构造方法
- DatagramSocket() :该构造方法用于创建发送端的DatagramSocket对象,在创建DatagramSocket对象时,并没有指定端口号,此时,系统会分配一个没有被其他网络程序所使用的端口号。
- DatagramSocket(int port ):该构造方法既可用于创建接收端的 DatagramSocket对象,又可以创建发送端的 DatagramSocket对象,在创建接收端的 DatagramSocket对象时,必须要指定一个端口号,这样就可以监听指定的端口。
- DatagramSocket(int port , InetAddress addr):使用该构造方法在创建 DatagramSocket对象时,不仅指定了端口号,还指定了相关的IP地址,==这种情况适用于计算机上有多块网卡的情况,可以明确规定数据通过哪块网卡向外发送和接收哪块网卡的数据。==由于计算机中针对不同的网卡会分配不同的IP,因此在创建 DatagramSocket对象时需要通过指定IP地址来确认使用哪块网卡进行通信。
<2.2.2> DatagramSocket类中常用的方法
- void receive(DatagramPacket p):该方法用于将接收到的数据填充到DatagramPacket数据包中,在接收到数据之前会一直处于阻塞状态,只有当接收到数据包时,该方法才会返回。
- void send(DatagramPacket p):该方法用于发送DatagramPacket数据包,发送的数据包中包含将要发送的数据、数据的长度、远程主机的IP地址和端口号。
- void close():关闭当前的Socket,通知驱动程序释放为这个Socket保留的资源。
<2.3> UDP网络程序
/*
* 创建接收端程序的步骤:
* 1.创建一个数组,用于接收数据(相当于集装箱中的内容)
* 2.创建一个DatagramPacket对象,将1中的数组传入,封装成为一个DatagramPacket数据包(相当于集装箱)
* 3.创建一个DatagramSocket对象,用于接收指定端口发送来的DatagramPacket数据包(相当于码头)
*
*
*/
public class Receive {
public static void main(String[] args) throws Exception {
//创建一个长度为1024的字节数组,用于接收数据
byte [] buf = new byte[1024];
//创建一个DatagramPacket对象,将数组封装为一个DatagramPacket数据包
DatagramPacket dp = new DatagramPacket(buf,1024);
//创建一个DatagramSocket对象,监听端口号8954
DatagramSocket ds = new DatagramSocket(8954);
System.out.println("等待接收数据");
//调用DatagramSocket类中的receive方法,将接收到的数据填充到DatagramPacket数据包中
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();//释放资源
}
/*
* 创建发送端程序的步骤:
* 1.定义要发送的数据
* 2.创建一个DatagramPacket对象,将要发送的信息封装为一个数据包
* 3.创建一个DatagramSocket对象,用于发送数据包
*/
public class Send {
public static void main(String[] args) throws Exception {
// 1.定义要发送的数据
String str = "hello world";
//2.创建一个DatagramPacket对象,将要发送的信息封装为一个数据包
DatagramPacket dp= new DatagramPacket(str.getBytes(),str.length(),InetAddress.getByName("localhost"),8954);
//3.创建一个DatagramSocket对象,用于发送数据包
DatagramSocket ds= new DatagramSocket ();
System.out.println("发送信息");
ds.send(dp);//发送信息
ds.close();//释放资源
}
运行后出现如图所示的异常:
出现该异常的原因:
在一台计算机中,一个端口号上只能运行一个程序,而我编写的UDP程序所使用的端口号已经被其他的程序占用。所以会出现如图所示的异常。
解决方案:
在命令行窗口输入“netstat-ano”命令来查看当前计算机端口占用情况,运行结果如图所示:
想要解决端口号占用的问题,只需关掉占用端口号的应用程序或者为程序分配一个未被占用的端口号即可。
解决异常之后的运行结果:
注意:
在创建发送端的DatagramSocket对象时,可以不指定端口号。但是因为在接收端我使用了dp.getPort()方法,所以如果不指定端口号的话,会出现如上图红色框中的值每次运行的结果不同的问题。
原因是,如果不指定系统会自行分配,所以每次运行后的端口号都不一样。
解决方案:在创建发送端的DatagramSocket对象时,传入一个固定值即可解决。
修改后结果验证:
三、TCP通信
TCP通信同UDP通信一样,都能实现两台计算机之间的通信,通信的两端都需要创建Socket对象。区别在于,UDP中只有发送端和接收端,不区分客户端与服务端的,在通信时,必须先由客户端去连接服务器才能实现通信,服务器不可以主动连接客户端,并且服务端程序需要事先启动,等待客户端的连接。
在JDK中提供了两个类用于实现TCP程序,一个是ServerSocket类,用于表示服务器端,一个是Socket类,用于表示客户端。 通信时,首先创建代表服务器端的ServerSocket对象,该对象相当于开启一个服务,并等待客户的连接,然后创建代表客户端的Socket对象向服务器端发出连接请求,服务器端响应请求,两者建立连接开始通信。
整个通信过程如下图所示:
< 3.1> ServerSocket类
在开发TCP程序时,首先要创建服务器端程序。 JDK的 java.net 包中提供了一个ServerSocket类,该类的实例对象可以实现一个服务器端的程序。
< 3.1.1> ServerSocket类的构造方法
- ServerSocket():使用该构造方法在创建ServerSocket对象时并没有绑定端口号,这样的对象创建的服务器端没有监听任何端口,不能直接使用,还需要继续调用bind(ServerAddress endpoint)方法将其绑定到指定的端口号上,才可以正常使用。
❤️(最常用)
- ServerSocket(int port):使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上(参数port就是端口号)。端口号可以指定为0,此时系统就会分配一个还没有被其他网络程序所使用的端口号。由于客户端需要根据指定的端口号来访问服务器端程序,因此端口号随机分配的情况并不常用,通常都会让服务器端程序监听一个指定的端口号。
- ServerSocket(int port , int backlog):该构造方法就是在第二个构造方法的基础上,增加了一个backlog参数。该参数用于指定在服务器忙时,可以与之保持连接请求的等待客户数量,如果没有指定这个参数,默认为50。
- ServerSocket(int port , int backlog , InetAddress bindAddr):该构造方法就是在第三个构造方法的基础上,还指定了相关的IP地址,这种情况适用于计算机上有多块网卡和多个IP的情况,我们可以明确规定ServerSocket在哪块网卡或IP地址上等待客户的连接请求。显然,对于一般只有一块网卡的情况,就不用专门指定了。
< 3.1.2> 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对象用于和客户端实现通信,程序才能继续向下执行。
< 3.2> Socket类
ServerSocket对象可以实现服务端程序,但只实现服务器端程序还不能完成通信,此时还需要一个客户端程序与之交互。为此JDK提供了一个Socket类,用于实现TCP客户端程序。
< 3.2.1> Socket类的常用构造方法
❤️(最常用)
- Socket():使用该构造方法在创建 Socket对象时,并没有指定IP地址和端口号,也就意味着只创建了客户端对象,并没有去连接任何服务器。通过该构造方法创建对象后还需调用connect(SocketAddress endpoint)方法,才能完成与指定服务器端的连接,其中参数endpoint用于封装IP地址和端口号。
- Socket(String host , int port):使用该构造方法在创建 Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。
- Socket(InetAddress address , int port):该方法在使用上与第二个构造方法类似,参数address用于接收InetAddress类型的对象,该对象用于封装一个IP地址。
< 3.2.1> Socket类的常用方法
- int getPort():该方法返回一个int类型对象,该对象是Socket对象与服务器端连接的端口号。
- InetAddress getLocalAddress():该方法用于获取Socket对象绑定的本地IP地址,并将IP地址封装成InetAddress类型的对象返回。
- void close():该方法用于关闭Socket连接,结束本次通信。在关闭Socket之前,应将与Socket相关的所有的输入输出流全部关闭,这是因为一个良好的程序应该执行完毕时释放所有的资源。
- InputStream getInputStream():该方法返回一个 InputStream类型的输入流对象,如果该对象是由服务器的Socket返回,就用于读取客户端发送的数据,反之,用于读取服务器端发送的数据。
- OutputStream getOutputStream():该方法返回一个OutputStream类型的输出流对象,如果该对象是由服务器端的Socket返回,就用于向客户端发送数据,反之,用于向服务器端发送数据。
注意:
当客户端和服务端建立连接后,数据是以IO流的形式进行交互的,从而实现通信。
< 3.2.2> 简单的TCP网络程序
/*
* 服务端
*/
public class TCPServerDemo {
public static void main(String[] args) throws Exception {
new TCPServer().listen();//创建 TCPServer对象,并调用listen()方法
}
}
class TCPServer{
private static final int port = 7788;//定义一个端口号
public void listen() throws Exception{
ServerSocket serverSocket = new ServerSocket(port);//创建ServerSocket对象
Socket client = serverSocket.accept();//调用ServerSocket的accept()方法接收数据
OutputStream os = client.getOutputStream();//获取客户端的输出流
System.out.println("开始与客户端交互数据");
os.write(("我们都有一个家,名字叫中国").getBytes());
Thread.sleep(5000);
System.out.println("结束与客户端交互数据");
os.close();
client.close();
}
/*
* 客户端
*/
public class TCPClientDemo {
public static void main(String[] args) throws Exception {
new TCPClient().connect();//创建TCPClient对象,并调用connect()方法
}
}
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对象,释放资源
}
< 3.2.2> 多线程的TCP网络程序
实际上,很多服务器端程序都是允许被多个应用程序访问的,例如门户网站可以被多个用户同时访问,因此服务器都是多线程的。
如下图所示,表示多个用户访问同一个服务器:
对服务器端代码改进后:
public class TCPServerDemo {
public static void main(String[] args) throws Exception {
new TCPServer().listen();//创建 TCPServer对象,并调用listen()方法
}
}
//服务端
class TCPServer{
private static final int port = 7788;//定义一个端口号
public void listen() throws Exception{
ServerSocket serverSocket = new ServerSocket(port);//创建ServerSocket对象,监听指定的端口
while(true){
//调用ServerSocket的accept()方法与客户端建立连接
Socket client = serverSocket.accept();
//下面的代码用于开启一个新的线程
new Thread() {
public void run(){
OutputStream os;//定义一个输出流对象
try {
os = client.getOutputStream();//获取客户端的输出流
System.out.println("开始与客户端交互数据");
os.write(("我们都有一个家,名字叫中国").getBytes());
Thread.sleep(5000);//使线程休眠5000ms
System.out.println("结束与客户端交互数据");
os.close();//关闭输出流
client.close();//关闭Socket对象
} catch (Exception e) {
e.printStackTrace();
}
};
}.start();
}
}
}