Golang 学习笔记12-网络编程

TCP

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议

如何保证连接可靠呢?(面试常考题)

  1. 三次握手
  2. 四次挥手

服务端

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	// net.ResolveTCPAddr 把字符串地址解析成一个 *TCPAddr 结构体对象
	// "tcp" 是网络类型,"127.0.0.1:8081" 是本机 8081 端口
	// 第二个返回值是错误,这里用 _ 忽略掉了(新手练习这样写可以,生产代码最好检查)
	addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8081")

	// 在上面解析出的地址上创建一个 TCP 监听器,开始监听 8081 端口
	// 类比:前端起 WebSocket 服务前,服务端得先 listen 一个端口
	listener, err := net.ListenTCP("tcp", addr)
	if err != nil {
		fmt.Println(err) // 监听失败就打印错误(比如端口已被占用)
		return
	}

	// 无限循环:服务端要持续接收新的客户端连接,每来一个就处理一个
	for {
		// listener.Accept() 会阻塞(卡住等待),直到有客户端连进来
		// 返回的 conn 代表和这一个客户端之间的连接通道
		conn, err := listener.Accept()
		if err != nil {
			break // 接收连接出错就跳出循环(通常意味着监听器被关了)
		}

		// conn.RemoteAddr() 返回对方的地址(客户端的 IP+端口),打印出来看看是谁连进来了
		fmt.Println(conn.RemoteAddr())

		// 给这个客户端发一条消息 "hello"
		// []byte("hello") 把字符串转成字节切片,因为 TCP 传输的是字节流
		conn.Write([]byte("hello"))

		// 睡眠 2 秒(类比前端的 await new Promise(r => setTimeout(r, 2000)))
		time.Sleep(2 * time.Second)

		// 主动关闭这个连接(这里属于演示:发完消息等 2 秒就断开)
		conn.Close()
	}
}

客户端

package main

import (
    "fmt"
    "io" // 提供标准错误值,比如 io.EOF(表示读到末尾/连接关闭)
    "net"
)

func main() {
    // net.Dial 主动去连接服务端,类似前端里 new WebSocket("ws://127.0.0.1:8081")
    // "tcp" 是协议,"127.0.0.1:8081" 是服务端地址
    // 返回的 conn 就是和服务端之间的这条连接通道
    conn, err := net.Dial("tcp", "127.0.0.1:8081")
    if err != nil {
       fmt.Println(err) // 连接失败就打印错误(比如服务端没开)
       return
    }

    // 无限循环:持续从连接里读服务端发来的数据
    for {
       // 准备一个 1024 字节的缓冲区,用来临时存放读到的数据
       // 类比前端的 new Uint8Array(1024)
       var byteData = make([]byte, 1024)

       // conn.Read 把数据读进 byteData,返回值 n 是实际读到的字节数
       // 这一步会阻塞,直到服务端发来数据或连接关闭
       n, err := conn.Read(byteData)

       // 如果错误是 io.EOF,表示连接已经被对方(服务端)关闭,读到末尾了,就跳出循环
       if err == io.EOF {
          break
       }

       // byteData 里可能有没用到的空间,用 byteData[:n] 只取前 n 个实际读到的字节
       // 再用 string(...) 转成字符串打印出来
       fmt.Println(string(byteData[:n]))
    }
}

如果是客户端发送消息

for {
	var text string
	fmt.Printf("请输入你需要发送的消息:")
	fmt.Scan(&text)
	if text == "q" {
		conn.Close()
		break
	}
	conn.Write([]byte(text))
}

服务端

buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		if err != nil { // 连接关闭或出错就退出
			break
		}
		fmt.Println("收到:", string(buf[:n]))

如果服务端需要收发,则需要在开一个协程处理

//因为下面已经是一个死循环了,需要开一个协程来接数据
go func() {
	for {
		// 准备一个 1024 字节的缓冲区,用来临时存放读到的数据
		// 类比前端的 new Uint8Array(1024)
		var byteData = make([]byte, 1024)

		// conn.Read 把数据读进 byteData,返回值 n 是实际读到的字节数
		// 这一步会阻塞,直到服务端发来数据或连接关闭
		n, err := conn.Read(byteData)

		// 如果错误是 io.EOF,表示连接已经被对方(服务端)关闭,读到末尾了,就跳出循环
		if err == io.EOF {
			break
		}

		// byteData 里可能有没用到的空间,用 byteData[:n] 只取前 n 个实际读到的字节
		// 再用 string(...) 转成字符串打印出来
		fmt.Println(string(byteData[:n]))
	}
}()
//	发数据
for {
	var text string
	fmt.Printf("请输入你需要发送的消息:")
	fmt.Scan(&text)
	if text == "q" {
		conn.Close()
		break
	}
	conn.Write([]byte(text))
}

HTTP

服务端

package main

import (
	"fmt"
	"net/http"
)

