TCP是全球所有数据传输的协议,例如HTTP和Websocket都通过TCP运行。即使是最常用的数据库,如 Mongo、Redis 或 Postgres,也使用 TCP 来运行其协议。
因此,编写自定义TCP应用程序只是创建一个TCP的超级协议。TCP 应用程序协议。
多亏了 golang,一半的工作就完成了,因为有一个用于此目的的原生包:“net”包。
首先,让我们看一下它在使用原始 TCP 时的一些注意事项。
- 客户端管理。
- 消息缓冲区管理。
- 应用程序自定义协议。
- 来自客户端的服务器连接。
由于 TCP 仅提供用于传输数据的协议,因此该日期的获取和解释是应用程序的工作。这就是存在这些考虑的原因。
Server
基本步骤是创建一个客户端可以连接到的服务器。如前所述,包网络具有所需的所有工具。
代码语言:go复制package server
func Listen(p int) error {
srv, err := net.Listen("tcp", "127.0.0.1:%d", p)
if err != nil {
return err
}
defer srv.Close()
return nil
}
函数“Listen”将侦听 127.0.0.1 IP(本地主机)上的端口。当函数结束时,松开获取的端口。
现在怎么样,服务器不能接受客户端,所以让我们编写代码。
代码语言:go复制package server
func Listen(p int) error {
srv, err := net.Listen("tcp", "127.0.0.1:%d", p)
if err != nil {
return err
}
defer srv.Close()
// infinity loop
for {
cli, err := srv.Accept()
if err != nil {
return err
}
}
return nil
}
太好了,现在服务器可以接受客户端了。Accept 方法阻止循环,直到新客户端执行连接。
Accept 函数内部发生的情况是 TCP 握手。这包括三个步骤。
- 客户端向服务器发送 SYN。
- 服务器通过使用 SYN-ACK 响应客户端来接受该 SYN。
- 客户端使用 ACK 进行响应。
完成这三个步骤后,即可建立连接。
SYN 和 ACK 是构成 TCP 本身一部分的 TCP 标志,其中 SYN menas Synchronize 和 ACK 表示 Acknowledged。
关闭连接
服务器目前只接受客户端,但从不对它们做任何事情。重要的是要知道关闭连接的责任在服务器上。这意味着如果服务器建立连接,则在使用后必须关闭它。
对于每个新的客户端连接,将执行一个处理客户端的 goroutine。
代码语言:go复制package server
func Listen(p int) error {
srv, err := net.Listen("tcp", "127.0.0.1:%d", p)
if err != nil {
return err
}
defer srv.Close()
// infinity loop
for {
cli, err := srv.Accept()
if err != nil {
return err
}
go handle(cli)
}
return nil
}
func handle(cli net.Conn) {
defer cli.Close()
}
Conn 是 net package 中的一个接口,它提供了操作该特定连接所需的方法。
读取消息问题
当与客户端建立连接时。客户端和服务器都可以共享数据。但是这里出现了很大的启动问题。如何阅读消息?
这是一个问题,因为消息长度未知。通常在 TCP 中,消息缓冲区由“n”个字节块读取,直到没有更多字节可供读取。
另一种解决方案是将消息长度作为消息元数据的一部分发送。例如,在 HTTP 中,此长度在 Header 中发送。
这种机制是应用程序协议的一部分,而不是TCP本身。
自定义应用程序协议
自定义应用程序的协议只是客户端和服务器相互理解的一组规则。
此应用程序要遵循的规则是。
- TCP 有效负载必须包含以下部分:长度消息和正文消息。
- 长度消息是 TCP 有效负载的前 2 个字节。
- 正文消息具有 JSON 格式。
- 处理消息后,连接将关闭。
第二条规则确定正文消息的最大长度,即 2 个字节的整数。从 0x0000 到 0xFFFF,以十进制为基数:0 到 65535 字节。2 个字节 int 是 int16。
此应用程序的消息将如下所示。
代码语言:shell复制LEN | BODY
0019 7B226D657373616765223A2268656C6C6F20776F726C64227D
LEN | BODY
25 {"message":"hello world"}
对协议进行编码
好吧,让我们对这个称为 JSONP(JSON 协议)的协议的规则进行编码
代码语言:go复制package jsonp
func ReadMessage(c net.Conn) []byte {
l := make([]byte, 2) // takes the first two bytes: the Length Message.
i, _ := c.Read(l) // read the bytes.
lm := binary.BigEndian.Uint16l(l[:i]) // convert the bytes into int16.
b := make([]byte, lm) // create the byte buffer for the body.
e, _ := c.Read(b) // read the bytes.
return b[:e] // returns the body
}
上面的函数有消息读取规则。一个导入的东西,提到长度消息字节是如何存储的。在这种情况下,以及在大多数情况下,对于 TCP,存储的字节具有 BigEndian 格式。
BigEndian 格式存储从右到左的字节。另一方面,LittleEndian 恰恰相反。
代码语言:shell复制len = {00, 01}
BigEndian(len) => 0001 => 1: int16
LittleEndian(len) => 0100 => 256: int16
代码语言:go复制package jsonp
func WriteMessage(m []byte, c net.Conn) {
l := make([]byte, 2) // creates the Length Message buffer.
binary.BigEndian.PutUint16(l, uint16(len(m))) // Converts len to bytes.
c.Write(append(l, m...)) // send the message
}
目前为止,一切都好。现在让我们创建一些 json marshal/unmarshal 函数,以便从该逻辑中抽象出来。
代码语言:go复制package jsonp
func ReadJSON(c net.Conn, a any) error {
msg := ReadMessage(c)
return json.Unmarshal(msg, a)
}
func SendJSON(c net.Conn, a any) error {
msg, err := json.Marshal(a)
if err != nil {
return err
}
WriteMessage(c, msg)
return nil
}
处理客户端
服务器的最后一部分需要对原始的 Listen 函数进行一些修改。
为协议添加一个类型别名。
代码语言:go复制package jsonp
type Handler func(c net.Conn)
此处理程序是发送和接收消息的上下文/回调。
在服务端中实现处理程序。
代码语言:go复制package server
func Listen(p int, h jsonp.Handler) error {
srv, err := net.Listen("tcp", "127.0.0.1:%d", p)
if err != nil {
return err
}
defer srv.Close()
// infinity loop
for {
cli, err := srv.Accept()
if err != nil {
return err
}
go func(c net.Conn, hd jsonp.Handler) {
defer c.Close()
hd(c)
}(cli, h)
}
return nil
}
通过此修改,应用程序拥有客户端处理,而不是服务器。最后一个仅提供使用客户端的安全上下文(通过在使用后关闭连接)。
客户端
服务端已经完成,让我们继续客户端。这是一个简单的部分,因为网络包对TCP套接字的两端都使用相同的接口。因此,工作的一半已经完成。
与服务器的连接可以通过网络完成。拨号功能。但是,由于连接仅在发送一条消息时处于活动状态,之后连接将关闭,因此服务器连接本身就是消息发送者。
代码语言:go复制package client
func Connect(p int, h jsonp.Handler) error {
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", p))
if err != nil {
return err
}
defer conn.Close()
h(conn)
return nil
}
客户端将发送“Hello World”消息。
代码语言:go复制package main
type Message struct {
Content string `json:"content"`
}
func main() {
client.Connect(8080, func(c net.Conn) {
jsonp.SendJSON(c, &Message{Content:"Hello World"})
var res Message
jsonp.ReadJSON(c, &res)
fmt.Println(res.Content)
})
}
服务器将执行与客户端相同的操作,但首先读取消息,然后发送“Hello World”作为响应。
代码语言:go复制package main
type Message struct {
Content string `json:"content"`
}
func main() {
server.Listen(8080, func(c net.Conn) {
var res Message
jsonp.ReadJSON(c, &res)
fmt.Println(res.Content)
jsonp.SendJSON(c, &Message{Content:"Hello World"})
})
}
本文不是要创建一个比现有协议更好的新协议,而是旨在了解在基于 TCP 的协议的引擎盖下会发生什么。
例如,使用本文中显示的概念,可以复制 HTTP 。
此外,为了避免每次发送消息时都连接和重新连接(就像 HTTP 一样),连接可以保持活动状态一段时间,直到客户端发送某种命令来关闭套接字。
我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!