网络基础
网络编程就是让两台电脑之间能互相说话。要说话得知道三个事:找谁(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 {
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());
out.println("bye"); System.out.println("客户端收到: " + in.readLine()); } }
|
通信流程
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/json、text/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 {
URL url = new URL("https://httpbin.org/get"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000);
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 {
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: SSRF攻击:用户 → 服务器 → 内网资源(http: → 本机服务(http: → 云环境元数据(http:
|
漏洞代码示例
1 2 3 4 5 6 7 8 9 10
| @Test public void testSSRFVulnerable() throws Exception {
String userInputUrl = "http://169.254.169.254/latest/meta-data/";
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";
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; }
InetAddress address = InetAddress.getByName(url.getHost()); if (address.isSiteLocalAddress() || address.isLoopbackAddress() || address.isLinkLocalAddress()) { System.out.println("拒绝访问:内网IP"); return; }
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(false); }
|
| 防御手段 |
说明 |
| URL白名单 |
只允许访问指定域名 |
| 禁止内网IP |
过滤 10.x、172.16-31.x、192.168.x、127.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
| @GetMapping("/proxy") public byte[] proxyImage(@RequestParam String imageUrl) throws Exception { URL url = new URL(imageUrl); return url.openStream().readAllBytes(); }
|