蹒跚学GO第八天-网络编程

Go的网络编程

通信协议

通信协议也叫网络传输协议或简称为传送协议(Communications Protocol),是指计算机通信或网络设备的共同语言。
现在最普及的计算机通信为网络通信,所以“传送协议”一般都指计算机通信的传送协议,如:TCP/IP、NetBEUI、HTTP、FTP等。
然而,传送协议也存在于计算机的其他形式通信,例如:面向对象编程里面对象之间的通信;操作系统内不同程序之间的消息,都需要有一个传送协议,以确保传信双方能够沟通无间。

协议 解释
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议 (Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议 (User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP 超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP 文件传输协议(File Transfer Protocol)
IP协议 是因特网互联协议(InternetProtocol)
ICMP协议 是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议 是Internet组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议 是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP 是反向地址转换协议,通过MAC地址确定IP地址。

Socket编程

对于底层网络应用开发者而言,几乎所有网络编程都是Socket,因为大部分底层网络的编程都离不开Socket编程。HTTP编程、Web开发、IM通信、视频流传输的底层都是Socket 编程。

  • Socket又称”套接字”,应用程序通常通过”套接字”向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。

  • 可以把Socket理解成类似插座的东西, 通过Socket就可以发送和接受数据了, 就像插座插上电器之后就可以向外提供电能了.

  • TCP编程的客户端和服务器端都是通过Socket来完成的.其实UDP协议通信也是使用的套接字, 和TCP协议稍有差别. TCP是面向连接的套接字, 而UDP是面向无连接的套接字.

简单的套接字原理图

在TCP/IP协议中,IP地址+TCP或UDP端口号唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

常见的网络设计模式

C/S模式

  • 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。

B/S模式

  • 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。

优缺点

  • 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。

  • B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的在线网页游戏,在各个平台上都可以完美运行。

  • B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。

TCP的架构

net包提供的函数

func(c*TCPConn)Write(b []byte)(n int,err os.Error)
func(c*TCPConn)Read(b []byte)(n int,err os.Error)
-> TCPConn 可以用在客户端和服务器端读写数据。
-> 还需要知道一个TCPAddr类型,它表示一个TCP的地址信息
type TCPAddr struct{
    IPIP
    Port int
Zone string//IPv6范围寻址区域
}
  • 在Go 语言中通过ResolveTCPAddr获取一个TCPAddr:
- func ResolveTCPAddr(net,addr string)(*TCPAddr,os.Error)
参数解释:
net参数是TCP4/TCP6/TCP中的任意一个,分别表示TCP(IPv4-only),TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
addr表示域名或者IP地址

Server端函数

Listen函数:
    func Listen(network, address string) (Listener, error)
        network:选用的协议:TCP、UDP,  如:“tcp”或 “udp”
        address:IP地址+端口号,           如:“127.0.0.1:8000”或 “:8000”
Listener 接口:
type Listener interface {
            Accept() (Conn, error)
            Close() error
            Addr() Addr
}
Conn 接口:
type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

Client端

Go语言通过net包中的DialTCP函数来建立一个TCP连接,并返回一个TCPConn类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的TCPConn对象进行数据交换。一般而言,客户端通过TCPConn对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭连接之后才失效,不然该连接可以一直使用。

func DialTCP(net string,laddr,raddr*TCPAddr)(c*TCPConn,err os.Error)
  • net参数是TCP4、TCP6、TCP中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4、IPv6的任意一个)

  • laddr表示本机地址,一般设置为nil;

  • raddr表示远程服务地址。

Dial函数:
    func Dial(network, address string) (Conn, error)
        network:选用的协议:TCP、UDP,如:“tcp”或 “udp”
        address:服务器IP地址+端口号, 如:“121.36.108.11:8000”或 “www.itcast.cn:8000”
Conn 接口:
type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

简单的C/S架构通讯

  • 服务端
func main() {
        //创建监听
        listener,err := net.Listen("tcp",":8000")
        if err != nil {
                fmt.Println("listen err:",err)
                return
        }
        defer listener.Close()  //主main结束自动关闭监听
        fmt.Println("服务端等待建立连接。。。")
        //监听,等待客户端的请求连接
        conn,err := listener.Accept()
        if err != nil {
                fmt.Println("accept err",err)
        }
        buf := make([]byte,1024)        //建立读取的缓冲区
        n,err := conn.Read(buf)
        if err != nil {
                fmt.Println("Read err",err)
                return
        }
        fmt.Println("服务端读取到:",string(buf[:n]))
}
  • 客户端
func main() {
    //主动发起连接请求
    conn, err := net.Dial("tcp", "docker.poph163.com:8000")
    if err != nil {
        fmt.Println("Dial err",err)
        return
    }
    defer conn.Close()
    //发送数据
    _,err = conn.Write([]byte("Are You Ok?"))
    if err != nil {
        fmt.Println("Write err:",err)
        return
    }
}

TCP状态转换

TCP状态转换图

  • 主动发起连接请求端:CLOSED() -> 完成三次握手 -> ESTABLISEHED(数据通信状态) -> Dial0函数返回

  • 被动发起连接请求端:CLOSED -> 调用Accept0函数 -> LISTEN -> 完成三次握手 -> ESTABLISEHED(数据通信状态) -> Accept0函数返回 -> 数据传递期间 -> ESTABLISEHED(数据通信状态)

  • 主动关闭连接请求端

    • ESTABLISEHED -> FIN_WAIT_2(半关闭) -> TIME_WAIT -> 2MSL -> 确认最后一个ACK被对端成功接收。 -> CLOSE半关闭、TIME_WAIT、2MSL -> 只会出现在“主动关闭连接请求端”

    • 被动关闭连接请求端:ESTABLISEHED——CLOSE

并发的C/S通讯

Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。

服务端

  • 1.创建监听套接字listener:=net.Listen(”tcp”,服务器的IP+port) //tcp不能大写

  • 2.defer listener.Close()

  • 3.for 循环阻塞监听客户端连接事件conn:=listener.Accept()

  • 4.创建go程对应每一个客户端进行数据通信go HandlerConnet05.实现HandlerConnet(conn net.Conn)

    • (1)defer conn.Close()

    • (2)获取成功连接的客户端Addrconn.RemoteAddr()

    • (3)for 循环读取客户端发送数据conn.Read(buf)

    • (4)处理数据小——大strings.ToUpper05)回写转化后的数据conn.write(buf:n)

