Golang 网络编程

TCP

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议

如何保证连接的可靠性?

  1. 三次握手
  2. 四次挥手
三次握手

TCP 三次握手(Three-way Handshake)是TCP/IP 协议用来在两个网络端点之间建立一个连接的过程。它涉及到发送者和接收者的三个步骤,确保两端都准备好接收和发送数据

以下是三次握手的步骤:

  1. SYN
    客户端发送一个带有 SYN(同步序列编号)标志的TCP段到服务器,以初始化一个连接。
    此时,客户端进入 SYN-SENT 状态
  2. SYN-ACK
    服务器接收到客户端的SYN请求后,必须确认客户的SYN(ACK),同时自己也发送一个SYN请求,即SYN+ACK
    此时,服务器进入 SYN-RECEIVED 状态
  3. ACK
    客户端接收到服务器的SYN+ACK后,发送一个确认包ACK给服务器。该包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,连接建立
    如果ACK没有在预定的时间内到达服务器,服务器通常会重试发送SYN-ACK,并继续等待客户端的ACK。这会重复几次,根据TCP实现的具体细节和配置而有所不同。如果在几次重试后仍没有收到ACK,服务器最终会超时并放弃这个连接尝试,连接建立失败

Go中,我们不直接操作这些底层细节,因为net包抽象了这些实现细节。当我们使用net.Dial去连接服务器,或者服务器使用Accept接收一个连接时,三次握手都在底层自动完成了

四次挥手

TCP 四次挥手(Four-way Handshake)是TCP/IP 协议用来在两个网络端点之间终止一个连接的过程。它比建立连接时的三次握手要多一步,因为TCP连接是全双工的,所以每个方向必须单独进行终止。这个过程确保了数据完全传输且双方都同意关闭连接

步骤:

  1. FIN
    客户端决定数据发送完毕之后,它需要关闭连接。它发送一个带有 FIN 标志的TCP段到服务器,表示没有更多的数据传输。
    此时,客户端进入 FIN-WAIT-1 状态
  2. ACK
    服务器接收到这个 FIN 段后,发送一个 ACK 确认,告诉客户端它的 FIN 已经收到。
    服务器进入 CLOSE-WAIT 状态。客户端在收到 ACK 后进入 FIN-WAIT-2 状态
  3. FIN (from server)
    一段时间后,服务器准备好关闭连接时,它发送一个带有 FIN 标志的TCP段回给客户端。
    服务器进入 LAST-ACK 状态
  4. ACK
    客户端收到服务器的 FIN 后,发送一个 ACK 确认,然后进入 TIME-WAIT 状态。客户端会在 TIME-WAIT 状态等待足够的时间以确保服务器收到它的 ACK
    服务器在收到客户端的 ACK 后,关闭连接,进入 CLOSED 状态。客户端在等待一段时间后也会关闭连接,进入 CLOSED 状态

这个过程确保了双方都不会丢失任何在网络中延迟的最后数据。Go中,这个过程通常是由 net 包的 Close 方法来触发的。当你调用 Close 方法时,底层的 TCP 套接字会开始四次挥手过程,但这个过程对于开发者来说是透明的

Golang 创建TCP服务器与客户端

创建服务端:

  1. 监听一个端口
  2. 接受连接
  3. 读取数据
  4. 写入响应
  5. 关闭连接
go 复制代码
package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

func main() {
    // 监听端口
    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer ln.Close()

    // 循环接受连接
    for {
        conn, err := ln.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }

        // 在goroutine中处理连接
        go handleConnection(conn)
    }
}

// 处理连接的函数
func handleConnection(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    for {
        // 读取数据
        message, err := reader.ReadString('\n')
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Print("Message Received:", message)

        // 写入响应
        conn.Write([]byte("Hello, Client!\n"))
    }
}

创建客户端:

  1. 连接到服务器
  2. 发送数据
  3. 读取响应
  4. 关闭连接
go 复制代码
package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

func main() {
    // 连接到服务器
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()

    // 发送数据
    fmt.Fprintf(conn, "Hello, Server!\n")

    // 读取响应
    message, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Print("Message from server:", message)
}

服务器监听8080端口,并在接受到连接后,在单独的goroutine中处理它。服务器读取来自客户端的消息,然后发送一个简单的响应。客户端连接到服务器,发送一个消息,并等待响应

net.Listennet.ListenTCP

net.Listennet.ListenTCP都是 Go 语言标准库 net 包中的函数,用于监听网络端口,但它们的用途和返回的类型有所不同
net.Listen 函数用于创建一个通用的网络监听器,它可以监听TCPUDPUnix sockets等多种类型的网络协议。该函数返回一个 net.Listener 接口,它抽象了网络监听操作的细节。使用 net.Listener 接口可以接受新的连接

go 复制代码
ln, err := net.Listen("tcp", "localhost:8080")

第一个参数是网络类型,第二个参数是地址和端口

net.ListenTCP 函数是一个更具体的函数,它只用于TCP网络协议。它返回一个 *net.TCPListener 类型的对象,这个对象提供了一些TCP特定的方法,比如 SetDeadlineSetKeepAlive

go 复制代码
addr, _ := net.ResolveTCPAddr("tcp", "localhost:8080")
ln, err := net.ListenTCP("tcp", addr)

