当前位置:首页>java>Java 网络编程:从 Socket 到 HttpClient,一口气打通

Java 网络编程:从 Socket 到 HttpClient,一口气打通

  • 2026-10-21 13:28:25
Java 网络编程:从 Socket 到 HttpClient,一口气打通

有段时间我挺怕“线上偶发网络问题”这几个字的——你写的代码明明很朴素:连上、发一句、收一句。结果一上生产就变成玄学:卡住不返回、偶尔超时、同一个地址有时行有时不行、POST 过去像石沉大海。后来我把 Java 这套网络工具链从低到高捋了一遍,才发现:不是网络太难,是我们经常“用错层级”或者“顺序搞反”。

这篇我就按我自己的理解路线走一遍:从 Socket → SSL Socket → URL/URI → URLConnection(含 POST)→ HttpClient(HTTP/2)→ JDK 自带 HTTP Server → 发邮件(别手搓 SMTP)。你看完基本就能把“网络交互”这块在 Java 里打通。


0)先从一个日常痛点开场:为啥我的请求总卡住?

最常见的坑不是 DNS 也不是防火墙(当然它们也烦),而是:

  • 你用的是 Socket,但你想解决的是 HTTP 的问题(比如重定向、Header、Content-Type)
  • 你用的是 URLConnection,但你把它当成了 “像 Socket 那样的输入输出流”——结果 调用顺序错了
  • 你发 POST,服务端回了 404/500,你只读 getInputStream(),异常一抛你就丢了服务端的错误页
  • 你没设置超时,网络抖一下就“永远等下去”(这才是真正的线上噩梦)

所以我的习惯是:先选对工具层级,再谈排查。下面从最低层开始。


1)Socket:它很原始,但你必须会(尤其是排查时)

1.1 最小可运行:回声服务(像“电话里复读机”)

这类 Demo 我很爱用:它不需要协议知识,能快速验证“网络通不通、读写有没有问题”。

服务端:接收一句就回一句,输入 BYE 退出。

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