// Index 是一个「路由处理函数」,类比 Vue Router 里某个路径对应的处理逻辑
// 例如前端写 router.get("/", handler) —— Go 这里就是把一个函数挂到某个 URL 路径上
//
// 两个固定参数(顺序和类型都不能变,这是 Go HTTP 框架约定好的签名):
//   - writer  http.ResponseWriter :「响应写入器」,类比你要 return 给浏览器的内容,
//                                    用 writer.Write(...) 把数据写回去,就像往响应体里塞东西
//   - request *http.Request        :「请求对象」,类比浏览器 fetch 时构造的 Request,
//                                    里面装着这次请求的 URL、方法、请求头、body 等
//
// ⚠️ 注意:Go 的函数参数「必须」有名字。
// 之前写成 `*http.Request` 没给名字,导致下面用到 request 时编译器找不到它,报一堆错。
func Index(writer http.ResponseWriter, request *http.Request) {
	// request.URL.Path 就是这次请求的路径,比如访问 http://127.0.0.1:8080/abc,这里就是 "/abc"
	fmt.Println(request.URL.Path)

	// writer.Write 把字节切片写回给浏览器,类比前端的 res.send("hello")
	// 为什么是 []byte?因为 HTTP 响应体本质就是字节流,跟 TCP 那边一样
	writer.Write([]byte("hello"))
}

func main() {
	// http.HandleFunc 把一个 URL 路径绑定到一个处理函数
	// 类比 Vue Router 的路由表注册:"/" 这个路径来了请求,就交给 Index 函数处理
	// 第一个参数 "/" 表示匹配所有路径(Go 的默认 HTTP 路由里,"/" 是兜底路由)
	http.HandleFunc("/", Index)

	fmt.Println("web server listen addr: http://127.0.0.1:8080")

	// http.ListenAndServe 在指定地址上启动 HTTP 服务,开始监听
	// 类比前端项目跑 npm run dev 起一个开发服务器监听某个端口
	// "127.0.0.1:8080" = 本机 8080 端口;第二个参数 nil 表示用默认的路由(就是上面 HandleFunc 注册的那些)
	//
	// ⚠️ 端口尽量用 8080 这类大于 1024 的端口。
	//    80、443 这种小于 1024 的叫「特权端口」,普通用户绑定会失败(需要 sudo),新手练习避开。
	//
	// 它会一直阻塞在这里服务请求,只有在出错时才返回 error,所以这里检查一下错误
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	if err != nil {
		fmt.Println("服务器启动失败:", err)
		return
	}
}

客户端

package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	res, err := http.Get("http://127.0.0.1:8080")
	if err != nil {
		fmt.Println(err)
		return
	}
	bytedata, _ := io.ReadAll(res.Body)
	fmt.Println(string(bytedata))
}

websocket

websocket是socket连接和http协议的结合体,可以实现网页和服务端的长连接

go get github.com/gorilla/websocket

服务端

package main

import (
  "fmt"
  "github.com/gorilla/websocket"
  "net/http"
)

var UP = websocket.Upgrader{
  ReadBufferSize:  1024,
  WriteBufferSize: 1024,
}

var connLis []*websocket.Conn

func handler(res http.ResponseWriter, req *http.Request) {
  // 服务升级
  conn, err := UP.Upgrade(res, req, nil)
  if err != nil {
    fmt.Println(err)
    return
  }
  connLis = append(connLis, conn)
  for {
    // 消息类型,消息,错误
    t, p, err := conn.ReadMessage()
    if err != nil {
      break
    }
    for index := range connLis {
      connLis[index].WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗?", string(p))))
    }
    fmt.Println(t, string(p))
  }
  defer conn.Close()
  fmt.Println("服务关闭")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

服务端给多个客户端发

package main

import (
  "fmt"
  "github.com/gorilla/websocket"
  "net/http"
)

var UP = websocket.Upgrader{
  ReadBufferSize:  1024,
  WriteBufferSize: 1024,
}

var connLis []*websocket.Conn

func handler(res http.ResponseWriter, req *http.Request) {
  // 服务升级
  conn, err := UP.Upgrade(res, req, nil)
  if err != nil {
    fmt.Println(err)
    return
  }
  connLis = append(connLis, conn)
  for {
    // 消息类型,消息,错误
    t, p, err := conn.ReadMessage()
    if err != nil {
      break
    }
    for index := range connLis {
      connLis[index].WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗?", string(p))))
    }
    fmt.Println(t, string(p))
  }
  defer conn.Close()
  fmt.Println("服务关闭")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

客户端

package main

import (
  "bufio"
  "fmt"
  "github.com/gorilla/websocket"
  "os"
)

func main() {
  dl := websocket.Dialer{}
  conn, _, err := dl.Dial("ws://127.0.0.1:8080", nil)
  if err != nil {
    fmt.Println(err)
    return
  }
  go send(conn)
  for {
    t, p, err := conn.ReadMessage()
    if err != nil {
      break
    }
    fmt.Println(t, string(p))
  }
}

func send(conn *websocket.Conn) {
  for {
    reader := bufio.NewReader(os.Stdin)
    l, _, _ := reader.ReadLine()
    conn.WriteMessage(websocket.TextMessage, l)
  }
}