一、项目框架
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方法,用于收到客户端的业务处理
- 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
- 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。
- 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。
- 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中读数据,读不到就以为对方下线。
- //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类中封装。
- //用户上线的业务
- 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发送给客户端全部在线用户
- //给当前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删除旧用户名,添加新用户名。
- 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到用户数据就重置定时器,如果超过十秒没有收到用户消息,就将连接断开。
- //当前的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。
- 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给其发送消息。
- }
复制代码 九、客户端基本构建
- 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 {}
- }
复制代码 十、解析命令行
- 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()
- }
复制代码 十一、添加客户端菜单
- 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更新用户名接口实现
- 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公聊模式接口实现
- 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私聊模式接口实现
- 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)
- }
- }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |