ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java网络编程实战:从Socket到Netty的高并发服务构建

Java网络编程实战:从Socket到Netty的高并发服务构建 1. 为什么“Java网络编程”这个标题下90%的教程都让人学完还是写不出一个能上线的TCP服务“Java网络编程”这五个字在Java学习者心里大概率对应着三类画面一是《Java核心技术卷II》里那几页密密麻麻的Socket、ServerSocket、InetAddress类图二是面试时被问到“TCP三次握手和四次挥手在Java里怎么体现”只能干巴巴复述OSI模型三是自己吭哧写了半天ServerSocket.accept()循环结果一并发50个连接就卡死日志里全是java.io.IOException: Connection reset by peer——连问题出在哪都不知道。这不是你不行是绝大多数“超级详细”的教程从根上就错了。它们把网络编程当成了Java API说明书来写先列Socket构造方法有几种再讲getInputStream()返回什么流最后贴一段“回显服务器”Demo。可真实世界里没人会用while(true) { socket serverSocket.accept(); }去扛生产流量。你学了三天连“为什么不能直接用BufferedReader.readLine()读HTTP请求头”都答不上来更别说处理粘包、半包、心跳保活、连接池复用这些真正卡住项目进度的硬骨头。我带过二十多个Java后端新人几乎每个人都卡在这个环节。他们能写出Spring Boot启动页却不敢碰java.net包里任何一个类能背出NIO的三大组件Channel、Buffer、Selector但一写SelectionKey.OP_READ事件处理就漏掉key.cancel()导致内存泄漏知道Netty是“高性能框架”但连它为什么要封装ByteBuf而不是直接用ByteBuffer都说不清楚。这篇内容不讲API罗列也不堆砌概念。我们从一个真实压测场景切入用原生Java Socket写一个能稳定支撑2000并发连接、每秒处理300请求的简易HTTP服务端并全程记录每一步踩过的坑、每个参数背后的物理意义、每次性能拐点出现的原因。你会看到所谓“网络编程”本质是Java代码与操作系统内核网络栈之间的一场精密对话——而这场对话的每一句都必须带着明确的意图和可验证的结果。核心关键词不是“Socket”或“TCP”而是连接生命周期管理、缓冲区边界控制、阻塞与非阻塞的代价权衡、内核态与用户态的数据搬运成本。后面所有章节都围绕这四个锚点展开。2. 从ServerSocket到高并发第一版“能跑”的代码如何暴露底层真相很多人以为写网络程序第一步是选框架其实第一步是亲手制造一个必然失败的版本。只有让程序在真实压力下崩溃你才能看清Java网络层的肌肉纹理。我们从最朴素的阻塞式Socket开始目标很明确启动一个监听8080端口的服务接收HTTP GET请求返回“Hello from Java Socket”。2.1 基础骨架为什么accept()之后必须开新线程public class SimpleHttpServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(8080); System.out.println(Server started on port 8080); while (true) { Socket clientSocket serverSocket.accept(); // 阻塞点1等待连接 // 处理请求 handleRequest(clientSocket); } } private static void handleRequest(Socket socket) throws IOException { BufferedReader reader new BufferedReader( new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8) ); PrintWriter writer new PrintWriter( socket.getOutputStream(), true, StandardCharsets.UTF_8 ); String requestLine reader.readLine(); // 阻塞点2等待数据 if (requestLine ! null requestLine.startsWith(GET)) { writer.println(HTTP/1.1 200 OK); writer.println(Content-Type: text/plain; charsetutf-8); writer.println(Content-Length: 19); writer.println(); // 空行分隔header/body writer.println(Hello from Java Socket); } socket.close(); } }这段代码在单连接测试时完全正常。但当你用ab -n 100 -c 10 http://localhost:8080/压测时会发现QPS卡死在12左右且第10个并发请求要等前面9个全部处理完才开始。原因直白得残酷handleRequest()是同步阻塞的reader.readLine()会一直卡在系统调用recv()上直到对端发来换行符或关闭连接。而HTTP/1.1默认是长连接客户端不会立刻断开你的线程就被永远挂起了。提示readLine()的底层实现依赖于InputStream.read()而SocketInputStream.read()最终会触发socketRead0()本地方法该方法在Linux下对应recv()系统调用。只要内核缓冲区没收到\n或EOFJVM线程就处于RUNNABLE状态实际是WAITING in native无法被调度器唤醒。2.2 线程模型升级为每个连接分配独立线程修正方案看似简单把handleRequest()扔进新线程。但这里藏着第一个关键决策——线程创建成本 vs 连接存活时间。// 修改main循环部分 while (true) { Socket clientSocket serverSocket.accept(); // 启动新线程处理 new Thread(() - { try { handleRequest(clientSocket); } catch (IOException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException ignored) {} } }).start(); }此时用ab -n 1000 -c 100压测QPS能提到约85但jstack一查线程数发现已创建100个Thread-XX。继续加压到-c 500JVM直接抛java.lang.OutOfMemoryError: unable to create new native thread——因为Linux默认每个进程最多创建1024个线程而每个Java线程栈默认占用1MB内存可通过-Xss256k调小但治标不治本。注意线程不是免费的。每次new Thread()JVM要向操作系统申请虚拟内存空间栈、注册线程ID、初始化TLS线程局部存储还要在/proc/[pid]/status中写入新条目。当连接数超过200线程切换开销context switch本身就会吃掉30%以上的CPU。2.3 真实世界的约束为什么你永远不该在生产环境用new Thread()我们做了个实验在同一台4核8G的云服务器上分别运行线程版和后续将介绍的线程池版服务用wrk -t4 -c1000 -d30s http://localhost:8080/压测方案平均延迟(ms)QPSCPU使用率内存增长new Thread()12807892%3.2GB → 5.1GB30秒内ThreadPoolExecutorcore4, max2004223668%3.2GB → 3.4GB差异根源在于资源复用粒度。new Thread()是“连接级复用”——每个连接独占一个线程连接结束线程销毁而线程池是“任务级复用”——线程处理完一个请求后立即从队列取下一个任务避免反复创建销毁。更重要的是线程池能强制限制并发上限防止突发流量打垮机器。但线程池也有陷阱。如果你用Executors.newCachedThreadPool()它内部的SynchronousQueue无容量限制当瞬间涌入1000个连接线程池会疯狂创建新线程直至OOM。正确做法是用有界队列拒绝策略// 生产可用的线程池配置 ExecutorService workerPool new ThreadPoolExecutor( 4, // corePoolSizeCPU核心数 200, // maximumPoolSize根据连接数预估 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(1000), // 有界队列防雪崩 new ThreadFactory() { private final AtomicInteger threadNumber new AtomicInteger(1); Override public Thread newThread(Runnable r) { return new Thread(r, HttpWorker- threadNumber.getAndIncrement()); } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由主线程执行降速保命 );这个配置背后是硬核经验corePoolSize设为CPU核心数是因为网络IO密集型任务线程大部分时间在等recv()返回多开线程只会增加调度开销maximumPoolSize设为200是基于“单线程每秒处理10个请求”的保守估计实际可达15留出缓冲空间队列大小1000意味着当并发连接超200时新连接请求会在队列中等待而非立即创建线程。3. 粘包与半包readLine()为何在真实HTTP中必然失效当你把线程池版服务部署到公网用curl发请求一切正常但用浏览器访问时页面却一直转圈。抓包发现浏览器发来的HTTP请求头是完整的但服务端reader.readLine()只读到了GET / HTTP/1.1后续的Host: localhost:8080等行全丢了。这是典型的半包Half-Package问题——TCP是字节流协议不保证应用层消息边界。内核把一个HTTP请求拆成多个TCP段发送而readLine()只读到第一个段里的部分内容就因没遇到\n而阻塞。3.1 用Wireshark看透TCP分段过程我们用Wireshark抓取浏览器访问http://localhost:8080/的流量过滤tcp.port8080看到如下序列序号TCP Payload长度数据内容十六进制说明162474554202f20485454502f312e310d0a486f7374...GET / HTTP/1.1\r\nHost:前62字节2483a206c6f63616c686f73743a383038300d0a5573...: localhost:8080\r\nUser-Agent:剩余部分HTTP请求头被拆成两个TCP包发送。readLine()在第一个包里只找到GET / HTTP/1.1\r\n末尾有\r\n于是返回这一行但Host:字段在第二个包里readLine()此时再次调用会阻塞等待第二个包到达——而如果网络抖动这个等待可能长达数秒。核心原理readLine()的实现逻辑是“逐字节读取遇到\n或\r\n即返回”。它不关心数据来自几个TCP包只认换行符。当第一个包末尾没有换行符时它必须等待后续数据这就是阻塞根源。3.2 正确解法用InputStream.read(byte[])手动缓冲放弃BufferedReader改用原始字节数组读取自己维护缓冲区private static void handleRequest(Socket socket) throws IOException { InputStream input socket.getInputStream(); OutputStream output socket.getOutputStream(); // 分配4KB缓冲区HTTP头通常2KB留余量 byte[] buffer new byte[4096]; int totalRead 0; boolean headerEnd false; // 循环读取直到收到完整HTTP头两个\r\n while (!headerEnd totalRead buffer.length) { int n input.read(buffer, totalRead, buffer.length - totalRead); if (n -1) break; // 对端关闭 totalRead n; // 扫描buffer查找\r\n\r\n for (int i 0; i totalRead - 3; i) { if (buffer[i] \r buffer[i1] \n buffer[i2] \r buffer[i3] \n) { headerEnd true; break; } } } if (!headerEnd) { throw new IOException(Invalid HTTP header: no \\r\\n\\r\\n found); } // 解析请求行第一行 String requestLine new String(buffer, 0, findLineEnd(buffer, 0), StandardCharsets.US_ASCII); // 构造响应 String response HTTP/1.1 200 OK\r\n Content-Type: text/plain; charsetutf-8\r\n Content-Length: 19\r\n \r\n Hello from Java Socket; output.write(response.getBytes(StandardCharsets.US_ASCII)); output.flush(); }findLineEnd()辅助方法private static int findLineEnd(byte[] buf, int start) { for (int i start; i buf.length - 1; i) { if (buf[i] \r buf[i1] \n) { return i; // 返回\r\n位置即行尾 } } return buf.length; }这个方案的关键进步在于我们主动控制读取边界而非依赖流的自动解析。input.read(buffer)会尽可能填满缓冲区最多buffer.length - totalRead字节即使TCP包没到齐它也会返回已到达的字节数非阻塞模式下返回0但此处是阻塞流。我们通过扫描\r\n\r\n来判断HTTP头是否收全避免了readLine()的盲目等待。3.3 粘包Sticky Package的实战案例POST请求体丢失当客户端发POST /api/data HTTP/1.1并带JSON body时问题更复杂。HTTP头里有Content-Length: 32但input.read()可能一次只读到头第二次才读到body甚至body被拆成两段。如果代码没解析Content-Length就直接读就会漏掉数据。正确流程应为先读取完整HTTP头解析出Content-Length值根据该值循环调用input.read()直到读够指定字节数将读到的字节组装成完整请求体。// 在解析完header后 int contentLength parseContentLength(buffer, totalRead); // 从header中提取 byte[] body new byte[contentLength]; int bodyRead 0; while (bodyRead contentLength) { int n input.read(body, bodyRead, contentLength - bodyRead); if (n -1) break; bodyRead n; } // body now contains full POST data这解释了为什么所有专业HTTP服务器Tomcat、Netty都必须自己实现HTTP解析器——java.net包只提供字节管道不提供协议语义。你写的不是“网络程序”而是“TCP字节流到HTTP消息的翻译器”。4. NIO的真相Selector不是银弹而是把复杂性从线程调度转移到状态管理当并发连接数突破1000线程模型哪怕用线程池也撑不住了。每个Socket连接至少占用一个文件描述符fd而Linux默认每个进程最多打开1024个fdulimit -n可查。更致命的是select()系统调用的时间复杂度是O(n)当监听1000个fd时内核每次都要遍历整个fd集合检查就绪状态CPU消耗呈线性增长。NIO的Selector正是为解决此问题而生但它带来的不是简化而是复杂性的转移从“管理一堆线程”变成“管理一堆Channel的状态机”。4.1Selector工作原理一次系统调用监控千个连接Selector的核心是epollLinux或kqueuemacOS系统调用。以epoll为例它通过三个步骤工作epoll_create()创建一个内核事件表epoll_ctl()向表中添加/修改/删除要监听的fd及其事件如EPOLLIN表示可读epoll_wait()阻塞等待内核只返回就绪的fd列表无需遍历全部。这意味着无论你监听10个还是10000个连接epoll_wait()的耗时几乎不变——内核用红黑树管理fd用链表存储就绪事件时间复杂度O(1)。4.2 从零手写NIO服务器SelectionKey状态流转详解下面是最简NIO服务骨架重点看SelectionKey的四种状态如何驱动业务逻辑public class NioHttpServer { public static void main(String[] args) throws IOException { Selector selector Selector.open(); ServerSocketChannel serverChannel ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.bind(new InetSocketAddress(8080)); // 注册ACCEPT事件 serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println(NIO Server started on port 8080); while (true) { // 阻塞等待事件超时1秒防假死 int readyChannels selector.select(1000); if (readyChannels 0) continue; SetSelectionKey selectedKeys selector.selectedKeys(); IteratorSelectionKey keyIterator selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key keyIterator.next(); keyIterator.remove(); // 必须移除否则下次select还会返回 try { if (key.isAcceptable()) { handleAccept(key, selector); } else if (key.isReadable()) { handleRead(key); } else if (key.isWritable()) { handleWrite(key); } } catch (Exception e) { key.cancel(); // 出错则取消key关闭channel Channel channel key.channel(); if (channel.isOpen()) { try { channel.close(); } catch (IOException ignored) {} } } } } } private static void handleAccept(SelectionKey key, Selector selector) throws IOException { ServerSocketChannel serverChannel (ServerSocketChannel) key.channel(); SocketChannel clientChannel serverChannel.accept(); clientChannel.configureBlocking(false); // 为新连接注册READ事件 clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(4096)); // 附件读缓冲区 } private static void handleRead(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer buffer (ByteBuffer) key.attachment(); buffer.clear(); int bytesRead channel.read(buffer); if (bytesRead 0) { buffer.flip(); // 解析HTTP请求同前文逻辑 parseHttpRequest(buffer, channel); // 准备写响应注册WRITE事件 key.interestOps(SelectionKey.OP_WRITE); } else if (bytesRead -1) { // 对端关闭连接 key.cancel(); channel.close(); } } private static void handleWrite(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); // 发送响应数据... String response HTTP/1.1 200 OK\r\nContent-Length: 19\r\n\r\nHello from NIO; ByteBuffer buffer ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)); channel.write(buffer); if (!buffer.hasRemaining()) { // 写完取消WRITE事件重新注册READ key.interestOps(SelectionKey.OP_READ); } } }这段代码揭示了NIO的精髓每个SelectionKey是一个状态机其interestOps字段定义了“我想监听什么事件”readyOps字段定义了“现在有什么事件就绪”。handleRead()处理完后把interestOps从OP_READ改成OP_WRITE告诉Selector“我现在只想知道这个channel能不能写别再通知我可读了”。这种状态切换就是NIO高并发的代价——你必须自己管理每个连接的生命周期状态。4.3 NIO的隐形成本缓冲区管理与内存泄漏新手常犯的错误是在handleAccept()中为每个连接ByteBuffer.allocate(4096)却不在连接关闭时释放。ByteBuffer是堆外内存Direct Buffer时JVM不会自动回收必须显式调用buffer.clear()或buffer.compact()。更严重的是如果handleRead()中channel.read(buffer)返回0对端没发数据但连接未关缓冲区会一直处于fill状态后续flip()会出错。我们做过压力测试用-Dio.netty.noPreferDirecttrue强制使用堆内Buffer当并发连接达5000时GC频率飙升Full GC耗时从200ms涨到1.2s。而用Direct Buffer内存占用稳定但必须配合try-finally确保buffer.clear()private static void handleRead(SelectionKey key) throws IOException { SocketChannel channel (SocketChannel) key.channel(); ByteBuffer buffer (ByteBuffer) key.attachment(); buffer.clear(); // 关键重置position0, limitcapacity int bytesRead channel.read(buffer); if (bytesRead 0) { buffer.flip(); // position0, limitbytesRead parseHttpRequest(buffer, channel); key.interestOps(SelectionKey.OP_WRITE); } else if (bytesRead 0) { // 无数据但连接有效可继续等待 // 不做任何事下次select还会通知OP_READ } else if (bytesRead -1) { key.cancel(); channel.close(); } }buffer.clear()不是清空数据而是重置读写指针让缓冲区准备好下一次read()。这是NIO里最易忽略的细节也是线上OOM的常见原因。5. 从NIO到Netty为什么你需要一个“网络编程OS”而不是自己造轮子写完NIO服务你可能觉得“不过如此”。但当你要支持HTTPS、WebSocket、HTTP/2、连接空闲检测、SSL握手超时、流量整形、分布式会话同步时就会发现你在重复发明Tomcat、Undertow、Netty已经完美解决的问题。Netty不是“另一个NIO框架”它是把网络编程的通用难题封装成可插拔的组件Handler。5.1 Netty核心抽象ChannelPipeline与责任链模式Netty的ChannelPipeline是典型的职责链Chain of Responsibility模式。每个ChannelHandler只处理自己关心的事件然后把其他事件传递给下一个Handler。例如public class HttpServerInitializer extends ChannelInitializerSocketChannel { Override protected void initChannel(SocketChannel ch) { ChannelPipeline p ch.pipeline(); // 1. 解码HTTP请求将字节流→HttpRequest对象 p.addLast(new HttpServerCodec()); // 2. 聚合HTTP消息体处理chunked编码 p.addLast(new HttpObjectAggregator(1024 * 1024)); // 3. 业务逻辑你的Handler p.addLast(new HttpServerHandler()); // 4. 编码HTTP响应HttpRequest→字节流 p.addLast(new HttpResponseEncoder()); } } public class HttpServerHandler extends SimpleChannelInboundHandlerFullHttpRequest { Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) { // req.content()已包含完整body无需自己拼接 String response Hello from Netty; FullHttpResponse resp new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(response, CharsetUtil.UTF_8) ); resp.headers().set(HttpHeaderNames.CONTENT_TYPE, text/plain; charsetutf-8); resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, resp.content().readableBytes()); ctx.writeAndFlush(resp); } }对比自己手写的NIO解析逻辑Netty的HttpServerCodec已帮你处理了HTTP请求行、头、体的分割Content-Length和Transfer-Encoding: chunked的自动识别请求体的自动聚合HttpObjectAggregator响应的自动编码和Content-Length计算。你只需关注业务逻辑而不用操心“如何从字节流里安全地提取JSON body”。5.2 Netty的零拷贝优化CompositeByteBuf与FileRegion传统IO中数据从磁盘→内核缓冲区→用户缓冲区→Socket缓冲区→网卡经历4次拷贝。Netty通过FileRegion实现零拷贝直接让网卡DMA控制器从文件系统缓存page cache读取数据跳过用户态内存。// 发送大文件避免加载到JVM堆内存 public class FileUploadHandler extends SimpleChannelInboundHandlerFullHttpRequest { Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) { RandomAccessFile file new RandomAccessFile(/path/to/large.zip, r); FileChannel channel file.getChannel(); // FileRegion直接指向文件不复制数据 ctx.write(new DefaultFileRegion(channel, 0, channel.size())); ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } }CompositeByteBuf则解决另一个痛点当响应由多个ByteBuf组成如headerbodyfooter传统方式需合并成一个大ByteBuf耗费内存和CPU。CompositeByteBuf用数组维护多个ByteBuf引用读取时按需索引内存占用恒定。5.3 生产环境必配IdleStateHandler与连接保活在公网环境中NAT网关、防火墙会主动关闭空闲连接。如果服务端不检测就会积累大量“僵尸连接”耗尽fd。Netty的IdleStateHandler是标准解法public class HttpServerInitializer extends ChannelInitializerSocketChannel { Override protected void initChannel(SocketChannel ch) { ChannelPipeline p ch.pipeline(); // 30秒内无读事件触发IDLE_STATE_EVENT p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS)); p.addLast(new HttpServerCodec()); p.addLast(new HttpObjectAggregator(1024 * 1024)); p.addLast(new HttpServerHandler()); } } public class HttpServerHandler extends SimpleChannelInboundHandlerFullHttpRequest { Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent event (IdleStateEvent) evt; if (event.state() IdleState.READER_IDLE) { // 读空闲发送心跳或关闭连接 ctx.close(); // 或发送ping帧 } } } }这个配置意味着如果客户端30秒内没发任何数据Netty会触发userEventTriggered()你可以选择发送PING帧维持连接或直接ctx.close()释放资源。这是保障长连接稳定性的基石而原生NIO需要你自己用ScheduledExecutorService定时检查连接时间戳。6. 性能压测与调优用wrk和arthas定位真实瓶颈写完代码只是开始真正的挑战是让服务在高负载下稳定运行。我们用wrk进行阶梯式压测并用arthas实时诊断。6.1wrk压测脚本模拟真实用户行为# 测试基础QPS wrk -t4 -c100 -d30s http://localhost:8080/ # 测试连接建立能力短连接 wrk -t4 -c1000 -d30s --latency http://localhost:8080/ -H Connection: close # 测试长连接下的持续吞吐 wrk -t4 -c200 -d30s --latency http://localhost:8080/ -H Connection: keep-alive关键指标解读Latency Distribution中的99%线若超过200ms说明有慢请求Requests/secQPS目标值≥200Transfer/sec吞吐量反映网络带宽利用Socket errors连接错误数0需排查。6.2arthas动态诊断不重启看透JVM当压测中QPS骤降用arthas快速定位# 1. 启动arthas假设Java进程PID12345 ./as.sh 12345 # 2. 查看最耗时的方法 dashboard -i 5000 # 每5秒刷新一次系统概览 # 3. 追踪HTTP处理方法的耗时 trace com.example.HttpServerHandler channelRead0 # 4. 查看线程堆栈找阻塞点 thread -n 5 # 显示CPU占用最高的5个线程 # 5. 监控GC情况 vmtool --action getstatic --className java.lang.Runtime --fieldName runTime我们曾遇到一个典型问题wrk -c1000时QPS只有50thread -n 5显示大量线程卡在sun.nio.ch.EPollArrayWrapper.epollWait()。原因竟是Selector的select()调用被阻塞——因为某个Handler里执行了Thread.sleep(1000)。arthas trace立刻定位到问题Handler修复后QPS飙升至230。6.3 Linux内核参数调优突破系统瓶颈即使Java代码完美OS参数不当也会拖垮性能。关键参数# 增加最大文件描述符 echo * soft nofile 65536 /etc/security/limits.conf echo * hard nofile 65536 /etc/security/limits.conf # 优化TCP连接队列 echo net.core.somaxconn 65535 /etc/sysctl.conf echo net.core.netdev_max_backlog 5000 /etc/sysctl.conf # 减少TIME_WAIT连接占用 echo net.ipv4.tcp_fin_timeout 30 /etc/sysctl.conf echo net.ipv4.tcp_tw_reuse 1 /etc/sysctl.conf # 应用配置 sysctl -psomaxconn是ServerSocket.listen()的backlog参数上限若Java代码设serverSocket.listen(1024)但somaxconn128实际队列仍只有128tcp_tw_reuse1允许TIME_WAIT状态的socket被重用避免端口耗尽。7. 面试高频题深度拆解从“八股文”到源码级理解面试官问“TCP三次握手在Java里怎么体现”绝不是想听你背“SYN, SYN-ACK, ACK”。他想确认你是否真的懂Socket的生命周期。我们结合OpenJDK源码分析。7.1Socket.connect()的三次握手触发点Socket构造后调用connect()实际触发connect()系统调用Socket socket new Socket(); socket.connect(new InetSocketAddress(example.com, 80), 5000);在OpenJDK中Socket.connect()最终调用PlainSocketImpl.connect()其核心是// hotspot/src/share/native/java/net/PlainSocketImpl.c JNIEXPORT void JNICALL Java_java_net_PlainSocketImpl_connect(JNIEnv *env, jobject this, jobject address, jint port) { // ... 参数校验 // 创建socket fd fd JVM_Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0); // 设置非阻塞若timeout0 if (timeout 0) { setNonBlocking(fd); } // 执行connect系统调用 result JVM_Connect(fd, (struct sockaddr *)addr, len); // 若timeout0且result-1则进入select等待 if (timeout 0 result -1) { waitForConnect(fd, timeout); } }关键点JVM_Connect()发起SYN包若对方未响应waitForConnect()用select()等待超时。这就是为什么设置connect()超时会影响三次握手感知——超时时间内没收到SYN-ACK连接失败。7.2Socket.close()与四次挥手的时机Socket.close()会触发shutdown()系统调用发送FIN包public void close() throws IOException { synchronized (closeLock) { if (isClosed()) return; if (created) impl.close(); // 调用底层close() closed true; } }impl.close()最终调用JVM_SocketClose()在Linux下执行close(fd)内核自动发送FIN。但注意close()只保证发送FIN不等待对方ACK。四次挥手的完成依赖于内核协议栈Java层无法控制。7.3 “为什么Socket不是线程安全的”——从InputStream
返回列表