038 go语言网络编程_Socket编程

Socket编程

什么是Socket

Socket起源于Unix,==而Unix基本哲学之一就是“一切皆文件”== ,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。==Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。==

常用的Socket类型有两种:==流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。== 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

TCP的C/S架构

038 go语言网络编程_Socket编程_第1张图片
clip_image002.png

CS模型介绍

cs模型(client and server ):客户端和服务器


客户端   =======>  客户
1)主动请求服务


服务器  ========》 客服
2)被动提供服务


brower/server
bs模型

brower =====》 客户端, html

示例程序1

服务器代码

package main

import (
    "fmt"
    "net"
)

func main() {
    //监听
    listener, err := net.Listen("tcp", "127.0.0.1:8001")  //使用Listen进行监听
    //判断是否有错误,有错误就不要执行了
    if err != nil {
        fmt.Println("err = ", err)
        return
    }

    defer listener.Close()  //最后使用 延迟调用函数defer 关闭监听

    //阻塞等待用户链接
    conn, err := listener.Accept() //调用方法
    if err != nil {
        fmt.Println("err = ", err)
        return
    }

    //接收用户的请求
    buf := make([]byte, 1024) //创建1024大小的缓冲区切片
    n, err1 := conn.Read(buf)  //将接收到的内容进行读取到缓冲区,n是占用了多少的切片
    if err1 != nil {
        fmt.Println("err1 = ", err1)
        return
    }

    fmt.Println("buf = ", string(buf[:n]))  //将切片转换为string进行打印

    defer conn.Close() //关闭当前用户链接,前面还有关闭监听,先关闭当前用户链接再关闭监听,defer的先进后出
}

Netcat 安装使用教程

  • 将压缩包解压到常用的软件安装盘
038 go语言网络编程_Socket编程_第2张图片
image-20200120181247081.png
  • 有两个可执行文件
038 go语言网络编程_Socket编程_第3张图片
image-20200120181905808.png
  • 添加环境变量
038 go语言网络编程_Socket编程_第4张图片
image-20200120184000028.png
  • 使用
038 go语言网络编程_Socket编程_第5张图片
image-20200120185957868.png

客服端代码

package main

import (
    "fmt"
    "net"
)

func main() {
    //主动连接服务器,使用的是Dial函数
    conn, err := net.Dial("tcp", "127.0.0.1:8001")
    if err != nil {
        fmt.Println("err = ", err)
        return
    }

    defer conn.Close()  //最后关闭连接

    //发送数据
    conn.Write([]byte("are u ok?"))  //发送字节切片(类型转换)

}

运行结果

038 go语言网络编程_Socket编程_第6张图片
录制_2020_01_20_19_11_14_278.gif

简单版并发服务器

TCP服务器

package main

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

//处理用户请求
func HandleConn(conn net.Conn) {  //函数传递的类型是net.Conn
    //函数调用完毕,自动关闭conn
    defer conn.Close()

    //获取客户端的网络地址信息  RemoteAddr 远程地址
    addr := conn.RemoteAddr().String()  //获取地址信息转换成字符串
    fmt.Println(addr, " conncet sucessful")

    buf := make([]byte, 2048)  //定义一个切片接收数据

    for {
        //读取用户数据
        n, err := conn.Read(buf)  //使用Read函数读取数据到buf中,并返回占用的字节数
        if err != nil {
            fmt.Println("err = ", err)
            return
        }
        fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
        fmt.Println("len = ", len(string(buf[:n])))

        if "exit" == string(buf[:n-1]) { //nc测试
        //if "exit" == string(buf[:n-2]) { //自己写的客户端测试, 发送时,多了2个字符, "\r\n"
            fmt.Println(addr, " exit")
            return
        }

        //把数据转换为大写,再给用户发送
        conn.Write([]byte(strings.ToUpper(string(buf[:n]))))  //strings.ToUpper 将字符串转换为全大写
    }

}

