最简单的多线程 tcp 服务器

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
	// 处理连接
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil {
		fmt.Println("read error:", err)
		conn.Close()
		return
	}

	// 输出客户端发来的消息
	fmt.Printf("received message: %s\n", buf[:n])

	// 关闭连接
	conn.Close()
}

func main() {
	// 创建监听器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	// 服务循环
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			continue
		}

		// 启动 goroutine 处理连接
		go handleConn(conn)
	}
}

在这个示例中,我们使用 net.Listen() 函数创建了一个 TCP 监听器,并在端口 8080 上监听客户端连接。然后,我们进入一个服务循环,不断接收客户端连接,并使用 go handleConn(conn) 启动一个 goroutine 来处理连接。

在 handleConn() 函数中,我们首先读取客户端发来的消息,并输出到终端上。然后,我们使用 conn.Close() 关闭连接。

由于每个客户端连接都会启动一个新的 goroutine,因此该例子可以同时服务多个客户端连接,而且每个连接之间是相互独立的。

总之,使用 goroutine 来服务客户端连接可以让我们编写高效的服务器程序,并且可以轻松地扩展处理连接的能力。

模拟时间 ntp 校准服务器

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
	// 获取当前时间
	t := time.Now()

	// 将时间写入连接
	fmt.Fprintln(conn, t.Format(time.RFC3339))

	// 关闭连接
	conn.Close()
}

func main() {
	// 监听端口
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	// 接受连接并处理
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("接收连接出错:", err)
			continue
		}

		// 使用 goroutine 处理连接
		go handleConn(conn)
	}
}

在 handleConn() 函数中,我们使用 time.Now() 函数获取当前时间,然后使用 fmt.Fprintln() 将时间以 time.RFC3339 的格式写入连接中。最后,我们使用 conn.Close() 关闭连接。

在 main() 函数中,我们使用 net.Listen() 函数监听端口,然后使用 listener.Accept() 接受客户端连接,并使用 go handleConn(conn) 在一个新的 goroutine 中处理每个连接。

和客户端保持链接并实现心跳

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
	defer conn.Close()
	fmt.Println("New client connected")

	// 创建定时器,每隔 5 秒向客户端发送数据
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			// 向客户端发送数据
			_, err := conn.Write([]byte("heartbeat"))
			if err != nil {
				fmt.Println("Write error:", err)
				return
			}
		default:
			// 读取客户端发送的数据
			data := make([]byte, 1024)
			_, err := conn.Read(data)
			if err != nil {
				fmt.Println("Read error:", err)
				return
			}
			fmt.Printf("Received data: %s\n", string(data))
		}
	}
}

func main() {
	// 监听端口
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	// 接受连接并处理
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Accept error:", err)
			continue
		}

		// 使用 goroutine 处理连接
		go handleConn(conn)
	}
}

在这个示例中,我们在 handleConn() 函数中创建了一个定时器 ticker,它每隔 5 秒会向客户端发送数据 "heartbeat"。同时,在函数 handleConn() 的主循环中,我们使用 default 分支读取客户端发送的数据。如果客户端没有发送数据,default 分支会被执行,如果客户端发送了数据,则 default 分支会被阻塞。

需要注意的是,我们在处理完一个连接后,使用 defer conn.Close() 关闭连接。这可以确保在退出该函数之前,连接会被正确关闭。

判断客户端主动关闭链接

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
	defer conn.Close()

	// 读取客户端发送的数据
	data := make([]byte, 1024)
	n, err := conn.Read(data)
	if err != nil {
		if err == io.EOF {
			fmt.Println("client closed connection")
			return
		}

		fmt.Println("read error:", err)
		return
	}

	// 处理数据
	fmt.Printf("received data: %s\n", string(data[:n]))
}

在这个示例中,我们使用 conn.Read() 从连接中读取数据。如果读取成功,我们会处理读取到的数据。如果读取过程中出现错误,我们检查错误是否为 io.EOF。如果是,那么我们就会显示提示信息并终止连接;否则,我们会输出错误信息并终止连接。

