本节是新知识,偏应用,需要反复练习才能掌握。
目录
- 1.C/S通信示意图
- 2.服务端通信
- 3.客户端通信
- 4.通信测试
- 5.进阶练习:客户端之间通信
1.C/S通信示意图
客户端与服务端通信的模式也称作C/S模式,流程图如下
其中P是协程调度器。可以看到,客户端都是通过一个共同的端口和服务端通信的,且服务端开了两个协程并发处理两个客户端。注意,协程和P之间,协程和端口之间,端口和客户端之间的链接都是双向的。
2.服务端通信
服务端功能要求如下:
(1)编写一个服务器端程序,在8888端口监听
fmt.Println("服务器开始监听....")
//net.Listen("tcp", "0.0.0.0:8888")
//1. tcp 表示使用网络协议是tcp
//2. 0.0.0.0:8888 表示在本地监听 8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
(2)可以和多个客户端创建链接
//循环等待客户端来链接我
for {
//等待客户端链接
fmt.Println("等待客户端来链接....")
conn, err := listen.Accept()
//这里准备其一个协程,为客户端服务
go process(conn)
}
(3)链接成功后,客户端可以发送数据,服务器端接受数据,并显示在终端上
完整代码如下:
package main
import (
"fmt"
"net" //做网络socket开发时,net包含有我们需要所有的方法和函数
_"io"
)
func process(conn net.Conn) {
//这里我们循环的接收客户端发送的数据
defer conn.Close() //关闭conn
for {
//创建一个新的切片
buf := make([]byte, 1024)
//conn.Read(buf)
//1. 等待客户端通过conn发送信息
//2. 如果客户端没有wrtie[发送],那么协程就阻塞在这里
//fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n , err := conn.Read(buf) //从conn读取
if err != nil {
fmt.Printf("客户端退出 err=%v", err)
return //!!!
}
//3. 显示客户端发送的内容到服务器的终端
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听....")
//net.Listen("tcp", "0.0.0.0:8888")
//1. tcp 表示使用网络协议是tcp
//2. 0.0.0.0:8888 表示在本地监听 8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close() //延时关闭listen
//循环等待客户端来链接我
for {
//等待客户端链接
fmt.Println("等待客户端来链接....")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备其一个协程,为客户端服务
go process(conn)
}
//fmt.Printf("listen suc=%v\n", listen)
}
流程总结:
1.使用net.Listen()
初始化监听端口,把初始信息存在变量listen中
2.循环使用listen.Accept()
,接收监听到的基本信息,存在变量conn中
3.每次循环用conn.Read(buf)
读取客户端发送的内容,将这些操作封装到一个协程中,并发执行
3.客户端通信
客户端功能:
(1)编写一个客户端程序,能链接到服务器端的8888端口
conn, err := net.Dial("tcp", "localhost:8888")
(2)客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
(3)能通过终端输入数据(输入一行发送一行),并发送给服务器端
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
其中ReadString(‘\n’)表示以换行符为截止符号,读取包括换行符在内的之前的字符,所以之后还需要去除line中的换行符。
(4)在终端输入exit,表示退出程序
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客户端退出..")
break
}
完整代码如下:
package main
import (
"fmt"
"net"
"bufio"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "localhost:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]
for {
//从终端读取一行用户输入,并准备发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//如果用户输入的是 exit就退出
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客户端退出..")
break
}
//再将line 发送给 服务器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn.Write err=", err)
}
}
}
注:localhost是本地ip地址,默认ipv6。
流程总结:
1.使用net.Dial()
建立与服务端的链接,把初始信息存在变量conn中
2.循环使用bufio.NewReader(os.Stdin)
创建缓冲区,读入用户输入的信息并存在reader中
3.使用reader.ReadString('\n')
提取每行字符
4.判断输入是否为exit,若是则退出循环
4.若输入步是exit,则用conn.Write()
将每行内容发送到服务端
4.通信测试
先启动服务端,内容如下:
服务器开始监听....
等待客户端来链接....
注意,如果出现类似下图的提示框请点击允许!
再打开当前目录的命令行,启动客户端,命令行会显示如下语句
Accept() suc con=&{{0xc00008a508}} 客户端ip=[::1]:端口号
注意:这个端口号不是8888,而是客户端用来连接服务器时的本地端口。每个客户端在连接到服务器时,操作系统会为它分配一个临时的随机端口。
客户端输入几行语句
D:\code\golang\尚硅谷golang\代码\chapter18\tcpdemo\client>go run "client(1).go"
123
你好
服务端内容
服务器开始监听....
等待客户端来链接....
Accept() suc con=&{{0xc00008a508}} 客户端ip=[::1]:端口号
等待客户端来链接....
123
你好
客户端输入exit即可结束
客户端退出..
D:\code\golang\尚硅谷golang\代码\chapter18\tcpdemo\client>
服务端内容
客户端退出 err=read tcp [::1]:8888->[::1]:端口号: wsarecv: An existing connection was forcibly closed by the remote host.
注意,此时服务端仍在循环监听,按ctrl+c即可结束程序。
5.进阶练习:客户端之间通信
为了使两个客户端能够相互通信,需要一个中介服务器(通常称为“中转服务器”),它负责转发每个客户端的消息给另一个客户端。服务器在两个客户端之间保持连接,接收其中一个客户端的消息,然后将其转发给另一个客户端。
实现步骤:
服务器端:
1.服务器负责接收客户端 A 和客户端 B 的连接。
2.服务器读取每个客户端发送的消息,然后将消息转发给另一个客户端。
package main
import (
"fmt"
"net"
"sync"
)
var (
clients = make(map[net.Conn]bool) // 存储连接的客户端
mu sync.Mutex // 保护 clients 的并发访问
)
func main() {
listener, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("服务器启动错误:", err)
return
}
defer listener.Close()
fmt.Println("服务器正在监听8888端口...")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("接受连接时发生错误:", err)
continue
}
// 在服务端提示有新的客户端连接
fmt.Printf("新的客户端已连接: %v\n", conn.RemoteAddr())
// 将客户端添加到客户端列表
mu.Lock()
clients[conn] = true
mu.Unlock()
go handleConnection(conn) // 处理每个连接
}
}
func handleConnection(conn net.Conn) {
defer func() {
conn.Close()
mu.Lock()
delete(clients, conn) // 从列表中删除客户端
mu.Unlock()
}()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf) // 读取客户端消息
if err != nil {
fmt.Printf("客户端 %v 断开连接: %v\n", conn.RemoteAddr(), err)
return
}
message := string(buf[:n])
// 判断是否是退出消息
if message == "exit\n" || message == "exit" {
fmt.Printf("客户端 %v 退出。\n", conn.RemoteAddr())
return
}
// 转发消息给其他客户端
mu.Lock()
for client := range clients {
if client != conn { // 不给发送者发送消息
_, _ = client.Write(buf[:n]) // 将消息发送到其他客户端
}
}
mu.Unlock()
}
}
客户端:
1.客户端连接到服务器,通过服务器发送和接收消息。
2.客户端之间不会直接通信,而是通过服务器中转消息。
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
// 使用 localhost 连接到服务器
conn, err := net.Dial("tcp", "localhost:8888")
if err != nil {
fmt.Println("连接服务器失败:", err)
return
}
defer conn.Close() // 确保在退出时关闭连接
// 启动一个 goroutine 负责接收服务器的消息
go func() {
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf) // 从服务器读取数据
if err != nil {
fmt.Println("与服务器断开连接:", err)
return
}
// 显示来自服务器的消息
fmt.Print("收到消息: ", string(buf[:n]))
}
}()
// 主协程负责发送用户输入的消息
reader := bufio.NewReader(os.Stdin)
fmt.Println("连接到服务器,输入消息发送,输入 'exit' 退出...")
for {
// 从终端读取一行用户输入
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取输入时发生错误:", err)
continue
}
// 去掉输入行的换行符和空格go ru
line = strings.TrimSpace(line)
// 如果用户输入的是 exit,退出程序
if line == "exit" {
fmt.Println("客户端退出...")
break
}
// 将用户输入发送给服务器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("发送消息时发生错误:", err)
}
}
}
效果截图