首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

构建高效且可靠的网络:Go语言中的TCP应用入门

近日见闻

Ant Design 5.16.0 发布,企业级 UI 设计语言和 React 实现. --ant design

OpenAI 宣布,将允许用户直接使用 ChatGPT,而无需注册该项服务,这将让人们更加容易体验人工智能的潜力。这是个好消息,大家都可以无门槛使用了。--openai

马斯克旗下的 AI 初创公司 xAI 宣布了其最新的生成式人工智能模型 Grok-1.5,上下文长度至 128K --Grok

摘抄:

有两种看待人生的方式,

一种是生活不存在奇迹,

另一种则是,

所有的一切都是奇迹。

——阿尔伯特·爱因斯坦

今天推荐一个很好的程序员备忘清单网站 https://cheatsheets.zip/,基本涵盖了所有开发必须的基础命令以及操作,中文版:https://cheatsheets.zip/zh-CN/是为开发人员分享快速参考备忘清单【速查表】。这是英文版 Reference 的中文版本,目的是为了方便自己的技术栈查阅。

使用Go实现基于TCP实时消息传送

之前在学习网络协议TCP的过程中,使用python实现了基于TCP协议的即时通信聊天应用,今天使用go语言实现,并再次复习一下客户端服务端交互的全流程。

为什么不用UDP?

虽然UDP在一些实时应用中确实有其优势(如视频会议、实时游戏等),因为它的延迟较低,但UDP是一个不可靠的协议。它不保证数据包的顺序,也不保证数据包的到达。在UDP中,如果网络出现问题导致数据包丢失,需要应用层来实现重传机制,这增加了开发的复杂性。此外,UDP也没有拥塞控制,网络状况不佳时可能会导致大量的丢包。

在聊天应用中,通常更倾向于使用TCP,因为消息的可靠传输比消息的实时到达更为重要。用户更愿意接受消息稍微有些延迟,也不希望出现消息丢失或乱序的情况。

以下为简单实现的代码

客户端:

// @Author  : Cillian

// @Email   : cilliandevops@gmail.com

// Website  : www.cillian.website

// Have a good day!

package main

import (

"bufio"

"fmt"

"net"

"os"

)

func main() {

// 连接到服务器

conn, err := net.Dial("tcp", "localhost:8080")

if err != nil {

panic(err)

}

defer conn.Close()

// 读取服务器发送的消息

go func() {

scanner := bufio.NewScanner(conn)

for scanner.Scan() {

msg := scanner.Text()

fmt.Println(msg)

}

}()

// 读取标准输入(键盘)的消息并发送到服务器

scanner := bufio.NewScanner(os.Stdin)

for scanner.Scan() {

msg := scanner.Text()

fmt.Println("请输入消息:")

fmt.Fprintf(conn, "%s\n", msg)

}

if err := scanner.Err(); err != nil {

fmt.Println("Error reading from stdin:", err)

}

}

服务端

// @Author  : Cillian

// @Email   : cilliandevops@gmail.com

// Website  : www.cillian.website

// Have a good day!

package main

import (

"bufio"

"fmt"

"net"

"sync"

)

var (

clients = make(map[net.Conn]struct{}) // 客户端集合

mu      sync.Mutex                    // 互斥锁,用于保护clients

)

func main() {

// 监听TCP端口

listener, err := net.Listen("tcp", ":8080")

if err != nil {

panic(err)

}

defer listener.Close()

fmt.Println("服务启动端口为 :8080")

for {

// 接受新的连接

conn, err := listener.Accept()

if err != nil {

fmt.Println("接受错误 ", err.Error())

continue

}

// 将新客户端添加到集合中

mu.Lock()

clients[conn] = struct{}{}

mu.Unlock()

// 处理客户端消息

go handleClient(conn)

}

}

func handleClient(conn net.Conn) {

defer func() {

// 移除客户端并关闭连接

mu.Lock()

delete(clients, conn)

mu.Unlock()

conn.Close()

}()

clientAddr := conn.RemoteAddr().String()

fmt.Printf("客户端已连接,连接地址: %s\n", clientAddr)

scanner := bufio.NewScanner(conn)

for scanner.Scan() {

msg := scanner.Text()

fmt.Printf("接收到客户端消息 %s: %s\n", clientAddr, msg)

broadcast(msg, conn)

}

if err := scanner.Err(); err != nil {

fmt.Printf("读取错误 %s: %s\n", clientAddr, err)

} else {

fmt.Printf("客户端已断开连接: %s\n", clientAddr)

}

}

