net 包使用指南
网络编程
net 包是网络相关的核心包。net 里面包含了 http、rpc 等关键包。
在 net 里面,最重要的两个调用:
- Listen(network, addr string):监听某个端口,等待客户端连接
- Dial(network, addr string):拨号,其实也就是连上某个服务端
│net
├── http
│ ├── cgi
│ │ └── testdata
│ ├── cookiejar
│ ├── fcgi
│ ├── httptest
│ ├── httptrace
│ ├── httputil
│ ├── internal
│ │ ├── ascii
│ │ └── testcert
│ ├── pprof
│ └── testdata
├── internal
│ └── socktest
├── mail
├── netip
├── rpc
│ └── jsonrpc
├── smtp
├── testdata
├── textproto
└── url
tree -d
通信基本流程
基本分成两个大阶段。
创建连接阶段:
- 服务端开始监听一个端口
- 客户端拨通服务端,两者协商创建连接 (TCP)
通信阶段:
- 客户端不断发送请求
- 服务端读取请求
- 服务端处理请求
- 服务端写回响应
net.Listen
Listen 是监听一个端口,准备读取数据。它还有几个类似接口,可以直接使用:
- ListenTCP
- ListenUDP
- ListenIP
- ListenUnix
这几个方法都是返回 Listener 的具体类,如 TCPListener。一般用 Listen 就可以,除非你要依赖于具体的网络协议特性。
网络通信用 TCP 还是用 UDP 是一个影响巨大的事情,一般确认了就不会改。
func Serve(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go func() {
handleConn(conn)
}()
}
}
处理连接
处理连接基本上就是在一个 for 循环内:
- 先读数据:读数据要根据上层协议来决定怎么读。例如,简单的 RPC 协议一般是分成两段读,先读头部,根据头部得知 Body 有多长,再把剩下的数据读出来。
- 处理数据
- 回写响应:即便处理数据出错,也要返回一个错误给客户端,不然客户端不知道你处理出错了。
func handleConn(conn net.Conn) {
for {
// 读数据
bs := make([]byte, 8)
_, err := conn.Read(bs)
if err == io.EOF || err == net.ErrClosed ||
err == io.ErrUnexpectedEOF {
// 一般关闭的错误比较懒得管
// 也可以把关闭错误输出到日志
_ = conn.Close()
return
}
if err != nil {
continue
}
res := handleMsg(bs)
_, err = conn.Write(res)
if err == io.EOF || err == net.ErrClosed ||
err == io.ErrUnexpectedEOF {
_ = conn.Close()
return
}
}
}
错误处理
在读写的时候,都可能遇到错误,一般来说代表连接已经关掉的是这三个: EOF、 ErrUnexpectedEOF 和 ErrClosed。
但是,我建议只要是出错了就直接关闭,这样对客户端和服务端代码都简单。
func handleConnV1(conn net.Conn) {
for {
// 读数据
bs := make([]byte, 8)
_, err := conn.Read(bs)
if err != nil {
// 一般关闭的错误比较懒得管
// 也可以把关闭错误输出到日志
_ = conn.Close()
return
}
res := handleMsg(bs)
_, err = conn.Write(res)
if err != nil {
_ = conn.Close()
return
}
}
}
net.Dial
net.Dial 是指创建一个连接,连上远端的服务器。
它也是有几个类似的方法:
- DiallP
- DialTCP
- DialUDP
- DialUnix
- DialTimeout
只有 DialTimeout 稍微特殊一点,它多了一个超时参数。
类似于 Listen,我也是建议大家直接使用 DialTimeout,因为设置超时可以避免一直阻塞。
func Connect(addr string) error {
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
if err != nil {
return err
}
defer func() {
_ = conn.Close()
}()
for {
// 发送请求
_, err = conn.Write([]byte("hello"))
if err != nil {
return err
}
res := make([]byte, 8)
// 接受响应
_, err = conn.Read(res)
if err != nil {
return err
}
// 这两句是为了测试,不用在意
fmt.Println(string(res))
time.Sleep(time.Second)
}
}
goroutine 问题
前面的模板,我们是在创建了连接之后,就交给另外一个 goroutine 去处理,除了这个位置,还有两个位置:
- 在读取了请求之后,交给别的 goroutine 处理,当前的 goroutine 继续读请求
- 写响应的时候,交给别的 goroutine 去写
由上至下:
- TCP 通信效率提高
- 系统复杂度提高
因为 goroutine 非常轻量,所以即便是在模式一下,对于小型应用来说,性能也可以满足。
代码演示 —— 创建简单的 TCP 服务器
type Server struct {
addr string
}
func (s *Server) StartAndServe() error {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
return err
}
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go func() {
// 直接在这里处理
er := s.handleConn(conn)
if er != nil {
_ = conn.Close()
fmt.Printf("connect error: %v", er)
}
}()
}
}
func (s *Server) handleConn(conn net.Conn) error {
for {
// 读数据长度
bs := make([]byte, lenBytes)
_, err := conn.Read(bs)
if err != nil {
return err
}
reqBs := make([]byte, binary.BigEndian.Uint64(bs))
_, err = conn.Read(reqBs)
if err != nil {
return err
}
res := string(reqBs) + ", from response"
// 总长度
bs = make([]byte, lenBytes, len(res)+lenBytes)
// 写入消息长度
binary.BigEndian.PutUint64(bs, uint64(len(res)))
bs = append(bs, res...)
_, err = conn.Write(bs)
if err != nil {
return err
}
}
}
func TestServer_StartAndServe(t *testing.T) {
s := &Server{
addr: ":8080",
}
_ = s.StartAndServe()
}
// 假定我们永远用 8 个字节来存放数据长度
const lenBytes = 8
type Client struct {
addr string
}
func (c *Client) Send(msg string) (string, error) {
conn, err := net.DialTimeout("tcp", c.addr, 3*time.Second)
if err != nil {
return "", err
}
defer func() {
_ = conn.Close()
}()
// 总长度
bs := make([]byte, lenBytes, len(msg)+lenBytes)
// 写入消息长度
binary.BigEndian.PutUint64(bs, uint64(len(msg)))
bs = append(bs, msg...)
_, err = conn.Write(bs)
if err != nil {
return "", err
}
// 读取响应长度
lenBs := make([]byte, lenBytes)
_, err = conn.Read(lenBs)
if err != nil {
return "", err
}
resLength := binary.BigEndian.Uint64(lenBs)
// 读取响应
resBs := make([]byte, resLength)
_, err = conn.Read(resBs)
return string(resBs), nil
}
func TestClient_Send(t *testing.T) {
testCases := []struct {
req string
resp string
}{
{
req: "hello",
resp: "hello, from response",
},
{
req: "aaa bbb cc \n",
resp: "aaa bbb cc \n, from response",
},
}
c := &Client{
addr: "localhost:8080",
}
for _, tc := range testCases {
t.Run(tc.req, func(t *testing.T) {
resp, err := c.Send(tc.req)
assert.Nil(t, err)
assert.Equal(t, tc.resp, resp)
})
}
}
总结
- 网络的基础知识,包含 TCP 和 UDP 的基础知识。
- 三次握手和四次挥手
- 如何利用 Go 写一个简单的 TCP 服务器。
- 记住 goroutine 和连接的关系,可以在不同的环节使用不同的 goroutine,以充分利用 TCP 的全双工通信。
延伸阅读
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。