这里需要先通过 net.ResolveTCPAddr 解析地址,然后传给 net.ListenTCP

使用 net.Listen 可以监听各种类型的网络协议,非常灵活。而 net.ListenTCP 则是专门用于TCP协议,提供了一些TCP特有的控制方法。根据具体需求选择使用哪一个

如果你只需要创建一个简单的TCP服务器,而不需要使用TCP协议的高级特性,net.Listen 就足够了。如果你需要对TCP连接进行更精细的控制,比如设置keep-alive参数或者设置连接的deadline,那么应该使用 net.ListenTCP

HTTP

可以使用net/http标准库来创建HTTP客户端和服务器

服务端

go 复制代码
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
    })

    fmt.Println("Server is running at http://localhost:8080/")
    http.ListenAndServe(":8080", nil)
}

客户端

go 复制代码
package main

import (
    "io"
    "log"
    "net/http"
)

func main() {
    resp, err := http.Get("http://localhost:8080/")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(string(body))
}

服务器端代码将在8080端口启动一个HTTP服务器,并对所有到达根路径"/"的请求作出响应。客户端代码将向该服务器发送一个请求,并打印出响应的内容

RPC

RPC(Remote Procedure Call,RPC)允许客户端像调用本地函数一样调用远程服务器上的函数。Go的net/rpc包支持通过TCP或HTTP进行通信

服务端

go 复制代码
package main

import (
	"log"
	"net"
	"net/rpc"
)

// Args 定义了RPC函数的参数
type Args struct {
	A, B int
}

// Arith 定义了RPC服务的结构体
type Arith int

// Multiply 是一个RPC服务方法,它将Args中的A和B相乘,并返回结果
func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func main() {
	arith := new(Arith)
	rpc.Register(arith)
	
	// 在TCP上监听
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("Listen error:", err)
	}
	log.Printf("Serving RPC server on port %d", 1234)

	// 接受连接请求
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
		}
		go rpc.ServeConn(conn)
	}
}

客户端

go 复制代码
package main

import (
	"log"
	"net/rpc"
)

// Args 和服务端定义的结构相同
type Args struct {
	A, B int
}

func main() {
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("Dialing:", err)
	}

	// Synchronous call
	args := &Args{A: 7, B: 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("Arith error:", err)
	}
	log.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
}

服务端注册了一个Arith服务,包含了一个Multiply方法。客户端通过创建一个rpc.Client对象,然后调用Call方法发起同步的RPC调用。Call方法的第一个参数是要调用的服务和方法的名称,格式是"服务名.方法名"。后面的参数分别是RPC方法的输入参数和输出参数

如果你打算在生产环境中使用RPC,可能需要考虑使用更现代的RPC框架,如gRPC,它提供更多的功能,包括支持Protocol Buffers和流式传输

WebSocket

gorilla/websocket是一个流行的库,用于处理WebSocket连接。WebSocket协议允许建立持久的全双工通信,这意味着服务器和客户端可以随时发送消息,而不需要建立多个HTTP连接

服务端

首先,你需要安装gorilla/websocket包:

go 复制代码
go get github.com/gorilla/websocket
go 复制代码
package main

import (
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // 不检查来源
    },
}

func echo(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer conn.Close()

    for {
        mt, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = conn.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/echo", echo)
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

客户端:可以用JavaScript编写

go 复制代码
const socket = new WebSocket('ws://localhost:8080/echo');

socket.onopen = function(e) {
  console.log("Connection established!");
  socket.send("Hello Server!");
};

socket.onmessage = function(event) {
  console.log(`Data received from server: ${event.data}`);
};

socket.onclose = function(event) {
  if (event.wasClean) {
    console.log(`Connection closed cleanly, code=${event.code}, reason=${event.reason}`);
  } else {
    console.error('Connection died');
  }
};

socket.onerror = function(error) {
  console.error(`[Error] ${error.message}`);
};

这段JavaScript代码创建了一个WebSocket客户端,连接到ws://localhost:8080/echo。客户端发送消息到服务器,然后服务器将相同的消息回传回来。客户端也处理打开、接收消息、关闭和错误事件

相关推荐
张声录15 小时前
使用client-go在命令空间test里面对pod进行操作
开发语言·后端·golang
fcopy10 小时前
Golang项目:实现生产者消费者模式
缓存·golang
桃园码工11 小时前
第一章:Go 语言概述 1.什么是 Go 语言? --Go 语言轻松入门
开发语言·后端·golang
桃园码工12 小时前
第一章:Go 语言概述 2.安装和配置 Go 开发环境 --Go 语言轻松入门
开发语言·后端·golang
fcopy12 小时前
Golang项目:实现一个内存缓存系统
缓存·golang
hummhumm13 小时前
第 36 章 - Go语言 服务网格
java·运维·前端·后端·python·golang·java-ee
凡人的AI工具箱13 小时前
40分钟学 Go 语言高并发:Pipeline模式(一)
开发语言·后端·缓存·架构·golang
LightOfNight17 小时前
【设计模式】创建型模式之单例模式(饿汉式 懒汉式 Golang实现)
单例模式·设计模式·golang
MelonTe1 天前
Golang网络模型netpoll源码解析
golang
Clown951 天前
go-zero(十) 数据缓存和Redis使用
redis·缓存·golang