Loading...
Loading...
Loading...

目录


Golang实战及时通讯系统

计算机编程 发布于:2022/3/16/23:50 1003 vk go 最近编辑于2 年,9 月前 预计阅读时长:34min

实战教程适用于有一定golang的基础,或者是认真读完前几篇的小伙伴,实战仅供语法巩固,并非企业版

往期回顾

基础server构建

写两个文件,一个文件(server.go)里写核心服务器的程序,另一个文件(main.go)里写启动服务器的程序

server.go

package main

import (
	"fmt"
	"net"
)

type Server struct {
	Ip   string
	Port int
}

//创建一个server对象
func Newserver(ip string, port int) *Server {
	server := &Server{
		Ip:   ip,
		Port: port,
	}
	return server
}
func (this *Server) Handler(conn net.Conn) {
	//连接
	fmt.Println("连接成功")

}

//启动server服务
func (this *Server) Start() {
	//listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	for {
		//accept
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener accept err:", err)
			continue
		}
		//do handler
		go this.Handler(conn)

	}

	//close
	defer listener.Close()
}

功能很简单,就是一直循环监听一个端口,如果有连接了就打印连接成功。先创建一个server结构体,然后再创建一个server对象,给创建两个类方法Start()和Handler()分别用于启动和处理连接后的逻辑,

这里用大写表示其他文件也可以访问到。handler那里启动一个go程用来异步处理连接。关闭的话只需用derfer处理在函数生命周期结束之前关闭就行

main.go

package main

func main() {
	server := Newserver("127.0.0.1", 8888)
	server.Start()
}

两个文件同时编译运行在浏览器输入:127.0.0.1:8888就可以在控制台成功看到:连接成功的输出

用户上线及广播的功能

要实现的功能是:上线一个新用户,那个新用户会收到已上线的提示,同时,其他在线用户也会收到已上线的提示。

这时候就需要存储所用的上线用户了,复杂了用数据库,这里我们只为了简单实现只需要创建一个用户的hash map即可。上一个功能里处理用户上线的handler函数只在用户上线的时候打印了上线成功,我们只需要新增一点处理逻辑就行。

在上线的同时,新建一个用户对象,每个用户都有他自己对应的channel,以及自己的地址名字,用户上线时将用户的地址名字作为键,用户对象作为值传入那个用户的hash map中。在用户新建的时候开启一个新的函数一直监听自己的channel中是否消息,如果有的话反馈给客户端即可,

最后只需要在server中创建一个广播函数,一但有用户上线就“将xxx上线成功”这句话写入channel中。

为了使代码可读性更高,将用户行为些逻辑单独抽离出来写道一个新的文件中

user.go

package main

import "net"

type User struct {
	Name string
	Addr string
	C    chan string
	conn net.Conn
}

func (this *User) ListenMessage() {
	for {
		msg := <-this.C

		this.conn.Write([]byte(msg + "\n"))
	}
}
func NewUser(conn net.Conn) *User {
	userAddr := conn.RemoteAddr().String()	//这句话可以获取客户端地址
	user := &User{
		Name: userAddr,
		Addr: userAddr,
		C:    make(chan string),
		conn: conn,						//新建的时候需要与服务器产生练习,于是传入这个conn对象
	}
	go user.ListenMessage()				//这个函数一直监听着自己的channel管道,只要有消息,就写入conn
	return user

}

这时候server.go也需要稍作修改,增加发消息的函数,结构体新增map

package main

import (
	"fmt"
	"net"
	"sync"
)

type Server struct {
	Ip   string
	Port int
	//存储用户的map
	OnlineMap map[string]*User
	mapLock   sync.RWMutex		//由于这个OnlineMap 是全局的变量,所以为了保证数据的准确性需要上锁
}

//创建一个server对象
func Newserver(ip string, port int) *Server {
	server := &Server{
		Ip:        ip,
		Port:      port,
		OnlineMap: make(map[string]*User),
	}

	return server
}
func (this *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

	this.mapLock.Lock()

	for _, client := range this.OnlineMap {		//一但有用户上线循环用户map将消息告诉每一个用户

		client.C <- sendMsg
	}
	this.mapLock.Unlock()
}
func (this *Server) Handler(conn net.Conn) {
	//连接
	user := NewUser(conn)
	this.mapLock.Lock()
	this.OnlineMap[user.Name] = user
	this.mapLock.Unlock()
	this.BroadCast(user, "connect success")

}

//启动server服务
func (this *Server) Start() {
	//listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	for {
		//accept
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener accept err:", err)
			continue
		}
		//do handler
		go this.Handler(conn)

	}

	//close
	defer listener.Close()
}

使用nc模拟两个客户端连接的过程

成功实现我们想要的功能,只需nc是什么以及怎么用自行百度,这里不再赘述

用户消息广播

上一个例子中,我们对用户上线进行了广播,那么接下来就好写了,因为这个广播函数是可以复用的,只需要将connect success换成我们从客户端读取过来的消息即可