在处理连接时,需要使用 defer conn.Close() 来确保连接在函数退出时被正确关闭。

总之,当 conn.Read() 返回 io.EOF 错误时,可以将其视为客户端主动关闭连接,应该在程序中进行处理以确保程序正常运行,并防止程序出现错误。

设置读取客户端连接超时的时间

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
	defer conn.Close()

	// 设置连接的读取超时为 10 秒
	conn.SetReadDeadline(time.Now().Add(10 * time.Second))

	// 读取客户端发送的数据
	data := make([]byte, 1024)
	n, err := conn.Read(data)
	if err != nil {
		if err, ok := err.(net.Error); ok && err.Timeout() {
			fmt.Println("read timeout")
		} else {
			fmt.Println("read error:", err)
		}
		return
	}

	// 处理数据
	fmt.Printf("received data: %s\n", string(data[:n]))
}

在这个示例中,我们首先调用 conn.SetReadDeadline() 方法设置连接的读取超时为 10 秒。如果在读取数据时超过这个时间,conn.Read() 方法将返回一个 i/o timeout 错误。我们检查错误类型,如果它是一个超时错误,我们会输出 "read timeout" 提示信息。

如果客户端在超时时间内发送了数据,我们读取数据并处理。如果读取过程中发生错误,我们将输出错误信息并终止连接。

总之,使用 conn.SetReadDeadline() 方法可以确保连接在超时时间内读取数据,从而提高程序的可用性和稳定性。

利用 head(size)+body 结构处理粘包

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn net.Conn) {
    defer conn.Close()

    // 循环读取客户端发送的数据
    reader := bufio.NewReader(conn)
    for {
        // 读取长度前缀
        lengthBytes, err := reader.Peek(4)
        if err != nil {
            fmt.Println("read length error:", err)
            return
        }
        length := binary.BigEndian.Uint32(lengthBytes)

        // 读取数据
        data := make([]byte, int(length)+4)
        _, err = io.ReadFull(reader, data)
        if err != nil {
            fmt.Println("read data error:", err)
            return
        }

        // 处理数据
        content := string(data[4:])
        fmt.Printf("received data: %s\n", content)
    }
}

在这个示例中,我们使用 bufio.NewReader() 构建一个带缓存的读取器,然后循环从连接中读取数据。在每次读取之前,我们使用 reader.Peek() 方法读取长度前缀,并使用 binary.BigEndian.Uint32() 方法将长度前缀转换为一个 uint32 类型的整数。然后我们读取该长度的数据。这样,我们可以确保不会读取到下一个数据包的数据。

在读取数据时,我们将整个数据包(包括长度前缀在内)读入到 data 变量中,并将 data 变量传递给处理函数。在处理函数中,我们使用 string(data[4:]) 将数据转换为字符串,并输出到控制台上。

总之,通过添加长度前缀,我们可以正确地读取每个数据包,并避免读取数据粘包的问题。

创建无连接的 udp 服务器

在线运行open in new window启动 AI 助手open in new window

func main() {
	// 创建 UDP 监听器
	addr, err := net.ResolveUDPAddr("udp", ":8080")
	if err != nil {
		panic(err)
	}

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 读取客户端发送的数据
	for {
		buf := make([]byte, 1024)
		n, addr, err := conn.ReadFromUDP(buf)
		if err != nil {
			fmt.Println("read error:", err)
			continue
		}

		// 处理数据
		data := string(buf[:n])
		fmt.Printf("received data from %s: %s\n", addr.String(), data)
	}
}

在这个示例中,我们首先使用 net.ResolveUDPAddr() 函数创建了一个 UDPAddr 结构体,该结构体指定了服务器的 IP 地址和端口号。然后,我们调用 net.ListenUDP() 函数创建一个 UDP 监听器,并使用 defer 关键字将其关闭。在 UDP 监听器被关闭之前,它将持续监听客户端发送的数据。

