Java网络编程学习笔记
网络编程概述
CS和BS架构
CS(Client Server):客户端-服务器架构
BS(Browser Server):浏览器-服务器架构
网络编程三要素
要素 | 描述 |
---|---|
IP | 设备在网络中的地址,唯一标识 |
端口 | 应用程序在设备中唯一的标识 |
协议 | 数据在网络中传输的规则,如TCP/UDP |
IP地址
IPv4
32位,点分十进制表示
范围:0.0.0.0 - 255.255.255.255
特殊地址:127.0.0.1(localhost,本地回环地址)
IPv6
128位,冒分十六进制表示
范围:0:0:0:0:0:0:0:1 - ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
0位压缩表示法:FF01:0:0:0:0:0:0:1101 → FF01::1101(只能使用一次)
常用命令
ipconfig
:获取本机IPping
:测试IP是否可用
端口
端口号范围:0-65535(2字节表示)
0-1023:系统保留端口,不能被程序使用
一个端口号只能被一个应用程序使用
协议
网络协议分层
层级 | 协议示例 |
---|---|
应用层 | HTTP, FTP, DNS, SMTP, Telnet |
传输层 | TCP, UDP |
网络层 | IP, ICMP, ARP |
物理链路层 | MAC |
UDP协议
用户数据报协议(User Datagram Protocol)
面向无连接通信协议
特点:速度快,有大小限制(一次最多发送64KB数据),数据不安全,易丢失数据
TCP协议
传输控制协议(Transmission Control Protocol)
面向连接的通信协议
特点:速度慢,没有大小限制,数据安全
InetAddress类
InetAddress
类表示互联网协议(IP)地址,封装IP地址。
常用方法
static InetAddress getByName(String host)
:确定主机名称的IP地址String getHostName()
:获取此IP地址的主机名String getHostAddress()
:返回文本显示中的IP地址字符串
import java.net.InetAddress;
import java.net.UnknownHostException;
public class NetDemo {
public static void main(String[] args) throws UnknownHostException {
// 获取InetAddress对象
InetAddress address = InetAddress.getByName("10.102.160.242");
System.out.println(address);
System.out.println("主机名: " + address.getHostName()); // DESKTOP-PB9U58V
System.out.println("IP地址: " + address.getHostAddress()); // 10.102.160.242
}
}
注意:
InetAddress.getByName()
方法根据提供的主机名进行网络查询或DNS解析如果提供的是IP地址字符串,则不会进行任何网络查询或DNS解析
UDP协议
UDP单播发送示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class SendMessageDemo {
public static void main(String[] args) throws IOException {
// 创建DatagramSocket对象
DatagramSocket ds = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入发送的数据:");
String str = sc.nextLine();
if ("886".equals(str)) {
break;
}
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;
// 打包数据
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// 发送数据
ds.send(dp);
}
// 释放资源
ds.close();
}
}
UDP单播接收示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ReceiveMessageDemo {
public static void main(String[] args) throws IOException {
// 创建DatagramSocket对象
DatagramSocket ds = new DatagramSocket(10086);
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
while (true) {
// 接收数据(阻塞方法)
ds.receive(dp);
// 解析数据
byte[] data = dp.getData();
int length = dp.getLength();
String hostName = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为" + hostName + "的" + name + "说:" +
new String(data, 0, length));
}
}
}
注意事项:
接收端需要绑定端口,且端口必须与发送端指定的端口一致
receive()
方法是阻塞的,程序会等待发送端发送消息
单播、组播和广播
组播地址
组播地址范围:224.0.0.0 - 239.255.255.255
其中224.0.0.0 - 224.0.0.255为预留的组播地址
广播地址
广播地址:255.255.255.255
组播发送示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class SendMessageDemo1 {
public static void main(String[] args) throws IOException {
// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket();
// 创建DatagramPacket对象
String data = "hello,udp";
byte[] bytes = data.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// 发送数据
ms.send(dp);
// 释放资源
ms.close();
}
}
组播接收示例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class ReceiveMessageDemo1 {
public static void main(String[] args) throws IOException {
// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10086);
// 将当前本机添加到224.0.0.1组中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
// 创建DatagramPacket对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
// 接收数据
ms.receive(dp);
// 解析数据
byte[] data = dp.getData();
int length = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
int port = dp.getPort();
System.out.println("数据是: " + new String(data, 0, length));
System.out.println("发送端的ip是: " + ip);
System.out.println("发送端的端口是: " + port);
System.out.println("发送端的名称是: " + name);
// 释放资源
ms.close();
}
}
TCP协议
TCP通信程序
通信的两端各建立一个Socket对象
通信之前要保证连接已经建立
通过Socket对象产生IO流来进行网络通信
TCP服务器示例
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// 先启动服务端
// 1.创建ServerSocket对象
ServerSocket ss = new ServerSocket(10086);
// 2.监听客户端连接(阻塞方法)
Socket accept = ss.accept(); // 返回客户的连接对象
// 3.从连接通道中获取输入流读取数据
InputStream in = accept.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b);
}
// 4.释放资源
in.close();
ss.close();
}
}
TCP 三次握手与四次挥手
三次握手(建立连接)
TCP 使用三次握手(3-way Handshake)机制来建立一条可靠的连接。这个过程确保客户端和服务器都准备好进行数据传输,并同步序列号。
详细过程:
步骤详解:
第一次握手(SYN):
客户端发送一个 TCP 数据包,其中:
SYN
标志位设置为 1(表示请求建立连接)随机生成一个序列号
Seq = J
客户端进入
SYN-SENT
状态目的:客户端告诉服务器"我想建立连接,我的初始序列号是 J"
第二次握手(SYN+ACK):
服务器收到请求后,发送回应数据包,其中:
SYN
和ACK
标志位都设置为 1随机生成自己的序列号
Seq = K
确认号
Ack = J + 1
(表示期望收到从 J+1 开始的数据)
服务器进入
SYN-RCVD
状态目的:服务器说"我同意连接,我的初始序列号是 K,已收到你的请求"
第三次握手(ACK):
客户端收到回应后,发送确认数据包,其中:
ACK
标志位设置为 1序列号
Seq = J + 1
确认号
Ack = K + 1
客户端进入
ESTABLISHED
状态服务器收到后也进入
ESTABLISHED
状态目的:客户端确认"收到你的同意,我们现在可以开始通信了"
为什么需要三次握手?
防止已失效的连接请求突然传到服务器:网络延迟可能导致旧的连接请求晚到达,二次握手无法区分新旧请求
同步双方序列号:确保双方都知道对方的初始序列号,为可靠传输做准备
双向确认:确保客户端和服务器都有发送和接收能力
四次挥手(断开连接)
TCP 使用四次挥手(4-way Handshake)机制来终止连接。这个过程允许双方优雅地关闭连接,确保所有数据都传输完毕。
详细过程:
步骤详解:
第一次挥手(FIN):
主动关闭方(假设是客户端)发送 FIN 数据包:
FIN
标志位设置为 1序列号
Seq = U
(等于已传送数据的最后一个字节的序号加1)
客户端进入
FIN-WAIT-1
状态目的:客户端告诉服务器"我的数据发完了,准备关闭连接"
第二次挥手(ACK):
服务器收到 FIN 后,发送 ACK 回应:
ACK
标志位设置为 1确认号
Ack = U + 1
序列号
Seq = V
服务器进入
CLOSE-WAIT
状态客户端收到后进入
FIN-WAIT-2
状态目的:服务器说"收到你的关闭请求,但我可能还有数据要发给你"
第三次挥手(FIN):
服务器完成数据发送后,发送 FIN 数据包:
FIN
标志位设置为 1序列号
Seq = W
(可能等于 V 或 V+已发送数据量)确认号
Ack = U + 1
服务器进入
LAST-ACK
状态目的:服务器说"我的数据也发完了,准备关闭连接"
第四次挥手(ACK):
客户端收到 FIN 后,发送 ACK 回应:
ACK
标志位设置为 1序列号
Seq = U + 1
确认号
Ack = W + 1
客户端进入
TIME-WAIT
状态,等待 2MSL(最大报文段生存时间)后关闭服务器收到 ACK 后立即关闭连接
目的:客户端确认"收到你的关闭请求,现在我们都关闭吧"
为什么需要四次挥手?
半关闭状态:TCP 连接是全双工的,允许一方在发送完数据后关闭发送通道,同时保持接收通道开放
确保数据完整传输:给予被动方时间处理完剩余数据后再完全关闭
可靠终止:确保双方都知道连接已终止,避免悬空连接
为什么需要 TIME-WAIT 状态?
防止旧连接数据包干扰:等待 2MSL 确保所有本连接的数据包都在网络中消失
保证可靠终止:如果最后一次 ACK 丢失,服务器会重发 FIN,客户端可以再次响应
实际编程中的注意事项:
作为服务器,应该正确处理连接关闭,调用
close()
方法客户端应该优雅关闭连接,而不是强制终止
网络编程中需要注意处理各种异常关闭情况
对于频繁建立关闭的连接,需要注意端口重用问题(SO_REUSEADDR)
补充说明
DatagramSocket细节
绑定端口:通过指定端口往外发送数据
空参构造:从所有可用端口中随机获取一个进行使用
带参构造:指定端口进行绑定使用
DatagramPacket参数
byte buf[]
:字符数组int length
:字符数组的长度InetAddress address
:目标地址int port
:目标端口
注意事项
TCP通信需要先启动服务端,再启动客户端
UDP接收端需要绑定端口,且端口必须与发送端指定的端口一致
receive()
和accept()
方法是阻塞的,程序会等待数据或连接使用完网络资源后需要及时关闭,释放资源