网络编程

网络基础

网络编程就是让两台电脑之间能互相说话。要说话得知道三个事:找谁(IP)、找哪个程序(端口)、怎么传(协议)

IP地址:电脑的门牌号

IPv4:192.168.1.100,32位,大约43亿个(不够用了)

IPv6:2001:0db8:85a3::8a2e:0370:7334,128位,够用到宇宙毁灭

127.0.0.1 / localhost:指自己这台电脑

端口号:电脑上某个程序的”房间号”,范围 0-65535

常见端口:HTTP=80, HTTPS=443, MySQL=3306, Redis=6379, SSH=22

0-1023 是系统保留端口,自己的程序一般用 1024 以上的

TCP vs UDP

对比 TCP UDP
比喻 打电话(确认对方接了才说) 发传单(扔出去就不管了)
连接 需要三次握手建立连接 无连接
可靠性 可靠,丢了会重传 不可靠,丢了就丢了
速度 慢一些
应用场景 网页、文件传输、邮件 视频直播、游戏、DNS

Socket编程(TCP)

Socket 就是网络通信的”插座”,一端插服务器,一端插客户端,数据就能流通了

**服务端用 ServerSocket**,客户端用 Socket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
public void testTcpServer() throws Exception {
// === 服务端:Echo服务器(收到什么就回什么) ===
new Thread(() -> {
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("服务器启动,等待连接...");
Socket client = serverSocket.accept(); // 阻塞等待客户端连接

BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), true);

String line;
while ((line = in.readLine()) != null) {
System.out.println("服务器收到: " + line);
out.println("Echo: " + line); // 原样回复
if ("bye".equalsIgnoreCase(line)) break;
}
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();

Thread.sleep(500); // 等服务器启动

// === 客户端 ===
try (Socket socket = new Socket("127.0.0.1", 8888)) {
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));

out.println("Hello");
System.out.println("客户端收到: " + in.readLine()); // Echo: Hello

out.println("bye");
System.out.println("客户端收到: " + in.readLine()); // Echo: bye
}
}

通信流程

1
2
3
4
5
6
7
服务端                           客户端
ServerSocket(8888)
accept() 等待... Socket("127.0.0.1", 8888) 发起连接
← 三次握手建立连接 →
读取数据 ← 发送数据
发送数据 → 读取数据
close() close()

HTTP协议基础

HTTP是Web世界的通用语言,浏览器和服务器之间就是用HTTP对话的。参考 网络编程

请求方法

方法 用途 有请求体
GET 获取资源(查询)
POST 提交数据(创建)
PUT 更新资源(全量)
DELETE 删除资源 一般无
PATCH 更新资源(局部)

状态码(记住这些就够日常用了)

状态码 含义 记忆
200 OK,成功 一切正常
301 永久重定向 搬家了,以后别来这个地址
302 临时重定向 临时搬走,下次还来这
304 未修改(用缓存) 没变,用上次的
400 请求有误 你说的话我听不懂
401 未认证 你谁啊?先登录
403 禁止访问 知道你是谁,但你没权限
404 找不到 你要的东西不存在
500 服务器内部错误 我的锅,服务器炸了
502 网关错误 代理/网关后面的服务器挂了
503 服务不可用 服务器忙/维护中

请求头/响应头

Content-Type:告诉对方数据格式(application/jsontext/html

Authorization:认证信息(Bearer Token等)

User-Agent:客户端信息(浏览器类型等)

Cookie / Set-Cookie:状态保持

URL与HttpURLConnection

Java自带的HTTP客户端,适合简单场景。实际开发中一般用 常用工具库 里的HttpClient或OkHttp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testHttpGet() throws Exception {
// 发送 HTTP GET 请求
URL url = new URL("https://httpbin.org/get");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000); // 连接超时5秒
conn.setReadTimeout(5000); // 读取超时5秒

int responseCode = conn.getResponseCode();
System.out.println("状态码: " + responseCode);

if (responseCode == 200) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
System.out.println("响应: " + response.toString());
}