func main() {
    //监听
    listener, err := net.Listen("tcp", "127.0.0.1:8001")  //本机的服务器的IP及port可以固定
    if err != nil {
        fmt.Println("err = ", err)
        return
    }

    defer listener.Close()

    //接收多个用户
    for {
        conn, err := listener.Accept()  //可以持续的接收连接
        if err != nil {
            fmt.Println("err = ", err)
            return
        }

        //处理用户请求, 新建一个协程
        go HandleConn(conn)  //给每一个用户请求一个协程去处理(协程的好处出现了)
    }

}

执行结果:使用NetCat 进行测试

038 go语言网络编程_Socket编程_第7张图片
录制_2020_01_20_19_34_48_891.gif

Client 客户端

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    //主动连接服务器
    conn, err := net.Dial("tcp", "127.0.0.1:8001")
    if err != nil {
        fmt.Println("net.Dial err = ", err)
        return
    }

    //main调用完毕,关闭连接
    defer conn.Close()

    go func() {
        //从键盘输入内容,给服务器发送内容
        str := make([]byte, 1024)
        for {
            n, err := os.Stdin.Read(str) //从键盘读取内容, 放在str   //os.Stdin 输入流
            if err != nil {
                fmt.Println("os.Stdin. err = ", err)
                return
            }

            //把输入的内容给服务器发送
            conn.Write(str[:n])
        }
    }()

    //接收服务器回复的数据
    //切片缓冲
    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf) //接收服务器的请求
        if err != nil {
            fmt.Println("conn.Read err = ", err)
            return
        }
        fmt.Println(string(buf[:n])) //打印接收到的内容, 转换为字符串再打印
    }
}

注意更改服务端判断退出的代码

038 go语言网络编程_Socket编程_第8张图片
image-20200120201506008.png

执行结果

038 go语言网络编程_Socket编程_第9张图片
录制_2020_01_20_20_16_09_679.gif

文件传输

原理分析

038 go语言网络编程_Socket编程_第10张图片
image-20200121140420395.png

获取文件属性

package main

import (
    "fmt"
    "os"
)

func main() {
    list := os.Args   //第一个参数是程序路径本身 ,第二个是文件的路径(文件名(相对路径))
    if len(list) != 2 {
        fmt.Println("useage: xxx file")
        return
    }

    fileName := list[1]  //获取传递来的文件名(或者路径)

    info, err := os.Stat(fileName)  //返回该文件的一系列参数
    if err != nil {  //要是有错误就打印什么错
        fmt.Println("err = ", err)
        return
    }
    //打印文件名
    fmt.Println("name = ", info.Name())
    //打印文件大小
    fmt.Println("size = ", info.Size())

}

运行结果

038 go语言网络编程_Socket编程_第11张图片
录制_2020_01_21_14_20_39_317.gif

传输文件发送方

package main

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

//发送文件内容
func SendFile(path string, conn net.Conn) {
    //以只读方式打开文件
    f, err := os.Open(path)  //使用Open函数打开该路径文件
    if err != nil {
        fmt.Println("os.Open err = ", err)
        return
    }

    defer f.Close()  //最后要关闭

    buf := make([]byte, 1024*4)  //缓冲区设置为4K大小

    //读文件内容,读多少发多少,一点不差
    for {
        n, err := f.Read(buf) //从文件读取内容,每次读取都是缓冲区大小的字节
        if err != nil {
            if err == io.EOF {  //读到头了
                fmt.Println("文件发送完毕")
            } else {
                fmt.Println("f.Read err = ", err)
            }

            return
        }

        //发送内容
        conn.Write(buf[:n]) //给服务器发送内容,读取多少发送多少
    }

}

