尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
手写Java版TCP文件上传下载工具:从三次握手到Socket编程实战
做Java开发这些年只要涉及到网络功能文件上传下载就永远是需求清单里的常客。无论是内部系统里的附件上传、定时备份服务器日志还是两台机器之间搬运几个G的离线包本质上都在做同一件事把文件通过TCP从一个进程搬到另一个进程。这篇文章我就用最土的办法手写一个Java版TCP文件上传下载工具全程自带源码顺便把三次握手、四次挥手用大白话讲明白。写完之后你会发现网络编程并没有那么玄乎难点就藏在那些看起来不起眼的细节里。1. 先说结论为什么文件传输绕不开TCP1.1 TCP和UDP之间的快速对比先给刚接触网络编程的朋友打个底。传输层有两个协议TCP和UDP它俩的脾气完全不同。TCP是面向连接的、可靠的、字节流式的协议UDP则是无连接的、不可靠的、报文式的协议。你可能会觉得有点抽象我直接拿文件传输的场景说。假设我要把一个压缩包发给另一台机器这个包在网络上传输的时候被拆成很多个小段数据包。这些包可能从不同的路径走可能顺序乱了也可能丢了。TCP做的事情就是把顺序排好、把丢了的补发、把重复的去掉最终让接收方拿到一份和我发送时完全一致的字节序列。UDP呢它只负责把每个独立报文扔到网上不保证到没到、乱没乱视频通话、游戏实时对战这类场景很适合因为它不在乎个别丢帧要的是快。文件传输恰恰是那种丢一个字节都没法接受的场景。一个压缩包损坏了解压就失败一个图片文件被改写一个字节颜色就花掉。所以项目里我几乎没犹豫过凡是正经文件传输全走TCP。1.2 “字节流”这三个字是要命的TCP还有一个关键特性也最容易被人忽略它是字节流不是消息流。什么意思呢你调用OutputStream.write往TCP里写数据相当于把字节倒进一个巨大的水管接收方从InputStream.read里拿数据拿到的也是水流里的字节它并不知道这批字节是你第一次写还是第二次写。水管里的水和水分界线之间没有天然的“标签”。这在后面实现文件上传下载时会引发一堆问题比如粘包、半包我会专门拿出一章来聊。你现在只需要记住一句话TCP传输里凡是需要区分“哪里是开头、哪里是结尾”的事情都得由应用程序自己定协议TCP不管这个。这也是为什么直接拿Socket读文件经常翻车的原因。2. 三次握手我用电话场景给你讲明白2.1 一次握手和两次握手为什么不行在Java里你创建Socket连接时底层会自动完成一次“三次握手”的过程。这个过程对程序员是透明的但去大厂面试时却没人会放过你。我先问一个问题为什么非要是三次两次不行吗拿打电话做比喻。甲拨号给乙乙接了说“喂听得到吗”甲说“听得到”这一段对话结束彼此才敢正式开始说正事。如果只有两次握手那就是甲说“喂”乙回“喂我也在”然后乙就认为连接建立成功了但甲可能根本没听见或者甲的呼叫是很多年前残留的“回音”这种对话谁能放心专业一点讲两次握手会造成一个问题网络中存在滞留的旧连接请求。假设客户端发了一个SYN包因为网络拥堵这个包在某个路由器里睡了一觉客户端认为连接失败重新发了一个新的SYN。如果两次握手服务端只要回了ACK就把连接建立到内存里可后来那个老SYN又漂过来了服务端再回一个ACK又建立了一个毫无意义的连接白白浪费系统资源。第三次握手的作用就是让服务端确认“客户端确实处于可用状态它真的打算和我建立连接”。2.2 三次握手每一步在干嘛我用最生活化的场景拆解一下。假设你要找同事对接工作你走到对方工位说第一步你客户端说“在吗我是小王我要跟你对接文件。”发送SYN表示同步请求捎带自己的初始序列号 第二步同事服务端抬头说“在的在的我听见了我能听到你说话吗”回复SYN-ACK既确认你的请求也请求确认他的能力 第三步你回答“能听到咱们开始对接吧。”发送ACK这三段对话完成双方都知道“我能听到你你也能听到我”连接正式建立。网络上把这个过程叫三次握手关键词分别是SYN、SYN-ACK、ACK。我当年刚学的时候就把攻击计划、驳回、确认想象成一问一答一下子记住了。三次握手完成之后客户端和服务端才各自进入ESTABLISHED状态应用层的read/write才能真正开始传输数据。放到文件上传下载里就是双方都确认“链路可靠”之后才开始搬文件。2.3 握手过程对文件传输意味着什么有些人觉得握手是操作系统的事跟我的代码没关系。错了。你的Socket.connect()能不能顺利返回取决于这次握手成不成功。如果对端IP根本不可达或是防火墙把端口丢了握手会反复超时你看到的现象就是客户端卡在connect()或accept()半天没反应。有个细节我提过很多次服务端在listen队列里会为还没完成握手的连接准备半连接队列等第三次握手完成连接才挪进全连接队列accept()才会返回一个可用的Socket。这个队列大小在某些系统上是有限制的连接多而accept得慢时新的连接会被拒绝客户端甩出“Connection refused”。后面我会讲到用线程池并发处理客户端就是为了避免“accept不过来了”。3. 四次挥手其实就是一次认真告别3.1 为什么告别比认识更麻烦三次握手是建立连接四次挥手是断开连接。你可能会问建立只需要三次断开为什么非要四次这里的关键在于TCP连接是双向的客户端能向服务端写数据服务端也能向客户端写数据两个方向是独立的管道。我依然用同事对接工作的比喻。文件传完了你们要散场。假设你是客户端先发起结束第一挥手你说“我这边事情处理完了我不再发文件了我要挂了。”发送FIN表示主动关闭写方向 第二挥手同事回复“收到我这边还有几个字写给你你别急着挂。”回复ACK确认收到你的结束请求但他还要继续发数据 等同事把那几个字发完他再执行第三挥手同事说“好了我这边也写完了我也要挂了。”发送FIN表示他的方向也关闭 第四挥手你回一句“收到再见了。”发送ACK到这一步双方的收发管道才全部关闭连接断开。你会发现中间过程中服务端的“确认收到”和“我也想关闭”是分开的两个包所以比握手多了一次。这也是网上经常看到的图解FIN、ACK、FIN、ACK。3.2 为什么不是三次挥手以及半关闭是什么刚才说服务端由于还有数据要发所以必须先把ACK发出去等数据发完再发FIN这就导致多了这一次。如果服务端恰好没有任何数据要补发理论上可以把ACK和FIN合并成一个包再等客户端回ACK那样看起来也像三次挥手。实际场景里服务端可能还需要写日志、回执、或者正在把缓存的数据冲刷出网卡不一定能立刻关闭写方向所以标准流程老老实实是四次。半关闭这个概念也值得提一句。TCP允许一端表达“我不再发送但我可以一直读”这就叫半关闭。Java里的Socket.shutdownOutput()就是这样一个动作它向对端发送FIN但Socket本身没有关闭你还能继续读对端发来的数据。我做文件下载时特别喜欢用这个方法因为可以让客户端在把自己的请求发送完之后优雅地告诉服务端“我不再向上写了你把我需要的数据回给我”而连接本身仍然活着。3.3 TIME_WAIT为什么要等两个MSL四次挥手里主动关闭方最后会进入一个叫TIME_WAIT的状态等待2MSL最大报文段生存时间节两倍后才彻底关闭端口。通俗理解就是主动挂电话的人不能一挂了就马上把听筒扔得远远的他得再等一会儿确认对方确实收到自己最后那句“再见”了。2MSL的存在有两个原因一是怕最后一个ACK丢包对方会反复重发FIN你等一会儿就能用同一个ACK再回应二是怕网络上还有之前的数据包残留在途中等待这段时间可以让它们彻底消失避免残余数据串到下次复用同一端口的连接里。所以做网络服务时你会看到主动关闭连接的一侧端口经常陷入TIME_WAIT这属于正常现象。只有TIME_WAIT积累过多才会导致端口不足这是后面踩坑章节要聊的话题。4. Java网络编程的地基Socket 与 ServerSocket 怎么分工4.1 服务端和客户端的最小骨架现在开始进入代码环节。先看最基础的两段代码理解了这两个类后面的文件传输完全就是在这之上加读写逻辑。服务端通常用ServerSocket它的任务是监听某个端口等着客户端来连。最基本的形态是这样的import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TcpServerDemo { public static void main(String[] args) throws IOException { // 监听本机9000端口申请建立TCP服务 ServerSocket server new ServerSocket(9000); System.out.println(服务端已启动等待客户端连接...); // accept()是阻塞方法会一直等到有客户端接入 Socket socket server.accept(); System.out.println(有一个客户端连进来了 socket.getRemoteSocketAddress()); // 先不写具体读写逻辑拿到Socket就代表连接建立了 socket.close(); server.close(); } }客户端用Socket类指定服务端IP和端口去发起连接这一瞬间就会触发三次握手import java.net.Socket; public class TcpClientDemo { public static void main(String[] args) throws Exception { // localhost替换成服务端IP即可9000端口要和服务端一致 Socket socket new Socket(localhost, 9000); System.out.println(连接建立成功 socket.getRemoteSocketAddress()); socket.close(); } }你可以把ServerSocket想象成前台总机它自己不管具体对话只是接电话然后分配一个Socket给你而客户端那个Socket是直接打进总机后分配给你的一条专线。服务端accept()一次只能接一个如果想让多个客户端同时连就得不断调用accept()为每个客户端单独开一个线程或者用线程池。4.2 InputStream和OutputStream的直觉Java里网络数据传输全部走IO流。Socket给你两个对象getInputStream()用来读从远端传来的数据getOutputStream()用来把数据写到远端。它们是双向管道所以你可以一边读一边写这也是实现文件传输的基础。但有一个极其重要的心智模型要建立read()返回-1代表对方关闭了输出流不代表这次一定能读到你想读的数据。文件传输时如果服务端发了一个文件内容客户端需要判断“数据发完没有”。对端不主动关闭连接的话read()会一直阻塞在等待数据的地方程序就挂在那儿了。所以后面设计文件协议时我习惯先给接收方一个明确的长度或者干脆让发送方在发完数据后主动关闭写方向用EOF信号通知对端。两种做法都行但要分清场景乱混就会踩坑。5. 文件上传完整实现把客户端的文件搬到服务器5.1 第一步先把协议想清楚写网络程序最忌讳的是拿到需求就开写。我习惯先想清楚数据怎么表示边界怎么确定服务端怎么知道文件名怎么直到文件传完了我选择一种非常简单但可靠的协议客户端先发一个文件名字符串再发一个文件长度最后发文件内容。服务端根据文件名创建本地文件再循环读取指定长度的字节写入文件。文件名的编码用DataOutputStream.writeUTF()完成长度用writeLong()写入8字节内容用write(byte[])批量发送。这样服务端的读取逻辑非常清晰先读UTF拿文件名再读Long拿文件长度然后循环读直到读满该长度。为什么不直接让发送方发完就shutdownOutput()靠EOF判断结束这个方案单次上传是没问题的但如果连接要复用做后续其他指令提前EOF等于断开了链路。用长度字段是更通用的做法也为后面扩展多文件、断点续传留了余地。5.2 服务端接收文件源码下面是服务端接收文件的实现。我用的是每个客户端来一个就开一个线程的方式虽然简单粗暴但演示非常直观。正式项目里建议换线程池。import java.io.DataInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class FileUploadServer { public static void main(String[] args) throws IOException { ServerSocket server new ServerSocket(9000); System.out.println(上传服务端启动监听端口 9000); while (true) { Socket socket server.accept(); // 每个客户端单独一个线程处理避免一个慢客户端阻塞其他连接 new Thread(() - receiveFile(socket)).start(); } } private static void receiveFile(Socket socket) { // try-with-resources 确保流和Socket最终都会关闭 try (DataInputStream dis new DataInputStream(socket.getInputStream()); Socket s socket) { // 按协议顺序读取文件名 - 文件长度 - 文件内容 String fileName dis.readUTF(); long fileLength dis.readLong(); // 为了演示收到文件名直接在原文件名前面加 prefix 避免覆盖本机文件 String savePath upload_ fileName; System.out.println(开始接收文件 fileName 大小 fileLength 字节); try (FileOutputStream fos new FileOutputStream(savePath)) { byte[] buffer new byte[8192]; long remain fileLength; while (remain 0) { int len dis.read(buffer, 0, (int) Math.min(buffer.length, remain)); if (len -1) { // 客户端数据没发完就断开说明异常 throw new IOException(连接提前断开文件接收不完整); } fos.write(buffer, 0, len); remain - len; } fos.flush(); } System.out.println(文件接收完成 savePath); } catch (IOException e) { e.printStackTrace(); } } }这段代码里最值得注意的是循环读取。当初我犯过一个错误以为dis.read(buffer)一次就能把剩余数据全部读出来。文件小的时候运气好一两次就读完了文件一大断断续续的就会读到null或不足。因为底层是字节流read()每次返回多少是不确定的必须把“剩余多少”和“每次读入多少”对应起来。所以我限制每次最多读remain长度的数据绝不多读一个字节保证服务端发完这个文件之后下一次继续读还能对齐后续协议。5.3 客户端发送文件源码客户端就简单多了打开本地文件把文件名、长度、内容按顺序写进输出流import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; import java.net.Socket; public class FileUploadClient { public static void main(String[] args) throws Exception { File file new File(test.docx); if (!file.exists()) { System.out.println(本地文件不存在 file.getAbsolutePath()); return; } try (Socket socket new Socket(localhost, 9000); DataOutputStream dos new DataOutputStream(socket.getOutputStream())) { // 协议头文件名 文件长度 dos.writeUTF(file.getName()); dos.writeLong(file.length()); // 文件内容 try (FileInputStream fis new FileInputStream(file)) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { dos.write(buffer, 0, len); } dos.flush(); } System.out.println(文件发送完成 file.getName()); } } }这里我没有调用socket.close()之前先shutdownOutput()因为协议已经通过长度告诉对端读多少了不需要用EOF传递结束信号。写完数据flush后try-with-resources会自动关闭Socket此时连接进入四次挥手流程。5.4 运行效果和缓冲区选择在服务端机器跑Server客户端跑Client控制台应该依次出现服务端打印“上传服务端启动监听端口 9000”客户端打印“文件发送完成test.docx”服务端打印“开始接收文件test.docx大小… 字节”和“文件接收完成upload_test.docx”缓冲区我选8192字节8KB这是个经验值。太小比如256字节会导致网络包数量爆炸CPU和IO开销猛增太大比如1MB单次申请内存浪费而且read返回可能不会填满那么大。8KB到64KB都是常见区间大部分普通场景8KB就够了。如果你在局域网传大文件换成64KB实测吞吐会更高但提升不是无限制的最终会被系统收发缓冲区和网卡性能卡住。6. 文件下载完整实现角色对调思路复用6.1 下载协议的设计下载本质上就是上传的反向操作。客户端发一个请求包告诉服务端“我想要哪个文件”服务端找到文件后回传内容。我设计的协议第一步客户端发送请求文件名writeUTF 第二步服务端用一个boolean表示文件是否存在 第三步如果存在服务端再writeLong发送文件长度 第四步服务端把文件内容源源不断写回客户端。客户端拿到boolean为false就直接结束不用再继续读长度和数据。这个设计比“不管有没有都发”更健壮也避免客户端把错误提示当成文件内容写进本地。6.2 服务端回传文件源码import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class FileDownloadServer { public static void main(String[] args) throws IOException { ServerSocket server new ServerSocket(9001); System.out.println(下载服务端启动监听端口 9001); while (true) { Socket socket server.accept(); new Thread(() - sendFile(socket)).start(); } } private static void sendFile(Socket socket) { try (DataInputStream dis new DataInputStream(socket.getInputStream()); DataOutputStream dos new DataOutputStream(socket.getOutputStream()); Socket s socket) { String fileName dis.readUTF(); System.out.println(收到下载请求 fileName); File file new File(fileName); if (!file.exists()) { dos.writeBoolean(false); dos.flush(); System.out.println(文件不存在已回复客户端 fileName); return; } dos.writeBoolean(true); dos.writeLong(file.length()); try (FileInputStream fis new FileInputStream(file)) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { dos.write(buffer, 0, len); } dos.flush(); } System.out.println(文件发送完成 fileName); } catch (IOException e) { e.printStackTrace(); } } }注意一个细节在写出boolean和Long之后我把flush()调用了一次。因为DataOutputStream会有缓冲不刷一下客户端可能迟迟读不到协议头里的结果造成“怎么卡住了”的假象。网络程序里凡是写完关键控制信息比如长度、状态码都要考虑flush时机。6.3 客户端接收文件源码import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileOutputStream; import java.net.Socket; public class FileDownloadClient { public static void main(String[] args) throws Exception { String target test.docx; try (Socket socket new Socket(localhost, 9001); DataInputStream dis new DataInputStream(socket.getInputStream()); DataOutputStream dos new DataOutputStream(socket.getOutputStream())) { // 1. 发送请求文件名 dos.writeUTF(target); dos.flush(); // 2. 判断文件是否存在 boolean exists dis.readBoolean(); if (!exists) { System.out.println(服务端不存在该文件 target); return; } // 3. 读取文件长度 long fileLength dis.readLong(); String savePath download_ target; // 4. 循环读取内容写入本地 try (FileOutputStream fos new FileOutputStream(savePath)) { byte[] buffer new byte[8192]; long remain fileLength; while (remain 0) { int len dis.read(buffer, 0, (int) Math.min(buffer.length, remain)); if (len -1) { throw new IOException(服务端数据发送中断); } fos.write(buffer, 0, len); remain - len; } fos.flush(); } System.out.println(文件下载成功 savePath); } } }在这个版本里客户端先行writeUTF请求文件名但并没有关闭写方向。因为服务端只需要读一个字符串就能判断没必要切断这个方向的通信。这里也体现出半关闭和“完整协议”的区别如果你用shutdownOutput()请求斯服务端读到EOF后也可以正常响应但有些代理和框架对半关闭支持不友好所以我通常只在一次性请求-响应模式里才用。运行下载Demo你会看到服务端打印“收到下载请求test.docx”和“文件发送完成test.docx”客户端打印“文件下载成功download_test.docx”。把前后两个文件对比一下如果字节数一样多半就没问题。7. 实测中绕不开的坑粘包、半包、连接关闭顺序7.1 粘包半包的本质很多人第一次做Socket通信都会遇到一个诡异的问题服务端收到的信息和我发的对不上。比如我用write(byte[])发了三次数据服务端read却只读到两次第一次读到的是前面两次数据的拼接体。这就是粘包。反过来我用write发了10KB服务端第一次read只读回来2KB第二次又读回来8KB这是半包。归根结底TCP是字节流没有任何“消息边界”。发送方可能连续写了三次系统在底层把这些字节合并成一个报文段一次性发给接收方接收方也可能因为网络延迟、缓冲区大小不同把一个报文段拆成多次read。这不是Bug是TCP的固有属性。解决思路永远是应用层自己界定边界。要么用固定长度定长帧要么用包头长度字段要么用特殊分隔符。我的文件传输方案用的就是第二种协议头写清楚文件名和文件长度接收方就知道应该按多少字节切分内容。7.2 连接关闭顺序的讲究我在调试下载功能时还踩过另一个坑客户端close()调用得早了服务端正在写文件结果抛出“Connection reset by peer”或“Broken pipe”。原因是我在客户端还没读完数据时就主动关闭了Socket服务端把数据写回一个已经关闭的管道自然就爆了。正确的关闭顺序是接收方确认没有更多数据可读再关闭发送方要么发完数据后调用shutdownOutput()发出EOF要么等接收方读完后再close。用shutdownOutput()有个好处它只关闭本端的发送方向不关闭本端的接收方向所以对端看到的是“数据流结束”而TCP双向连接中另一侧的通道还能继续使用。在一些对称连接里“读写关闭不同步”这个问题很典型你们以后如果写WebSocket、自定义RPC也会遇到同样的事。7.3 其他小坑再列几个我实际开发中频繁遇到的坑第一端口复用。频繁重启服务端时偶尔会报“Address already in use”。这是因为上一个连接TIME_WAIT还没结束端口还占着。ServerSocket可以设置setReuseAddress(true)让端口能够在TIME_WAIT状态下被再次绑定。第二单线程服务端。如果accept()之后直接在同一个线程里处理文件收发一个客户端慢慢传其他客户端全部排队等。文件传输是老长流程操作必须交给线程池或者其他异步模型处理。第三不做超时控制。文件传输时如果对端半路宕机read()可能一直阻塞在那线程就挂死了。给Socket设置setSoTimeout()能避免请求过长但注意设了超时后正常大数据读取也可能被超时打断所以超时时间要大于单次IO的合理耗时。第四传输凭证。演示代码只验证了文件名长度没有做留档和完整性校验真实环境一定要把文件名、目标路径做白名单过滤别直接拿客户端传的名字写盘防止路径穿越之类的问题。8. 完整源码整合与后续扩展方向8.1 整个项目的代码结构关心代码怎么组织的可以按这个目录看uploadFileUploadServer.java、FileUploadClient.javadownloadFileDownloadServer.java、FileDownloadClient.java两个Server都监听不同端口我用的是9000和9001跑在同一个主机上没有任何冲突。正式使用时你也可以用一个Server类同时提供上传和下载接口让客户端先发一个指令编码比如1表示上传2表示下载再读文件名。这也是很多RPC框架的雏形。完整跑通这两个Demo后我再建议你做一个加强练习把创建线程那步改成ExecutorService线程池避免无限new Thread导致线程爆炸同时把每次接收到的文件用SHA-256做个校验和客户端发送前和服务端接收后都算一遍两个值一致才认为传输成功。这一步加完后才算是一个能上生产环境的文件传输工具。8.2 后续扩展方向文件传输这个看似简单的话题往下挖是无底洞。我给你们几个自己和团队用过的方向断点续传客户端先问服务端“本地文件有多大了”服务端从对应偏移量处接着发文件头加一个offset字段内容从现在的位置开始读。多文件批量传输用压缩包把多个小文件zip成一个再传或者用ZipOutputStream边压边写避免传输海量小文件时握手开销太大。用NIO替代传统BIOJava的FileChannel和SocketChannel配合做得好的话能极大降低线程数吞吐也更高。Netty这类框架就是在这个基础上做封装。文件量特别大的时候stream.read和socket.read都是阻塞的线程会卡在IO上NIO写起来复杂但性能差异很明显。数据加密如果传输内容敏感建议在TCP之上再套TLSSSLServerSocket / SSLSocket或者传之前用AES加密文件内容密钥走另外的通道传递。网络层裸奔传文件在内网还好跨公网很容易被截获。以我的经验文件传输的骨架很容易搭难的是让它在真实网络环境下稳定工作包括超时重试、并发限制、容量监控、落盘清理。你在本地回环跑这个Demo粘包半包未必会复现但放到真实网络环境复杂问题一个都不会少。把这些边缘情况都处理过一遍你才算真正理解了TCP传输而不只是会用Socket API。
RELATED

相关推荐

AI辅助写专著全流程指南:从大纲设计到修稿校对的五段式实战方法

AI辅助写专著全流程指南:从大纲设计到修稿校对的五段式实战方法

大家写书,特别是写专著,最大的痛点从来不是“没话说”,而是“东西太多、战线太长、改到崩溃”。我从去年开始用AI辅助写一本专业领域的专著,到今天书稿已经进入终审阶段,整个过程踩过不少坑,也摸索出一套能…

📅 2026/10/19 13:48:57
PyTorch ReduceLROnPlateau调度器实战:原理、参数与踩坑指南

PyTorch ReduceLROnPlateau调度器实战:原理、参数与踩坑指南

前阵子训练一个短文本分类模型,loss在第八个epoch开始变得极其顽固,从2.3降到1.9之后就不再动了。手动把学习率从1e-3改成1e-4,loss又往前走了一截,再隔几个epoch再次卡住。我那时候就在想,能不能让训练过程自己感知到…

📅 2026/10/19 13:48:57
信息论与程序员:用Python看懂熵、交叉熵与KL散度

信息论与程序员:用Python看懂熵、交叉熵与KL散度

提到信息论,很多程序员第一反应是那些从天而降的数学公式。毕竟这门学科的奠基人写出那篇通信理论论文时,计算机还没走进公司。但今天的压缩算法、搜索引擎排序、深度学习里的交叉熵损失、异常检测,全都在信息论的辐射范围内。作为一个写了十…

📅 2026/10/19 13:48:57
MORE NEWS

更多资讯

📰

Node版本不兼容怎么办?nvm-windows切换实战指南

上个月我打开一个维护了大半年的前端项目,习惯性在终端敲下 npm run dev ,结果迎面涌出一片黄色警告:某个依赖声明只支持 Node 20 以下版本,而当前环境已经是 22.x。页面倒是勉强起来了,但样式全乱,按钮间…

📰

RHEL 8配置GPU加速实时推荐系统:驱动、容器与推理调优

前阵子在帮一个做电商推荐的项目做性能调优,线上服务的 CPU 一直处于高位,用户反馈推荐结果出得太慢。我们分析后发现,问题出在排序模型上——一个深度神经网络,在 CPU 上跑一次推理要十几毫秒,随着请求量上涨&#xf…

📰

开源CLI代码审查工作流:Git+LLM自动化审计实践

1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码审查工作流open-code-review 这个名字乍看像某个具体软件,但实际它代表的是一类正在快速成型的新型开发实践——用开源、可审计、可定制的 CLI 工具链,把大语言模型&…

📰

open-code-review:面向人机协同的开放代码审查范式

1. 项目概述:这不是又一个代码审查工具,而是一次对“人如何协作写代码”的重新定义“open-code-review”这个名称乍看平平无奇,甚至有点像某个被遗忘在 GitHub 某个角落的冷门仓库名。但如果你最近翻过几篇关于 LLM Agent 在软件工程中落地的…

📰

机器学习导论习题解答:概念、推导与算法流程的解题方法论

1. 从“做题”到“解题”:这套习题解答到底在解什么 先说个现象:很多人学机器学习导论,教材翻完一遍感觉全会,一合上书做题就卡壳。卡壳的还不是代码,而是概念题和推导题。比如“为什么朴素贝叶斯要假设特征独立”“正…

📰

vLLM自定义模型推理支持完整指南:从源码注册到权重映射

从零开始为 vLLM 添加一个自定义模型的推理支持,这件事听起来有点硬核,但实际上只要把 vLLM 的模型注册机制、权重加载流程、配置兼容性这三条线摸清楚,整个工程量是可控的。我最近因为项目需要,把一个社区里非常小众的开源对话模…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