外观
19 网络编程
4117 字约 14 分钟
2024-09-01
网络通信:两台设备之间通过网络实现数据传输。
java.net
包下提供了一系列类或接口,供程序员使用,完成网络通信
19.1 网络的相关概念
网络
网络:两台或多台设备通过一定物理设备连接起来构成了网络
根据网络覆盖范围的不同,对网络进行分类:
- 局域网:覆盖范围最小,仅覆盖一个教室·机房
- 城域网:覆盖范围较大,可覆盖一个城市
- 广域网:覆盖范围最大,可以覆盖全国,甚至全球。万维网 是广域网的代表
IP 地址
IP 地址:用于唯一标识网络中的每台计算机 / 主机
查看 IP 地址:ipconfig
IPv4 是 4 个字节(32位)表示。每个字节范围是 [0,255]
IP 地址的表示形式:点分十进制(xx.xx.xx.xx),每个十进制数范围是 [0,255]
IP 地址的组成 = 网络地址 + 主机地址
- A类:0 + 7 位网络号 + 24 位主机号(0.0.0.0 ~ 127.255.255.255)
- B类:1 + 0 + 14 位网络号 + 16 位主机号(128.0.0.0 ~ 191.255.255.255)
- C类:1 + 1 + 0 + 21 位网络号 + 8 位主机号(192.0.0.0 ~ 223.255.255.255)
- D类:1 + 1 + 1 + 0 + 28 位多播组号(224.0.0.0 ~ 239.255.255.255)
- E类:1 + 1 + 1 + 1 + 0 + 27 位(留待后用)(240.0.0.0 ~ 247.255.255.255)
IPv6 是互联网工程任务组设计的用于替代 IPv4 的下一代 IP 协议。其地址数量可以为全世界每一粒沙子编上一个地址
IPv4 最大的问题在于网络地址资源有限,严重制约了互联网的应用和发展。IPv6 的使用,不仅能解决网络地址资源数量的问题,也解决了多种接入设备接入互联网的障碍
IPv6 使用 16 个字节(128 位)表示地址。
表示形式有:
冒分十六进制表示法: (X:X:X:X:X:X:X:X)
:
之间的部分,出现 0 开头的场合,那些 0 可以省略0 位压缩表示法:把连续的
0
压缩为::
,这个压缩只能出现一次(X::X:X)内嵌 IPv4 地址表示法:前 96位 用冒分十六进制表示,后面 32位 用 IPv4 的点分十进制(X:X:X:X:X:XX:d.d.d.d)
子网掩码
只用一个 IP 地址,无法分辨网络部分与主机部分的分界线。因此,使用子网掩码来表示分界线。
这个场合,对应的网络部分的子网掩码的二进制数字设为 1
此外,还能把子网掩码与 IP 地址组合
- 在 IP 地址后加斜线及网络部分二进制数字数(IPV4):192.168.15.1/16
- IPv6:X:X:X:X:X:X:X:X/64
通过更改子网掩码,可以细分网络为多个子网。
保留地址
IP 还定义了一套特殊的地址格式,称为保留地址,这些保留地址不分配给任何主机。
网络号 | 主机号 | 地址类型 | 举例 | 用途 |
---|---|---|---|---|
全 0 | 全 0 | 本机地址 | 0.0.0.0 | 启动时使用 |
任意 | 全 0 | 网络地址 | 61.0.0.0 | 标识一个网络 |
任意 | 全 1 | 直接广播地址 | 129.21.255.255 | 在特定网络上广播 |
全 1 | 全 1 | 有线广播地址 | 255.255.255.255 | 在本网段上广播 |
第一段为 127 | 任意 | 回送地址 | 127.0.0.1 | 测试 |
私有地址
私有地址:与 IP 地址(全局地址)相比,在不同的网络中可以重复的地址。
私有地址是以下范围中的地址。这些地址不能作为全局地址使用:
- 10.0.0.0 ~ 10.255.255.255
- 172.16.0.0 ~ 172.31.255.255
- 192.168.0.0 ~ 192.168.255.255
将私有地址连接到全局地址的方法:
- NAT:一种私有地址与全局地址一一对应的机制
- NAPT:一种用一个全局地址连接多个计算机的机制
域名
示例:http://bbs.tianya.cn/post-house-252774-1.shtml
为了方便记忆,解决记忆 IP 的困难
IP 地址根据 HTTP 协议 映射成域名
通过 DNS(Domain Name System)服务将域名转化为 IP 地址
端口号
用于标识计算机上某个特定的网络程序
表示形式:以整数形式,范围 [0,65535]
0 ~ 1024 已经被占用,不要使用。比如 ssh 22、ftp 21、smtp 25、http 80
常见的网络程序端口号:
- tomcat:8080
- mysql:3306
- oracle:1521
- sqlserver:1433
网络通信协议
协议(TCP/IP)
TCP/IP:传输控制协议 / 因特网互联协议(Transmission Control Protocol / Internet Protocol),又叫 网络通讯协议。这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础。简单来讲,就是由 网络层的 IP 协议 和传输层的 TCP 协议 组成
(数据封装图_19.1)
OSI 模型(理论) | TCP/IP 模型(实际使用) | TCP/IP 模型各层对应协议 |
---|---|---|
应用层 | 应用层 | HTTP、ftp、telent、DNS…… |
表示层 | 应用层 | 同上 |
会话层 | 应用层 | 同上 |
传输层 | 传输层(TCP) | TCP、UDP…… |
网络层 | 网络层(IP) | IP、ICMP、ARP…… |
数据链路层 | 物理 + 数据链路层 | Link |
物理层 | 物理 + 数据链路层 | 同上 |
19.1.1 TCP 和 UDP
TCP
传输控制协议
- 使用 TCP 协议前,须先建立 TCP 连接,形成传输数据通道。TCP 通信是一对一通信
- 传输前,采用 “三次握手” 方式,是可靠的
- TCP 协议进行通信的两个应用进程:客户端、服务端
- 在连接中可进行大数据量的传输。传输前,先确认要交流的数据量。那个数据量、数据窗口取较小方的数值。
- 发送方没有收到接收方的确认应答时,(在一定次数内)会再次发送数据包
- 传输完毕,需释放已建立的连接,效率低
UDP
用户数据协议
- 将 数据、源、目的 封装成数据包,不需要建立连接。可以同时向多个接收方发送
- 每个数据包大小限制在 64K 以内,不适合传输大量数据
- 因无需连接,所以是不可靠的
- 接收方无需发送确认应答
- 发送数据结束时无需释放资源(因为不是面向连接的),速度快
19.2 InetAddress
类
相关方法
getLocalHost
:获取本机InetAddress
对象 //静态方法getByName
:根据指定主机名 / 域名获取 IP 地址对象 //静态方法getHostName
:获取InetAddress
对象的主机名/域名getHostAddress
:获取InetAddress
对象的地址
19.3 Socket
- 套接字(Socket)开发网络应用程序被广泛采用,以至于成为了事实上的标准
- 通信的两端都要有 Socket,是两台机器间通信的端点
- 网络通信其实就是 Socket 间的通信
- Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输
- 一般主动发起通信的应用程序属于客户端,等待通信请求的为服务端
19.3.1 TCP 网络通信编程
- 基于客户端——服务端的网络通信
- 底层使用的是 TCP / IP 协议
- 应用场景距离:客户端发送数据,服务端接收并显示
- 基于 Socket 的 TCP 编程
下面,示范一个 服务端
public void server() throws IOException{
ServerSocket serverSocket = new ServerSocket(9000); //[1]
Socket clientSocket = serverSocket.accept(); //[2]
//下面是输入流,不解释了
InputStream inputStream = clientSocket.getInputStream();
System.out.println(clientSocket.getInetAddress());
int n;
byte[] b = new byte[1024];
byte[] B = new byte[0];
while ((n = inputStream.read(b, 0, 1024)) != -1) {
B = Arrays.copyOf(B, B.length + n);
for (int i = 0; i < n; i++) {
B[B.length - n + i] = b[i];
}
}
System.out.println(new String(B));
InputStream.close();
Socket.close();
serverSocket.close(); //[3]
}
ServerSocket serverSocket = new ServerSocket(9000);
这个语句用以监听 9000 这个端口,且只会监听一次
如果想要一直保持监听状态,像下面的案例,将会一直在设置的端口监听,每次循环都会返回一个socket。
while (true) {//不停监听9000接口
System.out.println("服务端等待接收消息...");
Socket socket = serverSocket.accept()
... //对返回的socket进行处理
}
细节:这里要求该端口没有被其他服务占用。
Socket clientSocket = serverSocket.accept();
这个语句用以接收连接的
Socket
。没有连接时,程序会阻塞在这里。细节:此处
accept()
可以返回多个Socket
,即多并发serverSocket.close();
结束监听后,务必关闭!
下面,示范一个客户端
public void client() throws IOException{
String serverIP = "192.168.3.16"; //[1]
Socket socket = new Socket(serverIP, 9000); //[2]
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello,Server!".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput(); //[3]
outputStream.close();
socket.close();
}
这个 IP 是我的本机地址。代表的是 服务端 地址
Socket socket = new Socket(serverIP, 9000);
表示访问指定 IP 的 9000 端口
socket.shutdownOutput();
这里是输出一个结束标记。若不如此做,socket 就不知道是否数据发送完成
特别的,由 字节流 输出的场合,
writer.newLine()
可以替代结束标记,后面要使用writer.flush()
。但是这个场合,接收必须是 读取单行reader.readLine()
,不支持while循环读取,while读取场合还是需要使用==socket.shutdownOutput();
==
案例:
1.编写一个服务端,和一个客户端 2.服务器端在8888端口监听 3.客户端连接到服务端,发送一张图片eqie.png 4.服务器端接收到客户端发送的图片,保存到sc下,发送"收到图片"再退出 5.客户端接收到服务端发送的"收到图片",再退出 6.该程序要求使用StreamUtils..java
public class TCPFileUploadClient {
public static void main(String[] args) throws Exception {
//客户端连接服务端 8888,得到Socket对象
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
//创建读取磁盘文件的输入流
//String filePath = "e:\\qie.png";
String filePath = "e:\\abc.mp4";
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
//bytes 就是filePath对应的字节数组
byte[] bytes = StreamUtils.streamToByteArray(bis);
//通过socket获取到输出流, 将bytes数据发送给服务端
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
bos.write(bytes);//将文件对应的字节数组的内容,写入到数据通道
bis.close();
socket.shutdownOutput();//设置写入数据的结束标记
//=====接收从服务端回复的消息=====
InputStream inputStream = socket.getInputStream();
//使用StreamUtils 的方法,直接将 inputStream 读取到的内容 转成字符串
String s = StreamUtils.streamToString(inputStream);
System.out.println(s);
//关闭相关的流
inputStream.close();
bos.close();
socket.close();
}
}
public class TCPFileUploadServer {
public static void main(String[] args) throws Exception {
//1. 服务端在本机监听8888端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端在8888端口监听....");
//2. 等待连接
Socket socket = serverSocket.accept();
//3. 读取客户端发送的数据
// 通过Socket得到输入流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
byte[] bytes = StreamUtils.streamToByteArray(bis);
//4. 将得到 bytes 数组,写入到指定的路径,就得到一个文件了
String destFilePath = "src\\abc.mp4";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
bos.write(bytes);
bos.close();
// 向客户端回复 "收到图片"
// 通过socket 获取到输出流(字符)
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("收到图片");
writer.flush();//把内容刷新到数据通道,对于字符流,一般写入的时候想要马上看到一般需要flush()
socket.shutdownOutput();//设置写入结束标记
//关闭其他资源,一般和socket有关的都放在最后
writer.close();
bis.close();
socket.close();
serverSocket.close();
}
}
/**
* 此类用于演示关于流的读写方法
*/
public class StreamUtils {
/**
* 功能:将输入流转换成byte[], 即可以把文件的内容读入到byte[]
* @param is
* @return
* @throws Exception
*/
public static byte[] streamToByteArray(InputStream is) throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();//创建输出流对象
byte[] b = new byte[1024];//字节数组
int len;
while((len=is.read(b))!=-1){//循环读取
bos.write(b, 0, len);//把读取到的数据,写入bos
}
byte[] array = bos.toByteArray();//然后将bos 转成字节数组
bos.close();
return array;
}
/**
* 功能:将InputStream转换成String
* @param is
* @return
* @throws Exception
*/
public static String streamToString(InputStream is) throws Exception{
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder builder= new StringBuilder();
String line;
while((line=reader.readLine())!=null){
builder.append(line+"\r\n");
}
return builder.toString();
}
}
#19.3.1.1 netstat 指令
netstat -an
可以查看当前主机网络情况,包括端口监听情况和网络连接情况netstat -an | more
可以分页显示netstat -anb
可以显示占用端口的应用要求在 dos 控制台下执行
Listening 表示某个端口在监听。
如果有一个外部程序连接到该端口,就会显示一条连接信息 Established
#19.3.1.2 TCP 连接秘密
当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通讯的。这个端口由 TCP/IP 来分配,是不确定的,随机的。
19.3.2 UDP 网络通信编程
- 类
DatagramSocket
和DatagramPacket
实现了基于 UDP 协议网络程序 - 没有明确的服务端和客户端,演变成数据的发送端和接收端
- UDP 数据报通过数据报套接字
DatagramSocket
发送和接收。系统不保证 UDP 数据报一定能安全送到目的地,也不能确定什么时候能抵达 DatagramPacket
对象封装了 UDP 数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号- 接收到
DtagramPacket
对象时,需要进行拆包,取出数据 DatagramSocket
可以指定在哪个端口接收数据- UDP 协议中每个数据报都给出了完整的地址信息,因此无需发送方和接收方的连接
下面,示范一个接收端
DatagramSocket ds = new DatagramSocket(9000); //[1]
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length); //[2]
System.out.println("萝茵 聆听中……");
ds.receive(dp); //[3]
int len = dp.getLength();
bytes = dp.getData();
System.out.println("萝茵听到了如下内容:\n" + new String(bytes, 0, len));
ds.close(); //[4]
DatagramSocket ds = new DatagramSocket(9000);
以 9000 这个端口作为监听端口
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
构建
DatagramPacket
对象,准备接收数据ds.receive(dp);
监听信息,放到刚刚创建的
DatagramPacket
对象,如果没有发送端发送消息,会导致阻塞ds.close()
要记得关闭呦 ★ ~
下面,示范一个发送端
System.out.println("萝茵,大声喊道:你好,世界!");
DatagramSocket ds = new DatagramSocket(8000); //[1]
InetAddress ia = InetAddress.getByName(serverIP);
byte[] bytes = "你好,世界".getBytes(StandardCharsets.UTF_8);
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, ia, 9001);
//[2]
ds.send(dp); //[3]
System.out.println("声音在虚无中回荡着……");
ds.close(); //[4]
DatagramSocket ds = new DatagramSocket(8000);
以 8000 这个端口作为发送端口
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, ia, 9001);
把要发送的数据、数据长度、对象地址、对象端口 放到包里
ds.send(dp);
走你 ★ ~
ds.close();
鸟尽弓藏
A和B端都可以是接收端或发送端,没有客户端和服务器的概念。
public class DatagramSocketA {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(6666);
byte[] bytes = new byte[1024];
DatagramPacket data = new DatagramPacket(bytes,bytes.length);
socket.receive(data); //【1】如果没有发送端发送信息,将会导致阻塞,即代码不会进行下去。
int len = data.getLength();
bytes = data.getData();
System.out.println(new String(bytes,0,len));
byte[] buffer = "好的,明天见".getBytes();
DatagramPacket datagramPacket = new DatagramPacket(buffer,0,buffer.length,
InetAddress.getByName("DESKTOP-DP2R4CS"),5555);
socket.send(datagramPacket);
socket.close();
System.out.println("A端退出...");
}
}
public class DatagramSocketB {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(5555);
byte[] bytes = "hello,明天吃火锅".getBytes();
DatagramPacket packet = new DatagramPacket(bytes,0,bytes.length,
InetAddress.getByName("DESKTOP-DP2R4CS"),6666);
socket.send(packet);
byte[] buffer = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buffer,buffer.length);
socket.receive(datagramPacket);
int length = datagramPacket.getLength();
byte[] data = datagramPacket.getData();
System.out.println(new String(data,0,length));
socket.close();
System.out.println("B端退出...");
}
}
附录
项目开发流程
#1 需求分析
需求分析师(懂技术 + 懂行业)
需求分析报告
- 项目功能
- 客户要求
#2 设计阶段
架构师 / 项目经理
设计工作
- UML 类图
- 流程图
- 模块设计
- 数据库设计
- 架构
原型开发
组建团队
#3 实现阶段
程序员 / 码农
- 完成架构师的模块功能
- 测试自己的模块
#4 测试阶段
测试工程师
- 单元测试
- 测试用例
- 白盒测试
- 黑盒测试
- 集成测试
#5 实施阶段
实施工程师(开发能力 / 环境配置部署能力)
- 把项目正确地部署到客户的平台,并保证运行正常
- 身体好
#6 维护阶段
- 发现 bug 并解决
- 项目升级