在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程序。各客戶端程序可以發送消息給服務端,並得到回應。每一個客戶端的加入、離開以及發送的消息都會向其他在線的客戶端進行廣播。效果如下圖所示。

