什么是网络编程?
定义与目的
网络编程的主要目的是实现不同计算机之间的数据交互和通信。通过编写代码,开发者可以创建网络连接,发送和接收数据包,实现网络通信的各种功能。这些功能包括但不限于网页浏览、邮件传输、文件传输、远程登录等。
基本原理
网络编程的基本原理是利用计算机网络的通信协议,将数据分成小的数据包进行传输。这些数据包通过网络中的节点和路由器进行传输和中转,最终到达目标计算机。在传输过程中,数据包会按照网络协议的规定进行封装和解封装,以确保数据的完整性和可靠性。
基本的通信架构
在Java中,网络通信的基本通信架构主要涉及到两种形式:CS架构(Client/Server,客户端/服务器)和BS架构(Browser/Server,浏览器/服务器)。这两种架构为Java应用程序中的网络通信提供了不同的实现方式和应用场景。
1. CS架构(Client/Server)
CS架构是一种典型的软件系统架构,它通过将任务合理分配到客户端和服务器端,实现了软件的协同工作。在这种架构中,客户端负责与用户进行交互,并向服务器发送请求;服务器则负责处理这些请求,并返回相应的结果给客户端。
特点:
- 客户端开发:在CS架构中,客户端通常需要由程序员进行开发,并需要用户在自己的计算机上安装。
- 服务器开发:服务器端同样需要程序员进行开发,它负责处理来自客户端的请求,并提供相应的服务。
- 网络通信:客户端和服务器之间通过网络通信协议(如TCP/IP)进行数据传输和通信。
应用场景:
- 实时性要求较高或需要复杂交互的应用程序,如在线游戏、即时通讯软件等。
- 需要保护数据安全和隐私的应用场景,因为客户端和服务器之间的通信可以加密。
2. BS架构(Browser/Server)
BS架构是一种基于Web的架构模式,它将传统的客户端软件替换为Web浏览器,用户只需通过浏览器即可访问服务器上的应用程序。
特点:
- 客户端无需开发:在BS架构中,客户端通常是标准的Web浏览器,用户无需安装额外的软件即可访问服务器上的应用程序。
- 服务器开发:服务器端需要程序员进行开发,它负责处理来自浏览器的请求,并返回相应的Web页面或数据。
- 网络通信:浏览器和服务器之间通过HTTP等协议进行数据传输和通信。
应用场景:
- Web应用程序,如电子商务网站、企业内部管理系统等。
- 需要跨平台访问的应用程序,因为浏览器是跨平台的。
网络通信的关键三要素
网络通信的关键三要素主要包括IP地址、端口号以及传输协议。这三个要素共同构成了网络通信的基础框架,确保了数据在网络中的准确传输和接收。
1. IP地址
- 定义与功能:IP地址是网络中设备的唯一标识,用于在网络中定位和识别各个设备。它类似于现实生活中的地址或电话号码,使得数据能够准确地发送到目标设备。
- 分类:IP地址主要分为IPv4和IPv6两种类型。
IPv4地址由32位二进制数组成,通常采用点分十进制表示法(如192.168.1.1);
IPv6地址则更加庞大,由128位二进制数组成,采用冒分十六进制表示法(如 2001:0db8:85a3:0000:0000:8a2e:0370:7334)。 - 特殊地址:包括回送地址(如127.0.0.1,代表本机地址)和广播地址(如X.X.X.255,用于向同一子网内的所有设备发送数据)。
- 应用:通过IP地址,网络设备可以相互识别并进行数据交换。例如,在Web浏览中,用户的计算机通过IP地址与服务器建立连接,从而访问网页内容。
- 补充:
InetAddress
InetAddress
类在 Java 中是用于表示 IP 地址的,无论是 IPv4 还是 IPv6 地址。这个类没有公共的构造方法,因为它包含的信息被认为是不可变的(immutable),即一旦创建了一个 InetAddress
实例,就不能更改其代表的 IP 地址。InetAddress
类提供了多种静态方法来获取和解析 IP 地址信息。
示例
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressExample {
public static void main(String[] args) {
try {
// 通过主机名获取 InetAddress 实例
InetAddress addressByName = InetAddress.getByName("www.example.com");
System.out.println("Host Name: " + addressByName.getHostName());
System.out.println("Host Address: " + addressByName.getHostAddress());
// 通过 IP 地址字符串获取 InetAddress 实例
InetAddress addressByString = InetAddress.getByName("192.168.1.1");
System.out.println("Host Address from String: " + addressByString.getHostAddress());
// 获取本地主机的 InetAddress 实例
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("Local Host Name: " + localHost.getHostName());
System.out.println("Local Host Address: " + localHost.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
2. 端口号
- 定义与功能:端口号用于标识运行在计算机设备上的程序或进程。每个程序或进程在运行时都会占用一个或多个端口号,以便网络通信时能够准确找到对应的应用程序。
- 分类:端口号被规定为一个16位的二进制数,范围从0~65535。
0到1023为周知端口,被预先定义的知名应用占用(如HTTP占用80端口,FTP占用21端口);
1024到49151为注册端口,分配给用户进程或某些特定应用程序;
49152到65535为动态端口,一般不固定分配给某种进程,而是动态分配。
注意:我们自己开发的程序一般使用注册端口,且在一个设备中,不能出现两个程序的端口号一样,否则出错。 - 应用:在网络通信中,客户端和服务器通过指定的端口号进行连接和数据交换。例如,在Web服务中,服务器通常会在80端口上监听HTTP请求;而在FTP服务中,服务器则会在21端口上监听FTP请求。
3. 传输协议
- 定义与功能:传输协议是网络通信中用于规定数据传输格式、顺序、错误检测和纠正等规则的集合。它确保了数据在网络中的可靠传输和接收。
- 主要类型:常见的传输协议包括TCP(传输控制协议)和UDP(用户数据报协议)。TCP是一种面向连接的协议,提供可靠的数据传输服务;而UDP则是一种无连接的协议,提供快速但可能不可靠的数据传输服务。
- 应用:不同的应用场景需要选择不同的传输协议。例如,在需要确保数据完整性和可靠性的场景中(如文件传输、金融交易等),通常会选择TCP协议;而在对实时性要求较高且可以容忍一定数据丢失的场景中(如在线视频、语音通话等),则可能会选择UDP协议。
TCP协议
TCP通信过程详解
在Java中,TCP(Transmission Control Protocol,传输控制协议)通信的过程可以被生动地描述为一场精心策划的“对话”,其中涉及到了两个主要角色:客户端(Client)和服务器端(Server)。这个过程可以分为几个关键步骤,每个步骤都承载着特定的信息和任务,确保了数据能够在两者之间可靠地传输。
1. 握手建立连接
第一步:打招呼(客户端发起连接请求)
想象一下,客户端就像是一个初次来到陌生城市的人,它想要与服务器(这个城市中的某个特定建筑,比如一家图书馆)建立联系。于是,客户端向服务器发送了一个“你好,我想和你聊聊”的信号,这个信号就是TCP连接请求。这个过程被形象地称为“第一次握手”。
第二步:确认身份并邀请(服务器回复)
服务器收到客户端的请求后,它会确认自己的身份(确保自己是客户端想要连接的那个服务器),并发送一个“你好,我收到了你的请求,请继续”的回复。这个回复标志着服务器已经准备好与客户端进行通信,这个过程被称为“第二次握手”。
第三步:确认接收(客户端再次确认)
客户端收到服务器的回复后,它会再次发送一个“好的,我已经准备好开始对话了”的确认信息给服务器。这个确认信息确保了双方都已经为数据传输做好了准备,也标志着TCP连接的正式建立。这个过程就是“第三次握手”。
经过这三次握手,客户端和服务器之间就建立起了一条可靠的通信链路,就像两个人已经面对面坐好,准备开始一场深入的对话。
2. 数据传输
一旦连接建立,客户端和服务器就可以开始传输数据了。这就像两个人开始交谈,他们轮流说话,每个人都在等待对方说完后再继续。在TCP通信中,数据被分割成多个小的数据包进行传输,每个数据包都包含了序号,以确保接收方能够按照正确的顺序重新组合这些数据。
同时,TCP还提供了错误检测和重传机制。如果某个数据包在传输过程中丢失或损坏,接收方会发送一个错误消息给发送方,要求重新发送该数据包。这种机制确保了数据的完整性和可靠性。
3. 挥手断开连接
当数据传输完成后,客户端和服务器需要断开连接以释放资源。这个过程被形象地称为“四次挥手”。
第一次挥手:提出分手(客户端发送断开请求)
客户端会向服务器发送一个“我已经说完了,想结束对话”的请求。这就像一个人在交谈结束时告诉对方“我要走了”。
第二次挥手:同意分手但还有话说(服务器回复并等待)
服务器收到客户端的请求后,会回复一个“好的,我收到了你的请求,但请等一下,我还有一点话要说”。这个过程表示服务器已经同意断开连接,但可能还需要发送一些额外的数据给客户端。
第三次挥手:确认收到并说再见(服务器发送断开请求)
当服务器完成所有需要发送的数据后,它会向客户端发送一个“我也说完了,现在可以正式结束了”的请求。这个过程就像服务器在告诉客户端“我已经没有话要说了,我们可以正式分手了”。
第四次挥手:正式分手(客户端确认)
客户端收到服务器的断开请求后,会回复一个“好的,我也准备好了,正式分手吧”的确认信息。这个过程标志着TCP连接的正式断开。就像两个人在确认彼此都已经准备好离开后,正式结束了这场对话。
TCP客户端开发
Java中的Socket
类来开发TCP客户端程序。
- Socket类:
Socket
类是Java中用于表示TCP客户端和服务器端之间的连接的一个类。它封装了TCP连接的细节,提供了发送和接收数据的接口。- 构造器:
public Socket(String host, int port)
:这个构造器用于创建一个新的套接字,连接到指定的主机(由IP地址或主机名指定)和端口号。一旦连接成功,就建立了一个到服务器的TCP连接。- 方法:
public OutputStream getOutputStream()
:这个方法用于获取一个输出流,客户端可以通过这个输出流向服务器发送数据。public InputStream getInputStream()
,这个方法用于获取一个输入流,客户端可以通过这个输入流从服务器接收数据。
代码演示
import java.io.*;
import java.net.Socket;
public class TcpClient {
public static void main(String[] args) throws Exception {
// 连接到服务器
Socket socket = new Socket("localhost", 8888); // 假设服务器运行在localhost的8888端口
// 获取输出流,用于发送数据到服务器
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream, true);
printWriter.println("Hello, Server!"); // 发送消息到服务器
// 获取输入流,用于接收服务器返回的数据
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = bufferedReader.readLine(); // 读取服务器响应
// 输出服务器响应
System.out.println("Server Response: " + response);
// 关闭资源
bufferedReader.close();
printWriter.close();
socket.close();
}
}
TCP服务端开发
ServerSocket
类用于服务器端来监听来自客户端的连接请求
ServerSocket 类的构造器和方法
构造器
public ServerSocket(int port)
:此构造器用于创建服务器套接字,并将其绑定到指定的端口号上。服务器套接字将等待来自客户端的连接请求。方法
public Socket accept()
:此方法用于侦听并接受到此套接字的连接。此方法在连接到达之前一直处于阻塞状态。当连接到达时,它返回一个新的Socket
对象,该对象用于与连接的客户端进行通信。
代码演示
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听的端口号
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server is running on port " + port);
// 等待客户端连接
Socket socket = serverSocket.accept(); // 此处会阻塞,直到有客户端连接
System.out.println("Client connected!");
// 创建输入流和输出流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
// 读取客户端发送的消息并回显
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("Received from client: " + line);
printWriter.println("Echo: " + line); // 将接收到的消息回显给客户端
}
// 注意:在实际应用中,不应该依赖于客户端关闭连接来退出循环。
// 这里只是为了简化示例而这样做。
} catch (Exception e) {
e.printStackTrace();
}
// 注意:在try-with-resources语句中,serverSocket会在离开try块时自动关闭。
// 但是,对于socket,由于它是在循环外部创建的,并且我们需要等待客户端发送数据,
// 所以我们不会在这里关闭它(在这个简单的示例中)。
// 在实际应用中,你可能需要在另一个循环中处理多个客户端连接,并在适当的时机关闭每个socket。
}
}
UDP协议
DatagramSocket 类
DatagramSocket
类用于表示一个UDP套接字,它可以是客户端套接字,也可以是服务器端套接字。客户端套接字用于发送和接收数据包,而服务器端套接字则绑定到一个特定的端口上,以便接收来自客户端的数据包。
- 构造方法:
DatagramSocket()
:创建一个客户端的DatagramSocket
,系统会为其分配一个随机的端口号。DatagramSocket(int port)
:创建一个服务器端的DatagramSocket
,并绑定到指定的端口号上。
- 主要方法:
void send(DatagramPacket p)
:发送一个数据包。void receive(DatagramPacket p)
:接收一个数据包。此方法会阻塞,直到接收到数据包为止。
DatagramPacket 类
DatagramPacket
类用于表示一个数据包,它包含了要发送或接收的数据,以及发送方或接收方的地址和端口号。
- 构造方法:
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
:创建一个用于发送的数据包,其中buf
是包含要发送数据的字节数组,length
是数据的长度,address
是服务端的IP地址,port
是服务端程序的端口号。DatagramPacket(byte[] buf, int length)
:创建一个用于接收的数据包,其中buf
是接收数据的缓冲区,length
是缓冲区的长度。
- 其他方法:
InetAddress getAddress()
:获取数据包的发送方或接收方的地址。int getPort()
:获取数据包的发送方或接收方的端口号。int getLength()
:获取实际接收到的字节数(仅对接收数据包有效)。
代码示例:
客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UDPClient {
public static void main(String[] args) throws Exception {
//1.创建客户端对象(扔韭菜出去的人)
DatagramSocket socket = new DatagramSocket() ;
String message = "我是快乐的客户端";
//调用了String的getBytes()方法,将字符串转换为一系列的字节,并将这些字节存储在字节数组中
byte[] buffer = message.getBytes();
InetAddress address = InetAddress.getByName("localhost");
int port = 8888;
//2.创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
DatagramPacket packet = new DatagramPacket(buffer,
buffer.length, address, port);
//3.开始正式发送这个数据包的数据出去了。 (扔出韭菜)
socket.send(packet);
System.out.println("客户端数据发送完毕~~~");
// 接收响应
buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String response = new String(packet.getData(), 0, packet.getLength());
System.out.println("Response from server: " + response);
}
}
服务端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UDPServer {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端正式启动-----");
//1.创建一个服务端对象(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8888);
//2.创建一个数据包对象用于接收数据的。(韭菜盘子)
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
System.out.println("Server is listening on port 8888...");
//3.开始正式使用数据包来接收客户端发来的数据。(开始接收韭菜)
socket.receive(packet);
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + received);
InetAddress address = packet.getAddress();
int port = packet.getPort();
String capitalized = received.toUpperCase();
// 发送响应
byte[] response = capitalized.getBytes();
DatagramPacket responsePacket = new DatagramPacket(response, response.length, address, port);
socket.send(responsePacket);
}
}
以上代码只是一发一收,怎么实现多发多收呢?
要实现UDP的多发多收,你需要创建一个循环来不断地接收数据包,而不是只接收一次。对于客户端,如果你想要发送多个数据包,你只需在发送第一个数据包后继续发送其他数据包即可。
UDP服务器端(多发多收)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPServer {
public static void main(String[] args) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket(8888);
byte[] buffer = new byte[1024];
System.out.println("Server is listening on port 8888...");
while (true) { // 使用无限循环来持续接收数据包
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 阻塞直到接收到数据包
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + received);
InetAddress address = packet.getAddress();
int port = packet.getPort();
// 处理接收到的数据(例如,转换为大写)
String capitalized = received.toUpperCase();
// 发送响应
byte[] response = capitalized.getBytes();
DatagramPacket responsePacket = new DatagramPacket(response, response.length, address, port);
socket.send(responsePacket);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
socket.close();
}
}
}
}
UDP客户端(多发)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
public static void main(String[] args) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
String[] messages = {"Hello, UDP Server!", "This is another message.", "Final message."};
InetAddress address = InetAddress.getByName("localhost");
int port = 8888;
for (String message : messages) {
byte[] buffer = message.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);
socket.send(packet);
// 注意:在这个简单的客户端示例中,我们并没有等待服务器的响应。
// 如果你需要等待并处理响应,你需要编写额外的代码来接收响应数据包。
System.out.println("Sent: " + message);
// 这里可以添加一些延迟来模拟发送间隔,但在这个示例中我们省略了。
}
// 注意:在实际应用中,客户端通常也会监听来自服务器的响应。
// 但在这个简单的示例中,我们只是为了演示如何发送多个数据包而省略了接收响应的部分。
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
socket.close();
}
}
}
}
三要素之间的联系
1. IP地址与端口号的联系
- 唯一标识与具体定位:IP地址作为网络设备的唯一标识,确保了数据能够准确地发送到目标设备。而端口号则进一步在目标设备上定位到具体的程序或进程,使得数据能够被正确的程序所接收和处理。
- 协同工作:在网络通信中,IP地址和端口号通常是成对出现的。客户端在发起连接请求时,需要指定目标设备的IP地址和端口号,以便服务器能够准确地识别并响应请求。
2. IP地址与传输协议的联系
- 数据传输的基础:IP地址提供了数据传输的起点和终点,而传输协议则规定了数据传输的方式和规则。无论是TCP还是UDP协议,都需要依赖于IP地址来实现数据的发送和接收。
- 协议类型与IP地址的关系:不同的传输协议具有不同的特性和应用场景。例如,TCP协议提供可靠的数据传输服务,适用于需要确保数据完整性和可靠性的场景;而UDP协议则提供快速但可能不可靠的数据传输服务,适用于对实时性要求较高且可以容忍一定数据丢失的场景。在选择使用哪种传输协议时,需要考虑到IP地址所在的网络环境、数据传输的需求以及安全性等因素。
3. 端口号与传输协议的联系
- 数据传输的入口与出口:端口号作为程序或进程的标识,在网络通信中充当了数据传输的入口和出口。不同的程序或进程通常会使用不同的端口号来区分彼此,以便在网络中实现数据的独立传输和处理。