TCP
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议
如何保证连接的可靠性?
- 三次握手
- 四次挥手
三次握手
TCP 三次握手(Three-way Handshake)是TCP/IP 协议用来在两个网络端点之间建立一个连接的过程。它涉及到发送者和接收者的三个步骤,确保两端都准备好接收和发送数据
以下是三次握手的步骤:
SYN
客户端发送一个带有SYN
(同步序列编号)标志的TCP
段到服务器,以初始化一个连接。
此时,客户端进入SYN-SENT
状态SYN-ACK
服务器接收到客户端的SYN
请求后,必须确认客户的SYN(ACK)
,同时自己也发送一个SYN
请求,即SYN+ACK
。
此时,服务器进入SYN-RECEIVED
状态ACK
客户端接收到服务器的SYN+ACK
后,发送一个确认包ACK
给服务器。该包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手,连接建立
如果ACK
没有在预定的时间内到达服务器,服务器通常会重试发送SYN-ACK
,并继续等待客户端的ACK
。这会重复几次,根据TCP
实现的具体细节和配置而有所不同。如果在几次重试后仍没有收到ACK
,服务器最终会超时并放弃这个连接尝试,连接建立失败
Go中,我们不直接操作这些底层细节,因为net
包抽象了这些实现细节。当我们使用net.Dial
去连接服务器,或者服务器使用Accept
接收一个连接时,三次握手都在底层自动完成了
四次挥手
TCP 四次挥手(Four-way Handshake)是TCP/IP 协议用来在两个网络端点之间终止一个连接的过程。它比建立连接时的三次握手要多一步,因为TCP连接是全双工的,所以每个方向必须单独进行终止。这个过程确保了数据完全传输且双方都同意关闭连接
步骤:
FIN
客户端决定数据发送完毕之后,它需要关闭连接。它发送一个带有FIN
标志的TCP
段到服务器,表示没有更多的数据传输。
此时,客户端进入FIN-WAIT-1
状态ACK
服务器接收到这个FIN
段后,发送一个ACK
确认,告诉客户端它的FIN
已经收到。
服务器进入CLOSE-WAIT
状态。客户端在收到ACK
后进入FIN-WAIT-2
状态FIN (from server)
一段时间后,服务器准备好关闭连接时,它发送一个带有FIN
标志的TCP
段回给客户端。
服务器进入LAST-ACK
状态ACK
客户端收到服务器的FIN
后,发送一个ACK
确认,然后进入TIME-WAIT
状态。客户端会在TIME-WAIT
状态等待足够的时间以确保服务器收到它的ACK
。
服务器在收到客户端的ACK
后,关闭连接,进入CLOSED
状态。客户端在等待一段时间后也会关闭连接,进入CLOSED
状态
这个过程确保了双方都不会丢失任何在网络中延迟的最后数据。Go中,这个过程通常是由 net
包的 Close
方法来触发的。当你调用 Close
方法时,底层的 TCP 套接字会开始四次挥手过程,但这个过程对于开发者来说是透明的
Golang 创建TCP服务器与客户端
创建服务端:
- 监听一个端口
- 接受连接
- 读取数据
- 写入响应
- 关闭连接
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"))
}
}
创建客户端:
- 连接到服务器
- 发送数据
- 读取响应
- 关闭连接
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.Listen
和 net.ListenTCP
net.Listen
和 net.ListenTCP
都是 Go 语言标准库 net
包中的函数,用于监听网络端口,但它们的用途和返回的类型有所不同
net.Listen
函数用于创建一个通用的网络监听器,它可以监听TCP
、UDP
、Unix sockets
等多种类型的网络协议。该函数返回一个 net.Listener
接口,它抽象了网络监听操作的细节。使用 net.Listener 接口可以接受新的连接
go
ln, err := net.Listen("tcp", "localhost:8080")
第一个参数是网络类型,第二个参数是地址和端口
net.ListenTCP
函数是一个更具体的函数,它只用于TCP网络协议。它返回一个 *net.TCPListener
类型的对象,这个对象提供了一些TCP特定的方法,比如 SetDeadline
、SetKeepAlive
等
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。客户端发送消息到服务器,然后服务器将相同的消息回传回来。客户端也处理打开、接收消息、关闭和错误事件