<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Itzlenny&apos;s Blog</title><description>itzlennyface的博客</description><link>https://lennyface24.github.io/</link><language>zh_CN</language><item><title>现在刷新页面有随机背景图啦~</title><link>https://lennyface24.github.io/posts/2026-07-23-random-banner/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/2026-07-23-random-banner/</guid><description>随机背景图</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;接入了两个免费api，如果请求失败默认走设置好的背景图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;0fd8423f5246601e13ea9d6b1a09d5ae.gif&quot; alt=&quot;随机背景图效果演示&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>PageCMS测试</title><link>https://lennyface24.github.io/posts/2026-07-18-pagecms/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/2026-07-18-pagecms/</guid><description>PageCMS测试</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;这是啥？&lt;/h3&gt;
&lt;p&gt;PageCMS 是面向静态站点、静态博客设计的 SaaS 可视化内容管理平台，核心用于托管、管理博客文档、图文内容等静态资源，适配基于 GitHub 源码托管的静态博客&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;PageCMS 依靠静态项目根目录下的 &lt;code&gt;.page.yml&lt;/code&gt; 配置文件，以声明式语法完成全部功能定义，例如以下配置项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;博文 Markdown 文件存储目录（如 &lt;code&gt;source/_posts&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;博客图片、静态素材存放的资源目录；&lt;/li&gt;
&lt;li&gt;绑定仓库的 GitHub Actions CI/CD 工作流；&lt;/li&gt;
&lt;li&gt;在线页面内触发站点重新构建的交互按钮行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成声明后，平台会通过 GitHub 官方 API 读写仓库文件、触发构建流程，替代本地传统发文流程，我们不再需要手动本地启动本地的 VS Code、打开项目源码文件夹，手动新建博文、创建分类目录、导入配图等可脚本化的重复操作，这一切全部可在在线平台&lt;a href=&quot;https://app.pagescms.org/&quot;&gt;https://app.pagescms.org/&lt;/a&gt; 里全部自动化完成&lt;/p&gt;
&lt;h3&gt;后记&lt;/h3&gt;
&lt;p&gt;本来在为怎么能够更加方便的为我的博客写文章而头疼，打算为博客添加一个后台界面，添加一个撰写博客的功能页面，但是考虑到安全问题，在去部署worker的路上，恰巧发现了PageCMS这个东西，立刻就部署上了&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
</content:encoded></item><item><title>博客将迎来新的更新</title><link>https://lennyface24.github.io/posts/daili-2026-05-31/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/daili-2026-05-31/</guid><description>博客功能更新</description><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为博客添加了新的模块&lt;strong&gt;工具&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前只有封面生成器这一工具，不过未来会添加更多工具&lt;/p&gt;
&lt;p&gt;比如：二维码生成器、pdf转换器等&lt;/p&gt;
&lt;p&gt;目的是方便自己不用去收藏各种网站，我直接把这些工具整合到一个页面不就好了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;图片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;html转png使用 &lt;a href=&quot;https://github.com/bubkoo/html-to-image&quot;&gt;https://github.com/bubkoo/html-to-image&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;整体项目代码参考 &lt;a href=&quot;https://github.com/afoim/easy_cover&quot;&gt;https://github.com/afoim/easy_cover&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>go学习</title><link>https://lennyface24.github.io/posts/daily-2026-05-18/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/daily-2026-05-18/</guid><description>GOGOGO LETSGO</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;感觉我学习语言的方式不对&lt;/p&gt;
&lt;p&gt;我只是简单的在用语言都有的功能进行开发， 但是没有去了解语言的很多特性&lt;/p&gt;
&lt;p&gt;虽然可能会有点功利，但是是不是对面试不太好🥲🥲&lt;/p&gt;
&lt;p&gt;goroutine 协程的底层原理&lt;/p&gt;
&lt;p&gt;原来的线程：由内核创建，由操作系统分配cpu时间片的最小单位&lt;/p&gt;
&lt;p&gt;一个线程要有用户栈，内核栈，TCB（线程控制块，保存线程状态：阻塞、运行、就绪，优先级，CPU上下文的数据结构），通常1mb&lt;/p&gt;
&lt;p&gt;内核栈保存寄存器状态&lt;/p&gt;
&lt;p&gt;sync.Mutex 互斥锁，就是锁
sync.RWMutex 读写锁，读可并发，写不行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
	mu sync.Mutex
	mu2 sync.RWMutex
	wp sync.WaitGroup
	
)
func main(){
	wp.Add(1)
	go func(){
		defer wp.Done()
		log.Println(&quot;1&quot;)
	}
	
	mu.Lock() // 互斥锁
	mu2.Lock() // 写锁
	mu2.RLock() // 读锁 
	
	mu.UnLock() // 互斥锁
	mu2.UnLock() // 写锁
	mu2.RUnLock() // 读锁 
	
	wp.Wait()
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;channel：用通信的方式共享内存，而不是用共享的内存来通信&lt;/p&gt;
&lt;p&gt;go context底层原理&lt;/p&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt;的底层设计，我们可以概括为1个接口，4种实现与6个方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;1 个接口&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context 规定了&lt;code&gt;context&lt;/code&gt;的四个基本方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;4 种实现&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;emptyCtx 实现了一个空的&lt;code&gt;context&lt;/code&gt;，可以用作根节点&lt;/li&gt;
&lt;li&gt;cancelCtx 实现一个带&lt;code&gt;cancel&lt;/code&gt;功能的&lt;code&gt;context&lt;/code&gt;，可以主动取消&lt;/li&gt;
&lt;li&gt;timerCtx 实现一个通过定时器&lt;code&gt;timer&lt;/code&gt;和截止时间&lt;code&gt;deadline&lt;/code&gt;定时取消的&lt;code&gt;context&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;valueCtx 实现一个可以通过 &lt;code&gt;key、val&lt;/code&gt; 两个字段来存数据的&lt;code&gt;context&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;6 个方法&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Background 返回一个&lt;code&gt;emptyCtx&lt;/code&gt;作为根节点&lt;/li&gt;
&lt;li&gt;TODO 返回一个&lt;code&gt;emptyCtx&lt;/code&gt;作为未知节点&lt;/li&gt;
&lt;li&gt;WithCancel 返回一个&lt;code&gt;cancelCtx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WithDeadline 返回一个&lt;code&gt;timerCtx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WithTimeout 返回一个&lt;code&gt;timerCtx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WithValue 返回一个&lt;code&gt;valueCtx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;chennel源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type hchan struct {
    qcount   uint           // 队列中现存的元素总数
    dataqsiz uint           // 环形队列的容量（即缓冲区大小）
    buf      unsafe.Pointer // 指向底层连续内存数组的指针（存放数据的地方）
    elemsize uint16         // 元素的大小（字节数，比如 int64 就是 8 字节）
    closed   uint32         // 标识通道是否关闭（0: 打开, 1: 关闭）
    elemtype *_type        // 元素类型（用于反射和类型安全检查）
    
    sendx    uint           // 缓冲区发送索引（数组下标，下次写数据的位置）
    recvx    uint           // 缓冲区接收索引（数组下标，下次读数据的位置）

    recvq    waitq          // 阻塞在这里等待【接收】数据的协程链表（sudog 链表）
    sendq    waitq          // 阻塞在这里等待【发送】数据的协程链表（sudog 链表）

    lock     mutex          // 互斥锁！保护上面所有字段的并发安全
}

func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
    // 1. 如果 channel 是空的（nil）
    if c == nil {
        if !block {
            return false
        }
        // 如果是阻塞发送，直接让当前协程永久躺倒（挂起）
        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoBlockSend, 2)
        throw(&quot;unreachable&quot;)
    }

    // 2. 核心操作开始：第一步永远是【加锁】
    lock(&amp;amp;c.lock)

    // 3. 如果 channel 已经关闭，加锁状态下抛出 panic
    if c.closed != 0 {
        unlock(&amp;amp;c.lock)
        panic(plainError(&quot;send on closed channel&quot;))
    }

    // 4. 【情况一：有现成的接收者在等数据】（无锁零拷贝优化）
    // 从 recvq 队列里弹出一个正在死等数据的协程（sudog）
    if sg := c.recvq.dequeue(); sg != nil {
        // 找到了等待的接收者！直接把数据拷贝到它的栈内存里，并唤醒它
        // 这个过程不需要经过 c.buf 缓冲区中转
        send(c, sg, ep, func() { unlock(&amp;amp;c.lock) }, 3)
        return true
    }

    // 5. 【情况二：缓冲区还没满】
    if c.qcount &amp;lt; c.dataqsiz {
        // 还有空位，把数据放进环形数组 c.buf
        qp := chanbuf(c, c.sendx)          // 找到当前发送索引对应的内存地址
        typedmemmove(c.elemtype, qp, ep)   // 将数据 ep 拷贝到 qp 指向的缓冲区
        c.sendx++                          // 发送下标往后移
        if c.sendx == c.dataqsiz {         // 环形队列走到末尾，折返回头部
            c.sendx = 0
        }
        c.qcount++                         // 队列元素计数 +1
        
        unlock(&amp;amp;c.lock)                    // 【解锁】，发送成功返回
        return true
    }

    // 如果是异步非阻塞发送（例如 select 里的 send），缓冲区满了就直接解锁返回 false
    if !block {
        unlock(&amp;amp;c.lock)
        return false
    }

    // 6. 【情况三：缓冲区满了，且无人在等数据】（协程挂起）
    gp := getg()                       // 获取当前的协程对象（Goroutine）
    mysg := acquireSudog()             // 从对象池里打包一个临时的登记节点（sudog）
    mysg.elem = ep                     // 把你要发送的数据地址挂在节点上
    mysg.g = gp                        // 节点绑定当前协程
    mysg.c = c                         // 节点绑定当前 channel
    
    c.sendq.enqueue(mysg)              // 把这个节点推进 channel 的发送等待链表

    // 【关键动作】：调用 gopark 释放锁，并将当前协程转入等待状态
    // 从这一刻起，当前协程不占用 CPU，直到别人读取数据将其唤醒
    gopark(chanparkcommit, unsafe.Pointer(&amp;amp;c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

    // ———————— 被唤醒后的分割线 ————————
    // 当另一个协程读取了数据，它会调用 goready() 重新激活这个协程，代码从这里继续往下走
    
    releaseSudog(mysg)                 // 释放临时登记节点
    return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;channel源码总结
chennel send：
1.消息进入之后，先判断channel是否存在 (&lt;em&gt;hcan struct&lt;/em&gt;)
如果没有的话，判断是否是异步非阻塞
如果不是，直接返回，没有channel也就不需要进行逻辑了
是，则将该协程永久挂起（这里如果实在main里面使用无缓冲channel，会将main协程 （是的，main也是协程）直接挂起，由于现在main协程都没了，runtime轮询查找可用的协程，但是找不到，只剩堆里面一个channel在运作，直接判断死锁）
之后先加锁
如果channel关闭了，直接panic
如果有订阅者，就把消息发给这个人（FIFO 先到先得）
没有订阅者，缓冲区还没满，写入缓冲区
缓冲区满了，挂起当前协程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 运行时的底层判定逻辑（伪代码）
 func checkdead() { run := sched.runnablecnt // 当前可运行的协程数量 
	 if run &amp;gt; 0 { return // 还有人能跑，不算死锁 
	 } // 关键账目对比： // nmidlelocked 是被锁住的线程，sysmon 是系统监控 // 除去这些后，看看全天下总共还有多少个活着的协程（包括睡着的） 
	 aliveCount := runtime.gcount() if aliveCount == 0 { // 如果连睡着的协程都没有，说明程序是正常运行完自然退出的 
	 return } // 【致命判定】：如果活着的协程数量 &amp;gt; 0（比如 main 还在），但可运行的数量为 0 // 这意味着：这群活着的协程全部在睡觉，且断绝了所有被唤醒的可能！ 
	 panic(&quot;fatal error: all goroutines are asleep - deadlock!&quot;) 
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步阻塞：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch := make(chan int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同步非阻塞：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select{
case data &amp;lt;- ch:
      printlf(&quot;发送成功&quot;)
default:
      println(&quot;干一些别的事情&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异步非阻塞：
网络IO调用，我在等待数据的时候，把使用数据的回调函数注册，数据到了自动调用留下的回调函数&lt;/p&gt;
&lt;p&gt;随学随写了（&lt;/p&gt;
</content:encoded></item><item><title>更新</title><link>https://lennyface24.github.io/posts/daily-2026-05-17/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/daily-2026-05-17/</guid><description>记录一下</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;突然想要多写点博客记录下内容&lt;/p&gt;
&lt;p&gt;最近正好开始写每天的学习笔记，那就每天水水博客吧&lt;/p&gt;
&lt;p&gt;要不然我这个博客挂着也是挂着&lt;/p&gt;
</content:encoded></item><item><title>Vibe Coding实现了一个AI聊天小插件..</title><link>https://lennyface24.github.io/posts/extention-2026-04-04/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/extention-2026-04-04/</guid><description>Vibe Coding实现了一个AI聊天小插件..</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vibe Coding实现了一个AI聊天小插件..&lt;/h1&gt;
&lt;p&gt;不知道大家有没有遇到过这样的情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;向ai提问并阅读ai返回的内容时，可能我们看到一半，出现一个我们不认识的概念&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-1.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(Ada Lovelace是啥玩意？？)&lt;/p&gt;
&lt;p&gt;我们想知道这是啥，就得接着问ai&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-2.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一个问题还好，可是如果中间出现了多个我们看不懂得概念呢。。&lt;/p&gt;
&lt;p&gt;我们就得接着在这个窗口去问ai&lt;/p&gt;
&lt;p&gt;但是如果问题越来越多，我们看完了回答之后想要回到最开始的位置就不得不使劲的滚动鼠标，往上翻到最开始的聊天记录&lt;/p&gt;
&lt;p&gt;这非常的令人烦躁，而且可能我们要找的最开始的对话是中间的某次记录，这样的话更难定位了。。&lt;/p&gt;
&lt;p&gt;或许你会说，我一个问题一个开一个新的聊天窗口不就行了？&lt;/p&gt;
&lt;p&gt;但如果你开的窗口越来越多，你可能都找不到你原来的那个窗口所在的位置了。。。&lt;/p&gt;
&lt;p&gt;因为我之前使用ai的时候经常被这个困扰，所以我就vibe了一个插件，一个ai对话悬浮窗，可以随时向它提问&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-3.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就不必在意新的聊天记录会淹没最开始的记录了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-4.png&quot; alt=&quot;图片1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前只支持第三方接入
支持anthropic格式和openai格式
项目开源在github&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/LennyFace24/AskHelper&quot;&gt;https://github.com/LennyFace24/AskHelper&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java TCP通信简易实现</title><link>https://lennyface24.github.io/posts/java-2025-06-05/</link><guid isPermaLink="true">https://lennyface24.github.io/posts/java-2025-06-05/</guid><description>Java TCP通信简易实现</description><pubDate>Thu, 05 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java TCP通信简易实现&lt;/h1&gt;
&lt;h2&gt;客户端&lt;/h2&gt;
&lt;h5&gt;一，建立与服务端的连接&lt;/h5&gt;
&lt;p&gt;现在我们假设服务端已经在它的主机上的6666端口启动了服务，我们为了建立与它的连接，需要new一个socket对象，向服务端发起TCP连接请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 假设服务端在本地6666端口启动，
//              new Socket(IPAddress  ,port);
Socket socket = new Socket(&quot;127.0.0.1&quot;,6666);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好的，现在请求成功了，已经与服务端建立好了连接，接下来我们需要发送我们打算发送的数据&lt;/p&gt;
&lt;p&gt;于是我们需要一个输出流（OutputStream）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OutputStream os = socket.getOutputStream();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会问，&lt;/p&gt;
&lt;p&gt;Q：为什么不是自己new一个os呢？&lt;/p&gt;
&lt;p&gt;A：因为socket对象内部已经与服务器建立了底层的连接通道，它封装了网路的输入输出流，只有通过socket.getOutputStream() 获得的流对象，才真正连接到对方的接收端口。&lt;/p&gt;
&lt;p&gt;Q：那我不能把自己new的os对象手动建立连接吗？&lt;/p&gt;
&lt;p&gt;A：不能，os对象本身是一个抽象的数据流接口/抽象类，它只负责数据的“写出”功能，但它不具备“建立网络连接”这个能力。&lt;/p&gt;
&lt;p&gt;接下来我们又遇到一个问题，OutputStream对象只能写入原始字节，不能直接写 int、float、String 等数据类型，也就是byte类型，所以我们要进行包装，方便数据的写入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DataOutputStream dos = new DataOutputStream(os);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;太好了，这下我们可以进行数据的写入了！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
dos.writeUTF(msg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整合代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class TCPClientNote {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket(&quot;127.0.0.1&quot;,6666);
        OutputStream os = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);
        String msg = sc.nextLine();
        dos.writeUTF(msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是现在有一个问题，我们发完一次数据好像我的客户端就结束了，所以为了可以不断地发送信息，我们可以加一个循环嵌套：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
	Scanner sc = new Scanner(System.in);
    String msg = sc.nextLine();	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再加一个退出条件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
	Scanner sc = new Scanner(System.in);
    String msg = sc.nextLine();
 	if(msg == &quot;exit&quot;){
        // 先关闭 dos流，再关闭 socket连接
        dos.close();
       	socket.close();
        break;
    }
    dos.writeUTF(msg);
    // 强制把缓冲区中的消息发送出去，避免数据停留在内存里
	// DataOutputStream 内部有缓冲机制，调用 writeUTF() 后，数据可能暂时存放在内存缓冲区，还没真正	   
    // 通过网络发送出去。
    dos.flush();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class TCPClientNote {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket(&quot;127.0.0.1&quot;,6666);
        OutputStream os = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);

		while(true){
			Scanner sc = new Scanner(System.in);
   			String msg = sc.nextLine();
 			if(msg == &quot;exit&quot;){
        		dos.close();
       			socket.close();
       	 	    break;
    		}
    		dos.writeUTF(msg);
    		dos.flush();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;服务端：&lt;/h2&gt;
&lt;h5&gt;一，连接客户端&lt;/h5&gt;
&lt;p&gt;new 一个Serversocket对象，用于与客户端进行连接，需要指定端口，用于监听。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int port = 6666;
Serversocket ss = new Serversocket(port);
// Serversocket ss = new Serversocket(6666);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了与来进行连接的客户端进行连接，有以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Socket socket = ss.accept();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码会在此处阻塞等待，直到有客户端连接。&lt;/p&gt;
&lt;p&gt;ss.accept() 在与客户端连接之后会返回一个socket对象，通过创建socket变量并接收这个连接的对象，用于与客户端通信，读取客户端发送的数据。&lt;/p&gt;
&lt;h5&gt;二，读取客户端发来的消息&lt;/h5&gt;
&lt;p&gt;现在我们已经获取了客户端的socket，现在我们可以进行读取操作了&lt;/p&gt;
&lt;p&gt;首先将socket中的输入流取出来，也就是客户端发过来的数据流：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;InputStream is = socket.getInputStream();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样获取过来的都是原始的字节信息，即 byte数组，如果读取这样的信息，我们还要进行各种各样的转换操作才能将他变成可读的信息，为了更方便的读取数据，我们可以再次将输入流进行包装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传入字节流对象
DataInputStream dis = new DataInputStream(is);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下我们就可以方便的读取数据了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String msg = dis.readUTF();
System.out.println(msg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里，一个简易的服务端的代码编写基本就完成了&lt;/p&gt;
&lt;p&gt;整合代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServerNote{
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666);
        Socket socket = ss.accept();

        InputStream is = socket.getInputStream();
        DataInputStream dis = new DataInputStream(is);

        String msg = dis.readUTF();
        System.out.println(msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这段代码有个非常明显的问题，就是只接收了一次消息，然后程序就结束了&lt;/p&gt;
&lt;p&gt;同理，那我们加一层循环嵌套，可以一直接收消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
    String msg = dis.readUTF();
    System.out.println(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServerNote{
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666);
        Socket socket = ss.accept();

        InputStream is = socket.getInputStream();
        DataInputStream dis = new DataInputStream(is);

        while(true){
    		String msg = dis.readUTF();
    		System.out.println(msg);
		}
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下，客户端可以与服务端通信了&lt;/p&gt;
&lt;h2&gt;多用户连接与群发消息：&lt;/h2&gt;
&lt;h5&gt;一、多线程与实现&lt;/h5&gt;
&lt;p&gt;前面我们所实现的仅仅是一对一的连接，即一个服务端连接一个客户端，接下来我们来实现多个客户端连接一个服务端。&lt;/p&gt;
&lt;p&gt;有人会说，那我直接while true 不断让他接收新的客户端连接不就行了嘛&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
    Socket socket = serverSocket.accept();
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实是：不行&lt;/p&gt;
&lt;p&gt;每次接收到新socket连接时，serversocket会将之前的客户端连接所遗弃，只处理本次连接的客户端&lt;/p&gt;
&lt;p&gt;换句话说，现在的服务端只有一个main线程在运行，这样简单的循环实现没有告诉他我要并行处理多个客户端连接&lt;/p&gt;
&lt;p&gt;所以我们需要使用多线程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
    Socket socket = serverSocket.accept();
    // ...
    new ServerReaderThread(socket).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先不看线程类如何实现，我们先来关注如何存储我们的用户池&lt;/p&gt;
&lt;p&gt;其实非常简单，直接new一个ArrayLsit即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// static设置为类级别，只有一个线程池
public static List&amp;lt;Socket&amp;gt; ClientSockets = new ArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class TCPServer2 {
    public static List&amp;lt;Socket&amp;gt; onLineSockets = new ArrayList&amp;lt;&amp;gt;();
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(6666);

        while (true) {
            Socket socket = serverSocket.accept();
            onLineSockets.add(socket);
            // 启动线程
            // new ServerReaderThread(socket).start();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们来实现线程类&lt;/p&gt;
&lt;h5&gt;二、线程类&lt;/h5&gt;
&lt;p&gt;现在我们的主文件不再需要实现接收消息的功能，只需要在线程类中实现即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 继承自线程类
public class ServerReaderThread extends Thread{
    private Socket socket;
    // 构造方法注入socket对象
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
	@Override
    public void run(){
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们来看看启动线程后的代码如何写&lt;/p&gt;
&lt;p&gt;与之前服务类相同，先获取输入流，之后进行包装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环接收发来的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while(true){
    String msg = dis.readUTF();
    System.out.println(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当客户端断开连接时，会抛出异常，所以我们还要进行异常的捕获：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (true){
       try {
            String msg = dis.readUTF();
            System.out.println(msg);
            SendMsgAll(msg);
       }catch (Exception e) {
           //从线程池中移除断开连接的用户
           TCPServer2.onLineSockets.remove(socket);
           dis.close();
           socket.close();
           break;
       }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServerReaderThread extends Thread {
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        // 在 run() 方法中，获取输入流的过程可能抛出受检异常，如 IOException，但 run() 方法无法在			
        // 签名中使用 throws 抛出，因此必须用最外层 try-catch 捕获并处理。常见做法是将其转化为 			
        // RuntimeException，以便后续调试和统一异常管理。
        try {
            InputStream inputStream = socket.getInputStream();
            DataInputStream dis = new DataInputStream(inputStream);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                } catch(Exception e) {
                    TCPServer2.onLineSockets.remove(socket);
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;三、群发消息&lt;/h5&gt;
&lt;p&gt;下面我们来实现如何将用户的消息转发给别的用户&lt;/p&gt;
&lt;p&gt;实际上在我们的模型中，必须有一个人做服务端接收消息，而用户端都是发送消息的一方，如何实现也能接收消息呢？其实就是服务端进行转发&lt;/p&gt;
&lt;p&gt;为此，我们还要实现一个ClientReaderThread，用于客户端接收消息，这是因为如果我们直接在Client.java里实现接收的话，由于只有一个主线程，在接收消息时如果没人给他发送消息，主线程就会被阻塞，无法执行其他任务，以下是代码实现，与ServerReaderthread几乎一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

public class ClienReaderThread extends Thread{
    public Socket socket;
    public ClienReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            DataInputStream dis = new DataInputStream(inputStream);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                } catch(Exception e) {
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后将这个线程在客户端代码中启动即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		// ...其他代码...
		Socket socket = new Socket(&quot;127.0.0.1&quot;,6666);
        new ClienReaderThread(socket).start();
        OutputStream os = socket.getOutputStream();
		// ...其他代码...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;群发消息也很简单，就是遍历用户池中的用户，进行发消息即可，以下是方法实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void SendMsgAll(String msg) throws IOException {
        for (Socket onLineSocket : TCPServer2.onLineSockets) {
     		if(onLineSocket.getRemoteSocketAddress().equals(socket.getRemoteSocketAddress())){
                continue;
            }
            OutputStream os = onLineSocket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            dos.writeUTF(msg);
            dos.flush();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们实现了所有功能&lt;/p&gt;
&lt;p&gt;全部完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class TCPServer2 {
    public static List&amp;lt;Socket&amp;gt; onLineSockets = new ArrayList&amp;lt;&amp;gt;();
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(6666);

        while (true) {
            Socket socket = serverSocket.accept();
            onLineSockets.add(socket);
            System.out.println( socket.getRemoteSocketAddress() + &quot;上线了&quot;);
            new ServerReaderThread(socket).start();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        //服务器主机ip和端口
        Socket socket = new Socket(&quot;127.0.0.1&quot;,6666);

        new ClienReaderThread(socket).start();

        OutputStream os = socket.getOutputStream();

        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);
        System.out.println(&quot;已连接&quot;);
        while (true) {
            String msg = sc.nextLine();
            if (msg.equals(&quot;exit&quot;)){
                System.out.println(&quot;end&quot;);
                dos.close();
                socket.close();
                break;
            }
            dos.writeUTF(msg);
            dos.flush();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;
import java.net.Socket;

public class ServerReaderThread extends Thread {
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            DataInputStream dis = new DataInputStream(inputStream);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                    SendMsgAll(msg);
                } catch(Exception e) {
                    System.out.println(&quot;有人下线了&quot; + socket.getRemoteSocketAddress());
                    TCPServer2.onLineSockets.remove(socket);
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void SendMsgAll(String msg) throws IOException {
        for (Socket onLineSocket : TCPServer2.onLineSockets) {
            if (onLineSocket.getRemoteSocketAddress().equals(socket.getRemoteSocketAddress())){
                continue;
            }
            OutputStream os = onLineSocket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            dos.writeUTF(msg);
            dos.flush();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

public class ClienReaderThread extends Thread{
    public Socket socket;
    public ClienReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            DataInputStream dis = new DataInputStream(inputStream);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                } catch(Exception e) {
                    System.out.println(&quot;自己下线了&quot;);
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成于2025.6.5 16:50&lt;/p&gt;
</content:encoded></item></channel></rss>