Go 语言 Socket 的客户端和服务端简单实现:从 TCP 示例看连接、收发与回调

远山来客
远山来客
发布时间:2026-08-25| 109人浏览过
原创

前言

在 Go 里做 Socket 通信,最容易混淆的不是 API 名称,而是服务端和客户端各自该在什么时机建立连接、接收消息和触发回调。本文围绕一份简化的 TCP 示例,把服务端实现、客户端实现和实际调用方式拆开讲清楚,同时顺手指出源码里几个容易忽略的细节,方便你判断这套写法能否直接拿来用。

在 Go 里写 Socket 通信,难点往往不在 net.Listen 或 net.Dial 这些 API 本身,而是在把“连接建立、持续收消息、主动发消息、连接关闭”几件事组织清楚。本文基于一个简化的 TCP 封装示例,把服务端和客户端分别拆开来看,再串起来说明最基本的使用方式。

如果你正想快速写一个能双向收发消息的 TCP 通信样例,这套代码足够作为起点;同时,文中也会顺着源码指出几个实现细节,方便你判断哪些部分可以直接用,哪些地方还需要自己补强。

先看基础:三个事件回调分别负责什么

这份示例先定义了三个回调类型,用来把连接建立、收到消息和连接关闭这三类事件分离出来。这样做的好处是,服务端和客户端可以共用一套事件模型,业务逻辑也不必直接写死在底层读写流程里。

// listener.go

package socketer

import "net"

// ConnectListener 连接建立回调
type ConnectListener func(conn net.Conn)

// ReceiveListener 收到消息回调
type ReceiveListener func(data []byte, conn net.Conn)

// CloseListener 连接关闭事件
type CloseListener func(conn net.Conn)

从职责上看,它们分别对应三种常见场景:

  • ConnectListener:连接刚建立时触发,适合记录来源、初始化状态。
  • ReceiveListener:收到一段消息时触发,适合处理业务请求或返回结果。
  • CloseListener:连接结束时触发,适合做清理和日志记录。

后面的服务端和客户端结构体,都是围绕这三个事件来挂接逻辑的。

服务端怎么实现:监听连接并持续读取消息

服务端这部分主要做三件事:创建监听对象、注册事件回调、接受连接后持续读取数据。对应的核心代码如下。

服务端监听与单连接消息处理流程示意图
服务端处理链路用编辑卡片和箭头拆解服务端从监听到读取消息的处理链路。
// server.go

package socketer

import (
    "bufio"
    "io"
    "net"
)

type Server struct {
    listener net.Listener

    address string
    network string

    closeListener   CloseListener
    receiveListener ReceiveListener
    connectListener ConnectListener
}

func NewServer(network, address string) *Server {
    return &Server{
        network: network,
        address: address,
    }
}

func (s *Server) OnConnect(l ConnectListener) {
    s.connectListener = l
}

func (s *Server) OnReceive(l ReceiveListener) {
    s.receiveListener = l
}

func (s *Server) OnClose(l CloseListener) {
    s.closeListener = l
}

func (s *Server) Send(conn net.Conn, data []byte) (int, error) {
    return conn.Write(data)
}

func (s *Server) Close(conn net.Conn) error {
    err := conn.Close()
    if s.closeListener != nil {
        s.connectListener(conn)
    }
    return err
}

func (s *Server) Listen() error {
    var err error
    s.listener, err = net.Listen(s.network, s.address)
    if err != nil {
        return err
    }

    for {
        conn, err := s.listener.Accept()
        if err != nil {
            continue
        }
        if s.connectListener != nil {
            s.connectListener(conn)
        }
        go s.accept(conn)
    }
}

func (s *Server) accept(conn net.Conn) {
    defer conn.Close()

    reader := bufio.NewReader(conn)

    for {
        message, err := reader.ReadBytes('n')
        if err != nil {
            if err == io.EOF {
                s.Close(conn)
            }
            break
        }

        if s.receiveListener != nil {
            go s.receiveListener(message, conn)
        }
    }
}

1. 初始化和事件注册

NewServer(network, address string) 用来保存网络类型和监听地址,比如示例中的 "tcp" 和 "127.0.0.1:6000"。随后通过 OnConnect()、OnReceive()、OnClose() 把事件处理函数挂上去。