publicclassEchoServer{
publicstaticvoidmain(String[] args)throws Exception {
int port = args.length >= 1 ? Integer.parseInt(args[0]) : 8189;

try (ServerSocket server = new ServerSocket(port)) {
// 小技巧:用虚拟线程处理每个连接,写起来像同步,跑起来又很轻
            ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();

while (true) {
                Socket client = server.accept(); // 阻塞等待连接
                es.submit(() -> serve(client));
            }
        }
    }

privatestaticvoidserve(Socket client){
try (var in = new Scanner(client.getInputStream());
var out = new PrintWriter(client.getOutputStream(), true)) {

            out.println("你好!输入 BYE 结束。");

while (in.hasNextLine()) {
                String line = in.nextLine();
                out.println("回声: " + line);

// 用 strip() 比 trim() 更“现代”,能处理更多空白字符
if (line.strip().equals("BYE")) break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

为什么这么写:

  • accept() 是阻塞的:服务端天然就是“等人来敲门”
  • 处理每个连接最好别在主线程做,否则一个慢客户端就能把你拖死
  • PrintWriter(..., true) 开启自动 flush:不然你可能以为“没回包”,其实是你没 flush

1.2 升级:加 TLS(SSLServerSocketFactory / SSLSocketFactory)

如果你需要的是安全通道(比如内部服务用 TLS),Java 也能很快把 Socket 升级成 SSL Socket:

import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocketFactory;
import java.net.*;

publicclassSslEchoServer{
publicstaticvoidmain(String[] args)throws Exception {
int port = args.length >= 1 ? Integer.parseInt(args[0]) : 8189;

        ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket s = factory.createServerSocket(port)) {
while (true) {
                Socket incoming = s.accept();
// 这里省略处理逻辑,和普通 Socket 一样读写
            }
        }
    }
}

我个人的提醒:

  • “默认 SSL 工厂”背后依赖你本机/运行环境的证书配置
  • 真实项目里通常要配置 keystore/truststore(这里先不展开,不然篇幅炸)

2)URI vs URL:别再把字符串当“地址”直接拼

我以前写过很多这种代码:

String url = "https://xx.com/api?name=" + name + "&city=" + city;

看似简单,实际上超容易拼错、编码错。

2.1 先分清:URI 是“语法结构”,URL 才能“打开连接”

  • URI:负责解析、拼接、resolve/relativize(像“快递单格式”)
  • URL:能 openStream() 或 openConnection()(像“快递真的送出去”)

现在更推荐的构造方式是:

var url = new java.net.URI(urlString).toURL();

(是的,有些 URL 构造器在较新版本里被标记不建议使用,因为可能构造出“表面合法但实际上不规范”的 URL。)

2.2 URI 的解析能力:比你想的复杂

URI 里面可能有:

  • scheme(https、ftp、file、jar…)
  • authority(user-info@host:port)
  • path / query / fragment

你可以这样拆:

URI u = new URI("https://google.com?q=Beach+Chalet");
System.out.println(u.getScheme());
System.out.println(u.getHost());
System.out.println(u.getQuery());

2.3 resolve / relativize:处理相对路径神器

这个在写爬虫、做文档链接处理时特别爽:

URI base = new URI("https://docs.mycompany.com/api/java/net/ServerSocket.html");
URI relative = new URI("../../java/net/Socket.html
#Socket()");
URI combined = base.resolve(relative);

System.out.println(combined);
// https://docs.mycompany.com/api/java/net/Socket.html#Socket()

3)URLConnection:它能做很多,但“步骤顺序”必须对

如果你只是“拿网页内容”,URL.openStream() 就够了:

var url = new URI("https://example.com").toURL();
try (var in = new Scanner(url.openStream())) {
while (in.hasNextLine()) System.out.println(in.nextLine());
}

但一旦你需要:

  • 读响应 header
  • 设置 request header
  • 设置超时
  • POST 表单
  • 处理错误流 / 重定向

那就用 URLConnection / HttpURLConnection。

3.1 正确心智:URLConnection 不是“Socket 的输入输出流”

它背后会自动处理请求/响应头,所以调用顺序很关键:

  1. openConnection()
  2. 设置参数(header、timeout、doOutput…)
  3. connect()
  4. 读 header
  5. 再读 body(getInputStream())

3.2 读取响应头 + 前 10 行内容(实战小工具)

import java.io.*;
import java.net.*;
import java.util.*;

publicclassURLConnectionDemo{
publicstaticvoidmain(String[] args)throws Exception {
        String urlName = args.length > 0 ? args[0] : "http://horstmann.com";
var url = new URI(urlName).toURL();

        URLConnection conn = url.openConnection();
        conn.setConnectTimeout(5000); // 我个人习惯:必须设,别让线程无限等
        conn.setReadTimeout(5000);

// 如果需要 Basic Auth
// conn.setRequestProperty("Authorization", "Basic " + base64(username:password));

        conn.connect();

// 1) 打印 header
        Map<String, List<String>> headers = conn.getHeaderFields();
for (var e : headers.entrySet()) {
            String k = e.getKey();
for (String v : e.getValue()) System.out.println(k + ": " + v);
        }

        System.out.println("----------");
        System.out.println("Content-Type: " + conn.getContentType());
        System.out.println("Content-Length: " + conn.getContentLength());
        System.out.println("Content-Encoding: " + conn.getContentEncoding());
        System.out.println("Date: " + conn.getDate());
        System.out.println("Last-Modified: " + conn.getLastModified());
        System.out.println("----------");

// 2) 打印内容前 10 行
        String encoding = conn.getContentEncoding();
if (encoding == null) encoding = "UTF-8";

try (var in = new Scanner(conn.getInputStream(), encoding)) {
for (int i = 1; in.hasNextLine() && i <= 10; i++) {
                System.out.println(in.nextLine());
            }
if (in.hasNextLine()) System.out.println("...");
        }
    }
}

常见误区:

  • 以为 getInputStream() 只是“拿输入流”,其实它可能触发真正的网络交互
  • 没设 timeout,某些网络环境下你会卡到怀疑人生

4)POST 表单:GET 是“把参数贴 URL 上”,POST 是“写到输出流里”

4.1 GET 参数的编码规则(很容易踩坑)

  • 空格要变 +
  • 其他特殊字符要 %XX
  • 推荐统一用 UTF-8

4.2 用 URLConnection 发 application/x-www-form-urlencoded

这就是“模拟网页表单提交”的经典方式:

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

publicclassPostFormDemo{
publicstaticvoidmain(String[] args)throws Exception {
        URL url = new URI("https://host/path").toURL();
        URLConnection connection = url.openConnection();

        connection.setDoOutput(true); // 关键:不设就拿不到 OutputStream

try (var out = new PrintWriter(connection.getOutputStream())) {
            out.print("name=" + URLEncoder.encode("旺仔", StandardCharsets.UTF_8));
            out.print("&city=" + URLEncoder.encode("南京", StandardCharsets.UTF_8));
        }

try (var in = new Scanner(connection.getInputStream(), StandardCharsets.UTF_8)) {
while (in.hasNextLine()) System.out.println(in.nextLine());
        }
    }
}

为什么必须 setDoOutput(true):

  • 默认连接只有输入(读响应)没有输出(写请求体)
  • 这行就像在说:“我这次不是单纯 GET,我要写数据给你”

4.3 服务端报错时:别只盯 getInputStream(),要看 getErrorStream()

HTTP 404/500 时,getInputStream() 可能直接抛异常,但服务端其实回了错误页。你得这样兜底:

HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
try (InputStream in = conn.getInputStream()) {
// read ok
    }
} catch (IOException e) {
    InputStream err = conn.getErrorStream();
if (err != null) {
        System.out.println(new String(err.readAllBytes(), StandardCharsets.UTF_8));
    } else {
throw e;
    }
}