conn.disconnect();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testHttpPost() throws Exception {
// 发送 HTTP POST 请求
URL url = new URL("https://httpbin.org/post");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true); // 允许写入请求体
conn.setRequestProperty("Content-Type", "application/json");

// 写入请求体
String jsonBody = "{\"username\":\"test\",\"password\":\"123456\"}";
conn.getOutputStream().write(jsonBody.getBytes("UTF-8"));

int responseCode = conn.getResponseCode();
System.out.println("POST 状态码: " + responseCode);

conn.disconnect();
}

⭐ 安全角度:SSRF(Server-Side Request Forgery)

什么是SSRF:服务端请求伪造——你骗服务器帮你去访问它能访问但你不能访问的内网资源

比喻:你不能进公司办公楼,但你给前台打电话说”帮我去XX办公室拿份文件”,前台有权限进,就帮你拿了——这就是SSRF

攻击原理

1
2
3
4
正常请求:用户 → 服务器 → 外部资源(https://example.com/image.png)
SSRF攻击:用户 → 服务器 → 内网资源(http://192.168.1.1/admin)
→ 本机服务(http://127.0.0.1:6379 访问Redis)
→ 云环境元数据(http://169.254.169.254 拿AWS密钥)

漏洞代码示例

1
2
3
4
5
6
7
8
9
10
@Test
public void testSSRFVulnerable() throws Exception {
// ❌ 有SSRF漏洞的代码:直接用用户输入的URL去请求
String userInputUrl = "http://169.254.169.254/latest/meta-data/"; // AWS元数据
// String userInputUrl = "http://192.168.1.1/admin"; // 内网管理后台

URL url = new URL(userInputUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 服务器替攻击者去访问了内网资源!
}

防御方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testSSRFDefense() throws Exception {
String userInputUrl = "http://192.168.1.1/admin";

// ✅ 防御1:URL白名单
List<String> allowedHosts = Arrays.asList("img.example.com", "cdn.example.com");
URL url = new URL(userInputUrl);
if (!allowedHosts.contains(url.getHost())) {
System.out.println("拒绝访问:不在白名单中");
return;
}

// ✅ 防御2:禁止内网IP
InetAddress address = InetAddress.getByName(url.getHost());
if (address.isSiteLocalAddress() || address.isLoopbackAddress()
|| address.isLinkLocalAddress()) {
System.out.println("拒绝访问:内网IP");
return;
}

// ✅ 防御3:禁止重定向(攻击者可能先跳到外网再跳回内网)
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(false);
}
防御手段 说明
URL白名单 只允许访问指定域名
禁止内网IP 过滤 10.x172.16-31.x192.168.x127.x
禁止重定向 防止通过302跳转绕过限制
限制协议 只允许 http/https,禁止 file://、gopher://
DNS rebinding防御 先解析IP再校验,避免DNS二次解析到内网

常见坑

坑1:忘记设置超时,网络不通时程序会一直卡住

坑2:Socket用完不关,导致端口耗尽。用 try-with-resources

坑3:HTTP请求不读完响应体就关连接,底层TCP可能异常

坑4:端口号冲突——启动服务前检查端口是否被占用(lsof -i :8888

坑5:在服务端用了 accept() 但没开多线程处理,只能同时服务一个客户端

练习题

题1:写一个简单的HTTP服务器(用ServerSocket),对所有请求返回 “Hello World”(提示:需要返回HTTP响应格式 HTTP/1.1 200 OK\r\n\r\nHello World

题2:用HttpURLConnection实现一个简单的网页下载器,把指定URL的HTML保存到本地文件

题3(⭐ 安全题):以下代码有什么安全风险?如何修复?

1
2
3
4
5
6
// 图片代理服务:用户提供图片URL,服务器下载后返回
@GetMapping("/proxy")
public byte[] proxyImage(@RequestParam String imageUrl) throws Exception {
URL url = new URL(imageUrl);
return url.openStream().readAllBytes();
}

上一章 目录 下一章
多线程与并发 java基础 正则表达式