深入解析进程间通信与Socket原理:从理论到TypeScript实战
文章目录
- 一、进程中如何通信
- 1.1 管道
- 1.1.1 核心特性
- 1.1.2 缺点
- 1.1.3 匿名管道与命名管道的对比
- 1.2 信号
- 1.2.1 核心特性
- 1.2.2 缺点
- 1.2.3 信号分类对比
- 1.3 消息队列
- 1.3.1 核心特性
- 1.3.2 缺点
- 1.4 共享内存
- 1.4.1 核心特性
- 1.4.2 缺点
- 1.5 信号量
- 1.5.1 核心特性
- 1.5.2 缺点
- 二、Socket
- 2.1 Socket原理
- 2.1.1 什么是Socket
- 2.1.2 网络进程如何通信
- 2.1.3 Sokcet如何通信
- 2.2 TCP/IP协议
- 2.2.1 概念
- 2.2.2 TCP数据报结构
- 2.3 连接建立(三次握手)
- 2.3.1 建立过程
- 2.3.2 关键问题
- 为什么是三次握手,而不是两次四次?
- 2.4 断开连接(四次挥手)
- 2.4.1 断连过程
- 2.4.2 关键问题
- 为什么是四次挥手,不能是三次挥手
- 为什么不能是两次挥手
- 三、TS通过Socket实现聊天室基础功能
- 3.1 服务端实现原理
- 3.2 客户端实现原理
一、进程中如何通信
重要的进程间通信(不同进程之间传播或交换信息)方式分为六种
管道、信号、消息队列、共享内存、信号量、socket。其中前五种主要用于一台主机之中的各个进程之间的通信,socket套接字通信主要用于网络之中不同主机之间的通信。
1.1 管道
管道(Pipe)其本质是由内核维护的一段内存缓存区。一个进程向该缓存写入数据,另一个进程从中读取数据,形成单向数据流。管道传输的数据是无格式的字节流,且受内核缓冲区大小的限制。
1.1.1 核心特性
- 单向通信
匿名管道仅支持单向数据传输(一端写入,另一端读取),若需双向通信,必须建立两条独立的管道。这种单向性体现了其半双工通信的特性。 - 亲缘关系依赖
- 匿名管道:通常用于父子进程或兄弟进程等有亲缘关系的进程间通信。子进程通过继承父进程的文件描述符访问管道。
- 命名管道(FIFO):通过文件系统中的路径标识,允许无亲缘关系的进程通过打开同一路径进行通信,突破了匿名管道的亲缘限制。
- 阻塞与非阻塞模式
- 默认情况下,读进程在管道无数据时会阻塞等待;写进程在缓冲区满时也会阻塞,直到有空间释放。
- 可通过
fcntl
函数设为非阻塞模式:读空管道时直接返回EAGAIN
错误,写满时丢弃数据或部分写入。
- 生命周期管理
- 匿名管道随进程终止自动销毁。
- 命名管道需手动删除其文件路径(如
unlink
),否则会持久存在于文件系统中。
- 容量限制
内核缓冲区大小固定(通常为4KB~64KB)。若写入速度远超读取速度,写进程可能被长时间阻塞,需设计合理的读写协同逻辑。
1.1.2 缺点
- 半双工通信的天然限制
匿名管道仅支持单向数据传输,双向通信需额外建立一条管道,增加了资源管理和协调的复杂度。 - 读写阻塞的强依赖性
若管道内的数据未被读进程及时消费,写进程会因缓冲区满而阻塞,直到读进程取走数据。这种强同步机制可能导致进程间死锁(如双方同时等待对方读写)。
1.1.3 匿名管道与命名管道的对比
特性 | 匿名管道 | 命名管道(FIFO) |
---|---|---|
创建方式 | pipe() 系统调用 | mkfifo() 命令或函数 |
通信方向 | 半双工(单向,需双向则建两条) | 半双工,但支持多进程读写 |
进程关系 | 仅限亲缘进程 | 任意进程(通过文件路径访问) |
持久性 | 随进程结束销毁 | 需手动删除文件路径 |
注意事项
- 数据原子性:若单次写入数据量小于
PIPE_BUF
(通常512B~4KB),内核保证写入的原子性;反之可能被拆分。- 同步问题:共享内存需配合信号量,而管道自身通过阻塞机制隐式同步,但仍需注意读写端协调。
- 性能瓶颈:高频大数据传输时,管道可能因拷贝开销和容量限制成为瓶颈,此时可改用共享内存。
1.2 信号
信号(Signal)是轻量级异步通知机制,由内核或进程向目标进程发送特定事件的通知。其本质是预定义的事件编号(如SIGINT
对应终端中断),用于触发进程的默认行为或自定义处理逻辑。
信号是进程间通信中最简单、最直接的异步通知机制,适用于事件驱动、进程控制等场景。但其设计初衷是“通知”而非“数据传输”,因此复杂交互需结合其他IPC机制(如管道、共享内存)。
1.2.1 核心特性
- 异步通知
信号在任意时刻可中断进程当前操作,直接跳转到信号处理函数执行,与进程的执行流无关。 - 预定义类型
系统定义了约30种标准信号,编号范围通常为1~31。 - 处理方式灵活性
- 默认行为:终止进程、忽略信号、暂停进程。
- 自定义处理:通过
signal()
或sigaction()
注册用户函数(如处理SIGINT
实现优雅退出)。
- 生命周期
- 生成:由内核、其他进程或终端触发。
- 传递:内核将信号加入目标进程的信号队列,等待进程调度处理。
- 处理:进程从内核态返回用户态时,检查并执行信号处理函数。
1.2.2 缺点
- 信息传递能力弱
信号仅能传递事件编号,无法携带额外数据(实时信号如SIGRTMIN
可携带少量信息,但需复杂处理)。 - 信号丢失与覆盖
- 同类非实时信号多次到达时,可能被合并为一次。
- 处理函数执行期间,新到达的同类型信号可能被阻塞。
- 处理函数的安全限制
信号处理函数需为可重入函数,避免使用非线程安全操作。 - 实时性受限
非实时信号无优先级,内核可能延迟传递,无法保证严格时序。
1.2.3 信号分类对比
类型 | 非实时信号(标准信号) | 实时信号(SIGRTMIN~SIGRTMAX ) |
---|---|---|
编号范围 | 1~31 | 34~64(依系统不同) |
队列机制 | 不排队,多次发送可能合并 | 支持排队,按顺序处理 |
数据携带 | 不支持 | 可通过sigqueue() 附加数据 |
优先级 | 无 | 支持信号优先级 |
注意事项
- 避免处理函数阻塞
信号处理函数应快速完成,复杂逻辑可通过标记位在主线程序处理。- 信号屏蔽与竞态条件
- 使用
sigprocmask
屏蔽关键代码段的信号,防止处理函数中断敏感操作。- 处理共享资源时需考虑信号引发的竞态问题。
- 系统调用中断
信号可能中断阻塞的系统调用,需检查错误码EINTR
并重试。- 信号与多线程
多线程程序中,信号可能由任意线程处理,建议统一由主线程接管。
1.3 消息队列
消息队列(Message Queue)其本质是由内核维护的链表结构,允许进程以消息块(结构化数据)的形式异步通信。相比管道,消息队列支持更灵活的格式和随机读取,适用于频繁或结构化的数据交换场景。
消息队列弥补了管道在结构化数据和异步通信上的不足,适用于中等频率、结构化消息交换的场景。但其性能瓶颈(上下文切换与拷贝开销)使其难以应对超高频需求,此类场景可优先考虑共享内存或Unix域套接字。
1.3.1 核心特性
- 异步非阻塞通信
- 发送进程将消息写入队列后立即返回,无需等待接收进程响应。
- 接收进程可主动拉取消息,若队列为空可选择阻塞或非阻塞模式。
- 结构化消息
- 消息包含类型标识和数据体,双方需约定格式。
- 支持按消息类型读取,而非严格FIFO顺序。
- 内核持久性
- 消息队列独立于进程存在,进程终止后消息仍保留在内核中。
- 可通过权限控制限制其他进程访问。
- 原子性保证
- 单次写入的消息若小于
MSGMAX
,内核保证原子性)。
- 单次写入的消息若小于
- 多进程共享
任意进程(需权限)均可通过队列标识符访问同一队列,支持多对多通信。
1.3.2 缺点
- 消息大小限制
单条消息长度受内核参数MSGMAX
限制(默认约8KB),超出需分片处理。 - 性能开销
- CPU上下文切换:每次读写需通过系统调用进入内核态,频繁操作时开销显著。
- 数据拷贝:消息从用户空间拷贝到内核队列,再拷贝到接收方用户空间,高频场景效率低。
- 队列容量限制
队列总大小受内核参数MSGMNB
限制(默认约16KB~64KB),写满后发送进程默认阻塞。 - 复杂性
需自行处理消息类型匹配、分片重组、队列满/空等问题,开发复杂度较高。
注意事项
- 消息类型设计
- 类型值应明确区分用途(如正数用于请求,负数用于响应)。
- 避免类型冲突,建议使用枚举或宏定义。
- 队列泄露防护
- 确保进程退出前释放队列。
- 通过
ipcs -q
和ipcrm
命令管理残留队列。- 超长消息处理
- 若消息长度超过
MSGMAX
,需在应用层分片发送,接收端重组。- 信号量同步(可选)
- 多进程竞争读写时,可结合信号量实现互斥锁,避免消息覆盖。
1.4 共享内存
共享内存(Shared Memory)是进程间通信(IPC)中速度最快的机制,其本质是由内核分配的一段物理内存区域,被多个进程映射到各自的虚拟地址空间中。进程通过直接读写该内存区域实现数据交互,无需内核中转或数据拷贝,从而极大提升通信效率。
共享内存是进程间通信的性能天花板,尤其适合对吞吐量和延迟敏感的场景。但其“直接访问”的特性如同一把双刃剑,在提供极致速度的同时,也要求开发者严格管理同步与数据一致性。结合信号量、互斥锁等机制,可构建高效且稳定的多进程协作系统。
1.4.1 核心特性
- 零拷贝高效性
数据直接在共享内存区域读写,避免了管道、消息队列等机制中用户态与内核态间的数据拷贝开销。 - 虚拟地址映射
- 每个进程通过页表将共享内存映射到自身虚拟地址空间的不同位置。
- 进程通过虚拟地址访问共享内存,由MMU(内存管理单元)完成虚实地址转换。
- 多进程并发访问
多个进程可同时映射同一共享内存区域,实现高速数据共享,但需配合同步机制(如信号量、互斥锁)避免竞争。 - 内核持久性
- 共享内存独立于进程存在,进程退出后仍保留(除非显式删除)。
- 通过
shmctl(IPC_RMID)
销毁或系统重启后清除。
1.4.2 缺点
-
同步复杂度高
需额外机制(如信号量)协调读写
-
安全隐患
恶意进程可能改写数据
-
生命周期管理
需显示删除避免内存泄漏
注意事项
- 内存对齐与访问
- 确保数据结构对齐,避免不同进程因编译差异导致的内存解释错误。
- 缓存一致性
- 多核CPU中,共享内存可能引发缓存一致性问题,需通过内存屏障或原子操作保证可见性。
- 安全与权限控制
- 设置严格的IPC权限(如
0666
仅允许同组用户访问),防止未授权进程篡改数据。- 资源泄漏防护
- 确保进程退出前调用
shmdt()
和shmctl()
,避免内存段永久占用。
1.5 信号量
信号量(Semaphore)是进程间或线程间同步与互斥的核心工具,其本质是由内核维护的整型计数器,用于协调多个执行单元对共享资源的访问。信号量的核心思想是通过P
(等待)和V
(释放)操作,实现资源的原子性分配与释放,避免竞态条件(Race Condition)。
信号量是解决并发编程中同步与资源分配问题的基石,其灵活性使其适用于从简单互斥到复杂资源管理的广泛场景。然而,信号量的低级特性也要求开发者对并发逻辑有深刻理解,避免死锁、饥饿等典型问题。在实际开发中,可优先使用高层抽象(如线程池、无锁队列),但在需要精细控制时,信号量仍是不可替代的工具。
1.5.1 核心特性
- 计数器抽象
- 信号量值表示当前可用资源数量:
- 正值:剩余可用资源数。
- 零值:资源已被完全占用,请求者需等待。
- 负值:绝对值表示等待该资源的进程/线程数。
- 信号量值表示当前可用资源数量:
- 原子操作
P操作
(Proberen,尝试获取):
若信号量值 > 0,则减1并继续;否则阻塞等待。V操作
(Verhogen,释放资源):
信号量值加1,并唤醒一个等待进程。
- 分类
- 二进制信号量:值范围为0或1,等同于互斥锁(Mutex)。
- 计数信号量:值范围≥0,表示资源池容量(如连接池限制)。
- 内核与用户态实现
- System V信号量:内核维护,支持跨进程同步(如
semget()
)。 - POSIX信号量:可位于共享内存中,支持进程或线程级同步(如
sem_init()
)。
- System V信号量:内核维护,支持跨进程同步(如
1.5.2 缺点
-
死锁风险
错误使用可能导致进程永久阻塞
-
优先级反转
低优先级进程占用资源,高优先级进程饥饿
-
复杂性
需手动管理信号量创建、初始化和销毁
注意事项
- 死锁预防
- 顺序一致性:所有进程以相同顺序获取信号量。
- 超时机制:使用
sem_timedwait()
避免无限阻塞。- 信号量泄漏
- System V信号量需显式调用
semctl(IPC_RMID)
删除。- POSIX命名信号量需
sem_unlink()
防止残留。- 原子性与错误处理
- 确保
P/V
操作的原子性(如SEM_UNDO
标志应对进程崩溃)。- 检查
sem_wait()
返回值,处理EINTR
(信号中断)等错误。- 性能优化
- 避免过度使用信号量,高频场景可结合自旋锁或无锁数据结构。
二、Socket
2.1 Socket原理
2.1.1 什么是Socket
在计算机通信领域,socket被翻译为套接字,他是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
socket起源于Unix,而Unix/Linux基本哲学之一就是”一切皆文件“,都可以用”打开open -→读写write/read–> 关闭close”模式来操作。
我的理解就是Socket就是该模式的一个实现:即socket是一种特殊的文件,一些sokcet函数就是对其他进行的操作(读写IO、打开、关闭)。
Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
2.1.2 网络进程如何通信
我们要理解网络中进程如何通信,得解决两个问题:
a、我们要如何标识一台主机,即怎样确定我们将要通信的进程是在那一台主机上运行。
b、我们要如何标识唯一进程,本地通过pid标识,网络中应该怎样标识?
解决办法:
a、TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机
b、传输层的“协议+端口”可以唯一标识主机中的应用程序(进程),因此,我们利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互
2.1.3 Sokcet如何通信
现在,我们知道了网络中进程如何进行通信,即利用三元组d[ip地址,协议,端口]可以进行网络间通信了,那我们应该怎么实现?因此,我们sokcet应运而生,他就是利用三元组解决网络通信的一个中间件工具,就目前而言,几乎所有应用程序都是采用socket。
socket通信的数据传输方式常用的有两种:
- SOCK_STREAM:表示面向连接的数据传输方式。数据可以准确无误的达到另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的http协议就使用了SOCK_STREAM数据传输,因为要确保数据的正确性,否则网页不能正常解析。
- OCK_DGRAM:表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
2.2 TCP/IP协议
2.2.1 概念
TCP/IP提供点对点的连接机制,将数据应该如何封装、定址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式分别实现出不同通信协议。协议族下的各种协议,依其功能不同,被分别归属到四个层次结构中,常被视为是简化的七层OSI模型。
-
四层结构(由下至上):
- 网络接口层(链路层):负责物理介质的数据帧传输(如以太网协议)。
- 网络层(IP层):通过IP地址实现主机间的逻辑寻址和路由(如IP协议)。
- 传输层:提供端到端的数据传输服务(如TCP、UDP协议)。
- 应用层:面向用户提供具体服务(如HTTP、FTP协议)。
-
TCP(传输控制协议)是面向连接的、可靠的、基于字节流的传输层协议。其核心特性包括:
-
三次握手建立连接:确保双方通信能力及初始序列号同步。
-
四次挥手释放连接:保证数据完整性并优雅关闭双工通道。
-
超时重传、流量控制、拥塞控制:保障数据传输的可靠性。
-
2.2.2 TCP数据报结构
TCP报文头部固定20字节(不含选项字段),关键字段如下:
- 源端口与目的端口(各16位):标识发送方和接收方的应用进程。
- 序号(Seq,32位):本报文段发送数据的第一个字节的编号。
- 确认号(Ack,32位):期望接收的下一个字节的编号,Ack = 收到的Seq + 数据长度 + 1(若数据长度为0,如SYN/FIN标志位,视为占1个序号)。
- 数据偏移(4位):TCP首部长度(以4字节为单位)。
- 标志位(6位):
- URG:紧急指针有效(需配合紧急指针字段使用)。
- ACK:确认号有效(建立连接后所有报文必须置1)。
- PSH:接收方应立即将数据提交应用层。
- RST:强制断开连接(异常终止)。
- SYN:发起连接请求(同步序列号)。
- FIN:请求终止连接。
- 窗口大小(16位):接收方当前可接受的数据量(流量控制)。
- 校验和(16位):确保数据完整性。
2.3 连接建立(三次握手)
2.3.1 建立过程
客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求
这时客户端发起请求:
- 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。
- 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包
服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。
服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。
服务器将数据包发出,进入SYN-RECV状态 - 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。
客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。 - 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。
2.3.2 关键问题
为什么是三次握手,而不是两次四次?
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以双方同步的初始序列号
- 三次握手才可以避免浪费资源
-
阻止重复历史连接的初始化(主要原因)
-
在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
-
三次握手已满足上述所有需求,额外增加握手次数(如四次)会引入不必要的延迟,且无法进一步解决核心问题。三次是理论上的最小安全交互次数。
-
-
双方同步的初始序列号
当客户端发送携带「初始序列号」的
SYN
报文的时候,需要服务端回一个ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。 -
避免资源浪费
如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
2.4 断开连接(四次挥手)
2.4.1 断连过程
TCP连接的释放需要四次挥手,其本质是双向通信的全双工特性决定的:每个方向必须独立关闭。以下为详细过程(以客户端主动关闭为例):
- 第一次挥手(FIN)
客户端调用close()
后,发送FIN报文(FIN=1),进入FIN_WAIT_1
状态,表示客户端不再发送数据,但仍可接收数据。 - 第二次挥手(ACK)
服务端收到FIN后,立即回复ACK报文,进入CLOSE_WAIT
状态。此时服务端可能仍有未发送完的数据,客户端收到ACK后进入FIN_WAIT_2
状态。 - 第三次挥手(FIN)
当服务端数据发送完毕,准备好关闭连接时,发送FIN报文,进入LAST_ACK
状态,表示服务端不再发送数据。 - 第四次挥手(ACK)
客户端收到FIN后,回复ACK报文,进入TIME_WAIT
状态,等待2MSL(Maximum Segment Lifetime,报文最大生存时间)后关闭连接。服务端收到ACK后立即进入CLOSED
状态。
2.4.2 关键问题
为什么是四次挥手,不能是三次挥手
- 全双工通信的特性
TCP连接是全双工的,双方需独立关闭自己的数据通道。客户端发送FIN仅表示其不再发送数据(但可接收),服务端的ACK仅确认收到FIN。服务端的FIN需等待其数据发送完毕后再发送,因此ACK和FIN不能合并为一次。 - 数据完整性保障
若服务端收到FIN后立即合并ACK与FIN(变为三次挥手),可能丢失未传输完的数据。分开发送确保服务端有足够时间处理剩余数据。 - 可靠性设计
客户端最后的TIME_WAIT
状态(等待2MSL)有两个作用:- 确保服务端收到最后的ACK。若ACK丢失,服务端会重传FIN,客户端可再次响应。
- 防止旧连接的延迟报文干扰新连接。
为什么不能是两次挥手
- 服务端未确认自身数据是否已发送完毕。
- 客户端无法确认服务端是否收到最终ACK,可能造成服务端持续等待。
三、TS通过Socket实现聊天室基础功能
3.1 服务端实现原理
- 核心结构
-
使用Node.js的
net
模块创建TCP服务器 -
定义了
Room
类型管理聊天室信息:type Room = {roomName: string; // 房间名称port: number; // 监听端口users: [string, net.Socket][]; // 用户列表([客户端地址, Socket对象]) };
- 启动流程
-
关键功能实现
-
客户端连接管理:使用serverConnectEvent处理新连接
-
记录客户端地址(client.remoteAddress:client.remotePort)
-
存储Socket对象到用户列表
-
-
消息广播机制:
private broadcast(content: string) {for (const [_, userClient] of this.room.users) {if (userClient.writable) {userClient.write(content); // 向所有客户端发送消息}} }
-
-
服务端实现示例
// src/server/server.ts import * as net from 'net'; import * as readline from 'readline';// 定义房间类型 type Room = {roomName: string;port: number;users: [string, net.Socket][]; };// 定义服务器类 class MyTCPServer {private server: net.Server;private room: Room;constructor(port: number = 8080, roomName: string = '大厅') {this.room = {roomName,port,users: []};this.server = net.createServer(this.serverConnectEvent.bind(this));this.initServer();this.listenForShutdown()}private initServer() {this.server.listen(this.room.port, () => {console.log(`服务器已启动,监听端口 ${this.room.port}`);});this.server.on('close', () => {console.log('服务器已关闭');});}private serverConnectEvent(client: net.Socket) {console.log(`客户端已连接: ${client.remoteAddress}:${client.remotePort}`);//设计用户ID为标识const clientId = `${client.remoteAddress}:${client.remotePort}`;// 添加客户端到用户列表this.room.users.push([`${client.remoteAddress}:${client.remotePort}`, client]);client.on('data', (chunk) => {const content = chunk.toString();if( content === 'kick') {this.disconnectClient(client)} else {this.broadcast( `${clientId}: ${content}`, client)}});client.on('end', () => {console.log(`客户端已断开连接: ${client.remoteAddress}:${client.remotePort}`);this.removeClient(client);});client.on('error', (err) => {console.error(`客户端发生错误: ${err.message}`);this.removeClient(client);});}private broadcast(content: string, sender: net.Socket) {for (const [_, userClient] of this.room.users) {if (userClient.writable && userClient !== sender) {userClient.write(content);}}}private removeClient(client: net.Socket) {const index = this.room.users.findIndex(([_, userClient]) => userClient === client);if (index !== -1) {this.room.users.splice(index, 1);}}//断开客户端连接private disconnectClient(client: net.Socket) {const clientInfo = `${client.remoteAddress}:${client.remotePort}`;console.log(`正在断开客户端连接: ${clientInfo}`);client.end();this.removeClient(client);}//关闭服务器private listenForShutdown() {const rl = readline.createInterface({input: process.stdin,output: process.stdout});rl.question('输入 "shutdown" 关闭服务器: ', (input: string) => {if (input === 'shutdown') {// 关闭所有客户端连接for (const [_, userClient] of this.room.users) {userClient.destroy(); // 强制断开客户端}//关闭服务器this.server.close(() => {console.log('服务器已关闭');});rl.close();} else {rl.close();this.listenForShutdown();}});} }// 启动服务器 new MyTCPServer();
3.2 客户端实现原理
-
核心结构
- 使用net.Socket连接服务器
- 通过readline模块实现控制台的输入
- 事件驱动架构
-
工作流程
-
关键功能实现
-
输入处理:
private readInput() {this.rl.question('请输入消息: ', (input) => {this.client.write(input);this.readInput(); // 递归调用实现持续输入}); }
-
消息接收:
this.client.on('data', (chunk) => {const content = chunk.toString();console.log(content); // 直接打印原始消息 });
-
-
客户端实现示例
// src/client/client.ts import * as net from 'net'; import * as readline from 'readline';// 定义客户端类 class MyTCPClient {private client: net.Socket;private rl: readline.Interface;constructor() {this.client = new net.Socket();this.rl = readline.createInterface({input: process.stdin,output: process.stdout});this.connectToServer();}private connectToServer() {this.client.connect(8080, () => {console.log('已连接到服务器');this.readInput();});this.client.on('data', (chunk) => {const content = chunk.toString();console.log('你接收到了一条消息\n' + content);});this.client.on('end', () => {console.log('与服务器的连接已断开');this.rl.close();});this.client.on('error', (err) => {console.error(`与服务器的连接发生错误: ${err.message}`);this.rl.close();});}private readInput() {this.rl.question('请输入消息(输入"exit"断开连接):\n ', (input) => {if( input === 'exit') {this.client.end();this.rl.close();} else {this.client.write(input, (err) => {if (err) {console.log('发送消息失败');} else {console.log('你发出了一条消息\n' + input);}this.readInput();});}});} }// 启动客户端 new MyTCPClient();
相关文章:
深入解析进程间通信与Socket原理:从理论到TypeScript实战
文章目录 一、进程中如何通信1.1 管道1.1.1 核心特性1.1.2 缺点1.1.3 匿名管道与命名管道的对比 1.2 信号1.2.1 核心特性1.2.2 缺点1.2.3 信号分类对比 1.3 消息队列1.3.1 核心特性1.3.2 缺点 1.4 共享内存1.4.1 核心特性1.4.2 缺点 1.5 信号量1.5.1 核心特性1.5.2 缺点 二、So…...
[特殊字符] Milvus + LLM大模型:打造智能电影知识库系统
今天给大家分享一个超酷的技术组合:Milvus向量数据库 智谱AI大模型!我们将创建一个能理解电影内容的智能搜索系统,不仅能找到相关电影,还能用自然语言总结答案! 🌟 项目背景 这个项目基于Milvus官方案例…...
MapReduce架构-打包运行
(一)maven打包 MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序(例如:jar…...
信创生态核心技术栈:国产芯片架构适配详解
🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C、C#等开发语言,熟悉Java常用开…...
BBDM学习笔记
1. configs 1.1 LBBDM: Latent BBDM [readme]...
6. HTML 锚点链接与页面导航
在开发长页面或文档类网站时,锚点链接(Anchor Links)是一个非常实用的功能。通过学习 HTML 锚点技术,将会掌握如何在同一页面内实现快速跳转,以及如何优化长页面的导航体验。以下是基于给定素材的学习总结和实践心得 一、什么是锚点链接? 锚点链接(也称为页面内链接)允…...
绘制拖拽html
<!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8" /> <meta name"viewport" content"widthdevice-width, initial-scale1" /> <title>拖拽绘制矩形框 - 可移动可调整大小</ti…...
OpenCV计算机视觉实战(3)——计算机图像处理基础
OpenCV计算机视觉实战(3)——计算机图像处理基础 0. 前言1. 像素和图像表示1.1 像素 2. 色彩空间2.1 原色2.2 色彩空间2.3 像素和色彩空间 3. 文件类型3.1 图像文件类型3.2 视频文件3.3 图像与视频 4. 计算机图像编程简史5. OpenCV 概述小结系列链接 0. …...
零基础学Java——第九章:数据库编程(三)
第九章:数据库编程 - ORM框架(下) 在上一部分中,我们学习了ORM框架的基础知识和Hibernate框架。在这一部分中,我们将继续学习其他流行的ORM框架,包括MyBatis和Spring Data JPA。 1. MyBatis框架 1.1 MyB…...
Linux/AndroidOS中进程间的通信线程间的同步 - 信号量
1 概述 本文将介绍 POSIX 信号量,它允许进程和线程同步对共享资源的访问。有两种类型的 POSIX 信号量: 命名信号量:这种信号量拥有一个名字。通过使用相同的名字调用 sem_open(),不相关的进程能够访问同一个信号量。未命名信号量…...
精益数据分析(46/126):深入剖析用户生成内容(UGC)商业模式
精益数据分析(46/126):深入剖析用户生成内容(UGC)商业模式 在创业与数据分析的征程中,每一种商业模式都蕴含着独特的价值与挑战。今天,我们依旧怀揣着共同进步的信念,深入研读《精益…...
vue +xlsx+exceljs 导出excel文档
实现功能:分标题行导出数据过多,一个sheet表里表格条数有限制,需要分sheet显示。 步骤1:安装插件包 npm install exceljs npm install xlsx 步骤2:引用包 import XLSX from xlsx; import ExcelJS from exceljs; 步骤3&am…...
Android 10.0 SharedPreferences in credential encrypted storage are not avai
1.前言 在10.0的系统rom定制化开发中,在开机的过程中,由于某些应用在开机解锁阶段就开始访问查询短信和联系人等功能,所以 会出现抛异常的情况出现,接下来分析下相关的情况,然后来解决这些问题 2.SharedPreferences in credential encrypted storage are not available …...
面试高频算法:最长回文子串
题目:5. 最长回文子串 给你一个字符串 s,找到 s 中最长的回文子串。 回文:如果字符串向前和向后读都相同,则它满足回文性;子串:子字符串 是字符串中连续的非空字符序列。 示例 1: 输入&…...
RDK X5 交叉编译OSS\QT\opencv\openssl
RDK X5 交叉编译环境配置 1 资源2 使用vm安装Ubuntu22.043 安装依赖4 安装ide5 下载交叉编译工具6 编译oss库6.1 设置临时环境变量6.2 编译arm版本的openssl6.2 编译arm版本的curl6.1 下载oss源码6.1.1 创建arm-toolchain.cmake6.1.2 修改CMakeLists.txt6.1.3 编译 7 编译openc…...
Python cv2边缘检测与轮廓查找:从理论到实战
在计算机视觉领域,边缘检测与轮廓查找是图像分析的核心技术。本文将结合OpenCV库(cv2模块),从理论原理到代码实战,系统讲解如何通过Python实现这两个关键操作。 一、基础概念解析 1.1 边缘检测的本质 边缘是图像中灰…...
5月7日星期三今日早报简报微语报早读
5月7日星期三,农历四月初十,早报#微语早读。 1、1101名优秀运动员拟保送,全红婵、黄雨婷、盛李豪在列; 2、世界羽联主席巴达玛:中国组织赛事的能力无与伦比; 3、中国首位、亚洲首位!赵心童夺…...
智慧医院的可视化变革:可视化工具助力数字化转型
在科技飞速发展的当下,智慧医院已从概念逐步落地,深刻改变着传统医疗模式。它借助互联网、数字孪生及人工智能等前沿技术,在医疗服务领域掀起革新,涵盖面向医务人员的“智慧医疗”、面向患者的“智慧服务”以及面向医院的“智慧管…...
python+open3d选择点云上的某个点并获取其对应三维坐标
👑主页:吾名招财 👓简介:工科学硕,研究方向机器视觉,爱好较广泛… 💫签名:面朝大海,春暖花开! python+open3d选择点云上的某个点并获取其对应三维坐标 1,引言2,效果展示3,点云获取4,程序1,引言 有时候我们只想在点云上获取某个目标的具体坐标,通过程序根据…...
ROS第十三梯:RViz+Marker——自定义几何形状可视化
1)概述 在ROS(Robot Operating System)中,Marker是一种用于在RViz(Robot Visualization)中显示自定义几何形状和注释的工具。Marker是通过visualization_msgs/Marker消息类型发布的。可以在RViz中以各种形式(如点、线、文本、立方体等)显示数据。 2)主要消息格…...
Java高频面试之并发编程-13
hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶 面试官:详解原子性、可见性、有序性 在并发编程中,原子性(Atomicity)、可见性(…...
WSL 的 Ubuntu 子系统中启用图形化界面
sudo chmod w /home sudo apt update sudo apt install cifs-utils 1. 选择合适的 X 服务器 在 Windows 系统上,需要安装一个 X 服务器来处理 WSL 中 Ubuntu 的图形显示。常用的 X 服务器有 VcXsrv 和 X410,这里以 VcXsrv 为例: 从VcXsrv 官…...
项目模拟实现消息队列第二天
消息应答的模式 1.自动应答: 消费者把这个消息取走了,就算是应答了(相当于没有应答) 2.手动应答: basicAck方法属于手动应答(消费者需要主动调用这个api进行应答) 小结 1.需要实现生产者,broker server,消费者这三个部分的 2.针对生产者和消费…...
MySQL OCP和Oracle OCP怎么选?
近期oracle 为庆祝 MySQL 数据库发布 30 周年,Oracle 官方推出限时福利:2025 年 4 月 20 日至 7 月 31 日期间,所有人均可免费报考 MySQL OCP(Oracle Certified Professional)认证考试(具体可查看MySQL OCP…...
SR触发器为什么能够消抖
SR触发器(Set-Reset触发器)能够用于**消抖(Debounce)**,主要是因为它的双稳态特性和对输入信号的锁定能力。机械开关(如按键、拨动开关)在闭合或断开时,由于金属触点的弹性ÿ…...
2025ISCC练武校级赛部分题解WP
Web 战胜卞相壹 <!-- 路过的酒罐王柯洁九段说: --> <!-- 会叠棋子有什么用!你得在棋盘内战胜他!我教你个定式,要一直记得!一直! --> <!-- SGF B[ae];B[ce];B[df];B[cg];B[ag];B[ai];B[ci];…...
Microsoft Azure 在印度尼西亚区域正式上线
微软正式宣布,其首个落地印度尼西亚的云区域——Indonesia Central 已全面上线并正式投入使用!这一区域精心设置了三个可用性区(Availability Zones),每个可用性区均配备独立的电源、冷却系统以及网络设施,…...
day18 python聚类分析对数据集模型性能影响
聚类后的分析:推断簇的类型 知识点回顾: 推断簇含义的2个思路:先选特征和后选特征通过可视化图形借助ai定义簇的含义科研逻辑闭环:通过精度判断特征工程价值 作业:参考示例代码对心脏病数据集采取类似操作,并且评估特征…...
vue3的新特性
vue2 data属性和方法名散落于各个位置,量大了不好找 顺序变了,script在最前面 setup vue3中不用this,setup的执行时期比beforeCreate还要早,所以不要用this setup中写代码的特点 必须要有return,才能在上面使用 什么…...
NX二次开发——BlockUI 弹出另一个BlockUI对话框
最近在研究,装配体下自动导出BOM表格中需要用到BlockUI 弹出另一个BlockUI对话框。通过对网上资料进行整理总结,具体如下: 1、明确主对话框、子对话框1和子对话框2 使用BlockUI创建.cpp和.hpp文件,dlx文件内容如下所示 主对话框…...
《Overlapping Experiment Infrastructure: More, Better, Faster》论文阅读笔记
文章目录 1 背景2 三个核心概念3 Launch层:特性发布的专用机制4 流量分发策略和条件筛选4.1 四种流量分发类型4.2 条件筛选机制 5 工具链与监控体系6 实验设计原则7 培训参考与推荐 1 背景 谷歌(Google)以数据驱动著称,几乎所有可…...
【Machine Learning Q and AI 读书笔记】- 05 利用数据减少过拟合现象
Machine Learning Q and AI 中文译名 大模型技术30讲,主要总结了大模型相关的技术要点,结合学术和工程化,对LLM从业者来说,是一份非常好的学习实践技术地图. 本文是Machine Learning Q and AI 读书笔记的第5篇,对应原…...
前端面试测试题目(一)
一、Vue的双向绑定机制(v-model底层实现原理) Vue的双向绑定核心由 响应式系统 和 指令语法糖 共同实现,具体原理如下: 响应式系统 Vue通过数据劫持和依赖收集实现数据变化到视图的同步: • 数据劫持:在Vue…...
最优化方法Python计算:无约束优化应用——线性回归分类器
一、线性回归分类器 假设样本数据为 ( x i , y i ) (\boldsymbol{x}_i, y_i) (xi,yi),其中 i 1 , 2 , … , m i 1, 2, \dots, m i1,2,…,m。标签 y i y_i yi 取值于 k k k 个整数 { 1 , 2 , … , k } \{1, 2, \dots, k\} {1,2,…,k},从而构…...
【汇正自控阀门集团】签约智橙PLM,智橙助泵阀“以国代进”
签约智橙,汇正阀门的“以国代进”举措 随着阀门市场竞争日益激烈、市场需求日益多样化,无论是出口海外、以国代进,还是进军新能源、造船、油气等投资景气的下游市场,阀门企业能否在快速迭代产品、保持技术领先的同时,…...
【macOS】iTerm2介绍
iTerm2 和 iTerm 是 macOS 上两个不同的终端模拟器,虽然名字相似,但它们是两个独立的项目,且 iTerm2 是 iTerm 的现代化继承者。以下是它们的核心区别和演进关系: 1. 历史背景 项目诞生时间状态开发者iTerm2002 年已停止维护Greg…...
2025年五一假期旅游市场新趋势:理性消费、多元场景与科技赋能
2025年五一假期,国内旅游市场再次迎来爆发式增长,官方数据显示,假期期间国内出游人次达3.14亿,游客总消费1802.69亿元。尽管数据规模亮眼,但深入分析可发现,旅游市场正经历结构性变革——消费行为趋于理性、…...
第3章 模拟法
3.1 模拟法概述 模拟法设计思想 模拟法通过将现实问题抽象成计算机可识别的符号与操作,按逻辑顺序“模拟”其过程,从而得到结果;它不依赖复杂公式或高深技巧,只需理清问题背景与实现步骤即可。 示例:鸡兔同笼问题 题…...
16.状态模式:思考与解读
原文地址:状态模式:思考与解读 更多内容请关注:深入思考与解读设计模式 引言 在开发软件系统时,特别是当对象的行为会随着状态的变化而变化时,系统往往会变得复杂。你是否遇到过这样的情况:一个对象的行为在不同的状…...
ActiveMQ 源码剖析:消息存储与通信协议实现(二)
四、KahaDB 消息存储实现细节 (一)存储原理分析 KahaDB 作为 ActiveMQ 从 5.4 版本开始的默认消息存储引擎,其基于日志文件的存储原理具有独特的设计和优势 。在 KahaDB 的存储目录(如${activemq.data}/kahadb)下&am…...
明远智睿SD2351核心板:工业AIoT时代的创新引擎
在当今工业互联网飞速发展的浪潮中,人工智能(AI)与物联网(IoT)的深度融合正以前所未有的态势重塑着传统制造业的格局。从自动化生产线的精准控制到智能仓储的高效管理,从设备运行的实时监测到产品质量的严格…...
iPhone 和 Android 在日期格式方面的区别
整篇文章由iPhone 和 Android 在日期格式方面有所不同引起,大致介绍了,两种时间标准,以及在 JavaScript 下的格式转换方法。 Unix 时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。 iPhone 和 Android 在日期格式方面有所不同。其中,iPhone(iOS)使…...
使用VSCode在Windows 11上编译运行项目
使用VSCode在Windows 11上编译运行项目 VSCode是一个功能强大的跨平台代码编辑器,可以很好地支持C/C项目开发。以下是使用VSCode在Windows 11上编译运行此项目的详细步骤。 1. 安装VSCode 访问VSCode官网下载并安装VSCode安装完成后,启动VSCode 2. 安…...
边缘计算,运维架构从传统的集中式向分布式转变
在当今数字化时代,边缘计算的崛起正在改变着运维的格局。随着物联网、5G 等技术的快速发展,越来越多的数据和应用正在向边缘设备迁移,这给运维团队带来了新的挑战和机遇。 一、边缘计算崛起带来的运维挑战 边缘计算将计算和数据存储靠近数据…...
【基础篇】prometheus热更新解读
文章目录 本篇内容讲解热更新参数源码解读本篇总结本篇内容讲解 prometheus热更新源码解读 热更新参数 –web.enable-lifecycle : 代表开启热更新配置 修改配置文件发http请求# curl -X POST -vvv localhost:9090/-/reload * About to connect() to localhost port 9090 (…...
为了结合后端而学习前端的学习日志(1)——纯CSS静态卡片案例
前端设计专栏 使用纯CSS创建简洁名片卡片的学习实践 在这篇技术博客中,我将分享我的前端学习过程,如何使用纯HTML和CSS创建一个简洁美观的名片式卡片,就像我博客首页展示的那样。这种卡片设计非常适合作为个人简介、产品展示或团队成员介绍…...
汽车服务小程序功能点开发
汽车养护服务功能 智能保养预约:根据车辆品牌、型号及行驶里程,自动推荐保养项目,支持线上预约 4S 店或合作维修厂,选择服务时间与地点。故障诊断与维修:车主上传车辆故障现象,系统智能初步诊断࿰…...
SENSE2020BSI sCMOS科学级相机主要参数及应用场景
SENSE2020BSI sCMOS科学级相机是一款面向宽光谱成像需求的高性能科学成像设备,结合了背照式(Back-Side Illuminated, BSI)CMOS技术与先进信号处理算法,适用于天文观测、生物医学成像、工业检测等领域。以下是其核心特点及技术细节…...
《汽车噪声控制》复习重点
题型 选择 填空 分析 计算 第一章 噪声定义 不需要的声音,妨碍正常工作、学习、生活,危害身体健康的声音,统称为噪声 噪声污染 与大气污染、水污染并称现代社会三大公害 声波基本概念 定义 媒质质点的机械振动由近及远传播&am…...
物流无人机结构与载货设计分析!
一、物流无人机的结构与载货设计模块运行方式 1.结构设计特点 垂直起降与固定翼结合:针对复杂地形(如山区、城市)需求,采用垂直起降(VTOL)与固定翼结合的复合布局,例如“天马”H型无人机&am…...