这种写法的好处是简单直接,适合把“网络层”和“业务层”拆开。服务端本身只负责接连接、读消息、发消息,至于收到数据后怎么处理,则交给回调完成。

2. Listen() 的工作流程

Listen() 内部先调用 net.Listen(s.network, s.address) 开始监听,然后进入一个无限循环,不断执行 Accept()。

客户端一次收发与长连接接收两种模式对比图
客户端两种收消息方式把客户端的请求-响应模式与持续接收模式放在同一张图里,帮助读者快速区分使用场景。

每当有新的客户端接入时,代码会先触发一次 connectListener,然后用一个新的 goroutine 执行 s.accept(conn)。这意味着每个连接都可以独立处理,不会因为某一个客户端的读写阻塞整个服务端。

3. accept() 如何持续接收消息

accept(conn net.Conn) 是单个连接的读取循环。这里通过 bufio.NewReader(conn) 创建读取器,再反复调用 ReadBytes('n') 读取消息。

示例代码中需要留意的实现细节提示图
落地前的实现细节检查集中标出这份封装里最容易被忽略的几个实现细节,适合作为读者落地前的检查清单。

按这段代码的意图,它是希望以某个结束符来切分消息,读到结束符后就把完整消息交给 receiveListener 处理。收到消息后,回调会被放进 goroutine 中异步执行:

  • 连接不断开,循环就会一直读。
  • 只要读到一条完整消息,就触发一次 OnReceive 绑定的逻辑。
  • 如果读到 io.EOF,说明对端关闭连接,此时调用 s.Close(conn)。

这里有两个实现细节值得特别注意。第一,代码里使用的是 ReadBytes('n'),示例发送的数据也用了 "okn"、"啊哈哈n",从源码字面看它是以字符 n 作为消息结束标记,而不是换行符 '\n'。如果你原本想按换行分包,这里需要自己调整。

第二,Close() 方法里判断的是 s.closeListener != nil,但真正调用的却是 s.connectListener(conn)。按命名和语义看,这里更像是一个回调调用写错了:关闭连接时本应触发 closeListener,而不是再次触发连接建立回调。作为演示代码问题不大,但如果直接用于项目,最好先修正。

客户端怎么实现:建立连接、发送消息和持续接收

客户端对应的是另一套相对轻量的封装,重点放在主动连接服务端,以及选择“一次收一条”还是“保持长连接持续接收”。

// client.go

package socketer

import (
    "bufio"
    "net"
)

type Client struct {
    Conn net.Conn

    address string
    network string

    closeListener   CloseListener
    receiveListener ReceiveListener
    connectListener ConnectListener
}

func NewClient(network, address string) (*Client, error) {
    var err error
    client := &Client{
        address: address,
        network: network,
    }
    err = client.dial()
    return client, err
}

func (c *Client) dial() error {
    var err error
    c.Conn, err = net.Dial(c.network, c.address)
    if c.connectListener != nil {
        c.connectListener(c.Conn)
    }
    return err
}

func (c *Client) Send(data []byte) (int, error) {
    return c.Conn.Write(data)
}

func (c *Client) OnConnect(l ConnectListener) {
    c.connectListener = l
}

func (c *Client) OnReceive(l ReceiveListener) {
    c.receiveListener = l
}

func (c *Client) OnClose(l CloseListener) {
    c.closeListener = l
}

func (c *Client) Close() error {
    err := c.Conn.Close()
    if c.closeListener != nil {
        c.closeListener(c.Conn)
    }
    return err
}

func (c *Client) Recv() ([]byte, error) {
    reader := bufio.NewReader(c.Conn)

    return reader.ReadBytes('n')
}

func (c *Client) Open() {
    reader := bufio.NewReader(c.Conn)
    defer c.Conn.Close()

    for {
        message, err := reader.ReadBytes('n')
        if err != nil {
            break
        }
        if c.receiveListener != nil {
            go c.receiveListener(message, c.Conn)
        }
    }
}

1. NewClient() 建立连接

NewClient(network, address string) 会先创建一个 Client 实例,再立即调用内部的 dial()。而 dial() 通过 net.Dial(c.network, c.address) 去连接服务端。

这意味着客户端对象一创建,就会尝试连到目标地址;如果连接失败,错误会直接从 NewClient() 返回。

2. Send() 和 Recv() 适合一次请求一次响应