func main() {
        //创建监听套接字
        listener,err := net.Listen("tcp",":8000")
        checkerr(err,"net.Listen")
        defer listener.Close()
        //监听客户端连接请求
        for {
                conn,err := listener.Accept()
                //checkerr(err)
                checkerr(err,"listener.Accept")
                go HandlerConnect(conn)
                //defer conn.Close()
        }
        //具体完成服务器和客户端的数据通信
        //go HandlerConnect(conn)

}
func checkerr(err error,name string) {
        if err != nil {
                if name == "conn.Read" {
                        new := fmt.Sprintf("%s",err)
                        if new == "EOF" {
                                fmt.Println("!!!服务器接受到客户端请求关闭!!!")
                                return
                        }
                        if err != nil{
                                fmt.Println(name,"errer:",err)
                                return
                        }
                }else {
                        _, _ = fmt.Fprintf(os.Stderr,name ," %s", err.Error())
                }
                return
        }
}

func HandlerConnect(conn net.Conn) {
        defer conn.Close()
        //获取连接的客户端的网络地址
        addr := conn.RemoteAddr()
        fmt.Println("-> ",addr,"客户端成连接")
        //循环读取直到断开连接
        buf := make([]byte,1024)
        for {
                n,err := conn.Read(buf)
                //checkerr(err)
                checkerr(err,"conn.Read")
                if n == 0 {
                        fmt.Println("【客户端已经断开】 ->",addr)
                        return
                }
                fmt.Println("服务端读取到:",string(buf[:n]))
                conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
        }
        //fmt.Println("服务端读取到:",string(buf[:n]))
        //小写转化大小回复客户端
}

客户端

