引言
在当今的互联网时代,网络编程是软件开发中不可或缺的重要部分。Java 提供了丰富的类库来支持网络编程,使得开发者能够轻松地实现各种网络通信功能。本章将详细介绍 Java 网络编程的核心知识,包括网络基础概念、套接字通信、数据报通信以及 URL 编程等内容,并通过完整的代码示例帮助大家掌握实际应用技能。
18.1 网络概述
网络编程的本质是实现不同设备之间的数据传输。在学习 Java 网络编程之前,我们需要先了解一些计算机网络的基本概念。
18.1.1 网络分层与协议
为了实现不同计算机之间的通信,网络通信采用分层模型设计,每一层负责特定的功能,并通过协议规范通信方式。
最著名的网络模型有两种:
- OSI 七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
- TCP/IP 四层模型:网络接口层、网络层、传输层、应用层(或分为五层,将网络接口层细分为物理层和数据链路层)
常用协议:
- 网络层:IP 协议(Internet Protocol)
- 传输层:TCP 协议(Transmission Control Protocol)和 UDP 协议(User Datagram Protocol)
- 应用层:HTTP、FTP、SMTP 等
18.1.2 客户 / 服务器结构
在网络通信中,最常见的模式是客户 / 服务器(C/S)结构:
- 服务器(Server):提供服务的计算机或程序,被动等待客户端的连接请求
- 客户端(Client):请求服务的计算机或程序,主动向服务器发起连接请求
B/S 结构(浏览器 / 服务器)是一种特殊的 C/S 结构,客户端是浏览器,通过 HTTP 协议与服务器通信。
18.1.3 IP 地址和域名
IP 地址:网络中每个设备的唯一标识,用于设备之间的定位。
- IPv4:32 位,格式如
192.168.1.1
- IPv6:128 位,格式如
2001:0db8:85a3:0000:0000:8a2e:0370:7334
(为解决 IPv4 地址耗尽问题) - 本地回环地址:
127.0.0.1
,通常映射到域名localhost
- IPv4:32 位,格式如
域名:为了方便记忆,用字符串标识网络中的设备,如
www.csdn.net
。DNS(域名系统):负责将域名解析为对应的 IP 地址。
18.1.4 端口号与套接字
端口号:标识设备上运行的不同网络程序,范围是 0-65535。
- 0-1023:知名端口,被系统保留(如 HTTP 的 80,FTP 的 21)
- 1024-49151:注册端口
- 49152-65535:动态或私有端口,可用于普通应用程序
套接字(Socket):网络通信的端点,由 IP 地址和端口号组成,用于标识通信的双方。
- 格式:
IP地址:端口号
(如192.168.1.100:8080
) - Java 中通过
Socket
类实现套接字功能
- 格式:
18.2 Java 套接字通信
Java 中基于 TCP 协议的网络通信主要通过套接字(Socket)实现,核心类是 ServerSocket
(服务器端)和 Socket
(客户端)。
18.2.1 套接字 API
ServerSocket:服务器端套接字,用于监听客户端的连接请求。
- 构造方法:
ServerSocket(int port)
- 绑定到指定端口 - 核心方法:
Socket accept()
- 阻塞等待客户端连接,返回与客户端通信的 Socket 对象
- 构造方法:
Socket:客户端套接字,也用于服务器端与客户端通信。
- 构造方法:
Socket(String host, int port)
- 连接到指定主机的指定端口 - 核心方法:
InputStream getInputStream()
- 获取输入流,用于读取数据OutputStream getOutputStream()
- 获取输出流,用于发送数据void close()
- 关闭套接字
- 构造方法:
@startuml
class ServerSocket {
+ ServerSocket(int port) throws IOException
+ Socket accept() throws IOException
+ void close() throws IOException
+ int getLocalPort()
}
class Socket {
+ Socket(String host, int port) throws IOException
+ InputStream getInputStream() throws IOException
+ OutputStream getOutputStream() throws IOException
+ void close() throws IOException
+ InetAddress getInetAddress()
+ int getPort()
+ int getLocalPort()
}
ServerSocket "1" -- "*" Socket : 创建
@enduml
18.2.2 简单的客户和服务器程序
下面实现一个简单的 TCP 通信程序:客户端向服务器发送一条消息,服务器接收后回复一条消息。
服务器端代码(TCPServer.java):
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket clientSocket = null;
try {
// 创建服务器套接字,绑定到8888端口
serverSocket = new ServerSocket(8888);
System.out.println("服务器已启动,等待客户端连接...");
// 阻塞等待客户端连接
clientSocket = serverSocket.accept();
System.out.println("客户端已连接:" + clientSocket.getInetAddress().getHostAddress());
// 获取输入流和输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
// 读取客户端发送的消息
String clientMsg = in.readLine();
System.out.println("收到客户端消息:" + clientMsg);
// 向客户端发送响应
String serverMsg = "Hello Client! 我是服务器";
out.println(serverMsg);
System.out.println("已向客户端发送消息:" + serverMsg);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (clientSocket != null) clientSocket.close();
if (serverSocket != null) serverSocket.close();
System.out.println("服务器已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码(TCPClient.java):
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) {
Socket socket = null;
try {
// 连接到本地服务器的8888端口
socket = new Socket("localhost", 8888);
System.out.println("已连接到服务器");
// 获取输入流和输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
// 向服务器发送消息
String clientMsg = "Hello Server! 我是客户端";
out.println(clientMsg);
System.out.println("已向服务器发送消息:" + clientMsg);
// 读取服务器的响应
String serverMsg = in.readLine();
System.out.println("收到服务器消息:" + serverMsg);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
try {
if (socket != null) socket.close();
System.out.println("客户端已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行说明:
- 先运行
TCPServer
,服务器启动并等待连接 - 再运行
TCPClient
,客户端连接服务器并进行通信 - 程序执行后,服务器和客户端控制台会分别输出通信内容
18.2.3 服务多个客户
上面的服务器程序只能处理一个客户端连接。要实现同时服务多个客户端,需要使用多线程:主线程负责监听连接,每收到一个连接就创建一个新线程处理与该客户端的通信。
多线程服务器代码(MultiThreadTCPServer.java):
import java.io.*;
import java.net.*;
public class MultiThreadTCPServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8888);
System.out.println("多线程服务器已启动,等待客户端连接...");
int clientCount = 0; // 客户端计数器
while (true) { // 循环接受多个客户端连接
Socket clientSocket = serverSocket.accept();
clientCount++;
System.out.println("第" + clientCount + "个客户端已连接:" +
clientSocket.getInetAddress().getHostAddress());
// 创建新线程处理客户端通信
new ClientHandler(clientSocket, clientCount).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 客户端处理线程
static class ClientHandler extends Thread {
private Socket clientSocket;
private int clientId;
public ClientHandler(Socket socket, int id) {
this.clientSocket = socket;
this.clientId = id;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
// 获取输入流和输出流
in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream(), "UTF-8"));
out = new PrintWriter(
new OutputStreamWriter(clientSocket.getOutputStream(), "UTF-8"), true);
String clientMsg;
// 循环读取客户端消息,直到客户端关闭连接
while ((clientMsg = in.readLine()) != null) {
System.out.println("收到第" + clientId + "个客户端消息:" + clientMsg);
// 向客户端发送响应
String serverMsg = "服务器已收到你的消息:" + clientMsg;
out.println(serverMsg);
}
System.out.println("第" + clientId + "个客户端已断开连接");
} catch (IOException e) {
System.out.println("第" + clientId + "个客户端通信异常:" + e.getMessage());
} finally {
// 关闭资源
try {
if (in != null) in.close();
if (out != null) out.close();
if (clientSocket != null) clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端代码:可以使用前面的 TCPClient.java
,也可以稍作修改使其能发送多条消息:
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class MultiTCPClient {
public static void main(String[] args) {
Socket socket = null;
Scanner scanner = null;
try {
socket = new Socket("localhost", 8888);
System.out.println("已连接到服务器,输入消息发送(输入exit退出):");
// 获取输入流和输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "UTF-8"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
scanner = new Scanner(System.in);
String msg;
// 循环输入并发送消息
while (true) {
msg = scanner.nextLine();
out.println(msg);
if ("exit".equals(msg)) {
System.out.println("客户端将退出");
break;
}
// 读取服务器响应
String serverMsg = in.readLine();
System.out.println("服务器回复:" + serverMsg);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (scanner != null) scanner.close();
if (socket != null) socket.close();
System.out.println("客户端已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行说明:
- 运行
MultiThreadTCPServer
- 可以启动多个
MultiTCPClient
实例,每个客户端都能与服务器独立通信 - 在客户端输入消息发送,输入 "exit" 退出
18.3 数据报通信
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,与 TCP 相比,它不保证数据的可靠传输,但具有传输速度快、开销小的特点,适用于对实时性要求高但对可靠性要求不高的场景(如视频聊天、语音通话、游戏等)。
18.3.1 数据报通信概述
UDP 通信特点:
- 无连接:通信前不需要建立连接
- 不可靠:不保证数据一定到达,也不保证顺序
- 面向数据报:数据以数据报(Datagram)为单位传输
- 速度快:协议简单,开销小
UDP 通信流程:
- 发送方将数据打包成数据报,指定接收方的 IP 和端口
- 数据报通过网络发送
- 接收方从网络中接收数据报
18.3.2 DatagramSocket 类和 DatagramPacket 类
Java 中使用以下类实现 UDP 通信:
DatagramSocket:用于发送和接收数据报的套接字
- 构造方法:
DatagramSocket()
- 创建未绑定的套接字DatagramSocket(int port)
- 创建绑定到指定端口的套接字
- 核心方法:
void send(DatagramPacket p)
- 发送数据报void receive(DatagramPacket p)
- 接收数据报(阻塞)void close()
- 关闭套接字
- 构造方法:
DatagramPacket:表示数据报
- 构造方法(接收数据时):
DatagramPacket(byte[] buf, int length)
- 构造方法(发送数据时):
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
- 核心方法:
byte[] getData()
- 获取数据报中的数据int getLength()
- 获取数据长度InetAddress getAddress()
- 获取发送方 / 接收方的 IP 地址int getPort()
- 获取发送方 / 接收方的端口号
- 构造方法(接收数据时):
@startuml
class DatagramSocket {
+ DatagramSocket() throws SocketException
+ DatagramSocket(int port) throws SocketException
+ void send(DatagramPacket p) throws IOException
+ void receive(DatagramPacket p) throws IOException
+ void close()
}
class DatagramPacket {
+ DatagramPacket(byte[] buf, int length)
+ DatagramPacket(byte[] buf, int length, InetAddress address, int port)
+ byte[] getData()
+ int getLength()
+ InetAddress getAddress()
+ int getPort()
+ void setData(byte[] buf)
+ void setLength(int length)
}
DatagramSocket "1" -- "*" DatagramPacket : 发送/接收
@enduml
18.3.3 简单的 UDP 通信例子
下面实现一个简单的 UDP 通信程序:客户端向服务器发送消息,服务器接收后回复。
UDP 服务器代码(UDPServer.java):
import java.net.*;
import java.nio.charset.StandardCharsets;
public class UDPServer {
public static void main(String[] args) {
DatagramSocket socket = null;
byte[] receiveBuf = new byte[1024]; // 接收缓冲区
try {
// 创建UDP套接字,绑定到8888端口
socket = new DatagramSocket(8888);
System.out.println("UDP服务器已启动,等待消息...");
while (true) { // 循环接收消息
// 创建接收数据报
DatagramPacket receivePacket = new DatagramPacket(receiveBuf, receiveBuf.length);
// 接收数据报(阻塞)
socket.receive(receivePacket);
// 解析接收的数据
String clientMsg = new String(
receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
InetAddress clientAddr = receivePacket.getAddress();
int clientPort = receivePacket.getPort();
System.out.println("收到来自 " + clientAddr.getHostAddress() + ":" + clientPort +
" 的消息:" + clientMsg);
// 准备回复消息
String serverMsg = "服务器已收到:" + clientMsg;
byte[] sendData = serverMsg.getBytes(StandardCharsets.UTF_8);
// 创建发送数据报
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length, clientAddr, clientPort);
// 发送回复
socket.send(sendPacket);
System.out.println("已回复消息:" + serverMsg);
// 如果收到"exit",则退出服务器
if ("exit".equals(clientMsg)) {
System.out.println("服务器将关闭");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭套接字
if (socket != null) {
socket.close();
}
}
}
}
UDP 客户端代码(UDPClient.java):
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class UDPClient {
public static void main(String[] args) {
DatagramSocket socket = null;
Scanner scanner = null;
try {
// 创建UDP套接字(不指定端口,系统会分配一个临时端口)
socket = new DatagramSocket();
// 服务器地址和端口
InetAddress serverAddr = InetAddress.getByName("localhost");
int serverPort = 8888;
System.out.println("UDP客户端已启动,输入消息发送(输入exit退出):");
scanner = new Scanner(System.in);
while (true) {
// 读取用户输入
String msg = scanner.nextLine();
// 准备发送数据
byte[] sendData = msg.getBytes(StandardCharsets.UTF_8);
// 创建发送数据报
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length, serverAddr, serverPort);
// 发送数据报
socket.send(sendPacket);
System.out.println("已发送消息:" + msg);
// 如果输入"exit",则退出客户端
if ("exit".equals(msg)) {
System.out.println("客户端将关闭");
break;
}
// 接收服务器回复
byte[] receiveBuf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuf, receiveBuf.length);
socket.receive(receivePacket);
// 解析回复数据
String serverMsg = new String(
receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
System.out.println("收到服务器回复:" + serverMsg);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
if (scanner != null) scanner.close();
if (socket != null) socket.close();
}
}
}
运行说明:
- 先运行
UDPServer
- 再运行
UDPClient
- 在客户端输入消息,服务器会收到并回复
- 输入 "exit" 可退出程序
18.4 URL 类编程
URL(Uniform Resource Locator,统一资源定位符)用于标识互联网上的资源,如网页、图片、文件等。Java 提供了 URL
和 URLConnection
类来方便地访问 URL 指向的资源。
18.4.1 理解 HTTP
HTTP(HyperText Transfer Protocol,超文本传输协议)是一种基于 TCP 的应用层协议,用于在客户端和服务器之间传输超文本数据(如 HTML)。
HTTP 通信流程:
- 客户端(如浏览器)向服务器发送 HTTP 请求
- 服务器处理请求,返回 HTTP 响应
- 客户端解析响应内容并展示
HTTP 请求方法:
- GET:请求获取资源
- POST:向服务器提交数据
- PUT:更新资源
- DELETE:删除资源
- 等
HTTP 响应状态码:
- 200:成功
- 404:资源未找到
- 500:服务器内部错误
- 等
18.4.2 URL 和 URL 类
URL 的格式:协议://主机名:端口/路径?查询参数#片段
例如:https://www.csdn.net:443/article/list/1?type=1#content
Java 中的 java.net.URL
类用于表示 URL,并提供了访问 URL 资源的方法:
- 构造方法:
URL(String spec)
- 根据字符串创建 URL 对象 - 常用方法:
String getProtocol()
- 获取协议String getHost()
- 获取主机名int getPort()
- 获取端口号String getPath()
- 获取路径InputStream openStream()
- 获取 URL 资源的输入流
18.4.3 URLConnection 类
URLConnection
是一个抽象类,用于表示与 URL 所指向资源的连接。它比 URL
类提供了更多的功能,如设置请求头、获取响应头、处理表单提交等。
常用方法:
static URLConnection openConnection()
- 获取 URLConnection 对象void setRequestProperty(String key, String value)
- 设置请求头Map<String, List<String>> getHeaderFields()
- 获取响应头InputStream getInputStream()
- 获取输入流,用于读取资源OutputStream getOutputStream()
- 获取输出流,用于发送数据(如 POST 请求)void connect()
- 建立连接
使用 URL 读取网页内容的示例(URLReader.java):
import java.io.*;
import java.net.*;
public class URLReader {
public static void main(String[] args) {
// 要访问的URL
String urlStr = "https://www.baidu.com";
BufferedReader reader = null;
try {
// 创建URL对象
URL url = new URL(urlStr);
System.out.println("协议:" + url.getProtocol());
System.out.println("主机:" + url.getHost());
System.out.println("端口:" + url.getPort()); // -1表示使用协议默认端口
System.out.println("路径:" + url.getPath());
// 打开URL连接,获取输入流
reader = new BufferedReader(
new InputStreamReader(url.openStream(), "UTF-8"));
// 读取内容并输出
String line;
System.out.println("\n网页内容:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (MalformedURLException e) {
System.out.println("URL格式错误:" + e.getMessage());
} catch (IOException e) {
System.out.println("读取失败:" + e.getMessage());
} finally {
// 关闭资源
try {
if (reader != null) reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用 URLConnection 发送 POST 请求的示例(URLConnectionPost.java):
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class URLConnectionPost {
public static void main(String[] args) {
// 要提交的URL(这里使用一个测试接口)
String urlStr = "https://httpbin.org/post";
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
// 创建URL对象
URL url = new URL(urlStr);
// 打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为POST
connection.setRequestMethod("POST");
// 设置允许输出(发送数据)
connection.setDoOutput(true);
// 设置请求头
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("User-Agent", "Mozilla/5.0");
// 准备要提交的表单数据
Map<String, String> params = new HashMap<>();
params.put("name", "张三");
params.put("age", "25");
params.put("city", "北京");
// 构建请求参数字符串
StringBuilder postData = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (postData.length() > 0) {
postData.append('&');
}
// 对参数进行URL编码
postData.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));
postData.append('=');
postData.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
}
byte[] postDataBytes = postData.toString().getBytes(StandardCharsets.UTF_8);
// 设置请求内容长度
connection.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
// 获取输出流,发送数据
try (DataOutputStream out = new DataOutputStream(connection.getOutputStream())) {
out.write(postDataBytes);
}
// 获取响应状态码
int responseCode = connection.getResponseCode();
System.out.println("响应状态码:" + responseCode);
// 读取响应内容
reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
String line;
System.out.println("响应内容:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (MalformedURLException e) {
System.out.println("URL格式错误:" + e.getMessage());
} catch (IOException e) {
System.out.println("请求失败:" + e.getMessage());
} finally {
// 关闭资源
try {
if (reader != null) reader.close();
} catch (IOException e) {
e.printStackTrace();
}
if (connection != null) {
connection.disconnect();
}
}
}
}
运行说明:
URLReader
会读取指定 URL 的内容并输出到控制台URLConnectionPost
会向测试接口发送 POST 请求,并输出响应结果
注意:访问某些网站可能会被拒绝(如设置了反爬机制),可以尝试修改 User-Agent 等请求头信息模拟浏览器行为。
18.5 小结
本章主要介绍了 Java 网络编程的核心内容,包括:
- 网络基础概念:网络分层模型、C/S 结构、IP 地址、域名、端口号和套接字
- TCP 套接字通信:使用
ServerSocket
和Socket
类实现可靠的面向连接的通信,以及多线程服务器的实现 - UDP 数据报通信:使用
DatagramSocket
和DatagramPacket
类实现无连接的不可靠通信 - URL 编程:使用
URL
和URLConnection
类访问互联网资源,包括发送 GET 和 POST 请求
Java 网络编程是 Java 编程中的重要组成部分,掌握这些知识可以帮助我们开发各种网络应用,如客户端 / 服务器程序、网络爬虫、API 调用等。
编程练习
练习 1:实现文件传输
- 编写一个 TCP 服务器和客户端,实现文件上传功能(客户端将本地文件发送到服务器,服务器保存文件)
练习 2:UDP 广播程序
- 编写一个 UDP 程序,实现局域网内的广播功能(一个客户端发送消息,其他所有客户端都能收到)
练习 3:简易网页爬虫
- 使用
URLConnection
编写一个简单的网页爬虫,爬取指定网页中的所有链接(<a>
标签的 href 属性)
- 使用
练习 4:多线程聊天程序
- 基于多线程 TCP 通信,实现一个简易的聊天程序,支持多个客户端之间的群聊功能
通过这些练习,可以加深对 Java 网络编程的理解和应用能力。在实际开发中,还可以结合线程池、NIO 等技术进一步优化网络程序的性能。