Golang开发的即时通讯系统

操作演示

1,分别启动一个服务端和三个客户端

 2,将三个客户端更改用户名

 3,用张三测试公聊模式

 4,用王五测试私聊模式

4,用张三测试退出操作

 5,在规定时间内不活跃(未发消息),李四、王五被强踢

 源代码

服务端的实现 

将以下3个文件,共同编译为server

1,server.go服务端的代码实现

package main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

type Server struct {
	Ip   string // 服务端的IP地址
	Port int    //服务端的端口号

	OnlineMap map[string]*User //在线用户的列表,key:用户名   value:用户对象

	mapLock sync.RWMutex //访问公共资源map的锁

	Message chan string //消息广播的channel
}

// 创建一个Server的接口
func NewServer(ip string, port int) *Server {
	server := &Server{
		Ip:        ip,
		Port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}
	return server
}

// 监听Message广播消息channel的goroutine,一旦有消息就发送给全部的在线User
func (server *Server) ListenMessage() {
	for {
		msg := <-server.Message //从channel读取数据

		//将Message发送给全部的在线User
		server.mapLock.Lock()
		for _, cli := range server.OnlineMap {
			cli.C <- msg
		}
		server.mapLock.Unlock()
	}
}

// 广播消息的方法
func (server *Server) Broadcast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
	server.Message <- sendMsg //放入管道
}

func (server *Server) Handler(conn net.Conn) {
	//...当前连接的业务
	user := NewUser(conn, server) //创建user
	user.Online()                 //上线操作

	//监听用户是否活跃的channel
	isLive := make(chan bool)
	//接收客户端发送的消息
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				user.Offline() //下线操作
				return
			}
			if err != nil && err != io.EOF { //io.EOF代表读取到文件的末尾了
				fmt.Println("conn Read err:", err)
				return
			}
			//提取用户的消息,切片去除 "\n"
			msg := string(buf[:n-1])

			//用户针对msg进行消息处理
			user.DoMessage(msg)

			//用户的任意消息,代表当前用户是活跃的
			isLive <- true
		}
	}()

	//当前handle阻塞
	for {
		select {
		case <-isLive:
			//当前用户是活跃的应该重置定时器
			//不做任何处理,为了激活select 更新下面的定时器
		case <-time.After(time.Second * 180): //规定时间后定时器触发,执行即重置
			//已经超时,将当前User强制关闭
			user.sendMsg("你已被踢出\n")
			//销毁用户资源
			close(user.C)
			//关闭链接
			conn.Close()
			//退出当前的Handler
			return //或者:runtime.Goexit()
		}
	}
}

// 启动服务器的接口
func (server *Server) Start() {
	//socket listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", server.Ip, server.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	//close listen socket
	defer listener.Close()

	//启动监听Message的goroutine
	go server.ListenMessage()

	for {
		//accept
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener Accept err:", err)
			continue
		}
		//do handler
		go server.Handler(conn)
	}
}

2,user.go服务端对用户的操作

package main

import (
	"net"
	"strings"
)

type User struct {
	Name   string      //用户名
	Addr   string      //用户IP
	C      chan string //消息队列
	conn   net.Conn    //与客户端通信的客户端连接
	server *Server     //用户连接的服务器
}

// 创建一个用户的API
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,            //连接的服务端
	}
	//启动监听当前user channel消息的goroutinue
	go user.ListenMessage()
	return user
}

// 用户的上线业务
func (user *User) Online() {
	//用户上线,将用户加入到OnlineMap中,对公共资源访问 加锁
	user.server.mapLock.Lock()
	user.server.OnlineMap[user.Name] = user //key是user.Name  value是user
	user.server.mapLock.Unlock()

	//广播当前用户上线消息
	user.server.Broadcast(user, "已上线")
}

// 用户的下线业务
func (user *User) Offline() {
	//用户下线,将用户从OnlineMap中删除
	user.server.mapLock.Lock()
	delete(user.server.OnlineMap, user.Name) //删除user
	user.server.mapLock.Unlock()

	//广播当前用户下线消息
	user.server.Broadcast(user, "下线")
}