func broadcast(msg string, origin net.Conn) {

mu.Lock()

defer mu.Unlock()

for conn := range clients {

if conn != origin { // 不将消息发回给原始发送者

fmt.Fprintf(conn, "%s\n", msg)

}

}

}

服务端实现和原理

启动服务器并监听端口:

使用net.Listen("tcp", ":8080")监听TCP协议的8080端口。这个函数会返回一个net.Listener对象,用于等待客户端的连接请求。

defer listener.Close()确保在函数返回前关闭监听器。告诉Go运行时(runtime),无论包含这条语句的函数(假设是一个用于启动服务器并监听端口的函数)如何结束(正常结束或是因为错误而提前返回),都要执行listener.Close()。这条语句的作用是关闭网络监听器listener,它会停止监听新的网络连接,释放与这个监听器相关联的资源。这个机制非常重要,因为它提供了一种简单而可靠的方法来确保资源不会因为异常情况而遗漏清理,避免了资源泄露问题。

等待并接受连接:

在一个无限循环中,使用listener.Accept()等待和接受新的客户端连接。这个函数会阻塞直到一个新的连接建立,然后返回一个net.Conn对象,用于后续的数据读写。

管理客户端连接:

使用全局的clients映射来跟踪所有活跃的客户端连接。每当新的连接被接受,它就被添加到这个映射中。

使用互斥锁mu来确保对clients映射的访问是线程安全的,因为可能有多个goroutine同时访问它。addClient、removeClient 和 getClient 函数都在对clients映射进行操作前调用mu.Lock()来获取互斥锁,并且都使用defer mu.Unlock()来确保在函数返回前释放锁。这样,即使有多个goroutine同时调用这些函数,互斥锁也会确保每次只有一个goroutine能够操作映射,从而保证了线程安全性。

处理客户端消息:

对于每个接受的连接,启动一个新的goroutine来处理来自该客户端的消息,使用go handleClient(conn)。

handleClient函数中,首先是清理代码,确保在客户端断开连接时从clients映射中移除该连接,并关闭它。

使用bufio.NewScanner(conn)来读取来自客户端的每一行文本。对于每条接收到的消息,它会被打印出来,并通过broadcast函数发送给所有其他客户端。

广播消息:

broadcast函数遍历所有的客户端连接,并向它们发送消息。注意,发送者自己不会收到自己发的消息。

客户端实现和原理

连接到服务器:

使用net.Dial("tcp", "localhost:8080")连接到服务器的TCP地址。这个函数返回一个net.Conn对象,用于后续的数据读写。

接收服务器消息:

启动一个新的goroutine来持续读取来自服务器的消息。这里同样使用bufio.NewScanner(conn)来按行读取文本。

对于读取到的每一行,直接打印到标准输出。

发送消息到服务器:

主goroutine读取标准输入(即键盘输入)的每一行文本,并通过fmt.Fprintf(conn, "%s\n", msg)发送给服务器。

这允许用户在命令行中输入消息,并通过网络发送给服务器。

代码执行流程和网络通信原理

当服务端启动并监听端口后,它就可以接受客户端的连接请求了。每当一个客户端通过net.Dial成功连接到服务器,服务器的listener.Accept()就会返回并创建一个新的goroutine来处理该连接。

在服务器端,每个客户端连接都有自己的处理goroutine,它读取客户端发送的消息,然后通过广播将消息发送给其他所有客户端。

在客户端,一个goroutine专门用于读取并显示来自服务器(实际上是其他客户端)的消息,而主goroutine读取用户在终端的输入,并将这些输入发送到服务器。

这种模型允许服务端和客户端之间进行实时的数据交换,实现了一个基本的聊天室功能。每个连接的读写是并行处理的,使得服务器能够同时服务多个客户端。

聊点别的

转眼间已经四月了,明显感觉到天气变热了,各种花也都开始开放了。个人比较喜欢这种不热也不冷的天气,也期待着夏天的到来。想起来往年的四月,忙忙碌碌并没有在意身边的变化,近日下楼我会特意的看一眼院子里的那两棵樱花树,那一朵朵粉色的花,真的很是养眼。才发现,很久没有欣赏过身边这些不起眼的美了。

人间四月芳菲尽,

山寺桃花始盛开。

长恨春归无觅处,

不知转入此中来。

——唐·白居易《题大林寺桃花》

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OjslddL6k6tRFJi0ROoQn6Gg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券