【计算机网络实战】简易IM(一)websocket后台demo

前言

虽然已经学习过计算机网络的理论课程,也做过相关的搭网实验,但是感觉缺乏更进一步的实践,导致很多理论知识都流于表面,并且正在随着时间流逝而快速被遗忘。最近几天刚好有空,打算抽出时间来学习如何从0开始做一个简易的IM系统,借此夯实网络通信的基础。

关键词

go ;websocket

参考代码来源

git clone https://github.com/klintcheng/chatdemo.git

这是一位大佬在Github上开源的demo,他还有另外一个微服务版、支持企业级高并发的项目Kim,掘金小册 分布式IM原理与实战: 从0到1打造即时通讯云 就是作者为Kim写的。这本小册我正在看,感觉写得很不错,理论知识讲得很深入、扎实,整体逻辑清晰、层层递进。(非打广告,只是我学习别人的项目并且记录心得,理应注明一下出处,尊重他人的知识产权和劳动成果)

理论知识

websocket

什么是websocket?

websocket可以理解为是http的增强版,所以它使用的端口也是80。

它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此 API,可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

为什么是websocket?

IM通信的一个关键在于长连接,早期,在浏览器(IE6)还不兼容websocket的时候,一般采用如下两种方式实现长连接:

  • 集成flash或其插件(现已废弃)
  • 使用http的长轮询技术

后者有哪些缺点呢?

  • 长轮询技术涉及多次连接的建立和释放,对服务性能有影响。
  • 协议不兼容,http是超文本传输协议,但是长连接需要的是私有二进制协议。

websocket就像是让更专业的人做更专业的事。所以随着浏览器对协议的兼容,使用websocket势在必行。

下图是websocket连接模型的生命周期:

image.png

具体实现

下面这段服务端的代码就是依据上面的生命周期流程写的:

// Start server
func (s *Server) Start() error {
	mux := http.NewServeMux()
	log := logrus.WithFields(logrus.Fields{
		"module": "Server",
		"listen": s.address,
		"id":     s.id,
	})


	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		conn, _, _, err := ws.UpgradeHTTP(r, w)
		if err != nil {
			conn.Close()
			return
		}
		// 读取userId
		user := r.URL.Query().Get("user")
		if user == "" {
			conn.Close()
			return
		}

		// 添加到会话管理中
		old, ok := s.addUser(user, conn)
		if ok {
			// 断开旧的连接
			old.Close()
			log.Infof("close old connection %v", old.RemoteAddr())
		}
		log.Infof("user %s in from %v", user, conn.RemoteAddr())

		go func(user string, conn net.Conn) {
			err := s.readloop(user, conn)
			if err != nil {
				log.Warn("readloop - ", err)
			}
			conn.Close()
			// 删除用户
			s.delUser(user)

			log.Infof("connection of %s closed", user)
		}(user, conn)
	})
	log.Infoln("started")
	return http.ListenAndServe(s.address, mux)
}

代码的其他部分也很好地体现了模拟对象的性质:

先来看websocket对象,一个websocket对象包含了如下内容:

type ServerOptions struct {
	writewait time.Duration //写超时时间
	readwait  time.Duration //读超时时间
}

type Server struct {
	once    sync.Once  //在需要被执行的部分只执行一次
	options ServerOptions //读超时、写超时时限
	id      string    
	address string   
	sync.Mutex  //互斥区,用于并发控制锁定资源
	users map[string]net.Conn //用于模拟会话列表
} 

所谓的同账号互踢,就是用相同账号的新连接替换掉旧链接:

func (s *Server) addUser(user string, conn net.Conn) (net.Conn, bool) {
	s.Lock()
	defer s.Unlock()
	old, ok := s.users[user]
	s.users[user] = conn
	return old, ok
}

这里穿插一个关于Go的知识点:

关于其中defer的用法,最直接的说法是延迟调用,最直观的理解就是在该函数返回前会执行defer对应的那条语句的意思(可以理解为现在还不想执行,比如释放锁,释放资源,但是之后一定要做,又怕之后忘记了,所以先写上,然后加个defer,它到时候自然会执行)

删除账号类似,在这里就是把map中的对应数据清除:

func (s *Server) delUser(user string) {
	s.Lock()
	defer s.Unlock()
	delete(s.users, user)
}

这里的关闭连接就是通过for循环遍历map里面的所有net.Conn并调用Close()接口关闭:

func (s *Server) Shutdown() {
	s.once.Do(func() {
		s.Lock()
		defer s.Unlock()
		for _, conn := range s.users {
			conn.Close()
		}
	})

}

在demo中,通过遍历所有用户并打印文本消息的方式模拟广播:

	for u, conn := range s.users {
		if u == user { // 不发给自己
			continue
		}
		logrus.Infof("send to %s : %s", u, broadcast)
		err := s.writeText(conn, broadcast)
		if err != nil {
			logrus.Errorf("write to %s failed, error: %v", user, err)
		}
	}

这类操作都要涉及到并发编程的问题,所以要加锁保证操作过程的独立性。

readloop,顾名思义:循环地读取帧数据,直到连接关闭或者发生错误,才返回对应的内容。大体框架是这样的:

func (s *Server) readloop(user string, conn net.Conn) error {
	for {
		frame, err := ws.ReadFrame(conn)
		if err != nil {
			return err
		}
		if frame.Header.OpCode == xxx {
			...
			continue
		}
		if frame.Header.OpCode == ws.OpClose {
			return errors.New("remote side close the conn")
		}
                ...
		// 接收文本帧内容
		if frame.Header.OpCode == ws.OpText {
			xxx
		} else if frame.Header.OpCode == ws.OpBinary {
			xxx
		}
	}
}

TCP协议中规定,当双方超过一定时间均未发送数据,则默认为断开连接(两军问题的可能解,没有回应也是一种回应)。所以源代码中函数开头有如下一行:
_ = conn.SetReadDeadline(time.Now().Add(s.options.readwait))
它设定了一个连接过期时限。


demo中只对后台进行了模拟,按照代码作者的说法,可以自行找一个可以提供前端可视化输入的网站来验证效果,比如

coolaf (截止笔者发文时该网站仍能访问)

运行

按运行Go项目的方法编译运行即可。

注意:在界面中输入的url的协议不是http/https,而是这里对应的ws。

至此,对demo的学习就结束了。笔者也对websocket协议的应用以及网络通信的基本流程有了粗略的了解。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYTURBi1' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片