// 给当前User对应的客户端发送消息
func (user *User) sendMsg(msg string) {
	user.conn.Write([]byte(msg))
}

// 用户处理消息的业务
func (user *User) DoMessage(msg string) {

	if msg == "who" { //查询当前所有在线用户
		user.server.mapLock.Lock()
		for _, onlineUser := range user.server.OnlineMap {
			onlineMsg := "[" + onlineUser.Addr + "]" + onlineUser.Name + ":在线...\n"
			user.sendMsg(onlineMsg)
		}
		user.server.mapLock.Unlock()

	} else if len(msg) > 7 && msg[:7] == "rename|" { //更改用户名
		//消息格式 rename|张三
		newName := strings.Split(msg, "|")[1]

		//判断newName是否被占用
		_, ok := user.server.OnlineMap[newName]
		if ok {
			user.sendMsg("当前用户名被使用\n")
		} else {
			user.server.mapLock.Lock()
			delete(user.server.OnlineMap, user.Name) //在线列表,删除原有的用户
			user.server.OnlineMap[newName] = user    //在线列表,添加新的用户
			user.server.mapLock.Unlock()

			user.Name = newName
			user.sendMsg("你已经更新用户名:" + user.Name + "\n")
		}
	} else if len(msg) > 4 && msg[:3] == "to|" { //发私信
		//消息格式:to|张三|消息内容
		//1,获取对方的用户名
		remoteName := strings.Split(msg, "|")[1]
		if remoteName == "" {
			user.sendMsg("消息格式不正确,请使用:\"to|张三|你好吗\"格式。\n")
			return
		}

		//2,根据用户名,得到对方的User对象
		remoteUser, ok := user.server.OnlineMap[remoteName]
		if !ok {
			user.sendMsg("该用户名不存在\n")
			return
		}

		//3,获取消息内容,通过对方的User对象将消息内容发送过去
		content := strings.Split(msg, "|")[2]
		if content == "" {
			user.sendMsg("无消息内容,请重发\n")
			return
		}
		remoteUser.sendMsg(user.Name + " 对你说:" + content + "\n")

	} else {
		//将消息进行广播
		user.server.Broadcast(user, msg)
	}

}

// 当前监听User channel的方法,一旦有消息,就直接发送给对端客户端
func (user *User) ListenMessage() {
	for {
		msg := <-user.C
		user.conn.Write([]byte(msg + "\n"))
	}
}

3,main.go程序入口

package main

func main() {
	server := NewServer("127.0.0.1", 8888) //创建服务器
	server.Start()                         //运行服务器
}

客户端的实现  

 将以下1个文件,编译为client

1,client.go客户端的代码实现

package main

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

type Client struct {
	ServerIp   string //服务端Ip
	ServerPort int    //服务端端口
	Name       string //客户端名称
	conn       net.Conn
	flag       int //当前client的模式
}

// 创建、初始化client对象
func NewClient(serverIp string, serverPort int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
		flag:       999,
	}
	//连接server
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	if err != nil {
		fmt.Println("net.Dial error:", err)
		return nil
	}
	client.conn = conn
	//返回对象
	return client
}

// 处理server回应的消息,直接显示到标准输出
func (client *Client) DealResponse() {
	/*
		for{
			buf:=make()
			client.conn.Read(buf)
			fmt.Println(buf)
		}
	*/

	//一旦client.conn有数据,就直接copy到stdout标准输出上,永久阻塞监听
	io.Copy(os.Stdout, client.conn) //等价于以上代码
}

// 显示选择菜单
func (client *Client) menu() bool {
	var flag int
	fmt.Println("1.公聊模式")
	fmt.Println("2.私聊模式")
	fmt.Println("3.更新用户名")
	fmt.Println("0.退出")

	fmt.Scanln(&flag)
	if flag >= 0 && flag <= 3 {
		client.flag = flag
		return true
	} else {
		fmt.Println(">>>>请输入合法范围类的数字<<<<")
		return false
	}
}

