一、项目框架
1.1 Server端:
1.1.1Server:服务器
---OnlineMap:记录都有哪些用户在线。(key:用户名,value:用户对象),当给一个用户发消息时,在OnlineMap中查询是否在线并找到用户对象找到对应连接。
---Message channel:server用来广播的channel,如果收到一个消息,先将消息写道该channel中,该channel再将消息转发给特定user的channel或者广播给所有user的channel,每个user就会将自己channel中收到的消息发给客户端。
1.1.2User:在线的用户
user这部分有两个goroutine,读写分离模型。
---一个goroutine负责从user channel中读消息,一旦有消息,就会立刻将消息发送给客户端。
---handler go:永久阻塞等待客户端发消息,read。
1.2 Client端:
二、基础server构建
2.1 server.go
<1.创建server结构体(Ip、Port)
<2.提供接口创建、初始化server结构体对象
<3.写一个start启动服务器的server的类方法,listen、accept、go handler
<4.Handler方法,用于收到客户端的业务处理
Go
package main
import "net"
import "fmt"
type Server struct {
Ip string
Port int
}
func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
//...当前连接的业务
fmt.Println("链接建立成功!")
}
//创建一个server的接口-用于创建server对象-仅仅是对外提供的接口
func NewServer(ip string,port int) *Server {
server := &Server{ //把当前创建对象的地址传给返回值
Ip : ip,
Port : port,
}
return server
}
//启动服务器的方法-server的成员方法
func (this *Server) Start() {
//socket listen;Listen接口返回listener对象和error
listener,err := net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))//"127.0.0.1:8888"
if err != nil {
fmt.Println("Listener accept err:",err)
return
}
//close listen socket
defer listener.Close()
//
for {
//accept; 返回一个连接对象conn和err,这个conn可以中有读写等操作--跟客户端建立成功的套接字
conn,err := listener.Accept()
if err != nil {
fmt.Println("Listener accept err:",err)
continue
}
//do handler业务回调
go this.Handler(conn)//让一个go去执行业务,主go成继续循环接收下一个连接
}
}
2.2 main.go
Go
package main //server.go和main.go都属于main包,不需要再Import
func main() {
server := NewServer("127.0.0.1",8888)
server.Start()
}
二、用户上线及广播功能
client客户端在server中如何表示呢?封装一个user类。这个user类中包含一个conn与客户端简历链接,在线的user存到OnlineMap中,每个user对象还会绑定一个channel,用来接收Message channel中的消息,被user的goroutine阻塞监听,收到就会通过conn发送给client客户端。Message channel是接收客户端的消息,Message goroutine阻塞监听channel,一旦有消息,就遍历OnlineMap去通过user channel广播给在线用户。
2.1封装一个User类
<1.User类中成员变量:name、addr、c(server)、conn(与客户端的连接)
<2.提供创建并初始化User对象的接口,并在初始化时启动goroutine对user channel监听。
<3.监听方法。从user channel中读数据,读出后将数据通过conn写入client。
Go
package main
import "net"
type User struct {
Name string
Addr string
C chan string
Conn net.Conn
}
//创建一个用户的API
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String() //从conn获取addr
user := &User{
Name : userAddr,
Addr : userAddr,
C : make(chan string),
Conn : conn,
}
//启动监听当前user channel消息的goroutine
go user.ListenMessage()
return user
}
//监听当前User channel的方法,一旦有消息,就直接发送给对端客户端
func (this *User) ListenMessage() {
for {
//一直从管道中读数据
msg := <-this.C
//向客户端写消息
this.Conn.Write([]byte(msg + "\n"))
}
}
2.2对Server类的更改
<1.新增成员变量OnlineMap、mapLock(保护onlinemap)、Message(转发的channel)
<2.server在listen时每accept一个conn就表明一个用户上线,每一个客户端连接都分配一个gorountine,将每一个客户端conn封装成user对象,加入到OnlineMap中,再向Message channel中写数据。
<3.在start服务器时新增gorountine去监听Message有没有收到用户建立连接的消息,收到后就将该消息广播给所有user的user channel。
Go
package main
import "net"
import "fmt"
import "sync"
type Server struct {
Ip string
Port int
//在线用户的列表
OnlineMap map[string]*User
mapLock sync.RWMutex
//消息广播的channel
Message chan string
}
//监听message广播消息channel的gorountine,一旦有消息就发送给全部在线的user
func (this *Server) ListenMessage() {
for {
msg := <-this.Message//server中start时仅有一个gorountine专门来监听message channel,而每一个用来处理监听user连接业务的gorountine像这一个message channel中写数据
//将msg发送给全部在线的User
this.mapLock.Lock()
for _,cli := range this.OnlineMap {
cli.C <- msg //广播给每一个user的channel
}
this.mapLock.Unlock()
}
}
func(this *Server) BroadCast(user *User,msg string){
sendMsg := "["+user.Addr+"]"+user.Name+":"+msg
//给server类的channel写数据
this.Message <- sendMsg//将打包好的消息给massage channel,由它进行广播。server端listen监听客户端,连接上后启用goroutine去处理这个conn,将这个conn封装成user,将这个user给到hash
}
//user的goroutine
func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
//处理上线的用户
// fmt.Println("链接建立成功!")
user := NewUser(conn)
//用户上线,将用户加入到onlineMap中
this.mapLock.Lock()
this.OnlineMap[user.Name]=user
this.mapLock.Unlock()
//广播当前用户上线消息
this.BroadCast(user,"已上线")
//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
}
//创建一个server的接口-用于创建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
}
//启动服务器的方法-server的成员方法
func (this *Server) Start() {
//socket listen;Listen接口返回listener对象和error
listener,err := net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))//"127.0.0.1:8888"
if err != nil {
fmt.Println("Listener accept err:",err)
return
}
//close listen socket
defer listener.Close()
//启动监听Message的gorountine
go this.ListenMessage()
//
for {
//accept; 返回一个连接对象conn和err,这个conn可以中有读写等操作--跟客户端建立成功的套接字
conn,err := listener.Accept()//此时就有用户上线了
if err != nil {
fmt.Println("Listener accept err:",err)
continue
}
//do handler业务回调
go this.Handler(conn)//让一个go去执行业务,主go成继续循环接收下一个连接
}
}
三、用户消息广播功能
在接收每个client的conn的go程中又创建子go程去阻塞从conn中读数据,读不到就认为对方下线。
Go
//user的goroutine,从client->onlinemap->message channel的线
func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
//处理上线的用户
// fmt.Println("链接建立成功!")
user := NewUser(conn)
//用户上线,将用户加入到onlineMap中
this.mapLock.Lock()
this.OnlineMap[user.Name]=user
this.mapLock.Unlock()
//广播当前用户上线消息
this.BroadCast(user,"已上线")
//接收客户端发送的消息,一直监听,从conn中读;
go func() {
buf := make([]byte,4096)
for {
//Read(b []byte) (n int,err error) 读成功返回n为读到的字节数,读失败err
n,err := conn.Read(buf)
if n==0 {
this.BroadCast(user,"下线") //读不到消息就是下线了吗?依旧可...
return
}
if err != nil && err != io.EOF {//不为空且不是读到文件末尾
fmt.Println("Conn Read err:",err)
return
}
//提取用户的消息(去除'\n')
msg := string(buf[:n-1])//从0到n-1
//将得到的消息进行广播
this.BroadCast(user,msg)
}
}()
//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
select {}//让当前handler阻塞????
}
四、用户业务封装
<2.在User类中新增Server关联(组合),通过关联可以对server中的属性和方法操作。
<1.将用户上线、用户下线、用户消息处理在User类中封装。
Go
//用户上线的业务
func (this *User) Online() {
//用户上线,将用户加入到onlineMap中
this.server.mapLock.Lock()
this.server.OnlineMap[this.Name]=this
this.server.mapLock.Unlock()
//广播当前用户上线消息
this.server.BroadCast(this,"已上线")
}
//用户下线的业务
func (this *User) Offline() {
//用户下线,将用户从onlineMap中删除
this.server.mapLock.Lock()
delete(this.server.OnlineMap,this.Name)
this.server.mapLock.Unlock()
//广播当前用户上线消息
this.server.BroadCast(this,"下线")
}
//用户处理消息的业务,将该用户(客户端的)消息广播
func (this *User) DoMessage(msg string) {
this.server.BroadCast(this,msg)
}
五、在线用户查询
遍历OnlineMap表通过conn发送给客户端所有在线用户
Go
//给当前User对应的客户端发送消息
func (this *User) SendMsg(msg string) {
this.Conn.Write([]byte(msg))
}
//用户处理消息的业务,将该用户(客户端的)消息广播
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _,user := range this.server.OnlineMap {
OnlineMsg := "["+user.Addr+"]"+user.Name+":"+"在线...\n"
this.SendMsg(OnlineMsg)
}
this.server.mapLock.Unlock()
} else {
this.server.BroadCast(this,msg)
}
}
六、修改用户名
新用户名不能存在,如果存在提示。不存在就开始在OnlineMap删除旧用户名,添加新用户名。
Go
else if len(msg) > 7 && msg[:7]=="rename|"{
//消息格式:renanme|张三
newName := strings.Split(msg,"|")[1]
//判断name是否存在
_, ok := this.server.OnlineMap[newName]
if ok {
this.SendMsg("当前用户名被使用\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.SendMsg("您已经更新用户名:"+this.Name +"\n")
}
}
七、超时强踢功能
如果用户长时间不保持活跃,就断开它的连接。如果Read到用户数据就重置定时器,如果超过十秒没有收到用户消息,就将连接断开。
Go
//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
for {
select { //select会阻塞监控管道channel的消息,如果超时会满足case,触发,select不会阻塞
case <-isLive:
//当前用户活跃应重置定时器--不做处理,只为了激活select顺序执行time.After重置定时器
case <-time.After(time.Second*10): //time.After 返回一个单向通道(<-chan time.Time),该通道在指定的时间间隔后发送当前时间值
//如果超时,将当前的user强制关闭
user.SendMsg("你被超时踢出了")
close(user.C)//关闭channel
conn.Close()//关闭连接
return//退出当前hanler
}
}
八、用户私聊功能
<1.目前只实现了将用户的消息进行广播的功能。客户端先通过who查询在线用户,再发送给服务器具有格式的消息指明私聊消息发送给哪位在线用户;
<2.从msg中拿到对方userName,在OnlineMap找到它的user对象,用它的user对象调用sendMsg函数通过conn给其发送消息。
<3.私聊消息的本质是在OnlineMap中保存的多个用户的连接中找到目标用户的长连接conn向它write。从client A的conn读到的消息进行处理后在Map找到它要发送的client B的conn去write。
Go
else if len(msg) > 4 && msg[:3] == "to" {
//消息格式:to|张三|消息内容
//<1.获取对方的用户名
remoteName := strings.Split(msg,"|")[1]
if remoteName == "" {
this.SendMsg("消息格式不正确,请使用\"to|张三|你好啊\"格式,\n")
return
}
//<2.根据用户名得到对方的user对象
remoteUser,ok := this.server.OnlineMap[remoteName]
if !ok {
this.SendMsg("该用户名不存在\n")
return
}
//<3.获取消息内容,通过对方的User对象将消息发送过去
content := strings.Split(msg,"|")[2]
if content == "" {
this.SendMsg("无消息内容,请重发\n")
return
}
remoteUser.SendMsg(this.Name + "对您说:"+content) //从msg中拿到对方userName,在OnlineMap找到它的user对象,用它的user对象调用sendMsg函数通过conn给其发送消息。
}
九、客户端基本构建
Go
package main
import "net"
import "fmt"
type Client struct {
ServerIp string //服务器Ip
ServerPort int
Name string //客户端名称
conn net.Conn //连接句柄
}
func NewClient(serverIp string,serverPort int) *Client {
//创建客户端对象
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
}
//连接server,客户端去连接服务器func Dial(network,address string) (Conn,error)
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
}
func main() {
client := NewClient("127.0.0.1",8888)
if client == nil{
fmt.Println(">>>>>连接服务器失败>>>>>")
return
}
fmt.Println(">>>>>连接服务器成功>>>>>")
//启动客户端的业务
select {}
}
十、解析命令行
Go
var serverIp string
var serverPort int
func init() { //将两个形参绑定到flag包中
flag.StringVar(&serverIp,"ip","127.0.0.1","设置服务器IP地址(默认是127.0.0.1)")
flag.IntVar(&serverPort,"port",8888,"设置服务器Port地址(默认是8888)")
}
func main() {
//命令行解析
flag.Parse()
}
十一、添加客户端菜单
Go
func (client *Client) menu() bool {
var flag int
fmt.Println("************************************")
fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.更新用户名")
fmt.Println("0.退出")
fmt.Println("************************************")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 3{
client.flag = flag
return true
}else {
fmt.Println(">>>>请输入合法范围内的数字<<<<")
return false
}
}
func (client *Client) Run() {
for client.flag != 0{
for client.menu() != true {//阻塞式等待用户输入正确的flag或者退出
}
//根据不同的模式处理不同的业务
switch client.flag {
case 1:
//公聊模式
fmt.Println("公聊模式")
break
case 2:
//私聊模式
fmt.Println("私聊模式")
break
case 3:
//更新用户名
fmt.Println("更新用户名")
break
}
}
}
十二、公聊、私聊、更新用户名接口
12.1更新用户名接口实现
Go
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
}
12.2公聊模式接口实现
Go
func (client *Client) PublicChat() {
//提示用户输入消息
var chatMsg string
fmt.Println(">>>>>请输入聊天内容,exit退出")
fmt.Scanln(&chatMsg)
for chatMsg != "exit"{
//发给服务器
if len(chatMsg) != 0{
sendMsg := chatMsg + "\n"//协议中有\n
_,err := client.conn.Write([]byte(sendMsg)) //发送给服务器
if err != nil {
fmt.Println("conn Write err:",err)
break
}
}
//只要用户不退出,就一直在公聊模式中
chatMsg = ""
fmt.Println(">>>>>请输入聊天内容,exit退出")
fmt.Scanln(&chatMsg)
}
}
12.3私聊模式接口实现
Go
func (client *Client) PrivateChat() {
var remoteName string
var chatMsg string
client.SelectUsers()
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\n"
_,err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:",err)
break
}
}
chatMsg = ""
fmt.Println(">>>>>请输入消息内容,exit退出")
fmt.Scanln(&chatMsg)
}
remoteName = ""
client.SelectUsers()
fmt.Println(">>>>>请输入聊天对象[用户名],exit退出:")
fmt.Scanln(&remoteName)
}
}