从客户端读消息用Read()方法,它接受的参数是读取的长度。在客户端上线广播发送后创建一个新的go程用来循环读取客户端消息并且发送,当然直接写代码也无所谓。主要是体现一下有专门的go程用来处理消息广播更加专业一点。

异常处理:当读的读写为0的时候就代表客户端合法关闭了,那么这时候应该输出下线,当有错误的时候且错误不是io.EOF(读到文件的末尾)时,也要打印出错误信息

只需在handler函数中新增代码(旧代码用…代替):

func (this *Server) Handler(conn net.Conn) {

	...
	this.BroadCast(user, "connect success")
	...
	buf := make([]byte, 4096)
	go func() {
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				fmt.Println("offline")
				this.BroadCast(user,"offline")

				return
			}
			if err != nil && err != io.EOF {
				fmt.Println(err)
				return

			}
			msg := string(buf[:n-1]) //把末尾的回车去掉
			this.BroadCast(user, msg)
		}
	}()

}

优雅的代码

有没有发现我们的代码目前很不优雅,用户的下线上线,消息等逻辑都在server.go里面,我们这次需要把这些抽离到user.go中。

比如发送消息 this.BroadCast(user, msg),当把这句话抽离的user.go里面肯定不能用this访问这个BroadCast方法了,因为这是server的方法,所以在创建user对象时我们需要新传入一个server对象,以便可以调用它里面的方法。user.go新增函数:Online(),Offline(),SendMsg()

user.go

package main

import "net"

type User struct {
	Name   string
	Addr   string
	C      chan string
	conn   net.Conn
	server *Server
}

func (this *User) ListenMessage() {
	for {
		msg := <-this.C

		this.conn.Write([]byte(msg + "\n"))
	}
}
func (this *User) Online() {
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name] = this
	this.server.mapLock.Unlock()
	this.server.BroadCast(this, "connect success")
}
func (this *User) Offline() {
	this.server.mapLock.Lock()
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()
	this.server.BroadCast(this, "offline")
}
func (this *User) SendMsg(msg string) {
	this.server.BroadCast(this, msg)

}

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()
	user := &User{
		Name:   userAddr,
		Addr:   userAddr,
		C:      make(chan string),
		conn:   conn,
		server: server,
	}
	go user.ListenMessage()
	return user

}

server.go中的handler变成了

func (this *Server) Handler(conn net.Conn) {
	//连接
	user := NewUser(conn, this)
	user.Online()
	buf := make([]byte, 4096)
	go func() {
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				user.Offline()
				return
			}
			if err != nil && err != io.EOF {
				fmt.Println(err)
				return

			}
			msg := string(buf[:n-1]) //把末尾的回车去掉
			user.SendMsg(msg)
		}
	}()

}

可读性是不是高多了

在线用户查询

查询用户就不是广播了,是单播,只需要给发送查询指令的那个用户发送结果。至于怎么处理查询指令,我们只需要在用户的SendMsg里加一个判断,在发送消息前判断知否为查询在线用户指令,不是的话去发送广播,是的话单播

//发送消息给指定用户
func (this *User) SenToUser(msg string) {

	this.conn.Write([]byte(msg))

}

func (this *User) SendMsg(msg string) {
	if msg == "users" {
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			OnlineMsg := "User " + user.Name + " Is Online\n"
			this.SenToUser(OnlineMsg)
		}
		this.server.mapLock.Unlock()
	} else {
		this.server.BroadCast(this, msg)

	}

}

修改用户名

和上个例子一样,只需要修改user的SendMsg函数就行,多加一个判断,判断用户发来的消息是否为修改名字的指令

判断要修改的名字是否已经存在,存在的话修改失败,不存在的话修改成功

func (this *User) SendMsg(msg string) {
	'''
	'''
	 else if len(msg) > 7 && msg[:7] == "rename|" {
		newName := strings.Split(msg, "|")[1]
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SenToUser("sorry, this name is already used\n")

		} else {
			this.server.mapLock.Lock()
			delete(this.server.OnlineMap, this.Name)
			this.server.OnlineMap[newName] = this
			this.server.mapLock.Unlock()
			this.Name = newName
			this.SenToUser("success\n")

		}

	'''
	'''
}

超时强踢功能

当客户端一段时间没发送消息,我们就认为它不再活跃,把它踢出,要想实现这个功能,用到的东西就是计时器。设定一计时器比如10秒一但它计时完成那么就执行踢出用户的功能,还需要一个更新计时器判断,用户有消息输入时就重置计时器,并且认为改用户是活跃的。

思考一下之前学过的啥知识能满足这个问题。没错!就是selcet与case。

time.After(time.Second*10)这是计时器的写法,计时10秒,把它当作条件传入case,计时完成,这个case下的代码就会被执行。

那么如何重置这个计时器呢?答案就是再执行一遍这个代码,它自然就被重置了。

