Linux下Golang Socket编程原理分析与代码实现

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

一、socket简介

       首先进程之间可以进行通信的前提是进程可以被唯一标识,在本地通信时可以使用PID唯一标识,而在网络中这种方法不可行,我们可以通过IP地址+协议+端口号来唯一标识一个进程,然后利用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()方法创建套接字,绑定端口和监听端口。

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

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

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

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底层函数了。

  在Linux socket中使用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做了一定处理,使其看起来是阻塞的。

      在Linux socket中使用accept函数来实现同样功能:

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

3.4 Write()

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

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保存错误信息。

    Linux socket中对应的则是send函数:

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

3.5 Read()

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

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()中的含义一样,在Linux socket中使用recv函数完成此功能:

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

3.6 Close()

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

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
}

  在Linux socket中使用close函数:

int close(int socketfd);

四、golang实现网络聊天程序

4.1 server.go

package main

import (
    "bufio"
    "log"
    "net"
    "fmt"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:8848")
    if err != nil {
        log.Fatal(err)
    }
    go broadcaster()
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
            continue
        }
        go handleConn(conn)
    }
}

type client chan <- string // an outgoing message channel

var (
    entering = make(chan client)
    leaving = make(chan client)
    messages = make(chan string) // incoming messages from clients
)

func broadcaster() {
    clients := make(map[client]bool)
    for {
        select {
            // broadcast incoming message to all client's outgoing message channels
            case msg := <- messages:
                for cli := range clients {
                    cli <- msg
                }
            case cli := <- entering:
                clients[cli] = true
            case cli := <- leaving:
                delete(clients, cli)
                close(cli)
            }
    }
}

func handleConn(conn net.Conn) {
    ch := make(chan string) // outgoing clietnt messages
    go clientWriter(conn, ch)

    who := conn.RemoteAddr().String()
    ch <- "You are " + who
    messages <- who + " has arrived"
    entering <- ch

    input := bufio.NewScanner(conn)
    for input.Scan() {
        messages <- who + ": " + input.Text()
    }
    
    leaving <- ch
    messages <- who + " has left"
    conn.Close()
}

func clientWriter(conn net.Conn, ch <-chan string) {
    for msg := range ch {
        fmt.Fprintln(conn, msg)
    }
}

  server端设置了三类channel:entering、leaving以及messages用于在goroutine间共享客户端接入、离开及发送消息等数据状态。对于每一个客户端连接,server都开启单独的goroutine进行处理。对应于前述的各个channel,还设置了一个单独的broadcaster goroutine进行消息广播及客户端连接状态更新。

4.2 client.go

package main

import (
	"io"
	"log"
	"net"
	"os"
)

func main()  {
	conn, err := net.Dial("tcp", "localhost:8848")
	if err != nil {
		log.Fatal(err)
	}
	done := make(chan struct{})
	go func() {
		io.Copy(os.Stdout, conn)
		log.Println("done")
		done <- struct{}{}
	}()
	mustCopy(conn, os.Stdin)
	conn.Close()
	<- done
}

func mustCopy(dst io.Writer, src io.Reader) {
	if _, err := io.Copy(dst, src); err!= nil {
		log.Fatal(err)
	}
}

  编译两个源文件后,首先启动server程序,它监听8848端口,然后运行多个client程序。各客户端程序可以发送消息给服务端,并得到回应。每一个客户端的加入、离开以及发送的消息都会向其他在线的客户端进行广播。效果如下图所示。

Linux下Golang Socket编程原理分析与代码实现_第1张图片

 

 

 

你可能感兴趣的:(Linux下Golang Socket编程原理分析与代码实现)