• golang socket与Linux socket比较分析


           在posix标准推出后,socket在各大主流OS平台上都得到了很好的支持。而Golang是自带runtime的跨平台编程语言,Go中提供给开发者的socket API是建立在操作系统原生socket接口之上的。但golang 中的socket接口在行为特点与操作系统原生接口有一些不同。本文将对结合一个简单的hello/hi的网络聊天程序加以分析。

        一、socket简介

           首先进程之间可以进行通信的前提是进程可以被唯一标识,在本地通信时可以使用PID唯一标识,而在网络中这种方法不可行,我们可以通过IP地址+协议+端口号来唯一标识一个进程,然后利用socket进行通信。

           socket是位于应用层和传输层中的抽象层,它是不属于七层架构中的:

                                                         

         而socket通信流程如下:

    1.服务端创建socket

    2.服务端绑定socket和端口号

    3.服务端监听该端口号

    4.服务端启动accept()用来接收来自客户端的连接请求,此时如果有连接则继续执行,否则将阻塞在这里。

    5.客户端创建socket

    6.客户端通过IP地址和端口号连接服务端,即tcp中的三次握手

    7.如果连接成功,客户端可以向服务端发送数据

    8.服务端读取客户端发来的数据

    9.任何一端均可主动断开连接

                                                         

     二、socket编程

        有了抽象的socket后,当使用TCP或UDP协议进行web编程时,可以通过以下的方式进行

    服务端伪代码:

    listenfd = socket(……)
    bind(listenfd, ServerIp:Port, ……)
    listen(listenfd, ……)
    while(true) {
      conn = accept(listenfd, ……)
      receive(conn, ……)
      send(conn, ……)
    }

    客户端伪代码:

    clientfd = socket(……)
    connect(clientfd, serverIp:Port, ……)
    send(clientfd, data)
    receive(clientfd, ……)
    close(clientfd)

          上述伪代码中,listenfd就是为了实现服务端监听创建的socket描述符,而bind方法就是服务端进程占用端口,避免其它端口被其它进程使用,listen方法开始对端口进行监听。下面的while循环用来处理客户端源源不断的请求,accept方法返回一个conn,用来区分各个客户端的连接的,之后的接受和发送动作都是基于这个conn来实现的。其实accept就是和客户端的connect一起完成了TCP的三次握手。

    三、golang中的socket

          golang中提供了一些网络编程的API,包括Dial,Listen,Accept,Read,Write,Close等.

    3.1 Listen()

         首先使用服务端net.Listen()方法创建套接字,绑定端口和监听端口。

    1 func Listen(network, address string) (Listener, error) {
    2     var lc ListenConfig
    3     return lc.Listen(context.Background(), network, address)
    4 }

          以上是golang提供的Listen函数源码,其中network表示网络协议,如tcp,tcp4,tcp6,udp,udp4,udp6等。address为绑定的地址,返回的Listener实际上是一个套接字描述符,error中保存错误信息。

         而在Linuxsocket中使用socket,bind和listen函数来完成同样功能

    // socket(协议域,套接字类型,协议)
    int socket(int domain, int type, int protocol);
    
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    int listen(int sockfd, int backlog);

    3.2 Dial()

       当客户端想要发起某个连接时,就会使用net.Dial()方法来发起连接

    func Dial(network, address string) (Conn, error) {
        var d Dialer
        return d.Dial(network, address)
    }

         其中network表示网络协议,address为要建立连接的地址,返回的Conn实际是标识每一个客户端的,在golang中定义了一个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
    }

    type conn struct {
        fd *netFD
    }

         其中netFD是golang网络库里最核心的数据结构,贯穿了golang网络库所有的API,对底层的socket进行封装,屏蔽了不同操作系统的网络实现,这样通过返回的Conn,我们就可以使用golang提供的socket底层函数了。

      在Linuxsocket中使用connect函数来创建连接

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    3.3 Accept()

           当服务端调用net.Listen()后会开始监听指定地址,而客户端调用net.Dial()后发起连接请求,然后服务端调用net.Accept()接收请求,这里端与端的连接就建立好了,实际上到这一步也就完成了TCP中的三次握手。

    Accept() (Conn, error)

          golang的socket实际上是非阻塞的,但golang本身对socket做了一定处理,使其看起来是阻塞的。

          在Linuxsocket中使用accept函数来实现同样功能

    //sockfd是服务器套接字描述符,sockaddr返回客户端协议地址,socklen_t是协议地址长度。
    int
    accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    3.4 Write()

          端与端的连接已经建立了,接下来开始进行读写操作,conn.Write()向socket写数据

       Write(b []byte) (n int, err error)
    func (c *conn) Write(b []byte) (int, error) {
        if !c.ok() {
            return 0, syscall.EINVAL
        }
        n, err := c.fd.Write(b)
        if err != nil {
            err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
        }
        return n, err
    }

        其中写入的数据是一个二进制字节流,n返回的数据的长度,err保存错误信息

        Linuxsocket中对应的则是send函数

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    3.5 Read()

         客户端发送完数据以后,服务端可以接收数据,golang中调用conn.Read()读取数据,源码如下:

    Read(b []byte) (n int, err error)
    func (c *conn) Read(b []byte) (int, error) {
        if !c.ok() {
            return 0, syscall.EINVAL
        }
        n, err := c.fd.Read(b)
        if err != nil && err != io.EOF {
            err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
        }
        return n, err
    }

          其参数与Write()中的含义一样,在Linuxsocket中使用recv函数完成此功能

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    3.6 Close()

         当服务端或者客户端想要关闭套接字时,调用Close()方法关闭连接。

    Close() error
    func (c
    *conn) Close() error { if !c.ok() { return syscall.EINVAL } err := c.fd.Close() if err != nil { err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return err }

        在Linuxsocket中使用close函数

    int close(int socketfd)

    四、golang实现Hello/hi网络聊天程序

    4.1 server.go

    package main
    import (
        "fmt"
        "net"
        "strings"
    )
    //UserMap保存的是当前聊天室所有用户id的集合
    var UserMap map[string]net.Conn = make(map[string]net.Conn)
    func main() {
        //监听本地所有ip的8000端口
        listen_socket, err := net.Listen("tcp", "127.0.0.1:8000")
        if err != nil {
            fmt.Println("服务启动失败")
        }
        //关闭监听的端口
        defer listen_socket.Close()
        fmt.Println("等待用户加入聊天室")
        for {
            //用于conn接收链接
            conn, err := listen_socket.Accept()
            if err != nil {
                fmt.Println("连接失败")
            }
            //打印加入聊天室的公网IP地址
            fmt.Println(conn.RemoteAddr(), "连接成功")
            //定义一个goroutine,这里主要是为了并发运行
            go DataProcessing(conn)
        }
    }
    func DataProcessing(conn net.Conn) {
        for {
            //定义一个长度为255的切片
            data := make([]byte, 255)
            //读取客户端传来的数据,msg_length保存长度,err保存错误信息
            msg_length, err := conn.Read(data)
            if msg_length == 0 || err != nil {
                continue
            }
            //解析协议,通过分隔符"|"获取需要的数据,msg_str[0]存放操作类别
            //msg_str[1]存放用户名,msg_str[2]如果有就存放发送的消息
            msg_str := strings.Split(string(data[0:msg_length]), "|")
            switch msg_str[0] {
            case "nick":
                fmt.Println(conn.RemoteAddr(), "的用户名是", msg_str[1])
                for user, message := range UserMap {
                    //向除自己之外的用户发送加入聊天室的消息
                    if user != msg_str[1] {
                        message.Write([]byte("用户" + msg_str[1] + "加入聊天室"))
                    }
                }
                //将该用户加入用户id的集合
                UserMap[msg_str[1]] = conn
            case "send":
                for user, message := range UserMap {
                    if user != msg_str[1] {
                        fmt.Println("Send "+msg_str[2]+" to ", user)
                        //向除自己之外的用户发送聊天消息
                        message.Write([]byte("       用户" + msg_str[1] + ": " + msg_str[2]))
                    }
                }
            case "quit":
                for user, message := range UserMap {
                    if user != msg_str[1] {
                        //向除自己之外的用户发送退出聊天室的消失
                        message.Write([]byte("用户" + msg_str[1] + "退出聊天室"))
                    }
                }
                fmt.Println("用户 " + msg_str[1] + "退出聊天室")
                //将该用户名从用户id的集合中删除
                delete(UserMap, msg_str[1])
            }
        }
    }

    5.2 client.go

    package main
    import (
        "bufio"
        "fmt"
        "net"
        "os"
    )
    var nick string = ""
    func main() {
        //拨号操作
        conn, err := net.Dial("tcp", "127.0.0.1:8000")
        if err != nil {
            fmt.Println("连接失败")
        }
        defer conn.Close()
        fmt.Println("连接服务成功 
    ")
        //创建用户名
        fmt.Printf("在进入聊天室之前给自己取个名字吧:")
        fmt.Scanf("%s", &nick)
        fmt.Println("用户" + nick + "欢迎进入聊天室")
        //向服务器发送数据
        conn.Write([]byte("nick|" + nick))
        //定义一个goroutine,这里主要是为了并发运行
        go SendMessage(conn)
        var msg string
        for {
            msg = ""
            //由于golangz的fmt包输入字符串不能读取空格,所以此处重写了一个Scanf函数
            Scanf(&msg)
            if msg == "quit" {
                //这里的quit,send,以及上面的nick是为了识别客户端做的是设置用户名,发消息还是退出
                conn.Write([]byte("quit|" + nick))
                break
            }
            if msg != "" {
                conn.Write([]byte("send|" + nick + "|" + msg))
            }
        }
    }
    func SendMessage(conn net.Conn) {
        for {
            //定义一个长度为255的切片
            data := make([]byte, 255)
            //读取服务器传来的数据,msg_length保存长度,err保存错误信息
            msg_length, err := conn.Read(data)
            if msg_length == 0 || err != nil {
                break
            }
            fmt.Println(string(data[0:msg_length]))
        }
    }
    //重写的Scanf函数
    func Scanf(a *string) {
        reader := bufio.NewReader(os.Stdin)
        data, _, _ := reader.ReadLine()
        *a = string(data)
    }

          golang中使用goroutine实现并发

    5.3 运行截图

    多人聊天截图(左上角为服务端)

    用户退出聊天室(左上角为服务端)

     参考资料:

         https://tonybai.com/2015/11/17/tcp-programming-in-golang/

         https://www.jianshu.com/p/325ac02fc31c

         https://blog.csdn.net/dyd961121/article/details/81252920

  • 相关阅读:
    自定义simple_tag
    测试流程规范系列(2):测试计划
    测试流程规范系列(4):测试准入
    测试流程规范系列(3):测试用例
    测试流程规范系列(1):测试流程
    Jmeter系列(1):安装
    Jmeter系列(2):简介
    Jmeter系列(3):使用
    Jmeter系列(4):插件
    Jmeter系列(5):参数化
  • 原文地址:https://www.cnblogs.com/ustc-kunkun/p/11990285.html
Copyright © 2020-2023  润新知