//Tcp客户端
func main() {
    //与服务器建立连接
    conn,err := net.Dial("tcp","docker.poph163.com:8000")
    if err != nil {
        fmt.Println("net.Dial Error",err)
        return
    }
    defer conn.Close()
    go func() {
        str := make([]byte,4088)
        for {
            //获取用户的标准输入
            n,err := os.Stdin.Read(str)
            if err != nil {
                fmt.Println("os.stdin.Read Error:",err)
                continue
            }
            //写给服务端
            conn.Write(str[:n])
        }
    }()
    //读服务器回复,并以大写回复服务器
    buf := make([]byte,4000)
    for {
        n,err := conn.Read(buf)
        if n == 0 {
            fmt.Println("与服务器连接断开!")
            return
        }
        if err != nil {
            fmt.Println("conn.Read err",err)
            return
        }
        fmt.Println("客户端读取成功回复服务器",string(buf[:n]))
        conn.Write([]byte(strings.ToLower(string(buf[:n]))))    //双方互怼停不下来
    }
}

UDP通讯

由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。

  • UDP也叫用户数据报协议。UDP是面向无连接的, 一方负责发送数据(客户端), 只要知道对方(接受数据:服务器) 的地址就可以直接发数据了, 但是能不能达到就没有办法保证了

  • UDP编程相比TCP编程简单了很多,因为UDP不是面向连接的, 而是面向无连接的。TCP是面向连接的, 客户端和服务端必须连接之后才能通讯, 就像打电话, 必须先接通才能通话.

  • 虽然用UDP传输面向无连接, 数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。 比如局域网的视频同步, 使用 udp 是比较合适的:快, 延迟越小越好

创建监听地址:
    func ResolveUDPAddr(network, address string) (*UDPAddr, error) 
创建监听连接:
    func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) 
接收udp数据:
    func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
写出数据到udp:
    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

服务端

  • 1.创建 server端地址结构(IP+port)net.ResolveUDPAddr()

  • 2.创建用于通信的socket,绑定地址结构 udpConn=net.ListenUDPO

  • 3.defer udpConn.Close()

  • 4.读取客户端发送数据ReadFromUDP()返回:n,cltAddr(客户端的IP+port),err

  • 5.写数据给客户端WriteToUDP(”待写数据”,cltAddr)

func main() {
    //组织一个UDP地址结构,udpaddr是地址结构
    udpaddr,err :=net.ResolveUDPAddr("udp","127.0.0.1:9000")
    checkerr(err,"net.ResolveUDPAddr")
    fmt.Println("构建UDP服务器地址结构结束")
    //创建用户通讯的socket
    udpconn,err := net.ListenUDP("udp",udpaddr)
    checkerr(err,"net.ListenUDP")
    fmt.Println("监听端口完成!等待数据通讯...")
    defer udpconn.Close()
    //读取客户端发送的数据
    buf := make([]byte,3000)
    //返回三个值:读取到的字节数,客户端地址,err
    n,ipaddr,err := udpconn.ReadFromUDP(buf)
    checkerr(err,"udpconn.ReadFromUDP")
    //模拟数据处理
    fmt.Printf("服务器读取到 %v 数据,%s\n",ipaddr,string(buf[:n]))
    //提取当前的系统时间
    nowday := time.Now().String()
    //写数据到服务端
    _,err = udpconn.WriteToUDP([]byte(nowday),ipaddr)
    checkerr(err,"udpconn.WriteToUDP")
}

func checkerr(err error,name string) {
    if err != nil {
        fmt.Printf("%s Error :%s",name,err)
        return
    }
}

客户端

使用TCP即可将net.Dial("udp","ip:port")   //即可

UDP和TCP的差异

TCP UDP
面向连接 面向无连接
要求系统资源较多 要求系统资源较少
TCP程序结构较复杂 UDP程序结构较简单
使用流式 使用数据包式
保证数据准确性 不保证数据准确性
保证数据顺序 不保证数据顺序
通讯速度较慢 通讯速度较快
  • 优点 => TCP:稳定、安全、有序。 –> UDP:效率高、开销小。开发复杂度低。

  • 缺点 => TCP效率低、开销大。开发复杂度高。 –> UDP:稳定性查、安全低、无序。

使用场景

  • TCP:对数据传输安全性、稳定性要求较高的场合。网络文件传输。下载、上传。

  • UDP:对数据实时传输要求较高的场合。视频直播、在线电话会议。游戏

文件传输

原理图

  • 首先获取文件名。借助os包中的stat()函数来获取文件属性信息。在函数返回的文件属性中包含文件名和文件大小。Stat参数name传入的是文件访问的绝对路径。FileInfo中的Name()函数可以将文件名单独提取出来。
