该文章Github地址:https://github.com/AntonyCheng/java-notes
在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!
上一章:由浅到深认识Java语言(38):I/O流
45.网络编程
软件结构
**C/S结构:**全称为 Client/Server 结构,是指客户端和服务器结构,重点在于客户端开发,常见的程序有QQ,迅雷,百度网盘等软件,但是客户端开发并不适合于 Java ,而适合于 C/C++ ;
**B/S结构:**全称为 Browser/Server 结构,是指浏览器和服务器结构,重点在于服务器端开发,常见浏览器有 IE ,谷歌,火狐等,服务端开发才是适合用 Java 实现的;
两种架构各有优势,但是无论是哪种架构,都离不开网络的支持;
**网络编程:**就是在一定的协议下,实现两台计算机的通信的程序;
网络通信协议
TCP/IP协议参考模型
-
**网络通信协议:**通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。
-
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
上图中,OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广。
TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。
TCP/IP协议中的四层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能。
- 链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
- 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。而IP协议是一种非常重要的协议。IP(internet protocal)又称为互联网协议。IP的责任就是把数据从源传送到目的地。它在源地址和目的地址之间传送一种称之为数据包的东西,它还提供对数据大小的重新组装功能,以适应不同网络对包大小的要求。
- 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。TCP(Transmission Control Protocol)协议,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务。
- 应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。
而通常我们说的TCP/IP协议,其实是指TCP/IP协议族,因为该协议家族的两个最核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准,所以简称为TCP/IP协议。
TCP与UDP协议
我们所接触较多的是 TCP 协议,而 UDP 协议了解即可;
java.net 包中提供了两种常见的网络协议的支持:
-
UDP:用户数据报协议(User Datagram Protocol);
- 面向无连接的,不可靠的且不安全的协议:UDP 是无连接通信协议,即在传输数据时,数据的发送端和接收端不建立逻辑链接,就是一台计算机向另一台计算机发送数据时不会确认接收端是否存在就会发出信息,同样接收端在收到数据时也不会向发送端反馈是否收到数据,==所以容易产生丢包(数据包丢失)的现象==;
- **消耗资源小,通信效率高:**所以常用于音频,视频和普通数据(QQ,微信消息)的传输,即使丢失一两个数据包也不会对接收结果产生太大影响,但是Java 并不擅长这样的协议;
- **有大小限制:**UDP 协议会把数据限制在 64KB 之内,超出这个范围就不能发送了;
- **数据报(Datagram):**网络传输的基本单位;
-
TCP:传输控制协议(Transmission Control Protocol);
-
**面向连接的,可靠协议:**通信双方必须建立逻辑链接才能传输数据,理论上此种数据传输是可靠无差错的,往往现实不然,它是基于字节流的传输层通信协议,可以连续传输大量的数据,类似于打电话或者是百度云盘的下载;
-
**握手和挥手:**当一台计算机与另一台计算机建立连接时,TCP 协议会采用“三次握手”方式让它们建立一个连接——用于发送和接收数据虚拟链路,当数据传输完毕后 TCP 协议会采用“四次挥手”的方式断开连接;
-
**三次握手:**TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠,但是会浪费网络资源,速度慢;
-
第一次握手:客户端向服务器端发出连接请求,等待服务器确认;
注意:服务器端永远不会向客户端主动发起连接请求;
-
第二次握手:服务器端向客户端送回一个响应,通知客户端收到了连接请求;
-
第三次握手:客户端再次向服务器端发出确认信息,确认连接;
-
-
**四次挥手:**TCP协议中,在发送数据结束后,释放连接时需要经过四次挥手;
-
第一次挥手:客户端向服务器端提出结束连接,让服务器端做最后的准备工作,此时客户端处于半关闭状态,即不再向服务器端发送数据,但是可以接收数据;
-
第二次挥手:服务器端收到释放连接的请求后,会将最后的数据发给客户端,并且告知上层应用进程不再接收数据;
-
第三次挥手:服务器端发送完数据后,会给客户端发送一个释放连接的报文,那么客户端接收后就知道可以正式释放连接了;
-
第四次挥手:客户端接收到服务器端最后的释放连接报文后,要回复一个彻底断开的报文,这样服务器端收到之后才会彻底释放连接;
-
-
-
网络编程三要素
协议
**协议:**计算机网络通信必须遵守的规则,详情请看上节介绍;
IP地址
IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”;
IP地址分类方式一:
-
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成
a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个; -
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张;
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题;
IP地址分类方式二:
公网地址(万维网使用)和 私有地址(局域网使用)。192.168.开头的就是私有址址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用;
常用命令:
- 查看本机IP地址,在控制台输入:
ipconfig
- 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 192.168.80.1
特殊的IP地址:
- 本地回环地址(hostAddress):
127.0.0.1
- 主机名(hostName):
localhost
域名:
因为IP地址数字不便于记忆,因此出现了域名,域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ------- 域名解析;
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了;
- 端口号:用两个字节表示的整数,它的取值范围是0~65535。
- 公认端口:0~1023。被预先定义的服务通信占用,如:HTTP(80),FTP(21),Telnet(23);
- 注册端口:1024~49151。分配给用户进程或应用程序。如:Tomcat(8080),MySQL(3306),Oracle(1521);
- 动态/ 私有端口:49152~65535;
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败;
利用 协议
+IP地址
+端口号
三元组合,就可以标识网络中的进程: IP 192.168.1.100:8080
,那么进程间的通信就可以利用这个标识与其它进程进行交互;
InetAddress类
InetAddress 类获取本机 IP 地址对象;
package top.sharehome.Demo;import java.net.InetAddress;public class Demo {public static void main(String[] args) throws Exception {/*** 以下是该类获取自己计算机的ip地址和主机名*/InetAddress inetAddress = InetAddress.getLocalHost();String ip = inetAddress.getHostAddress();String hostName = inetAddress.getHostName();System.out.println("inetAddress = " + inetAddress);System.out.println("ip = " + ip);System.out.println("hostName = " + hostName);System.out.println("===================================");/*** 以下是该类获取远程计算机的ip地址和主机名*/InetAddress inetAddress1 = InetAddress.getByName("byName");String ip1 = inetAddress1.getHostAddress();String hostName1 = inetAddress1.getHostName();System.out.println("inetAddress1 = " + inetAddress1);System.out.println("ip1 = " + ip1);System.out.println("hostName1 = " + hostName1);}
}
打印效果如下:
报错是因为这个远程计算机的主机名是不存在的!
TCP:Socket和ServerSocket
通信的两端都要有Socket(也可以叫“套接字”),是两台机器间通信的端点。网络通信其实就是Socket间的通信。Socket可以分为:
- 流套接字(stream socket):使用TCP提供可依赖的字节流服务
- ServerSocket:此类实现TCP服务器套接字。服务器套接字等待请求通过网络传入。
- Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
- 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
- DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。
图示如下:
图示示例如下:
服务器端:
package top.sharehome.Demo;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;/*** 下面实现了一个服务器端*/
public class ICPServer {public static void main(String[] args) {ServerSocket serverSocket = null;InputStream inputStream = null;OutputStream outputStream = null;try {//使用serverSocket建立服务器端,并设置端口serverSocket = new ServerSocket(8888);//使用serverSocket类中的accept()方法获取接收端,即客户端的对象Socket accept = serverSocket.accept();//使用accept.getInputStream()方法创建输入流并从客户端获取内容inputStream = accept.getInputStream();byte[] arr = new byte[1024];int flag = inputStream.read(arr);System.out.println(new String(arr, 0, flag));System.out.println("accept = " + accept);//accept.getOutputStream()方法撞见一个输出流并向客户端输出内容outputStream = accept.getOutputStream();outputStream.write("thank".getBytes());} catch (IOException e) {e.printStackTrace();} finally {try {if (serverSocket != null) {serverSocket.close();}} catch (IOException e) {e.printStackTrace();}try {if (inputStream != null) {inputStream.close();}} catch (IOException e) {e.printStackTrace();}try {if (outputStream != null) {outputStream.close();}} catch (IOException e) {e.printStackTrace();}}}
}
客户端:
package top.sharehome.Demo;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;/*** 下面实现了一个客户端*/
public class ICPclient {public static void main(String[] args) {Socket socket = null;OutputStream outputStream = null;InputStream inputStream = null;try {//使用Socket建立客户端,并指定ip地址和端口socket = new Socket("127.0.0.1", 8888);//调用socket.getOutputStream()方法获取输出流并向服务器端输出内容outputStream = socket.getOutputStream();outputStream.write("Hello".getBytes());//调用socket.getInputStream()方法获取输入流并从服务器端输入内容inputStream = socket.getInputStream();byte[] arr = new byte[1024];int flag = inputStream.read(arr);System.out.println(new String(arr, 0, flag));System.out.println("socket = " + socket);} catch (IOException e) {e.printStackTrace();} finally {try {if (socket != null) {socket.close();}} catch (IOException e) {e.printStackTrace();}try {if (outputStream != null) {outputStream.close();}} catch (IOException e) {e.printStackTrace();}try {if (inputStream != null) {inputStream.close();}} catch (IOException e) {e.printStackTrace();}}}
}
打印效果如下:
上传服务器死循环问题
在 I/O 流的学习中,我们结识到了通过循环来快速写入和读取的方法,但是这个循环放在客户端和服务器端之间就会出现很严重的问题,以 客户端通过基础流读取客户端文件数据然后向服务器写入数据,服务器端读取客户端数据后再通过基础流向服务器端文件里 为例:
//客户端相关操作代码:
byte[] bytes = new byte[1024];
int flag = 0;
while ((flag = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, flag);
}
inputStream = socket.getInputStream();
flag = inputStream.read(bytes);
//服务器端相关操作代码:
byte[] bytes = new byte[1024];
int flag = 0;
while ((flag = inputStream.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, flag);
}
当客户端读取数据到客户端文件末尾处时会返回 -1,然后结束掉客户端的循环,但是由于 read() 方法的阻塞性,服务器端中的 read() 方法会一直等待着客户端继续写入数据,但是此时的客户端并不会再向服务器端传输数据,所以导致了服务器没办法完成该次相应,从而使客户端中的 read() 也进入了阻塞状态,进而进入一种双终端之间的死循环;
解决办法:
再写入完毕之后使用 Socket 类中的 shutdownOutput() 方法手动关闭写入通道,并且告诉对方自己已经写入完毕;
//客户端相关操作代码:
byte[] bytes = new byte[1024];
int flag = 0;
while ((flag = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, flag);
}
socket.shutdownOutput(); //手动关闭写入通道
inputStream = socket.getInputStream();
flag = inputStream.read(bytes);
//服务器端相关操作代码:
byte[] bytes = new byte[1024];
int flag = 0;
while ((flag = inputStream.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, flag);
}
文件名重复问题
向服务器上传内容就类似于复制粘贴,如果粘贴时的文件名和粘贴路径中的文件有所重复,那么原来的文件就会被覆盖掉,这样的话向服务器上传文件始终只能是一个文件,所以我们要自定义一些文件名,最常见的就是 System.currentTimeMillis() 获取得到毫秒值再加上是 new Random().nextInt() 随机数;
示例如下:
//客户端相关代码:
fileInputStream = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\ImgClient\\1.jpg"); //由客户端读取客户端文件;
//服务器端相关代码:
fileOutputStream = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\ImgServer\\" + System.currentTimeMillis() + new Random().nextInt(999999999) +".jpg"); //由服务器端接收客户端的数据并且将数据打包重命名为System.currentTimeMillis() + new Random().nextInt(999999999).jpg
图片上传案例
示例图片如下:
客户端代码:
package top.sharehome.Demo;import java.io.*;
import java.net.Socket;public class ICPclient {public static void main(String[] args) {/*** 我们需要创建四个流* 1.连接服务器* 2.字节流将图片引入* 3.链接对象的字节输出流,将图片写入服务器* 4.读取服务器返回的上传成功的消息*/FileInputStream fileInputStream = null;OutputStream outputStream = null;Socket socket = null;InputStream inputStream = null;try {socket = new Socket("127.0.0.1", 8888);fileInputStream = new FileInputStream("d:\\大学课程学习文档\\java\\Practice\\ImgClient\\1.jpg");outputStream = socket.getOutputStream();byte[] bytes = new byte[1024];int flag = 0;while ((flag = fileInputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, flag);}socket.shutdownOutput();inputStream = socket.getInputStream();flag = inputStream.read(bytes);System.out.println("socket = " + socket);System.out.println(new String(bytes, 0, flag));} catch (IOException e) {e.printStackTrace();} finally {try {if (fileInputStream != null) {fileInputStream.close();}} catch (IOException e) {e.printStackTrace();}try {if (socket != null) {socket.close();}} catch (IOException e) {e.printStackTrace();}}}
}
服务器端代码:
package top.sharehome.Demo;import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;public class ICPServer {public static void main(String[] args) {/*** 我们需要建立三个流* 1.和客户端建立连接* 2.接收客户端传来的数据* 3.返回上传成功的信息*/ServerSocket serverSocket = null;FileOutputStream fileOutputStream = null;InputStream inputStream = null;OutputStream outputStream = null;Socket client = null;try {serverSocket = new ServerSocket(8888);client = serverSocket.accept();fileOutputStream = new FileOutputStream("d:\\大学课程学习文档\\java\\Practice\\ImgServer\\" + System.currentTimeMillis() + new Random().nextInt(999999999) +".jpg");inputStream = client.getInputStream();byte[] bytes = new byte[1024];int flag = 0;while ((flag = inputStream.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, flag);}System.out.println("client = " + client);outputStream = client.getOutputStream();outputStream.write("上传图片成功!".getBytes());} catch (IOException e) {e.printStackTrace();} finally {try {if (serverSocket != null) {serverSocket.close();}} catch (IOException e) {e.printStackTrace();}try {if (fileOutputStream != null) {fileOutputStream.close();}} catch (IOException e) {e.printStackTrace();}try {if (client != null) {client.close();}} catch (IOException e) {e.printStackTrace();}}}
}
打印效果如下:
UDP:DatagramSocket
基于UDP协议的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报;
DatagramSocket 类的常用方法:
- public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择;
- public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择;
- public void close()关闭此数据报套接字;
- public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号;
- public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短;
DatagramPacket类的常用方法:
- public DatagramPacket(byte[] buf,int length) 构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length;
- public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length;
- public int getLength()返回将要发送或接收到的数据的长度;
示例代码
发送端:
package top.sharehome.Demo;import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;public class Send {public static void main(String[] args)throws Exception {
// 1、建立发送端的DatagramSocketDatagramSocket ds = new DatagramSocket();//要发送的数据ArrayList<String> all = new ArrayList<String>();all.add("hello java");all.add("hello C");all.add("hello php");all.add("hello python");//接收方的IP地址InetAddress ip = InetAddress.getByName("127.0.0.1");//接收方的监听端口号int port = 9999;//发送多个数据报for (int i = 0; i < all.size(); i++) {
// 2、建立数据包DatagramPacketbyte[] data = all.get(i).getBytes();DatagramPacket dp = new DatagramPacket(data, data.length, ip, port);
// 3、调用Socket的发送方法ds.send(dp);}// 4、关闭Socketds.close();}
}
接收端:
package top.sharehome.Demo;import java.net.DatagramPacket;
import java.net.DatagramSocket;public class Receive {public static void main(String[] args) throws Exception {
// 1、建立接收端的DatagramSocket,需要指定本端的监听端口号DatagramSocket ds = new DatagramSocket(9999);//一直监听数据while(true){// 2、建立数据包DatagramPacketbyte[] buffer = new byte[1024*64];DatagramPacket dp = new DatagramPacket(buffer , buffer.length);// 3、调用Socket的接收方法ds.receive(dp);//4、拆封数据String str = new String(buffer,0,dp.getLength());System.out.println(str);}}
}