单机轻松支持百万并发的go协程的简单tcpsocket服务端客户端通信小程序示例源码
服务端 server.go
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"strings"
"time"
)
// 简单的客户端服务端通信示例
// 知识点总结:
// 1.listener.Accept() 这个会一直等待客户端链接, 有一个链接就往后面执行代码没有链接就阻塞;
// 2. go process(conn) 会对每个链接开启一个协程进行处理
// 3. 在协程处理函数里面必须要在开头加上 defer conn.Close() 确保协程结束后关闭相应的链接资源
// 4. 从终端获取用户输入数据使用了os.Stdin作为输入源 reader := bufio.NewReader(os.Stdin)
// 5. 输入源在每次读取完毕后可以重置 reader.Reset(os.Stdin) // 每次数据发送后重置reader
// 在从conn.Read(buf)读取数据显示时需要带上这里读取到的n 如:string(buf[:n])
func main() {
listener, err := net.Listen("tcp", ":8008")
if err != nil {
fmt.Println("链接异常:", err)
}
defer listener.Close() //关闭链接
// 循环,等待客户端来链接
for {
conn, err := listener.Accept() // 等待链接
if err != nil {
fmt.Println("Listener.Accept:", err)
continue // 跳转到下一个链接
}
go process(conn)
}
}
// 处理链接函数
func process(conn net.Conn) {
defer conn.Close() //这个处理进程退出后确保关闭链接conn
fmt.Printf("收到来自%v的链接\n", conn.RemoteAddr())
//向客户端发送一条消息
conn.Write([]byte("Welcome to Go tcp server!"))
chExit := make(chan int, 2) // 创建一个退出管道
go sendMsFromStdin(conn, chExit)
go readMsgFromClient(conn, chExit)
//阻塞等待用户输入
for {
n, ok := <-chExit
if n == 1 {
fmt.Printf("客户端 %v 链接已关闭\n", conn.RemoteAddr())
return
} else {
fmt.Printf("main管道 n=%v ok=%v \n", n, ok)
break
}
}
}
func readMsgFromClient(conn net.Conn, ch chan int) {
for {
// 每次循环都新创建一个切片用于读取客户端发送的数据
buf := make([]byte, 1024)
n, err := conn.Read(buf) // 这个Read会使协程一直阻塞在这里,直到客户端发送消息
if err == nil && n > 0 {
// string(buf[:n]) 这里的n很重要,代表仅显示从conn中读取到的信息
fmt.Printf("收到数据:%v", string(buf[:n]))
} else if err == io.EOF {
//fmt.Printf("错误退出:n=%v, %v", n, err)
ch <- 1
break //退出
} else if _, ok := <-ch; !ok {
//fmt.Print("client closed!!")
return //管道已经关闭,直接退出
} else {
fmt.Printf("等待客户端%v发送消息", conn.RemoteAddr())
}
}
close(ch) //关闭ch
}
// 从终端获取数据并发送到客户端
func sendMsFromStdin(conn net.Conn, ch chan int) {
// 这里使用标准输入作为rd, os.Stdin
reader := bufio.NewReader(os.Stdin)
maxErrCnt := 3 // 最多重试次数,超过这个次数这个链接就会直接关闭
//循环处理
for {
// 从终端读取用户的输入并发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("获取数据失败, %v", err)
}
if strings.HasPrefix(line, "quit") {
fmt.Printf("服务端强制关闭了客户端 %v\n", conn.RemoteAddr())
ch <- 1 //写入1 退出循环
break
}
// 读取数据正常,开始发送数据
len, err := conn.Write([]byte(line))
if err != nil && maxErrCnt > 0 {
maxErrCnt--
fmt.Printf("发送数据错误 %v 等待3秒后重试,还剩 %d次重试机会\n", err, maxErrCnt)
time.Sleep(3 * time.Second)
} else if err == nil {
fmt.Printf("本次发送了 %v 个字节的数据\n", len)
reader.Reset(os.Stdin) // 每次数据发送后重置reader
} else if maxErrCnt < 0 {
ch <- 1 //写入1 退出循环
break
} else if _, ok := <-ch; !ok { // 因为这个管道有2个地方可以关闭, 所以加这个判断确保管道关闭后可以正确及时退出本函数
return //管道已经关闭,直接退出
}
}
close(ch) //关闭ch
}
客户端 client.go
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"strings"
"time"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8008")
if err != nil {
fmt.Println("链接失败: ", err)
return
}
defer conn.Close()
chExit := make(chan int, 1)
// 从终端获取数据并发送到服务端
go sendMsFromStdin(conn, chExit)
go readMsgFromServer(conn)
//阻塞等待用户输入
for {
n, ok := <-chExit
if n == 1 {
fmt.Println("用户退出客户端!")
break
} else {
fmt.Printf("main管道 n=%v ok=%v \n", n, ok)
}
}
fmt.Println("client exit")
}
func readMsgFromServer(conn net.Conn) {
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err == io.EOF {
break
} else if err == nil && len(buf[:n]) > 0 {
fmt.Printf("接收到服务端的信息: %v\n", string(buf[:n]))
}
}
}
// 从终端获取数据并发送到服务端
func sendMsFromStdin(conn net.Conn, ch chan<- int) {
// 这里使用标准输入作为rd, os.Stdin
reader := bufio.NewReader(os.Stdin)
maxErrCnt := 3
//循环处理
for {
// 从终端读取用户的输入并发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("获取数据失败, %v", err)
}
if strings.HasPrefix(line, "quit") {
ch <- 1 //写入1 退出循环
break
}
// 读取数据正常,开始发送数据
len, err := conn.Write([]byte(line))
if err != nil && maxErrCnt > 0 {
maxErrCnt--
fmt.Printf("发送数据错误 %v 等待3秒后重试,还剩 %d次重试机会\n", err, maxErrCnt)
time.Sleep(3 * time.Second)
} else if err == nil {
fmt.Printf("本次发送了 %v 个字节的数据\n", len)
reader.Reset(os.Stdin) // 每次数据发送后重置reader
} else if maxErrCnt <= 0 {
ch <- 1 //写入1 退出循环
break
} else {
fmt.Printf("else len:%v err:%v \n", len, err)
}
}
fmt.Println("即将关闭ch")
close(ch) //关闭ch
}