func (this *Server) Handler(conn net.Conn) {

	...

	...
	//定义是否活跃
	isLive := make(chan bool)
	go func() {
		for {
			...
			...
			isLive <- true
		}
	}()
	for {sil
		select {
		case <-isLive:
		case <-time.After(time.Second * 10):
			//踢出超时用户
			user.SenToUser("you have been kicked")
			//主动销毁资源
			close(user.C)
			conn.Close()
			return

		}
	}

}

当用户有输入的时候执行 isLive <- true,这时候 case <-isLive:条件触发这里面可以啥也不用写。因为我们只想让time.After(time.Second * 10)重新执行一遍重置定时器对吧,当isLive为True的时候就已经激活了select,select有个特点就是当所有case条件为true的时候,它会随机选择一个执行。那么它怎么知道其他case是否满足的呢?当然是执行一遍”<- “右边的代码,看是否满足条件。

私聊功能

很简单,类似于重置用户名和查询所有用户的功能,我甚至不准备写了,但是想了想还是写上了,因为毕竟这也是一个全新的功能。

只需要获取对方的名字,根据对方的名字在onlinemap中获取该对象,再调用对方的发送给指定用户的方法就行,类似于下线这种只告诉自己。

只不过这个消息变成了从别的客户端获取的了

func (this *User) SendMsg(msg string) {

'''
'''

	else if len(msg) > 4 && msg[:3] == "to|" {

		remoteName := strings.Split(msg, "|")[1]
		if remoteName == "" {
			this.SenToUser("格式错误")
			return
		}
		remoteUser, ok := this.server.OnlineMap[remoteName]
		if !ok {
			this.SenToUser("用户名不存在")
			return
		}
		content := strings.Split(msg, "|")[2]
		if content != "" {
			remoteUser.SenToUser(this.Name + ":" + content)
		}

	} 

'''

'''
}

客户端

之前发送请求使用nc模拟的,现在我们自己写一个客户端吧。写法和服务端差不多,只要实现接受用户输入的功能,过程不再赘述。客户端为了精简没有异常处理,如果需要自行加入

package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"os"
)

type Client struct {
	ServerIp   string
	ServerPort int
	Name       string
	conn       net.Conn
	flag       int
}

func NewCilent(serverIp string, serverPort int) *Client {
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
		flag:       666,
	}
	conn, _ := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	client.conn = conn
	return client
}

var serverIp string
var serverPort int

func init() {
	flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置ip默认127.0.0.1")
	flag.IntVar(&serverPort, "port", 8888, "设置端口默认8888")

}
func (client *Client) menu() bool {
	var flag int
	fmt.Println("1.talk with everybody")
	fmt.Println("2.talk with the person")
	fmt.Println("3.update username")
	fmt.Println("4.exit")
	fmt.Scanln(&flag)
	if flag >= 0 && flag <= 3 {
		client.flag = flag
		return true
	} else {
		return false
	}
}
func (client *Client) UpdateName() bool {
	fmt.Println("enter your username:")
	fmt.Scanln(&client.Name)
	sendMsg := "rename|" + client.Name + "\n"
	client.conn.Write([]byte(sendMsg))
	return true

}
func (client *Client) PublicChat() {
	var Msg string

	for Msg != "q" {
		fmt.Println("please enter,enter q to exit")
		fmt.Scanln(&Msg)
		if len(Msg) != 0 {
			sendMsg := Msg + "\n"
			client.conn.Write([]byte(sendMsg))
		}
		Msg = ""

	}

}
func (client *Client) GetUsers() {

	sendMsg := "users\n"
	client.conn.Write([]byte(sendMsg))

}
func (client *Client) PrivateChat() {
	var remoteName string
	var Msg string
	client.GetUsers()

	for Msg != "q" {

		fmt.Println("请输入对方用户名|,q退出")
		fmt.Scanln(&remoteName)
		fmt.Println("请输入内容")
		fmt.Scanln(&Msg)
		if len(Msg) != 0 {
			sendMsg := "to|" + remoteName + "|" + Msg + "\n"
			client.conn.Write([]byte(sendMsg))
		}
		Msg = ""
		client.GetUsers()

	}

}
func (client *Client) DealResponse() {
	io.Copy(os.Stdout, client.conn)
}
func (client *Client) Run() {
	for client.flag != 0 {
		for client.menu() != true {
		}
		switch client.flag {
		case 1:
			fmt.Println("talk with everybody")
			client.PublicChat()
			break
		case 2:
			fmt.Println("talk with the person")
			client.PrivateChat()
			break
		case 3:
			fmt.Println("update username")
			client.UpdateName()

			break

		}
	}
}

func main() {
	flag.Parse()
	client := NewCilent(serverIp, serverPort)
	if client == nil {
		return
	}
	go client.DealResponse()
	fmt.Println("connect success")
	client.Run()
}
单词数:866字符数:10286

共有0条评论