func main() {
    //提示输入文件
    fmt.Println("请输入需要传输的文件:")
    var path string  //先声明一个文件名的类型
    fmt.Scan(&path) //获取文件路径.=,使用&获取

    //获取文件名 info.Name()
    info, err := os.Stat(path)  //获取该文件的众多参数
    if err != nil {
        fmt.Println("os.Stat err = ", err)
        return
    }

    //主动连接服务器
    conn, err1 := net.Dial("tcp", "127.0.0.1:8001")
    if err1 != nil {
        fmt.Println("net.Dial err1 = ", err1)
        return
    }

    defer conn.Close()  //最后要关闭连接

    //给接收方,先发送文件名
    _, err = conn.Write([]byte(info.Name()))  //将文件名转化成字节切片,发送
    if err != nil {
        fmt.Println("conn.Write err = ", err)
        return
    }

    //接收对方的回复,如果回复"ok", 说明对方准备好,可以发文件
    var n int //提前声明n ,后面自动推导也是可以的
    buf := make([]byte, 1024)

    n, err = conn.Read(buf)
    if err != nil {
        fmt.Println("conn.Read err = ", err)
        return
    }

    if "ok" == string(buf[:n]) {
        //发送文件内容
        SendFile(path, conn)  //调用发送文件的函数,传递的是参数是路径以及连接的服务器
    }

}

传输文件接收方

package main

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

//接收文件内容
func RecvFile(fileName string, conn net.Conn) {
    //新建文件
    f, err := os.Create(fileName)

    defer f.Close()
    
    if err != nil {
        fmt.Println("os.Create err = ", err)
        return
    }

    buf := make([]byte, 1024*4)

    //接收多少,写多少,一点不差
    for {
        n, err := conn.Read(buf) //接收对方发过来的文件内容
        if err != nil {
            if err == io.EOF {
                fmt.Println("文件接收完毕")
            } else {
                fmt.Println("conn.Read err = ", err)
            }
            return
        }

        if n == 0 {
            fmt.Println("n == 0 文件接收完毕")
            break
        }

        f.Write(buf[:n]) //往文件写入内容
    }

}

func main() {
    //监听
    listenner, err := net.Listen("tcp", "127.0.0.1:8001")
    if err != nil {
        fmt.Println("net.Listen err = ", err)
        return
    }

    defer listenner.Close()

    //阻塞等待用户连接
    conn, err1 := listenner.Accept()
    if err1 != nil {
        fmt.Println("listenner.Accept err = ", err1)
        return
    }

    defer conn.Close()

    buf := make([]byte, 1024)
    var n int
    n, err = conn.Read(buf) //读取对方发送的文件名
    if err != nil {
        fmt.Println("conn.Read err = ", err)
        return
    }

    fileName := string(buf[:n])

    //回复"ok"
    conn.Write([]byte("ok"))

    //接收文件内容
    RecvFile(fileName, conn)

}

使用

038 go语言网络编程_Socket编程_第12张图片
image-20200121145546814.png

并发聊天服务器

原理分析

038 go语言网络编程_Socket编程_第13张图片
image-20200121145638405.png

代码实现

package main

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



type Client struct {  //定义一个结构体用来存储不同的数据
    C    chan string //用户发送数据的管道
    Name string      //用户名
    Addr string      //网络地址
}

//保存在线用户   cliAddr =====> Client
var onlineMap map[string]Client     //声明键值对字典用来保存在线用户,下面需要使用make进行初始化

var messaage = make(chan string)   ////存储转发信息的通道

//新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息
func Manager() {
    //给map分配空间,只是声明只有 nil 必须初始化才能使用
    onlineMap = make(map[string]Client)  //因为这个协程是最早开启的,只会分配一次的map空间,只声明是不能使用的

    for {
        msg := <-messaage //没有消息前,这里会阻塞

        //遍历map, 给map每个成员都发送此消息
        for _, cli := range onlineMap {
            cli.C <- msg
        }
    }
}

func WriteMsgToClient(cli Client, conn net.Conn) {
    for msg := range cli.C { //给当前客户端发送信息,cli.C是通道,只有有数据才行写入
        conn.Write([]byte(msg + "\n"))
    }
}