func Stat(name string) (FileInfo, error) 
type FileInfo interface {
   Name() string
   Size() int64
   Mode() FileMode
   ModTime() time.Time
   IsDir() bool
   Sys() interface{}
}
  • 获取文件属性
func main() {
    list := os.Args //获取命令行参数
    if len(list) !=2 {  //确保用户输入一个命令参数
        fmt.Println("格式:xxx.go 文件名")
        return
    }
    filename := list[1] //从命令行保存文件名包括路径
    fileinfo ,err := os.Stat(filename)  //根据文件名获取出文件的属性信息
    if err != nil {
        fmt.Printf("os.stat Error:",err)
        return
    }
    fmt.Printf("当前的文件名:%s\n当前的文件大小为:%dbyte",fileinfo.Name(),fileinfo.Size())
}
调试结果:
C:\golearn>go run demo.go qq.txt
当前的文件名:qq.txt
当前的文件大小为:21byte

发送端的实现

规划大致编写的方针如下所示:

  • 1.提示用户输入文件名。接收文件名path(含访问路径)

  • 2.使用os.Stat()获取文件属性,得到纯文件名(去除访问路径)

  • 3.主动连接服务器,结束时关闭连接

  • 4.给接收端(服务器)发送文件名conn.Write()

  • 5.读取接收端回发的确认数据conn.Read()

  • 6.判断是否为“ok”。如果是,封装函数SendFile() 发送文件内容。传参path和conn

  • 7.只读Open文件, 结束时Close文件

  • 8.循环读文件,读到EOF终止文件读取

  • 9.将读到的内容原封不动Write给接收端(服务器)

func main() {
    list := os.Args    //获取命令行参数
    if len(list) !=2 {    //确保用户输入一个命令参数
        fmt.Println("格式:xxx.go 文件名")
        return
    }
    filename := list[1]    //从命令行保存文件名包括路径
    fileinfo ,err := os.Stat(filename)    //根据文件名获取出文件的属性信息
    if err != nil {
        fmt.Printf("os.stat Error:",err)
        return
    }
    //客户端主动发起连接
    conn,err :=net.Dial("tcp","docker.poph163.com:8000")
    checkerr(err,"net.Dial")
    defer conn.Close()

    //发送文件名字给接收端
    _,err =conn.Write([]byte(string(filename)))
    checkerr(err,"conn.Write")
    fmt.Printf("当前已经将%s文件名发送给目标机器!",filename)

    //读取服务器是否接受成功
    buf := make([]byte,2048)
    n,err := conn.Read(buf)
    checkerr(err,"conn.Read")

    //写文件内容给服务器
    if "ok" == string(buf[:n]) {
    fmt.Println("对端已经创建文件名,开始传输!")
        sendfile(conn,filename,fileinfo.Size())
    }
}
func checkerr(err error,name string) {
    if err != nil {
        fmt.Printf("%s Error -> %s",name,err)
        return
    }
}

//发送文件
func sendfile(conn net.Conn,filePath string,size int64) {
    //只读打开文件
    f , err :=os.Open(filePath)
    checkerr(err,"os.open")
    defer f.Close()

    //从本地文件中读取数据写给接收端
    buf := make([]byte,size)
    for {
        n,err := f.Read(buf)
    if n == 0 {
        fmt.Println("传输结束!")
        return
    }
        //写到socket中
        _,err = conn.Write(buf[:n])
        checkerr(err,"conn.write")
    }
}

接收端实现

  • 1.创建监听listener,程序结束时关闭。

  • 2.阻塞等待客户端连接,程序结束时关闭conn。

  • 3.读取客户端发送文件名。保存fileName。

  • 4.回发“ok”给客户端做应答

  • 5.封装函数 RecvFile接收客户端发送的文件内容。传参fileName 和conn

  • 6.按文件名Create文件,结束时Close

  • 7.循环Read客户端发送的文件内容,当读到EOF说明文件读取完毕。

  • 8.将读到的内容原封不动Write到创建的文件中

func main() {
    //监听socke
    listener ,err := net.Listen("tcp",":8000")
    checkerr(err,"net.Listen")
    defer listener.Close()
    //阻塞监听
    conn,err := listener.Accept()
    checkerr(err,"listener.Accept")
    defer conn.Close()
    //获取文件名并且保存
    buf := make([]byte,1024)
    n,err := conn.Read(buf)
    checkerr(err,"conn.Read")
    recfile(conn,string(buf[:n]))
    //filename:= "asdasdasd"
    //_,err =conn.Write([]byte(string(filename)))
    //checkerr(err,"conn.Write")

}

