有两种看待人生的方式,
一种是生活不存在奇迹,
另一种则是,
所有的一切都是奇迹。
——阿尔伯特·爱因斯坦
今天推荐一个很好的程序员备忘清单网站
https://cheatsheets.zip/
,基本涵盖了所有开发必须的基础命令以及操作,中文版:https://cheatsheets.zip/zh-CN/
是为开发人员分享快速参考备忘清单【速查表】。这是英文版 Reference 的中文版本,目的是为了方便自己的技术栈查阅。
之前在学习网络协议TCP的过程中,使用python实现了基于TCP协议的即时通信聊天应用,今天使用go语言实现,并再次复习一下客户端服务端交互的全流程。
虽然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能够操作映射,从而保证了线程安全性。go handleClient(conn)
。handleClient
函数中,首先是清理代码,确保在客户端断开连接时从clients
映射中移除该连接,并关闭它。bufio.NewScanner(conn)
来读取来自客户端的每一行文本。对于每条接收到的消息,它会被打印出来,并通过broadcast
函数发送给所有其他客户端。broadcast
函数遍历所有的客户端连接,并向它们发送消息。注意,发送者自己不会收到自己发的消息。net.Dial("tcp", "localhost:8080")
连接到服务器的TCP地址。这个函数返回一个net.Conn
对象,用于后续的数据读写。bufio.NewScanner(conn)
来按行读取文本。fmt.Fprintf(conn, "%s\n", msg)
发送给服务器。net.Dial
成功连接到服务器,服务器的listener.Accept()
就会返回并创建一个新的goroutine来处理该连接。