在主循环中,我们使用 conn.ReadFromUDP() 方法从 UDP 监听器中读取数据。如果读取成功,我们将数据转换为字符串,并输出到控制台上。如果读取过程中发生错误,我们将输出错误信息并继续监听客户端发送的数据。

这个示例程序没有为每个UDP连接启动 goroutine。因为UDP是一个无连接的协议,因此每个收到的数据包都是独立的,而不需要为每个连接保持单独的状态。因此,我们只需要在主线程中循环读取数据,而不必为每个连接创建一个 goroutine。

总之,使用 Go 创建 UDP 服务器非常简单,只需要使用 net.ListenPacket() 函数来创建一个 UDP 监听套接字,并使用 p.ReadFrom() 方法从该套接字中读取数据。

使用缓冲区处理 udp 的大数据包

在线运行open in new window启动 AI 助手open in new window

func handleConn(conn *net.UDPConn) {
	// 创建缓冲区
	buf := make([]byte, 1024)

	// 读取客户端发送的数据
	for {
		n, addr, err := conn.ReadFromUDP(buf)
		if err != nil {
			fmt.Println("read error:", err)
			continue
		}

		// 处理数据
		data := string(buf[:n])
		fmt.Printf("received data from %s: %s\n", addr.String(), data)
	}
}

func main() {
	// 创建 UDP 监听器
	addr, err := net.ResolveUDPAddr("udp", ":8080")
	if err != nil {
		panic(err)
	}

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 处理连接
	handleConn(conn)
}

在这个示例中,我们在 handleConn() 函数中创建一个 1024 字节的缓冲区。在主循环中,我们使用 conn.ReadFromUDP() 方法读取数据,并将其存储在缓冲区中。然后,我们将缓冲区中的数据转换为一个字符串,并将其输出到控制台上。

通过使用缓冲区,我们可以确保每个数据包都能够完整地处理。需要注意的是,如果您的数据包非常大,可能需要使用更大的缓冲区来存储数据。同时,应该注意,每个连接只有一个缓冲区,而不是为每个数据包创建一个缓冲区。

链接 tcp 服务器的客户端

在线运行open in new window启动 AI 助手open in new window

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

	// 向服务器发送数据
	message := "Hello, server!"
	_, err = conn.Write([]byte(message))
	if err != nil {
		fmt.Println("write error:", err)
		return
	}

	// 从服务器接收数据
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}

	// 输出服务器发送的消息
	fmt.Printf("received message from server: %s\n", string(buf[:n]))
}

在这个示例中,我们使用 net.Dial() 函数连接到一个运行 TCP 服务器的地址 127.0.0.1:8080。然后,我们使用 conn.Write() 向服务器发送数据。如果写操作成功,我们将从服务器读取响应,并将响应作为字符串输出到终端上。

需要注意的是,在处理连接时,我们使用 defer conn.Close() 以确保在函数退出时将连接关闭。

链接 udp 服务器的客户端

在线运行open in new window启动 AI 助手open in new window

func main() {
	// 解析服务器地址
	serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
	if err != nil {
		panic(err)
	}

	// 建立 UDP 连接
	conn, err := net.DialUDP("udp", nil, serverAddr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// 发送数据
	message := "Hello, server!"
	_, err = conn.Write([]byte(message))
	if err != nil {
		fmt.Println("write error:", err)
		return
	}

	fmt.Println("message sent to server")
}

在这个示例中,我们首先使用 net.ResolveUDPAddr() 函数解析 UDP 服务器的地址,然后使用 net.DialUDP() 函数建立 UDP 连接。我们使用 conn.Write() 将数据发送给服务器。如果写操作成功,我们将输出 "message sent to server" 提示信息。

需要注意的是,在处理连接时,我们使用 defer conn.Close() 以确保在函数退出时将连接关闭。

Last Updated:
Contributors: Bob Wang