func checkerr(err error,name string) {
    if err != nil {
        fmt.Printf("%s Error:%s\n",name,err)
        return
    }
}

func recfile(conn net.Conn,file string) {
    f,err:=os.Create(file)
    checkerr(err,"os.Creat")
    fmt.Println("创建文件名成功")
    defer f.Close()
    //执行完成创建
    _,err = conn.Write([]byte("ok"))
    checkerr(err,"conn.Write")
    //网络传输读取文件并且保存在本地
    buf := make([]byte,4089)
    fmt.Printf("正在接收文件。。。")

    //一直读文件直到读取全部结束
    for {
        n,err := conn.Read(buf)
        fmt.Printf("...")
        checkerr(err,"client conn.Read")
        if n == 0 {
            fmt.Println("接收文件结束")
            return
        }
        f.Write(buf[:n])
    }
}

效果图

聊天室

设计架构图

设计理念

  • 1.主协程(服务器):
    • 负责监听、接收用户(客户端)连接请求,建立通信关系。同时启动相应的协程处理任务。
  • 2.处理用户连接协程:HandleConnect
    • 负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理。为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
  • 3.用户消息广播协程:Manager
    • 负责在线用户遍历,用户消息广播发送。需要与HandleConnect协程及用户子协程协作完成。
  • 4.协程间应用数据及通信:
    • map:存储所有登录聊天室的用户信息, key:用户的ip+port。Value:Client结构体。
    • Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
    • 通道message:协调并发协程间消息的传递。

定义全局map 存储在线用户信息。Key为用户网络地址。Value为用户结构体。
主协程,监听客户端连接请求,当有新的客户端连接,创建新协程handleConnet处理用户连接。
handleConnet协程,获取用户网络地址(Ip+port),创建新用户结构体,包含成员C、Name、Addr。新用户的Name和Addr初值都是用户网络地址(Ip+port)。将用户结构体存入map中。并创建WriteMsgToClient协程,专门负责给当前用户发送消息。组织新用户上线广播消息内容,写入全局通道message中。
WriteMsgToClient协程,读取用户结构体C中的数据,没有则阻塞等待,有数据写出给登录用户。
Manager协程,给map分配空间。循环读取 message 通道中是否有数据。没有,阻塞等待。有则解除阻塞,将message通道中读到的数据写到用户结构体中的C通道。

服务端

  • 1.主go程中,创建监听Socket套接字。

  • 2.for 循环监听户端连接请求。Accept()

  • 3.有一个户端连接,创建新go程处理户端数据 HandlerConnet(conn)defer

  • 4.定义全局结构体类型C、Name、Addr

  • 5.创建全局map、channel

  • 6.实现HandlerConnet,获取户端IP+port—>RemoteAddr()。初始化新用户结构体信息。name=Addr

  • 7.创建Manager 实现管理go程。Accept()之前。

  • 8.实现Manager。初始化在线用户map。循环读取全局 channel,如果无据,阻。如果有数据,遍历在线用户map,将数据写到用户的C里

  • 9.将新用户添加到在线用户map中。Key==IP+port value=新用户结构体

  • 10.创建WriteMsgToClient go程,专门给当前用户写数据。——来源于用户自带的C中

  • 11.实现WriteMsgToClient(clnt,conn)。遍历自带的C,读数据,conn.Write到奋户端。

  • 12.HandlerConnet中,结束位置,组织用户上线信息,将用户上线信息写到全局 channel——Manager的读就被激活(原来一直被阻塞)

  • 13.HandlerConnet中结尾使用,for{;}保证进程不结束

package main

import (
    "fmt"
    "net"
    "strings"
    "time"
)

//定义每个客户端的维护通道和信息
type client struct {
    name string
    addr string
    C chan string
}

//全局性Message监听是否是消息通信的
var message = make(chan string)

//全局Map字典存储client信息,Key:IP+Port,Value:client信息
var onlinClient map[string]client

