go网络编程
我自己写的思维导图
go网络编程主要是使用net包下面的东西
我参考下面的教程
- https://github.com/daliuchen/build-web-application-with-golang/blob/master/zh/08.1.md
- https://studygolang.com/articles/20368
1. 服务端定时发送消息给客户端(时间同步)
1.1 时序图
1.2 代码
- 服务端
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
//监听端口
port := ":7777"
//构建一个基于tcp4的 端口为7777的监听地址对象
tcpAddr, err := net.ResolveTCPAddr("tcp4", port)
checkErr(err)
//采用tcp连接
listener, err := net.ListenTCP("tcp", tcpAddr)
//for循环一个监听客户端的连接
for {
//接受客户端的连接,如果没有连接,就堵塞
con, err := listener.Accept()
checkErr(err)
//输出客户端的地址
log.Println("客户端连接成功:", con.RemoteAddr().String())
handleConnection(con)
}
}
//输出消息给客户端
func handleConnection(conn net.Conn) {
for {
//格式化当前时间,很抱歉,好像go格式化时间 不支持 YYYYMMdd 这样的写法
format := time.Now().Format("2006/01/02 15:04:05")
//输出到客户端
fmt.Fprintln(conn, format)
//线程睡眠一秒
time.Sleep(time.Second * 1)
}
}
func checkErr(err error) {
if err != nil {
log.Fatalln(err)
}
}
- 客户端
func startClient() {
log.Print("start client ....")
//连接端口
port := ":7777"
//基于tcp4 端口为7777的地址对象
tcpAddr, err := net.ResolveTCPAddr("tcp4", port)
checkErr(err)
// 基于tcp的连接,
// 第一个参数 是"tcp4"、"tcp6"、"tcp"中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
// 第二个参数 表示本机地址,一般设置为nil
// 第三个参数 表示远程的服务地址
conn, err := net.DialTCP("tcp", nil, tcpAddr)
//go 提供的流拷贝,将conn的值拷贝到stdout里面,标准输出,也就是控制台
io.Copy(os.Stdout,conn)
}
还有一个客户端 可以用 telnet localhost 7777 来充当客户端来使用。
但是这样有一个很大的问题,连接不是并发的,不支持多个连接,。主线程一直是被第一个来的连接占用,只支持单个连接
1.3 时间同步支持多线程
只需要在这里增加一个go关键字就能支持多个客户端。这样就变成了下面的结构
即就是每一个客户端都有一个对应的线程来处理。
2. 客户端发送消息给服务端,服务端应答
2.1 时序图
2.2 代码
服务端基本还是之前那样,就多了一个字符串处理。
客户端多了一个从控制台输入
- 服务端
//开启服务端
func startServer() {
log.Print("start server ....")
port := ":7777"
tcpAddr, err := net.ResolveTCPAddr("tcp4", port)
checkErr(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
for {
con, err := listener.Accept()
checkErr(err)
log.Println("客户端连接成功:", con.RemoteAddr().String())
go handleConnection(con)
}
}
func handleConnection(conn net.Conn) {
//将 con 的数据读取到 buffer的数据,
// 没有数据就一直堵塞。
input := bufio.NewScanner(conn)
// 不断读取 buffer的数据
for input.Scan() {
//将客户端的输入大写返回
fmt.Fprintln(conn, "\t", strings.ToUpper(input.Text()))
//睡眠1秒
time.Sleep(time.Second * 1)
// 原封不动的写出
fmt.Fprintln(conn, "\t", input.Text())
time.Sleep(time.Second * 1)
//将客户端的输入小写返回
fmt.Fprintln(conn, "\t", strings.ToLower(input.Text()))
}
}
func checkErr(err error) {
if err != nil {
log.Fatalln(err)
}
}
func main() {
//服务端开启
go startServer()
time.Sleep(time.Hour * 1)
}
- 客户端
//开启客户端
func startClient() {
log.Print("start client ....")
port := ":7777"
tcpAddr, err := net.ResolveTCPAddr("tcp4", port)
checkErr(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
//开启一个线程,输入读取到con,此线程要一直获取输入的值
go io.Copy(conn,os.Stdin)
//接受con的输出
io.Copy(os.Stdout,conn)
}
func main() {
go startClient()
time.Sleep(time.Hour * 1)
}
3. 聊天室
主要分为客户端和服务端
- 服务端
定义不同的chan,用于监听不同的事件。- leaving。客户端离开
- entering。客户端连接
- messages 存放消息
- 主线程启动的时候,开启一个广播线程,该线程里面维护了一个所有连接的map。通过select 不停的监听不同的通道里面的消息。
- 主线程监听连接,连接建立,开启一个处理该connection 的线程。该线程里面 ,向客户端发送消息,并且接受客户端的输入。并且往不同的chan里面发送消息。
- 客户端
3.1 时序图
3.2 代码
func startServer() {
log.Print("start server ....")
port := ":7777"
tcpAddr, err := net.ResolveTCPAddr("tcp4", port)
checkErr(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
//开启广播。
go broadcasting()
for {
clientConnection, err := listener.Accept()
checkErr(err)
log.Println("客户端连接成功:", clientConnection.RemoteAddr().String())
go handleConnection(clientConnection)
}
}
func handleConnection(conn net.Conn) {
ch := make(chan string) // 对外发送客户消息的通道
go clientWriter(conn, ch)
who := conn.RemoteAddr().String()
ch <- "You are " + who // 这条单发给自己
messages <- who + " has arrived" // 这条进行进行广播,但是自己还没加到广播列表中
cli := clientInfo{who, ch}
entering <- cli // 然后把自己加到广播列表中
done := make(chan struct{}, 2) // 等待下面两个 goroutine 其中一个执行完成。使用缓冲通道防止 goroutine 泄漏
// 计算超时的goroutine
inputSignal := make(chan struct{}) // 有任何输入,就发送一个信号
timeout := 15 * time.Second // 客户端空闲的超时时间
go func() {
timer := time.NewTimer(timeout)
for {
select {
case <-inputSignal:
timer.Reset(timeout)
//定时器,时间到了之后给就会有消息
case <-timer.C:
// 超时,断开连接
done <- struct{}{}
return
}
}
}()
go func() {
input := bufio.NewScanner(conn)
for input.Scan() {
inputSignal <- struct{}{}
if len(strings.TrimSpace(input.Text())) == 0 { // 禁止发送纯空白字符
continue
}
messages <- who + ": " + input.Text()
}
// 注意,忽略input.Err()中可能的错误
done <- struct{}{}
}()
// 注意,忽略input.Err()中可能的错误
<-done
leaving <- cli
messages <- who + " has left"
conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
// 在消息结尾使用 \r\n ,提升平台兼容
fmt.Fprintf(conn, "%s\r\n", msg) // 注意,忽略网络层面的错误
}
}
func broadcasting() {
//维护一个客户端的集合
mapClients := make(map[clientInfo]bool)
//通过select给所有的通道发消息
for {
select {
case msg := <-messages:
// 把所有接收的消息广播给所有的客户
// 发送消息通道
for cli := range mapClients {
cli.ch <- msg
}
case cli := <-entering:
// 在每一个新用户到来的时候,通知当前存在的用户
var users []string
for cli := range mapClients {
users = append(users, cli.name)
}
if len(users) > 0 {
cli.ch <- fmt.Sprintf("Other users in room: %s", strings.Join(users, "; "))
} else {
cli.ch <- "You are the only user in this room."
}
mapClients[cli] = true
case cli := <-leaving:
delete(mapClients, cli)
close(cli.ch)
}
}
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}