Go是自带runtime的跨平台编程语言,Go中暴露给语言使用者的TCP socket API是建立OS原生TCP socket接口之上的,所以在使用上相对简单。
建立网络连接过程:TCP连接的建立需要经历客户端和服务端的三次握手的过程。Go 语言net包封装了系列API,在TCP连接中,服务端是一个标准的Listen + Accept的结构,而在客户端Go语言使用net.Dial或DialTimeout进行连接建立:
在Go语言的net包中有一个类型TCPConn,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数:
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)
TCPConn可以用在客户端和服务器端来读写数据。
在Go语言中通过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地址,例如"www.google.com:80" 或者"127.0.0.1:22"。
我们来看一个TCP 连接建立的具体代码:
// TCP server 服务端代码
package main
import (
"bufio"
"fmt"
"io"
"net"
"time"
)
func main() {
var tcpAddr *net.TCPAddr
tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:999")
tcpListener, _ := net.ListenTCP("tcp", tcpAddr)
defer tcpListener.Close()
fmt.Println("Server ready to read ...")
for {
tcpConn, err := tcpListener.AcceptTCP()
if err != nil {
fmt.Println("accept error:", err)
continue
}
fmt.Println("A client connected : " + tcpConn.RemoteAddr().String())
go tcpPipe(tcpConn)
}
}
func tcpPipe(conn *net.TCPConn) {
ipStr := conn.RemoteAddr().String()
defer func() {
fmt.Println(" Disconnected : " + ipStr)
conn.Close()
}()
reader := bufio.NewReader(conn)
i := 0
for {
message, err := reader.ReadString('\n') //将数据按照换行符进行读取。
if err != nil || err == io.EOF {
break
}
fmt.Println(string(message))
time.Sleep(time.Second * 3)
msg := time.Now().String() + conn.RemoteAddr().String() + " Server Say hello! \n"
b := []byte(msg)
conn.Write(b)
i++
if i > 10 {
break
}
}
}
服务端 TCPListener.AcceptTCP() 接受一个客户端连接请求,通过go tcpPipe(tcpConn) 开启一个新协程来管理这对连接。 在func tcpPipe(conn *net.TCPConn) 中,处理服务端和客户端数据的交换,在这段代码for中,通过 bufio.NewReader 读取客户端发送过来的数据。
客户端代码:
// TCP client
package main
import (
"bufio"
"fmt"
"io"
"net"
"time"
)
func main() {
var tcpAddr *net.TCPAddr
tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:999")
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
fmt.Println("Client connect error ! " + err.Error())
return
}
defer conn.Close()
fmt.Println(conn.LocalAddr().String() + " : Client connected!")
onMessageRecived(conn)
}
func onMessageRecived(conn *net.TCPConn) {
reader := bufio.NewReader(conn)
b := []byte(conn.LocalAddr().String() + " Say hello to Server... \n")
conn.Write(b)
for {
msg, err := reader.ReadString('\n')
fmt.Println("ReadString")
fmt.Println(msg)
if err != nil || err == io.EOF {
fmt.Println(err)
break
}
time.Sleep(time.Second * 2)
fmt.Println("writing...")
b := []byte(conn.LocalAddr().String() + " write data to Server... \n")
_, err = conn.Write(b)
if err != nil {
fmt.Println(err)
break
}
}
}
客户端 net.DialTCP("tcp", nil, tcpAddr)
向服务端发起一个连接请求,调用onMessageRecived(conn)
,处理客户端和服务端数据的发送与接收。在 func onMessageRecived(conn *net.TCPConn)
中,通过 bufio.NewReader
读取客户端发送过来的数据。
上面2个例子你可以试着运行一下,程序支持多个客户端同时运行。当然,这两个例子只是简单的TCP原始连接,在实际中,我们还可能需要定义协议。
用Socket进行通信,发送的数据包一定是有结构的,类似于:数据头+数据长度+数据内容+校验码+数据尾。而在TCP流传输的过程中,可能会出现分包与黏包的现象。我们为了解决这些问题,需要我们自定义通信协议进行封包与解包。对这方面内容如有兴趣可以去了解更多相关知识。