Send(data []byte) 很直接,就是调用 c.Conn.Write(data)。Recv() 则创建一个 reader,然后读取一条以 'n' 结尾的消息:

func (c *Client) Recv() ([]byte, error) {
    reader := bufio.NewReader(c.Conn)

    return reader.ReadBytes('n')
}

如果你的通信模式比较简单,比如客户端发一条、服务端回一条,那么 Send() 配合 Recv() 就够用了。它的特点是调用方自己掌握节奏,适合请求-响应式的交互。

3. Open() 适合保持长连接

如果你不想每次都手动调用 Recv(),而是希望客户端在连接建立后一直监听服务端推送的数据,就可以使用 Open()。

这个方法内部同样是一个读取循环,不断执行 ReadBytes('n')。每读到一条消息,就异步触发一次 receiveListener。这样一来,客户端既能主动 Send(),也能被动持续接收服务端发来的内容。

不过要注意一个顺序问题:示例中 NewClient() 创建客户端时就已经调用了 dial(),而 OnConnect() 往往是在对象创建后才注册。也就是说,如果你在 NewClient() 之后再设置 OnConnect(),那这次初始连接建立事件其实已经错过了。这也是这份简化封装在实际使用时需要考虑的地方。

把服务端和客户端串起来:最基本的用法示例

理解了两边的结构后,再看示例的实际调用方式会更清楚。

服务端先注册消息回调并启动监听

serv := NewServer("tcp", "127.0.0.1:6000")

// 消息回调
serv.OnReceive(func(data []byte, conn net.Conn) {
    fmt.Println("Server Received: ", string(data))
    serv.Send(conn, []byte("okn"))
})

// 开始监听
serv.Listen()

这段服务端代码完成了两件事:

  • 监听地址 127.0.0.1:6000,网络类型是 tcp。
  • 每当收到客户端消息时,先打印内容,再回写一条 "okn"。

因为 Listen() 会持续阻塞等待连接,所以它通常就是服务端主流程的入口。

客户端连接后发送消息,再读取返回结果

client, err := NewClient("tcp", "127.0.0.1:6000")
if err != nil {
    panic(err)
}

defer client.Close()

// 消息回调
client.OnReceive(func(data []byte, conn net.Conn) {
    fmt.Println("Client Received: ", string(data))
})

// 发送消息
client.Send([]byte("啊哈哈n"))

data, err := client.Recv()
if err != nil {
    panic(err)
}

fmt.Println("Client Received: ", string(data))

// 如果想保持长连接,持续接收发送消息
client.Open()

客户端先调用 NewClient("tcp", "127.0.0.1:6000") 建立连接,失败就直接 panic(err)。连接成功后,先通过 defer client.Close() 保证退出前关闭连接。

接下来它发送一条 "啊哈哈n",随后调用 Recv() 等待服务端响应,并打印返回结果。按服务端示例逻辑,这里应该能读到 "okn"。

最后的 client.Open() 表示另一种运行模式:如果你不满足于“一发一收”这一轮交互,而是希望连接保持着、后续持续收发消息,就可以进入长连接读取循环。

这份示例适合怎么用,落地前要注意什么

作为入门示例,这套代码的价值很明确:它把 Go 里 TCP Socket 最核心的几步都摆出来了,包括监听、连接、读取、回调、发送,以及如何用 goroutine 让每个连接独立运行。对于理解客户端和服务端的交互关系,这样的拆分已经足够直观。

但如果准备继续往真实项目里用,至少要先关注下面几件事:

  • 消息分隔符目前写成了 'n',对应发送内容里的 "okn"、"啊哈哈n"。如果你的协议不是这样设计的,就需要修改分包方式。
  • 服务端 Close() 中触发的是 connectListener,从语义上看应当检查是否应改为 closeListener。
  • 客户端的 OnConnect() 如果在 NewClient() 之后再注册,初始连接回调可能不会被触发。
  • Recv() 和 Open() 都依赖同样的读取规则,使用时要避免和你的协议设计冲突。

如果只是做本地实验、学习 Go Socket 双向通信的基本流程,这份代码已经能跑通一个最小闭环:服务端监听,客户端连接,双方都能发送和接收消息。若要进一步扩展到更稳定的网络程序,再沿着错误处理、协议边界和回调时机这几个方向完善即可。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。