// 公聊模式
func (client *Client) PublicChat() {
	//提示用户输入消息
	var chatMsg string
	fmt.Println(">>>>请输入聊天内容, exit退出.<<<<")
	fmt.Scanln(&chatMsg)

	//发给服务器
	for chatMsg != "exit" {

		//当消息不为空则发送
		if len(chatMsg) != 0 {
			sendMsg := chatMsg + "\n"
			_, err := client.conn.Write([]byte(sendMsg))
			if err != nil {
				fmt.Println("conn Write err:", err)
				break
			}
		}
		chatMsg = ""
		fmt.Println(">>>>请输入聊天内容, exit退出.<<<<")
		fmt.Scanln(&chatMsg)
	}
}

// 私聊模式
func (client *Client) PrivateChat() {
	var remoteName string
	var chatMsg string
	client.SelectUser()
	fmt.Println(">>>>请输入聊天对象[用户名], exit退出.<<<<")
	fmt.Scanln(&remoteName)

	for remoteName != "exit" {
		fmt.Println(">>>>请输入消息内容, exit退出.<<<<")
		fmt.Scanln(&chatMsg)

		for chatMsg != "exit" {
			//消息不为空则发送
			if len(chatMsg) != 0 {
				sendMsg := "to|" + remoteName + "|" + chatMsg + "\n"
				_, err := client.conn.Write([]byte(sendMsg))
				if err != nil {
					fmt.Println("conn Write err:", err)
					break
				}
			}
			chatMsg = ""
			fmt.Println(">>>>请输入消息内容, exit退出.<<<<")
			fmt.Scanln(&chatMsg)
		}
		client.SelectUser()
		fmt.Println(">>>>请输入聊天对象[用户名], exit退出.<<<<")
		fmt.Scanln(&remoteName)
	}
}

// 私聊模式,查询在线用户
func (client *Client) SelectUser() {
	sendMsg := "who\n"
	_, err := client.conn.Write([]byte(sendMsg))
	if err != nil {
		fmt.Println("conn Write err:", err)
		return
	}
}

// 更新用户名
func (client *Client) UpdateName() bool {
	fmt.Println(">>>>请输入用户名<<<<")
	fmt.Scanln(&client.Name)
	sendMsg := "rename|" + client.Name + "\n"
	_, err := client.conn.Write([]byte(sendMsg))
	if err != nil {
		fmt.Println("conn.Write err:", err)
		return false
	}
	return true
}

// 处理client的业务
func (client *Client) Run() {
	for client.flag != 0 {
		for client.menu() != true {
		}
		//根据不同的模式处理不同的业务
		//聊天内容不能有空格,不然缓冲区有问题
		switch client.flag {
		case 1:
			//1.公聊模式
			client.PublicChat()
			break
		case 2:
			//2.私聊模式
			client.PrivateChat()
			break
		case 3:
			//3.更新用户名
			client.UpdateName()
			break
		}
	}
}

var serverIp string
var serverPort int

// 解析命令行
// ./client -ip 127.0.0.1 -port 8888
func init() {
	//绑定参数: 绑定的参数,名称,默认值,说明
	flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认127.0.0.1)")
	flag.IntVar(&serverPort, "port", 8888, "设置服务端IP地址(默认8888)")
}

func main() {
	//命令行解析
	flag.Parse()

	client := NewClient(serverIp, serverPort)
	if client == nil {
		fmt.Println(">>>> 服务器连接失败...")
		return
	}
	fmt.Println(">>>> 服务器连接成功...")

	//单独开启一个goroutine去处理server的回执的消息
	go client.DealResponse()

	//启动客户端的业务
	client.Run()
}

参考链接

项目案例-即时通信系统-课程介绍_哔哩哔哩_bilibili

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值