// 第7步 广播信息的创建
func MakeMsg(cli Client, msg string) (buf string) {
    buf = "[" + cli.Addr + "]" + cli.Name + ": " + msg

    return
}

// 第5步,进行连接的处理
func HandleConn(conn net.Conn) { //处理用户链接
    defer conn.Close()

    //获取客户端的网络地址
    cliAddr := conn.RemoteAddr().String()

    //创建一个结构体, 默认,用户名和网络地址一样
    cli := Client{make(chan string), cliAddr, cliAddr}
    //把结构体添加到map
    onlineMap[cliAddr] = cli   //键使用网络地址作为唯一值

    //新开一个协程,专门给当前客户端发送信息   第6步
    go WriteMsgToClient(cli, conn)
    //广播某个在线
    //messaage <- "[" + cli.Addr + "]" + cli.Name + ": login"
    messaage <- MakeMsg(cli, "login")  //用来广播的信息
    //提示,我是谁
    cli.C <- MakeMsg(cli, "I am here")  //写入以后给当前客户端发送消息

    isQuit := make(chan bool)  //对方是否主动退出
    hasData := make(chan bool) //对方是否有数据发送

    //新建一个协程,接收用户发送过来的数据  第8步
    go func() {
        buf := make([]byte, 2048)  //定义一个切片用来读取数据的
        for {   //循环别忘记加
            n, err := conn.Read(buf)
            if n == 0 { //对方断开,或者,出问题
                isQuit <- true //对方退出了设置一个通道的断开标志位为true
                fmt.Println("conn.Read err = ", err)
                return
            }
            // 当前客户端发送的信息需要进行转发
            msg := string(buf[:n-1]) //通过windows nc测试,多一个换行
            if len(msg) == 3 && msg == "who" {
                //遍历map,给当前用户发送所有成员  //第10步 查询有哪些在线用户
                conn.Write([]byte("user list:\n"))
                for _, tmp := range onlineMap {
                    msg = tmp.Addr + ":" + tmp.Name + "\n"
                    conn.Write([]byte(msg))
                }

                //第11步 修改当前用户名
            } else if len(msg) >= 8 && msg[:6] == "rename" {
                // rename|mike
                name := strings.Split(msg, "|")[1]  //查看需要修改成什么名
                cli.Name = name //进行名字的修改
                onlineMap[cliAddr] = cli //修改该键对应的结构体
                conn.Write([]byte("rename ok\n"))  //返回是否改名成功

            } else {
                //转发此内容 第9步
                messaage <- MakeMsg(cli, msg)
            }

            hasData <- true //代表有数据
        }

    }() //别忘了()

    for {
        //通过select检测channel的流动 //第12步用户主动退出
        select {
        case <-isQuit: //使用select进行检测,如果有退出的标志位,则删除此用户
            delete(onlineMap, cliAddr)            //当前用户从map移除
            messaage <- MakeMsg(cli, "login out") //广播谁下线了

            return
        case <-hasData:
           //  有数据标志位就不用管
        case <-time.After(30 * time.Second): //60s后   //60s后将当前用户移除
            delete(onlineMap, cliAddr)                     //当前用户从map移除
            messaage <- MakeMsg(cli, "time out leave out") //广播谁下线了
            return
        }
    }
}

func main() {
    //监听 第1步
    listener, err := net.Listen("tcp", ":8001")
    if err != nil {
        fmt.Println("net.Listen err = ", err)
        return
    }

    defer listener.Close()

    //新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息  4
    go Manager()  //第4步

    //主协程,循环阻塞等待用户链接  第2步
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("listener.Accept err = ", err)
            continue   //防止整个程序的中断
        }

        go HandleConn(conn) //处理用户链接 第3步
    }

}

需要结合上面的原理分析详细的分析代码的实现

执行结果

038 go语言网络编程_Socket编程_第14张图片
录制_2020_01_22_09_01_31_758.gif

你可能感兴趣的:(038 go语言网络编程_Socket编程)