Golang Socket Program

in #golang6 years ago

socket编程

什么是Socket?

Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。

Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。S

ocket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

常用的Socket类型有两种:

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

Socket如何通信?

首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。

其实TCP/IP协议族已经帮我们解决了这个问题:

  • 网络层的“ip地址”可以唯一标识网络中的主机,
  • 传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在他们之间进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。

就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆Socket”。

Socket基础知识:

Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,

而要确定一个进程的需要三元组,需要IP地址和端口。

  • IPv4地址:

    IPv4的地址位数为32位,也就是最多有2的32次方的网络设备可以联到Internet上.

  • IPv6地址:

    IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。

    地址格式类似这样:2002:c0e8:82e7:0:0:0:c0e8:82e7

Go支持的IP类型

type IP []byte

在net包中有很多函数来操作IP,但是其中比较有用的也就几个,其中ParseIP(s string) IP函数会把一个IPv4或者IPv6的地址转化成IP类型

  • Example: 判断给定的参数是否是一个合法的IP地址:
package main

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

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr,"Usage: %s ip addr\n",os.Args[0])
        os.Exit(1)
    }

    name := os.Args[1]
    addr := net.ParseIP(name)
    if addr == nil {
        fmt.Println("Invalid address")
    } else {
        fmt.Println("The address is ",addr.String())
    }
    os.Exit(0)
}

TCP Socket

  • TCPConn类型,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数:
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 {
    IP IP
    Port int
}

函数: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地址;

TCP client:

net包中的DialTCP函数来建立一个TCP连接,并返回一个TCPConn类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的TCPConn对象来进行数据交换。

客户端通过TCPConn对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下:

func DialTCP(net string, laddr *TCPAddr) (c *TCPConn, err os.Error)
  • net参数是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
  • laddr表示本机地址,一般设置为nil
  • raddr表示远程的服务地址
package main

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

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port ",os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4",service)
    checkError(err)
    conn, err := net.DialTCP("tcp",nil,tcpAddr)
    checkError(err)
    _, err = conn.Write([]byte("HEAD / http/1.0\r\n\r\n"))
    checkError(err)
    result,err := ioutil.ReadAll(conn)
    checkError(err)
    fmt.Println(string(result))
    os.Exit(0)
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr,"Fatal error: %s",err.Error())
        os.Exit(1)
    }
}

TCP server:

通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下:

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

实现一个简单的时间同步服务,监听7777端口:

package main

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

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr,"Fatal error: %s",err.Error())
        os.Exit(1)
    }
}

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4",service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn,err := listener.Accept()
        if err != nil {
            continue
        }
        daytime := time.Now().String()
        conn.Write([]byte(daytime))  // don't care about return value.
        conn.Close()                // we're finished with this client.
    }
}

上面的代码有个缺点,执行的时候是单任务的,不能同时接收多个请求,那么该如何改造以使它支持多并发呢?

package main

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

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr,"Fatal error: %s",err.Error())
        os.Exit(1)
    }
}

func handleClient(conn net.Conn) {
    defer conn.Close()
    daytime := time.Now().String()
    conn.Write([]byte(daytime + "\n"))
}

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4",service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn,err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

通过把业务处理分离到函数handleClient,我们就可以进一步地实现多并发执行了。

  • 如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?
package main

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

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

func handleClient(conn net.Conn) {
    conn.SetDeadline(time.Now().Add(2 * time.Minute))   // set 2 minutes timeout
    request := make([]byte,128) // set maxium request length to 128B to prevent flood attack
    defer conn.Close()  // close connection befor exit.

    for {
        read_len, err := conn.Read(request)

        if err != nil {
            fmt.Println(err)
            break
        }

        if read_len == 0 {
            break // connection already close by client.
        } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" {
            daytime := strconv.FormatInt(time.Now().Unix(),10)
            conn.Write([]byte(daytime))
        } else {
            daytime := time.Now().String()
            conn.Write([]byte(daytime))
        }

        request = make([]byte,128)  // clear last read content
    }
}

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener,err := net.ListenTCP("tcp",tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

使用conn.Read()不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于conn.SetReadDeadline()设置了超时,当一定时间内客户端无请求发送,conn便会自动关闭,下面的for循环即会因为连接已关闭而跳出。

需要注意的是,request在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为conn.Read()会将新读取到的内容append到原内容之后。

控制TCP连接:

TCP有很多连接控制函数,我们平常用到比较多的有如下几个函数:

func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)

设置建立链接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接会自动关闭。

func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error

用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。

func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

设置keepAlive属性,是操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统可以通过该包来判断一个tcp连接是否已经断开,在windows上默认2个小时没有收到数据和keepalive包的时候认为tcp连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。

UDP Socket:

Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。

其他基本几乎一模一样,只有TCP换成了UDP而已。

UDP的几个主要函数如下所示:

func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

一个UDP的客户端代码如下所示,我们可以看到不同的就是TCP换成了UDP而已:

package main

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

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr,"Usage: %s host:port",os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    udpAddr,err := net.ResolveUDPAddr("udp4",service)
    checkError(err)
    conn,err := net.DialUDP("udp",nil,udpAddr)
    checkError(err)
    _,err = conn.Write([]byte("anything"))
    checkError(err)
    var buf [512]byte
    n, err := conn.Read(buf[0:])
    checkError(err)
    fmt.Println(string(buf[0:n]))
    os.Exit(0)
}
  • UDP服务器端:
package main

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

func main() {
    service := ":1200"
    udpAddr, err := net.ResolveUDPAddr("udp4",service)
    checkError(err)
    conn,err := net.ListenUDP("udp",udpAddr)
    checkError(err)

    for {
        handleClient(conn)
    }
}

func handleClient(conn *net.UDPConn) {
    var buf [512]byte
    _, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }
    daytime := time.Now().String()
    conn.WriteToUDP([]byte(daytime),addr)
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr,"Fatal error ",err.Error())
        os.Exit(1)
    }
}
Sort:  

Thank you so much for sharing this amazing post with us!

Have you heard about Partiko? It’s a really convenient mobile app for Steem! With Partiko, you can easily see what’s going on in the Steem community, make posts and comments (no beneficiary cut forever!), and always stayed connected with your followers via push notification!

Partiko also rewards you with Partiko Points (3000 Partiko Point bonus when you first use it!), and Partiko Points can be converted into Steem tokens. You can earn Partiko Points easily by making posts and comments using Partiko.

We also noticed that your Steem Power is low. We will be very happy to delegate 15 Steem Power to you once you have made a post using Partiko! With more Steem Power, you can make more posts and comments, and earn more rewards!

If that all sounds interesting, you can:

Thank you so much for reading this message!

Coin Marketplace

STEEM 0.20
TRX 0.15
JST 0.029
BTC 63483.25
ETH 2601.63
USDT 1.00
SBD 2.81