Go语言学习#17-如何进行TCP socket编程


=Start=

缘由:

学习、整理一下如何用Go语言进行TCP socket编程,方便以后需要的时候进行参考和使用。

正文:

参考解答:

基于TCP(面向连接)的socket编程,分为客户端和服务器端。

客户端的流程如下:
(1)创建套接字(socket)
(2)向服务器发出连接请求(connect)
(3)和服务器端进行通信(send/recv)
(4)关闭套接字(close)

服务器端的流程如下:
(1)创建套接字(socket)
(2)将套接字绑定到一个本地地址和端口上(bind)
(3)将套接字设为监听模式,准备接收客户端请求(listen)
(4)等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)
(5)用返回的套接字和客户端进行通信(send/recv)
(6)返回,等待另一个客户请求
(7)关闭套接字(close)


从TCP socket诞生后,网络编程架构模型也几经演化,大致是:“每进程一个连接” –> “每线程一个连接” –> “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化,服务程序愈加强大,可以支持更多的连接,获得更好的处理性能。

目前主流Web Server一般均采用的都是”Non-Block + I/O多路复用”(有的也结合了多线程、多进程)。不过I/O多路复用也给使用者带来了不小的复杂度,以至于后续出现了许多高性能的I/O多路复用框架, 比如libeventlibevlibuv等,以帮助开发者简化开发复杂性,降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂,且有悖于“一般逻辑”设计,为此Go语言将该“复杂性”隐藏在Runtime中了:Go开发者无需关注socket是否是 non-block的,也无需亲自注册文件描述符的回调,只需在每个连接对应的goroutine中以“block I/O”的方式对待socket处理即可,这可以说大大降低了开发人员的心智负担。一个典型的Go Server端程序大致如下:

func handleConn(c net.Conn) { 
    defer c.Close() 
    for { 
        // read from the connection 
        // ... ... 
        // write to the connection 
        //... ... 
    } 
} 

func main() { 
    l, err := net.Listen("tcp", ":8888") 
    if err != nil { 
        fmt.Println("listen error:", err) 
        return 
    }
    defer l.Close()

    for { 
        c, err := l.Accept() 
        if err != nil { 
            fmt.Println("accept error:", err) 
            break 
        } 
        // start a new goroutine to handle 
        // the new connection. 
        go handleConn(c) 
    } 
}

上面的内容可能更偏概念性的描述,下面通过几个简单的例子帮助建立起直观的认识。

客户端代码:

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strconv"
)

const (
    SERVER_IP       = "127.0.0.1"
    SERVER_PORT     = 10006
    SERVER_RECV_LEN = 10
)

func checkError(err error) {
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func main() {
    address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT)
    fmt.Printf("value of address: %v (%T)\n\n", address, address)

    conn, err := net.Dial("tcp", address)
    checkError(err)
    defer conn.Close()

    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        line := input.Text()
        lineLen := len(line)

        n := 0
        for written := 0; written < lineLen; written += n {
            var toWrite string
            if lineLen-written > SERVER_RECV_LEN {
                toWrite = line[written : written+SERVER_RECV_LEN]
            } else {
                toWrite = line[written:]
            }

            n, err = conn.Write([]byte(toWrite))
            checkError(err)

            fmt.Println("Write:", toWrite)

            msg := make([]byte, SERVER_RECV_LEN)
            n, err = conn.Read(msg)
            checkError(err)

            fmt.Println("Response:", string(msg))
        }
    }
}

服务端(单进程/单线程/单goroutine)代码:

package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "strings"
)

const (
    SERVER_IP       = "127.0.0.1"
    SERVER_PORT     = 10006
    SERVER_RECV_LEN = 10
)

func main() {
    address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT)
    listener, err := net.Listen("tcp", address)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        defer conn.Close()

        fmt.Println("Client Info:", conn.RemoteAddr().String())
        for {
            data := make([]byte, SERVER_RECV_LEN)
            _, err = conn.Read(data)
            if err != nil {
                fmt.Println(err)
                break
            }

            strData := string(data)
            fmt.Println("Received:", strData)

            upper := strings.ToUpper(strData)
            _, err = conn.Write([]byte(upper))
            if err != nil {
                fmt.Println(err)
                break
            }

            fmt.Println("Send:", upper)
        }
    }
}

上面的服务端程序存在一个问题,那就是一次只能为一个客户端提供服务(for循环),如果一个客户端连接时间过长,可能会让服务器无法服务于其他的客户端,这就是拒绝服务攻击,最简单的解决这种问题的方式就是将conn的处理单独启动线程,在Go 语言中我们只需要借助于 go 方法启动一个routine即可。

package main

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "strings"
)

const (
    SERVER_IP       = "127.0.0.1"
    SERVER_PORT     = 10006
    SERVER_RECV_LEN = 10
)

func main() {
    address := SERVER_IP + ":" + strconv.Itoa(SERVER_PORT)
    listener, err := net.Listen("tcp", address)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }

        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()

    fmt.Println("Client Info:", conn.RemoteAddr().String())
    for {
        data := make([]byte, SERVER_RECV_LEN)
        _, err := conn.Read(data)
        if err != nil {
            fmt.Println(err)
            break
        }

        strData := string(data)
        fmt.Println("Received:", strData)

        upper := strings.ToUpper(strData)
        _, err = conn.Write([]byte(upper))
        if err != nil {
            fmt.Println(err)
            break
        }

        fmt.Println("Send:", upper)
    }
}

将客户端数据处理的部分封装成了一个handleConn函数,在与客户端连接建立时,通过Go 语言的go关键字调用handleConn函数,启动一个新的协程进行处理,这样,main函数会立刻重新阻塞在Accept上,从而保证服务器可以时刻监听客户端连接的建立,提高了处理性能。

 

参考链接:

=END=

, ,

《 “Go语言学习#17-如何进行TCP socket编程” 》 有 3 条评论

发表回复

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