go语言聊天服务器

服务端程序

       服务端程序中包含4个goroutine,分别是一个主goroutine和广播(broadcaster),每一个连接里面又包含一个来连接处理(handleConn)goroutine 和一个客户写入(clientwriter)goroutine。广播器(broadcaster)是用于如何使用 select 的一个规范说明,因为它需要对三种不同的消息进行响应。主 goroutine 的工作是监听端口,接受连接客户端的网络连接,对每一个连接,它将创建一个新的 handleConn goroutine。

完整的实例代码

package main

 

import (

        "bufio"

        "fmt"

        "log"

        "net"

)

 

func main() {

        listener, err := net.Listen("tcp", "localhost:8001")

        if err != nil {

                log.Fatal(err)

        }

        go broadcaster()

        for {

                conn, err := listener.Accept()

                if err != nil {

                        log.Print(err)

                        continue

                }

                go hanleConn(conn)

        }

}

 

type client chan<- string //对外发送消息的通道

var (

        entering = make(chan client)

        leaving  = make(chan client)

        messages = make(chan string) //所有连接的客户端

)

 

func broadcaster() {

        clients := make(map[client]bool)

        for {

                select {

                case msg := <-messages:

                        //把所有接收到的消息广播给所有客户端

                        //发送消息通道

                        for cli := range clients {

                                cli <- msg

                        }

                case cli := <-entering:

                        clients[cli] = true

                case cli := <-leaving:

                        delete(clients, cli)

                        close(cli)

                }

        }

}

 

func hanleConn(conn net.Conn) {

        ch := make(chan string)

        go clientWriter(conn, ch)

        who := conn.RemoteAddr().String()

        ch <- "欢迎" + who

        messages <- who + "上线"

        entering <- ch

        input := bufio.NewScanner(conn)

        for input.Scan() {

                messages <- who + ":" + input.Text()

        }

        //注意:忽略input.Err()中可能的错误

        leaving <- ch

        messages <- who + "下线"

        conn.Close()

}

func clientWriter(conn net.Conn, ch <-chan string) {

        for msg := range ch {

                fmt.Fprintln(conn, msg) //注意,忽略网络层面的错误

        }

}

代码中main函数里写的代码非常简单,其实服务器要做的事情总结写一下无非就获得listener对象,然后不停的获取链接上来的conn对象,最后把这些对象丢给处理链接函数去进行处理,在使用 handleConn 方法处理 conn 对象的时候,对不同的链接都启一个 goroutine 去并发处理每个 conn 这样则无需等待。由于要给所有在线的用户发送消息,而不同用户的 conn 对象都在不同的 goroutine 里面,但是Go语言中有 channel 来处理各不同 goroutine 之间的消息传递,所以在这里我们选择使用 channel 在各不同的 goroutine 中传递广播消息。

下面来介绍一下 broadcaster 广播器,它使用局部变量 clients 来记录当前连接的客户集合,每个客户唯一被记录的信息是其对外发送消息通道的 ID,下面是细节:

 

type client chan<- string //对外发送消息的通道

var (

        entering = make(chan client)

        leaving  = make(chan client)

        messages = make(chan string) //所有连接的客户端

)

 

func broadcaster() {

        clients := make(map[client]bool)

        for {

                select {

                case msg := <-messages:

                        //把所有接收到的消息广播给所有客户端

                        //发送消息通道

                        for cli := range clients {

                                cli <- msg

                        }

                case cli := <-entering:

                        clients[cli] = true

                case cli := <-leaving:

                        delete(clients, cli)

                        close(cli)

                }

        }

}

 

在main函数里面使用Goroutine开启了一个broadcaster函数来负责广播所有用户发送的消息

这里使用一个字典来保存用户 clients,字典的 key 是各连接申明的单向并发队列。

使用一个selsect开启一个多路复用:
每当有广播消息从messages发送出来,都会循环clients对里面的每个channel发消息。

每当有消息从entering里面发送过来,就生成一个新的key-value,相当于给clients里面增加一个新的client.

每当有消息从leaving里面发送过来,就删掉这个key-value对,并关闭对应的channel。

下面再来看一下每个客户自己goroutine.

handleConn 函数创建一个对外发送消息的新通道,然后通过 entering 通道通知广播者新客户到来,接着它读取客户发来的每一行文本,通过全局接收消息通道将每一行发送给广播者,发送时在每条消息前面加上发送者 ID 作为前缀。一旦从客户端读取完毕消息,handleConn 通过 leaving 通道通知客户离开,然后关闭连接。

 

func hanleConn(conn net.Conn) {

        ch := make(chan string)

        go clientWriter(conn, ch)

        who := conn.RemoteAddr().String()

        ch <- "欢迎" + who

        messages <- who + "上线"

        entering <- ch

        input := bufio.NewScanner(conn)

        for input.Scan() {

                messages <- who + ":" + input.Text()

        }

        //注意:忽略input.Err()中可能的错误

        leaving <- ch

        messages <- who + "下线"

        conn.Close()

}

hanleConn函数会为每个过来处理的conn都创建一个新的channel,开启一个新的goroutine去把发送给这个channel的消息写进conn。

hanleConn函数的执行过程可以简单总结如下几个步骤:

1、获取链接过来的ip地址和端口号;

2、把欢迎信息写进channel返回给客户端;

3、生成一条广播消息写进messages里面;

4、把这个channel加入到客户端集合,也就是entering<-ch;

5、监听客户端con里写的数据,每列扫描到一条就将这条消息发送到广播channel中;

6、如果关闭客户端,那么把队列离开写入leaving 交给广播函数去删除这个客户端并关闭客户端;

7、广播通知其他客户端该关闭的客户端已关闭;

8、最后关闭这个客户端的连接Conn.Close()。

 

客户端程序

 

package main

 

import (

        "io"

        "log"

        "net"

        "os"

)

 

func main() {

        conn, err := net.Dial("tcp", "localhost:8001")

        if err != nil {

                log.Fatal(err)

        }

        done := make(chan struct{})

        go func() {

                io.Copy(os.Stdout, conn) //注意:忽略错误

                log.Println("done")

                done <- struct{}{} //向Goroutinue发出信号

        }()

        mustCopy(conn, os.Stdin)

        conn.Close()

        <-done //等待后代goroutinue完成

}

func mustCopy(dst io.Writer, src io.Reader) {

        if _, err := io.Copy(dst, src); err != nil {

                log.Fatal(err)

        }

}

 

当有n个客户session在连接的时候,程序并发运行着2n+2个相互通信的goroutine,它不需要隐式的枷锁操作。Clients map限制在广播器这一个goroutine中被访问,所以不会并发访问它,唯一被多个goroutine共享的变量是通道以及net.Conn的实例,他们又多是并发安全的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章