(这个技巧我用来排查第三方接口特别好用,能看到他们到底回了啥。)

4.4 重定向:有时要手动跟(尤其跨 http/https)

HttpURLConnection 能自动跟一些 redirect,但跨 HTTPS→HTTP默认不自动(安全原因)。更坑的是:有些场景你设置的 header(比如 User-Agent)在自动重定向里不一定被保留,你就得手动跟 Location。


5)HttpClient:现代、顺手、HTTP/2,终于像 2026 年的 API 了

URLConnection 历史包袱挺重。现在我更推荐优先用 java.net.http.HttpClient:

5.1 GET:最基础用法

import java.net.URI;
import java.net.http.*;

publicclassHttpClientGetDemo{
publicstaticvoidmain(String[] args)throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://example.com"))
                .GET()
                .build();

        HttpResponse<String> resp =
                client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(resp.statusCode());
        System.out.println(resp.body());
    }
}

5.2 POST JSON:比 URLConnection 清爽太多

import java.net.URI;
import java.net.http.*;

publicclassHttpClientPostJsonDemo{
publicstaticvoidmain(String[] args)throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.ALWAYS)
                .build();

        String json = """
                {"
name":"旺仔","city":"南京","note":"来杯热美式"}
                "
"";

        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://host/api/order"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

        HttpResponse<String> resp =
                client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(resp.statusCode());
        System.out.println(resp.body());
    }
}

5.3 读 Header:firstValue 很舒服(且大小写不敏感)

var lastModified = resp.headers().firstValue("Last-Modified");
System.out.println(lastModified.orElse("(none)"));

5.4 异步:sendAsync + CompletableFuture

适合“并发拉多个接口,然后聚合结果”:

client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
      .thenAccept(r -> System.out.println(r.body()));

我个人建议:

  • 业务里用异步之前,先把超时、重试、限流、熔断这套想清楚(否则你只是把问题“并发放大”)

6)JDK 自带 HTTP Server:本地联调神器(别再到处装 Nginx 了)

你需要一个临时的 HTTP 服务?JDK 就自带。

6.1 命令行:jwebserver

在当前目录起一个静态文件服务(默认 8000 端口):

jwebserver

换目录/端口:

jwebserver -d /tmp -p 8189

像极了 python -m http.server,但不用装 Python(当然大部分人都有…)。

6.2 编程方式:写个 /echo,回显请求信息

这个我用来观察浏览器/客户端到底发了什么:

import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

publicclassMiniHttpEchoServer{
publicstaticvoidmain(String[] args)throws Exception {
int port = args.length >= 1 ? Integer.parseInt(args[0]) : 8189;

        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
        server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

        server.createContext("/echo", exchange -> {
            String method = exchange.getRequestMethod();
            URI uri = exchange.getRequestURI();

var sb = new StringBuilder();
            sb.append(method).append(" ").append(uri).append("\n\n");

            exchange.getRequestHeaders().forEach(
                    (k, vs) -> vs.forEach(v -> sb.append(k).append(": ").append(v).append("\n"))
            );

            sb.append("\n");
byte[] bodyBytes = exchange.getRequestBody().readAllBytes();
            sb.append(new String(bodyBytes, StandardCharsets.UTF_8)).append("\n");

byte[] resp = sb.toString().getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
            exchange.sendResponseHeaders(200, resp.length);

try (OutputStream os = exchange.getResponseBody()) {
                os.write(resp);
            }
        });

        server.start();
        System.out.println("Listening on http://localhost:" + port + "/echo");
    }
}

7)发邮件:别再手搓 SMTP 了,用 Jakarta Mail

以前确实可以直接 Socket 连 25 端口,然后按 SMTP 协议写 HELO / MAIL FROM / RCPT TO ...。但现在基本都会遇到:

  • 服务器不让你随便发(反垃圾策略)
  • 需要认证
  • 要走 TLS
  • 甚至要应用专用密码

所以我的结论很简单:用 Jakarta Mail,别跟 SMTP 细节硬刚(尤其你只是想发一封告警邮件)。


8)我踩过的坑(你大概率也会踩)

  • 把 URLConnection 当 Socket 用:以为能随意先读后写,结果 header/重定向处理把你“安排了”
  • 忘了设置 timeout:线上线程池被卡满,慢慢把你服务拖死
  • POST 没设 Content-Type:服务端解析失败,你却以为是网络不通
  • 只看 InputStream,不看 ErrorStream:第三方接口回的错误内容全丢了
  • 手动拼 query 参数:中文/空格/特殊符号全炸,建议统一 URLEncoder.encode(..., UTF_8)

9)更进一步:如果你要写“可上线”的网络调用

我现在判断一个网络调用能不能上线,除了功能正确,还会问自己几个问题:

  • 超时策略是什么?连接超时 + 读取超时都设置了吗?
  • 失败要不要重试?重试有没有退避?会不会把对方打挂?
  • 是否需要记录关键 header / status code / request id(排查用)
  • 是否需要手动处理 redirect / cookie / auth?
  • 你是在合适的层级上解决问题吗?(Socket / URLConnection / HttpClient)

这些东西不会让你代码更“炫”,但会让你半夜少爬几次起来看日志。


结尾:网络编程不是玄学,是“选对工具 + 尊重协议”

我个人的实践顺序一般是:

  • 本地联调:JDK HttpServer / jwebserver
  • 正经 HTTP 调用:优先 HttpClient
  • 必须兼容老代码/特定协议细节:用 URLConnection/HttpURLConnection
  • 排查、做极简探测、做自定义协议:才回到 Socket/SSL Socket

你把这条路线记住,网络问题就不会那么“玄”了——它只是在提醒你:别用螺丝刀去敲钉子。

最新文章

随机文章