基于TCP实现简单的聊天室
原文出处:《Go 语言编程之旅》第四章4.1节
基于TCP的聊天室
1、服务端
- 新用户到来,生成一个User的实例,代表该用户。
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
- 新开一个goroutine用于给用户发送消息
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
结合User结构体的MessageChannel,很容易知道,需要给某个用户发送消息,只需要往该用户的MessageChannel中写入消息即可。这里需要特别提醒下,因为sendMessage在一个新的goroutine中,如果函数的ch不关闭,该goroutine是不会退出的,因此需要注意不关闭ch导致goroutine泄露问题。
- 给当前用户发送欢迎信息,同时给聊天室所有的用户发送有新用户到来的提醒
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
- 将该新用户写入全局用户列表,也就是聊天室用户列表。同时控制用户超时退出,超过5分钟没有任何响应,则提出
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
读取用户的输入,并将用户信息发送给其他用户。
在bufio包中有多重方式获取文本输入,ReadBytes、ReadString和独特的ReadLine,对于简单的目的这些都有些复杂。在Go1,1中添加了一个新类型,Scabber,以便更容易的处理如按行读取输入序列或空格分隔单词等这类简单任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都提出了分隔标识。
// 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
- 用户离开,需要做登记,并给连天使其他用户发通知
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
完整代码
package main
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"sync"
"time"
)
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
// 给用户发送信息
type Message struct{
OwnerID int
Content string
}
var (
// 新用户到来,通过该channel进行登记
enteringChannel = make(chan *User)
// 用户离开,通过该channel进行登记
leavingChannel = make(chan *User)
// 广播专用的用户普通消息channel, 缓冲是尽可能避免出现异常情况阻塞
messageChannel = make(chan Message, 9)
)
func (u *User) String() string{
return u.Addr + ",UID:" + strconv.Itoa(u.ID) + ", Enter At:" + u.EnterAt.Format("2006-01-02 15:04:05+8000")
}
func main() {
listener, err := net.Listen("tcp",":2020")
if err != nil {
panic(err)
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConn(conn)
}
}
// broadcaster 用于记录聊天室用户,并进行消息广播
// 1. 新用户进来; 2.用户普通消息; 3.用户离开
func broadcaster(){
users := make(map[*User]struct{})
for {
select{
case user := <- enteringChannel:
// 新用户进入
users[user] = struct{}{}
case user := <- leavingChannel:
// 用户离开
delete(users, user)
// 避免goroutine泄露
close(user.MessageChannel)
case msg := <-messageChannel:
// 给所有在线用户发送消息
for user := range users {
if user.ID == msg.OwnerID{
continue
}
user.MessageChannel <- msg.Content
}
}
}
}
func handleConn(conn net.Conn){
defer conn.Close()
// 1. 新用户进来,构建该用户实例
user := &User{
ID: GenUserID(),
Addr: conn.RemoteAddr().String(),
EnterAt: time.Now(),
MessageChannel: make(chan string,8),
}
// 2. 当前在一个新的goroutine 中,用来进行读写操作,因此需要开一个goroutine用于读写操作
// 读写goroutine 之间通过channel 进行通信
go sendMessage(conn, user.MessageChannel)
// 3. 给当前用户发送欢迎信息;给所有用户告知新用户列表
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
// 4. 将该记录到全局的用户列表中,避免用锁
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
// 5. 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
// 6. 用户离开
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
}
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
// 生成用户id
var (
globalID int
idocker sync.Mutex
)
func GenUserID() int {
idocker.Lock()
defer idocker.Unlock()
globalID ++
return globalID
}
2、客户端
客户端的实现直接采用 《The Go Programming Language》一书对应的示例源码:ch8/netcat3/netcat.go 。
func main() {
conn, err := net.Dial("tcp", ":2020")
if err != nil {
panic(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
- 新开了一个 goroutine 用于接收消息;
- 通过 io.Copy 来操作 IO,包括从标准输入读取数据写入 TCP 连接中,以及从 TCP 连接中读取数据写入标准输出;
- 新开的 goroutine 通过一个 channel 来和 main goroutine 通讯;
基于TCP实现简单的聊天室的更多相关文章
- 基于TCP/IP的局域网聊天室---C语言
具备注册账号,群聊,查看在线人员信息,私发文件和接收文件功能,因为每个客户端只有一个属于自己的socket,所以无论客户端是发聊天消息还是文件都是通过这一个socket发送, 这也意味着服务器收发任何 ...
- 基于websocket实现的一个简单的聊天室
本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊.是基于websocket的注解方式编写的.(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知 ...
- 基于LINUX的多功能聊天室
原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...
- Android简单的聊天室开发(client与server沟通)
请尊重他人的劳动成果.转载请注明出处:Android开发之简单的聊天室(client与server进行通信) 1. 预备知识:Tcp/IP协议与Socket TCP/IP 是Transmission ...
- Netty学习笔记(四) 简单的聊天室功能之服务端开发
前面三个章节,我们使用了Netty实现了DISCARD丢弃服务和回复以及自定义编码解码,这篇博客,我们要用Netty实现简单的聊天室功能. Ps: 突然想起来大学里面有个课程实训,给予UDP还是TCP ...
- 基于EPOLL模型的局域网聊天室和Echo服务器
一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...
- 基于 OpenResty 实现一个 WS 聊天室
基于 OpenResty 实现一个 WS 聊天室 WebSocket WebSocket 协议分析 WebSocket 协议解决了浏览器和服务器之间的全双工通信问题.在WebSocket出现之前,浏览 ...
- 玩转Node.js(四)-搭建简单的聊天室
玩转Node.js(四)-搭建简单的聊天室 Nodejs好久没有跟进了,最近想用它搞一个聊天室,然后便偶遇了socket.io这个东东,说是可以用它来简单的实现实时双向的基于事件的通讯机制.我便看了一 ...
- TCP/IP以及Socket聊天室带类库源码分享
TCP/IP以及Socket聊天室带类库源码分享 最近遇到个设备,需要去和客户的软件做一个网络通信交互,一般的我们的上位机都是作为客户端来和设备通信的,这次要作为服务端来监听客户端,在这个背景下,我查 ...
- 简单的聊天室代码php+swoole
php swoole+websocket 客户端代码 <!DOCTYPE html> <html> <head> <title></title&g ...
随机推荐
- nuxt.js 使用 Typescript 在 VSCode 报错: File 'xxx/components/Logo.vue' is not a module. Vetur(2306)
nuxt.js 生成的默认文件 components/Logo.vue 源码大概如下: 1 <template> 2 <svg 3 class="NuxtLogo" ...
- 基于webman实现的服务层框架-webman-biz-framework
简介 webman的基础上扩展的一个服务层框架,基于分层体系结构的代码模式. 如果觉得有用,可以帮我在webman-biz-framework点个小星星哟,也希望大家交流 分层体系结构的代码模式 什么 ...
- mysql基础中的基础 函数
前段时间b站看sql基础语法,在此做一总结 1.基本函数 mysql中的函数基本可以分为单行函数和分组函数,单行函数用于处理单个的数据,分组函数则是传输一组值过去进行处理.单行函数有可分为字符函数,数 ...
- js回忆录(1) -- 变量,null 和 undefined
变量:这个东西不同的高度的人看法不一样,甚至不同领域的人的看法也不一样,当初上机组的时候依稀记得老师说这个寄存器那个锁存器什么的,然后根据高低电位就变成了二进制认识的0和1了,当然了具体细节本博主大人 ...
- 微信小程序分包体积优化建议
代码包体积优化 启动性能优化最直接的手段是降低代码包大小,代码包大小直接影响了下载耗时,影响用户启动小程序时的体验. 开发者可以采取以下手段优化代码包体积: 1. 合理使用分包加载 推荐所有小程序使用 ...
- Go配置管理神器—Viper中文教程
Viper中文教程 Viper是适用于Go应用程序的完整配置解决方案.它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式. 安装 go get github.com/spf13/vip ...
- 分布式一致性算法-Paxos、Raft、ZAB、Gossip
为什么需要一致性 数据不能存在单个节点(主机)上,否则可能出现单点故障. 多个节点(主机)需要保证具有相同的数据. 一致性算法就是为了解决上面两个问题. 一致性算法的定义 一致性就是数据保持一致, ...
- 【Spring】JdbcTemplate的使用方法
概念和准备 什么是 JdbcTemplate Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作 准备工作 引入相关 jar 包 在 spring 配置文件 ...
- 『Plotly实战指南』--饼图绘制基础篇
在数据可视化的世界里,饼图是一种直观且广泛使用的图表类型. 它能够将数据各个部分占整体的比例关系清晰地展现出来,适用于诸如市场占有率分析.调查结果分布.预算分配等多个领域. 饼图以扇形面积比例直观展示 ...
- Go初入武林之乘法表
为统一管理源码, 请到gitee中查看. GoTimesTable