{"id":19845502,"url":"https://github.com/smileexpression/im","last_synced_at":"2025-10-18T14:27:13.465Z","repository":{"id":225953249,"uuid":"767336718","full_name":"smileexpression/IM","owner":"smileexpression","description":"A repo to learn IM.","archived":false,"fork":false,"pushed_at":"2024-03-08T05:02:30.000Z","size":7835,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-11T12:32:37.398Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smileexpression.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-05T05:29:55.000Z","updated_at":"2024-03-06T15:28:50.000Z","dependencies_parsed_at":"2024-10-11T13:41:39.245Z","dependency_job_id":"84e97461-6ee3-4e19-abeb-47a08f5988cd","html_url":"https://github.com/smileexpression/IM","commit_stats":null,"previous_names":["arronvague/im","smileexpression/im"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smileexpression%2FIM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smileexpression%2FIM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smileexpression%2FIM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smileexpression%2FIM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smileexpression","download_url":"https://codeload.github.com/smileexpression/IM/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241224393,"owners_count":19929919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-12T13:08:11.804Z","updated_at":"2025-10-18T14:27:13.376Z","avatar_url":"https://github.com/smileexpression.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# IM学习笔记\n\n为什么选择go？\n\n1. **并发模型**：Go 语言的并发模型是它的一大特色。Go 语言的 goroutine（轻量级线程）和 channel（用于在 goroutine 之间传递数据和同步的机制）使得并发编程更加简单和高效。\n2. **静态类型和编译**：Go 是静态类型语言，这意味着在编译时就会发现很多类型错误，而不是在运行时。此外，Go 语言的编译速度非常快，这使得开发过程更加高效。\n3. **内置网络库**：Go 语言有强大的内置网络库，包括 http、grpc 等，非常适合开发网络应用和服务端应用。\n4. **跨平台**：Go 语言支持跨平台编译，你可以在一种操作系统下编译生成另一种操作系统的可执行程序。\n5. **垃圾回收**：Go 语言有垃圾回收功能，这意味着开发者不需要（也不能）自己管理内存，降低了内存泄漏的可能性。\n6. **简洁和清晰的语法**：Go 语言的语法简洁而清晰，易于学习，易于阅读，易于维护。\n7. **标准库**：Go 语言拥有丰富的标准库，覆盖了 I/O、文本处理、图像处理、数据库、网络编程、并发编程等许多领域。\n8. **工具链**：Go 语言的工具链非常完整，包括用于下载依赖、格式化代码、生成文档、运行测试、编译和安装程序等的工具。\n9. **云原生支持**：Go 语言是 Docker 和 Kubernetes 等多个重要的云原生项目的首选语言，对云原生应用有很好的支持。\n\n## 3、深入理解WebSocket协议\n\n### 3.1简介\n\nWebSocket：全双工、双向通信。使用自定义协议，客户端和服务端之间发送非常少量数据。\n\nhttp：请求-相应模式，半双工通信。字节级开销。\n\n\u003e AJAX 是 \"Asynchronous JavaScript and XML\"（异步 JavaScript 和 XML）的缩写。它不是一种新的编程语言，而是一种使用现有的标准和技术的新方法。AJAX 是一种在无需重新加载整个网页的情况下，能够更新部分网页的技术。\n\u003e\n\u003e 在 AJAX 中，JavaScript 通过异步方式与服务器进行数据交换，这意味着它可以在不影响页面的其余部分或用户交互的情况下，请求额外的数据。这使得网页在等待服务器响应时可以继续处理其他事情，而不会阻塞或延迟，从而极大地提高了用户体验。\n\u003e\n\u003e AJAX 的工作原理如下：\n\u003e\n\u003e 1. 用户执行某个动作，如点击一个按钮，这会触发 JavaScript 事件。\n\u003e 2. JavaScript 创建一个 XMLHttpRequest 对象，然后向服务器发送一个请求，这个请求可以是获取数据，也可以是发送数据。\n\u003e 3. 服务器处理这个请求，然后返回一个响应。\n\u003e 4. JavaScript 使用这个响应更新网页。\n\u003e\n\u003e 虽然 AJAX 的名字中包含 \"XML\"，但实际上它可以处理各种数据格式，包括纯文本、HTML、JSON，甚至是二进制数据。\n\u003e\n\u003e AJAX 技术大大改善了网页的响应性和灵活性，使得网页能够提供类似于桌面应用的交互体验。\n\n### 3.2WebSocket复用了HTTP的握手通道\n\nWebSocket复用了http的握手通道。ajax就是在这样的通道上完成数据传输的，只不过ajax交互是短连接，在一次 request-\u003eresponse 之后，“通道”连接就断开了。\n\n### 3.3HTTP协议升级为WebSocket协议\n\n**在这个握手的过程当中，客户端和服务端主要做了两件事情：**\n\n- 1）建立了一条连接“握手通道”用于通信（这点和HTTP协议相同，不同的是HTTP协议完成数据交互后就释放了这条握手通道，这就是所谓的“短连接”，它的生命周期是一次数据交互的时间，通常是毫秒级别的）；\n- 2）将HTTP协议升级到WebSocket协议，并复用HTTP协议的握手通道，从而建立一条持久连接。\n\n注意：维持“长连接”需要消耗服务器资源。\n\n### 3.4WebSocket的帧和数据分片传输\n\nHTTP协议是基于TCP实现的，HTTP发送数据也是分包转发的，就是将大数据根据报文形式分割成一小块一小块发送到服务端，服务端接收到客户端发送的报文后，再将小块的数据拼接组装。WebSocket协议也是通过分片打包数据进行转发的，不过策略上和HTTP的分包不一样。\n\nWebSocket通信中，客户端发送数据分片是有序的，这一点和HTTP不一样，HTTP将消息分包之后，是并发无序的发送给服务端的，包信息在数据中的位置则在HTTP报文中存储，而WebSocket仅仅需要一个FIN比特位就能保证将数据完整的发送到服务端。\n\n### 3.5Websocket连接保持和心跳检测\n\nWebSocket长连接浪费服务端资源。但是，举个例子：你几个月没有和一个QQ好友聊天了，突然有一天他发QQ消息告诉你他要结婚了，你还是能在第一时间收到。那是因为，客户端和服务端一直再采用心跳来检查连接。\n\n\u003e在即时通讯（IM）系统中，服务器发送心跳的一个主要原因是，服务器通常需要维护与多个客户端的连接，而每个客户端可能会有不同的网络条件和活动状态。通过定期从服务器发送心跳消息，服务器可以更有效地管理和监控所有的连接状态，及时检测到任何断开的连接，并可以在必要时关闭那些无响应的连接，释放资源。\n\n\u003e`go func()` 是 Go 语言中创建协程（goroutine）的一种方式。在 Go 语言中，协程是一种轻量级的线程，由 Go 运行时管理。\n\u003e\n\u003e协程在 Go 中是非常重要的一个特性，它使得并发编程变得非常简单。你可以轻易地启动成千上万的协程，而不需要担心线程切换的开销。这使得 Go 语言非常适合编写高并发的程序。\n\n**线程**：\n\n1. 线程是操作系统层面的并发执行实体，每个线程都有自己的栈和寄存器，线程切换需要涉及到**内核态和用户态的切换**，开销较大。\n2. 线程之间的通信通常使用**共享内存**，这需要程序员小心处理同步和锁，以避免出现竞态条件。\n3. 线程的生命周期由**操作系统**来管理。\n\n**协程**：\n\n1. 协程是程序级别的，也就是说它完全运行在**用户态**，协程切换不涉及内核态和用户态的切换，开销较小。\n2. 协程可以使用共享内存进行通信，但更常见的是通过使用**通道（Channel）**进行通信。这种方式更安全，更容易避免竞态条件。\n3. 协程的生命周期由**程序**来管理。\n\n## 4、开始动手，快速搭建高性能、可拓展的IM系统\n\n### 4.1系统架构和代码文件目录结构\n\n![跟着源码学IM(六)：手把手教你用Go快速搭建高性能、可扩展的IM系统_10.jpg](http://www.52im.net/data/attachment/forum/202004/26/143945a1vvlldxb3vv3z03.jpg)\n\n\u003e WebSocket 协议在即时通讯（IM）系统的接入层中常常被选择，其主要优点包括：\n\u003e\n\u003e 1. **全双工通信**：WebSocket 提供了全双工通信，这意味着服务器和客户端可以在任何时候互相发送数据，而不需要等待对方的响应。这对于 IM 系统来说非常重要，因为它允许服务器实时地向客户端推送新的消息。\n\u003e 2. **低延迟**：与 HTTP 轮询或长轮询相比，WebSocket 的延迟更低。这是因为一旦建立了 WebSocket 连接，数据就可以立即在服务器和客户端之间传输，而不需要每次发送请求和等待响应。\n\u003e 3. **减少网络流量**：由于 WebSocket 连接一旦建立就会保持打开状态，因此它可以减少由于频繁建立和关闭 HTTP 连接所产生的网络流量。\n\u003e 4. **实时性**：WebSocket 支持服务器向客户端推送信息，客户端无需请求即可实时接收信息，这对于 IM 系统来说非常重要。\n\u003e 5. **兼容性**：WebSocket 被所有主流浏览器支持，因此可以在多种平台和设备上使用。\n\n### 4.210行代码万能模版渲染\n\n暂时渲染不出来\n\n### 4.3注册、登录和鉴权\n\n```go\ntype User struct {\n        Id         int64     `xorm:\"pk autoincr bigint(64)\" form:\"id\" json:\"id\"`\n        Mobile   string                 `xorm:\"varchar(20)\" form:\"mobile\" json:\"mobile\"`\n        Passwd       string        `xorm:\"varchar(40)\" form:\"passwd\" json:\"-\"`   // 用户密码 md5(passwd + salt)\n        Avatar           string                 `xorm:\"varchar(150)\" form:\"avatar\" json:\"avatar\"`\n        Sex        string        `xorm:\"varchar(2)\" form:\"sex\" json:\"sex\"`\n        Nickname    string        `xorm:\"varchar(20)\" form:\"nickname\" json:\"nickname\"`\n        Salt       string        `xorm:\"varchar(10)\" form:\"salt\" json:\"-\"`\n        Online     int        `xorm:\"int(10)\" form:\"online\" json:\"online\"`   //是否在线\n        Token      string        `xorm:\"varchar(40)\" form:\"token\" json:\"token\"`   //用户鉴权\n        Memo      string        `xorm:\"varchar(140)\" form:\"memo\" json:\"memo\"`\n        Createat   time.Time        `xorm:\"datetime\" form:\"createat\" json:\"createat\"`   //创建时间, 统计用户增量时使用\n}\n```\n\n我认为比较重要的几个字段\n\n- Salt（随机值，增强安全性）\n- Online\n- Token\n\n存储了token标示用户在用户登录之后，http协议升级为websocket协议进行鉴权。\n\n注册和登录的业务逻辑比较简单。\n\n比较难的是util.Bind(request, \u0026user)将用户参数绑定到user对象上，具体实现细节读者可以自行研究，主要模仿了Gin框架的参数绑定。\n\n用户客户端如何请求建立与服务端的websocket长连接？\n\n服务端收到建立连接的请求之后，会对客户端请求进行校验，以确实是否建立长连接，然后将这条长连接的句柄添加到map当中(因为服务端不仅仅对一个客户端服务，可能存在千千万万个长连接)维护起来。\n\n```go\n//userid和Node映射关系表\ntype Node struct {\n        Conn *websocket.Conn\n        //并行转串行,\n        DataQueue chan []byte\n        GroupSets set.Interface\n}\n```\n\n每一个用户都是Node的实例，消息存放在管道（channel）DataQueue中，GroupSets中存放自己的聊天对象（包括个人用户和群聊）。\n\n注意：建立userid和Node映射的时候加写锁，保证原子安全。\n\n```go\n        rwlocker.Lock()\n        clientMap[userId] = node\n        rwlocker.Unlock()\n```\n\n服务端和客户端建立了长链接之后，会开启两个协程专门来处理客户端消息的收发工作。\n\n例子：客户端A发送消息给客户端B，服务端充当中介，使用recvproc接收客户端A发送来的消息，通过一系列处理，将消息填入B的DataQueue中。sendproc检测到B的管道中有消息，就会将消息发送给客户端B。\n\n### 4.4实现单聊和群聊\n\n|      | 服务端接收               | 服务端发送                 |\n| ---- | ------------------------ | -------------------------- |\n| 单聊 | 填入目标的管道           | 无区别，管道收到消息就发送 |\n| 群聊 | 遍历群友，填入群友的管道 | 同上                       |\n\n添加好友的业务逻辑也很简单，但是要注意用事务，同时添加双方的联系。\n\n### 4.5发送表情和图片\n\n为什么是将文件的内容复制到新文件中，而不是将原文件存到本地？\n\n\u003e 这是因为在处理 HTTP 文件上传时，上传的文件通常是以数据流的形式存在的，而不是一个实体的文件。当你在浏览器中选择一个文件并点击上传时，你的浏览器会将这个文件的内容读取到内存中，然后通过 HTTP 请求将这些内容发送到服务器。在服务器端，这些内容会被封装成一个临时的文件对象（在 Go 语言中，这个对象通常是一个 `*multipart.File` 类型的对象）。\n\u003e\n\u003e 因此，当服务器接收到这个上传的文件时，它实际上是在接收一个包含了文件内容的数据流，而不是一个实体的文件。服务器需要做的是将这个数据流保存到一个新的文件中，这就是为什么需要将上传的文件的内容复制到新的文件中。\n\u003e\n\u003e 另外，这种方式还有以下几点好处：\n\u003e\n\u003e 1. 可以在保存文件时对文件名进行修改，例如添加时间戳或随机字符串，以避免文件名冲突。\n\u003e 2. 可以在保存文件时对文件内容进行处理，例如进行压缩或加密。\n\u003e 3. 可以控制文件保存的位置，例如保存到特定的目录或者分布式文件系统中。\n\n## 参考文献\n\n[**跟着源码学IM(六)：手把手教你用Go快速搭建高性能、可扩展的IM系统**](http://www.52im.net/thread-2988-1-1.html)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmileexpression%2Fim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmileexpression%2Fim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmileexpression%2Fim/lists"}