//处理所有的Client接入信息
func handleconnect(conn net.Conn) {
    fmt.Println("开启成功")
    conn.Write([]byte("查看在线用户请使用【who】,改名格式:【Rename|xxx】 -> 【|】为分隔符\n"))
    defer conn.Close()
    //获取用户的连接地址
    ipPort :=conn.RemoteAddr().String()
    fmt.Printf("当前上线了【%s】用户 => 在线\n",ipPort)

    //创建用户连接结构体
    user := client{"test",ipPort,make(chan string)}

    //将连接的用户,添加到在线用户表Map中(key:ip+port,value:client)
    onlinClient[ipPort] = user

    //创建专门用来给当前用户发送消息的Gorouting
    go WriteMessageClient(user,conn)

    //发送用户上线消息到全局Message中去
    message <- MakeMessage(user,"login")

    //创建用于用户下线的channel
    Quit := make(chan bool)

    //创建用于检测用户超时的channel
    hashDate := make(chan bool)

    //创建匿名Goroutine,专门处理用户发送的消息
    go func() {
        buf := make([]byte,2048)
        for {
            n ,err := conn.Read(buf)
            if n == 0 {
                fmt.Printf("检测到客户端:【%s】 => 退出\n",user.name)
                return
            }
            checkerr(err,"匿名conn.Read")
            //将读取到的用户消息,写入到message中
            msg := string(buf[:n-1])
            //获取用户在线列表
            if msg == "who" && len(msg) == 3{
                conn.Write([]byte("当前在线用户:\n"))
                //循环遍历OnlineCline字典中的在线用户信息
                for _,value := range onlinClient {
                    UserInfo := value.addr + ":" + "上线" + value.name + "\n"
                    conn.Write([]byte(UserInfo))
                }
            }else if  len(msg) >=8 && msg[:6] == "Rename" {
                Rename := strings.Split(msg,"|")[1]
                user.name = Rename
                onlinClient[ipPort] = user
                conn.Write([]byte("Your Rename test ->"+ Rename + "is Successful"+ "\n"))
            }else {
                //将参数写到channel中去触发WriteClient
                message <- MakeMessage(user,msg)
            }
            hashDate <- true
        }
    }()
    //检测用户主动下线
    go func() {
        buf := make([]byte,2048)
        for {
            n,err := conn.Read(buf)
            if n == 0 {
                Quit <- true
                //fmt.Printf("用户【%s】已经下线",user.name)
                return
            }
            checkerr(err,user.name+"下线异常 ->")
        }
    }()
    //处理掉下线的Map-key
    for {
        //监听数据的流动
        select {
        case <-Quit:
            delete(onlinClient, ipPort)     //将用户从Map中移除
            message <- MakeMessage(user, "当前用户已经下线")        //写入用户下线全局发送
            return
        case <-hashDate:
        case <-time.After(60 * time.Second):
            delete(onlinClient,ipPort)
            message <- MakeMessage(user,"timeout!!!")
            return
        }
    }
}

//用户消息函数,组合所有的需要的参数
func MakeMessage(user client,msg string) (buf string) {
    buf = "【" + user.addr + "】" + user.name + ":" + msg
    return buf
}

//创建全局channel处理传递用户消息信息
func WriteMessageClient(user client,conn net.Conn) {
    //监听用户自带的channel上是否有消息,有则发送给所有的用户
    for message := range user.C {
        conn.Write([]byte(message + "\n"))
    }
}

//创建Manager
func Manager() {
    //初始化onlineMap
    onlinClient = make(map[string]client)

    //监听全局的channel中是否存在数据,有数据存储没有数据阻塞
    for {
        msg := <- message

        //循环发送消息给所有的的在线用户
        for _, user := range onlinClient {
            user.C <- msg
        }
    }
}

func checkerr(err error,name string) {
    if err != nil {
        fmt.Printf("【%s】 当前发生异常 Error -> %s\n",name,err)
        return
    }
}

func main() {
    //创建监听的套接字
    listener,err := net.Listen("tcp",":8000")
    checkerr(err,"net.Listen")
    defer listener.Close()

    //创建管理者,管理Map和全局的channel
    go Manager()

    //循环的监听客户端的连接请求
    for  {
        conn,err := listener.Accept()
        checkerr(err,"Listerner.Accept")
        //循环启动Goroutine处理客户端请求
        go handleconnect(conn)
    }
}

蹒跚学GO第八天-网络编程》有3个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注