Java网络编程实战指南:TCP/UDP通信、HTTP请求与正则爬虫开发
网络基础与诊断工具
在进行网络编程之前,掌握基本的网络诊断工具是必不可少的。可以使用 ipconfig (Windows) 或 ifconfig (Linux/macOS) 查看本机网络配置,使用 ping 测试网络连通性。为了监控本机的网络连接状态和端口占用情况,netstat -an 或系统自带的资源监视器是非常实用的工具。在调试阶段,Telnet 或 NetAssist 等网络调试助手可以作为简易的客户端或服务器,帮助验证端口监听和数据收发是否正常。
TCP客户端编程实战
TCP协议提供面向连接的可靠数据传输。在Java中,通过 Socket 类即可实现TCP客户端。编写客户端时,核心在于正确处理输入输出流,并解决常见的缓冲区刷新与字符编码问题。
以下是一个重构后的TCP客户端示例,实现了与控制台交互并向服务器收发数据的功能,同时使用 InputStreamReader 和 OutputStreamWriter 解决了中文乱码问题:
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class TcpClientApp {
public static void main(String[] args) {
String serverHost = "192.168.1.100";
int serverPort = 8888;
try (Socket clientConnection = new Socket(serverHost, serverPort);
BufferedReader reader = new BufferedReader(new InputStreamReader(clientConnection.getInputStream(), StandardCharsets.UTF_8));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientConnection.getOutputStream(), StandardCharsets.UTF_8), true);
Scanner consoleScanner = new Scanner(System.in)) {
System.out.println("已连接到服务器,请输入消息(输入'exit'退出):");
while (true) {
String userInput = consoleScanner.nextLine();
writer.println(userInput);
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
String serverResponse = reader.readLine();
if (serverResponse != null) {
System.out.println("服务器响应: " + serverResponse);
} else {
System.out.println("服务器已断开连接。");
break;
}
}
} catch (IOException e) {
System.err.println("网络通信异常: " + e.getMessage());
}
}
}
注意事项:
- 如果服务器端使用
readLine()读取数据,客户端发送的消息必须包含换行符(如使用println)。 - 若遇到
Connection Refused,请检查服务器是否启动、IP是否正确以及防火墙设置。 - 严格交替的"一发一收"模式容易导致死锁,实际应用中通常需要引入多线程分别处理读写操作。
TCP服务器与多线程模型
服务器端使用 ServerSocket 监听特定端口。当接收到客户端连接时,会返回一个新的 Socket 用于与该客户端通信。为了支持多客户端并发访问,必须引入多线程机制。
下面展示了一个基于线程池的多线程菜单服务器实现,相比传统的为每个连接创建新线程,使用线程池能有效控制资源消耗并提升响应速度:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiThreadMenuServer {
private static final int PORT = 8888;
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
try (ServerSocket serverListener = new ServerSocket(PORT)) {
System.out.println("菜单服务器已启动,监听端口: " + PORT);
while (true) {
Socket clientSocket = serverListener.accept();
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器启动失败: " + e.getMessage());
} finally {
threadPool.shutdown();
}
}
}
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
System.out.println("新客户端连接: " + clientInfo);
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true)) {
out.println("欢迎使用菜单系统:\n1. 获取服务器时间\n2. 获取您的IP和端口\n3. 退出");
String request;
while ((request = in.readLine()) != null) {
switch (request.trim()) {
case "1":
out.println("当前服务器时间: " + LocalDateTime.now());
break;
case "2":
out.println("您的连接信息: " + clientInfo);
break;
case "3":
out.println("再见!");
return;
default:
out.println("无效选项,请重新输入。");
}
out.println("请继续输入选项(1-3):");
}
} catch (IOException e) {
System.err.println("处理客户端 " + clientInfo + " 时发生错误: " + e.getMessage());
} finally {
try { clientSocket.close(); } catch (IOException ignored) {}
System.out.println("客户端断开: " + clientInfo);
}
}
}
如果需要在客户端和服务器之间传输对象或字节数组,可以将 InputStream 包装为 ObjectInputStream,或直接操作字节流进行文件下载与上传。
HTTP协议交互与网页抓取
HTTP协议是基于TCP的应用层协议。通过原生Socket发送符合HTTP规范的请求头,即可获取网页内容。一个标准的GET请求头如下:
GET /index.html HTTP/1.1
Host: www.example.com
Connection: close
注意请求头末尾必须包含一个空行。在Java中,除了使用底层Socket,更推荐使用高级API:
- Java 11 HttpClient:提供了现代化的异步/同步请求能力,原生支持HTTP/2。
- Jsoup:强大的HTML解析库,不仅能发起请求,还能通过CSS选择器轻松提取DOM节点,是编写网络爬虫的首选工具。
使用正则表达式提取网页链接
在不使用DOM解析库的情况下,可以通过正则表达式从HTML源码中提取超链接和图片地址。以下代码展示了如何匹配 <a href="..."> 和 <img src="..."> 标签中的URL:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
public class HtmlLinkExtractor {
private static final String URL_REGEX = "(?:href|src)\\s*=\\s*(?:\"([^\"]+)\"|'([^']+)'|([^\\s>]+))";
private static final Pattern LINK_PATTERN = Pattern.compile(URL_REGEX, Pattern.CASE_INSENSITIVE);
public static List<String> extractUrls(String htmlContent) {
List<String> urlList = new ArrayList<>();
Matcher matcher = LINK_PATTERN.matcher(htmlContent);
while (matcher.find()) {
String url = matcher.group(1) != null ? matcher.group(1) :
(matcher.group(2) != null ? matcher.group(2) : matcher.group(3));
if (url != null && !url.startsWith("javascript:")) {
urlList.add(url);
}
}
return urlList;
}
public static void main(String[] args) {
String sampleHtml = "<div><a href=\"https://example.com/page1\">Link1</a>" +
"<img src='images/pic.jpg' border='0'/>" +
"<a href=https://example.com/page2>Link2</a></div>";
List<String> extractedLinks = extractUrls(sampleHtml);
for (String link : extractedLinks) {
System.out.println("发现链接: " + link);
}
}
}
提取出相对路径后,通常需要结合当前网页的Base URL,将其转换为绝对路径,以便后续进行资源下载或深度爬取。
UDP数据报通信
与TCP不同,UDP是无连接的协议,通过 DatagramSocket 和 DatagramPacket 进行数据收发。UDP服务器不需要 accept() 等待连接,而是直接通过 receive() 方法接收数据报,提取发送方的IP和端口后,再将响应数据打包成 DatagramPacket 发送回去。UDP适用于对实时性要求高、允许少量丢包的场景,如视频流传输和实时在线游戏。